From ad997940b136d315787fcb11c03fc70a40c7e8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 23 Aug 2024 11:43:46 +0200 Subject: [PATCH] feat: Support attribute syntax in `__all__` values Issue-316: https://github.com/mkdocstrings/griffe/issues/316 --- docs/reference/api/agents.md | 8 +++-- src/_griffe/agents/nodes/exports.py | 48 +++++++++++++++++------------ src/griffe/__init__.py | 3 ++ tests/test_visitor.py | 25 +++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 0b000de9..22442669 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -58,8 +58,6 @@ Griffe is able to analyze code both statically and dynamically. ::: griffe.get_instance_names -::: griffe.ExportedName - ::: griffe.get__all__ ::: griffe.safe_get__all__ @@ -71,3 +69,9 @@ Griffe is able to analyze code both statically and dynamically. ::: griffe.get_value ::: griffe.safe_get_value + + +## **Deprecated API** + + +::: griffe.ExportedName diff --git a/src/_griffe/agents/nodes/exports.py b/src/_griffe/agents/nodes/exports.py index 4961cbc2..c96deabd 100644 --- a/src/_griffe/agents/nodes/exports.py +++ b/src/_griffe/agents/nodes/exports.py @@ -9,15 +9,20 @@ from _griffe.agents.nodes.values import get_value from _griffe.enumerations import LogLevel +from _griffe.expressions import ExprName from _griffe.logger import logger if TYPE_CHECKING: from _griffe.models import Module +# YORE: Bump 2: Remove block. @dataclass class ExportedName: - """An intermediate class to store names.""" + """Deprecated. An intermediate class to store names. + + The [`get__all__`][griffe.get__all__] function now returns instances of [`ExprName`][griffe.ExprName] instead. + """ name: str """The exported name.""" @@ -25,47 +30,52 @@ class ExportedName: """The parent module.""" -def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExportedName]: - return [node.value] +def _extract_attribute(node: ast.Attribute, parent: Module) -> list[str | ExprName]: + return [ExprName(name=node.attr, parent=_extract(node.value, parent)[0])] + + +def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExprName]: + left = _extract(node.left, parent) + right = _extract(node.right, parent) + return left + right -def _extract_name(node: ast.Name, parent: Module) -> list[str | ExportedName]: - return [ExportedName(node.id, parent)] +def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExprName]: + return [node.value] -def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExportedName]: - return _extract(node.value, parent) +def _extract_name(node: ast.Name, parent: Module) -> list[str | ExprName]: + return [ExprName(node.id, parent)] -def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExportedName]: +def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExprName]: sequence = [] for elt in node.elts: sequence.extend(_extract(elt, parent)) return sequence -def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExportedName]: - left = _extract(node.left, parent) - right = _extract(node.right, parent) - return left + right +def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExprName]: + return _extract(node.value, parent) -_node_map: dict[type, Callable[[Any, Module], list[str | ExportedName]]] = { +_node_map: dict[type, Callable[[Any, Module], list[str | ExprName]]] = { + ast.Attribute: _extract_attribute, + ast.BinOp: _extract_binop, ast.Constant: _extract_constant, - ast.Name: _extract_name, - ast.Starred: _extract_starred, ast.List: _extract_sequence, + ast.Name: _extract_name, ast.Set: _extract_sequence, + ast.Starred: _extract_starred, ast.Tuple: _extract_sequence, - ast.BinOp: _extract_binop, } -def _extract(node: ast.AST, parent: Module) -> list[str | ExportedName]: +def _extract(node: ast.AST, parent: Module) -> list[str | ExprName]: return _node_map[type(node)](node, parent) -def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | ExportedName]: +def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | ExprName]: """Get the values declared in `__all__`. Parameters: @@ -84,7 +94,7 @@ def safe_get__all__( node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module, log_level: LogLevel = LogLevel.debug, # TODO: set to error when we handle more things -) -> list[str | ExportedName]: +) -> list[str | ExprName]: """Safely (no exception) extract values in `__all__`. Parameters: diff --git a/src/griffe/__init__.py b/src/griffe/__init__.py index b7d1f674..68bfa4b2 100644 --- a/src/griffe/__init__.py +++ b/src/griffe/__init__.py @@ -38,6 +38,8 @@ ast_siblings, ) from _griffe.agents.nodes.docstrings import get_docstring + +# YORE: Bump 2: Replace `ExportedName, ` with `` within line. from _griffe.agents.nodes.exports import ExportedName, get__all__, safe_get__all__ from _griffe.agents.nodes.imports import relative_to_absolute from _griffe.agents.nodes.parameters import ParametersType, get_parameters @@ -277,6 +279,7 @@ "DocstringWarn", "DocstringYield", "ExplanationStyle", + # YORE: Bump 2: Remove line. "ExportedName", "Expr", "ExprAttribute", diff --git a/tests/test_visitor.py b/tests/test_visitor.py index 70bd454b..c2b6a557 100644 --- a/tests/test_visitor.py +++ b/tests/test_visitor.py @@ -360,3 +360,28 @@ def test_visiting_relative_imports_triggering_cyclic_aliases() -> None: assert "a" not in pkg.imports assert "b" in pkg["a"].imports assert pkg["a"].imports["b"] == "pkg.b" + + +def test_parse_attributes_in__all__() -> None: + """Parse attributes in `__all__`.""" + with temporary_visited_package( + "package", + { + "__init__.py": "from package import module\n__all__ = module.__all__", + "module.py": "def hello(): ...\n__all__ = ['hello']", + }, + ) as package: + assert "hello" in package.exports # type: ignore[operator] + + +def test_parse_deep_attributes_in__all__() -> None: + """Parse deep attributes in `__all__`.""" + with temporary_visited_package( + "package", + { + "__init__.py": "from package import subpackage\n__all__ = subpackage.module.__all__", + "subpackage/__init__.py": "from package.subpackage import module", + "subpackage/module.py": "def hello(): ...\n__all__ = ['hello']", + }, + ) as package: + assert "hello" in package.exports # type: ignore[operator]