From 92594a99fed42eb2daa3bbeb797edbf3507f3068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 10 Jul 2024 20:55:35 +0200 Subject: [PATCH] refactor: Clean up and document internal API, mark legacy code --- src/_griffe/__init__.py | 35 ---- src/_griffe/agents/inspector.py | 58 ++++-- src/_griffe/agents/nodes/__init__.py | 81 -------- src/_griffe/agents/nodes/assignments.py | 8 +- src/_griffe/agents/nodes/ast.py | 20 +- src/_griffe/agents/nodes/docstrings.py | 8 +- src/_griffe/agents/nodes/exports.py | 39 ++-- src/_griffe/agents/nodes/imports.py | 10 +- src/_griffe/agents/nodes/parameters.py | 11 +- src/_griffe/agents/nodes/runtime.py | 14 +- src/_griffe/agents/nodes/values.py | 10 +- src/_griffe/agents/visitor.py | 63 ++++-- src/_griffe/c3linear.py | 21 +- src/_griffe/cli.py | 53 +++-- src/_griffe/collections.py | 14 +- src/_griffe/debug.py | 28 ++- src/_griffe/diff.py | 61 +++--- src/_griffe/docstrings/__init__.py | 5 - src/_griffe/docstrings/google.py | 45 ++--- src/_griffe/docstrings/models.py | 38 +--- src/_griffe/docstrings/numpy.py | 35 ++-- src/_griffe/docstrings/parsers.py | 15 +- src/_griffe/docstrings/sphinx.py | 82 ++++---- src/_griffe/docstrings/utils.py | 32 +-- src/_griffe/encoders.py | 25 +-- src/_griffe/enumerations.py | 19 +- src/_griffe/exceptions.py | 19 +- src/_griffe/expressions.py | 251 ++++++++++-------------- src/_griffe/extensions/__init__.py | 20 -- src/_griffe/extensions/base.py | 78 +++++--- src/_griffe/extensions/dataclasses.py | 8 +- src/_griffe/extensions/hybrid.py | 27 +-- src/_griffe/finder.py | 32 +-- src/_griffe/git.py | 27 +-- src/_griffe/importer.py | 3 - src/_griffe/loader.py | 123 ++++++------ src/_griffe/logger.py | 7 +- src/_griffe/merger.py | 16 +- src/_griffe/mixins.py | 80 +++++--- src/_griffe/models.py | 148 ++++++++------ src/_griffe/stats.py | 41 ++-- src/_griffe/tests.py | 41 ++-- 42 files changed, 810 insertions(+), 941 deletions(-) diff --git a/src/_griffe/__init__.py b/src/_griffe/__init__.py index ff3fe315..a799a7a3 100644 --- a/src/_griffe/__init__.py +++ b/src/_griffe/__init__.py @@ -4,38 +4,3 @@ Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. """ - -from __future__ import annotations - -from griffe.agents.nodes import ObjectNode -from griffe.models import Attribute, Class, Docstring, Function, Module, Object -from griffe.diff import find_breaking_changes -from griffe.docstrings.google import parse as parse_google -from griffe.docstrings.numpy import parse as parse_numpy -from griffe.docstrings.sphinx import parse as parse_sphinx -from griffe.enumerations import Parser -from griffe.extensions.base import Extension, load_extensions -from griffe.importer import dynamic_import -from griffe.loader import load, load_git -from griffe.logger import get_logger - -__all__: list[str] = [ - "Attribute", - "Class", - "Docstring", - "dynamic_import", - "Extension", - "Function", - "find_breaking_changes", - "get_logger", - "load", - "load_extensions", - "load_git", - "Module", - "Object", - "ObjectNode", - "Parser", - "parse_google", - "parse_numpy", - "parse_sphinx", -] diff --git a/src/_griffe/agents/inspector.py b/src/_griffe/agents/inspector.py index ed61685d..45d5b836 100644 --- a/src/_griffe/agents/inspector.py +++ b/src/_griffe/agents/inspector.py @@ -10,9 +10,9 @@ we always try to visit the code first, and only then we load the object to update the data with introspection. -This module exposes a public function, [`inspect()`][griffe.agents.inspector.inspect], +This module exposes a public function, [`inspect()`][griffe.inspect], which inspects the module using [`inspect.getmembers()`][inspect.getmembers], -and returns a new [`Module`][griffe.models.Module] instance, +and returns a new [`Module`][griffe.Module] instance, populating its members recursively, by using a [`NodeVisitor`][ast.NodeVisitor]-like class. The inspection agent works similarly to the regular "node visitor" agent, @@ -28,24 +28,25 @@ from inspect import signature as getsignature from typing import TYPE_CHECKING, Any, Sequence -from griffe.agents.nodes import ObjectNode -from griffe.collections import LinesCollection, ModulesCollection -from griffe.models import Alias, Attribute, Class, Docstring, Function, Module, Parameter, Parameters -from griffe.enumerations import ObjectKind, ParameterKind -from griffe.expressions import safe_get_annotation -from griffe.extensions.base import Extensions, load_extensions -from griffe.importer import dynamic_import -from griffe.logger import get_logger +from _griffe.agents.nodes.runtime import ObjectNode +from _griffe.collections import LinesCollection, ModulesCollection +from _griffe.enumerations import ObjectKind, ParameterKind +from _griffe.expressions import safe_get_annotation +from _griffe.extensions.base import Extensions, load_extensions +from _griffe.importer import dynamic_import +from _griffe.logger import get_logger +from _griffe.models import Alias, Attribute, Class, Docstring, Function, Module, Parameter, Parameters if TYPE_CHECKING: from pathlib import Path - from griffe.enumerations import Parser - from griffe.expressions import Expr + from _griffe.enumerations import Parser + from _griffe.expressions import Expr -logger = get_logger(__name__) -empty = Signature.empty +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.inspector") +_empty = Signature.empty def inspect( @@ -118,15 +119,33 @@ def __init__( modules_collection: A collection of modules. """ super().__init__() + self.module_name: str = module_name + """The module name.""" + self.filepath: Path | None = filepath + """The module file path.""" + self.extensions: Extensions = extensions.attach_inspector(self) + """The extensions to use when inspecting.""" + self.parent: Module | None = parent + """An optional parent for the final module object.""" + self.current: Module | Class = None # type: ignore[assignment] + """The current object being inspected.""" + self.docstring_parser: Parser | None = docstring_parser + """The docstring parser to use.""" + self.docstring_options: dict[str, Any] = docstring_options or {} + """The docstring parsing options.""" + self.lines_collection: LinesCollection = lines_collection or LinesCollection() + """A collection of source code lines.""" + self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() + """A collection of modules.""" def _get_docstring(self, node: ObjectNode) -> Docstring | None: try: @@ -239,7 +258,7 @@ def generic_inspect(self, node: ObjectNode) -> None: # so we skip it here (no member, no alias, just skip it). if child.is_module and target_path == f"{self.current.path}.{child.name}": if not hasattr(child.obj, "__file__"): - logger.debug(f"Module {target_path} is not discoverable on disk, inspecting right now") + _logger.debug(f"Module {target_path} is not discoverable on disk, inspecting right now") inspector = Inspector( child.name, filepath=None, @@ -422,7 +441,7 @@ def handle_function(self, node: ObjectNode, labels: set | None = None) -> None: return_annotation = signature.return_annotation returns = ( None - if return_annotation is empty + if return_annotation is _empty else _convert_object_to_annotation(return_annotation, parent=self.current) ) @@ -524,10 +543,10 @@ def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = Non def _convert_parameter(parameter: SignatureParameter, parent: Module | Class) -> Parameter: name = parameter.name annotation = ( - None if parameter.annotation is empty else _convert_object_to_annotation(parameter.annotation, parent=parent) + None if parameter.annotation is _empty else _convert_object_to_annotation(parameter.annotation, parent=parent) ) kind = _kind_map[parameter.kind] - if parameter.default is empty: + if parameter.default is _empty: default = None elif hasattr(parameter.default, "__name__"): # avoid repr containing chevrons and memory addresses @@ -555,6 +574,3 @@ def _convert_object_to_annotation(obj: Any, parent: Module | Class) -> str | Exp except SyntaxError: return obj return safe_get_annotation(annotation_node.body, parent=parent) # type: ignore[attr-defined] - - -__all__ = ["inspect", "Inspector"] diff --git a/src/_griffe/agents/nodes/__init__.py b/src/_griffe/agents/nodes/__init__.py index 4f1cad8d..0337c509 100644 --- a/src/_griffe/agents/nodes/__init__.py +++ b/src/_griffe/agents/nodes/__init__.py @@ -1,82 +1 @@ """This module contains utilities for extracting information from nodes.""" - -from __future__ import annotations - -import warnings -from typing import Any - -from griffe.agents.nodes._all import get__all__, safe_get__all__ -from griffe.agents.nodes._ast import ( - ast_children, - ast_first_child, - ast_kind, - ast_last_child, - ast_next, - ast_next_siblings, - ast_previous, - ast_previous_siblings, - ast_siblings, -) -from griffe.agents.nodes._docstrings import get_docstring -from griffe.agents.nodes._imports import relative_to_absolute -from griffe.agents.nodes._names import get_instance_names, get_name, get_names -from griffe.agents.nodes._parameters import get_parameters -from griffe.agents.nodes._runtime import ObjectNode -from griffe.agents.nodes._values import get_value, safe_get_value -from griffe.enumerations import ObjectKind - - -def __getattr__(name: str) -> Any: - if name in { - "get_annotation", - "get_base_class", - "get_condition", - "get_expression", - "safe_get_annotation", - "safe_get_base_class", - "safe_get_condition", - "safe_get_expression", - }: - warnings.warn( - f"Importing {name} from griffe.agents.node is deprecated. Import it from griffe.expressions instead.", - DeprecationWarning, - stacklevel=2, - ) - - from griffe import expressions - - return getattr(expressions, name) - raise AttributeError - - -__all__ = [ - "ast_children", - "ast_first_child", - "ast_kind", - "ast_last_child", - "ast_next", - "ast_next_siblings", - "ast_previous", - "ast_previous_siblings", - "ast_siblings", - "get__all__", - "get_annotation", - "get_base_class", - "get_condition", - "get_docstring", - "get_expression", - "get_instance_names", - "get_name", - "get_names", - "get_parameters", - "get_value", - "ObjectKind", - "ObjectNode", - "relative_to_absolute", - "safe_get__all__", - "safe_get_annotation", - "safe_get_base_class", - "safe_get_condition", - "safe_get_expression", - "safe_get_value", -] diff --git a/src/_griffe/agents/nodes/assignments.py b/src/_griffe/agents/nodes/assignments.py index b3fc5fda..89b38514 100644 --- a/src/_griffe/agents/nodes/assignments.py +++ b/src/_griffe/agents/nodes/assignments.py @@ -5,9 +5,10 @@ import ast from typing import Any, Callable -from griffe.logger import get_logger +from _griffe.logger import get_logger -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._names") def _get_attribute_name(node: ast.Attribute) -> str: @@ -74,6 +75,3 @@ def get_instance_names(node: ast.AST) -> list[str]: A list of names. """ return [name.split(".", 1)[1] for name in get_names(node) if name.startswith("self.")] - - -__all__ = ["get_instance_names", "get_name", "get_names"] diff --git a/src/_griffe/agents/nodes/ast.py b/src/_griffe/agents/nodes/ast.py index 197c65aa..66662d95 100644 --- a/src/_griffe/agents/nodes/ast.py +++ b/src/_griffe/agents/nodes/ast.py @@ -5,10 +5,11 @@ from ast import AST from typing import Iterator -from griffe.exceptions import LastNodeError -from griffe.logger import get_logger +from _griffe.exceptions import LastNodeError +from _griffe.logger import get_logger -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._ast") def ast_kind(node: AST) -> str: @@ -169,16 +170,3 @@ def ast_last_child(node: AST) -> AST: except ValueError as error: raise LastNodeError("there are no children node") from error return last - - -__all__ = [ - "ast_children", - "ast_first_child", - "ast_kind", - "ast_last_child", - "ast_next", - "ast_next_siblings", - "ast_previous", - "ast_previous_siblings", - "ast_siblings", -] diff --git a/src/_griffe/agents/nodes/docstrings.py b/src/_griffe/agents/nodes/docstrings.py index 31cc48f2..1059efaf 100644 --- a/src/_griffe/agents/nodes/docstrings.py +++ b/src/_griffe/agents/nodes/docstrings.py @@ -4,9 +4,10 @@ import ast -from griffe.logger import get_logger +from _griffe.logger import get_logger -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._docstrings") def get_docstring( @@ -33,6 +34,3 @@ def get_docstring( if isinstance(doc, ast.Constant) and isinstance(doc.value, str): return doc.value, doc.lineno, doc.end_lineno return None, None, None - - -__all__ = ["get_docstring"] diff --git a/src/_griffe/agents/nodes/exports.py b/src/_griffe/agents/nodes/exports.py index b12c892b..21bb940b 100644 --- a/src/_griffe/agents/nodes/exports.py +++ b/src/_griffe/agents/nodes/exports.py @@ -7,50 +7,54 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable -from griffe.agents.nodes._values import get_value -from griffe.logger import LogLevel, get_logger +from _griffe.agents.nodes.values import get_value +from _griffe.enumerations import LogLevel +from _griffe.logger import get_logger if TYPE_CHECKING: - from griffe.models import Module + from _griffe.models import Module -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._all") @dataclass -class Name: +class ExportedName: """An intermediate class to store names.""" name: str + """The exported name.""" parent: Module + """The parent module.""" -def _extract_constant(node: ast.Constant, parent: Module) -> list[str | Name]: +def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExportedName]: return [node.value] -def _extract_name(node: ast.Name, parent: Module) -> list[str | Name]: - return [Name(node.id, parent)] +def _extract_name(node: ast.Name, parent: Module) -> list[str | ExportedName]: + return [ExportedName(node.id, parent)] -def _extract_starred(node: ast.Starred, parent: Module) -> list[str | Name]: +def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExportedName]: return _extract(node.value, parent) -def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | Name]: +def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExportedName]: sequence = [] for elt in node.elts: sequence.extend(_extract(elt, parent)) return sequence -def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | Name]: +def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExportedName]: left = _extract(node.left, parent) right = _extract(node.right, parent) return left + right -_node_map: dict[type, Callable[[Any, Module], list[str | Name]]] = { +_node_map: dict[type, Callable[[Any, Module], list[str | ExportedName]]] = { ast.Constant: _extract_constant, ast.Name: _extract_name, ast.Starred: _extract_starred, @@ -61,11 +65,11 @@ def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | Name]: } -def _extract(node: ast.AST, parent: Module) -> list[str | Name]: +def _extract(node: ast.AST, parent: Module) -> list[str | ExportedName]: return _node_map[type(node)](node, parent) -def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | Name]: +def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | ExportedName]: """Get the values declared in `__all__`. Parameters: @@ -84,7 +88,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 | Name]: +) -> list[str | ExportedName]: """Safely (no exception) extract values in `__all__`. Parameters: @@ -105,8 +109,5 @@ def safe_get__all__( message += f": unsupported node {error}" else: message += f": {error}" - getattr(logger, log_level.value)(message) + getattr(_logger, log_level.value)(message) return [] - - -__all__ = ["get__all__", "safe_get__all__"] diff --git a/src/_griffe/agents/nodes/imports.py b/src/_griffe/agents/nodes/imports.py index 2c60398d..1116bf98 100644 --- a/src/_griffe/agents/nodes/imports.py +++ b/src/_griffe/agents/nodes/imports.py @@ -4,15 +4,16 @@ from typing import TYPE_CHECKING -from griffe.logger import get_logger +from _griffe.logger import get_logger if TYPE_CHECKING: import ast - from griffe.models import Module + from _griffe.models import Module -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._imports") def relative_to_absolute(node: ast.ImportFrom, name: ast.alias, current_module: Module) -> str: @@ -35,6 +36,3 @@ def relative_to_absolute(node: ast.ImportFrom, name: ast.alias, current_module: base = current_module.path + "." if node.level > 0 else "" node_module = node.module + "." if node.module else "" return base + node_module + name.name - - -__all__ = ["relative_to_absolute"] diff --git a/src/_griffe/agents/nodes/parameters.py b/src/_griffe/agents/nodes/parameters.py index f075c90b..50bd30b6 100644 --- a/src/_griffe/agents/nodes/parameters.py +++ b/src/_griffe/agents/nodes/parameters.py @@ -6,12 +6,14 @@ from itertools import zip_longest from typing import Iterable, List, Optional, Tuple, Union -from griffe.enumerations import ParameterKind -from griffe.logger import get_logger +from _griffe.enumerations import ParameterKind +from _griffe.logger import get_logger -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._parameters") ParametersType = List[Tuple[str, Optional[ast.AST], ParameterKind, Optional[Union[str, ast.AST]]]] +"""Type alias for the list of parameters of a function.""" def get_parameters(node: ast.arguments) -> ParametersType: @@ -80,6 +82,3 @@ def get_parameters(node: ast.arguments) -> ParametersType: ) return parameters - - -__all__ = ["get_parameters"] diff --git a/src/_griffe/agents/nodes/runtime.py b/src/_griffe/agents/nodes/runtime.py index 5e3e01c6..66c4d445 100644 --- a/src/_griffe/agents/nodes/runtime.py +++ b/src/_griffe/agents/nodes/runtime.py @@ -7,10 +7,11 @@ from functools import cached_property from typing import Any, ClassVar, Sequence -from griffe.enumerations import ObjectKind -from griffe.logger import get_logger +from _griffe.enumerations import ObjectKind +from _griffe.logger import get_logger -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._runtime") _builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names} _cyclic_relationships = { @@ -34,8 +35,8 @@ class ObjectNode: Each node stores an object, its name, and a reference to its parent node. """ - # low level stuff known to cause issues when resolving aliases exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"} + """Low level attributes known to cause issues when resolving aliases.""" def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> None: """Initialize the object. @@ -53,7 +54,7 @@ def __init__(self, obj: Any, name: str, parent: ObjectNode | None = None) -> Non # which triggers the __getattr__ method of the object, which in # turn can raise various exceptions. Probably not just __getattr__. # See https://github.com/pawamoy/pytkdocs/issues/45 - logger.debug(f"Could not unwrap {name}: {error!r}") + _logger.debug(f"Could not unwrap {name}: {error!r}") # Unwrap cached properties (`inpsect.unwrap` doesn't do that). if isinstance(obj, cached_property): @@ -275,6 +276,3 @@ def alias_target_path(self) -> str | None: return child_module_path child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :]) return f"{child_module_path}.{child_name}" - - -__all__ = ["ObjectKind", "ObjectNode"] diff --git a/src/_griffe/agents/nodes/values.py b/src/_griffe/agents/nodes/values.py index 59bfacac..4f2de2e4 100644 --- a/src/_griffe/agents/nodes/values.py +++ b/src/_griffe/agents/nodes/values.py @@ -6,7 +6,7 @@ import sys from typing import TYPE_CHECKING -from griffe.logger import get_logger +from _griffe.logger import get_logger # YORE: EOL 3.8: Replace block with line 4. if sys.version_info < (3, 9): @@ -17,7 +17,8 @@ if TYPE_CHECKING: from pathlib import Path -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.agents.nodes._values") def get_value(node: ast.AST | None) -> str | None: @@ -51,8 +52,5 @@ def safe_get_value(node: ast.AST | None, filepath: str | Path | None = None) -> if filepath: message += f" at {filepath}:{node.lineno}" # type: ignore[union-attr] message += f": {error}" - logger.exception(message) + _logger.exception(message) return None - - -__all__ = ["get_value", "safe_get_value"] diff --git a/src/_griffe/agents/visitor.py b/src/_griffe/agents/visitor.py index 09b4b0ad..030da8e8 100644 --- a/src/_griffe/agents/visitor.py +++ b/src/_griffe/agents/visitor.py @@ -1,8 +1,8 @@ """Code parsing and data extraction utilies. -This module exposes a public function, [`visit()`][griffe.agents.visitor.visit], +This module exposes a public function, [`visit()`][griffe.visit], which parses the module code using [`parse()`][ast.parse], -and returns a new [`Module`][griffe.models.Module] instance, +and returns a new [`Module`][griffe.Module] instance, populating its members recursively, by using a [`NodeVisitor`][ast.NodeVisitor]-like class. """ @@ -12,22 +12,20 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any -from griffe.agents.nodes import ( +from _griffe.agents.nodes.assignments import get_instance_names, get_names +from _griffe.agents.nodes.ast import ( ast_children, ast_kind, ast_next, - get_docstring, - get_instance_names, - get_names, - get_parameters, - relative_to_absolute, - safe_get__all__, ) -from griffe.collections import LinesCollection, ModulesCollection -from griffe.models import Alias, Attribute, Class, Decorator, Docstring, Function, Module, Parameter, Parameters -from griffe.enumerations import Kind -from griffe.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError -from griffe.expressions import ( +from _griffe.agents.nodes.docstrings import get_docstring +from _griffe.agents.nodes.exports import safe_get__all__ +from _griffe.agents.nodes.imports import relative_to_absolute +from _griffe.agents.nodes.parameters import get_parameters +from _griffe.collections import LinesCollection, ModulesCollection +from _griffe.enumerations import Kind +from _griffe.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError +from _griffe.expressions import ( Expr, ExprName, safe_get_annotation, @@ -35,12 +33,13 @@ safe_get_condition, safe_get_expression, ) -from griffe.extensions.base import Extensions, load_extensions +from _griffe.extensions.base import Extensions, load_extensions +from _griffe.models import Alias, Attribute, Class, Decorator, Docstring, Function, Module, Parameter, Parameters if TYPE_CHECKING: from pathlib import Path - from griffe.enumerations import Parser + from _griffe.enumerations import Parser builtin_decorators = { @@ -48,6 +47,7 @@ "staticmethod": "staticmethod", "classmethod": "classmethod", } +"""Mapping of builtin decorators to labels.""" stdlib_decorators = { "abc.abstractmethod": {"abstractmethod"}, @@ -57,7 +57,13 @@ "functools.lru_cache": {"cached"}, "dataclasses.dataclass": {"dataclass"}, } +"""Mapping of standard library decorators to labels.""" + typing_overload = {"typing.overload", "typing_extensions.overload"} +"""Set of recognized typing overload decorators. + +When such a decorator is found, the decorated function becomes an overload. +""" def visit( @@ -133,17 +139,39 @@ def __init__( modules_collection: A collection of modules. """ super().__init__() + self.module_name: str = module_name + """The module name.""" + self.filepath: Path = filepath + """The module filepath.""" + self.code: str = code + """The module source code.""" + self.extensions: Extensions = extensions.attach_visitor(self) + """The extensions to use when visiting the AST.""" + self.parent: Module | None = parent + """An optional parent for the final module object.""" + self.current: Module | Class = None # type: ignore[assignment] + """The current object being visited.""" + self.docstring_parser: Parser | None = docstring_parser + """The docstring parser to use.""" + self.docstring_options: dict[str, Any] = docstring_options or {} + """The docstring parsing options.""" + self.lines_collection: LinesCollection = lines_collection or LinesCollection() + """A collection of source code lines.""" + self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() + """A collection of modules.""" + self.type_guarded: bool = False + """Whether the current code branch is type-guarded.""" def _get_docstring(self, node: ast.AST, *, strict: bool = False) -> Docstring | None: value, lineno, endlineno = get_docstring(node, strict=strict) @@ -637,6 +665,3 @@ def visit_if(self, node: ast.If) -> None: self.type_guarded = True self.generic_visit(node) self.type_guarded = False - - -__all__ = ["visit", "Visitor"] diff --git a/src/_griffe/c3linear.py b/src/_griffe/c3linear.py index 96b1e2e4..01f0a2d3 100644 --- a/src/_griffe/c3linear.py +++ b/src/_griffe/c3linear.py @@ -9,14 +9,14 @@ from itertools import islice from typing import Deque, TypeVar -T = TypeVar("T") +_T = TypeVar("_T") -class _Dependency(Deque[T]): +class _Dependency(Deque[_T]): """A class representing a (doubly-ended) queue of items.""" @property - def head(self) -> T | None: + def head(self) -> _T | None: """Head of the dependency.""" try: return self[0] @@ -43,7 +43,7 @@ class _DependencyList: precedence order of direct parent classes. """ - def __init__(self, *lists: list[T | None]) -> None: + def __init__(self, *lists: list[_T | None]) -> None: """Initialize the list. Parameters: @@ -51,7 +51,7 @@ def __init__(self, *lists: list[T | None]) -> None: """ self._lists = [_Dependency(lst) for lst in lists] - def __contains__(self, item: T) -> bool: + def __contains__(self, item: _T) -> bool: """Return True if any linearization's tail contains an item.""" return any(item in lst.tail for lst in self._lists) @@ -63,7 +63,7 @@ def __repr__(self) -> str: return self._lists.__repr__() @property - def heads(self) -> list[T | None]: + def heads(self) -> list[_T | None]: """Return the heads.""" return [lst.head for lst in self._lists] @@ -77,7 +77,7 @@ def exhausted(self) -> bool: """True if all elements of the lists are exhausted.""" return all(len(x) == 0 for x in self._lists) - def remove(self, item: T | None) -> None: + def remove(self, item: _T | None) -> None: """Remove an item from the lists. Once an item removed from heads, the leftmost elements of the tails @@ -88,7 +88,7 @@ def remove(self, item: T | None) -> None: i.popleft() -def c3linear_merge(*lists: list[T]) -> list[T]: +def c3linear_merge(*lists: list[_T]) -> list[_T]: """Merge lists of lists in the order defined by the C3Linear algorithm. Parameters: @@ -97,7 +97,7 @@ def c3linear_merge(*lists: list[T]) -> list[T]: Returns: The merged list of items. """ - result: list[T] = [] + result: list[_T] = [] linearizations = _DependencyList(*lists) # type: ignore[arg-type] while True: @@ -115,6 +115,3 @@ def c3linear_merge(*lists: list[T]) -> list[T]: else: # Loop never broke, no linearization could possibly be found. raise ValueError("Cannot compute C3 linearization") - - -__all__ = ["c3linear_merge"] diff --git a/src/_griffe/cli.py b/src/_griffe/cli.py index d0a0defd..76ff14b5 100644 --- a/src/_griffe/cli.py +++ b/src/_griffe/cli.py @@ -24,22 +24,24 @@ import colorama -from griffe import debug -from griffe.diff import find_breaking_changes -from griffe.encoders import JSONEncoder -from griffe.enumerations import ExplanationStyle, Parser -from griffe.exceptions import ExtensionError, GitError -from griffe.extensions.base import load_extensions -from griffe.git import get_latest_tag, get_repo_root -from griffe.loader import GriffeLoader, load, load_git -from griffe.logger import get_logger +from _griffe import debug +from _griffe.diff import find_breaking_changes +from _griffe.encoders import JSONEncoder +from _griffe.enumerations import ExplanationStyle, Parser +from _griffe.exceptions import ExtensionError, GitError +from _griffe.extensions.base import load_extensions +from _griffe.git import get_latest_tag, get_repo_root +from _griffe.loader import GriffeLoader, load, load_git +from _griffe.logger import get_logger if TYPE_CHECKING: - from griffe.extensions.base import Extensions, ExtensionType + from _griffe.extensions.base import Extensions, ExtensionType DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper() -logger = get_logger(__name__) + +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.cli") class _DebugInfo(argparse.Action): @@ -47,7 +49,7 @@ def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None: super().__init__(nargs=nargs, **kwargs) def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - debug.print_debug_info() + debug._print_debug_info() sys.exit(0) @@ -90,25 +92,25 @@ def _load_packages( # Load each package. for package in packages: if not package: - logger.debug("Empty package name, continuing") + _logger.debug("Empty package name, continuing") continue - logger.info(f"Loading package {package}") + _logger.info(f"Loading package {package}") try: loader.load(package, try_relative_path=True, find_stubs_package=find_stubs_package) except ModuleNotFoundError as error: - logger.error(f"Could not find package {package}: {error}") # noqa: TRY400 + _logger.error(f"Could not find package {package}: {error}") # noqa: TRY400 except ImportError as error: - logger.exception(f"Tried but could not import package {package}: {error}") # noqa: TRY401 - logger.info("Finished loading packages") + _logger.exception(f"Tried but could not import package {package}: {error}") # noqa: TRY401 + _logger.info("Finished loading packages") # Resolve aliases. if resolve_aliases: - logger.info("Starting alias resolution") + _logger.info("Starting alias resolution") unresolved, iterations = loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external) if unresolved: - logger.info(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") + _logger.info(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") else: - logger.info(f"All aliases were resolved after {iterations} iterations") + _logger.info(f"All aliases were resolved after {iterations} iterations") return loader @@ -139,7 +141,7 @@ def get_parser() -> argparse.ArgumentParser: global_options = parser.add_argument_group(title="Global options") global_options.add_argument("-h", "--help", action="help", help=main_help) - global_options.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug.get_version()}") + global_options.add_argument("-V", "--version", action="version", version=f"%(prog)s {debug._get_version()}") global_options.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.") def add_common_options(subparser: argparse.ArgumentParser) -> None: @@ -378,7 +380,7 @@ def dump( try: loaded_extensions = load_extensions(*(extensions or ())) except ExtensionError as error: - logger.exception(str(error)) # noqa: TRY401 + _logger.exception(str(error)) # noqa: TRY401 return 1 # Load packages. @@ -412,7 +414,7 @@ def dump( if stats: loader_stats = loader.stats() loader_stats.time_spent_serializing = elapsed.microseconds - logger.info(loader_stats.as_text()) + _logger.info(loader_stats.as_text()) return 0 if len(data_packages) == len(packages) else 1 @@ -466,7 +468,7 @@ def check( try: loaded_extensions = load_extensions(*(extensions or ())) except ExtensionError as error: - logger.exception(str(error)) # noqa: TRY401 + _logger.exception(str(error)) # noqa: TRY401 return 1 # Load old and new version of the package. @@ -553,6 +555,3 @@ def main(args: list[str] | None = None) -> int: # Run subcommand. commands: dict[str, Callable[..., int]] = {"check": check, "dump": dump} return commands[subcommand](**opts_dict) - - -__all__ = ["check", "dump", "get_parser", "main"] diff --git a/src/_griffe/collections.py b/src/_griffe/collections.py index cdb5eecb..917b9c9f 100644 --- a/src/_griffe/collections.py +++ b/src/_griffe/collections.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING, Any, ItemsView, KeysView, ValuesView -from griffe.mixins import DelMembersMixin, GetMembersMixin, SetMembersMixin +from _griffe.mixins import DelMembersMixin, GetMembersMixin, SetMembersMixin if TYPE_CHECKING: from pathlib import Path - from griffe.models import Module + from _griffe.models import Module class LinesCollection: @@ -20,15 +20,19 @@ def __init__(self) -> None: self._data: dict[Path, list[str]] = {} def __getitem__(self, key: Path) -> list[str]: + """Get the lines of a file path.""" return self._data[key] def __setitem__(self, key: Path, value: list[str]) -> None: + """Set the lines of a file path.""" self._data[key] = value def __contains__(self, item: Path) -> bool: + """Check if a file path is in the collection.""" return item in self._data def __bool__(self) -> bool: + """A lines collection is always true-ish.""" return True def keys(self) -> KeysView: @@ -60,6 +64,7 @@ class ModulesCollection(GetMembersMixin, SetMembersMixin, DelMembersMixin): """A collection of modules, allowing easy access to members.""" is_collection = True + """Marked as collection to distinguish from objects.""" def __init__(self) -> None: """Initialize the collection.""" @@ -67,9 +72,11 @@ def __init__(self) -> None: """Members (modules) of the collection.""" def __bool__(self) -> bool: + """A modules collection is always true-ish.""" return True def __contains__(self, item: Any) -> bool: + """Check if a module is in the collection.""" return item in self.members @property @@ -80,6 +87,3 @@ def all_members(self) -> dict[str, Module]: as `all_members` does not make sense for a modules collection. """ return self.members - - -__all__ = ["LinesCollection", "ModulesCollection"] diff --git a/src/_griffe/debug.py b/src/_griffe/debug.py index 64e678e7..3e93bb8e 100644 --- a/src/_griffe/debug.py +++ b/src/_griffe/debug.py @@ -10,7 +10,7 @@ @dataclass -class Variable: +class _Variable: """Dataclass describing an environment variable.""" name: str @@ -20,7 +20,7 @@ class Variable: @dataclass -class Package: +class _Package: """Dataclass describing a Python package.""" name: str @@ -30,7 +30,7 @@ class Package: @dataclass -class Environment: +class _Environment: """Dataclass to store environment information.""" interpreter_name: str @@ -41,9 +41,9 @@ class Environment: """Path to Python executable.""" platform: str """Operating System.""" - packages: list[Package] + packages: list[_Package] """Installed packages.""" - variables: list[Variable] + variables: list[_Variable] """Environment variables.""" @@ -58,7 +58,7 @@ def _interpreter_name_version() -> tuple[str, str]: return "", "0.0.0" -def get_version(dist: str = "griffe") -> str: +def _get_version(dist: str = "griffe") -> str: """Get version of the given distribution. Parameters: @@ -73,7 +73,7 @@ def get_version(dist: str = "griffe") -> str: return "0.0.0" -def get_debug_info() -> Environment: +def _get_debug_info() -> _Environment: """Get debug/environment information. Returns: @@ -82,19 +82,19 @@ def get_debug_info() -> Environment: py_name, py_version = _interpreter_name_version() packages = ["griffe"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("GRIFFE")]] - return Environment( + return _Environment( interpreter_name=py_name, interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), - variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], - packages=[Package(pkg, get_version(pkg)) for pkg in packages], + variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], + packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) -def print_debug_info() -> None: +def _print_debug_info() -> None: """Print debug/environment information.""" - info = get_debug_info() + info = _get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") @@ -103,7 +103,3 @@ def print_debug_info() -> None: print("- __Installed packages__:") for pkg in info.packages: print(f" - `{pkg.name}` v{pkg.version}") - - -if __name__ == "__main__": - print_debug_info() diff --git a/src/_griffe/diff.py b/src/_griffe/diff.py index fa7ba14b..b115bf70 100644 --- a/src/_griffe/diff.py +++ b/src/_griffe/diff.py @@ -9,26 +9,28 @@ from colorama import Fore, Style -from griffe.enumerations import BreakageKind, ExplanationStyle, ParameterKind -from griffe.exceptions import AliasResolutionError -from griffe.git import WORKTREE_PREFIX -from griffe.logger import get_logger +from _griffe.enumerations import BreakageKind, ExplanationStyle, ParameterKind +from _griffe.exceptions import AliasResolutionError +from _griffe.git import _WORKTREE_PREFIX +from _griffe.logger import get_logger if TYPE_CHECKING: - from griffe.models import Alias, Attribute, Class, Function, Object + from _griffe.models import Alias, Attribute, Class, Function, Object -POSITIONAL = frozenset((ParameterKind.positional_only, ParameterKind.positional_or_keyword)) -KEYWORD = frozenset((ParameterKind.keyword_only, ParameterKind.positional_or_keyword)) -POSITIONAL_KEYWORD_ONLY = frozenset((ParameterKind.positional_only, ParameterKind.keyword_only)) -VARIADIC = frozenset((ParameterKind.var_positional, ParameterKind.var_keyword)) +_POSITIONAL = frozenset((ParameterKind.positional_only, ParameterKind.positional_or_keyword)) +_KEYWORD = frozenset((ParameterKind.keyword_only, ParameterKind.positional_or_keyword)) +_POSITIONAL_KEYWORD_ONLY = frozenset((ParameterKind.positional_only, ParameterKind.keyword_only)) +_VARIADIC = frozenset((ParameterKind.var_positional, ParameterKind.var_keyword)) -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.diff") class Breakage: """Breakages can explain what broke from a version to another.""" kind: BreakageKind + """The kind of breakage.""" def __init__(self, obj: Object, old_value: Any, new_value: Any, details: str = "") -> None: """Initialize the breakage. @@ -40,9 +42,13 @@ def __init__(self, obj: Object, old_value: Any, new_value: Any, details: str = " details: Some details about the breakage. """ self.obj = obj + """The object related to the breakage.""" self.old_value = old_value + """The old value.""" self.new_value = new_value + """The new, incompatible value.""" self.details = details + """Some details about the breakage.""" def __str__(self) -> str: return self.kind.value @@ -104,7 +110,7 @@ def _location(self) -> Path: if self._relative_filepath.is_absolute(): parts = self._relative_filepath.parts for index, part in enumerate(parts): - if part.startswith(WORKTREE_PREFIX): + if part.startswith(_WORKTREE_PREFIX): return Path(*parts[index + 2 :]) return self._relative_filepath @@ -373,7 +379,7 @@ def _function_incompatibilities(old_function: Function, new_function: Function) yield ParameterChangedRequiredBreakage(new_function, old_param, new_param) # Check if the parameter was moved. - if old_param.kind in POSITIONAL and new_param.kind in POSITIONAL: + if old_param.kind in _POSITIONAL and new_param.kind in _POSITIONAL: new_index = new_param_names.index(old_param.name) if new_index != old_index: details = f"position: from {old_index} to {new_index} ({new_index - old_index:+})" @@ -388,7 +394,8 @@ def _function_incompatibilities(old_function: Function, new_function: Function) # keyword-only to positional-only old_param.kind is ParameterKind.keyword_only and new_param.kind is ParameterKind.positional_only, # positional or keyword to positional-only/keyword-only - old_param.kind is ParameterKind.positional_or_keyword and new_param.kind in POSITIONAL_KEYWORD_ONLY, + old_param.kind is ParameterKind.positional_or_keyword + and new_param.kind in _POSITIONAL_KEYWORD_ONLY, # not keyword-only to variadic keyword, without variadic positional new_param.kind is ParameterKind.var_keyword and old_param.kind is not ParameterKind.keyword_only @@ -405,7 +412,7 @@ def _function_incompatibilities(old_function: Function, new_function: Function) # Check if the parameter changed default. breakage = ParameterChangedDefaultBreakage(new_function, old_param, new_param) non_required = not old_param.required and not new_param.required - non_variadic = old_param.kind not in VARIADIC and new_param.kind not in VARIADIC + non_variadic = old_param.kind not in _VARIADIC and new_param.kind not in _VARIADIC if non_required and non_variadic: try: if old_param.default != new_param.default: @@ -444,7 +451,7 @@ def _alias_incompatibilities( old_member = old_obj.target if old_obj.is_alias else old_obj # type: ignore[union-attr] new_member = new_obj.target if new_obj.is_alias else new_obj # type: ignore[union-attr] except AliasResolutionError: - logger.debug(f"API check: {old_obj.path} | {new_obj.path}: skip alias with unknown target") + _logger.debug(f"API check: {old_obj.path} | {new_obj.path}: skip alias with unknown target") return yield from _type_based_yield(old_member, new_member, seen_paths=seen_paths) @@ -459,9 +466,9 @@ def _member_incompatibilities( seen_paths = set() if seen_paths is None else seen_paths for name, old_member in old_obj.all_members.items(): if not old_member.is_public: - logger.debug(f"API check: {old_obj.path}.{name}: skip non-public object") + _logger.debug(f"API check: {old_obj.path}.{name}: skip non-public object") continue - logger.debug(f"API check: {old_obj.path}.{name}") + _logger.debug(f"API check: {old_obj.path}.{name}") try: new_member = new_obj.all_members[name] except KeyError: @@ -564,23 +571,3 @@ def find_breaking_changes( stacklevel=2, ) yield from _member_incompatibilities(old_obj, new_obj) - - -__all__ = [ - "AttributeChangedTypeBreakage", - "AttributeChangedValueBreakage", - "Breakage", - "BreakageKind", - "ClassRemovedBaseBreakage", - "ExplanationStyle", - "find_breaking_changes", - "ObjectChangedKindBreakage", - "ObjectRemovedBreakage", - "ParameterAddedRequiredBreakage", - "ParameterChangedDefaultBreakage", - "ParameterChangedKindBreakage", - "ParameterChangedRequiredBreakage", - "ParameterMovedBreakage", - "ParameterRemovedBreakage", - "ReturnChangedTypeBreakage", -] diff --git a/src/_griffe/docstrings/__init__.py b/src/_griffe/docstrings/__init__.py index 0a751f64..f6878256 100644 --- a/src/_griffe/docstrings/__init__.py +++ b/src/_griffe/docstrings/__init__.py @@ -1,6 +1 @@ """This module exposes objects related to docstrings.""" - -from griffe.docstrings.parsers import parse, parsers -from griffe.enumerations import Parser - -__all__ = ["Parser", "parse", "parsers"] diff --git a/src/_griffe/docstrings/google.py b/src/_griffe/docstrings/google.py index e0fa8efc..ec644875 100644 --- a/src/_griffe/docstrings/google.py +++ b/src/_griffe/docstrings/google.py @@ -6,7 +6,7 @@ from contextlib import suppress from typing import TYPE_CHECKING, List, Tuple -from griffe.docstrings.models import ( +from _griffe.docstrings.models import ( DocstringAttribute, DocstringClass, DocstringFunction, @@ -34,18 +34,18 @@ DocstringWarn, DocstringYield, ) -from griffe.docstrings.utils import parse_annotation, warning -from griffe.enumerations import DocstringSectionKind -from griffe.expressions import ExprName -from griffe.logger import LogLevel +from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation +from _griffe.enumerations import DocstringSectionKind, LogLevel +from _griffe.expressions import ExprName if TYPE_CHECKING: from typing import Any, Literal, Pattern - from griffe.models import Docstring - from griffe.expressions import Expr + from _griffe.expressions import Expr + from _griffe.models import Docstring -_warn = warning(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_warn = docstring_warning("griffe.docstrings.google") _section_kind = { "args": DocstringSectionKind.parameters, @@ -73,9 +73,9 @@ "warnings": DocstringSectionKind.warns, } -BlockItem = Tuple[int, List[str]] -BlockItems = List[BlockItem] -ItemsBlock = Tuple[BlockItems, int] +_BlockItem = Tuple[int, List[str]] +_BlockItems = List[_BlockItem] +_ItemsBlock = Tuple[_BlockItems, int] _RE_ADMONITION: Pattern = re.compile(r"^(?P[\w][\s\w-]*):(\s+(?P[^\s].*))?\s*$", re.I) _RE_NAME_ANNOTATION_DESCRIPTION: Pattern = re.compile(r"^(?:(?P<name>\w+)?\s*(?:\((?P<type>.+)\))?:\s*)?(?P<desc>.*)$") @@ -83,13 +83,13 @@ _RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") -def _read_block_items(docstring: Docstring, *, offset: int, **options: Any) -> ItemsBlock: # noqa: ARG001 +def _read_block_items(docstring: Docstring, *, offset: int, **options: Any) -> _ItemsBlock: # noqa: ARG001 lines = docstring.lines if offset >= len(lines): return [], offset new_offset = offset - items: BlockItems = [] + items: _BlockItems = [] # skip first empty lines while _is_empty_line(lines[new_offset]): @@ -206,7 +206,7 @@ def _read_parameters( if annotation.endswith(", optional"): annotation = annotation[:-10] # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) else: name = name_with_type # try to use the annotation from the signature @@ -285,7 +285,7 @@ def _read_attributes_section( if annotation.endswith(", optional"): annotation = annotation[:-10] # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) else: name = name_with_type with suppress(AttributeError, KeyError): @@ -397,7 +397,7 @@ def _read_raises_section( else: description = "\n".join([description.lstrip(), *exception_lines[1:]]).rstrip("\n") # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) exceptions.append(DocstringRaise(annotation=annotation, description=description)) return DocstringSectionRaises(exceptions), new_offset @@ -459,7 +459,7 @@ def _read_returns_section( if annotation: # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) else: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, KeyError, ValueError): @@ -515,7 +515,7 @@ def _read_yields_section( if annotation: # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) else: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, KeyError, ValueError): @@ -562,7 +562,7 @@ def _read_receives_section( if annotation: # try to compile the annotation to transform it into an expression - annotation = parse_annotation(annotation, docstring) + annotation = parse_docstring_annotation(annotation, docstring) else: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, KeyError): @@ -691,7 +691,7 @@ def _is_empty_line(line: str) -> bool: _sentinel = object() -def parse( +def parse_google( docstring: Docstring, *, ignore_init_summary: bool = False, @@ -847,11 +847,8 @@ def parse( sections[0].value = "\n".join(lines) sections.append( DocstringSectionReturns( - [DocstringReturn("", description="", annotation=parse_annotation(annotation, docstring))], + [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))], ), ) return sections - - -__all__ = ["parse"] diff --git a/src/_griffe/docstrings/models.py b/src/_griffe/docstrings/models.py index 8a969956..2b4dd11e 100644 --- a/src/_griffe/docstrings/models.py +++ b/src/_griffe/docstrings/models.py @@ -1,15 +1,15 @@ -"""This module contains the models related to docstrings.""" +"""This module contains the dataclasses related to docstrings.""" from __future__ import annotations from typing import TYPE_CHECKING -from griffe.enumerations import DocstringSectionKind +from _griffe.enumerations import DocstringSectionKind if TYPE_CHECKING: from typing import Any, Literal - from griffe.expressions import Expr + from _griffe.expressions import Expr # Elements ----------------------------------------------- @@ -160,6 +160,7 @@ class DocstringFunction(DocstringNamedElement): @property def signature(self) -> str | Expr | None: + """The function signature.""" return self.annotation @@ -168,6 +169,7 @@ class DocstringClass(DocstringNamedElement): @property def signature(self) -> str | Expr | None: + """The class signature.""" return self.annotation @@ -194,6 +196,7 @@ def __init__(self, title: str | None = None) -> None: """The section value.""" def __bool__(self) -> bool: + """Whether this section has a true-ish value.""" return bool(self.value) def as_dict(self, **kwargs: Any) -> dict[str, Any]: @@ -449,32 +452,3 @@ def __init__(self, kind: str, text: str, title: str | None = None) -> None: """ super().__init__(title) self.value: DocstringAdmonition = DocstringAdmonition(annotation=kind, description=text) - - -__all__ = [ - "DocstringAdmonition", - "DocstringAttribute", - "DocstringDeprecated", - "DocstringElement", - "DocstringNamedElement", - "DocstringParameter", - "DocstringRaise", - "DocstringReceive", - "DocstringReturn", - "DocstringSection", - "DocstringSectionAdmonition", - "DocstringSectionAttributes", - "DocstringSectionDeprecated", - "DocstringSectionExamples", - "DocstringSectionKind", - "DocstringSectionOtherParameters", - "DocstringSectionParameters", - "DocstringSectionRaises", - "DocstringSectionReceives", - "DocstringSectionReturns", - "DocstringSectionText", - "DocstringSectionWarns", - "DocstringSectionYields", - "DocstringWarn", - "DocstringYield", -] diff --git a/src/_griffe/docstrings/numpy.py b/src/_griffe/docstrings/numpy.py index 841188be..e1953292 100644 --- a/src/_griffe/docstrings/numpy.py +++ b/src/_griffe/docstrings/numpy.py @@ -24,7 +24,7 @@ from textwrap import dedent from typing import TYPE_CHECKING -from griffe.docstrings.models import ( +from _griffe.docstrings.models import ( DocstringAttribute, DocstringClass, DocstringFunction, @@ -52,19 +52,19 @@ DocstringWarn, DocstringYield, ) -from griffe.docstrings.utils import parse_annotation, warning -from griffe.enumerations import DocstringSectionKind -from griffe.expressions import ExprName -from griffe.logger import LogLevel +from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation +from _griffe.enumerations import DocstringSectionKind, LogLevel +from _griffe.expressions import ExprName if TYPE_CHECKING: from typing import Any, Literal, Pattern - from griffe.models import Docstring - from griffe.expressions import Expr + from _griffe.expressions import Expr + from _griffe.models import Docstring -_warn = warning(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_warn = docstring_warning("griffe.docstrings.numpy") _section_kind = { "deprecated": DocstringSectionKind.deprecated, @@ -257,7 +257,7 @@ def _read_parameters( else: _warn(docstring, new_offset, f"No types or annotations for parameters {names}") else: - annotation = parse_annotation(annotation, docstring, log_level=LogLevel.debug) + annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) if default is None: for name in names: @@ -389,7 +389,7 @@ def _read_returns_section( else: annotation = return_item else: - annotation = parse_annotation(annotation, docstring, log_level=LogLevel.debug) + annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) returns.append(DocstringReturn(name=name or "", annotation=annotation, description=text)) return DocstringSectionReturns(returns), new_offset @@ -437,7 +437,7 @@ def _read_yields_section( else: annotation = yield_item else: - annotation = parse_annotation(annotation, docstring, log_level=LogLevel.debug) + annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) yields.append(DocstringYield(name=name or "", annotation=annotation, description=text)) return DocstringSectionYields(yields), new_offset @@ -481,7 +481,7 @@ def _read_receives_section( else: annotation = receives_item else: - annotation = parse_annotation(annotation, docstring, log_level=LogLevel.debug) + annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) receives.append(DocstringReceive(name=name or "", annotation=annotation, description=text)) return DocstringSectionReceives(receives), new_offset @@ -503,7 +503,7 @@ def _read_raises_section( raises = [] for item in items: - annotation = parse_annotation(item[0], docstring) + annotation = parse_docstring_annotation(item[0], docstring) text = dedent("\n".join(item[1:])) raises.append(DocstringRaise(annotation=annotation, description=text)) return DocstringSectionRaises(raises), new_offset @@ -526,7 +526,7 @@ def _read_warns_section( warns = [] for item in items: - annotation = parse_annotation(item[0], docstring) + annotation = parse_docstring_annotation(item[0], docstring) text = dedent("\n".join(item[1:])) warns.append(DocstringWarn(annotation=annotation, description=text)) return DocstringSectionWarns(warns), new_offset @@ -562,7 +562,7 @@ def _read_attributes_section( with suppress(AttributeError, KeyError): annotation = docstring.parent.members[name].annotation # type: ignore[union-attr] else: - annotation = parse_annotation(annotation, docstring, log_level=LogLevel.debug) + annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) text = dedent("\n".join(item[1:])) attributes.append(DocstringAttribute(name=name, annotation=annotation, description=text)) return DocstringSectionAttributes(attributes), new_offset @@ -757,7 +757,7 @@ def _append_section(sections: list, current: list[str], admonition_title: str) - } -def parse( +def parse_numpy( docstring: Docstring, *, ignore_init_summary: bool = False, @@ -861,6 +861,3 @@ def parse( _append_section(sections, current_section, admonition_title) return sections - - -__all__ = ["parse"] diff --git a/src/_griffe/docstrings/parsers.py b/src/_griffe/docstrings/parsers.py index 7739779c..8aff5a24 100644 --- a/src/_griffe/docstrings/parsers.py +++ b/src/_griffe/docstrings/parsers.py @@ -4,14 +4,14 @@ from typing import TYPE_CHECKING, Any, Literal -from griffe.docstrings.models import DocstringSection, DocstringSectionText -from griffe.docstrings.google import parse as parse_google -from griffe.docstrings.numpy import parse as parse_numpy -from griffe.docstrings.sphinx import parse as parse_sphinx -from griffe.enumerations import Parser +from _griffe.docstrings.google import parse_google +from _griffe.docstrings.models import DocstringSection, DocstringSectionText +from _griffe.docstrings.numpy import parse_numpy +from _griffe.docstrings.sphinx import parse_sphinx +from _griffe.enumerations import Parser if TYPE_CHECKING: - from griffe.models import Docstring + from _griffe.models import Docstring parsers = { Parser.google: parse_google, @@ -40,6 +40,3 @@ def parse( parser = Parser(parser) return parsers[parser](docstring, **options) # type: ignore[operator] return [DocstringSectionText(docstring.value)] - - -__all__ = ["parse", "Parser", "parsers"] diff --git a/src/_griffe/docstrings/sphinx.py b/src/_griffe/docstrings/sphinx.py index d1524d00..728f89e6 100644 --- a/src/_griffe/docstrings/sphinx.py +++ b/src/_griffe/docstrings/sphinx.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable -from griffe.docstrings.models import ( +from _griffe.docstrings.models import ( DocstringAttribute, DocstringParameter, DocstringRaise, @@ -23,30 +23,31 @@ DocstringSectionReturns, DocstringSectionText, ) -from griffe.docstrings.utils import warning +from _griffe.docstrings.utils import docstring_warning if TYPE_CHECKING: - from griffe.models import Docstring - from griffe.expressions import Expr + from _griffe.expressions import Expr + from _griffe.models import Docstring -_warn = warning(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_warn = docstring_warning("griffe.docstrings.sphinx") # TODO: Examples: from the documentation, we're not sure there is a standard format for examples -PARAM_NAMES = frozenset(("param", "parameter", "arg", "argument", "key", "keyword")) -PARAM_TYPE_NAMES = frozenset(("type",)) -ATTRIBUTE_NAMES = frozenset(("var", "ivar", "cvar")) -ATTRIBUTE_TYPE_NAMES = frozenset(("vartype",)) -RETURN_NAMES = frozenset(("returns", "return")) -RETURN_TYPE_NAMES = frozenset(("rtype",)) -EXCEPTION_NAMES = frozenset(("raises", "raise", "except", "exception")) +_PARAM_NAMES = frozenset(("param", "parameter", "arg", "argument", "key", "keyword")) +_PARAM_TYPE_NAMES = frozenset(("type",)) +_ATTRIBUTE_NAMES = frozenset(("var", "ivar", "cvar")) +_ATTRIBUTE_TYPE_NAMES = frozenset(("vartype",)) +_RETURN_NAMES = frozenset(("returns", "return")) +_RETURN_TYPE_NAMES = frozenset(("rtype",)) +_EXCEPTION_NAMES = frozenset(("raises", "raise", "except", "exception")) @dataclass(frozen=True) -class FieldType: +class _FieldType: """Maps directive names to parser functions.""" names: frozenset[str] - reader: Callable[[Docstring, int, ParsedValues], int] + reader: Callable[[Docstring, int, _ParsedValues], int] def matches(self, line: str) -> bool: """Check if a line matches the field type. @@ -61,7 +62,7 @@ def matches(self, line: str) -> bool: @dataclass -class ParsedDirective: +class _ParsedDirective: """Directive information that has been parsed from a docstring.""" line: str @@ -72,7 +73,7 @@ class ParsedDirective: @dataclass -class ParsedValues: +class _ParsedValues: """Values parsed from the docstring to be used to produce sections.""" description: list[str] = field(default_factory=list) @@ -85,7 +86,7 @@ class ParsedValues: return_type: str | None = None -def parse(docstring: Docstring, *, warn_unknown_params: bool = True, **options: Any) -> list[DocstringSection]: +def parse_sphinx(docstring: Docstring, *, warn_unknown_params: bool = True, **options: Any) -> list[DocstringSection]: """Parse a Sphinx-style docstring. Parameters: @@ -96,7 +97,7 @@ def parse(docstring: Docstring, *, warn_unknown_params: bool = True, **options: Returns: A list of docstring sections. """ - parsed_values = ParsedValues() + parsed_values = _ParsedValues() options = { "warn_unknown_params": warn_unknown_params, @@ -108,7 +109,7 @@ def parse(docstring: Docstring, *, warn_unknown_params: bool = True, **options: while curr_line_index < len(lines): line = lines[curr_line_index] - for field_type in field_types: + for field_type in _field_types: if field_type.matches(line): # https://github.com/python/mypy/issues/5485 curr_line_index = field_type.reader(docstring, curr_line_index, parsed_values, **options) @@ -124,7 +125,7 @@ def parse(docstring: Docstring, *, warn_unknown_params: bool = True, **options: def _read_parameter( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, *, warn_unknown_params: bool = True, **options: Any, # noqa: ARG001 @@ -183,7 +184,7 @@ def _determine_param_annotation( docstring: Docstring, name: str, directive_type: str | None, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, ) -> Any: # Annotation precedence: # - in-line directive type @@ -214,7 +215,7 @@ def _determine_param_annotation( def _read_parameter_type( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) @@ -241,7 +242,7 @@ def _read_parameter_type( def _read_attribute( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) @@ -283,7 +284,7 @@ def _read_attribute( def _read_attribute_type( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) @@ -310,7 +311,7 @@ def _read_attribute_type( def _read_exception( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) @@ -326,7 +327,7 @@ def _read_exception( return parsed_directive.next_index -def _read_return(docstring: Docstring, offset: int, parsed_values: ParsedValues, **options: Any) -> int: # noqa: ARG001 +def _read_return(docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any) -> int: # noqa: ARG001 parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index @@ -354,7 +355,7 @@ def _read_return(docstring: Docstring, offset: int, parsed_values: ParsedValues, def _read_return_type( docstring: Docstring, offset: int, - parsed_values: ParsedValues, + parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) @@ -370,7 +371,7 @@ def _read_return_type( return parsed_directive.next_index -def _parsed_values_to_sections(parsed_values: ParsedValues) -> list[DocstringSection]: +def _parsed_values_to_sections(parsed_values: _ParsedValues) -> list[DocstringSection]: text = "\n".join(_strip_blank_lines(parsed_values.description)) result: list[DocstringSection] = [DocstringSectionText(text)] if parsed_values.parameters: @@ -386,16 +387,16 @@ def _parsed_values_to_sections(parsed_values: ParsedValues) -> list[DocstringSec return result -def _parse_directive(docstring: Docstring, offset: int) -> ParsedDirective: +def _parse_directive(docstring: Docstring, offset: int) -> _ParsedDirective: line, next_index = _consolidate_continuation_lines(docstring.lines, offset) try: _, directive, value = line.split(":", 2) except ValueError: _warn(docstring, 0, f"Failed to get ':directive: value' pair from '{line}'") - return ParsedDirective(line, next_index, [], "", invalid=True) + return _ParsedDirective(line, next_index, [], "", invalid=True) value = value.strip() - return ParsedDirective(line, next_index, directive.split(" "), value) + return _ParsedDirective(line, next_index, directive.split(" "), value) def _consolidate_continuation_lines(lines: list[str], offset: int) -> tuple[str, int]: @@ -433,15 +434,12 @@ def _strip_blank_lines(lines: list[str]) -> list[str]: return lines[initial_content : final_content + 1] -field_types = [ - FieldType(PARAM_TYPE_NAMES, _read_parameter_type), - FieldType(PARAM_NAMES, _read_parameter), - FieldType(ATTRIBUTE_TYPE_NAMES, _read_attribute_type), - FieldType(ATTRIBUTE_NAMES, _read_attribute), - FieldType(EXCEPTION_NAMES, _read_exception), - FieldType(RETURN_NAMES, _read_return), - FieldType(RETURN_TYPE_NAMES, _read_return_type), +_field_types = [ + _FieldType(_PARAM_TYPE_NAMES, _read_parameter_type), + _FieldType(_PARAM_NAMES, _read_parameter), + _FieldType(_ATTRIBUTE_TYPE_NAMES, _read_attribute_type), + _FieldType(_ATTRIBUTE_NAMES, _read_attribute), + _FieldType(_EXCEPTION_NAMES, _read_exception), + _FieldType(_RETURN_NAMES, _read_return), + _FieldType(_RETURN_TYPE_NAMES, _read_return_type), ] - - -__all__ = ["parse"] diff --git a/src/_griffe/docstrings/utils.py b/src/_griffe/docstrings/utils.py index 654bf9ed..2c26b037 100644 --- a/src/_griffe/docstrings/utils.py +++ b/src/_griffe/docstrings/utils.py @@ -6,20 +6,31 @@ from contextlib import suppress from typing import TYPE_CHECKING, Protocol -from griffe.exceptions import BuiltinModuleError -from griffe.expressions import safe_get_annotation -from griffe.logger import LogLevel, get_logger +from _griffe.enumerations import LogLevel +from _griffe.exceptions import BuiltinModuleError +from _griffe.expressions import safe_get_annotation +from _griffe.logger import get_logger if TYPE_CHECKING: - from griffe.models import Docstring - from griffe.expressions import Expr + from _griffe.expressions import Expr + from _griffe.models import Docstring -class WarningCallable(Protocol): - def __call__(self, docstring: Docstring, offset: int, message: str, log_level: LogLevel = ...) -> None: ... +class DocstringWarningCallable(Protocol): + """A callable that logs a warning message.""" + def __call__(self, docstring: Docstring, offset: int, message: str, log_level: LogLevel = ...) -> None: + """Log a warning message. -def warning(name: str) -> WarningCallable: + Parameters: + docstring: The docstring in which the warning occurred. + offset: The offset in the docstring lines. + message: The message to log. + log_level: The log level to use. + """ + + +def docstring_warning(name: str) -> DocstringWarningCallable: """Create and return a warn function. Parameters: @@ -50,7 +61,7 @@ def warn(docstring: Docstring, offset: int, message: str, log_level: LogLevel = return warn -def parse_annotation( +def parse_docstring_annotation( annotation: str, docstring: Docstring, log_level: LogLevel = LogLevel.error, @@ -79,6 +90,3 @@ def parse_annotation( ) return name_or_expr or annotation return annotation - - -__all__ = ["parse_annotation", "warning"] diff --git a/src/_griffe/encoders.py b/src/_griffe/encoders.py index 985fe697..eaea81fc 100644 --- a/src/_griffe/encoders.py +++ b/src/_griffe/encoders.py @@ -2,7 +2,7 @@ The available formats are: -- `JSON`: see the [`JSONEncoder`][griffe.encoders.JSONEncoder] and [`json_decoder`][griffe.encoders.json_decoder]. +- `JSON`: see the [`JSONEncoder`][griffe.JSONEncoder] and [`json_decoder`][griffe.json_decoder]. """ from __future__ import annotations @@ -12,8 +12,9 @@ from pathlib import Path, PosixPath, WindowsPath from typing import TYPE_CHECKING, Any, Callable -from griffe import expressions -from griffe.models import ( +from _griffe import expressions +from _griffe.enumerations import DocstringSectionKind, Kind, ParameterKind +from _griffe.models import ( Alias, Attribute, Class, @@ -55,11 +56,11 @@ class JSONEncoder(json.JSONEncoder): the [`json.dump`][] or [`json.dumps`][] methods. Examples: - >>> from griffe.encoders import JSONEncoder + >>> from griffe import JSONEncoder >>> JSONEncoder(full=True).encode(..., **kwargs) >>> import json - >>> from griffe.encoders import JSONEncoder + >>> from griffe import JSONEncoder >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs) """ @@ -67,7 +68,9 @@ def __init__( self, *args: Any, full: bool = False, + # YORE: Bump 1.0.0: Remove line. docstring_parser: Parser | None = None, + # YORE: Bump 1.0.0: Remove line. docstring_options: dict[str, Any] | None = None, **kwargs: Any, ) -> None: @@ -77,20 +80,21 @@ def __init__( *args: See [`json.JSONEncoder`][]. full: Whether to dump full data or base data. If you plan to reload the data in Python memory - using the [`json_decoder`][griffe.encoders.json_decoder], + using the [`json_decoder`][griffe.json_decoder], you don't need the full data as it can be infered again using the base data. If you want to feed a non-Python tool instead, dump the full data. - docstring_parser: Deprecated. The docstring parser to use. By default, no parsing is done. - docstring_options: Deprecated. Additional docstring parsing options. **kwargs: See [`json.JSONEncoder`][]. """ super().__init__(*args, **kwargs) self.full: bool = full + """Whether to dump full data or base data.""" # YORE: Bump 1.0.0: Remove block. self.docstring_parser: Parser | None = docstring_parser + """Deprecated. The docstring parser to use. By default, no parsing is done.""" self.docstring_options: dict[str, Any] = docstring_options or {} + """Deprecated. Additional docstring parsing options.""" if docstring_parser is not None: warnings.warn("Parameter `docstring_parser` is deprecated and has no effect.", stacklevel=1) if docstring_options is not None: @@ -263,7 +267,7 @@ def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | Examples: >>> import json - >>> from griffe.encoders import json_decoder + >>> from griffe import json_decoder >>> json.loads(..., object_hook=json_decoder) Parameters: @@ -286,6 +290,3 @@ def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | # Return dict as is. return obj_dict - - -__all__ = ["JSONEncoder", "json_decoder"] diff --git a/src/_griffe/enumerations.py b/src/_griffe/enumerations.py index 46633323..97394a2c 100644 --- a/src/_griffe/enumerations.py +++ b/src/_griffe/enumerations.py @@ -87,17 +87,29 @@ class BreakageKind(enum.Enum): """Enumeration of the possible API breakages.""" PARAMETER_MOVED: str = "Positional parameter was moved" + """Positional parameter was moved""" PARAMETER_REMOVED: str = "Parameter was removed" + """Parameter was removed""" PARAMETER_CHANGED_KIND: str = "Parameter kind was changed" + """Parameter kind was changed""" PARAMETER_CHANGED_DEFAULT: str = "Parameter default was changed" + """Parameter default was changed""" PARAMETER_CHANGED_REQUIRED: str = "Parameter is now required" + """Parameter is now required""" PARAMETER_ADDED_REQUIRED: str = "Parameter was added as required" + """Parameter was added as required""" RETURN_CHANGED_TYPE: str = "Return types are incompatible" + """Return types are incompatible""" OBJECT_REMOVED: str = "Public object was removed" + """Public object was removed""" OBJECT_CHANGED_KIND: str = "Public object points to a different kind of object" + """Public object points to a different kind of object""" ATTRIBUTE_CHANGED_TYPE: str = "Attribute types are incompatible" + """Attribute types are incompatible""" ATTRIBUTE_CHANGED_VALUE: str = "Attribute value was changed" + """Attribute value was changed""" CLASS_REMOVED_BASE: str = "Base class was removed" + """Base class was removed""" class Parser(enum.Enum): @@ -145,8 +157,13 @@ def __str__(self) -> str: return self.value +# YORE: Bump 1.0.0: Remove block. class When(enum.Enum): - """Enumeration of the different times at which an extension is used.""" + """Enumeration of the different times at which an extension is used. + + Deprecated. This enumeration is used with the `VisitorExtension` and `InspectorExtension` classes, + which are deprecated. Use the `Extension` class instead, which does not need `When`. + """ before_all: int = 1 """For each node, before the visit/inspection.""" diff --git a/src/_griffe/exceptions.py b/src/_griffe/exceptions.py index 7b19a353..0ad09757 100644 --- a/src/_griffe/exceptions.py +++ b/src/_griffe/exceptions.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from griffe.models import Alias + from _griffe.models import Alias class GriffeError(Exception): @@ -87,20 +87,3 @@ class ExtensionNotLoadedError(ExtensionError): class GitError(GriffeError): """Exception raised for errors related to Git.""" - - -__all__ = [ - "AliasResolutionError", - "BuiltinModuleError", - "CyclicAliasError", - "ExtensionError", - "ExtensionNotLoadedError", - "GitError", - "GriffeError", - "LastNodeError", - "LoadingError", - "NameResolutionError", - "RootNodeError", - "UnhandledEditableModuleError", - "UnimportableModuleError", -] diff --git a/src/_griffe/expressions.py b/src/_griffe/expressions.py index b559cbf0..d4d11058 100644 --- a/src/_griffe/expressions.py +++ b/src/_griffe/expressions.py @@ -10,18 +10,19 @@ from itertools import zip_longest from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Sequence -from griffe.agents.nodes import get_parameters -from griffe.enumerations import ParameterKind -from griffe.exceptions import NameResolutionError -from griffe.logger import LogLevel, get_logger +from _griffe.agents.nodes.parameters import get_parameters +from _griffe.enumerations import LogLevel, ParameterKind +from _griffe.exceptions import NameResolutionError +from _griffe.logger import get_logger if TYPE_CHECKING: from pathlib import Path - from griffe.models import Class, Module + from _griffe.models import Class, Module -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.expressions") def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]: @@ -74,9 +75,9 @@ def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]: # YORE: EOL 3.9: Remove block. -dataclass_opts: dict[str, bool] = {} +_dataclass_opts: dict[str, bool] = {} if sys.version_info >= (3, 10): - dataclass_opts["slots"] = True + _dataclass_opts["slots"] = True @dataclass @@ -87,6 +88,7 @@ def __str__(self) -> str: return "".join(elem if isinstance(elem, str) else elem.name for elem in self.iterate(flat=True)) # type: ignore[attr-defined] def __iter__(self) -> Iterator[str | Expr]: + """Iterate on the expression syntax and elements.""" yield from self.iterate(flat=False) def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002 @@ -101,7 +103,7 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002 without them getting rendered as strings. On the contrary, when flat is true, the whole tree is flattened as a sequence - of strings and instances of [Names][griffe.expressions.ExprName]. + of strings and instances of [Names][griffe.ExprName]. Yields: Strings and names when flat, strings and expressions otherwise. @@ -171,15 +173,15 @@ def is_generator(self) -> bool: return isinstance(self, ExprSubscript) and self.canonical_name == "Generator" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprAttribute(Expr): """Attributes like `a.b`.""" values: list[str | Expr] """The different parts of the dotted chain.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _join(self.values, ".", flat=flat) def append(self, value: ExprName) -> None: @@ -215,8 +217,8 @@ def canonical_path(self) -> str: return self.last.canonical_path -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprBinOp(Expr): """Binary operations like `a + b`.""" @@ -227,14 +229,14 @@ class ExprBinOp(Expr): right: str | Expr """Right part.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.left, flat=flat) yield f" {self.operator} " yield from _yield(self.right, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprBoolOp(Expr): """Boolean operations like `a or b`.""" @@ -243,12 +245,12 @@ class ExprBoolOp(Expr): values: Sequence[str | Expr] """Operands.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _join(self.values, f" {self.operator} ", flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprCall(Expr): """Calls like `f()`.""" @@ -262,15 +264,15 @@ def canonical_path(self) -> str: """The canonical path of this subscript's left part.""" return self.function.canonical_path - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.function, flat=flat) yield "(" yield from _join(self.arguments, ", ", flat=flat) yield ")" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprCompare(Expr): """Comparisons like `a > b`.""" @@ -281,14 +283,14 @@ class ExprCompare(Expr): comparators: Sequence[str | Expr] """Things compared.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.left, flat=flat) yield " " yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprComprehension(Expr): """Comprehensions like `a for b in c if d`.""" @@ -301,7 +303,7 @@ class ExprComprehension(Expr): is_async: bool = False """Async comprehension or not.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if self.is_async: yield "async " yield "for " @@ -313,20 +315,20 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _join(self.conditions, " if ", flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprConstant(Expr): """Constants like `"a"` or `1`.""" value: str """Constant value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002,D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002 yield self.value -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprDict(Expr): """Dictionaries like `{"a": 0}`.""" @@ -335,7 +337,7 @@ class ExprDict(Expr): values: Sequence[str | Expr] """Dict values.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _join( (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)), @@ -345,8 +347,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "}" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprDictComp(Expr): """Dict comprehensions like `{k: v for k, v in a}`.""" @@ -357,7 +359,7 @@ class ExprDictComp(Expr): generators: Sequence[Expr] """Generators iterated on.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _yield(self.key, flat=flat) yield ": " @@ -366,34 +368,34 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "}" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprExtSlice(Expr): """Extended slice like `a[x:y, z]`.""" dims: Sequence[str | Expr] """Dims.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _join(self.dims, ", ", flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprFormatted(Expr): """Formatted string like `{1 + 1}`.""" value: str | Expr """Formatted value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _yield(self.value, flat=flat) yield "}" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprGeneratorExp(Expr): """Generator expressions like `a for b in c for d in e`.""" @@ -402,14 +404,14 @@ class ExprGeneratorExp(Expr): generators: Sequence[Expr] """Generators iterated on.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprIfExp(Expr): """Conditions like `a if b else c`.""" @@ -420,7 +422,7 @@ class ExprIfExp(Expr): orelse: str | Expr """Other expression.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.body, flat=flat) yield " if " yield from _yield(self.test, flat=flat) @@ -428,22 +430,22 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _yield(self.orelse, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprJoinedStr(Expr): """Joined strings like `f"a {b} c"`.""" values: Sequence[str | Expr] """Joined values.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "f'" yield from _join(self.values, "", flat=flat) yield "'" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprKeyword(Expr): """Keyword arguments like `a=b`.""" @@ -477,40 +479,40 @@ def canonical_path(self) -> str: return f"{self.function.canonical_path}({self.name})" return super(ExprKeyword, self).canonical_path # noqa: UP008 - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield self.name yield "=" yield from _yield(self.value, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprVarPositional(Expr): """Variadic positional parameters like `*args`.""" value: Expr """Starred value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "*" yield from _yield(self.value, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprVarKeyword(Expr): """Variadic keyword parameters like `**kwargs`.""" value: Expr """Double-starred value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "**" yield from _yield(self.value, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprLambda(Expr): """Lambda expressions like `lambda a: a.b`.""" @@ -519,7 +521,7 @@ class ExprLambda(Expr): body: str | Expr """Lambda's body.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: pos_only = False pos_or_kw = False kw_only = False @@ -552,22 +554,22 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _yield(self.body, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprList(Expr): """Lists like `[0, 1, 2]`.""" elements: Sequence[Expr] """List elements.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "[" yield from _join(self.elements, ", ", flat=flat) yield "]" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprListComp(Expr): """List comprehensions like `[a for b in c]`.""" @@ -576,7 +578,7 @@ class ExprListComp(Expr): generators: Sequence[Expr] """Generators iterated on.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "[" yield from _yield(self.element, flat=flat) yield " " @@ -584,8 +586,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "]" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=False, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=False, **_dataclass_opts) class ExprName(Expr): """This class represents a Python object identified by a name in a given scope.""" @@ -595,11 +597,12 @@ class ExprName(Expr): """Parent (for resolution in its scope).""" def __eq__(self, other: object) -> bool: + """Two name expressions are equal if they have the same `name` value (`parent` is ignored).""" if isinstance(other, ExprName): return self.name == other.name return NotImplemented - def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002,D102 + def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002 yield self @property @@ -645,6 +648,7 @@ def is_enum_class(self) -> bool: return False # TODO: Support inheritance? + # TODO: Support `StrEnum` and `IntEnum`. return any(isinstance(base, Expr) and base.canonical_path == "enum.Enum" for base in bases) @property @@ -664,8 +668,8 @@ def is_enum_value(self) -> bool: return False -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprNamedExpr(Expr): """Named/assignment expressions like `a := b`.""" @@ -674,7 +678,7 @@ class ExprNamedExpr(Expr): value: str | Expr """Value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "(" yield from _yield(self.target, flat=flat) yield " := " @@ -682,8 +686,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield ")" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprParameter(Expr): """Parameters in function signatures like `a: int = 0`.""" @@ -697,22 +701,22 @@ class ExprParameter(Expr): """Parameter default.""" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprSet(Expr): """Sets like `{0, 1, 2}`.""" elements: Sequence[str | Expr] """Set elements.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _join(self.elements, ", ", flat=flat) yield "}" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprSetComp(Expr): """Set comprehensions like `{a for b in c}`.""" @@ -721,7 +725,7 @@ class ExprSetComp(Expr): generators: Sequence[Expr] """Generators iterated on.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _yield(self.element, flat=flat) yield " " @@ -729,8 +733,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "}" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprSlice(Expr): """Slices like `[a:b:c]`.""" @@ -741,7 +745,7 @@ class ExprSlice(Expr): step: str | Expr | None = None """Iteration step.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if self.lower is not None: yield from _yield(self.lower, flat=flat) yield ":" @@ -752,8 +756,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _yield(self.step, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprSubscript(Expr): """Subscripts like `a[b]`.""" @@ -762,7 +766,7 @@ class ExprSubscript(Expr): slice: Expr """Slice part.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.left, flat=flat) yield "[" yield from _yield(self.slice, flat=flat) @@ -783,8 +787,8 @@ def canonical_path(self) -> str: return self.left.canonical_path -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprTuple(Expr): """Tuples like `(0, 1, 2)`.""" @@ -793,7 +797,7 @@ class ExprTuple(Expr): implicit: bool = False """Whether the tuple is implicit (e.g. without parentheses in a subscript's slice).""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if not self.implicit: yield "(" yield from _join(self.elements, ", ", flat=flat) @@ -801,8 +805,8 @@ def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield ")" -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprUnaryOp(Expr): """Unary operations like `-1`.""" @@ -811,28 +815,28 @@ class ExprUnaryOp(Expr): value: str | Expr """Value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield self.operator yield from _yield(self.value, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprYield(Expr): """Yield statements like `yield a`.""" value: str | Expr | None = None """Yielded value.""" - def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 + def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "yield" if self.value is not None: yield " " yield from _yield(self.value, flat=flat) -# YORE: EOL 3.9: Replace `**dataclass_opts` with `slots=True` within line. -@dataclass(eq=True, **dataclass_opts) +# YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. +@dataclass(eq=True, **_dataclass_opts) class ExprYieldFrom(Expr): """Yield statements like `yield from a`.""" @@ -965,7 +969,7 @@ def _build_constant( optimize=1, ) except SyntaxError: - logger.debug( + _logger.debug( f"Tried and failed to parse {node.value!r} as Python code, " "falling back to using it as a string literal " "(postponed annotations might help: https://peps.python.org/pep-0563/)", @@ -1245,7 +1249,7 @@ def safe_get_expression( lineno = node.lineno # type: ignore[union-attr] error_str = f"{error.__class__.__name__}: {error}" message = msg_format.format(path=path, lineno=lineno, node_class=node_class, error=error_str) - getattr(logger, log_level.value)(message) + getattr(_logger, log_level.value)(message) return None @@ -1268,46 +1272,3 @@ def safe_get_expression( parse_strings=False, msg_format=_msg_format % "condition", ) - - -__all__ = [ - "Expr", - "ExprAttribute", - "ExprBinOp", - "ExprBoolOp", - "ExprCall", - "ExprCompare", - "ExprComprehension", - "ExprConstant", - "ExprDict", - "ExprDictComp", - "ExprExtSlice", - "ExprFormatted", - "ExprGeneratorExp", - "ExprIfExp", - "ExprJoinedStr", - "ExprKeyword", - "ExprVarPositional", - "ExprVarKeyword", - "ExprLambda", - "ExprList", - "ExprListComp", - "ExprName", - "ExprNamedExpr", - "ExprParameter", - "ExprSet", - "ExprSetComp", - "ExprSlice", - "ExprSubscript", - "ExprTuple", - "ExprUnaryOp", - "ExprYield", - "get_annotation", - "get_base_class", - "get_condition", - "get_expression", - "safe_get_annotation", - "safe_get_base_class", - "safe_get_condition", - "safe_get_expression", -] diff --git a/src/_griffe/extensions/__init__.py b/src/_griffe/extensions/__init__.py index 43001e42..ecd09b20 100644 --- a/src/_griffe/extensions/__init__.py +++ b/src/_griffe/extensions/__init__.py @@ -1,21 +1 @@ """This module is the public interface to import elements from the base.""" - -from griffe.enumerations import When -from griffe.extensions.base import ( - Extension, - Extensions, - ExtensionType, - InspectorExtension, - VisitorExtension, - load_extensions, -) - -__all__ = [ - "Extension", - "Extensions", - "ExtensionType", - "InspectorExtension", - "load_extensions", - "VisitorExtension", - "When", -] diff --git a/src/_griffe/extensions/base.py b/src/_griffe/extensions/base.py index 72274af7..8fb8b684 100644 --- a/src/_griffe/extensions/base.py +++ b/src/_griffe/extensions/base.py @@ -11,25 +11,27 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Sequence, Type, Union -from griffe.agents.nodes import ast_children, ast_kind -from griffe.enumerations import When -from griffe.exceptions import ExtensionNotLoadedError -from griffe.importer import dynamic_import +from _griffe.agents.nodes.ast import ast_children, ast_kind +from _griffe.enumerations import When +from _griffe.exceptions import ExtensionNotLoadedError +from _griffe.importer import dynamic_import if TYPE_CHECKING: import ast from types import ModuleType - from griffe.agents.inspector import Inspector - from griffe.agents.nodes import ObjectNode - from griffe.agents.visitor import Visitor - from griffe.models import Attribute, Class, Function, Module, Object + from _griffe.agents.inspector import Inspector + from _griffe.agents.nodes.runtime import ObjectNode + from _griffe.agents.visitor import Visitor + from _griffe.models import Attribute, Class, Function, Module, Object +# YORE: Bump 1.0.0: Remove block. class VisitorExtension: """Deprecated in favor of `Extension`. The node visitor extension base class, to inherit from.""" when: When = When.after_all + """When the visitor extension should run.""" def __init__(self) -> None: """Initialize the visitor extension.""" @@ -40,6 +42,7 @@ def __init__(self) -> None: stacklevel=1, ) self.visitor: Visitor = None # type: ignore[assignment] + """The parent visitor.""" def attach(self, visitor: Visitor) -> None: """Attach the parent visitor to this extension. @@ -58,10 +61,12 @@ def visit(self, node: ast.AST) -> None: getattr(self, f"visit_{ast_kind(node)}", lambda _: None)(node) +# YORE: Bump 1.0.0: Remove block. class InspectorExtension: """Deprecated in favor of `Extension`. The object inspector extension base class, to inherit from.""" when: When = When.after_all + """When the inspector extension should run.""" def __init__(self) -> None: """Initialize the inspector extension.""" @@ -72,6 +77,7 @@ def __init__(self) -> None: stacklevel=1, ) self.inspector: Inspector = None # type: ignore[assignment] + """The parent inspector.""" def attach(self, inspector: Inspector) -> None: """Attach the parent inspector to this extension. @@ -235,24 +241,33 @@ def on_package_loaded(self, *, pkg: Module) -> None: """ +# YORE: Bump 1.0.0: Remove block. ExtensionType = Union[VisitorExtension, InspectorExtension, Extension] -"""All the types that can be passed to `Extensions.add`.""" +"""All the types that can be passed to `Extensions.add`. Deprecated. Use `Extension` instead.""" + +# YORE: Bump 1.0.0: Regex-replace `\bExtensionType\b` with `Extension` within line. +LoadableExtensionType = Union[str, Dict[str, Any], ExtensionType, Type[ExtensionType]] +"""All the types that can be passed to `load_extensions`.""" class Extensions: """This class helps iterating on extensions that should run at different times.""" + # YORE: Bump 1.0.0: Replace `ExtensionType` with `Extension` within line. def __init__(self, *extensions: ExtensionType) -> None: """Initialize the extensions container. Parameters: *extensions: The extensions to add. """ + # YORE: Bump 1.0.0: Remove block. self._visitors: dict[When, list[VisitorExtension]] = defaultdict(list) self._inspectors: dict[When, list[InspectorExtension]] = defaultdict(list) + self._extensions: list[Extension] = [] self.add(*extensions) + # YORE: Bump 1.0.0: Replace `ExtensionType` with `Extension` within line. def add(self, *extensions: ExtensionType) -> None: """Add extensions to this container. @@ -260,6 +275,7 @@ def add(self, *extensions: ExtensionType) -> None: *extensions: The extensions to add. """ for extension in extensions: + # YORE: Bump 1.0.0: Replace block with line 6 if isinstance(extension, VisitorExtension): self._visitors[extension.when].append(extension) elif isinstance(extension, InspectorExtension): @@ -267,6 +283,7 @@ def add(self, *extensions: ExtensionType) -> None: else: self._extensions.append(extension) + # YORE: Bump 1.0.0: Remove block. def attach_visitor(self, parent_visitor: Visitor) -> Extensions: """Attach a parent visitor to the visitor extensions. @@ -281,6 +298,7 @@ def attach_visitor(self, parent_visitor: Visitor) -> Extensions: visitor.attach(parent_visitor) return self + # YORE: Bump 1.0.0: Remove block. def attach_inspector(self, parent_inspector: Inspector) -> Extensions: """Attach a parent inspector to the inspector extensions. @@ -295,41 +313,49 @@ def attach_inspector(self, parent_inspector: Inspector) -> Extensions: inspector.attach(parent_inspector) return self + # YORE: Bump 1.0.0: Remove block. @property def before_visit(self) -> list[VisitorExtension]: """The visitors that run before the visit.""" return self._visitors[When.before_all] + # YORE: Bump 1.0.0: Remove block. @property def before_children_visit(self) -> list[VisitorExtension]: """The visitors that run before the children visit.""" return self._visitors[When.before_children] + # YORE: Bump 1.0.0: Remove block. @property def after_children_visit(self) -> list[VisitorExtension]: """The visitors that run after the children visit.""" return self._visitors[When.after_children] + # YORE: Bump 1.0.0: Remove block. @property def after_visit(self) -> list[VisitorExtension]: """The visitors that run after the visit.""" return self._visitors[When.after_all] + # YORE: Bump 1.0.0: Remove block. @property def before_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the inspection.""" return self._inspectors[When.before_all] + # YORE: Bump 1.0.0: Remove block. @property def before_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the children inspection.""" return self._inspectors[When.before_children] + # YORE: Bump 1.0.0: Remove block. @property def after_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run after the children inspection.""" return self._inspectors[When.after_children] + # YORE: Bump 1.0.0: Remove block. @property def after_inspection(self) -> list[InspectorExtension]: """The inspectors that run after the inspection.""" @@ -347,9 +373,11 @@ def call(self, event: str, **kwargs: Any) -> None: builtin_extensions: set[str] = { + # YORE: Bump 1.0.0: Remove line. "hybrid", "dataclasses", } +"""The names of built-in Griffe extensions.""" def _load_extension_path(path: str) -> ModuleType: @@ -363,6 +391,7 @@ def _load_extension_path(path: str) -> ModuleType: return module +# YORE: Bump 1.0.0: Replace `ExtensionType` with `Extension` within block. def _load_extension( extension: str | dict[str, Any] | ExtensionType | type[ExtensionType], ) -> ExtensionType | list[ExtensionType]: @@ -381,13 +410,17 @@ def _load_extension( An extension instance. """ ext_object = None + + # YORE: Bump 1.0.0: Remove line. ext_classes = (VisitorExtension, InspectorExtension, Extension) # If it's already an extension instance, return it. + # YORE: Bump 1.0.0: Replace `ext_classes` with `Extension` within line. if isinstance(extension, ext_classes): return extension # If it's an extension class, instantiate it (without options) and return it. + # YORE: Bump 1.0.0: Replace `ext_classes` with `Extension` within line. if isclass(extension) and issubclass(extension, ext_classes): return extension() @@ -415,7 +448,7 @@ def _load_extension( # If the import path corresponds to a built-in extension, expand it. if import_path in builtin_extensions: - import_path = f"griffe.extensions.{import_path}" + import_path = f"_griffe.extensions.{import_path}" # If the import path is a path to an existing file, load it. elif os.path.exists(import_path): try: @@ -434,6 +467,7 @@ def _load_extension( raise ExtensionNotLoadedError(f"Error while importing extension '{import_path}': {error}") from error # If the loaded object is an extension class, instantiate it with options and return it. + # YORE: Bump 1.0.0: Replace `ext_classes` with `Extension` within line. if isclass(ext_object) and issubclass(ext_object, ext_classes): return ext_object(**options) # type: ignore[misc] @@ -451,18 +485,16 @@ def _load_extension( # instantiate each with the same options, and return them. extensions = [] for obj in vars(ext_object).values(): + # YORE: Bump 1.0.0: Replace `ext_classes` with `Extension` within line. + # YORE: Bump 1.0.0: Replace `not in` with `is not` within line. if isclass(obj) and issubclass(obj, ext_classes) and obj not in ext_classes: extensions.append(obj) return [ext(**options) for ext in extensions] -LoadableExtension = Union[str, Dict[str, Any], ExtensionType, Type[ExtensionType]] -"""All the types that can be passed to `load_extensions`.""" - - def load_extensions( # YORE: Bump 1.0.0: Replace ` | Sequence[LoadableExtension],` with `` within line. - *exts: LoadableExtension | Sequence[LoadableExtension], + *exts: LoadableExtensionType | Sequence[LoadableExtensionType], ) -> Extensions: """Load configured extensions. @@ -475,7 +507,7 @@ def load_extensions( extensions = Extensions() # YORE: Bump 1.0.0: Remove block. - all_exts: list[LoadableExtension] = [] + all_exts: list[LoadableExtensionType] = [] for ext in exts: if isinstance(ext, (list, tuple)): warnings.warn( @@ -498,7 +530,7 @@ def load_extensions( # TODO: Deprecate and remove at some point? # Always add our built-in dataclasses extension. - from griffe.extensions.dataclasses import DataclassesExtension + from _griffe.extensions.dataclasses import DataclassesExtension for ext in extensions._extensions: if type(ext) == DataclassesExtension: @@ -507,15 +539,3 @@ def load_extensions( extensions.add(*_load_extension("dataclasses")) # type: ignore[misc] return extensions - - -__all__ = [ - "builtin_extensions", - "Extension", - "Extensions", - "ExtensionType", - "InspectorExtension", - "load_extensions", - "VisitorExtension", - "When", -] diff --git a/src/_griffe/extensions/dataclasses.py b/src/_griffe/extensions/dataclasses.py index 02196d6d..5341bdfc 100644 --- a/src/_griffe/extensions/dataclasses.py +++ b/src/_griffe/extensions/dataclasses.py @@ -11,15 +11,15 @@ from functools import lru_cache from typing import Any, cast -from griffe.models import Attribute, Class, Decorator, Function, Module, Parameter, Parameters -from griffe.enumerations import ParameterKind -from griffe.expressions import ( +from _griffe.enumerations import ParameterKind +from _griffe.expressions import ( Expr, ExprAttribute, ExprCall, ExprDict, ) -from griffe.extensions.base import Extension +from _griffe.extensions.base import Extension +from _griffe.models import Attribute, Class, Decorator, Function, Module, Parameter, Parameters def _dataclass_decorator(decorators: list[Decorator]) -> Expr | None: diff --git a/src/_griffe/extensions/hybrid.py b/src/_griffe/extensions/hybrid.py index a03dcf28..1c9f18c3 100644 --- a/src/_griffe/extensions/hybrid.py +++ b/src/_griffe/extensions/hybrid.py @@ -1,23 +1,25 @@ """Deprecated. This extension provides an hybrid behavior while loading data.""" +# YORE: Bump 1.0.0: Remove module. + from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Pattern, Sequence -from griffe.agents.nodes import ObjectNode -from griffe.enumerations import When -from griffe.exceptions import ExtensionError -from griffe.extensions.base import InspectorExtension, VisitorExtension, _load_extension -from griffe.importer import dynamic_import -from griffe.logger import get_logger +from _griffe.agents.nodes.runtime import ObjectNode +from _griffe.enumerations import When +from _griffe.exceptions import ExtensionError +from _griffe.extensions.base import InspectorExtension, VisitorExtension, _load_extension +from _griffe.importer import dynamic_import +from _griffe.logger import get_logger if TYPE_CHECKING: import ast - from griffe.agents.visitor import Visitor + from _griffe.agents.visitor import Visitor -logger = get_logger(__name__) +_logger = get_logger("griffe.extensions.hybrid") class HybridExtension(VisitorExtension): @@ -34,6 +36,7 @@ class HybridExtension(VisitorExtension): """ when = When.after_all + """The moment when the extension should be executed.""" def __init__( self, @@ -60,14 +63,15 @@ def __init__( "to your extensions configuration, without using 'hybrid'.", ) self.object_paths = [re.compile(op) if isinstance(op, str) else op for op in object_paths or []] + """The list of regular expressions to match against objects paths.""" super().__init__() - def attach(self, visitor: Visitor) -> None: # noqa: D102 + def attach(self, visitor: Visitor) -> None: super().attach(visitor) for extension in self._extensions: extension.attach(visitor) # type: ignore[arg-type] # tolerate hybrid behavior - def visit(self, node: ast.AST) -> None: # noqa: D102 + def visit(self, node: ast.AST) -> None: try: just_visited = self.visitor.current.get_member(node.name) # type: ignore[attr-defined] except (KeyError, AttributeError, TypeError): @@ -88,6 +92,3 @@ def visit(self, node: ast.AST) -> None: # noqa: D102 object_node = ObjectNode(value, name=node.name, parent=parent) # type: ignore[attr-defined] for extension in self._extensions: extension.inspect(object_node) - - -__all__ = ["HybridExtension"] diff --git a/src/_griffe/finder.py b/src/_griffe/finder.py index b3db02bc..a43ea48b 100644 --- a/src/_griffe/finder.py +++ b/src/_griffe/finder.py @@ -24,23 +24,27 @@ from pathlib import Path from typing import TYPE_CHECKING, ClassVar, Iterator, Sequence, Tuple -from griffe.exceptions import UnhandledEditableModuleError -from griffe.logger import get_logger +from _griffe.exceptions import UnhandledEditableModuleError +from _griffe.logger import get_logger if TYPE_CHECKING: from typing import Pattern - from griffe.models import Module + from _griffe.models import Module +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.finder") -NamePartsType = Tuple[str, ...] -NamePartsAndPathType = Tuple[NamePartsType, Path] -logger = get_logger(__name__) _editable_editables_patterns = [re.compile(pat) for pat in (r"^__editables_\w+\.py$", r"^_editable_impl_\w+\.py$")] _editable_setuptools_patterns = [re.compile(pat) for pat in (r"^__editable__\w+\.py$",)] _editable_scikit_build_core_patterns = [re.compile(pat) for pat in (r"^_\w+_editable.py$",)] _editable_meson_python_patterns = [re.compile(pat) for pat in (r"^_\w+_editable_loader.py$",)] +NamePartsType = Tuple[str, ...] +"""Type alias for the parts of a module name.""" +NamePartsAndPathType = Tuple[NamePartsType, Path] +"""Type alias for the parts of a module name and its path.""" + def _match_pattern(string: str, patterns: Sequence[Pattern]) -> bool: return any(pattern.match(string) for pattern in patterns) @@ -80,7 +84,13 @@ class NamespacePackage: class ModuleFinder: - """The Griffe finder, allowing to find modules on the file system.""" + """The Griffe finder, allowing to find modules on the file system. + + The module finder is generally not used directly. + Each [`GriffeLoader`][griffe.GriffeLoader] instance creates its own module finder instance. + The finder can be configured when instantiating the loader + thanks to the loader [`search_paths`][griffe.GriffeLoader(search_paths)] parameter. + """ accepted_py_module_extensions: ClassVar[list[str]] = [".py", ".pyc", ".pyo", ".pyd", ".pyi", ".so"] """List of extensions supported by the finder.""" @@ -138,7 +148,7 @@ def find_spec( try_relative_path: bool = True, find_stubs_package: bool = False, ) -> tuple[str, Package | NamespacePackage]: - """Find the top module of a module. + """Find the top-level parent module of a module. If a Path is passed, only try to find the module as a file path. If a string is passed, first try to find the module as a file path, @@ -307,7 +317,7 @@ def iter_submodules( for subpath in self._filter_py_modules(path): rel_subpath = subpath.relative_to(path) if rel_subpath.parent in skip: - logger.debug(f"Skip {subpath}, another module took precedence") + _logger.debug(f"Skip {subpath}, another module took precedence") continue py_file = rel_subpath.suffix == ".py" stem = rel_subpath.stem @@ -500,9 +510,7 @@ def _handle_editable_module(path: Path) -> list[_SP]: and isinstance(node.value.args[1], ast.Constant) ): build_path = Path(node.value.args[1].value, "src") + # NOTE: What if there are multiple packages? pkg_name = next(build_path.iterdir()).name return [_SP(build_path, always_scan_for=pkg_name)] raise UnhandledEditableModuleError(path) - - -__all__ = ["ModuleFinder", "NamespacePackage", "Package"] diff --git a/src/_griffe/git.py b/src/_griffe/git.py index d91fcb7f..be6af8c0 100644 --- a/src/_griffe/git.py +++ b/src/_griffe/git.py @@ -5,30 +5,14 @@ import os import shutil import subprocess -import warnings from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory -from typing import Any, Iterator +from typing import Iterator -from griffe.exceptions import GitError +from _griffe.exceptions import GitError -WORKTREE_PREFIX = "griffe-worktree-" - - -# YORE: Bump 1.0.0: Remove block. -def __getattr__(name: str) -> Any: - if name == "load_git": - warnings.warn( - f"Importing {name} from griffe.git is deprecated. Import it from griffe.loader instead.", - DeprecationWarning, - stacklevel=2, - ) - - from griffe.loader import load_git - - return load_git - raise AttributeError +_WORKTREE_PREFIX = "griffe-worktree-" def assert_git_repo(path: str | Path) -> None: @@ -118,7 +102,7 @@ def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: """ assert_git_repo(repo) repo_name = Path(repo).resolve().name - with TemporaryDirectory(prefix=f"{WORKTREE_PREFIX}{repo_name}-{ref}-") as tmp_dir: + with TemporaryDirectory(prefix=f"{_WORKTREE_PREFIX}{repo_name}-{ref}-") as tmp_dir: branch = f"griffe_{ref}" location = os.path.join(tmp_dir, branch) process = subprocess.run( @@ -135,6 +119,3 @@ def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: subprocess.run(["git", "-C", repo, "worktree", "remove", branch], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "worktree", "prune"], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "branch", "-D", branch], stdout=subprocess.DEVNULL, check=False) - - -__all__ = ["assert_git_repo", "get_latest_tag", "get_repo_root", "tmp_worktree"] diff --git a/src/_griffe/importer.py b/src/_griffe/importer.py index 4157ea26..faf3c241 100644 --- a/src/_griffe/importer.py +++ b/src/_griffe/importer.py @@ -118,6 +118,3 @@ def dynamic_import(import_path: str, import_paths: Sequence[str | Path] | None = raise ImportError("; ".join(errors)) # noqa: B904,TRY200 return value - - -__all__ = ["dynamic_import", "sys_path"] diff --git a/src/_griffe/loader.py b/src/_griffe/loader.py index d0bc27f5..7328c3ac 100644 --- a/src/_griffe/loader.py +++ b/src/_griffe/loader.py @@ -1,13 +1,21 @@ -"""This module contains the code allowing to load modules data. +"""Griffe provides "loading" utilities. -This is the entrypoint to use griffe programatically: +In Griffe's context, loading means: -```python -from griffe.loader import GriffeLoader +- searching for a package, and finding it on the file system or as a builtin module + (see the [`ModuleFinder`][griffe.ModuleFinder] class for more information) +- extracting information from each of its (sub)modules, by either parsing + the source code (see the [`visit`][griffe.visit] function) + or inspecting the module at runtime (see the [`inspect`][griffe.inspect] function) -griffe = GriffeLoader() -fastapi = griffe.load("fastapi") -``` +The extracted information is stored in a collection of modules, which can be queried later. +Each collected module is a tree of objects, representing the structure of the module. +See the [`Module`][griffe.Module], [`Class`][griffe.Class], +[`Function`][griffe.Function], and [`Attribute`][griffe.Attribute] classes +for more information. + +The main class used to load modules is [`GriffeLoader`][griffe.GriffeLoader]. +Convenience functions [`load`][griffe.load] and [`load_git`][griffe.load_git] are also available. """ from __future__ import annotations @@ -19,25 +27,26 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Sequence, cast -from griffe.agents.inspector import inspect -from griffe.agents.visitor import visit -from griffe.collections import LinesCollection, ModulesCollection -from griffe.models import Alias, Module, Object -from griffe.enumerations import Kind -from griffe.exceptions import AliasResolutionError, CyclicAliasError, LoadingError, UnimportableModuleError -from griffe.expressions import ExprName -from griffe.extensions.base import Extensions, load_extensions -from griffe.finder import ModuleFinder, NamespacePackage, Package -from griffe.git import tmp_worktree -from griffe.importer import dynamic_import -from griffe.logger import get_logger -from griffe.merger import merge_stubs -from griffe.stats import Stats +from _griffe.agents.inspector import inspect +from _griffe.agents.visitor import visit +from _griffe.collections import LinesCollection, ModulesCollection +from _griffe.enumerations import Kind +from _griffe.exceptions import AliasResolutionError, CyclicAliasError, LoadingError, UnimportableModuleError +from _griffe.expressions import ExprName +from _griffe.extensions.base import Extensions, load_extensions +from _griffe.finder import ModuleFinder, NamespacePackage, Package +from _griffe.git import tmp_worktree +from _griffe.importer import dynamic_import +from _griffe.logger import get_logger +from _griffe.merger import merge_stubs +from _griffe.models import Alias, Module, Object +from _griffe.stats import Stats if TYPE_CHECKING: - from griffe.enumerations import Parser + from _griffe.enumerations import Parser -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.loader") _builtin_modules: set[str] = set(sys.builtin_module_names) @@ -45,6 +54,10 @@ class GriffeLoader: """The Griffe loader, allowing to load data from modules.""" ignored_modules: ClassVar[set[str]] = {"debugpy", "_pydev"} + """Special modules to ignore when loading. + + For example, `debugpy` and `_pydev` are used when debugging with VSCode and should generally never be loaded. + """ def __init__( self, @@ -105,7 +118,7 @@ def load_module( ) -> Object: """Renamed `load`. Load an object as a Griffe object, given its dotted path. - This method was renamed [`load`][griffe.loader.GriffeLoader.load]. + This method was renamed [`load`][griffe.GriffeLoader.load]. """ warnings.warn( "The `load_module` method was renamed `load`, and is deprecated.", @@ -138,10 +151,8 @@ def load( with regular methods and properties (`parent`, `members`, etc.). Examples: - >>> loader.load("griffe.models.Module") - Class("Module") - >>> loader.load("src/griffe/models.py") - Module("models") + >>> loader.load("griffe.Module") + Alias("Module", "_griffe.models.Module") Parameters: objspec: The Python path of an object, or file path to a module. @@ -177,7 +188,7 @@ def load( # We always start by searching paths on the disk, # even if inspection is forced. - logger.debug(f"Searching path(s) for {objspec}") + _logger.debug(f"Searching path(s) for {objspec}") try: obj_path, package = self.finder.find_spec( objspec, # type: ignore[arg-type] @@ -187,14 +198,14 @@ def load( except ModuleNotFoundError: # If we couldn't find paths on disk and inspection is disabled, # re-raise ModuleNotFoundError. - logger.debug(f"Could not find path for {objspec} on disk") + _logger.debug(f"Could not find path for {objspec} on disk") if not (self.allow_inspection or self.force_inspection): raise # Otherwise we try to dynamically import the top-level module. obj_path = str(objspec) top_module_name = obj_path.split(".", 1)[0] - logger.debug(f"Trying to dynamically import {top_module_name}") + _logger.debug(f"Trying to dynamically import {top_module_name}") top_module_object = dynamic_import(top_module_name, self.finder.search_paths) try: @@ -204,7 +215,7 @@ def load( except (AttributeError, ValueError): # If the top-level module has no `__path__`, we inspect it as-is, # and do not try to recurse into submodules (there shouldn't be any in builtin/compiled modules). - logger.debug(f"Module {top_module_name} has no paths set (built-in module?). Inspecting it as-is.") + _logger.debug(f"Module {top_module_name} has no paths set (built-in module?). Inspecting it as-is.") top_module = self._inspect_module(top_module_name) self.modules_collection.set_member(top_module.path, top_module) obj = self.modules_collection.get_member(obj_path) @@ -212,7 +223,7 @@ def load( return obj # We found paths, and use them to build our intermediate Package or NamespacePackage struct. - logger.debug(f"Module {top_module_name} has paths set: {top_module_path}") + _logger.debug(f"Module {top_module_name} has paths set: {top_module_path}") top_module_path = [Path(path) for path in top_module_path] if len(top_module_path) > 1: package = NamespacePackage(top_module_name, top_module_path) @@ -220,11 +231,11 @@ def load( package = Package(top_module_name, top_module_path[0]) # We have an intermediate package, and an object path: we're ready to load. - logger.debug(f"Found {objspec}: loading") + _logger.debug(f"Found {objspec}: loading") try: top_module = self._load_package(package, submodules=submodules) except LoadingError as error: - logger.exception(str(error)) # noqa: TRY401 + _logger.exception(str(error)) # noqa: TRY401 raise # Package is loaded, we now retrieve the initially requested object and return it. @@ -280,7 +291,7 @@ def resolve_aliases( ) resolved |= next_resolved unresolved |= next_unresolved - logger.debug( + _logger.debug( f"Iteration {iteration} finished, {len(resolved)} aliases resolved, still {len(unresolved)} to go", ) return unresolved, iteration @@ -305,14 +316,14 @@ def expand_exports(self, module: Module, seen: set | None = None) -> None: try: next_module = self.modules_collection.get_member(module_path) except KeyError: - logger.debug(f"Cannot expand '{export.canonical_path}', try pre-loading corresponding package") + _logger.debug(f"Cannot expand '{export.canonical_path}', try pre-loading corresponding package") continue if next_module.path not in seen: self.expand_exports(next_module, seen) try: expanded |= next_module.exports except TypeError: - logger.warning(f"Unsupported item in {module.path}.__all__: {export} (use strings only)") + _logger.warning(f"Unsupported item in {module.path}.__all__: {export} (use strings only)") # It's a string, simply add it to the current exports. else: expanded.add(export) @@ -352,14 +363,14 @@ def expand_wildcards( try: self.load(package, try_relative_path=False) except (ImportError, LoadingError) as error: - logger.debug(f"Could not expand wildcard import {member.name} in {obj.path}: {error}") + _logger.debug(f"Could not expand wildcard import {member.name} in {obj.path}: {error}") continue # Try getting the module from which every public object is imported. try: target = self.modules_collection.get_member(member.target_path) # type: ignore[union-attr] except KeyError: - logger.debug( + _logger.debug( f"Could not expand wildcard import {member.name} in {obj.path}: " f"{cast(Alias, member).target_path} not found in modules collection", ) @@ -370,7 +381,7 @@ def expand_wildcards( try: self.expand_wildcards(target, external=external, seen=seen) except (AliasResolutionError, CyclicAliasError) as error: - logger.debug(f"Could not expand wildcard import {member.name} in {obj.path}: {error}") + _logger.debug(f"Could not expand wildcard import {member.name} in {obj.path}: {error}") continue # Collect every imported object. @@ -482,16 +493,17 @@ def resolve_module_aliases( and package not in self.modules_collection ) if load_module: - logger.debug(f"Failed to resolve alias {member.path} -> {target}") + _logger.debug(f"Failed to resolve alias {member.path} -> {target}") try: self.load(package, try_relative_path=False) except (ImportError, LoadingError) as error: - logger.debug(f"Could not follow alias {member.path}: {error}") + _logger.debug(f"Could not follow alias {member.path}: {error}") load_failures.add(package) + # TODO: Immediately try again? except CyclicAliasError as error: - logger.debug(str(error)) + _logger.debug(str(error)) else: - logger.debug(f"Alias {member.path} was resolved to {member.final_target.path}") # type: ignore[union-attr] + _logger.debug(f"Alias {member.path} was resolved to {member.final_target.path}") # type: ignore[union-attr] resolved.add(member.path) # Recurse into unseen modules and classes. @@ -562,7 +574,7 @@ def _load_module_path( submodules: bool = True, parent: Module | None = None, ) -> Module: - logger.debug(f"Loading path {module_path}") + _logger.debug(f"Loading path {module_path}") if isinstance(module_path, list): module = self._create_module(module_name, module_path) elif self.force_inspection: @@ -584,7 +596,7 @@ def _load_submodules(self, module: Module) -> None: def _load_submodule(self, module: Module, subparts: tuple[str, ...], subpath: Path) -> None: for subpart in subparts: if "." in subpart: - logger.debug(f"Skip {subpath}, dots in filenames are not supported") + _logger.debug(f"Skip {subpath}, dots in filenames are not supported") return try: parent_module = self._get_or_create_parent_module(module, subparts, subpath) @@ -608,7 +620,7 @@ def _load_submodule(self, module: Module, subparts: tuple[str, ...], subpath: Pa # It works when the namespace package appears in only one search path (`sys.path`), # but will fail if it appears in multiple search paths: Python will only find the first occurrence. # It's better to not falsely suuport this, and to warn users. - logger.debug(f"{error}. Missing __init__ module?") + _logger.debug(f"{error}. Missing __init__ module?") return submodule_name = subparts[-1] try: @@ -619,12 +631,12 @@ def _load_submodule(self, module: Module, subparts: tuple[str, ...], subpath: Pa parent=parent_module, ) except LoadingError as error: - logger.debug(str(error)) + _logger.debug(str(error)) else: if submodule_name in parent_module.members: member = parent_module.members[submodule_name] if member.is_alias or not member.is_module: - logger.debug( + _logger.debug( f"Submodule '{submodule.path}' is shadowing the member at the same path. " "We recommend renaming the member or the submodule (for example prefixing it with `_`), " "see https://mkdocstrings.github.io/griffe/best_practices/#avoid-member-submodule-name-shadowing.", @@ -757,13 +769,13 @@ def load( This is a shortcut for: ```python - from griffe.loader import GriffeLoader + from griffe import GriffeLoader loader = GriffeLoader(...) module = loader.load(...) ``` - See the documentation for the loader: [`GriffeLoader`][griffe.loader.GriffeLoader]. + See the documentation for the loader: [`GriffeLoader`][griffe.GriffeLoader]. Parameters: objspec: The Python path of an object, or file path to a module. @@ -842,13 +854,13 @@ def load_git( This function will create a temporary [git worktree](https://git-scm.com/docs/git-worktree) at the requested reference - before loading `module` with [`griffe.load`][griffe.loader.load]. + before loading `module` with [`griffe.load`][griffe.load]. This function requires that the `git` executable is installed. Examples: ```python - from griffe.loader import load_git + from griffe import load_git old_api = load_git("my_module", ref="v0.1.0", repo="path/to/repo") ``` @@ -908,6 +920,3 @@ def load_git( resolve_external=resolve_external, resolve_implicit=resolve_implicit, ) - - -__all__ = ["GriffeLoader", "load", "load_git"] diff --git a/src/_griffe/logger.py b/src/_griffe/logger.py index cd0ac17a..d71de713 100644 --- a/src/_griffe/logger.py +++ b/src/_griffe/logger.py @@ -1,6 +1,6 @@ """This module contains logging utilities. -We provide the [`patch_loggers`][griffe.logger.patch_loggers] +We provide the [`patch_loggers`][griffe.patch_loggers] function so dependant libraries can patch loggers as they see fit. For example, to fit in the MkDocs logging configuration @@ -8,7 +8,7 @@ ```python import logging -from griffe.logger import patch_loggers +from griffe import patch_loggers class LoggerAdapter(logging.LoggerAdapter): @@ -90,6 +90,3 @@ def patch_loggers(get_logger_func: Callable[[str], Any]) -> None: get_logger_func: A function accepting a name as parameter and returning a logger. """ _Logger._patch_loggers(get_logger_func) - - -__all__ = ["get_logger", "LogLevel", "patch_loggers"] diff --git a/src/_griffe/merger.py b/src/_griffe/merger.py index 7bd96e0d..1bfac755 100644 --- a/src/_griffe/merger.py +++ b/src/_griffe/merger.py @@ -5,14 +5,15 @@ from contextlib import suppress from typing import TYPE_CHECKING -from griffe.exceptions import AliasResolutionError, CyclicAliasError -from griffe.logger import get_logger +from _griffe.exceptions import AliasResolutionError, CyclicAliasError +from _griffe.logger import get_logger if TYPE_CHECKING: - from griffe.models import Attribute, Class, Function, Module, Object + from _griffe.models import Attribute, Class, Function, Module, Object -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.merger") def _merge_module_stubs(module: Module, stubs: Module) -> None: @@ -68,7 +69,7 @@ def _merge_stubs_members(obj: Module | Class, stubs: Module | Class) -> None: # not the canonical one. Therefore, we must allow merging stubs into the target of an alias, # as long as the stub and target are of the same kind. if obj_member.kind is not stub_member.kind and not obj_member.is_alias: - logger.debug( + _logger.debug( f"Cannot merge stubs for {obj_member.path}: kind {stub_member.kind.value} != {obj_member.kind.value}", ) elif obj_member.is_module: @@ -97,7 +98,7 @@ def merge_stubs(mod1: Module, mod2: Module) -> Module: Returns: The regular module. """ - logger.debug(f"Trying to merge {mod1.filepath} and {mod2.filepath}") + _logger.debug(f"Trying to merge {mod1.filepath} and {mod2.filepath}") if mod1.filepath.suffix == ".pyi": # type: ignore[union-attr] stubs = mod1 module = mod2 @@ -108,6 +109,3 @@ def merge_stubs(mod1: Module, mod2: Module) -> Module: raise ValueError("cannot merge regular (non-stubs) modules together") _merge_module_stubs(module, stubs) return module - - -__all__ = ["merge_stubs"] diff --git a/src/_griffe/mixins.py b/src/_griffe/mixins.py index dcea71d8..95df759d 100644 --- a/src/_griffe/mixins.py +++ b/src/_griffe/mixins.py @@ -7,15 +7,16 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any, Sequence, TypeVar -from griffe.enumerations import Kind -from griffe.exceptions import AliasResolutionError, CyclicAliasError -from griffe.logger import get_logger -from griffe.merger import merge_stubs +from _griffe.enumerations import Kind +from _griffe.exceptions import AliasResolutionError, CyclicAliasError +from _griffe.logger import get_logger +from _griffe.merger import merge_stubs if TYPE_CHECKING: - from griffe.models import Alias, Attribute, Class, Function, Module, Object + from _griffe.models import Alias, Attribute, Class, Function, Module, Object -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Regex-replace `\.[^"]+` with `` within line. +_logger = get_logger("griffe.mixins") _ObjType = TypeVar("_ObjType") @@ -32,7 +33,12 @@ def _get_parts(key: str | Sequence[str]) -> Sequence[str]: class GetMembersMixin: - """Mixin class to share methods for accessing members.""" + """Mixin class to share methods for accessing members. + + Methods: + get_member: Get a member with its name or path. + __getitem__: Same as `get_member`, with the item syntax `[]`. + """ def __getitem__(self, key: str | Sequence[str]) -> Any: """Get a member with its name or path. @@ -79,8 +85,15 @@ def get_member(self, key: str | Sequence[str]) -> Any: return self.members[parts[0]].get_member(parts[1:]) # type: ignore[attr-defined] +# FIXME: Are `aliases` in other objects correctly updated when we delete a member? +# Would weak references be useful there? class DelMembersMixin: - """Mixin class to share methods for deleting members.""" + """Mixin class to share methods for deleting members. + + Methods: + del_member: Delete a member with its name or path. + __delitem__: Same as `del_member`, with the item syntax `[]`. + """ def __delitem__(self, key: str | Sequence[str]) -> None: """Delete a member with its name or path. @@ -135,7 +148,12 @@ def del_member(self, key: str | Sequence[str]) -> None: class SetMembersMixin: - """Mixin class to share methods for setting members.""" + """Mixin class to share methods for setting members. + + Methods: + set_member: Set a member with its name or path. + __setitem__: Same as `set_member`, with the item syntax `[]`. + """ def __setitem__(self, key: str | Sequence[str], value: Object | Alias) -> None: """Set a member with its name or path. @@ -206,7 +224,12 @@ def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None: class SerializationMixin: - """A mixin that adds de/serialization conveniences.""" + """Mixin class to share methods for de/serializing objects. + + Methods: + as_json: Return this object's data as a JSON string. + from_json: Create an instance of this class from a JSON string. + """ def as_json(self, *, full: bool = False, **kwargs: Any) -> str: """Return this object's data as a JSON string. @@ -218,7 +241,7 @@ def as_json(self, *, full: bool = False, **kwargs: Any) -> str: Returns: A JSON string. """ - from griffe.encoders import JSONEncoder # avoid circular import + from _griffe.encoders import JSONEncoder # avoid circular import return json.dumps(self, cls=JSONEncoder, full=full, **kwargs) @@ -237,7 +260,7 @@ def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType: TypeError: When the json_string does not represent and object of the class from which this classmethod has been called. """ - from griffe.encoders import json_decoder # avoid circular import + from _griffe.encoders import json_decoder # avoid circular import kwargs.setdefault("object_hook", json_decoder) obj = json.loads(json_string, **kwargs) @@ -247,7 +270,23 @@ def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType: class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin): - """A mixin for methods that appear both in objects and aliases, unchanged.""" + """Mixin class to share methods that appear both in objects and aliases, unchanged. + + Attributes: + all_members: All members (declared and inherited). + modules: The module members. + classes: The class members. + functions: The function members. + attributes: The attribute members. + is_private: Whether this object/alias is private (starts with `_`) but not special. + is_class_private: Whether this object/alias is class-private (starts with `__` and is a class member). + is_special: Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`). + is_imported: Whether this object/alias was imported from another module. + is_exported: Whether this object/alias is exported (listed in `__all__`). + is_wildcard_exposed: Whether this object/alias is exposed to wildcard imports. + is_public: Whether this object is considered public. + is_deprecated: Whether this object is deprecated. + """ @property def all_members(self) -> dict[str, Object | Alias]: @@ -342,7 +381,7 @@ def is_exported(self) -> bool: @property def is_explicitely_exported(self) -> bool: - """Deprecated. Use the [`is_exported`][griffe.mixins.ObjectAliasMixin.is_exported] property instead.""" + """Deprecated. Use the [`is_exported`][griffe.ObjectAliasMixin.is_exported] property instead.""" warnings.warn( "The `is_explicitely_exported` property is deprecated. Use `is_exported` instead.", DeprecationWarning, @@ -352,7 +391,7 @@ def is_explicitely_exported(self) -> bool: @property def is_implicitely_exported(self) -> bool: - """Deprecated. Use the [`is_exported`][griffe.mixins.ObjectAliasMixin.is_exported] property instead.""" + """Deprecated. Use the [`is_exported`][griffe.ObjectAliasMixin.is_exported] property instead.""" warnings.warn( "The `is_implicitely_exported` property is deprecated. Use `is_exported` instead.", DeprecationWarning, @@ -402,7 +441,8 @@ def is_public(self) -> bool: Therefore, to decide whether an object is public, we follow this algorithm: - If the object's `public` attribute is set (boolean), return its value. - - If the object is exposed to wildcard imports, it is public. + - If the object is listed in its parent's (a module) `__all__` attribute, it is public. + - If the parent (module) defines `__all__` and the object is not listed in, it is private. - If the object has a private name, it is private. - If the object was imported from another module, it is private. - Otherwise, the object is public. @@ -467,11 +507,3 @@ def __call__(self, *args: Any, **kwargs: Any) -> bool: # noqa: ARG002 _True = _Bool(True) # noqa: FBT003 _False = _Bool(False) # noqa: FBT003 - -__all__ = [ - "DelMembersMixin", - "GetMembersMixin", - "ObjectAliasMixin", - "SerializationMixin", - "SetMembersMixin", -] diff --git a/src/_griffe/models.py b/src/_griffe/models.py index e0e1b1b0..d7724005 100644 --- a/src/_griffe/models.py +++ b/src/_griffe/models.py @@ -1,7 +1,18 @@ -"""This module contains the data classes that represent Python objects. +"""Griffe stores information extracted from Python source code into data models. -The different objects are modules, classes, functions, and attribute -(variables like module/class/instance attributes). +These models represent trees of objects, starting with modules, +and containing classes, functions, and attributes. + +Modules can have submodules, classes, functions and attributes. +Classes can have nested classes, methods and attributes. +Functions and attributes do not have any members. + +Indirections to objects declared in other modules are represented as "aliases". +An alias therefore represents an imported object, +and behaves almost exactly like the object it points to: +it is a light wrapper around the object, +with special methods and properties +that allow to access the target's data transparently. """ from __future__ import annotations @@ -14,22 +25,23 @@ from textwrap import dedent from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union, cast -from griffe.c3linear import c3linear_merge -from griffe.docstrings.parsers import parse -from griffe.enumerations import Kind, ParameterKind, Parser -from griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError -from griffe.expressions import ExprCall, ExprName -from griffe.logger import get_logger -from griffe.mixins import ObjectAliasMixin +from _griffe.c3linear import c3linear_merge +from _griffe.docstrings.parsers import parse +from _griffe.enumerations import Kind, ParameterKind, Parser +from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError +from _griffe.expressions import ExprCall, ExprName +from _griffe.logger import get_logger +from _griffe.mixins import ObjectAliasMixin if TYPE_CHECKING: - from griffe.collections import LinesCollection, ModulesCollection - from griffe.docstrings.models import DocstringSection - from griffe.expressions import Expr + from _griffe.collections import LinesCollection, ModulesCollection + from _griffe.docstrings.models import DocstringSection + from _griffe.expressions import Expr from functools import cached_property -logger = get_logger(__name__) +# YORE: Bump 1.0.0: Replace `.dataclasses` with `` within line. +_logger = get_logger("griffe.dataclasses") class Decorator: @@ -212,6 +224,7 @@ def __repr__(self) -> str: return f"Parameter(name={self.name!r}, annotation={self.annotation!r}, kind={self.kind!r}, default={self.default!r})" def __eq__(self, __value: object) -> bool: + """Parameters are equal if all their attributes except `docstring` and `function` are equal.""" if not isinstance(__value, Parameter): return NotImplemented return ( @@ -273,17 +286,21 @@ def __repr__(self) -> str: return f"Parameters({', '.join(repr(param) for param in self._parameters_list)})" def __getitem__(self, name_or_index: int | str) -> Parameter: + """Get a parameter by index or name.""" if isinstance(name_or_index, int): return self._parameters_list[name_or_index] return self._parameters_dict[name_or_index.lstrip("*")] def __len__(self): + """The number of parameters.""" return len(self._parameters_list) def __iter__(self): + """Iterate over the parameters, in order.""" return iter(self._parameters_list) def __contains__(self, param_name: str): + """Whether a parameter with the given name is present.""" return param_name.lstrip("*") in self._parameters_dict def add(self, parameter: Parameter) -> None: @@ -308,13 +325,13 @@ class Object(ObjectAliasMixin): kind: Kind """The object kind.""" is_alias: bool = False - """Whether this object is an alias.""" + """Always false for objects.""" is_collection: bool = False - """Whether this object is a (modules) collection.""" + """Always false for objects.""" inherited: bool = False - """Whether this object (alias) is inherited. + """Always false for objects. - Objects can never be inherited, only aliases can. + Only aliases can be marked as inherited. """ def __init__( @@ -413,11 +430,13 @@ def __init__( def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r}, {self.lineno!r}, {self.endlineno!r})" + # Prevent using `__len__`. def __bool__(self) -> bool: - # Prevent using `__len__`. + """An object is always true-ish.""" return True def __len__(self) -> int: + """The number of members in this object, recursively.""" return len(self.members) + sum(len(member) for member in self.members.values()) @property @@ -433,7 +452,7 @@ def has_docstrings(self) -> bool: return any(member.has_docstrings for member in self.members.values()) def member_is_exported(self, member: Object | Alias, *, explicitely: bool = True) -> bool: # noqa: ARG002 - """Deprecated. Use [`member.is_exported`][griffe.models.Object.is_exported] instead.""" + """Deprecated. Use [`member.is_exported`][griffe.Object.is_exported] instead.""" warnings.warn( "Method `member_is_exported` is deprecated. Use `member.is_exported` instead.", DeprecationWarning, @@ -473,7 +492,7 @@ def inherited_members(self) -> dict[str, Alias]: try: mro = self.mro() except ValueError as error: - logger.debug(error) + _logger.debug(error) return {} inherited_members = {} for base in reversed(mro): @@ -574,6 +593,15 @@ def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[ def module(self) -> Module: """The parent module of this object. + Examples: + >>> import griffe + >>> markdown = griffe.load("markdown") + >>> markdown["core.Markdown.references"].module + Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py')) + >>> # The `module` of a module is itself. + >>> markdown["core"].module + Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py')) + Raises: ValueError: When the object is not a module and does not have a parent. """ @@ -585,7 +613,14 @@ def module(self) -> Module: @property def package(self) -> Module: - """The absolute top module (the package) of this object.""" + """The absolute top module (the package) of this object. + + Examples: + >>> import griffe + >>> markdown = griffe.load("markdown") + >>> markdown["core.Markdown.references"].package + Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py')) + """ module = self.module while module.parent: module = module.parent # type: ignore[assignment] # always a module @@ -593,7 +628,14 @@ def package(self) -> Module: @property def filepath(self) -> Path | list[Path]: - """The file path (or directory list for namespace packages) where this object was defined.""" + """The file path (or directory list for namespace packages) where this object was defined. + + Examples: + >>> import griffe + >>> markdown = griffe.load("markdown") + >>> markdown.filepath + PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py') + """ return self.module.filepath @property @@ -662,6 +704,12 @@ def path(self) -> str: """The dotted path of this object. On regular objects (not aliases), the path is the canonical path. + + Examples: + >>> import griffe + >>> markdown = griffe.load("markdown") + >>> markdown["core.Markdown.references"].path + 'markdown.core.Markdown.references' """ return self.canonical_path @@ -737,6 +785,9 @@ def resolve(self, name: str) -> str: Returns: The resolved name. """ + # TODO: Better match Python's own scoping rules? + # Also, maybe return regular paths instead of canonical ones? + # Name is a member this object. if name in self.members: if self.members[name].is_alias: @@ -770,7 +821,7 @@ def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: Returns: A dictionary. """ - base = { + base: dict[str, Any] = { "kind": self.kind, "name": self.name, } @@ -811,11 +862,13 @@ class Alias(ObjectAliasMixin): - the path is the alias path, not the canonical one - the name can be different from the target's - if the target can be resolved, the kind is the target's kind - - if the target cannot be resolved, the kind becomes [Kind.ALIAS][griffe.models.Kind] + - if the target cannot be resolved, the kind becomes [Kind.ALIAS][griffe.Kind] """ is_alias: bool = True + """Always true for aliases.""" is_collection: bool = False + """Always false for aliases.""" def __init__( self, @@ -878,11 +931,13 @@ def __init__( def __repr__(self) -> str: return f"Alias({self.name!r}, {self.target_path!r})" + # Prevent using `__len__`. def __bool__(self) -> bool: - # Prevent using `__len__`. + """An alias is always true-ish.""" return True def __len__(self) -> int: + """The length of an alias is always 1.""" return 1 # SPECIAL PROXIES ------------------------------- @@ -1051,7 +1106,7 @@ def aliases(self) -> dict[str, Alias]: return self.final_target.aliases def member_is_exported(self, member: Object | Alias, *, explicitely: bool = True) -> bool: # noqa: ARG002 - """Deprecated. Use [`member.is_exported`][griffe.models.Alias.is_exported] instead.""" + """Deprecated. Use [`member.is_exported`][griffe.Alias.is_exported] instead.""" warnings.warn( "Method `member_is_exported` is deprecated. Use `member.is_exported` instead.", DeprecationWarning, @@ -1435,7 +1490,7 @@ def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # no Returns: A dictionary. """ - base = { + base: dict[str, Any] = { "kind": Kind.ALIAS, "name": self.name, "target_path": self.target_path, @@ -1461,14 +1516,14 @@ def __init__(self, *args: Any, filepath: Path | list[Path] | None = None, **kwar """Initialize the module. Parameters: - *args: See [`griffe.models.Object`][]. + *args: See [`griffe.Object`][]. filepath: The module file path (directory for namespace [sub]packages, none for builtin modules). - **kwargs: See [`griffe.models.Object`][]. + **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self._filepath: Path | list[Path] | None = filepath self.overloads: dict[str, list[Function]] = defaultdict(list) - """The overloaded signature declared in this module.""" + """The overloaded signatures declared in this module.""" def __repr__(self) -> str: try: @@ -1572,10 +1627,10 @@ def __init__( """Initialize the class. Parameters: - *args: See [`griffe.models.Object`][]. + *args: See [`griffe.Object`][]. bases: The list of base classes, if any. decorators: The class decorators, if any. - **kwargs: See [`griffe.models.Object`][]. + **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.bases: list[Expr | str] = list(bases) if bases else [] @@ -1613,7 +1668,7 @@ def resolved_bases(self) -> list[Object]: if resolved_base.is_alias: resolved_base = resolved_base.final_target except (AliasResolutionError, CyclicAliasError, KeyError): - logger.debug(f"Base class {base_path} is not loaded, or not static, it cannot be resolved") + _logger.debug(f"Base class {base_path} is not loaded, or not static, it cannot be resolved") else: resolved_bases.append(resolved_base) return resolved_bases @@ -1664,11 +1719,11 @@ def __init__( """Initialize the function. Parameters: - *args: See [`griffe.models.Object`][]. + *args: See [`griffe.Object`][]. parameters: The function parameters. returns: The function return annotation. decorators: The function decorators, if any. - **kwargs: See [`griffe.models.Object`][]. + **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.parameters: Parameters = parameters or Parameters() @@ -1723,10 +1778,10 @@ def __init__( """Initialize the function. Parameters: - *args: See [`griffe.models.Object`][]. + *args: See [`griffe.Object`][]. value: The attribute value, if any. annotation: The attribute annotation, if any. - **kwargs: See [`griffe.models.Object`][]. + **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.value: str | Expr | None = value @@ -1749,20 +1804,3 @@ def as_dict(self, **kwargs: Any) -> dict[str, Any]: if self.annotation is not None: base["annotation"] = self.annotation return base - - -__all__ = [ - "Alias", - "Attribute", - "Class", - "Decorator", - "Docstring", - "Function", - "Kind", - "Module", - "Object", - "Parameter", - "ParameterKind", - "ParameterKind", - "Parameters", -] diff --git a/src/_griffe/stats.py b/src/_griffe/stats.py index 19931fec..059fa044 100644 --- a/src/_griffe/stats.py +++ b/src/_griffe/stats.py @@ -2,27 +2,15 @@ from __future__ import annotations -import warnings from collections import defaultdict from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING -from griffe.enumerations import Kind +from _griffe.enumerations import Kind if TYPE_CHECKING: - from griffe.models import Alias, Object - from griffe.loader import GriffeLoader - - -def __getattr__(name: str) -> Any: - if name == "stats": - warnings.warn( - "The 'stats' function was made into a class and renamed 'Stats'.", - DeprecationWarning, - stacklevel=2, - ) - return Stats - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + from _griffe.loader import GriffeLoader + from _griffe.models import Alias, Object class Stats: @@ -35,6 +23,8 @@ def __init__(self, loader: GriffeLoader) -> None: loader: The loader to compute stats for. """ self.loader = loader + """The loader to compute stats for.""" + modules_by_extension = defaultdict( int, { @@ -47,19 +37,35 @@ def __init__(self, loader: GriffeLoader) -> None: ".so": 0, }, ) + top_modules = loader.modules_collection.members.values() + self.by_kind = { Kind.MODULE: 0, Kind.CLASS: 0, Kind.FUNCTION: 0, Kind.ATTRIBUTE: 0, } + """Number of objects by kind.""" + self.packages = len(top_modules) + """Number of packages.""" + self.modules_by_extension = modules_by_extension + """Number of modules by extension.""" + self.lines = sum(len(lines) for lines in loader.lines_collection.values()) + """Total number of lines.""" + self.time_spent_visiting = 0 + """Time spent visiting modules.""" + self.time_spent_inspecting = 0 + """Time spent inspecting modules.""" + self.time_spent_serializing = 0 + """Time spent serializing objects.""" + for module in top_modules: self._itercount(module) @@ -148,6 +154,3 @@ def as_text(self) -> str: lines.append(f"Time spent serializing: {serialize_time}ms, {serialize_time_per_module:.02f}ms/module") return "\n".join(lines) - - -__all__ = ["Stats"] diff --git a/src/_griffe/tests.py b/src/_griffe/tests.py index ef64f76a..177c3140 100644 --- a/src/_griffe/tests.py +++ b/src/_griffe/tests.py @@ -11,23 +11,27 @@ from textwrap import dedent from typing import TYPE_CHECKING, Any, Iterator, Mapping, Sequence -from griffe.agents.inspector import inspect -from griffe.agents.visitor import visit -from griffe.collections import LinesCollection -from griffe.models import Module, Object -from griffe.loader import GriffeLoader +from _griffe.agents.inspector import inspect +from _griffe.agents.visitor import visit +from _griffe.collections import LinesCollection +from _griffe.loader import GriffeLoader +from _griffe.models import Module, Object if TYPE_CHECKING: - from griffe.collections import ModulesCollection - from griffe.enumerations import Parser - from griffe.extensions.base import Extensions + from _griffe.collections import ModulesCollection + from _griffe.enumerations import Parser + from _griffe.extensions.base import Extensions -TMPDIR_PREFIX = "griffe_" +_TMPDIR_PREFIX = "griffe_" @dataclass class TmpPackage: - """A temporary package.""" + """A temporary package. + + The `tmpdir` and `path` parameters can be passed as relative path. + They will be resolved to absolute paths after initialization. + """ tmpdir: Path """The temporary directory containing the package.""" @@ -53,7 +57,7 @@ def temporary_pyfile(code: str, *, module_name: str = "module") -> Iterator[tupl module_name: The module name, as to dynamically import it. module_path: The module path. """ - with tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX) as tmpdir: + with tempfile.TemporaryDirectory(prefix=_TMPDIR_PREFIX) as tmpdir: tmpfile = Path(tmpdir) / f"{module_name}.py" tmpfile.write_text(dedent(code)) yield module_name, tmpfile @@ -88,7 +92,7 @@ def temporary_pypackage( if isinstance(modules, list): modules = {mod: "" for mod in modules} mkdir_kwargs = {"parents": True, "exist_ok": True} - with tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX) as tmpdir: + with tempfile.TemporaryDirectory(prefix=_TMPDIR_PREFIX) as tmpdir: tmpdirpath = Path(tmpdir) package_name = ".".join(Path(package).parts) package_path = tmpdirpath / package @@ -325,16 +329,3 @@ def module_vtree(path: str, *, leaf_package: bool = True, return_leaf: bool = Fa modules[-1]._filepath = filepath return vtree(*modules, return_leaf=return_leaf) # type: ignore[return-value] - - -__all__ = [ - "htree", - "module_vtree", - "temporary_inspected_module", - "temporary_pyfile", - "temporary_pypackage", - "temporary_visited_module", - "temporary_visited_package", - "TmpPackage", - "vtree", -]