python-griffe-1.6.2/0000775000175000017500000000000014767006246014177 5ustar carstencarstenpython-griffe-1.6.2/LICENSE0000664000175000017500000000136214767006246015206 0ustar carstencarstenISC License Copyright (c) 2021, Timothée Mazzucotelli Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. python-griffe-1.6.2/src/0000775000175000017500000000000014767006246014766 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/0000775000175000017500000000000014767006246016367 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/debug.py0000664000175000017500000000570014767006246020031 0ustar carstencarsten# This module is here to help users report bugs. # It provides a function to print environment information, # which is called from the public `griffe.debug` module # (when called with `python -m griffe.debug`) # or thanks to the `--debug-info` CLI flag. from __future__ import annotations import os import platform import sys from dataclasses import dataclass from importlib import metadata @dataclass class _Variable: """Dataclass describing an environment variable.""" name: str """Variable name.""" value: str """Variable value.""" @dataclass class _Package: """Dataclass describing a Python package.""" name: str """Package name.""" version: str """Package version.""" @dataclass class _Environment: """Dataclass to store environment information.""" interpreter_name: str """Python interpreter name.""" interpreter_version: str """Python interpreter version.""" interpreter_path: str """Path to Python executable.""" platform: str """Operating System.""" packages: list[_Package] """Installed packages.""" variables: list[_Variable] """Environment variables.""" def _interpreter_name_version() -> tuple[str, str]: if hasattr(sys, "implementation"): impl = sys.implementation.version version = f"{impl.major}.{impl.minor}.{impl.micro}" kind = impl.releaselevel if kind != "final": version += kind[0] + str(impl.serial) return sys.implementation.name, version return "", "0.0.0" def _get_version(dist: str = "griffe") -> str: """Get version of the given distribution. Parameters: dist: A distribution name. Returns: A version number. """ try: return metadata.version(dist) except metadata.PackageNotFoundError: return "0.0.0" def _get_debug_info() -> _Environment: """Get debug/environment information. Returns: Environment information. """ py_name, py_version = _interpreter_name_version() packages = ["griffe"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("GRIFFE")]] 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], ) def _print_debug_info() -> None: """Print debug/environment information.""" info = _get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") for var in info.variables: print(f" - `{var.name}`: `{var.value}`") print("- __Installed packages__:") for pkg in info.packages: print(f" - `{pkg.name}` v{pkg.version}") python-griffe-1.6.2/src/_griffe/enumerations.py0000664000175000017500000001266714767006246021466 0ustar carstencarsten# This module contains all the enumerations of the package. from __future__ import annotations from enum import Enum class LogLevel(str, Enum): """Enumeration of available log levels.""" trace = "trace" """The TRACE log level.""" debug = "debug" """The DEBUG log level.""" info = "info" """The INFO log level.""" success = "success" """The SUCCESS log level.""" warning = "warning" """The WARNING log level.""" error = "error" """The ERROR log level.""" critical = "critical" """The CRITICAL log level.""" class DocstringSectionKind(str, Enum): """Enumeration of the possible docstring section kinds.""" text = "text" """Text section.""" parameters = "parameters" """Parameters section.""" other_parameters = "other parameters" """Other parameters (keyword arguments) section.""" raises = "raises" """Raises (exceptions) section.""" warns = "warns" """Warnings section.""" returns = "returns" """Returned value(s) section.""" yields = "yields" """Yielded value(s) (generators) section.""" receives = "receives" """Received value(s) (generators) section.""" examples = "examples" """Examples section.""" attributes = "attributes" """Attributes section.""" functions = "functions" """Functions section.""" classes = "classes" """Classes section.""" modules = "modules" """Modules section.""" deprecated = "deprecated" """Deprecation section.""" admonition = "admonition" """Admonition block.""" class ParameterKind(str, Enum): """Enumeration of the different parameter kinds.""" positional_only = "positional-only" """Positional-only parameter.""" positional_or_keyword = "positional or keyword" """Positional or keyword parameter.""" var_positional = "variadic positional" """Variadic positional parameter.""" keyword_only = "keyword-only" """Keyword-only parameter.""" var_keyword = "variadic keyword" """Variadic keyword parameter.""" class Kind(str, Enum): """Enumeration of the different object kinds.""" MODULE = "module" """Modules.""" CLASS = "class" """Classes.""" FUNCTION = "function" """Functions and methods.""" ATTRIBUTE = "attribute" """Attributes and properties.""" ALIAS = "alias" """Aliases (imported objects).""" class ExplanationStyle(str, Enum): """Enumeration of the possible styles for explanations.""" ONE_LINE = "oneline" """Explanations on one-line.""" VERBOSE = "verbose" """Explanations on multiple lines.""" MARKDOWN = "markdown" """Explanations in Markdown, adapted to changelogs.""" GITHUB = "github" """Explanation as GitHub workflow commands warnings, adapted to CI.""" class BreakageKind(str, Enum): """Enumeration of the possible API breakages.""" PARAMETER_MOVED = "Positional parameter was moved" """Positional parameter was moved""" PARAMETER_REMOVED = "Parameter was removed" """Parameter was removed""" PARAMETER_CHANGED_KIND = "Parameter kind was changed" """Parameter kind was changed""" PARAMETER_CHANGED_DEFAULT = "Parameter default was changed" """Parameter default was changed""" PARAMETER_CHANGED_REQUIRED = "Parameter is now required" """Parameter is now required""" PARAMETER_ADDED_REQUIRED = "Parameter was added as required" """Parameter was added as required""" RETURN_CHANGED_TYPE = "Return types are incompatible" """Return types are incompatible""" OBJECT_REMOVED = "Public object was removed" """Public object was removed""" OBJECT_CHANGED_KIND = "Public object points to a different kind of object" """Public object points to a different kind of object""" ATTRIBUTE_CHANGED_TYPE = "Attribute types are incompatible" """Attribute types are incompatible""" ATTRIBUTE_CHANGED_VALUE = "Attribute value was changed" """Attribute value was changed""" CLASS_REMOVED_BASE = "Base class was removed" """Base class was removed""" class Parser(str, Enum): """Enumeration of the different docstring parsers.""" auto = "auto" """Infer docstring parser. [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.3.0](../../../insiders/changelog.md#1.3.0). """ google = "google" """Google-style docstrings parser.""" sphinx = "sphinx" """Sphinx-style docstrings parser.""" numpy = "numpy" """Numpydoc-style docstrings parser.""" class ObjectKind(str, Enum): """Enumeration of the different runtime object kinds.""" MODULE = "module" """Modules.""" CLASS = "class" """Classes.""" STATICMETHOD = "staticmethod" """Static methods.""" CLASSMETHOD = "classmethod" """Class methods.""" METHOD_DESCRIPTOR = "method_descriptor" """Method descriptors.""" METHOD = "method" """Methods.""" BUILTIN_METHOD = "builtin_method" """Built-in methods.""" COROUTINE = "coroutine" """Coroutines""" FUNCTION = "function" """Functions.""" BUILTIN_FUNCTION = "builtin_function" """Built-in functions.""" CACHED_PROPERTY = "cached_property" """Cached properties.""" GETSET_DESCRIPTOR = "getset_descriptor" """Get/set descriptors.""" PROPERTY = "property" """Properties.""" ATTRIBUTE = "attribute" """Attributes.""" def __str__(self) -> str: return self.value python-griffe-1.6.2/src/_griffe/loader.py0000664000175000017500000012636414767006246020223 0ustar carstencarsten# This module contains all the logic for loading API data from sources or compiled modules. from __future__ import annotations from contextlib import suppress from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, cast 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 logger from _griffe.merger import merge_stubs from _griffe.models import Alias, Module, Object from _griffe.stats import Stats if TYPE_CHECKING: from collections.abc import Sequence from _griffe.docstrings.parsers import DocstringStyle from _griffe.enumerations import Parser 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, *, extensions: Extensions | None = None, search_paths: Sequence[str | Path] | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, allow_inspection: bool = True, force_inspection: bool = False, store_source: bool = True, ) -> None: """Initialize the loader. Parameters: extensions: The extensions to use. search_paths: The paths to search into. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. store_source: Whether to store code source in the lines collection. """ self.extensions: Extensions = extensions or load_extensions() """Loaded Griffe extensions.""" self.docstring_parser: DocstringStyle | Parser | None = docstring_parser """Selected docstring parser.""" self.docstring_options: dict[str, Any] = docstring_options or {} """Configured parsing options.""" self.lines_collection: LinesCollection = lines_collection or LinesCollection() """Collection of source code lines.""" self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() """Collection of modules.""" self.allow_inspection: bool = allow_inspection """Whether to allow inspecting (importing) modules for which we can't find sources.""" self.force_inspection: bool = force_inspection """Whether to force inspecting (importing) modules, even when sources were found.""" self.store_source: bool = store_source """Whether to store source code in the lines collection.""" self.finder: ModuleFinder = ModuleFinder(search_paths) """The module source finder.""" self._time_stats: dict = { "time_spent_visiting": 0, "time_spent_inspecting": 0, } def load( self, objspec: str | Path | None = None, /, *, submodules: bool = True, try_relative_path: bool = True, find_stubs_package: bool = False, ) -> Object | Alias: """Load an object as a Griffe object, given its Python or file path. Note that this will load the whole object's package, and return only the specified object. The rest of the package can be accessed from the returned object with regular methods and properties (`parent`, `members`, etc.). Examples: >>> loader.load("griffe.Module") Alias("Module", "_griffe.models.Module") Parameters: objspec: The Python path of an object, or file path to a module. submodules: Whether to recurse on the submodules. This parameter only makes sense when loading a package (top-level module). try_relative_path: Whether to try finding the module as a relative path. find_stubs_package: Whether to search for stubs-only package. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. Raises: LoadingError: When loading a module failed for various reasons. ModuleNotFoundError: When a module was not found and inspection is disallowed. Returns: A Griffe object. """ obj_path: str package = None top_module = None # We always start by searching paths on the disk, # even if inspection is forced. logger.debug("Searching path(s) for %s", objspec) try: obj_path, package = self.finder.find_spec( objspec, # type: ignore[arg-type] try_relative_path=try_relative_path, find_stubs_package=find_stubs_package, ) except ModuleNotFoundError: # If we couldn't find paths on disk and inspection is disabled, # re-raise ModuleNotFoundError. logger.debug("Could not find path for %s on disk", objspec) 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("Trying to dynamically import %s", top_module_name) top_module_object = dynamic_import(top_module_name, self.finder.search_paths) try: top_module_path = top_module_object.__path__ if not top_module_path: raise ValueError(f"Module {top_module_name} has no paths set") # noqa: TRY301 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("Module %s has no paths set (built-in module?). Inspecting it as-is.", top_module_name) top_module = self._inspect_module(top_module_name) self.modules_collection.set_member(top_module.path, top_module) return self._post_load(top_module, obj_path) # We found paths, and use them to build our intermediate Package or NamespacePackage struct. logger.debug("Module %s has paths set: %s", top_module_name, 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) else: 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("Found %s: loading", objspec) try: top_module = self._load_package(package, submodules=submodules) except LoadingError: logger.exception("Could not load package %s", package) raise return self._post_load(top_module, obj_path) def _post_load(self, module: Module, obj_path: str) -> Object | Alias: # Pre-emptively expand exports (`__all__` values), # as well as wildcard imports (without ever loading additional packages). # This is a best-effort to return the most correct API data # before firing the `on_package_loaded` event. # # Packages that wildcard imports from external, non-loaded packages # will still have incomplete data, requiring subsequent calls to # `load()` and/or `resolve_aliases()`. self.expand_exports(module) self.expand_wildcards(module, external=False) # Package is loaded, we now retrieve the initially requested object and return it. obj = self.modules_collection.get_member(obj_path) self.extensions.call("on_package_loaded", pkg=module, loader=self) return obj def resolve_aliases( self, *, implicit: bool = False, external: bool | None = None, max_iterations: int | None = None, ) -> tuple[set[str], int]: """Resolve aliases. Parameters: implicit: When false, only try to resolve an alias if it is explicitly exported. external: When false, don't try to load unspecified modules to resolve aliases. max_iterations: Maximum number of iterations on the loader modules collection. Returns: The unresolved aliases and the number of iterations done. """ if max_iterations is None: max_iterations = float("inf") # type: ignore[assignment] prev_unresolved: set[str] = set() unresolved: set[str] = set("0") # Init to enter loop. iteration = 0 collection = self.modules_collection.members # Before resolving aliases, we try to expand wildcard imports again # (this was already done in `_post_load()`), # this time with the user-configured `external` setting, # and with potentially more packages loaded in the collection, # allowing to resolve more aliases. for wildcards_module in list(collection.values()): self.expand_wildcards(wildcards_module, external=external) load_failures: set[str] = set() while unresolved and unresolved != prev_unresolved and iteration < max_iterations: # type: ignore[operator] prev_unresolved = unresolved - {"0"} unresolved = set() resolved: set[str] = set() iteration += 1 for module_name in list(collection.keys()): module = collection[module_name] next_resolved, next_unresolved = self.resolve_module_aliases( module, implicit=implicit, external=external, load_failures=load_failures, ) resolved |= next_resolved unresolved |= next_unresolved logger.debug( "Iteration %s finished, %s aliases resolved, still %s to go", iteration, len(resolved), len(unresolved), ) return unresolved, iteration def expand_exports(self, module: Module, seen: set | None = None) -> None: """Expand exports: try to recursively expand all module exports (`__all__` values). See also: [`Module.exports`][griffe.Module.exports]. Parameters: module: The module to recurse on. seen: Used to avoid infinite recursion. """ seen = seen or set() seen.add(module.path) if module.exports is None: return expanded = [] for export in module.exports: # It's a name: we resolve it, get the module it comes from, # recurse into it, and add its exports to the current ones. if isinstance(export, ExprName): module_path = export.canonical_path.rsplit(".", 1)[0] # Remove trailing `.__all__`. try: next_module = self.modules_collection.get_member(module_path) except KeyError: logger.debug("Cannot expand '%s', try pre-loading corresponding package", export.canonical_path) continue if next_module.path not in seen: self.expand_exports(next_module, seen) try: expanded += [export for export in next_module.exports if export not in expanded] except TypeError: logger.warning("Unsupported item in %s.__all__: %s (use strings only)", module.path, export) # It's a string, simply add it to the current exports. else: expanded.append(export) module.exports = expanded # Make sure to expand exports in all modules. for submodule in module.modules.values(): if not submodule.is_alias and submodule.path not in seen: self.expand_exports(submodule, seen) def expand_wildcards( self, obj: Object, *, external: bool | None = None, seen: set | None = None, ) -> None: """Expand wildcards: try to recursively expand all found wildcards. See also: [`Alias.wildcard`][griffe.Alias.wildcard]. Parameters: obj: The object and its members to recurse on. external: When true, try to load unspecified modules to expand wildcards. seen: Used to avoid infinite recursion. """ expanded = [] to_remove = [] seen = seen or set() seen.add(obj.path) # First we expand wildcard imports and store the objects in a temporary `expanded` variable, # while also keeping track of the members representing wildcard import, to remove them later. for member in obj.members.values(): # Handle a wildcard. if member.is_alias and member.wildcard: # type: ignore[union-attr] package = member.wildcard.split(".", 1)[0] # type: ignore[union-attr] not_loaded = obj.package.path != package and package not in self.modules_collection # Try loading the (unknown) package containing the wildcard importe module (if allowed to). if not_loaded: if external is False or (external is None and package != f"_{obj.package.name}"): continue try: self.load(package, try_relative_path=False) except (ImportError, LoadingError) as error: logger.debug("Could not expand wildcard import %s in %s: %s", member.name, 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( "Could not expand wildcard import %s in %s: %s not found in modules collection", member.name, obj.path, cast("Alias", member).target_path, ) continue # Recurse into this module, expanding wildcards there before collecting everything. if target.path not in seen: try: self.expand_wildcards(target, external=external, seen=seen) except (AliasResolutionError, CyclicAliasError) as error: logger.debug("Could not expand wildcard import %s in %s: %s", member.name, obj.path, error) continue # Collect every imported object. expanded.extend(self._expand_wildcard(member)) # type: ignore[arg-type] to_remove.append(member.name) # Recurse in unseen submodules. elif not member.is_alias and member.is_module and member.path not in seen: self.expand_wildcards(member, external=external, seen=seen) # type: ignore[arg-type] # Then we remove the members representing wildcard imports. for name in to_remove: obj.del_member(name) # Finally we process the collected objects. for new_member, alias_lineno, alias_endlineno in expanded: overwrite = False already_present = new_member.name in obj.members self_alias = ( new_member.is_alias and cast("Alias", new_member).target_path == f"{obj.path}.{new_member.name}" ) # If a member with the same name is already present in the current object, # we only overwrite it if the alias is imported lower in the module # (meaning that the alias takes precedence at runtime). if already_present: old_member = obj.get_member(new_member.name) old_lineno = old_member.alias_lineno if old_member.is_alias else old_member.lineno overwrite = alias_lineno > (old_lineno or 0) # type: ignore[operator] # 1. If the expanded member is an alias with a target path equal to its own path, we stop. # This situation can arise because of Griffe's mishandling of (abusive) wildcard imports. # We have yet to check how Python handles this itself, or if there's an algorithm # that we could follow to untangle back-and-forth wildcard imports. # 2. If the expanded member was already present and we decided not to overwrite it, we stop. # 3. Otherwise we proceed further. if not self_alias and (not already_present or overwrite): alias = Alias( new_member.name, new_member, lineno=alias_lineno, endlineno=alias_endlineno, parent=obj, # type: ignore[arg-type] ) # Special case: we avoid overwriting a submodule with an alias pointing to it. # Griffe suffers from this design flaw where an object cannot store both # a submodule and a member of the same name, while this poses (almost) no issue in Python. # We at least prevent this case where a submodule is overwritten by an imported version of itself. if already_present: prev_member = obj.get_member(new_member.name) with suppress(AliasResolutionError, CyclicAliasError): if prev_member.is_module: if prev_member.is_alias: prev_member = prev_member.final_target if alias.final_target is prev_member: # Alias named after the module it targets: skip to avoid cyclic aliases. continue # Everything went right (supposedly), we add the alias as a member of the current object. obj.set_member(new_member.name, alias) self.extensions.call("on_wildcard_expansion", alias=alias, loader=self) def resolve_module_aliases( self, obj: Object | Alias, *, implicit: bool = False, external: bool | None = None, seen: set[str] | None = None, load_failures: set[str] | None = None, ) -> tuple[set[str], set[str]]: """Follow aliases: try to recursively resolve all found aliases. Parameters: obj: The object and its members to recurse on. implicit: When false, only try to resolve an alias if it is explicitly exported. external: When false, don't try to load unspecified modules to resolve aliases. seen: Used to avoid infinite recursion. load_failures: Set of external packages we failed to load (to prevent retries). Returns: Both sets of resolved and unresolved aliases. """ resolved = set() unresolved = set() if load_failures is None: load_failures = set() seen = seen or set() seen.add(obj.path) for member in obj.members.values(): # Handle aliases. if member.is_alias: if member.wildcard or member.resolved: # type: ignore[union-attr] continue if not implicit and not member.is_exported: continue # Try resolving the alias. If it fails, check if it is because it comes # from an external package, and decide if we should load that package # to allow the alias to be resolved at the next iteration (maybe). try: member.resolve_target() # type: ignore[union-attr] except AliasResolutionError as error: target = error.alias.target_path unresolved.add(member.path) package = target.split(".", 1)[0] load_module = ( (external is True or (external is None and package == f"_{obj.package.name}")) and package not in load_failures and obj.package.path != package and package not in self.modules_collection ) if load_module: logger.debug("Failed to resolve alias %s -> %s", member.path, target) try: self.load(package, try_relative_path=False) except (ImportError, LoadingError) as error: logger.debug("Could not follow alias %s: %s", member.path, error) load_failures.add(package) except CyclicAliasError as error: logger.debug(str(error)) else: logger.debug("Alias %s was resolved to %s", member.path, member.final_target.path) # type: ignore[union-attr] resolved.add(member.path) # Recurse into unseen modules and classes. elif member.kind in {Kind.MODULE, Kind.CLASS} and member.path not in seen: sub_resolved, sub_unresolved = self.resolve_module_aliases( member, implicit=implicit, external=external, seen=seen, load_failures=load_failures, ) resolved |= sub_resolved unresolved |= sub_unresolved return resolved, unresolved def stats(self) -> Stats: """Compute some statistics. Returns: Some statistics. """ stats = Stats(self) stats.time_spent_visiting = self._time_stats["time_spent_visiting"] stats.time_spent_inspecting = self._time_stats["time_spent_inspecting"] return stats def _load_package(self, package: Package | NamespacePackage, *, submodules: bool = True) -> Module: top_module = self._load_module(package.name, package.path, submodules=submodules) if isinstance(package, NamespacePackage): return top_module if package.stubs: self.expand_wildcards(top_module) # If stubs are in the package itself, they have been merged while loading modules, # so only the top-level init module needs to be merged still. # If stubs are in another package (a stubs-only package), # then we need to load the entire stubs package to merge everything. submodules = submodules and package.stubs.parent != package.path.parent stubs = self._load_module(package.name, package.stubs, submodules=submodules) return merge_stubs(top_module, stubs) return top_module def _load_module( self, module_name: str, module_path: Path | list[Path], *, submodules: bool = True, parent: Module | None = None, ) -> Module: try: return self._load_module_path(module_name, module_path, submodules=submodules, parent=parent) except SyntaxError as error: raise LoadingError(f"Syntax error: {error}") from error except ImportError as error: raise LoadingError(f"Import error: {error}") from error except UnicodeDecodeError as error: raise LoadingError(f"UnicodeDecodeError when loading {module_path}: {error}") from error except OSError as error: raise LoadingError(f"OSError when loading {module_path}: {error}") from error def _load_module_path( self, module_name: str, module_path: Path | list[Path], *, submodules: bool = True, parent: Module | None = None, ) -> Module: logger.debug("Loading path %s", module_path) if isinstance(module_path, list): module = self._create_module(module_name, module_path) elif self.force_inspection: module = self._inspect_module(module_name, module_path, parent) elif module_path.suffix in {".py", ".pyi"}: module = self._visit_module(module_name, module_path, parent) elif self.allow_inspection: module = self._inspect_module(module_name, module_path, parent) else: raise LoadingError("Cannot load compiled module without inspection") if parent is None: self.modules_collection.set_member(module.path, module) if submodules: self._load_submodules(module) return module def _load_submodules(self, module: Module) -> None: for subparts, subpath in self.finder.submodules(module): self._load_submodule(module, subparts, subpath) def _load_submodule(self, module: Module, subparts: tuple[str, ...], subpath: Path) -> None: for subpart in subparts: if "." in subpart: logger.debug("Skip %s, dots in filenames are not supported", subpath) return try: parent_module = self._get_or_create_parent_module(module, subparts, subpath) except UnimportableModuleError as error: # NOTE: Why don't we load submodules when there's no init module in their folder? # Usually when a folder with Python files does not have an __init__.py module, # it's because the Python files are scripts that should never be imported. # Django has manage.py somewhere for example, in a folder without init module. # This script isn't part of the Python API, as it's meant to be called on the CLI exclusively # (at least it was the case a few years ago when I was still using Django). # The other case when there's no init module is when a package is a native namespace package (PEP 420). # It does not make sense to have a native namespace package inside of a regular package (having init modules above), # because the regular package above blocks the namespace feature from happening, so I consider it a user error. # It's true that users could have a native namespace package inside of a pkg_resources-style namespace package, # but I've never seen this happen. # It's also true that Python can actually import the module under the (wrongly declared) native namespace package, # so the Griffe debug log message is a bit misleading, # but that's because in that case Python acts like the whole tree is a regular package. # 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 support this, and to warn users. logger.debug("%s. Missing __init__ module?", error) return submodule_name = subparts[-1] try: submodule = self._load_module( submodule_name, subpath, submodules=False, parent=parent_module, ) except LoadingError as 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( "Submodule '%s' 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.", submodule.path, ) parent_module.set_member(submodule_name, submodule) def _create_module(self, module_name: str, module_path: Path | list[Path]) -> Module: return Module( module_name, filepath=module_path, lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) def _visit_module(self, module_name: str, module_path: Path, parent: Module | None = None) -> Module: code = module_path.read_text(encoding="utf8") if self.store_source: self.lines_collection[module_path] = code.splitlines(keepends=False) start = datetime.now(tz=timezone.utc) module = visit( module_name, filepath=module_path, code=code, extensions=self.extensions, parent=parent, docstring_parser=self.docstring_parser, docstring_options=self.docstring_options, lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) elapsed = datetime.now(tz=timezone.utc) - start self._time_stats["time_spent_visiting"] += elapsed.microseconds return module def _inspect_module(self, module_name: str, filepath: Path | None = None, parent: Module | None = None) -> Module: for prefix in self.ignored_modules: if module_name.startswith(prefix): raise ImportError(f"Ignored module '{module_name}'") if self.store_source and filepath and filepath.suffix in {".py", ".pyi"}: self.lines_collection[filepath] = filepath.read_text(encoding="utf8").splitlines(keepends=False) start = datetime.now(tz=timezone.utc) try: module = inspect( module_name, filepath=filepath, import_paths=self.finder.search_paths, extensions=self.extensions, parent=parent, docstring_parser=self.docstring_parser, docstring_options=self.docstring_options, lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) except SystemExit as error: raise ImportError(f"Importing '{module_name}' raised a system exit") from error elapsed = datetime.now(tz=timezone.utc) - start self._time_stats["time_spent_inspecting"] += elapsed.microseconds return module def _get_or_create_parent_module( self, module: Module, subparts: tuple[str, ...], subpath: Path, ) -> Module: parent_parts = subparts[:-1] if not parent_parts: return module parent_module = module parents = list(subpath.parents) if subpath.stem == "__init__": parents.pop(0) for parent_offset, parent_part in enumerate(parent_parts, 2): module_filepath = parents[len(subparts) - parent_offset] try: parent_module = parent_module.get_member(parent_part) except KeyError as error: if parent_module.is_namespace_package or parent_module.is_namespace_subpackage: next_parent_module = self._create_module(parent_part, [module_filepath]) parent_module.set_member(parent_part, next_parent_module) parent_module = next_parent_module else: raise UnimportableModuleError(f"Skip {subpath}, it is not importable") from error else: parent_namespace = parent_module.is_namespace_package or parent_module.is_namespace_subpackage if parent_namespace and module_filepath not in parent_module.filepath: # type: ignore[operator] parent_module.filepath.append(module_filepath) # type: ignore[union-attr] return parent_module def _expand_wildcard(self, wildcard_obj: Alias) -> list[tuple[Object | Alias, int | None, int | None]]: module = self.modules_collection.get_member(wildcard_obj.wildcard) # type: ignore[arg-type] return [ (imported_member, wildcard_obj.alias_lineno, wildcard_obj.alias_endlineno) for imported_member in module.members.values() if imported_member.is_wildcard_exposed ] def load( objspec: str | Path | None = None, /, *, submodules: bool = True, try_relative_path: bool = True, extensions: Extensions | None = None, search_paths: Sequence[str | Path] | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, allow_inspection: bool = True, force_inspection: bool = False, store_source: bool = True, find_stubs_package: bool = False, resolve_aliases: bool = False, resolve_external: bool | None = None, resolve_implicit: bool = False, ) -> Object | Alias: """Load and return a Griffe object. In Griffe's context, loading means: - 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) 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 like this one and [`load_git`][griffe.load_git] are also available. Example: ```python import griffe module = griffe.load(...) ``` This is a shortcut for: ```python from griffe import GriffeLoader loader = GriffeLoader(...) module = loader.load(...) ``` See the documentation for the loader: [`GriffeLoader`][griffe.GriffeLoader]. Parameters: objspec: The Python path of an object, or file path to a module. submodules: Whether to recurse on the submodules. This parameter only makes sense when loading a package (top-level module). try_relative_path: Whether to try finding the module as a relative path. extensions: The extensions to use. search_paths: The paths to search into. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. force_inspection: Whether to force using dynamic analysis when loading data. store_source: Whether to store code source in the lines collection. find_stubs_package: Whether to search for stubs-only package. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. resolve_aliases: Whether to resolve aliases. resolve_external: Whether to try to load unspecified modules to resolve aliases. Default value (`None`) means to load external modules only if they are the private sibling or the origin module (for example when `ast` imports from `_ast`). resolve_implicit: When false, only try to resolve an alias if it is explicitly exported. Returns: A Griffe object. """ loader = GriffeLoader( extensions=extensions, search_paths=search_paths, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, allow_inspection=allow_inspection, force_inspection=force_inspection, store_source=store_source, ) result = loader.load( objspec, submodules=submodules, try_relative_path=try_relative_path, find_stubs_package=find_stubs_package, ) if resolve_aliases: loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external) return result def load_git( objspec: str | Path | None = None, /, *, ref: str = "HEAD", repo: str | Path = ".", submodules: bool = True, extensions: Extensions | None = None, search_paths: Sequence[str | Path] | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, allow_inspection: bool = True, force_inspection: bool = False, find_stubs_package: bool = False, resolve_aliases: bool = False, resolve_external: bool | None = None, resolve_implicit: bool = False, ) -> Object | Alias: """Load and return a module from a specific Git reference. 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.load]. This function requires that the `git` executable is installed. Examples: ```python from griffe import load_git old_api = load_git("my_module", ref="v0.1.0", repo="path/to/repo") ``` Parameters: objspec: The Python path of an object, or file path to a module. ref: A Git reference such as a commit, tag or branch. repo: Path to the repository (i.e. the directory *containing* the `.git` directory) submodules: Whether to recurse on the submodules. This parameter only makes sense when loading a package (top-level module). extensions: The extensions to use. search_paths: The paths to search into (relative to the repository root). docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. force_inspection: Whether to force using dynamic analysis when loading data. find_stubs_package: Whether to search for stubs-only package. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. resolve_aliases: Whether to resolve aliases. resolve_external: Whether to try to load unspecified modules to resolve aliases. Default value (`None`) means to load external modules only if they are the private sibling or the origin module (for example when `ast` imports from `_ast`). resolve_implicit: When false, only try to resolve an alias if it is explicitly exported. Returns: A Griffe object. """ with tmp_worktree(repo, ref) as worktree: search_paths = [worktree / path for path in search_paths or ["."]] if isinstance(objspec, Path): objspec = worktree / objspec return load( objspec, submodules=submodules, try_relative_path=False, extensions=extensions, search_paths=search_paths, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, allow_inspection=allow_inspection, force_inspection=force_inspection, find_stubs_package=find_stubs_package, resolve_aliases=resolve_aliases, resolve_external=resolve_external, resolve_implicit=resolve_implicit, ) def load_pypi( package: str, # noqa: ARG001 distribution: str, # noqa: ARG001 version_spec: str, # noqa: ARG001 *, submodules: bool = True, # noqa: ARG001 extensions: Extensions | None = None, # noqa: ARG001 search_paths: Sequence[str | Path] | None = None, # noqa: ARG001 docstring_parser: DocstringStyle | Parser | None = None, # noqa: ARG001 docstring_options: dict[str, Any] | None = None, # noqa: ARG001 lines_collection: LinesCollection | None = None, # noqa: ARG001 modules_collection: ModulesCollection | None = None, # noqa: ARG001 allow_inspection: bool = True, # noqa: ARG001 force_inspection: bool = False, # noqa: ARG001 find_stubs_package: bool = False, # noqa: ARG001 ) -> Object | Alias: """Load and return a module from a specific package version downloaded using pip. [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.1.0](../../insiders/changelog.md#1.1.0). Parameters: package: The package import name. distribution: The distribution name. version_spec: The version specifier to use when installing with pip. submodules: Whether to recurse on the submodules. This parameter only makes sense when loading a package (top-level module). extensions: The extensions to use. search_paths: The paths to search into (relative to the repository root). docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. force_inspection: Whether to force using dynamic analysis when loading data. find_stubs_package: Whether to search for stubs-only package. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. """ raise ValueError("Not available in non-Insiders versions of Griffe") python-griffe-1.6.2/src/_griffe/expressions.py0000664000175000017500000012076114767006246021332 0ustar carstencarsten# This module contains the data classes that represent resolvable names and expressions. # First we declare data classes for each kind of expression, mostly corresponding to Python's AST nodes. # Then we declare builder methods, that iterate AST nodes and build the corresponding data classes, # and two utilities `_yield` and `_join` to help iterate on expressions. # Finally we declare a few public helpers to safely get expressions from AST nodes in different scenarios. from __future__ import annotations import ast import sys from dataclasses import dataclass from dataclasses import fields as getfields from functools import partial from itertools import zip_longest from typing import TYPE_CHECKING, Any, Callable from _griffe.agents.nodes.parameters import get_parameters from _griffe.enumerations import LogLevel, ParameterKind from _griffe.exceptions import NameResolutionError from _griffe.logger import logger if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence from pathlib import Path from _griffe.models import Class, Module def _yield(element: str | Expr | tuple[str | Expr, ...], *, flat: bool = True) -> Iterator[str | Expr]: if isinstance(element, str): yield element elif isinstance(element, tuple): for elem in element: yield from _yield(elem, flat=flat) elif flat: yield from element.iterate(flat=True) else: yield element def _join( elements: Iterable[str | Expr | tuple[str | Expr, ...]], joint: str | Expr, *, flat: bool = True, ) -> Iterator[str | Expr]: it = iter(elements) try: yield from _yield(next(it), flat=flat) except StopIteration: return for element in it: yield from _yield(joint, flat=flat) yield from _yield(element, flat=flat) def _field_as_dict( element: str | bool | Expr | list[str | Expr] | None, **kwargs: Any, ) -> str | bool | None | list | dict: if isinstance(element, Expr): return _expr_as_dict(element, **kwargs) if isinstance(element, list): return [_field_as_dict(elem, **kwargs) for elem in element] return element def _expr_as_dict(expression: Expr, **kwargs: Any) -> dict[str, Any]: fields = { field.name: _field_as_dict(getattr(expression, field.name), **kwargs) for field in sorted(getfields(expression), key=lambda f: f.name) if field.name != "parent" } fields["cls"] = expression.classname return fields # YORE: EOL 3.9: Remove block. _dataclass_opts: dict[str, bool] = {} if sys.version_info >= (3, 10): _dataclass_opts["slots"] = True @dataclass class Expr: """Base class for expressions.""" 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 """Iterate on the expression elements. Parameters: flat: Expressions are trees. When flat is false, this method iterates only on the first layer of the tree. To iterate on all the subparts of the expression, you have to do so recursively. It allows to handle each subpart specifically (for example subscripts, attribute, etc.), 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.ExprName]. Yields: Strings and names when flat, strings and expressions otherwise. """ yield from () def modernize(self) -> Expr: """Modernize the expression. For example, use PEP 604 type unions `|` instead of `typing.Union`. Returns: A modernized expression. """ return self def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return the expression as a dictionary. Parameters: **kwargs: Configuration options (none available yet). Returns: A dictionary. """ return _expr_as_dict(self, **kwargs) @property def classname(self) -> str: """The expression class name.""" return self.__class__.__name__ @property def path(self) -> str: """Path of the expressed name/attribute.""" return str(self) @property def canonical_path(self) -> str: """Path of the expressed name/attribute.""" return str(self) @property def canonical_name(self) -> str: """Name of the expressed name/attribute/parameter.""" # We must handle things like `griffe.Visitor` and `griffe.Visitor(code)`. return self.canonical_path.rsplit(".", 1)[-1].split("(", 1)[-1].removesuffix(")") @property def is_classvar(self) -> bool: """Whether this attribute is annotated with `ClassVar`.""" return isinstance(self, ExprSubscript) and self.canonical_name == "ClassVar" @property def is_tuple(self) -> bool: """Whether this expression is a tuple.""" return isinstance(self, ExprSubscript) and self.canonical_name.lower() == "tuple" @property def is_iterator(self) -> bool: """Whether this expression is an iterator.""" return isinstance(self, ExprSubscript) and self.canonical_name == "Iterator" @property def is_generator(self) -> bool: """Whether this expression is a generator.""" 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) 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]: yield from _join(self.values, ".", flat=flat) def append(self, value: ExprName) -> None: """Append a name to this attribute. Parameters: value: The expression name to append. """ if value.parent is None: value.parent = self.last self.values.append(value) @property def last(self) -> ExprName: """The last part of this attribute (on the right).""" # All values except the first one can *only* be names: # we can't do `a.(b or c)` or `a."string"`. return self.values[-1] # type: ignore[return-value] @property def first(self) -> str | Expr: """The first part of this attribute (on the left).""" return self.values[0] @property def path(self) -> str: """The path of this attribute.""" return self.last.path @property def canonical_path(self) -> str: """The canonical path of this attribute.""" return self.last.canonical_path # 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`.""" left: str | Expr """Left part.""" operator: str """Binary operator.""" right: str | Expr """Right part.""" 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) class ExprBoolOp(Expr): """Boolean operations like `a or b`.""" operator: str """Boolean operator.""" values: Sequence[str | Expr] """Operands.""" 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) class ExprCall(Expr): """Calls like `f()`.""" function: Expr """Function called.""" arguments: Sequence[str | Expr] """Passed arguments.""" @property 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]: 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) class ExprCompare(Expr): """Comparisons like `a > b`.""" left: str | Expr """Left part.""" operators: Sequence[str] """Comparison operators.""" comparators: Sequence[str | Expr] """Things compared.""" 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) class ExprComprehension(Expr): """Comprehensions like `a for b in c if d`.""" target: str | Expr """Comprehension target (value added to the result).""" iterable: str | Expr """Value iterated on.""" conditions: Sequence[str | Expr] """Conditions to include the target in the result.""" is_async: bool = False """Async comprehension or not.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if self.is_async: yield "async " yield "for " yield from _yield(self.target, flat=flat) yield " in " yield from _yield(self.iterable, flat=flat) if self.conditions: yield " if " yield from _join(self.conditions, " if ", flat=flat) # TODO: `ExprConstant` is never instantiated, # see `_build_constant` below (it always returns the value directly). # Maybe we could simply get rid of it, as it wouldn't bring much value # if used anyway. # 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 yield self.value # YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. @dataclass(eq=True, **_dataclass_opts) class ExprDict(Expr): """Dictionaries like `{"a": 0}`.""" keys: Sequence[str | Expr | None] """Dict keys.""" values: Sequence[str | Expr] """Dict values.""" 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)), ", ", flat=flat, ) yield "}" # 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}`.""" key: str | Expr """Target key.""" value: str | Expr """Target value.""" generators: Sequence[Expr] """Generators iterated on.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _yield(self.key, flat=flat) yield ": " yield from _yield(self.value, flat=flat) yield from _join(self.generators, " ", flat=flat) yield "}" # 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]: yield from _join(self.dims, ", ", flat=flat) # 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]: 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) class ExprGeneratorExp(Expr): """Generator expressions like `a for b in c for d in e`.""" element: str | Expr """Yielded element.""" generators: Sequence[Expr] """Generators iterated on.""" 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) class ExprIfExp(Expr): """Conditions like `a if b else c`.""" body: str | Expr """Value if test.""" test: str | Expr """Condition.""" orelse: str | Expr """Other expression.""" 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) yield " else " yield from _yield(self.orelse, flat=flat) # 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]: 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) class ExprKeyword(Expr): """Keyword arguments like `a=b`.""" name: str """Name.""" value: str | Expr """Value.""" # Griffe is designed around accessing Python objects # with the dot notation, for example `module.Class`. # Function parameters were not taken into account # because they are not accessible the same way. # But we still want to be able to cross-reference # documentation of function parameters in downstream # tools like mkdocstrings. So we add a special case # for keyword expressions, where they get a meaningful # canonical path (contrary to most other expressions that # aren't or do not begin with names or attributes) # of the form `path.to.called_function(param_name)`. # For this we need to store a reference to the `func` part # of the call expression in the keyword one, # hence the following field. # We allow it to be None for backward compatibility. function: Expr | None = None """Expression referencing the function called with this parameter.""" @property def canonical_path(self) -> str: """Path of the expressed keyword.""" if self.function: 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]: 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) class ExprVarPositional(Expr): """Variadic positional parameters like `*args`.""" value: Expr """Starred value.""" 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) class ExprVarKeyword(Expr): """Variadic keyword parameters like `**kwargs`.""" value: Expr """Double-starred value.""" 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) class ExprLambda(Expr): """Lambda expressions like `lambda a: a.b`.""" parameters: Sequence[ExprParameter] """Lambda's parameters.""" body: str | Expr """Lambda's body.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: pos_only = False pos_or_kw = False kw_only = False length = len(self.parameters) yield "lambda" if length: yield " " for index, parameter in enumerate(self.parameters, 1): if parameter.kind is ParameterKind.positional_only: pos_only = True elif parameter.kind is ParameterKind.var_positional: yield "*" elif parameter.kind is ParameterKind.var_keyword: yield "**" elif parameter.kind is ParameterKind.positional_or_keyword and not pos_or_kw: pos_or_kw = True elif parameter.kind is ParameterKind.keyword_only and not kw_only: kw_only = True yield "*, " if parameter.kind is not ParameterKind.positional_only and pos_only: pos_only = False yield "/, " yield parameter.name if parameter.default and parameter.kind not in (ParameterKind.var_positional, ParameterKind.var_keyword): yield "=" yield from _yield(parameter.default, flat=flat) if index < length: yield ", " yield ": " yield from _yield(self.body, flat=flat) # 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]: 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) class ExprListComp(Expr): """List comprehensions like `[a for b in c]`.""" element: str | Expr """Target value.""" generators: Sequence[Expr] """Generators iterated on.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "[" yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) yield "]" # 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.""" name: str """Actual name.""" parent: str | ExprName | Module | Class | None = None """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 yield self @property def path(self) -> str: """The full, resolved name. If it was given when creating the name, return that. If a callable was given, call it and return its result. It the name cannot be resolved, return the source. """ if isinstance(self.parent, ExprName): return f"{self.parent.path}.{self.name}" return self.name @property def canonical_path(self) -> str: """The canonical name (resolved one, not alias name).""" if self.parent is None: return self.name if isinstance(self.parent, ExprName): return f"{self.parent.canonical_path}.{self.name}" if isinstance(self.parent, str): return f"{self.parent}.{self.name}" try: return self.parent.resolve(self.name) except NameResolutionError: return self.name @property def resolved(self) -> Module | Class | None: """The resolved object this name refers to.""" try: return self.parent.modules_collection[self.parent.resolve(self.name)] # type: ignore[union-attr] except Exception: # noqa: BLE001 return self.parent.resolved[self.name] # type: ignore[union-attr,index] @property def is_enum_class(self) -> bool: """Whether this name resolves to an enumeration class.""" try: bases = self.resolved.bases # type: ignore[union-attr] except Exception: # noqa: BLE001 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 def is_enum_instance(self) -> bool: """Whether this name resolves to an enumeration instance.""" try: return self.parent.is_enum_class # type: ignore[union-attr] except Exception: # noqa: BLE001 return False @property def is_enum_value(self) -> bool: """Whether this name resolves to an enumeration value.""" try: return self.name == "value" and self.parent.is_enum_instance # type: ignore[union-attr] except Exception: # noqa: BLE001 return False # 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`.""" target: Expr """Target name.""" value: str | Expr """Value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "(" yield from _yield(self.target, flat=flat) 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) class ExprParameter(Expr): """Parameters in function signatures like `a: int = 0`.""" name: str """Parameter name.""" kind: ParameterKind = ParameterKind.positional_or_keyword """Parameter kind.""" annotation: Expr | None = None """Parameter type.""" default: str | Expr | None = None """Parameter default.""" # 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]: 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) class ExprSetComp(Expr): """Set comprehensions like `{a for b in c}`.""" element: str | Expr """Target value.""" generators: Sequence[Expr] """Generators iterated on.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "{" yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) yield "}" # 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]`.""" lower: str | Expr | None = None """Lower bound.""" upper: str | Expr | None = None """Upper bound.""" step: str | Expr | None = None """Iteration step.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: if self.lower is not None: yield from _yield(self.lower, flat=flat) yield ":" if self.upper is not None: yield from _yield(self.upper, flat=flat) if self.step is not None: yield ":" yield from _yield(self.step, flat=flat) # YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. @dataclass(eq=True, **_dataclass_opts) class ExprSubscript(Expr): """Subscripts like `a[b]`.""" left: str | Expr """Left part.""" slice: Expr """Slice part.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield from _yield(self.left, flat=flat) yield "[" yield from _yield(self.slice, flat=flat) yield "]" @property def path(self) -> str: """The path of this subscript's left part.""" if isinstance(self.left, str): return self.left return self.left.path @property def canonical_path(self) -> str: """The canonical path of this subscript's left part.""" if isinstance(self.left, str): return self.left return self.left.canonical_path # 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)`.""" elements: Sequence[str | Expr] """Tuple elements.""" 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]: if not self.implicit: yield "(" yield from _join(self.elements, ", ", flat=flat) if len(self.elements) == 1: yield "," if not self.implicit: yield ")" # YORE: EOL 3.9: Replace `**_dataclass_opts` with `slots=True` within line. @dataclass(eq=True, **_dataclass_opts) class ExprUnaryOp(Expr): """Unary operations like `-1`.""" operator: str """Unary operator.""" value: str | Expr """Value.""" 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) class ExprYield(Expr): """Yield statements like `yield a`.""" value: str | Expr | None = None """Yielded value.""" 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) class ExprYieldFrom(Expr): """Yield statements like `yield from a`.""" value: str | Expr """Yielded-from value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: yield "yield from " yield from _yield(self.value, flat=flat) _unary_op_map = { ast.Invert: "~", ast.Not: "not ", ast.UAdd: "+", ast.USub: "-", } _binary_op_map = { ast.Add: "+", ast.BitAnd: "&", ast.BitOr: "|", ast.BitXor: "^", ast.Div: "/", ast.FloorDiv: "//", ast.LShift: "<<", ast.MatMult: "@", ast.Mod: "%", ast.Mult: "*", ast.Pow: "**", ast.RShift: ">>", ast.Sub: "-", } _bool_op_map = { ast.And: "and", ast.Or: "or", } _compare_op_map = { ast.Eq: "==", ast.NotEq: "!=", ast.Lt: "<", ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">=", ast.Is: "is", ast.IsNot: "is not", ast.In: "in", ast.NotIn: "not in", } def _build_attribute(node: ast.Attribute, parent: Module | Class, **kwargs: Any) -> Expr: left = _build(node.value, parent, **kwargs) if isinstance(left, ExprAttribute): left.append(ExprName(node.attr)) return left if isinstance(left, ExprName): return ExprAttribute([left, ExprName(node.attr, left)]) if isinstance(left, str): return ExprAttribute([left, ExprName(node.attr, "str")]) return ExprAttribute([left, ExprName(node.attr)]) def _build_binop(node: ast.BinOp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprBinOp( _build(node.left, parent, **kwargs), _binary_op_map[type(node.op)], _build(node.right, parent, **kwargs), ) def _build_boolop(node: ast.BoolOp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprBoolOp( _bool_op_map[type(node.op)], [_build(value, parent, **kwargs) for value in node.values], ) def _build_call(node: ast.Call, parent: Module | Class, **kwargs: Any) -> Expr: function = _build(node.func, parent, **kwargs) positional_args = [_build(arg, parent, **kwargs) for arg in node.args] keyword_args = [_build(kwarg, parent, function=function, **kwargs) for kwarg in node.keywords] return ExprCall(function, [*positional_args, *keyword_args]) def _build_compare(node: ast.Compare, parent: Module | Class, **kwargs: Any) -> Expr: return ExprCompare( _build(node.left, parent, **kwargs), [_compare_op_map[type(op)] for op in node.ops], [_build(comp, parent, **kwargs) for comp in node.comparators], ) def _build_comprehension(node: ast.comprehension, parent: Module | Class, **kwargs: Any) -> Expr: return ExprComprehension( _build(node.target, parent, **kwargs), _build(node.iter, parent, **kwargs), [_build(condition, parent, **kwargs) for condition in node.ifs], is_async=bool(node.is_async), ) def _build_constant( node: ast.Constant, parent: Module | Class, *, in_formatted_str: bool = False, in_joined_str: bool = False, parse_strings: bool = False, literal_strings: bool = False, **kwargs: Any, ) -> str | Expr: if isinstance(node.value, str): if in_joined_str and not in_formatted_str: # We're in a f-string, not in a formatted value, don't keep quotes. return node.value if parse_strings and not literal_strings: # We're in a place where a string could be a type annotation # (and not in a Literal[...] type annotation). # We parse the string and build from the resulting nodes again. # If we fail to parse it (syntax errors), we consider it's a literal string and log a message. try: parsed = compile( node.value, mode="eval", filename="", flags=ast.PyCF_ONLY_AST, optimize=1, ) except SyntaxError: logger.debug( "Tried and failed to parse %r as Python code, " "falling back to using it as a string literal " "(postponed annotations might help: https://peps.python.org/pep-0563/)", node.value, ) else: return _build(parsed.body, parent, **kwargs) # type: ignore[attr-defined] return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value) def _build_dict(node: ast.Dict, parent: Module | Class, **kwargs: Any) -> Expr: return ExprDict( [None if key is None else _build(key, parent, **kwargs) for key in node.keys], [_build(value, parent, **kwargs) for value in node.values], ) def _build_dictcomp(node: ast.DictComp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprDictComp( _build(node.key, parent, **kwargs), _build(node.value, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators], ) def _build_formatted( node: ast.FormattedValue, parent: Module | Class, *, in_formatted_str: bool = False, # noqa: ARG001 **kwargs: Any, ) -> Expr: return ExprFormatted(_build(node.value, parent, in_formatted_str=True, **kwargs)) def _build_generatorexp(node: ast.GeneratorExp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprGeneratorExp( _build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators], ) def _build_ifexp(node: ast.IfExp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprIfExp( _build(node.body, parent, **kwargs), _build(node.test, parent, **kwargs), _build(node.orelse, parent, **kwargs), ) def _build_joinedstr( node: ast.JoinedStr, parent: Module | Class, *, in_joined_str: bool = False, # noqa: ARG001 **kwargs: Any, ) -> Expr: return ExprJoinedStr([_build(value, parent, in_joined_str=True, **kwargs) for value in node.values]) def _build_keyword(node: ast.keyword, parent: Module | Class, function: Expr | None = None, **kwargs: Any) -> Expr: if node.arg is None: return ExprVarKeyword(_build(node.value, parent, **kwargs)) return ExprKeyword(node.arg, _build(node.value, parent, **kwargs), function=function) def _build_lambda(node: ast.Lambda, parent: Module | Class, **kwargs: Any) -> Expr: return ExprLambda( parameters=[ ExprParameter( name=name, kind=kind, annotation=None, default=default if isinstance(default, str) else safe_get_expression(default, parent=parent, parse_strings=False), ) for name, _, kind, default in get_parameters(node.args) ], body=_build(node.body, parent, **kwargs), ) def _build_list(node: ast.List, parent: Module | Class, **kwargs: Any) -> Expr: return ExprList([_build(el, parent, **kwargs) for el in node.elts]) def _build_listcomp(node: ast.ListComp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprListComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators]) def _build_name(node: ast.Name, parent: Module | Class, **kwargs: Any) -> Expr: # noqa: ARG001 return ExprName(node.id, parent) def _build_named_expr(node: ast.NamedExpr, parent: Module | Class, **kwargs: Any) -> Expr: return ExprNamedExpr(_build(node.target, parent, **kwargs), _build(node.value, parent, **kwargs)) def _build_set(node: ast.Set, parent: Module | Class, **kwargs: Any) -> Expr: return ExprSet([_build(el, parent, **kwargs) for el in node.elts]) def _build_setcomp(node: ast.SetComp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprSetComp(_build(node.elt, parent, **kwargs), [_build(gen, parent, **kwargs) for gen in node.generators]) def _build_slice(node: ast.Slice, parent: Module | Class, **kwargs: Any) -> Expr: return ExprSlice( None if node.lower is None else _build(node.lower, parent, **kwargs), None if node.upper is None else _build(node.upper, parent, **kwargs), None if node.step is None else _build(node.step, parent, **kwargs), ) def _build_starred(node: ast.Starred, parent: Module | Class, **kwargs: Any) -> Expr: return ExprVarPositional(_build(node.value, parent, **kwargs)) def _build_subscript( node: ast.Subscript, parent: Module | Class, *, parse_strings: bool = False, literal_strings: bool = False, in_subscript: bool = False, # noqa: ARG001 **kwargs: Any, ) -> Expr: left = _build(node.value, parent, **kwargs) if parse_strings: if isinstance(left, (ExprAttribute, ExprName)) and left.canonical_path in { "typing.Literal", "typing_extensions.Literal", }: literal_strings = True slice = _build( node.slice, parent, parse_strings=True, literal_strings=literal_strings, in_subscript=True, **kwargs, ) else: slice = _build(node.slice, parent, in_subscript=True, **kwargs) return ExprSubscript(left, slice) def _build_tuple( node: ast.Tuple, parent: Module | Class, *, in_subscript: bool = False, **kwargs: Any, ) -> Expr: return ExprTuple([_build(el, parent, **kwargs) for el in node.elts], implicit=in_subscript) def _build_unaryop(node: ast.UnaryOp, parent: Module | Class, **kwargs: Any) -> Expr: return ExprUnaryOp(_unary_op_map[type(node.op)], _build(node.operand, parent, **kwargs)) def _build_yield(node: ast.Yield, parent: Module | Class, **kwargs: Any) -> Expr: return ExprYield(None if node.value is None else _build(node.value, parent, **kwargs)) def _build_yield_from(node: ast.YieldFrom, parent: Module | Class, **kwargs: Any) -> Expr: return ExprYieldFrom(_build(node.value, parent, **kwargs)) _node_map: dict[type, Callable[[Any, Module | Class], Expr]] = { ast.Attribute: _build_attribute, ast.BinOp: _build_binop, ast.BoolOp: _build_boolop, ast.Call: _build_call, ast.Compare: _build_compare, ast.comprehension: _build_comprehension, ast.Constant: _build_constant, # type: ignore[dict-item] ast.Dict: _build_dict, ast.DictComp: _build_dictcomp, ast.FormattedValue: _build_formatted, ast.GeneratorExp: _build_generatorexp, ast.IfExp: _build_ifexp, ast.JoinedStr: _build_joinedstr, ast.keyword: _build_keyword, ast.Lambda: _build_lambda, ast.List: _build_list, ast.ListComp: _build_listcomp, ast.Name: _build_name, ast.NamedExpr: _build_named_expr, ast.Set: _build_set, ast.SetComp: _build_setcomp, ast.Slice: _build_slice, ast.Starred: _build_starred, ast.Subscript: _build_subscript, ast.Tuple: _build_tuple, ast.UnaryOp: _build_unaryop, ast.Yield: _build_yield, ast.YieldFrom: _build_yield_from, } def _build(node: ast.AST, parent: Module | Class, **kwargs: Any) -> Expr: return _node_map[type(node)](node, parent, **kwargs) def get_expression( node: ast.AST | None, parent: Module | Class, *, parse_strings: bool | None = None, ) -> Expr | None: """Build an expression from an AST. Parameters: node: The annotation node. parent: The parent used to resolve the name. parse_strings: Whether to try and parse strings as type annotations. Returns: A string or resovable name or expression. """ if node is None: return None if parse_strings is None: try: module = parent.module except ValueError: parse_strings = False else: parse_strings = not module.imports_future_annotations return _build(node, parent, parse_strings=parse_strings) def safe_get_expression( node: ast.AST | None, parent: Module | Class, *, parse_strings: bool | None = None, log_level: LogLevel | None = LogLevel.error, msg_format: str = "{path}:{lineno}: Failed to get expression from {node_class}: {error}", ) -> Expr | None: """Safely (no exception) build a resolvable annotation. Parameters: node: The annotation node. parent: The parent used to resolve the name. parse_strings: Whether to try and parse strings as type annotations. log_level: Log level to use to log a message. None to disable logging. msg_format: A format string for the log message. Available placeholders: path, lineno, node, error. Returns: A string or resovable name or expression. """ try: return get_expression(node, parent, parse_strings=parse_strings) except Exception as error: # noqa: BLE001 if log_level is None: return None node_class = node.__class__.__name__ try: path: Path | str = parent.relative_filepath except ValueError: path = "" 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) return None _msg_format = "{path}:{lineno}: Failed to get %s expression from {node_class}: {error}" get_annotation = partial(get_expression, parse_strings=None) safe_get_annotation = partial( safe_get_expression, parse_strings=None, msg_format=_msg_format % "annotation", ) get_base_class = partial(get_expression, parse_strings=False) safe_get_base_class = partial( safe_get_expression, parse_strings=False, msg_format=_msg_format % "base class", ) get_condition = partial(get_expression, parse_strings=False) safe_get_condition = partial( safe_get_expression, parse_strings=False, msg_format=_msg_format % "condition", ) python-griffe-1.6.2/src/_griffe/git.py0000664000175000017500000001040014767006246017517 0ustar carstencarsten# This module contains Git utilities, used by our [`load_git`][griffe.load_git] function, # which in turn is used to load the API for different snapshots of a Git repository # and find breaking changes between them. from __future__ import annotations import os import re import shutil import subprocess import unicodedata from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory from typing import TYPE_CHECKING from _griffe.exceptions import GitError if TYPE_CHECKING: from collections.abc import Iterator _WORKTREE_PREFIX = "griffe-worktree-" def _normalize(value: str) -> str: value = unicodedata.normalize("NFKC", value) value = re.sub(r"[^\w]+", "-", value) return re.sub(r"[-\s]+", "-", value).strip("-") def assert_git_repo(path: str | Path) -> None: """Assert that a directory is a Git repository. Parameters: path: Path to a directory. Raises: OSError: When the directory is not a Git repository. """ if not shutil.which("git"): raise RuntimeError("Could not find git executable. Please install git.") try: subprocess.run( ["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except subprocess.CalledProcessError as err: raise OSError(f"Not a git repository: {path}") from err def get_latest_tag(repo: str | Path) -> str: """Get latest tag of a Git repository. Parameters: repo: The path to Git repository. Returns: The latest tag. """ if isinstance(repo, str): repo = Path(repo) if not repo.is_dir(): repo = repo.parent process = subprocess.run( ["git", "tag", "-l", "--sort=-creatordate"], cwd=repo, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) output = process.stdout.strip() if process.returncode != 0 or not output: raise GitError(f"Cannot list Git tags in {repo}: {output or 'no tags'}") return output.split("\n", 1)[0] def get_repo_root(repo: str | Path) -> str: """Get the root of a Git repository. Parameters: repo: The path to a Git repository. Returns: The root of the repository. """ if isinstance(repo, str): repo = Path(repo) if not repo.is_dir(): repo = repo.parent output = subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], cwd=repo, ) return output.decode().strip() @contextmanager def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: """Context manager that checks out the given reference in the given repository to a temporary worktree. Parameters: repo: Path to the repository (i.e. the directory *containing* the `.git` directory) ref: A Git reference such as a commit, tag or branch. Yields: The path to the temporary worktree. Raises: OSError: If `repo` is not a valid `.git` repository RuntimeError: If the `git` executable is unavailable, or if it cannot create a worktree """ assert_git_repo(repo) repo_name = Path(repo).resolve().name normref = _normalize(ref) # Branch names can contain slashes. with TemporaryDirectory(prefix=f"{_WORKTREE_PREFIX}{repo_name}-{normref}-") as tmp_dir: location = os.path.join(tmp_dir, normref) # noqa: PTH118 tmp_branch = f"griffe-{normref}" # Temporary branch name must not already exist. process = subprocess.run( ["git", "-C", repo, "worktree", "add", "-b", tmp_branch, location, ref], capture_output=True, check=False, ) if process.returncode: raise RuntimeError(f"Could not create git worktree: {process.stderr.decode()}") try: yield Path(location) finally: subprocess.run(["git", "-C", repo, "worktree", "remove", location], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "worktree", "prune"], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "branch", "-D", tmp_branch], stdout=subprocess.DEVNULL, check=False) python-griffe-1.6.2/src/_griffe/__init__.py0000664000175000017500000000023214767006246020475 0ustar carstencarsten# The internal API layout doesn't follow any particular paradigm: # we simply organize code in different modules, depending on what the code is used for. python-griffe-1.6.2/src/_griffe/diff.py0000664000175000017500000005426014767006246017660 0ustar carstencarsten# This module exports "breaking changes" related utilities. # The logic here is to iterate on objects and their members recursively, # to yield found breaking changes. # # The breakage class definitions might sound a bit verbose, # but declaring them this way helps with (de)serialization, # which we don't use yet, but could use in the future. from __future__ import annotations import contextlib from pathlib import Path from typing import TYPE_CHECKING, Any 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 logger if TYPE_CHECKING: from collections.abc import Iterable, Iterator 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)) 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. Parameters: obj: The object related to the breakage. old_value: The old value. new_value: The new, incompatible value. 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 def __repr__(self) -> str: return self.kind.name def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this object's data as a dictionary. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options. Returns: A dictionary. """ return { "kind": self.kind, "object_path": self.obj.path, "old_value": self.old_value, "new_value": self.new_value, } def explain(self, style: ExplanationStyle = ExplanationStyle.ONE_LINE) -> str: """Explain the breakage by showing old and new value. Parameters: style: The explanation style to use. Returns: An explanation. """ return getattr(self, f"_explain_{style.value}")() @property def _filepath(self) -> Path: if self.obj.is_alias: return self.obj.parent.filepath # type: ignore[union-attr,return-value] return self.obj.filepath # type: ignore[return-value] @property def _relative_filepath(self) -> Path: if self.obj.is_alias: return self.obj.parent.relative_filepath # type: ignore[union-attr] return self.obj.relative_filepath @property def _relative_package_filepath(self) -> Path: if self.obj.is_alias: return self.obj.parent.relative_package_filepath # type: ignore[union-attr] return self.obj.relative_package_filepath @property def _location(self) -> Path: # Absolute file path probably means temporary worktree. # We use our worktree prefix to remove some components # of the path on the left (`/tmp/griffe-worktree-*/griffe_*/repo`). if self._relative_filepath.is_absolute(): parts = self._relative_filepath.parts for index, part in enumerate(parts): if part.startswith(_WORKTREE_PREFIX): return Path(*parts[index + 2 :]) return self._relative_filepath @property def _canonical_path(self) -> str: if self.obj.is_alias: return self.obj.path return self.obj.canonical_path @property def _module_path(self) -> str: if self.obj.is_alias: return self.obj.parent.module.path # type: ignore[union-attr] return self.obj.module.path @property def _relative_path(self) -> str: return self._canonical_path[len(self._module_path) + 1 :] or "" @property def _lineno(self) -> int: # If the object was removed, and we are able to get the location (file path) # as a relative path, then we use 0 instead of the original line number # (it helps when checking current sources, and avoids pointing to now missing contents). if self.kind is BreakageKind.OBJECT_REMOVED and self._relative_filepath != self._location: return 0 if self.obj.is_alias: return self.obj.alias_lineno or 0 # type: ignore[attr-defined] return self.obj.lineno or 0 def _format_location(self) -> str: return f"{Style.BRIGHT}{self._location}{Style.RESET_ALL}:{self._lineno}" def _format_title(self) -> str: return self._relative_path def _format_kind(self) -> str: return f"{Fore.YELLOW}{self.kind.value}{Fore.RESET}" def _format_old_value(self) -> str: return str(self.old_value) def _format_new_value(self) -> str: return str(self.new_value) def _explain_oneline(self) -> str: explanation = f"{self._format_location()}: {self._format_title()}: {self._format_kind()}" old = self._format_old_value() new = self._format_new_value() if old and new: change = f"{old} -> {new}" elif old: change = old elif new: change = new else: change = "" if change: return f"{explanation}: {change}" return explanation def _explain_verbose(self) -> str: lines = [f"{self._format_location()}: {self._format_title()}:"] kind = self._format_kind() old = self._format_old_value() new = self._format_new_value() if old or new: lines.append(f"{kind}:") else: lines.append(kind) if old: lines.append(f" Old: {old}") if new: lines.append(f" New: {new}") if self.details: lines.append(f" Details: {self.details}") lines.append("") return "\n".join(lines) def _explain_markdown(self) -> str: explanation = f"- `{self._relative_path}`: *{self.kind.value}*" old = self._format_old_value() if old and old != "unset": old = f"`{old}`" new = self._format_new_value() if new and new != "unset": new = f"`{new}`" if old and new: change = f"{old} -> {new}" elif old: change = old elif new: change = new else: change = "" if change: return f"{explanation}: {change}" return explanation def _explain_github(self) -> str: location = f"file={self._location},line={self._lineno}" title = f"title={self._format_title()}" explanation = f"::warning {location},{title}::{self.kind.value}" old = self._format_old_value() if old and old != "unset": old = f"`{old}`" new = self._format_new_value() if new and new != "unset": new = f"`{new}`" if old and new: change = f"{old} -> {new}" elif old: change = old elif new: change = new else: change = "" if change: return f"{explanation}: {change}" return explanation class ParameterMovedBreakage(Breakage): """Specific breakage class for moved parameters.""" kind: BreakageKind = BreakageKind.PARAMETER_MOVED @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.old_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.old_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return "" def _format_new_value(self) -> str: return "" class ParameterRemovedBreakage(Breakage): """Specific breakage class for removed parameters.""" kind: BreakageKind = BreakageKind.PARAMETER_REMOVED @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.old_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.old_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return "" def _format_new_value(self) -> str: return "" class ParameterChangedKindBreakage(Breakage): """Specific breakage class for parameters whose kind changed.""" kind: BreakageKind = BreakageKind.PARAMETER_CHANGED_KIND @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.old_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.old_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return str(self.old_value.kind.value) def _format_new_value(self) -> str: return str(self.new_value.kind.value) class ParameterChangedDefaultBreakage(Breakage): """Specific breakage class for parameters whose default value changed.""" kind: BreakageKind = BreakageKind.PARAMETER_CHANGED_DEFAULT @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.old_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.old_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return str(self.old_value.default) def _format_new_value(self) -> str: return str(self.new_value.default) class ParameterChangedRequiredBreakage(Breakage): """Specific breakage class for parameters which became required.""" kind: BreakageKind = BreakageKind.PARAMETER_CHANGED_REQUIRED @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.old_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.old_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return "" def _format_new_value(self) -> str: return "" class ParameterAddedRequiredBreakage(Breakage): """Specific breakage class for new parameters added as required.""" kind: BreakageKind = BreakageKind.PARAMETER_ADDED_REQUIRED @property def _relative_path(self) -> str: return f"{super()._relative_path}({self.new_value.name})" def _format_title(self) -> str: return f"{super()._relative_path}({Fore.BLUE}{self.new_value.name}{Fore.RESET})" def _format_old_value(self) -> str: return "" def _format_new_value(self) -> str: return "" class ReturnChangedTypeBreakage(Breakage): """Specific breakage class for return values which changed type.""" kind: BreakageKind = BreakageKind.RETURN_CHANGED_TYPE class ObjectRemovedBreakage(Breakage): """Specific breakage class for removed objects.""" kind: BreakageKind = BreakageKind.OBJECT_REMOVED def _format_old_value(self) -> str: return "" def _format_new_value(self) -> str: return "" class ObjectChangedKindBreakage(Breakage): """Specific breakage class for objects whose kind changed.""" kind: BreakageKind = BreakageKind.OBJECT_CHANGED_KIND def _format_old_value(self) -> str: return self.old_value.value def _format_new_value(self) -> str: return self.new_value.value class AttributeChangedTypeBreakage(Breakage): """Specific breakage class for attributes whose type changed.""" kind: BreakageKind = BreakageKind.ATTRIBUTE_CHANGED_TYPE class AttributeChangedValueBreakage(Breakage): """Specific breakage class for attributes whose value changed.""" kind: BreakageKind = BreakageKind.ATTRIBUTE_CHANGED_VALUE class ClassRemovedBaseBreakage(Breakage): """Specific breakage class for removed base classes.""" kind: BreakageKind = BreakageKind.CLASS_REMOVED_BASE def _format_old_value(self) -> str: return "[" + ", ".join(base.canonical_path for base in self.old_value) + "]" def _format_new_value(self) -> str: return "[" + ", ".join(base.canonical_path for base in self.new_value) + "]" # TODO: Check decorators? Maybe resolved by extensions and/or dynamic analysis. def _class_incompatibilities( old_class: Class, new_class: Class, *, seen_paths: set[str], ) -> Iterable[Breakage]: yield from () if new_class.bases != old_class.bases and len(new_class.bases) < len(old_class.bases): yield ClassRemovedBaseBreakage(new_class, old_class.bases, new_class.bases) yield from _member_incompatibilities(old_class, new_class, seen_paths=seen_paths) # TODO: Check decorators? Maybe resolved by extensions and/or dynamic analysis. def _function_incompatibilities(old_function: Function, new_function: Function) -> Iterator[Breakage]: new_param_names = [param.name for param in new_function.parameters] param_kinds = {param.kind for param in new_function.parameters} has_variadic_args = ParameterKind.var_positional in param_kinds has_variadic_kwargs = ParameterKind.var_keyword in param_kinds for old_index, old_param in enumerate(old_function.parameters): # Check if the parameter was removed. if old_param.name not in new_function.parameters: swallowed = ( (old_param.kind is ParameterKind.keyword_only and has_variadic_kwargs) or (old_param.kind is ParameterKind.positional_only and has_variadic_args) or (old_param.kind is ParameterKind.positional_or_keyword and has_variadic_args and has_variadic_kwargs) ) if not swallowed: yield ParameterRemovedBreakage(new_function, old_param, None) continue # Check if the parameter became required. new_param = new_function.parameters[old_param.name] if new_param.required and not old_param.required: 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: 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:+})" yield ParameterMovedBreakage(new_function, old_param, new_param, details=details) # Check if the parameter changed kind. if old_param.kind is not new_param.kind: incompatible_kind = any( ( # Positional-only to keyword-only. old_param.kind is ParameterKind.positional_only and new_param.kind is ParameterKind.keyword_only, # 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, # 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 and not has_variadic_args, # Not positional-only to variadic positional, without variadic keyword. new_param.kind is ParameterKind.var_positional and old_param.kind is not ParameterKind.positional_only and not has_variadic_kwargs, ), ) if incompatible_kind: yield ParameterChangedKindBreakage(new_function, old_param, new_param) # 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 if non_required and non_variadic: try: if old_param.default != new_param.default: yield breakage except Exception: # noqa: BLE001 (equality checks sometimes fail, e.g. numpy arrays) # NOTE: Emitting breakage on a failed comparison could be a preference. yield breakage # Check if required parameters were added. for new_param in new_function.parameters: if new_param.name not in old_function.parameters and new_param.required: yield ParameterAddedRequiredBreakage(new_function, None, new_param) if not _returns_are_compatible(old_function, new_function): yield ReturnChangedTypeBreakage(new_function, old_function.returns, new_function.returns) def _attribute_incompatibilities(old_attribute: Attribute, new_attribute: Attribute) -> Iterable[Breakage]: # TODO: Support annotation breaking changes. if old_attribute.value != new_attribute.value: if new_attribute.value is None: yield AttributeChangedValueBreakage(new_attribute, old_attribute.value, "unset") else: yield AttributeChangedValueBreakage(new_attribute, old_attribute.value, new_attribute.value) def _alias_incompatibilities( old_obj: Object | Alias, new_obj: Object | Alias, *, seen_paths: set[str], ) -> Iterable[Breakage]: try: 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("API check: %s | %s: skip alias with unknown target", old_obj.path, new_obj.path) return yield from _type_based_yield(old_member, new_member, seen_paths=seen_paths) def _member_incompatibilities( old_obj: Object | Alias, new_obj: Object | Alias, *, seen_paths: set[str] | None = None, ) -> Iterator[Breakage]: 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("API check: %s.%s: skip non-public object", old_obj.path, name) continue logger.debug("API check: %s.%s", old_obj.path, name) try: new_member = new_obj.all_members[name] except KeyError: if (not old_member.is_alias and old_member.is_module) or old_member.is_public: yield ObjectRemovedBreakage(old_member, old_member, None) # type: ignore[arg-type] else: yield from _type_based_yield(old_member, new_member, seen_paths=seen_paths) def _type_based_yield( old_member: Object | Alias, new_member: Object | Alias, *, seen_paths: set[str], ) -> Iterator[Breakage]: if old_member.path in seen_paths: return seen_paths.add(old_member.path) if old_member.is_alias or new_member.is_alias: # Should be first, since there can be the case where there is an alias and another kind of object, # which may not be a breaking change. yield from _alias_incompatibilities( old_member, new_member, seen_paths=seen_paths, ) elif new_member.kind != old_member.kind: yield ObjectChangedKindBreakage(new_member, old_member.kind, new_member.kind) # type: ignore[arg-type] elif old_member.is_module: yield from _member_incompatibilities( old_member, new_member, seen_paths=seen_paths, ) elif old_member.is_class: yield from _class_incompatibilities( old_member, # type: ignore[arg-type] new_member, # type: ignore[arg-type] seen_paths=seen_paths, ) elif old_member.is_function: yield from _function_incompatibilities(old_member, new_member) # type: ignore[arg-type] elif old_member.is_attribute: yield from _attribute_incompatibilities(old_member, new_member) # type: ignore[arg-type] def _returns_are_compatible(old_function: Function, new_function: Function) -> bool: # We consider that a return value of `None` only is not a strong contract, # it just means that the function returns nothing. We don't expect users # to be asserting that the return value is `None`. # Therefore we don't consider it a breakage if the return changes from `None` # to something else: the function just gained a return value. if old_function.returns is None: return True if new_function.returns is None: # NOTE: Should it be configurable to allow/disallow removing a return type? return False with contextlib.suppress(AttributeError): if new_function.returns == old_function.returns: return True # TODO: Support annotation breaking changes. return True _sentinel = object() def find_breaking_changes( old_obj: Object | Alias, new_obj: Object | Alias, ) -> Iterator[Breakage]: """Find breaking changes between two versions of the same API. The function will iterate recursively on all objects and yield breaking changes with detailed information. Parameters: old_obj: The old version of an object. new_obj: The new version of an object. Yields: Breaking changes. Examples: >>> import sys, griffe >>> new = griffe.load("pkg") >>> old = griffe.load_git("pkg", "1.2.3") >>> for breakage in griffe.find_breaking_changes(old, new) ... print(breakage.explain(style=style), file=sys.stderr) """ yield from _member_incompatibilities(old_obj, new_obj) python-griffe-1.6.2/src/_griffe/agents/0000775000175000017500000000000014767006246017650 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/agents/nodes/0000775000175000017500000000000014767006246020760 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/agents/nodes/parameters.py0000664000175000017500000000454214767006246023502 0ustar carstencarsten# This module contains utilities for extracting information from parameter nodes. from __future__ import annotations import ast from itertools import zip_longest from typing import TYPE_CHECKING, Optional, Union from _griffe.enumerations import ParameterKind if TYPE_CHECKING: from collections.abc import Iterable 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: parameters: ParametersType = [] # TODO: Probably some optimizations to do here. args_kinds_defaults: Iterable = reversed( ( *zip_longest( reversed( ( *zip_longest( node.posonlyargs, [], fillvalue=ParameterKind.positional_only, ), *zip_longest(node.args, [], fillvalue=ParameterKind.positional_or_keyword), ), ), reversed(node.defaults), fillvalue=None, ), ), ) arg: ast.arg kind: ParameterKind arg_default: ast.AST | None for (arg, kind), arg_default in args_kinds_defaults: parameters.append((arg.arg, arg.annotation, kind, arg_default)) if node.vararg: parameters.append( ( node.vararg.arg, node.vararg.annotation, ParameterKind.var_positional, "()", ), ) # TODO: Probably some optimizations to do here. kwargs_defaults: Iterable = reversed( ( *zip_longest( reversed(node.kwonlyargs), reversed(node.kw_defaults), fillvalue=None, ), ), ) kwarg: ast.arg kwarg_default: ast.AST | None for kwarg, kwarg_default in kwargs_defaults: parameters.append( (kwarg.arg, kwarg.annotation, ParameterKind.keyword_only, kwarg_default), ) if node.kwarg: parameters.append( ( node.kwarg.arg, node.kwarg.annotation, ParameterKind.var_keyword, "{}", ), ) return parameters python-griffe-1.6.2/src/_griffe/agents/nodes/__init__.py0000664000175000017500000000011414767006246023065 0ustar carstencarsten# These submodules contain utilities for working with AST and object nodes. python-griffe-1.6.2/src/_griffe/agents/nodes/docstrings.py0000664000175000017500000000174214767006246023515 0ustar carstencarsten# This module contains utilities for extracting docstrings from nodes. from __future__ import annotations import ast def get_docstring( node: ast.AST, *, strict: bool = False, ) -> tuple[str | None, int | None, int | None]: """Extract a docstring. Parameters: node: The node to extract the docstring from. strict: Whether to skip searching the body (functions). Returns: A tuple with the value and line numbers of the docstring. """ # TODO: Possible optimization using a type map. if isinstance(node, ast.Expr): doc = node.value elif not strict and node.body and isinstance(node.body, list) and isinstance(node.body[0], ast.Expr): # type: ignore[attr-defined] doc = node.body[0].value # type: ignore[attr-defined] else: return None, None, None if isinstance(doc, ast.Constant) and isinstance(doc.value, str): return doc.value, doc.lineno, doc.end_lineno return None, None, None python-griffe-1.6.2/src/_griffe/agents/nodes/exports.py0000664000175000017500000000674614767006246023053 0ustar carstencarsten# This module contains utilities for extracting exports from `__all__` assignments. from __future__ import annotations import ast from contextlib import suppress from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Callable from _griffe.agents.nodes.values import get_value from _griffe.enumerations import LogLevel from _griffe.expressions import ExprName from _griffe.logger import logger if TYPE_CHECKING: from _griffe.models import Module # YORE: Bump 2: Remove block. @dataclass class ExportedName: """Deprecated. An intermediate class to store names. The [`get__all__`][griffe.get__all__] function now returns instances of [`ExprName`][griffe.ExprName] instead. """ name: str """The exported name.""" parent: Module """The parent module.""" def _extract_attribute(node: ast.Attribute, parent: Module) -> list[str | ExprName]: return [ExprName(name=node.attr, parent=_extract(node.value, parent)[0])] def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExprName]: left = _extract(node.left, parent) right = _extract(node.right, parent) return left + right def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExprName]: return [node.value] def _extract_name(node: ast.Name, parent: Module) -> list[str | ExprName]: return [ExprName(node.id, parent)] def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExprName]: sequence = [] for elt in node.elts: sequence.extend(_extract(elt, parent)) return sequence def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExprName]: return _extract(node.value, parent) _node_map: dict[type, Callable[[Any, Module], list[str | ExprName]]] = { ast.Attribute: _extract_attribute, ast.BinOp: _extract_binop, ast.Constant: _extract_constant, ast.List: _extract_sequence, ast.Name: _extract_name, ast.Set: _extract_sequence, ast.Starred: _extract_starred, ast.Tuple: _extract_sequence, } def _extract(node: ast.AST, parent: Module) -> list[str | ExprName]: return _node_map[type(node)](node, parent) def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | ExprName]: """Get the values declared in `__all__`. Parameters: node: The assignment node. parent: The parent module. Returns: A set of names. """ if node.value is None: return [] return _extract(node.value, parent) 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 | ExprName]: """Safely (no exception) extract values in `__all__`. Parameters: node: The `__all__` assignment node. parent: The parent used to resolve the names. log_level: Log level to use to log a message. Returns: A list of strings or resolvable names. """ try: return get__all__(node, parent) except Exception as error: # noqa: BLE001 message = f"Failed to extract `__all__` value: {get_value(node.value)}" with suppress(Exception): message += f" at {parent.relative_filepath}:{node.lineno}" if isinstance(error, KeyError): message += f": unsupported node {error}" else: message += f": {error}" getattr(logger, log_level.value)(message) return [] python-griffe-1.6.2/src/_griffe/agents/nodes/ast.py0000664000175000017500000000752114767006246022126 0ustar carstencarsten# This module contains utilities for navigating AST nodes. from __future__ import annotations from ast import AST from typing import TYPE_CHECKING from _griffe.exceptions import LastNodeError if TYPE_CHECKING: from collections.abc import Iterator def ast_kind(node: AST) -> str: """Return the kind of an AST node. Parameters: node: The AST node. Returns: The node kind. """ return node.__class__.__name__.lower() def ast_children(node: AST) -> Iterator[AST]: """Return the children of an AST node. Parameters: node: The AST node. Yields: The node children. """ for field_name in node._fields: try: field = getattr(node, field_name) except AttributeError: continue if isinstance(field, AST): field.parent = node # type: ignore[attr-defined] yield field elif isinstance(field, list): for child in field: if isinstance(child, AST): child.parent = node # type: ignore[attr-defined] yield child def ast_previous_siblings(node: AST) -> Iterator[AST]: """Return the previous siblings of this node, starting from the closest. Parameters: node: The AST node. Yields: The previous siblings. """ for sibling in ast_children(node.parent): # type: ignore[attr-defined] if sibling is not node: yield sibling else: return def ast_next_siblings(node: AST) -> Iterator[AST]: """Return the next siblings of this node, starting from the closest. Parameters: node: The AST node. Yields: The next siblings. """ siblings = ast_children(node.parent) # type: ignore[attr-defined] for sibling in siblings: if sibling is node: break yield from siblings def ast_siblings(node: AST) -> Iterator[AST]: """Return the siblings of this node. Parameters: node: The AST node. Yields: The siblings. """ siblings = ast_children(node.parent) # type: ignore[attr-defined] for sibling in siblings: if sibling is not node: yield sibling else: break yield from siblings def ast_previous(node: AST) -> AST: """Return the previous sibling of this node. Parameters: node: The AST node. Raises: LastNodeError: When the node does not have previous siblings. Returns: The sibling. """ try: *_, last = ast_previous_siblings(node) except ValueError: raise LastNodeError("there is no previous node") from None return last def ast_next(node: AST) -> AST: """Return the next sibling of this node. Parameters: node: The AST node. Raises: LastNodeError: When the node does not have next siblings. Returns: The sibling. """ try: return next(ast_next_siblings(node)) except StopIteration: raise LastNodeError("there is no next node") from None def ast_first_child(node: AST) -> AST: """Return the first child of this node. Parameters: node: The AST node. Raises: LastNodeError: When the node does not have children. Returns: The child. """ try: return next(ast_children(node)) except StopIteration as error: raise LastNodeError("there are no children node") from error def ast_last_child(node: AST) -> AST: """Return the lasts child of this node. Parameters: node: The AST node. Raises: LastNodeError: When the node does not have children. Returns: The child. """ try: *_, last = ast_children(node) except ValueError as error: raise LastNodeError("there are no children node") from error return last python-griffe-1.6.2/src/_griffe/agents/nodes/runtime.py0000664000175000017500000002531314767006246023021 0ustar carstencarsten# This module contains utilities for extracting information from runtime objects. from __future__ import annotations import inspect import sys from functools import cached_property from types import GetSetDescriptorType from typing import TYPE_CHECKING, Any, ClassVar from _griffe.enumerations import ObjectKind from _griffe.logger import logger if TYPE_CHECKING: from collections.abc import Sequence _builtin_module_names = {_.lstrip("_") for _ in sys.builtin_module_names} _cyclic_relationships = { ("os", "nt"), ("os", "posix"), ("numpy.core._multiarray_umath", "numpy.core.multiarray"), } def _same_components(a: str, b: str, /) -> bool: return [cpn.lstrip("_") for cpn in a.split(".")] == [cpn.lstrip("_") for cpn in b.split(".")] class ObjectNode: """Helper class to represent an object tree. It's not really a tree but more a backward-linked list: each node has a reference to its parent, but not to its child (for simplicity purposes and to avoid bugs). Each node stores an object, its name, and a reference to its parent node. """ 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. Parameters: obj: A Python object. name: The object's name. parent: The object's parent node. """ # Unwrap object. try: obj = inspect.unwrap(obj) except Exception as error: # noqa: BLE001 # `inspect.unwrap` at some point runs `hasattr(obj, "__wrapped__")`, # 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("Could not unwrap %s: %r", name, error) # Unwrap cached properties (`inspect.unwrap` doesn't do that). if isinstance(obj, cached_property): is_cached_property = True obj = obj.func else: is_cached_property = False self.obj: Any = obj """The actual Python object.""" self.name: str = name """The Python object's name.""" self.parent: ObjectNode | None = parent """The parent node.""" self.is_cached_property: bool = is_cached_property """Whether this node's object is a cached property.""" def __repr__(self) -> str: return f"ObjectNode(name={self.name!r})" @property def path(self) -> str: """The object's (Python) path.""" if self.parent is None: return self.name return f"{self.parent.path}.{self.name}" @property def module(self) -> ObjectNode: """The object's module, fetched from the node tree.""" if self.is_module: return self if self.parent is not None: return self.parent.module raise ValueError(f"Object node {self.path} does not have a parent module") @property def module_path(self) -> str | None: """The object's module path.""" try: return self.obj.__module__ except AttributeError: try: module = inspect.getmodule(self.obj) or self.module.obj except ValueError: return None try: return module.__spec__.name # type: ignore[union-attr] except AttributeError: return getattr(module, "__name__", None) @property def kind(self) -> ObjectKind: """The kind of this node.""" if self.is_module: return ObjectKind.MODULE if self.is_class: return ObjectKind.CLASS if self.is_staticmethod: return ObjectKind.STATICMETHOD if self.is_classmethod: return ObjectKind.CLASSMETHOD if self.is_cached_property: return ObjectKind.CACHED_PROPERTY if self.is_method: return ObjectKind.METHOD if self.is_builtin_method: return ObjectKind.BUILTIN_METHOD if self.is_coroutine: return ObjectKind.COROUTINE if self.is_builtin_function: return ObjectKind.BUILTIN_FUNCTION if self.is_method_descriptor: return ObjectKind.METHOD_DESCRIPTOR if self.is_function: return ObjectKind.FUNCTION if self.is_getset_descriptor: return ObjectKind.GETSET_DESCRIPTOR if self.is_property: return ObjectKind.PROPERTY return ObjectKind.ATTRIBUTE @cached_property def children(self) -> Sequence[ObjectNode]: """The children of this node.""" children = [] for name, member in inspect.getmembers(self.obj): if self._pick_member(name, member): children.append(ObjectNode(member, name, parent=self)) return children @cached_property def is_module(self) -> bool: """Whether this node's object is a module.""" return inspect.ismodule(self.obj) @cached_property def is_class(self) -> bool: """Whether this node's object is a class.""" return inspect.isclass(self.obj) @cached_property def is_function(self) -> bool: """Whether this node's object is a function.""" # `inspect.isfunction` returns `False` for partials. return inspect.isfunction(self.obj) or (callable(self.obj) and not self.is_class) @cached_property def is_builtin_function(self) -> bool: """Whether this node's object is a builtin function.""" return inspect.isbuiltin(self.obj) @cached_property def is_coroutine(self) -> bool: """Whether this node's object is a coroutine.""" return inspect.iscoroutinefunction(self.obj) @cached_property def is_getset_descriptor(self) -> bool: """Whether this node's object is a get/set descriptor.""" return isinstance(self.obj, GetSetDescriptorType) @cached_property def is_property(self) -> bool: """Whether this node's object is a property.""" return isinstance(self.obj, property) or self.is_cached_property @cached_property def parent_is_class(self) -> bool: """Whether the object of this node's parent is a class.""" return bool(self.parent and self.parent.is_class) @cached_property def is_method(self) -> bool: """Whether this node's object is a method.""" function_type = type(lambda: None) return self.parent_is_class and isinstance(self.obj, function_type) @cached_property def is_method_descriptor(self) -> bool: """Whether this node's object is a method descriptor. Built-in methods (e.g. those implemented in C/Rust) are often method descriptors, rather than normal methods. """ return inspect.ismethoddescriptor(self.obj) @cached_property def is_builtin_method(self) -> bool: """Whether this node's object is a builtin method.""" return self.is_builtin_function and self.parent_is_class @cached_property def is_staticmethod(self) -> bool: """Whether this node's object is a staticmethod.""" if self.parent is None: return False try: self_from_parent = self.parent.obj.__dict__.get(self.name, None) except AttributeError: return False return self.parent_is_class and isinstance(self_from_parent, staticmethod) @cached_property def is_classmethod(self) -> bool: """Whether this node's object is a classmethod.""" if self.parent is None: return False try: self_from_parent = self.parent.obj.__dict__.get(self.name, None) except AttributeError: return False return self.parent_is_class and isinstance(self_from_parent, classmethod) @cached_property def is_attribute(self) -> bool: """Whether this node's object is an attribute.""" return self.kind is ObjectKind.ATTRIBUTE @cached_property def _ids(self) -> set[int]: if self.parent is None: return {id(self.obj)} return {id(self.obj)} | self.parent._ids def _pick_member(self, name: str, member: Any) -> bool: return ( name not in self.exclude_specials and member is not type and member is not object and id(member) not in self._ids and name in vars(self.obj) ) @cached_property def alias_target_path(self) -> str | None: """Alias target path of this node, if the node should be an alias.""" # Top-level objects can't have been imported. if self.parent is None: return None # We can't ever know if an attribute was imported. if self.is_attribute: return None # Get the path of the module the child object was declared in. child_module_path = self.module_path if not child_module_path: return None # Get the path of the module the parent object was declared in. parent_module_path = self.parent.module_path if not parent_module_path: return None # Special cases: break cycles. if (parent_module_path, child_module_path) in _cyclic_relationships: return None # If the current object was declared in the same module as its parent, # or in a module with the same path components but starting/not starting with underscores, # we don't want to alias it. Examples: (a, a), (a, _a), (_a, a), (_a, _a), # (a.b, a.b), (a.b, _a.b), (_a._b, a.b), (a._b, _a.b), etc.. if _same_components(parent_module_path, child_module_path): return None # If the current object was declared in any other module, we alias it. # We remove the leading underscore from the child module path # if it's a built-in module (e.g. _io -> io). That's because objects # in built-in modules inconsistently lie about their module path, # so we prefer to use the non-underscored (public) version, # as users most likely import from the public module and not the private one. if child_module_path.lstrip("_") in _builtin_module_names: child_module_path = child_module_path.lstrip("_") # Child object is a module, return its path directly. if self.is_module: return child_module_path # Rebuild the child object path. child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :]) return f"{child_module_path}.{child_name}" python-griffe-1.6.2/src/_griffe/agents/nodes/assignments.py0000664000175000017500000000324414767006246023670 0ustar carstencarsten# This module contains utilities for extracting information from assignment nodes. from __future__ import annotations import ast from typing import Any, Callable def _get_attribute_name(node: ast.Attribute) -> str: return f"{get_name(node.value)}.{node.attr}" def _get_name_name(node: ast.Name) -> str: return node.id _node_name_map: dict[type, Callable[[Any], str]] = { ast.Name: _get_name_name, ast.Attribute: _get_attribute_name, } def get_name(node: ast.AST) -> str: """Extract name from an assignment node. Parameters: node: The node to extract names from. Returns: A list of names. """ return _node_name_map[type(node)](node) def _get_assign_names(node: ast.Assign) -> list[str]: names = (get_name(target) for target in node.targets) return [name for name in names if name] def _get_annassign_names(node: ast.AnnAssign) -> list[str]: name = get_name(node.target) return [name] if name else [] _node_names_map: dict[type, Callable[[Any], list[str]]] = { ast.Assign: _get_assign_names, ast.AnnAssign: _get_annassign_names, } def get_names(node: ast.AST) -> list[str]: """Extract names from an assignment node. Parameters: node: The node to extract names from. Returns: A list of names. """ return _node_names_map[type(node)](node) def get_instance_names(node: ast.AST) -> list[str]: """Extract names from an assignment node, only for instance attributes. Parameters: node: The node to extract names from. Returns: A list of names. """ return [name.split(".", 1)[1] for name in get_names(node) if name.startswith("self.")] python-griffe-1.6.2/src/_griffe/agents/nodes/imports.py0000664000175000017500000000202014767006246023021 0ustar carstencarsten# This module contains utilities for working with imports and relative imports. from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: import ast from _griffe.models import Module def relative_to_absolute(node: ast.ImportFrom, name: ast.alias, current_module: Module) -> str: """Convert a relative import path to an absolute one. Parameters: node: The "from ... import ..." AST node. name: The imported name. current_module: The module in which the import happens. Returns: The absolute import path. """ level = node.level if (level > 0 and current_module.is_package) or current_module.is_subpackage: level -= 1 while level > 0 and current_module.parent is not None: current_module = current_module.parent # type: ignore[assignment] level -= 1 base = current_module.path + "." if node.level > 0 else "" node_module = node.module + "." if node.module else "" return base + node_module + name.name python-griffe-1.6.2/src/_griffe/agents/nodes/values.py0000664000175000017500000000226514767006246022636 0ustar carstencarsten# This module contains utilities for extracting attribute values. from __future__ import annotations import ast from ast import unparse from typing import TYPE_CHECKING from _griffe.logger import logger if TYPE_CHECKING: from pathlib import Path def get_value(node: ast.AST | None) -> str | None: """Get the string representation of a node. Parameters: node: The node to represent. Returns: The representing code for the node. """ if node is None: return None return unparse(node) def safe_get_value(node: ast.AST | None, filepath: str | Path | None = None) -> str | None: """Safely (no exception) get the string representation of a node. Parameters: node: The node to represent. filepath: An optional filepath from where the node comes. Returns: The representing code for the node. """ try: return get_value(node) except Exception as error: message = f"Failed to represent node {node}" if filepath: message += f" at {filepath}:{node.lineno}" # type: ignore[union-attr] message += f": {error}" logger.exception(message) return None python-griffe-1.6.2/src/_griffe/agents/__init__.py0000664000175000017500000000011414767006246021755 0ustar carstencarsten# These modules contain the different agents that are able to extract data. python-griffe-1.6.2/src/_griffe/agents/inspector.py0000664000175000017500000005301114767006246022230 0ustar carstencarsten# This module contains our dynamic analysis agent, # capable of inspecting modules and objects in memory, at runtime. from __future__ import annotations import ast from inspect import Parameter as SignatureParameter from inspect import Signature, cleandoc, getsourcelines from inspect import signature as getsignature from typing import TYPE_CHECKING, Any from _griffe.agents.nodes.runtime import ObjectNode from _griffe.collections import LinesCollection, ModulesCollection from _griffe.enumerations import Kind, 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 logger from _griffe.models import Alias, Attribute, Class, Docstring, Function, Module, Parameter, Parameters if TYPE_CHECKING: from collections.abc import Sequence from pathlib import Path from _griffe.docstrings.parsers import DocstringStyle from _griffe.enumerations import Parser from _griffe.expressions import Expr _empty = Signature.empty def inspect( module_name: str, *, filepath: Path | None = None, import_paths: Sequence[str | Path] | None = None, extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> Module: """Inspect a module. Sometimes we cannot get the source code of a module or an object, typically built-in modules like `itertools`. The only way to know what they are made of is to actually import them and inspect their contents. Sometimes, even if the source code is available, loading the object is desired because it was created or modified dynamically, and our static agent is not powerful enough to infer all these dynamic modifications. In this case, we load the module using introspection. Griffe therefore provides this function for dynamic analysis. It uses a [`NodeVisitor`][ast.NodeVisitor]-like class, the [`Inspector`][griffe.Inspector], to inspect the module with [`inspect.getmembers()`][inspect.getmembers]. The inspection agent works similarly to the regular [`Visitor`][griffe.Visitor] agent, in that it maintains a state with the current object being handled, and recursively handle its members. Important: This function is generally not used directly. In most cases, users can rely on the [`GriffeLoader`][griffe.GriffeLoader] and its accompanying [`load`][griffe.load] shortcut and their respective options to load modules using dynamic analysis. Parameters: module_name: The module name (as when importing [from] it). filepath: The module file path. import_paths: Paths to import the module from. extensions: The extensions to use when inspecting the module. parent: The optional parent of this module. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. Returns: The module, with its members populated. """ return Inspector( module_name, filepath, extensions or load_extensions(), parent, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, ).get_module(import_paths) class Inspector: """This class is used to instantiate an inspector. Inspectors iterate on objects members to extract data from them. """ def __init__( self, module_name: str, filepath: Path | None, extensions: Extensions, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> None: """Initialize the inspector. Parameters: module_name: The module name. filepath: The optional filepath. extensions: Extensions to use when inspecting. parent: The module parent. docstring_parser: The docstring parser to use. docstring_options: The docstring parsing options. lines_collection: A collection of source code lines. 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 """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: DocstringStyle | 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: # Access `__doc__` directly to avoid taking the `__doc__` attribute from a parent class. value = getattr(node.obj, "__doc__", None) except Exception: # noqa: BLE001 return None if value is None: return None try: # We avoid `inspect.getdoc` to avoid getting # the `__doc__` attribute from a parent class, # but we still want to clean the doc. cleaned = cleandoc(value) except AttributeError: # Triggered on method descriptors. return None return Docstring( cleaned, parser=self.docstring_parser, parser_options=self.docstring_options, ) def _get_linenos(self, node: ObjectNode) -> tuple[int, int] | tuple[None, None]: # Line numbers won't be useful if we don't have the source code. if not self.filepath or self.filepath not in self.lines_collection: return None, None try: lines, lineno = getsourcelines(node.obj) except (OSError, TypeError): return None, None return lineno, lineno + "".join(lines).rstrip().count("\n") def get_module(self, import_paths: Sequence[str | Path] | None = None) -> Module: """Build and return the object representing the module attached to this inspector. This method triggers a complete inspection of the module members. Parameters: import_paths: Paths replacing `sys.path` to import the module. Returns: A module instance. """ import_path = self.module_name if self.parent is not None: import_path = f"{self.parent.path}.{import_path}" # Make sure `import_paths` is a list, in case we want to `insert` into it. import_paths = list(import_paths or ()) # If the thing we want to import has a filepath, # we make sure to insert the right parent directory # at the front of our list of import paths. # We do this by counting the number of dots `.` in the import path, # corresponding to slashes `/` in the filesystem, # and go up in the file tree the same number of times. if self.filepath: parent_path = self.filepath.parent for _ in range(import_path.count(".")): parent_path = parent_path.parent # Climb up one more time for `__init__` modules. if self.filepath.stem == "__init__": parent_path = parent_path.parent if parent_path not in import_paths: import_paths.insert(0, parent_path) value = dynamic_import(import_path, import_paths) # We successfully imported the given object, # and we now create the object tree with all the necessary nodes, # from the root of the package to this leaf object. parent_node = None if self.parent is not None: for part in self.parent.path.split("."): parent_node = ObjectNode(None, name=part, parent=parent_node) module_node = ObjectNode(value, self.module_name, parent=parent_node) self.inspect(module_node) return self.current.module def inspect(self, node: ObjectNode) -> None: """Extend the base inspection with extensions. Parameters: node: The node to inspect. """ getattr(self, f"inspect_{node.kind}", self.generic_inspect)(node) def generic_inspect(self, node: ObjectNode) -> None: """Extend the base generic inspection with extensions. Parameters: node: The node to inspect. """ for child in node.children: if target_path := child.alias_target_path: # If the child is an actual submodule of the current module, # and has no `__file__` set, we won't find it on the disk so we must inspect it now. # For that we instantiate a new inspector and use it to inspect the submodule, # then assign the submodule as member of the current module. # If the submodule has a `__file__` set, the loader should find it on the disk, # 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("Module %s is not discoverable on disk, inspecting right now", target_path) inspector = Inspector( child.name, filepath=None, parent=self.current.module, extensions=self.extensions, docstring_parser=self.docstring_parser, docstring_options=self.docstring_options, lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) inspector.inspect_module(child) self.current.set_member(child.name, inspector.current.module) # Otherwise, alias the object. else: alias = Alias(child.name, target_path) self.current.set_member(child.name, alias) self.extensions.call("on_alias", alias=alias, node=node, agent=self) else: self.inspect(child) def inspect_module(self, node: ObjectNode) -> None: """Inspect a module. Parameters: node: The node to inspect. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_module_node", node=node, agent=self) self.current = module = Module( name=self.module_name, filepath=self.filepath, parent=self.parent, docstring=self._get_docstring(node), lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) self.extensions.call("on_instance", node=node, obj=module, agent=self) self.extensions.call("on_module_instance", node=node, mod=module, agent=self) self.generic_inspect(node) self.extensions.call("on_members", node=node, obj=module, agent=self) self.extensions.call("on_module_members", node=node, mod=module, agent=self) def inspect_class(self, node: ObjectNode) -> None: """Inspect a class. Parameters: node: The node to inspect. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_class_node", node=node, agent=self) bases = [] for base in node.obj.__bases__: if base is object: continue bases.append(f"{base.__module__}.{base.__qualname__}") lineno, endlineno = self._get_linenos(node) class_ = Class( name=node.name, docstring=self._get_docstring(node), bases=bases, lineno=lineno, endlineno=endlineno, ) self.current.set_member(node.name, class_) self.current = class_ self.extensions.call("on_instance", node=node, obj=class_, agent=self) self.extensions.call("on_class_instance", node=node, cls=class_, agent=self) self.generic_inspect(node) self.extensions.call("on_members", node=node, obj=class_, agent=self) self.extensions.call("on_class_members", node=node, cls=class_, agent=self) self.current = self.current.parent # type: ignore[assignment] def inspect_staticmethod(self, node: ObjectNode) -> None: """Inspect a static method. Parameters: node: The node to inspect. """ self.handle_function(node, {"staticmethod"}) def inspect_classmethod(self, node: ObjectNode) -> None: """Inspect a class method. Parameters: node: The node to inspect. """ self.handle_function(node, {"classmethod"}) def inspect_method_descriptor(self, node: ObjectNode) -> None: """Inspect a method descriptor. Parameters: node: The node to inspect. """ self.handle_function(node, {"method descriptor"}) def inspect_builtin_method(self, node: ObjectNode) -> None: """Inspect a builtin method. Parameters: node: The node to inspect. """ self.handle_function(node, {"builtin"}) def inspect_method(self, node: ObjectNode) -> None: """Inspect a method. Parameters: node: The node to inspect. """ self.handle_function(node) def inspect_coroutine(self, node: ObjectNode) -> None: """Inspect a coroutine. Parameters: node: The node to inspect. """ self.handle_function(node, {"async"}) def inspect_builtin_function(self, node: ObjectNode) -> None: """Inspect a builtin function. Parameters: node: The node to inspect. """ self.handle_function(node, {"builtin"}) def inspect_function(self, node: ObjectNode) -> None: """Inspect a function. Parameters: node: The node to inspect. """ self.handle_function(node) def inspect_cached_property(self, node: ObjectNode) -> None: """Inspect a cached property. Parameters: node: The node to inspect. """ self.handle_function(node, {"cached", "property"}) def inspect_property(self, node: ObjectNode) -> None: """Inspect a property. Parameters: node: The node to inspect. """ self.handle_function(node, {"property"}) def inspect_getset_descriptor(self, node: ObjectNode) -> None: """Inspect a get/set descriptor. Parameters: node: The node to inspect. """ self.handle_function(node, {"property"}) def handle_function(self, node: ObjectNode, labels: set | None = None) -> None: """Handle a function. Parameters: node: The node to inspect. labels: Labels to add to the data object. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_function_node", node=node, agent=self) try: signature = getsignature(node.obj) except Exception: # noqa: BLE001 # So many exceptions can be raised here: # AttributeError, NameError, RuntimeError, ValueError, TokenError, TypeError... parameters = None returns = None else: parameters = Parameters( *[_convert_parameter(parameter, parent=self.current) for parameter in signature.parameters.values()], ) return_annotation = signature.return_annotation returns = ( None if return_annotation is _empty else _convert_object_to_annotation(return_annotation, parent=self.current) ) lineno, endlineno = self._get_linenos(node) obj: Attribute | Function labels = labels or set() if "property" in labels: obj = Attribute( name=node.name, value=None, annotation=returns, docstring=self._get_docstring(node), lineno=lineno, endlineno=endlineno, ) else: obj = Function( name=node.name, parameters=parameters, returns=returns, docstring=self._get_docstring(node), lineno=lineno, endlineno=endlineno, ) obj.labels |= labels self.current.set_member(node.name, obj) self.extensions.call("on_instance", node=node, obj=obj, agent=self) if obj.is_attribute: self.extensions.call("on_attribute_instance", node=node, attr=obj, agent=self) else: self.extensions.call("on_function_instance", node=node, func=obj, agent=self) def inspect_attribute(self, node: ObjectNode) -> None: """Inspect an attribute. Parameters: node: The node to inspect. """ self.handle_attribute(node) def handle_attribute(self, node: ObjectNode, annotation: str | Expr | None = None) -> None: """Handle an attribute. Parameters: node: The node to inspect. annotation: A potential annotation. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_attribute_node", node=node, agent=self) # TODO: To improve. parent = self.current labels: set[str] = set() if parent.kind is Kind.MODULE: labels.add("module") elif parent.kind is Kind.CLASS: labels.add("class") elif parent.kind is Kind.FUNCTION: if parent.name != "__init__": return parent = parent.parent # type: ignore[assignment] labels.add("instance") try: value = repr(node.obj) except Exception: # noqa: BLE001 value = None try: docstring = self._get_docstring(node) except Exception: # noqa: BLE001 docstring = None attribute = Attribute( name=node.name, value=value, annotation=annotation, docstring=docstring, ) attribute.labels |= labels parent.set_member(node.name, attribute) if node.name == "__all__": parent.exports = list(node.obj) self.extensions.call("on_instance", node=node, obj=attribute, agent=self) self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) _kind_map = { SignatureParameter.POSITIONAL_ONLY: ParameterKind.positional_only, SignatureParameter.POSITIONAL_OR_KEYWORD: ParameterKind.positional_or_keyword, SignatureParameter.VAR_POSITIONAL: ParameterKind.var_positional, SignatureParameter.KEYWORD_ONLY: ParameterKind.keyword_only, SignatureParameter.VAR_KEYWORD: ParameterKind.var_keyword, } 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) ) kind = _kind_map[parameter.kind] if parameter.default is _empty: default = None elif hasattr(parameter.default, "__name__"): # Avoid `repr` containing chevrons and memory addresses. default = parameter.default.__name__ else: default = repr(parameter.default) return Parameter(name, annotation=annotation, kind=kind, default=default) def _convert_object_to_annotation(obj: Any, parent: Module | Class) -> str | Expr | None: # Even when *we* import future annotations, # the object from which we get a signature # can come from modules which did *not* import them, # so `inspect.signature` returns actual Python objects # that we must deal with. if not isinstance(obj, str): if hasattr(obj, "__name__"): # noqa: SIM108 # Simple types like `int`, `str`, custom classes, etc.. obj = obj.__name__ else: # Other, more complex types: hope for the best. obj = repr(obj) try: annotation_node = compile(obj, mode="eval", filename="<>", flags=ast.PyCF_ONLY_AST, optimize=2) except SyntaxError: return obj return safe_get_annotation(annotation_node.body, parent=parent) # type: ignore[attr-defined] python-griffe-1.6.2/src/_griffe/agents/visitor.py0000664000175000017500000006172014767006246021727 0ustar carstencarsten# This module contains our static analysis agent, # capable of parsing and visiting sources, statically. from __future__ import annotations import ast from contextlib import suppress from typing import TYPE_CHECKING, Any from _griffe.agents.nodes.assignments import get_instance_names, get_names from _griffe.agents.nodes.ast import ( ast_children, ast_kind, ast_next, ) 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, safe_get_base_class, safe_get_condition, safe_get_expression, ) 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.docstrings.parsers import DocstringStyle from _griffe.enumerations import Parser builtin_decorators = { "property": "property", "staticmethod": "staticmethod", "classmethod": "classmethod", } """Mapping of builtin decorators to labels.""" stdlib_decorators = { "abc.abstractmethod": {"abstractmethod"}, "functools.cache": {"cached"}, "functools.cached_property": {"cached", "property"}, "cached_property.cached_property": {"cached", "property"}, "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( module_name: str, filepath: Path, code: str, *, extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> Module: """Parse and visit a module file. We provide this function for static analysis. It uses a [`NodeVisitor`][ast.NodeVisitor]-like class, the [`Visitor`][griffe.Visitor], to compile and parse code (using [`compile`][]) then visit the resulting AST (Abstract Syntax Tree). Important: This function is generally not used directly. In most cases, users can rely on the [`GriffeLoader`][griffe.GriffeLoader] and its accompanying [`load`][griffe.load] shortcut and their respective options to load modules using static analysis. Parameters: module_name: The module name (as when importing [from] it). filepath: The module file path. code: The module contents. extensions: The extensions to use when visiting the AST. parent: The optional parent of this module. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. Returns: The module, with its members populated. """ return Visitor( module_name, filepath, code, extensions or load_extensions(), parent, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, ).get_module() class Visitor: """This class is used to instantiate a visitor. Visitors iterate on AST nodes to extract data from them. """ def __init__( self, module_name: str, filepath: Path, code: str, extensions: Extensions, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> None: """Initialize the visitor. Parameters: module_name: The module name. filepath: The module filepath. code: The module source code. extensions: The extensions to use when visiting. parent: An optional parent for the final module object. docstring_parser: The docstring parser to use. docstring_options: The docstring parsing options. lines_collection: A collection of source code lines. 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 """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: DocstringStyle | 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) if value is None: return None return Docstring( value, lineno=lineno, endlineno=endlineno, parser=self.docstring_parser, parser_options=self.docstring_options, ) def get_module(self) -> Module: """Build and return the object representing the module attached to this visitor. This method triggers a complete visit of the module nodes. Returns: A module instance. """ # Optimization: equivalent to `ast.parse`, but with `optimize=1` to remove assert statements. # TODO: With options, could use `optimize=2` to remove docstrings. top_node = compile(self.code, mode="exec", filename=str(self.filepath), flags=ast.PyCF_ONLY_AST, optimize=1) self.visit(top_node) return self.current.module def visit(self, node: ast.AST) -> None: """Extend the base visit with extensions. Parameters: node: The node to visit. """ getattr(self, f"visit_{ast_kind(node)}", self.generic_visit)(node) def generic_visit(self, node: ast.AST) -> None: """Extend the base generic visit with extensions. Parameters: node: The node to visit. """ for child in ast_children(node): self.visit(child) def visit_module(self, node: ast.Module) -> None: """Visit a module node. Parameters: node: The node to visit. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_module_node", node=node, agent=self) self.current = module = Module( name=self.module_name, filepath=self.filepath, parent=self.parent, docstring=self._get_docstring(node), lines_collection=self.lines_collection, modules_collection=self.modules_collection, ) self.extensions.call("on_instance", node=node, obj=module, agent=self) self.extensions.call("on_module_instance", node=node, mod=module, agent=self) self.generic_visit(node) self.extensions.call("on_members", node=node, obj=module, agent=self) self.extensions.call("on_module_members", node=node, mod=module, agent=self) def visit_classdef(self, node: ast.ClassDef) -> None: """Visit a class definition node. Parameters: node: The node to visit. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_class_node", node=node, agent=self) # Handle decorators. decorators: list[Decorator] = [] if node.decorator_list: lineno = node.decorator_list[0].lineno decorators.extend( Decorator( safe_get_expression(decorator_node, parent=self.current, parse_strings=False), # type: ignore[arg-type] lineno=decorator_node.lineno, endlineno=decorator_node.end_lineno, ) for decorator_node in node.decorator_list ) else: lineno = node.lineno # Handle base classes. bases = [safe_get_base_class(base, parent=self.current) for base in node.bases] class_ = Class( name=node.name, lineno=lineno, endlineno=node.end_lineno, docstring=self._get_docstring(node), decorators=decorators, bases=bases, # type: ignore[arg-type] runtime=not self.type_guarded, ) class_.labels |= self.decorators_to_labels(decorators) self.current.set_member(node.name, class_) self.current = class_ self.extensions.call("on_instance", node=node, obj=class_, agent=self) self.extensions.call("on_class_instance", node=node, cls=class_, agent=self) self.generic_visit(node) self.extensions.call("on_members", node=node, obj=class_, agent=self) self.extensions.call("on_class_members", node=node, cls=class_, agent=self) self.current = self.current.parent # type: ignore[assignment] def decorators_to_labels(self, decorators: list[Decorator]) -> set[str]: """Build and return a set of labels based on decorators. Parameters: decorators: The decorators to check. Returns: A set of labels. """ labels = set() for decorator in decorators: callable_path = decorator.callable_path if callable_path in builtin_decorators: labels.add(builtin_decorators[callable_path]) elif callable_path in stdlib_decorators: labels |= stdlib_decorators[callable_path] return labels def get_base_property(self, decorators: list[Decorator], function: Function) -> str | None: """Check decorators to return the base property in case of setters and deleters. Parameters: decorators: The decorators to check. Returns: base_property: The property for which the setter/deleted is set. property_function: Either `"setter"` or `"deleter"`. """ for decorator in decorators: try: path, prop_function = decorator.callable_path.rsplit(".", 1) except ValueError: continue property_setter_or_deleter = ( prop_function in {"setter", "deleter"} and path == function.path and self.current.get_member(function.name).has_labels("property") ) if property_setter_or_deleter: return prop_function return None def handle_function(self, node: ast.AsyncFunctionDef | ast.FunctionDef, labels: set | None = None) -> None: """Handle a function definition node. Parameters: node: The node to visit. labels: Labels to add to the data object. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_function_node", node=node, agent=self) labels = labels or set() # Handle decorators. decorators = [] overload = False if node.decorator_list: lineno = node.decorator_list[0].lineno for decorator_node in node.decorator_list: decorator_value = safe_get_expression(decorator_node, parent=self.current, parse_strings=False) if decorator_value is None: continue decorator = Decorator( decorator_value, lineno=decorator_node.lineno, endlineno=decorator_node.end_lineno, ) decorators.append(decorator) overload |= decorator.callable_path in typing_overload else: lineno = node.lineno labels |= self.decorators_to_labels(decorators) if "property" in labels: attribute = Attribute( name=node.name, value=None, annotation=safe_get_annotation(node.returns, parent=self.current), lineno=node.lineno, endlineno=node.end_lineno, docstring=self._get_docstring(node), runtime=not self.type_guarded, ) attribute.labels |= labels self.current.set_member(node.name, attribute) self.extensions.call("on_instance", node=node, obj=attribute, agent=self) self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) return # Handle parameters. parameters = Parameters( *[ Parameter( name, kind=kind, annotation=safe_get_annotation(annotation, parent=self.current), default=default if isinstance(default, str) else safe_get_expression(default, parent=self.current, parse_strings=False), ) for name, annotation, kind, default in get_parameters(node.args) ], ) function = Function( name=node.name, lineno=lineno, endlineno=node.end_lineno, parameters=parameters, returns=safe_get_annotation(node.returns, parent=self.current), decorators=decorators, docstring=self._get_docstring(node), runtime=not self.type_guarded, parent=self.current, ) property_function = self.get_base_property(decorators, function) if overload: self.current.overloads[function.name].append(function) elif property_function: base_property: Attribute = self.current.members[node.name] # type: ignore[assignment] if property_function == "setter": base_property.setter = function base_property.labels.add("writable") elif property_function == "deleter": base_property.deleter = function base_property.labels.add("deletable") else: self.current.set_member(node.name, function) if self.current.kind in {Kind.MODULE, Kind.CLASS} and self.current.overloads[function.name]: function.overloads = self.current.overloads[function.name] del self.current.overloads[function.name] function.labels |= labels self.extensions.call("on_instance", node=node, obj=function, agent=self) self.extensions.call("on_function_instance", node=node, func=function, agent=self) if self.current.kind is Kind.CLASS and function.name == "__init__": self.current = function # type: ignore[assignment] self.generic_visit(node) self.current = self.current.parent # type: ignore[assignment] def visit_functiondef(self, node: ast.FunctionDef) -> None: """Visit a function definition node. Parameters: node: The node to visit. """ self.handle_function(node) def visit_asyncfunctiondef(self, node: ast.AsyncFunctionDef) -> None: """Visit an async function definition node. Parameters: node: The node to visit. """ self.handle_function(node, labels={"async"}) def visit_import(self, node: ast.Import) -> None: """Visit an import node. Parameters: node: The node to visit. """ for name in node.names: alias_path = name.name if name.asname else name.name.split(".", 1)[0] alias_name = name.asname or alias_path.split(".", 1)[0] self.current.imports[alias_name] = alias_path alias = Alias( alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno, runtime=not self.type_guarded, ) self.current.set_member(alias_name, alias) self.extensions.call("on_alias", alias=alias, node=node, agent=self) def visit_importfrom(self, node: ast.ImportFrom) -> None: """Visit an "import from" node. Parameters: node: The node to visit. """ for name in node.names: if not node.module and node.level == 1 and not name.asname and self.current.module.is_init_module: # Special case: when being in `a/__init__.py` and doing `from . import b`, # we are effectively creating a member `b` in `a` that is pointing to `a.b` # -> cyclic alias! In that case, we just skip it, as both the member and module # have the same name and can be accessed the same way. continue alias_path = relative_to_absolute(node, name, self.current.module) if name.name == "*": alias_name = alias_path.replace(".", "/") alias_path = alias_path.replace(".*", "") else: alias_name = name.asname or name.name self.current.imports[alias_name] = alias_path # Do not create aliases pointing to themselves (it happens with # `from package.current_module import Thing as Thing` or # `from . import thing as thing`). if alias_path != f"{self.current.path}.{alias_name}": alias = Alias( alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno, runtime=not self.type_guarded, ) self.current.set_member(alias_name, alias) self.extensions.call("on_alias", alias=alias, node=node, agent=self) def handle_attribute( self, node: ast.Assign | ast.AnnAssign, annotation: str | Expr | None = None, ) -> None: """Handle an attribute (assignment) node. Parameters: node: The node to visit. annotation: A potential annotation. """ self.extensions.call("on_node", node=node, agent=self) self.extensions.call("on_attribute_node", node=node, agent=self) parent = self.current labels = set() if parent.kind is Kind.MODULE: try: names = get_names(node) except KeyError: # Unsupported nodes, like subscript. return labels.add("module-attribute") elif parent.kind is Kind.CLASS: try: names = get_names(node) except KeyError: # Unsupported nodes, like subscript. return if isinstance(annotation, Expr) and annotation.is_classvar: # Explicit `ClassVar`: class attribute only. annotation = annotation.slice # type: ignore[attr-defined] labels.add("class-attribute") elif node.value: # Attribute assigned at class-level: available in instances as well. labels.add("class-attribute") labels.add("instance-attribute") else: # Annotated attribute only: not available at class-level. labels.add("instance-attribute") elif parent.kind is Kind.FUNCTION: if parent.name != "__init__": return try: names = get_instance_names(node) except KeyError: # Unsupported nodes, like subscript. return parent = parent.parent # type: ignore[assignment] labels.add("instance-attribute") if not names: return value = safe_get_expression(node.value, parent=self.current, parse_strings=False) try: docstring = self._get_docstring(ast_next(node), strict=True) except (LastNodeError, AttributeError): docstring = None for name in names: # TODO: Handle assigns like `x.y = z`. # We need to resolve `x.y` and add `z` in its members. if "." in name: continue if name in parent.members: # Assigning multiple times. # TODO: Might be better to inspect. if isinstance(node.parent, (ast.If, ast.ExceptHandler)): # type: ignore[union-attr] continue # Prefer "no-exception" case. existing_member = parent.members[name] with suppress(AliasResolutionError, CyclicAliasError): labels |= existing_member.labels # Forward previous docstring and annotation instead of erasing them. if existing_member.docstring and not docstring: docstring = existing_member.docstring with suppress(AttributeError): if existing_member.annotation and not annotation: # type: ignore[union-attr] annotation = existing_member.annotation # type: ignore[union-attr] attribute = Attribute( name=name, value=value, annotation=annotation, lineno=node.lineno, endlineno=node.end_lineno, docstring=docstring, runtime=not self.type_guarded, ) attribute.labels |= labels parent.set_member(name, attribute) if name == "__all__": with suppress(AttributeError): parent.exports = [ name if isinstance(name, str) else ExprName(name.name, parent=name.parent) for name in safe_get__all__(node, self.current) # type: ignore[arg-type] ] self.extensions.call("on_instance", node=node, obj=attribute, agent=self) self.extensions.call("on_attribute_instance", node=node, attr=attribute, agent=self) def visit_assign(self, node: ast.Assign) -> None: """Visit an assignment node. Parameters: node: The node to visit. """ self.handle_attribute(node) def visit_annassign(self, node: ast.AnnAssign) -> None: """Visit an annotated assignment node. Parameters: node: The node to visit. """ self.handle_attribute(node, safe_get_annotation(node.annotation, parent=self.current)) def visit_augassign(self, node: ast.AugAssign) -> None: """Visit an augmented assignment node. Parameters: node: The node to visit. """ with suppress(AttributeError): all_augment = ( node.target.id == "__all__" # type: ignore[union-attr] and self.current.is_module and isinstance(node.op, ast.Add) ) if all_augment: # We assume `exports` is not `None` at this point. self.current.exports.extend( # type: ignore[union-attr] [ name if isinstance(name, str) else ExprName(name.name, parent=name.parent) for name in safe_get__all__(node, self.current) # type: ignore[arg-type] ], ) def visit_if(self, node: ast.If) -> None: """Visit an "if" node. Parameters: node: The node to visit. """ if isinstance(node.parent, (ast.Module, ast.ClassDef)): # type: ignore[attr-defined] condition = safe_get_condition(node.test, parent=self.current, log_level=None) if str(condition) in {"typing.TYPE_CHECKING", "TYPE_CHECKING"}: self.type_guarded = True self.generic_visit(node) self.type_guarded = False python-griffe-1.6.2/src/_griffe/encoders.py0000664000175000017500000002122114767006246020541 0ustar carstencarsten# This module contains data encoders/serializers and decoders/deserializers. # We only support JSON for now, but might want to add more formats in the future. from __future__ import annotations import json from pathlib import Path, PosixPath, WindowsPath from typing import Any, Callable from _griffe import expressions from _griffe.enumerations import Kind, ParameterKind from _griffe.models import ( Alias, Attribute, Class, Decorator, Docstring, Function, Module, Object, Parameter, Parameters, ) _json_encoder_map: dict[type, Callable[[Any], Any]] = { Path: str, PosixPath: str, WindowsPath: str, set: sorted, } class JSONEncoder(json.JSONEncoder): """JSON encoder. JSON encoders can be used directly, or through the [`json.dump`][] or [`json.dumps`][] methods. Examples: >>> from griffe import JSONEncoder >>> JSONEncoder(full=True).encode(..., **kwargs) >>> import json >>> from griffe import JSONEncoder >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs) """ def __init__( self, *args: Any, full: bool = False, **kwargs: Any, ) -> None: """Initialize the encoder. Parameters: *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.json_decoder], you don't need the full data as it can be inferred again using the base data. If you want to feed a non-Python tool instead, dump the full data. **kwargs: See [`json.JSONEncoder`][]. """ super().__init__(*args, **kwargs) self.full: bool = full """Whether to dump full data or base data.""" def default(self, obj: Any) -> Any: """Return a serializable representation of the given object. Parameters: obj: The object to serialize. Returns: A serializable representation. """ try: return obj.as_dict(full=self.full) except AttributeError: return _json_encoder_map.get(type(obj), super().default)(obj) def _load_docstring(obj_dict: dict) -> Docstring | None: if "docstring" in obj_dict: return Docstring(**obj_dict["docstring"]) return None def _load_decorators(obj_dict: dict) -> list[Decorator]: return [Decorator(**dec) for dec in obj_dict.get("decorators", [])] def _load_expression(expression: dict) -> expressions.Expr: # The expression class name is stored in the `cls` key-value. cls = getattr(expressions, expression.pop("cls")) expr = cls(**expression) # For attributes, we need to re-attach names (`values`) together, # as a single linked list, from right to left: # in `a.b.c`, `c` links to `b` which links to `a`. # In `(a or b).c` however, `c` does not link to `(a or b)`, # as `(a or b)` is not a name and wouldn't allow to resolve `c`. if cls is expressions.ExprAttribute: previous = None for value in expr.values: if previous is not None: value.parent = previous if isinstance(value, expressions.ExprName): previous = value return expr def _load_parameter(obj_dict: dict[str, Any]) -> Parameter: return Parameter( obj_dict["name"], annotation=obj_dict["annotation"], kind=ParameterKind(obj_dict["kind"]), default=obj_dict["default"], docstring=_load_docstring(obj_dict), ) def _attach_parent_to_expr(expr: expressions.Expr | str | None, parent: Module | Class) -> None: if not isinstance(expr, expressions.Expr): return for elem in expr: if isinstance(elem, expressions.ExprName): elem.parent = parent elif isinstance(elem, expressions.ExprAttribute) and isinstance(elem.first, expressions.ExprName): elem.first.parent = parent def _attach_parent_to_exprs(obj: Class | Function | Attribute, parent: Module | Class) -> None: # Every name and attribute expression must be reattached # to its parent Griffe object (using its `parent` attribute), # to allow resolving names. if isinstance(obj, Class): if obj.docstring: _attach_parent_to_expr(obj.docstring.value, parent) for decorator in obj.decorators: _attach_parent_to_expr(decorator.value, parent) elif isinstance(obj, Function): if obj.docstring: _attach_parent_to_expr(obj.docstring.value, parent) for decorator in obj.decorators: _attach_parent_to_expr(decorator.value, parent) for param in obj.parameters: _attach_parent_to_expr(param.annotation, parent) _attach_parent_to_expr(param.default, parent) _attach_parent_to_expr(obj.returns, parent) elif isinstance(obj, Attribute): if obj.docstring: _attach_parent_to_expr(obj.docstring.value, parent) _attach_parent_to_expr(obj.value, parent) def _load_module(obj_dict: dict[str, Any]) -> Module: module = Module(name=obj_dict["name"], filepath=Path(obj_dict["filepath"]), docstring=_load_docstring(obj_dict)) # YORE: Bump 2: Replace line with `members = obj_dict.get("members", {}).values()`. members = obj_dict.get("members", []) # YORE: Bump 2: Remove block. if isinstance(members, dict): members = members.values() for module_member in members: module.set_member(module_member.name, module_member) _attach_parent_to_exprs(module_member, module) module.labels |= set(obj_dict.get("labels", ())) return module def _load_class(obj_dict: dict[str, Any]) -> Class: class_ = Class( name=obj_dict["name"], lineno=obj_dict["lineno"], endlineno=obj_dict.get("endlineno"), docstring=_load_docstring(obj_dict), decorators=_load_decorators(obj_dict), bases=obj_dict["bases"], ) # YORE: Bump 2: Replace line with `members = obj_dict.get("members", {}).values()`. members = obj_dict.get("members", []) # YORE: Bump 2: Remove block. if isinstance(members, dict): members = members.values() for class_member in members: class_.set_member(class_member.name, class_member) _attach_parent_to_exprs(class_member, class_) class_.labels |= set(obj_dict.get("labels", ())) _attach_parent_to_exprs(class_, class_) return class_ def _load_function(obj_dict: dict[str, Any]) -> Function: function = Function( name=obj_dict["name"], parameters=Parameters(*obj_dict["parameters"]), returns=obj_dict["returns"], decorators=_load_decorators(obj_dict), lineno=obj_dict["lineno"], endlineno=obj_dict.get("endlineno"), docstring=_load_docstring(obj_dict), ) function.labels |= set(obj_dict.get("labels", ())) return function def _load_attribute(obj_dict: dict[str, Any]) -> Attribute: attribute = Attribute( name=obj_dict["name"], lineno=obj_dict["lineno"], endlineno=obj_dict.get("endlineno"), docstring=_load_docstring(obj_dict), value=obj_dict.get("value"), annotation=obj_dict.get("annotation"), ) attribute.labels |= set(obj_dict.get("labels", ())) return attribute def _load_alias(obj_dict: dict[str, Any]) -> Alias: return Alias( name=obj_dict["name"], target=obj_dict["target_path"], lineno=obj_dict["lineno"], endlineno=obj_dict.get("endlineno"), ) _loader_map: dict[Kind, Callable[[dict[str, Any]], Module | Class | Function | Attribute | Alias]] = { Kind.MODULE: _load_module, Kind.CLASS: _load_class, Kind.FUNCTION: _load_function, Kind.ATTRIBUTE: _load_attribute, Kind.ALIAS: _load_alias, } def json_decoder(obj_dict: dict[str, Any]) -> dict[str, Any] | Object | Alias | Parameter | str | expressions.Expr: """Decode dictionaries as data classes. The [`json.loads`][] method walks the tree from bottom to top. Examples: >>> import json >>> from griffe import json_decoder >>> json.loads(..., object_hook=json_decoder) Parameters: obj_dict: The dictionary to decode. Returns: An instance of a data class. """ # Load expressions. if "cls" in obj_dict: return _load_expression(obj_dict) # Load objects and parameters. if "kind" in obj_dict: try: kind = Kind(obj_dict["kind"]) except ValueError: return _load_parameter(obj_dict) return _loader_map[kind](obj_dict) # Return dict as is. return obj_dict python-griffe-1.6.2/src/_griffe/py.typed0000664000175000017500000000000014767006246020054 0ustar carstencarstenpython-griffe-1.6.2/src/_griffe/exceptions.py0000664000175000017500000000473714767006246021135 0ustar carstencarsten# This module contains all the exceptions specific to Griffe. from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from _griffe.models import Alias class GriffeError(Exception): """The base exception for all Griffe errors.""" class LoadingError(GriffeError): """The base exception for all Griffe errors.""" class NameResolutionError(GriffeError): """Exception for names that cannot be resolved in a object scope.""" class UnhandledEditableModuleError(GriffeError): """Exception for unhandled editables modules, when searching modules.""" class UnimportableModuleError(GriffeError): """Exception for modules that cannot be imported.""" class AliasResolutionError(GriffeError): """Exception for alias that cannot be resolved.""" def __init__(self, alias: Alias) -> None: """Initialize the exception. Parameters: alias: The alias that could not be resolved. """ self.alias: Alias = alias """The alias that triggered the error.""" message = f"Could not resolve alias {alias.path} pointing at {alias.target_path}" try: filepath = alias.parent.relative_filepath # type: ignore[union-attr] except BuiltinModuleError: pass else: message += f" (in {filepath}:{alias.alias_lineno})" super().__init__(message) class CyclicAliasError(GriffeError): """Exception raised when a cycle is detected in aliases.""" def __init__(self, chain: list[str]) -> None: """Initialize the exception. Parameters: chain: The cyclic chain of items (such as target path). """ self.chain: list[str] = chain """The chain of aliases that created the cycle.""" super().__init__("Cyclic aliases detected:\n " + "\n ".join(self.chain)) class LastNodeError(GriffeError): """Exception raised when trying to access a next or previous node.""" class RootNodeError(GriffeError): """Exception raised when trying to use siblings properties on a root node.""" class BuiltinModuleError(GriffeError): """Exception raised when trying to access the filepath of a builtin module.""" class ExtensionError(GriffeError): """Base class for errors raised by extensions.""" class ExtensionNotLoadedError(ExtensionError): """Exception raised when an extension could not be loaded.""" class GitError(GriffeError): """Exception raised for errors related to Git.""" python-griffe-1.6.2/src/_griffe/stats.py0000664000175000017500000001231314767006246020077 0ustar carstencarsten# This module contains utilities to compute loading statistics, # like time spent visiting modules statically or dynamically. from __future__ import annotations from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING from _griffe.enumerations import Kind if TYPE_CHECKING: from _griffe.loader import GriffeLoader from _griffe.models import Alias, Object class Stats: """Load statistics for a Griffe loader.""" def __init__(self, loader: GriffeLoader) -> None: """Initialiwe the stats object. Parameters: loader: The loader to compute stats for. """ self.loader = loader """The loader to compute stats for.""" modules_by_extension = defaultdict( int, { "": 0, ".py": 0, ".pyi": 0, ".pyc": 0, ".pyo": 0, ".pyd": 0, ".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) def _itercount(self, root: Object | Alias) -> None: if root.is_alias: return self.by_kind[root.kind] += 1 if root.is_module: if isinstance(root.filepath, Path): self.modules_by_extension[root.filepath.suffix] += 1 elif root.filepath is None: self.modules_by_extension[""] += 1 for member in root.members.values(): self._itercount(member) def as_text(self) -> str: """Format the statistics as text. Returns: Text stats. """ lines = [] packages = self.packages modules = self.by_kind[Kind.MODULE] classes = self.by_kind[Kind.CLASS] functions = self.by_kind[Kind.FUNCTION] attributes = self.by_kind[Kind.ATTRIBUTE] objects = sum((modules, classes, functions, attributes)) lines.append("Statistics") lines.append("---------------------") lines.append("Number of loaded objects") lines.append(f" Modules: {modules}") lines.append(f" Classes: {classes}") lines.append(f" Functions: {functions}") lines.append(f" Attributes: {attributes}") lines.append(f" Total: {objects} across {packages} packages") per_ext = self.modules_by_extension builtin = per_ext[""] regular = per_ext[".py"] stubs = per_ext[".pyi"] compiled = modules - builtin - regular - stubs lines.append("") lines.append(f"Total number of lines: {self.lines}") lines.append("") lines.append("Modules") lines.append(f" Builtin: {builtin}") lines.append(f" Compiled: {compiled}") lines.append(f" Regular: {regular}") lines.append(f" Stubs: {stubs}") lines.append(" Per extension:") for ext, number in sorted(per_ext.items()): if ext: lines.append(f" {ext}: {number}") visit_time = self.time_spent_visiting / 1000 inspect_time = self.time_spent_inspecting / 1000 total_time = visit_time + inspect_time visit_percent = visit_time / total_time * 100 inspect_percent = inspect_time / total_time * 100 force_inspection = self.loader.force_inspection visited_modules = 0 if force_inspection else regular try: visit_time_per_module = visit_time / visited_modules except ZeroDivisionError: visit_time_per_module = 0 inspected_modules = builtin + compiled + (regular if force_inspection else 0) try: inspect_time_per_module = inspect_time / inspected_modules except ZeroDivisionError: inspect_time_per_module = 0 lines.append("") lines.append( f"Time spent visiting modules ({visited_modules}): " f"{visit_time}ms, {visit_time_per_module:.02f}ms/module ({visit_percent:.02f}%)", ) lines.append( f"Time spent inspecting modules ({inspected_modules}): " f"{inspect_time}ms, {inspect_time_per_module:.02f}ms/module ({inspect_percent:.02f}%)", ) serialize_time = self.time_spent_serializing / 1000 serialize_time_per_module = serialize_time / modules lines.append(f"Time spent serializing: {serialize_time}ms, {serialize_time_per_module:.02f}ms/module") return "\n".join(lines) python-griffe-1.6.2/src/_griffe/logger.py0000664000175000017500000000634214767006246020225 0ustar carstencarsten# This module contains the logger used throughout Griffe. # The logger is actually a wrapper around the standard Python logger. # We wrap it so that it is easier for other downstream libraries to patch it. # For example, mkdocstrings-python patches the logger to relocate it as a child # of `mkdocs.plugins` so that it fits in the MkDocs logging configuration. # # We use a single, global logger because our public API is exposed in a single module, `griffe`. # Extensions however should use their own logger, which is why we provide the `get_logger` function. from __future__ import annotations import logging from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Callable, ClassVar if TYPE_CHECKING: from collections.abc import Iterator class Logger: _default_logger: Any = logging.getLogger _instances: ClassVar[dict[str, Logger]] = {} def __init__(self, name: str) -> None: # Default logger that can be patched by third-party. self._logger = self.__class__._default_logger(name) def __getattr__(self, name: str) -> Any: # Forward everything to the logger. return getattr(self._logger, name) @contextmanager def disable(self) -> Iterator[None]: """Temporarily disable logging.""" old_level = self._logger.level self._logger.setLevel(100) try: yield finally: self._logger.setLevel(old_level) @classmethod def _get(cls, name: str = "griffe") -> Logger: if name not in cls._instances: cls._instances[name] = cls(name) return cls._instances[name] @classmethod def _patch_loggers(cls, get_logger_func: Callable) -> None: # Patch current instances. for name, instance in cls._instances.items(): instance._logger = get_logger_func(name) # Future instances will be patched as well. cls._default_logger = get_logger_func logger: Logger = Logger._get() """Our global logger, used throughout the library. Griffe's output and error messages are logging messages. Griffe provides the [`patch_loggers`][griffe.patch_loggers] function so dependent libraries can patch Griffe loggers as they see fit. For example, to fit in the MkDocs logging configuration and prefix each log message with the module name: ```python import logging from griffe import patch_loggers class LoggerAdapter(logging.LoggerAdapter): def __init__(self, prefix, logger): super().__init__(logger, {}) self.prefix = prefix def process(self, msg, kwargs): return f"{self.prefix}: {msg}", kwargs def get_logger(name): logger = logging.getLogger(f"mkdocs.plugins.{name}") return LoggerAdapter(name, logger) patch_loggers(get_logger) ``` """ def get_logger(name: str = "griffe") -> Logger: """Create and return a new logger instance. Parameters: name: The logger name. Returns: The logger. """ return Logger._get(name) def patch_loggers(get_logger_func: Callable[[str], Any]) -> None: """Patch Griffe logger and Griffe extensions' loggers. Parameters: get_logger_func: A function accepting a name as parameter and returning a logger. """ Logger._patch_loggers(get_logger_func) python-griffe-1.6.2/src/_griffe/merger.py0000664000175000017500000001070614767006246020226 0ustar carstencarsten# This module contains utilities to merge stubs data and concrete data. from __future__ import annotations from contextlib import suppress from typing import TYPE_CHECKING from _griffe.exceptions import AliasResolutionError, CyclicAliasError from _griffe.logger import logger if TYPE_CHECKING: from _griffe.models import Attribute, Class, Function, Module, Object def _merge_module_stubs(module: Module, stubs: Module) -> None: _merge_stubs_docstring(module, stubs) _merge_stubs_overloads(module, stubs) _merge_stubs_members(module, stubs) def _merge_class_stubs(class_: Class, stubs: Class) -> None: _merge_stubs_docstring(class_, stubs) _merge_stubs_overloads(class_, stubs) _merge_stubs_members(class_, stubs) def _merge_function_stubs(function: Function, stubs: Function) -> None: _merge_stubs_docstring(function, stubs) for parameter in stubs.parameters: with suppress(KeyError): function.parameters[parameter.name].annotation = parameter.annotation function.returns = stubs.returns def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None: _merge_stubs_docstring(attribute, stubs) attribute.annotation = stubs.annotation def _merge_stubs_docstring(obj: Object, stubs: Object) -> None: if not obj.docstring and stubs.docstring: obj.docstring = stubs.docstring def _merge_stubs_overloads(obj: Module | Class, stubs: Module | Class) -> None: for function_name, overloads in list(stubs.overloads.items()): if overloads: with suppress(KeyError): obj.get_member(function_name).overloads = overloads del stubs.overloads[function_name] def _merge_stubs_members(obj: Module | Class, stubs: Module | Class) -> None: # Merge imports to later know if objects coming from the stubs were imported. obj.imports.update(stubs.imports) for member_name, stub_member in stubs.members.items(): if member_name in obj.members: # We don't merge imported stub objects that already exist in the concrete module. # Stub objects must be defined where they are exposed in the concrete package, # not be imported from other stub modules. if stub_member.is_alias: continue obj_member = obj.get_member(member_name) with suppress(AliasResolutionError, CyclicAliasError): # An object's canonical location can differ from its equivalent stub location. # Devs usually declare stubs at the public location of the corresponding object, # 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: logger.debug( "Cannot merge stubs for %s: kind %s != %s", obj_member.path, stub_member.kind.value, obj_member.kind.value, ) elif obj_member.is_module: _merge_module_stubs(obj_member, stub_member) # type: ignore[arg-type] elif obj_member.is_class: _merge_class_stubs(obj_member, stub_member) # type: ignore[arg-type] elif obj_member.is_function: _merge_function_stubs(obj_member, stub_member) # type: ignore[arg-type] elif obj_member.is_attribute: _merge_attribute_stubs(obj_member, stub_member) # type: ignore[arg-type] else: stub_member.runtime = False obj.set_member(member_name, stub_member) def merge_stubs(mod1: Module, mod2: Module) -> Module: """Merge stubs into a module. Parameters: mod1: A regular module or stubs module. mod2: A regular module or stubs module. Raises: ValueError: When both modules are regular modules (no stubs is passed). Returns: The regular module. """ logger.debug("Trying to merge %s and %s", mod1.filepath, mod2.filepath) if mod1.filepath.suffix == ".pyi": # type: ignore[union-attr] stubs = mod1 module = mod2 elif mod2.filepath.suffix == ".pyi": # type: ignore[union-attr] stubs = mod2 module = mod1 else: raise ValueError("cannot merge regular (non-stubs) modules together") _merge_module_stubs(module, stubs) return module python-griffe-1.6.2/src/_griffe/importer.py0000664000175000017500000001152214767006246020603 0ustar carstencarsten# This module contains utilities to dynamically import objects. # These utilities are used by our [`Inspector`][griffe.Inspector] to dynamically import objects # specified as Python paths, like `package.module.Class.method`. from __future__ import annotations import sys from contextlib import contextmanager from importlib import import_module from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Iterator, Sequence from pathlib import Path def _error_details(error: BaseException, objpath: str) -> str: return f"With sys.path = {sys.path!r}, accessing {objpath!r} raises {error.__class__.__name__}: {error}" @contextmanager def sys_path(*paths: str | Path) -> Iterator[None]: """Redefine `sys.path` temporarily. Parameters: *paths: The paths to use when importing modules. If no paths are given, keep `sys.path` untouched. Yields: Nothing. """ if not paths: yield return old_path = sys.path sys.path = [str(path) for path in paths] try: yield finally: sys.path = old_path def dynamic_import(import_path: str, import_paths: Sequence[str | Path] | None = None) -> Any: """Dynamically import the specified object. It can be a module, class, method, function, attribute, nested arbitrarily. It works like this: - for a given object path `a.b.x.y` - it tries to import `a.b.x.y` as a module (with `importlib.import_module`) - if it fails, it tries again with `a.b.x`, storing `y` - then `a.b`, storing `x.y` - then `a`, storing `b.x.y` - if nothing worked, it raises an error - if one of the iteration worked, it moves on, and... - it tries to get the remaining (stored) parts with `getattr` - for example it gets `b` from `a`, then `x` from `b`, etc. - if a single attribute access fails, it raises an error - if everything worked, it returns the last obtained attribute Since the function potentially tries multiple things before succeeding, all errors happening along the way are recorded, and re-emitted with an `ImportError` when it fails, to let users know what was tried. IMPORTANT: The paths given through the `import_paths` parameter are used to temporarily patch `sys.path`: this function is therefore not thread-safe. IMPORTANT: The paths given as `import_paths` must be *correct*. The contents of `sys.path` must be consistent to what a user of the imported code would expect. Given a set of paths, if the import fails for a user, it will fail here too, with potentially unintuitive errors. If we wanted to make this function more robust, we could add a loop to "roll the window" of given paths, shifting them to the left (for example: `("/a/a", "/a/b", "/a/c/")`, then `("/a/b", "/a/c", "/a/a/")`, then `("/a/c", "/a/a", "/a/b/")`), to make sure each entry is given highest priority at least once, maintaining relative order, but we deem this unnecessary for now. Parameters: import_path: The path of the object to import. import_paths: The (sys) paths to import the object from. Raises: ModuleNotFoundError: When the object's module could not be found. ImportError: When there was an import error or when couldn't get the attribute. Returns: The imported object. """ module_parts: list[str] = import_path.split(".") object_parts: list[str] = [] errors = [] with sys_path(*(import_paths or ())): while module_parts: module_path = ".".join(module_parts) try: module = import_module(module_path) except BaseException as error: # noqa: BLE001 # pyo3's PanicException can only be caught with BaseException. # We do want to catch base exceptions anyway (exit, interrupt, etc.). errors.append(_error_details(error, module_path)) object_parts.insert(0, module_parts.pop(-1)) else: break else: raise ImportError("; ".join(errors)) # Sometimes extra dependencies are not installed, # so importing the leaf module fails with a ModuleNotFoundError, # or later `getattr` triggers additional code that fails. # In these cases, and for consistency, we always re-raise an ImportError # instead of an any other exception (it's called "dynamic import" after all). # See https://github.com/mkdocstrings/mkdocstrings/issues/380. value = module for part in object_parts: try: value = getattr(value, part) except BaseException as error: # noqa: BLE001 errors.append(_error_details(error, module_path + ":" + ".".join(object_parts))) raise ImportError("; ".join(errors)) # noqa: B904 return value python-griffe-1.6.2/src/_griffe/mixins.py0000664000175000017500000004356514767006246020265 0ustar carstencarsten# This module contains some mixins classes that hold shared methods # of the different kinds of objects, and aliases. from __future__ import annotations import json from contextlib import suppress from typing import TYPE_CHECKING, Any, TypeVar from _griffe.enumerations import Kind from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError from _griffe.merger import merge_stubs if TYPE_CHECKING: from collections.abc import Sequence from _griffe.models import Alias, Attribute, Class, Function, Module, Object _ObjType = TypeVar("_ObjType") def _get_parts(key: str | Sequence[str]) -> Sequence[str]: if isinstance(key, str): if not key: raise ValueError("Empty strings are not supported") parts = key.split(".") else: parts = list(key) if not parts: raise ValueError("Empty tuples are not supported") return parts class GetMembersMixin: """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. This method is part of the consumer API: do not use when producing Griffe trees! Members will be looked up in both declared members and inherited ones, triggering computation of the latter. Parameters: key: The name or path of the member. Examples: >>> foo = griffe_object["foo"] >>> bar = griffe_object["path.to.bar"] >>> qux = griffe_object[("path", "to", "qux")] """ parts = _get_parts(key) if len(parts) == 1: return self.all_members[parts[0]] # type: ignore[attr-defined] return self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] def get_member(self, key: str | Sequence[str]) -> Any: """Get a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Members will be looked up in declared members only, not inherited ones. Parameters: key: The name or path of the member. Examples: >>> foo = griffe_object["foo"] >>> bar = griffe_object["path.to.bar"] >>> bar = griffe_object[("path", "to", "bar")] """ parts = _get_parts(key) if len(parts) == 1: return self.members[parts[0]] # type: ignore[attr-defined] 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. 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. This method is part of the consumer API: do not use when producing Griffe trees! Members will be looked up in both declared members and inherited ones, triggering computation of the latter. Parameters: key: The name or path of the member. Examples: >>> del griffe_object["foo"] >>> del griffe_object["path.to.bar"] >>> del griffe_object[("path", "to", "qux")] """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] try: del self.members[name] # type: ignore[attr-defined] except KeyError: del self.inherited_members[name] # type: ignore[attr-defined] else: del self.all_members[parts[0]][parts[1:]] # type: ignore[attr-defined] def del_member(self, key: str | Sequence[str]) -> None: """Delete a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Members will be looked up in declared members only, not inherited ones. Parameters: key: The name or path of the member. Examples: >>> griffe_object.del_member("foo") >>> griffe_object.del_member("path.to.bar") >>> griffe_object.del_member(("path", "to", "qux")) """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] del self.members[name] # type: ignore[attr-defined] else: self.members[parts[0]].del_member(parts[1:]) # type: ignore[attr-defined] class SetMembersMixin: """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. This method is part of the consumer API: do not use when producing Griffe trees! Parameters: key: The name or path of the member. value: The member. Examples: >>> griffe_object["foo"] = foo >>> griffe_object["path.to.bar"] = bar >>> griffe_object[("path", "to", "qux")] = qux """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] self.members[name] = value # type: ignore[attr-defined] if self.is_collection: # type: ignore[attr-defined] value._modules_collection = self # type: ignore[union-attr] else: value.parent = self # type: ignore[assignment] else: self.members[parts[0]][parts[1:]] = value # type: ignore[attr-defined] def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None: """Set a member with its name or path. This method is part of the producer API: you can use it safely while building Griffe trees (for example in Griffe extensions). Parameters: key: The name or path of the member. value: The member. Examples: >>> griffe_object.set_member("foo", foo) >>> griffe_object.set_member("path.to.bar", bar) >>> griffe_object.set_member(("path", "to", "qux"), qux) """ parts = _get_parts(key) if len(parts) == 1: name = parts[0] if name in self.members: # type: ignore[attr-defined] member = self.members[name] # type: ignore[attr-defined] if not member.is_alias: # When reassigning a module to an existing one, # try to merge them as one regular and one stubs module # (implicit support for .pyi modules). if member.is_module and not (member.is_namespace_package or member.is_namespace_subpackage): # Accessing attributes of the value or member can trigger alias errors. # Accessing file paths can trigger a builtin module error. with suppress(AliasResolutionError, CyclicAliasError, BuiltinModuleError): if value.is_module and value.filepath != member.filepath: with suppress(ValueError): value = merge_stubs(member, value) # type: ignore[arg-type] for alias in member.aliases.values(): with suppress(CyclicAliasError): alias.target = value self.members[name] = value # type: ignore[attr-defined] if self.is_collection: # type: ignore[attr-defined] value._modules_collection = self # type: ignore[union-attr] else: value.parent = self # type: ignore[assignment] else: self.members[parts[0]].set_member(parts[1:], value) # type: ignore[attr-defined] class SerializationMixin: """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. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options passed to encoder. Returns: A JSON string. """ from _griffe.encoders import JSONEncoder # Avoid circular import. return json.dumps(self, cls=JSONEncoder, full=full, **kwargs) @classmethod def from_json(cls: type[_ObjType], json_string: str, **kwargs: Any) -> _ObjType: # noqa: PYI019 """Create an instance of this class from a JSON string. Parameters: json_string: JSON to decode into Object. **kwargs: Additional options passed to decoder. Returns: An Object instance. Raises: 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. kwargs.setdefault("object_hook", json_decoder) obj = json.loads(json_string, **kwargs) if not isinstance(obj, cls): raise TypeError(f"provided JSON object is not of type {cls}") return obj class ObjectAliasMixin(GetMembersMixin, SetMembersMixin, DelMembersMixin, SerializationMixin): """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]: """All members (declared and inherited). This method is part of the consumer API: do not use when producing Griffe trees! """ if self.is_class: # type: ignore[attr-defined] return {**self.inherited_members, **self.members} # type: ignore[attr-defined] return self.members # type: ignore[attr-defined] @property def modules(self) -> dict[str, Module]: """The module members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.MODULE} # type: ignore[misc] @property def classes(self) -> dict[str, Class]: """The class members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.CLASS} # type: ignore[misc] @property def functions(self) -> dict[str, Function]: """The function members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.FUNCTION} # type: ignore[misc] @property def attributes(self) -> dict[str, Attribute]: """The attribute members. This method is part of the consumer API: do not use when producing Griffe trees! """ return {name: member for name, member in self.all_members.items() if member.kind is Kind.ATTRIBUTE} # type: ignore[misc] @property def is_private(self) -> bool: """Whether this object/alias is private (starts with `_`) but not special.""" return self.name.startswith("_") and not self.is_special # type: ignore[attr-defined] @property def is_special(self) -> bool: """Whether this object/alias is special ("dunder" attribute/method, starts and end with `__`).""" return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined] @property def is_class_private(self) -> bool: """Whether this object/alias is class-private (starts with `__` and is a class member).""" return self.parent and self.parent.is_class and self.name.startswith("__") and not self.name.endswith("__") # type: ignore[attr-defined] @property def is_imported(self) -> bool: """Whether this object/alias was imported from another module.""" return self.parent and self.name in self.parent.imports # type: ignore[attr-defined] @property def is_exported(self) -> bool: """Whether this object/alias is exported (listed in `__all__`).""" return self.parent.is_module and bool(self.parent.exports and self.name in self.parent.exports) # type: ignore[attr-defined] @property def is_wildcard_exposed(self) -> bool: """Whether this object/alias is exposed to wildcard imports. To be exposed to wildcard imports, an object/alias must: - be available at runtime - have a module as parent - be listed in `__all__` if `__all__` is defined - or not be private (having a name starting with an underscore) Special case for Griffe trees: a submodule is only exposed if its parent imports it. Returns: True or False. """ # If the object is not available at runtime or is not defined at the module level, it is not exposed. if not self.runtime or not self.parent.is_module: # type: ignore[attr-defined] return False # If the parent module defines `__all__`, the object is exposed if it is listed in it. if self.parent.exports is not None: # type: ignore[attr-defined] return self.name in self.parent.exports # type: ignore[attr-defined] # If the object's name starts with an underscore, it is not exposed. # We don't use `is_private` or `is_special` here to avoid redundant string checks. if self.name.startswith("_"): # type: ignore[attr-defined] return False # Special case for Griffe trees: a submodule is only exposed if its parent imports it. return self.is_alias or not self.is_module or self.is_imported # type: ignore[attr-defined] @property def is_public(self) -> bool: """Whether this object is considered public. In modules, developers can mark objects as public thanks to the `__all__` variable. In classes however, there is no convention or standard to do so. 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 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. """ # Give priority to the `public` attribute if it is set. if self.public is not None: # type: ignore[attr-defined] return self.public # type: ignore[attr-defined] # If the object is a module and its name does not start with an underscore, it is public. # Modules are not subject to the `__all__` convention, only the underscore prefix one. if not self.is_alias and self.is_module and not self.name.startswith("_"): # type: ignore[attr-defined] return True # If the object is defined at the module-level and is listed in `__all__`, it is public. # If the parent module defines `__all__` but does not list the object, it is private. if self.parent and self.parent.is_module and bool(self.parent.exports): # type: ignore[attr-defined] return self.name in self.parent.exports # type: ignore[attr-defined] # Special objects are always considered public. # Even if we don't access them directly, they are used through different *public* means # like instantiating classes (`__init__`), using operators (`__eq__`), etc.. if self.is_private: return False # TODO: In a future version, we will support two conventions regarding imports: # - `from a import x as x` marks `x` as public. # - `from a import *` marks all wildcard imported objects as public. if self.is_imported: # noqa: SIM103 return False # If we reached this point, the object is public. return True @property def is_deprecated(self) -> bool: """Whether this object is deprecated.""" # NOTE: We might want to add more ways to detect deprecations in the future. return bool(self.deprecated) # type: ignore[attr-defined] python-griffe-1.6.2/src/_griffe/tests.py0000664000175000017500000003766314767006246020122 0ustar carstencarsten# This module contains helpers. They simplify programmatic use of Griffe, # for example to load data from strings or to create temporary packages. # They are particularly useful for our own tests suite. from __future__ import annotations import sys import tempfile from contextlib import contextmanager from dataclasses import dataclass from importlib import invalidate_caches from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any from _griffe.agents.inspector import inspect from _griffe.agents.visitor import visit from _griffe.collections import LinesCollection from _griffe.loader import load from _griffe.models import Module, Object if TYPE_CHECKING: from collections.abc import Iterator, Mapping, Sequence from _griffe.collections import ModulesCollection from _griffe.docstrings.parsers import DocstringStyle from _griffe.enumerations import Parser from _griffe.extensions.base import Extensions _TMPDIR_PREFIX = "griffe_" @dataclass class TmpPackage: """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.""" name: str """The package name, as to dynamically import it.""" path: Path """The package path.""" def __post_init__(self) -> None: self.tmpdir = self.tmpdir.resolve() self.path = self.path.resolve() @contextmanager def temporary_pyfile(code: str, *, module_name: str = "module") -> Iterator[tuple[str, Path]]: """Create a Python file containing the given code in a temporary directory. Parameters: code: The code to write to the temporary file. module_name: The name of the temporary module. Yields: module_name: The module name, as to dynamically import it. module_path: The module path. """ with tempfile.TemporaryDirectory(prefix=_TMPDIR_PREFIX) as tmpdir: tmpfile = Path(tmpdir) / f"{module_name}.py" tmpfile.write_text(dedent(code)) yield module_name, tmpfile @contextmanager def temporary_pypackage( package: str, modules: Sequence[str] | Mapping[str, str] | None = None, *, init: bool = True, inits: bool = True, ) -> Iterator[TmpPackage]: """Create a package containing the given modules in a temporary directory. Parameters: package: The package name. Example: `"a"` gives a package named `a`, while `"a/b"` gives a namespace package named `a` with a package inside named `b`. If `init` is false, then `b` is also a namespace package. modules: Additional modules to create in the package. If a list, simply touch the files: `["b.py", "c/d.py", "e/f"]`. If a dict, keys are the file names and values their contents: `{"b.py": "b = 1", "c/d.py": "print('hey from c')"}`. init: Whether to create an `__init__` module in the top package. inits: Whether to create `__init__` modules in subpackages. Yields: A temporary package. """ modules = modules or {} if isinstance(modules, list): modules = dict.fromkeys(modules, "") mkdir_kwargs = {"parents": True, "exist_ok": True} with tempfile.TemporaryDirectory(prefix=_TMPDIR_PREFIX) as tmpdir: tmpdirpath = Path(tmpdir) package_name = ".".join(Path(package).parts) package_path = tmpdirpath / package package_path.mkdir(**mkdir_kwargs) if init: package_path.joinpath("__init__.py").touch() for module_name, module_contents in modules.items(): # type: ignore[union-attr] current_path = package_path for part in Path(module_name).parts: if part.endswith((".py", ".pyi")): current_path.joinpath(part).write_text(dedent(module_contents)) else: current_path /= part current_path.mkdir(**mkdir_kwargs) if inits: current_path.joinpath("__init__.py").touch() yield TmpPackage(tmpdirpath, package_name, package_path) @contextmanager def temporary_visited_package( package: str, modules: Sequence[str] | Mapping[str, str] | None = None, *, init: bool = True, inits: bool = True, extensions: Extensions | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, allow_inspection: bool = False, store_source: bool = True, resolve_aliases: bool = False, resolve_external: bool | None = None, resolve_implicit: bool = False, ) -> Iterator[Module]: """Create and visit a temporary package. Parameters: package: The package name. Example: `"a"` gives a package named `a`, while `"a/b"` gives a namespace package named `a` with a package inside named `b`. If `init` is false, then `b` is also a namespace package. modules: Additional modules to create in the package. If a list, simply touch the files: `["b.py", "c/d.py", "e/f"]`. If a dict, keys are the file names and values their contents: `{"b.py": "b = 1", "c/d.py": "print('hey from c')"}`. init: Whether to create an `__init__` module in the top package. inits: Whether to create `__init__` modules in subpackages. extensions: The extensions to use. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. store_source: Whether to store code source in the lines collection. resolve_aliases: Whether to resolve aliases. resolve_external: Whether to try to load unspecified modules to resolve aliases. Default value (`None`) means to load external modules only if they are the private sibling or the origin module (for example when `ast` imports from `_ast`). resolve_implicit: When false, only try to resolve an alias if it is explicitly exported. Yields: A module. """ with temporary_pypackage(package, modules, init=init, inits=inits) as tmp_package: yield load( # type: ignore[misc] tmp_package.name, search_paths=[tmp_package.tmpdir], extensions=extensions, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, allow_inspection=allow_inspection, store_source=store_source, resolve_aliases=resolve_aliases, resolve_external=resolve_external, resolve_implicit=resolve_implicit, force_inspection=False, ) @contextmanager def temporary_inspected_package( package: str, modules: Sequence[str] | Mapping[str, str] | None = None, *, init: bool = True, inits: bool = True, extensions: Extensions | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, allow_inspection: bool = True, store_source: bool = True, resolve_aliases: bool = False, resolve_external: bool | None = None, resolve_implicit: bool = False, ) -> Iterator[Module]: """Create and inspect a temporary package. Parameters: package: The package name. Example: `"a"` gives a package named `a`, while `"a/b"` gives a namespace package named `a` with a package inside named `b`. If `init` is false, then `b` is also a namespace package. modules: Additional modules to create in the package. If a list, simply touch the files: `["b.py", "c/d.py", "e/f"]`. If a dict, keys are the file names and values their contents: `{"b.py": "b = 1", "c/d.py": "print('hey from c')"}`. init: Whether to create an `__init__` module in the top package. inits: Whether to create `__init__` modules in subpackages. extensions: The extensions to use. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. allow_inspection: Whether to allow inspecting modules. store_source: Whether to store code source in the lines collection. resolve_aliases: Whether to resolve aliases. resolve_external: Whether to try to load unspecified modules to resolve aliases. Default value (`None`) means to load external modules only if they are the private sibling or the origin module (for example when `ast` imports from `_ast`). resolve_implicit: When false, only try to resolve an alias if it is explicitly exported. Yields: A module. """ with temporary_pypackage(package, modules, init=init, inits=inits) as tmp_package: try: yield load( # type: ignore[misc] tmp_package.name, search_paths=[tmp_package.tmpdir], extensions=extensions, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, allow_inspection=allow_inspection, store_source=store_source, resolve_aliases=resolve_aliases, resolve_external=resolve_external, resolve_implicit=resolve_implicit, force_inspection=True, ) finally: for name in tuple(sys.modules.keys()): if name == package or name.startswith(f"{package}."): sys.modules.pop(name, None) invalidate_caches() @contextmanager def temporary_visited_module( code: str, *, module_name: str = "module", extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> Iterator[Module]: """Create and visit a temporary module with the given code. Parameters: code: The code of the module. module_name: The name of the temporary module. extensions: The extensions to use when visiting the AST. parent: The optional parent of this module. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. Yields: The visited module. """ code = dedent(code) with temporary_pyfile(code, module_name=module_name) as (_, path): lines_collection = lines_collection or LinesCollection() lines_collection[path] = code.splitlines() module = visit( module_name, filepath=path, code=code, extensions=extensions, parent=parent, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, ) module.modules_collection[module_name] = module yield module @contextmanager def temporary_inspected_module( code: str, *, module_name: str = "module", import_paths: list[Path] | None = None, extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: DocstringStyle | Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> Iterator[Module]: """Create and inspect a temporary module with the given code. Parameters: code: The code of the module. module_name: The name of the temporary module. import_paths: Paths to import the module from. extensions: The extensions to use when visiting the AST. parent: The optional parent of this module. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. lines_collection: A collection of source code lines. modules_collection: A collection of modules. Yields: The inspected module. """ with temporary_pyfile(code, module_name=module_name) as (_, path): lines_collection = lines_collection or LinesCollection() lines_collection[path] = code.splitlines() try: module = inspect( module_name, filepath=path, import_paths=import_paths, extensions=extensions, parent=parent, docstring_parser=docstring_parser, docstring_options=docstring_options, lines_collection=lines_collection, modules_collection=modules_collection, ) module.modules_collection[module_name] = module yield module finally: if module_name in sys.modules: del sys.modules[module_name] invalidate_caches() def vtree(*objects: Object, return_leaf: bool = False) -> Object: """Link objects together, vertically. Parameters: *objects: A sequence of objects. The first one is at the top of the tree. return_leaf: Whether to return the leaf instead of the root. Raises: ValueError: When no objects are provided. Returns: The top or leaf object. """ if not objects: raise ValueError("At least one object must be provided") top = objects[0] leaf = top for obj in objects[1:]: leaf.set_member(obj.name, obj) leaf = obj return leaf if return_leaf else top def htree(*objects: Object) -> Object: """Link objects together, horizontally. Parameters: *objects: A sequence of objects. All objects starting at the second become members of the first. Raises: ValueError: When no objects are provided. Returns: The first given object, with all the other objects as members of it. """ if not objects: raise ValueError("At least one object must be provided") top = objects[0] for obj in objects[1:]: top.set_member(obj.name, obj) return top def module_vtree(path: str, *, leaf_package: bool = True, return_leaf: bool = False) -> Module: """Link objects together, vertically. Parameters: path: The complete module path, like `"a.b.c.d"`. leaf_package: Whether the deepest module should also be a package. return_leaf: Whether to return the leaf instead of the root. Raises: ValueError: When no objects are provided. Returns: The top or leaf module. """ parts = path.split(".") modules = [Module(name, filepath=Path(*parts[:index], "__init__.py")) for index, name in enumerate(parts)] if not leaf_package: filepath = modules[-1].filepath.with_stem(parts[-1]) # type: ignore[union-attr] modules[-1]._filepath = filepath return vtree(*modules, return_leaf=return_leaf) # type: ignore[return-value] python-griffe-1.6.2/src/_griffe/extensions/0000775000175000017500000000000014767006246020566 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/extensions/__init__.py0000664000175000017500000000012314767006246022673 0ustar carstencarsten# These submodules contain our extension system, # as well as built-in extensions. python-griffe-1.6.2/src/_griffe/extensions/base.py0000664000175000017500000003471014767006246022057 0ustar carstencarsten# This module contains the base class for extensions # and the functions to load them. from __future__ import annotations import os import sys from importlib.util import module_from_spec, spec_from_file_location from inspect import isclass from pathlib import Path from typing import TYPE_CHECKING, Any, Union from _griffe.agents.nodes.ast import ast_children, ast_kind 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.runtime import ObjectNode from _griffe.agents.visitor import Visitor from _griffe.loader import GriffeLoader from _griffe.models import Alias, Attribute, Class, Function, Module, Object class Extension: """Base class for Griffe extensions.""" def visit(self, node: ast.AST) -> None: """Visit a node. Parameters: node: The node to visit. """ getattr(self, f"visit_{ast_kind(node)}", lambda _: None)(node) def generic_visit(self, node: ast.AST) -> None: """Visit children nodes. Parameters: node: The node to visit the children of. """ for child in ast_children(node): self.visit(child) def inspect(self, node: ObjectNode) -> None: """Inspect a node. Parameters: node: The node to inspect. """ getattr(self, f"inspect_{node.kind}", lambda _: None)(node) def generic_inspect(self, node: ObjectNode) -> None: """Extend the base generic inspection with extensions. Parameters: node: The node to inspect. """ for child in node.children: if not child.alias_target_path: self.inspect(child) def on_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when visiting a new node during static/dynamic analysis. Parameters: node: The currently visited node. """ def on_instance( self, *, node: ast.AST | ObjectNode, obj: Object, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when an Object has been created. Parameters: node: The currently visited node. obj: The object instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_members(self, *, node: ast.AST | ObjectNode, obj: Object, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when members of an Object have been loaded. Parameters: node: The currently visited node. obj: The object instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_module_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when visiting a new module node during static/dynamic analysis. Parameters: node: The currently visited node. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_module_instance( self, *, node: ast.AST | ObjectNode, mod: Module, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when a Module has been created. Parameters: node: The currently visited node. mod: The module instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_module_members( self, *, node: ast.AST | ObjectNode, mod: Module, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when members of a Module have been loaded. Parameters: node: The currently visited node. mod: The module instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_class_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when visiting a new class node during static/dynamic analysis. Parameters: node: The currently visited node. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_class_instance( self, *, node: ast.AST | ObjectNode, cls: Class, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when a Class has been created. Parameters: node: The currently visited node. cls: The class instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_class_members( self, *, node: ast.AST | ObjectNode, cls: Class, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when members of a Class have been loaded. Parameters: node: The currently visited node. cls: The class instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_function_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when visiting a new function node during static/dynamic analysis. Parameters: node: The currently visited node. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_function_instance( self, *, node: ast.AST | ObjectNode, func: Function, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when a Function has been created. Parameters: node: The currently visited node. func: The function instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_attribute_node(self, *, node: ast.AST | ObjectNode, agent: Visitor | Inspector, **kwargs: Any) -> None: """Run when visiting a new attribute node during static/dynamic analysis. Parameters: node: The currently visited node. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_attribute_instance( self, *, node: ast.AST | ObjectNode, attr: Attribute, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when an Attribute has been created. Parameters: node: The currently visited node. attr: The attribute instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_alias( self, *, node: ast.AST | ObjectNode, alias: Alias, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Run when an Alias has been created. Parameters: node: The currently visited node. alias: The alias instance. agent: The analysis agent currently running. **kwargs: For forward-compatibility. """ def on_package_loaded(self, *, pkg: Module, loader: GriffeLoader, **kwargs: Any) -> None: """Run when a package has been completely loaded. Parameters: pkg: The package (Module) instance. loader: The loader currently in use. **kwargs: For forward-compatibility. """ def on_wildcard_expansion( self, *, alias: Alias, loader: GriffeLoader, **kwargs: Any, ) -> None: """Run when wildcard imports are expanded into aliases. Parameters: alias: The alias instance. loader: The loader currently in use. **kwargs: For forward-compatibility. """ LoadableExtensionType = Union[str, dict[str, Any], Extension, type[Extension]] """All the types that can be passed to `load_extensions`.""" class Extensions: """This class helps iterating on extensions that should run at different times.""" def __init__(self, *extensions: Extension) -> None: """Initialize the extensions container. Parameters: *extensions: The extensions to add. """ self._extensions: list[Extension] = [] self.add(*extensions) def add(self, *extensions: Extension) -> None: """Add extensions to this container. Parameters: *extensions: The extensions to add. """ for extension in extensions: self._extensions.append(extension) def call(self, event: str, **kwargs: Any) -> None: """Call the extension hook for the given event. Parameters: event: The triggered event. **kwargs: Arguments passed to the hook. """ for extension in self._extensions: getattr(extension, event)(**kwargs) builtin_extensions: set[str] = { "dataclasses", } """The names of built-in Griffe extensions.""" def _load_extension_path(path: str) -> ModuleType: module_name = os.path.basename(path).rsplit(".", 1)[0] # noqa: PTH119 spec = spec_from_file_location(module_name, path) if not spec: raise ExtensionNotLoadedError(f"Could not import module from path '{path}'") module = module_from_spec(spec) sys.modules[module_name] = module spec.loader.exec_module(module) # type: ignore[union-attr] return module def _load_extension( extension: str | dict[str, Any] | Extension | type[Extension], ) -> Extension | list[Extension]: """Load a configured extension. Parameters: extension: An extension, with potential configuration options. Raises: ExtensionNotLoadedError: When the extension cannot be loaded, either because the module is not found, or because it does not expose the Extension attribute. ImportError will bubble up so users can see the traceback. Returns: An extension instance. """ ext_object = None # If it's already an extension instance, return it. if isinstance(extension, Extension): return extension # If it's an extension class, instantiate it (without options) and return it. if isclass(extension) and issubclass(extension, Extension): return extension() # If it's a dictionary, we expect the only key to be an import path # and the value to be a dictionary of options. if isinstance(extension, dict): import_path, options = next(iter(extension.items())) # Force path to be a string, as it could have been passed from `mkdocs.yml`, # using the custom YAML tag `!relative`, which gives an instance of MkDocs # path placeholder classes, which are not iterable. import_path = str(import_path) # Otherwise we consider it's an import path, without options. else: import_path = str(extension) options = {} # If the import path contains a colon, we split into path and class name. colons = import_path.count(":") # Special case for The Annoying Operating System. if colons > 1 or (colons and ":" not in Path(import_path).drive): import_path, extension_name = import_path.rsplit(":", 1) else: extension_name = None # 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}" # If the import path is a path to an existing file, load it. elif os.path.exists(import_path): # noqa: PTH110 try: ext_object = _load_extension_path(import_path) except ImportError as error: raise ExtensionNotLoadedError(f"Extension module '{import_path}' could not be found") from error # If the extension wasn't loaded yet, we consider the import path # to be a Python dotted path like `package.module` or `package.module.Extension`. if not ext_object: try: ext_object = dynamic_import(import_path) except ModuleNotFoundError as error: raise ExtensionNotLoadedError(f"Extension module '{import_path}' could not be found") from error except ImportError as error: 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. if isclass(ext_object) and issubclass(ext_object, Extension): return ext_object(**options) # Otherwise the loaded object is a module, so we get the extension class by name, # instantiate it with options and return it. if extension_name: try: return getattr(ext_object, extension_name)(**options) except AttributeError as error: raise ExtensionNotLoadedError( f"Extension module '{import_path}' has no '{extension_name}' attribute", ) from error # No class name was specified so we search all extension classes in the module, # instantiate each with the same options, and return them. extensions = [ obj for obj in vars(ext_object).values() if isclass(obj) and issubclass(obj, Extension) and obj is not Extension ] return [ext(**options) for ext in extensions] def load_extensions(*exts: LoadableExtensionType) -> Extensions: """Load configured extensions. Parameters: exts: Extensions with potential configuration options. Returns: An extensions container. """ extensions = Extensions() for extension in exts: ext = _load_extension(extension) if isinstance(ext, list): extensions.add(*ext) else: extensions.add(ext) # TODO: Deprecate and remove at some point? # Always add our built-in dataclasses extension. from _griffe.extensions.dataclasses import DataclassesExtension for ext in extensions._extensions: if type(ext) is DataclassesExtension: break else: extensions.add(*_load_extension("dataclasses")) # type: ignore[misc] return extensions python-griffe-1.6.2/src/_griffe/extensions/dataclasses.py0000664000175000017500000002027114767006246023431 0ustar carstencarsten# Built-in extension adding support for dataclasses. # # This extension re-creates `__init__` methods of dataclasses # during static analysis. from __future__ import annotations import ast from contextlib import suppress from functools import cache from typing import Any, cast from _griffe.enumerations import ParameterKind from _griffe.expressions import ( Expr, ExprAttribute, ExprCall, ExprDict, ) from _griffe.extensions.base import Extension from _griffe.logger import logger from _griffe.models import Attribute, Class, Decorator, Function, Module, Parameter, Parameters def _dataclass_decorator(decorators: list[Decorator]) -> Expr | None: for decorator in decorators: if isinstance(decorator.value, Expr) and decorator.value.canonical_path == "dataclasses.dataclass": return decorator.value return None def _expr_args(expr: Expr) -> dict[str, str | Expr]: args = {} if isinstance(expr, ExprCall): for argument in expr.arguments: try: args[argument.name] = argument.value # type: ignore[union-attr] except AttributeError: # Argument is a unpacked variable. with suppress(Exception): collection = expr.function.parent.modules_collection # type: ignore[attr-defined] var = collection[argument.value.canonical_path] # type: ignore[union-attr] args.update(_expr_args(var.value)) elif isinstance(expr, ExprDict): args.update({ast.literal_eval(str(key)): value for key, value in zip(expr.keys, expr.values)}) return args def _dataclass_arguments(decorators: list[Decorator]) -> dict[str, Any]: if (expr := _dataclass_decorator(decorators)) and isinstance(expr, ExprCall): return _expr_args(expr) return {} def _field_arguments(attribute: Attribute) -> dict[str, Any]: if attribute.value: value = attribute.value if isinstance(value, ExprAttribute): value = value.last if isinstance(value, ExprCall) and value.canonical_path == "dataclasses.field": return _expr_args(value) return {} @cache def _dataclass_parameters(class_: Class) -> list[Parameter]: # Fetch `@dataclass` arguments if any. dec_args = _dataclass_arguments(class_.decorators) # Parameters not added to `__init__`, return empty list. if dec_args.get("init") == "False": return [] # All parameters marked as keyword-only. kw_only = dec_args.get("kw_only") == "True" # Iterate on current attributes to find parameters. parameters = [] for member in class_.members.values(): if member.is_attribute: member = cast("Attribute", member) # All dataclass parameters have annotations. if member.annotation is None: continue # Attributes that have labels for these characteristics are # not class parameters: # - @property # - @cached_property # - ClassVar annotation if "property" in member.labels or ( # TODO: It is better to explicitly check for `ClassVar`, but # `Visitor.handle_attribute` unwraps it from the annotation. # Maybe create `internal_labels` and store "classvar" in there. "class-attribute" in member.labels and "instance-attribute" not in member.labels ): continue # Start of keyword-only parameters. if isinstance(member.annotation, Expr) and member.annotation.canonical_path == "dataclasses.KW_ONLY": kw_only = True continue # Fetch `field` arguments if any. field_args = _field_arguments(member) # Parameter not added to `__init__`, skip it. if field_args.get("init") == "False": continue # Determine parameter kind. kind = ( ParameterKind.keyword_only if kw_only or field_args.get("kw_only") == "True" else ParameterKind.positional_or_keyword ) # Determine parameter default. if "default_factory" in field_args: default = ExprCall(function=field_args["default_factory"], arguments=[]) else: default = field_args.get("default", None if field_args else member.value) # Add parameter to the list. parameters.append( Parameter( member.name, annotation=member.annotation, kind=kind, default=default, docstring=member.docstring, ), ) return parameters def _reorder_parameters(parameters: list[Parameter]) -> list[Parameter]: # De-duplicate, overwriting previous parameters. params_dict = {param.name: param for param in parameters} # Re-order, putting positional-only in front and keyword-only at the end. pos_only = [] pos_kw = [] kw_only = [] for param in params_dict.values(): if param.kind is ParameterKind.positional_only: pos_only.append(param) elif param.kind is ParameterKind.keyword_only: kw_only.append(param) else: pos_kw.append(param) return pos_only + pos_kw + kw_only def _set_dataclass_init(class_: Class) -> None: # Retrieve parameters from all parent dataclasses. parameters = [] try: mro = class_.mro() except ValueError: mro = () # type: ignore[assignment] for parent in reversed(mro): if _dataclass_decorator(parent.decorators): parameters.extend(_dataclass_parameters(parent)) # At least one parent dataclass makes the current class a dataclass: # that's how `dataclasses.is_dataclass` works. class_.labels.add("dataclass") # If the class is not decorated with `@dataclass`, skip it. if not _dataclass_decorator(class_.decorators): return logger.debug("Handling dataclass: %s", class_.path) # Add current class parameters. parameters.extend(_dataclass_parameters(class_)) # Create `__init__` method with re-ordered parameters. init = Function( "__init__", lineno=0, endlineno=0, parent=class_, parameters=Parameters( Parameter(name="self", annotation=None, kind=ParameterKind.positional_or_keyword, default=None), *_reorder_parameters(parameters), ), returns="None", ) class_.set_member("__init__", init) def _del_members_annotated_as_initvar(class_: Class) -> None: # Definitions annotated as InitVar are not class members. attributes = [member for member in class_.members.values() if isinstance(member, Attribute)] for attribute in attributes: if isinstance(attribute.annotation, Expr) and attribute.annotation.canonical_path == "dataclasses.InitVar": class_.del_member(attribute.name) def _apply_recursively(mod_cls: Module | Class, processed: set[str]) -> None: if mod_cls.canonical_path in processed: return processed.add(mod_cls.canonical_path) if isinstance(mod_cls, Class): if "__init__" not in mod_cls.members: _set_dataclass_init(mod_cls) _del_members_annotated_as_initvar(mod_cls) for member in mod_cls.members.values(): if not member.is_alias and member.is_class: _apply_recursively(member, processed) # type: ignore[arg-type] elif isinstance(mod_cls, Module): for member in mod_cls.members.values(): if not member.is_alias and (member.is_module or member.is_class): _apply_recursively(member, processed) # type: ignore[arg-type] class DataclassesExtension(Extension): """Built-in extension adding support for dataclasses. This extension creates `__init__` methods of dataclasses if they don't already exist. """ def on_package_loaded(self, *, pkg: Module, **kwargs: Any) -> None: # noqa: ARG002 """Hook for loaded packages. Parameters: pkg: The loaded package. """ _apply_recursively(pkg, set()) python-griffe-1.6.2/src/_griffe/models.py0000664000175000017500000022071114767006246020227 0ustar carstencarsten# This module contains our models definitions, # to represent Python objects (and other aspects of Python APIs)... in Python. from __future__ import annotations import inspect from collections import defaultdict from contextlib import suppress from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any, Callable, Union, cast from _griffe.c3linear import c3linear_merge from _griffe.docstrings.parsers import DocstringStyle, 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 logger from _griffe.mixins import ObjectAliasMixin if TYPE_CHECKING: from collections.abc import Sequence from _griffe.collections import LinesCollection, ModulesCollection from _griffe.docstrings.models import DocstringSection from _griffe.expressions import Expr from functools import cached_property class Decorator: """This class represents decorators.""" def __init__(self, value: str | Expr, *, lineno: int | None, endlineno: int | None) -> None: """Initialize the decorator. Parameters: value: The decorator code. lineno: The starting line number. endlineno: The ending line number. """ self.value: str | Expr = value """The decorator value (as a Griffe expression or string).""" self.lineno: int | None = lineno """The starting line number of the decorator.""" self.endlineno: int | None = endlineno """The ending line number of the decorator.""" @property def callable_path(self) -> str: """The path of the callable used as decorator.""" value = self.value.function if isinstance(self.value, ExprCall) else self.value return value if isinstance(value, str) else value.canonical_path def as_dict(self, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this decorator's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ return { "value": self.value, "lineno": self.lineno, "endlineno": self.endlineno, } class Docstring: """This class represents docstrings.""" def __init__( self, value: str, *, lineno: int | None = None, endlineno: int | None = None, parent: Object | None = None, parser: DocstringStyle | Parser | None = None, parser_options: dict[str, Any] | None = None, ) -> None: """Initialize the docstring. Parameters: value: The docstring value. lineno: The starting line number. endlineno: The ending line number. parent: The parent object on which this docstring is attached. parser: The docstring parser to use. By default, no parsing is done. parser_options: Additional docstring parsing options. """ self.value: str = inspect.cleandoc(value.rstrip()) """The original value of the docstring, cleaned by `inspect.cleandoc`. See also: [`source`][griffe.Docstring.source]. """ self.lineno: int | None = lineno """The starting line number of the docstring. See also: [`endlineno`][griffe.Docstring.endlineno].""" self.endlineno: int | None = endlineno """The ending line number of the docstring. See also: [`lineno`][griffe.Docstring.lineno].""" self.parent: Object | None = parent """The object this docstring is attached to.""" self.parser: DocstringStyle | Parser | None = parser """The selected docstring parser. See also: [`parser_options`][griffe.Docstring.parser_options], [`parse`][griffe.Docstring.parse]. """ self.parser_options: dict[str, Any] = parser_options or {} """The configured parsing options. See also: [`parser`][griffe.Docstring.parser], [`parse`][griffe.Docstring.parse]. """ @property def lines(self) -> list[str]: """The lines of the docstring. See also: [`source`][griffe.Docstring.source]. """ return self.value.split("\n") @property def source(self) -> str: """The original, uncleaned value of the docstring as written in the source. See also: [`value`][griffe.Docstring.value]. """ if self.parent is None: raise ValueError("Cannot get original docstring without parent object") if isinstance(self.parent.filepath, list): raise ValueError("Cannot get original docstring for namespace package") # noqa: TRY004 if self.lineno is None or self.endlineno is None: raise ValueError("Cannot get original docstring without line numbers") return "\n".join(self.parent.lines_collection[self.parent.filepath][self.lineno - 1 : self.endlineno]) @cached_property def parsed(self) -> list[DocstringSection]: """The docstring sections, parsed into structured data.""" return self.parse() def parse( self, parser: DocstringStyle | Parser | None = None, **options: Any, ) -> list[DocstringSection]: """Parse the docstring into structured data. See also: [`parser`][griffe.Docstring.parser], [`parser_options`][griffe.Docstring.parser_options]. Parameters: parser: The docstring parser to use. In order: use the given parser, or the self parser, or no parser (return a single text section). **options: Additional docstring parsing options. Returns: The parsed docstring as a list of sections. """ return parse(self, parser or self.parser, **(options or self.parser_options)) def as_dict( self, *, full: bool = False, **kwargs: Any, # noqa: ARG002 ) -> dict[str, Any]: """Return this docstring's data as a dictionary. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options. Returns: A dictionary. """ base: dict[str, Any] = { "value": self.value, "lineno": self.lineno, "endlineno": self.endlineno, } if full: base["parsed"] = self.parsed return base class Parameter: """This class represent a function parameter. See also: [`Parameters`][griffe.Parameters]. """ def __init__( self, name: str, *, annotation: str | Expr | None = None, kind: ParameterKind | None = None, default: str | Expr | None = None, docstring: Docstring | None = None, ) -> None: """Initialize the parameter. Parameters: name: The parameter name, without leading stars (`*` or `**`). annotation: The parameter annotation, if any. kind: The parameter kind. default: The parameter default, if any. docstring: The parameter docstring. """ self.name: str = name """The parameter name.""" self.annotation: str | Expr | None = annotation """The parameter type annotation.""" self.kind: ParameterKind | None = kind """The parameter kind.""" self.default: str | Expr | None = default """The parameter default value.""" self.docstring: Docstring | None = docstring """The parameter docstring.""" # The parent function is set in `Function.__init__`, # when the parameters are assigned to the function. self.function: Function | None = None """The parent function of the parameter.""" def __str__(self) -> str: param = f"{self.name}: {self.annotation} = {self.default}" if self.kind: return f"[{self.kind.value}] {param}" return param 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 ( self.name == value.name and self.annotation == value.annotation and self.kind == value.kind and self.default == value.default ) @property def required(self) -> bool: """Whether this parameter is required.""" return self.default is None def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this parameter's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base: dict[str, Any] = { "name": self.name, "annotation": self.annotation, "kind": self.kind, "default": self.default, } if self.docstring: base["docstring"] = self.docstring.as_dict(full=full) return base class Parameters: """This class is a container for parameters. It allows to get parameters using their position (index) or their name: ```pycon >>> parameters = Parameters(Parameter("hello")) >>> parameters[0] is parameters["hello"] True ``` See also: [`Parameter`][griffe.Parameter]. """ def __init__(self, *parameters: Parameter) -> None: """Initialize the parameters container. Parameters: *parameters: The initial parameters to add to the container. """ self._params: list[Parameter] = list(parameters) def __repr__(self) -> str: return f"Parameters({', '.join(repr(param) for param in self._params)})" def __getitem__(self, name_or_index: int | str) -> Parameter: """Get a parameter by index or name.""" if isinstance(name_or_index, int): return self._params[name_or_index] name = name_or_index.lstrip("*") try: return next(param for param in self._params if param.name == name) except StopIteration as error: raise KeyError(f"parameter {name_or_index} not found") from error def __setitem__(self, name_or_index: int | str, parameter: Parameter) -> None: """Set a parameter by index or name.""" if isinstance(name_or_index, int): self._params[name_or_index] = parameter else: name = name_or_index.lstrip("*") try: index = next(idx for idx, param in enumerate(self._params) if param.name == name) except StopIteration: self._params.append(parameter) else: self._params[index] = parameter def __delitem__(self, name_or_index: int | str) -> None: """Delete a parameter by index or name.""" if isinstance(name_or_index, int): del self._params[name_or_index] else: name = name_or_index.lstrip("*") try: index = next(idx for idx, param in enumerate(self._params) if param.name == name) except StopIteration as error: raise KeyError(f"parameter {name_or_index} not found") from error del self._params[index] def __len__(self): """The number of parameters.""" return len(self._params) def __iter__(self): """Iterate over the parameters, in order.""" return iter(self._params) def __contains__(self, param_name: str): """Whether a parameter with the given name is present.""" try: next(param for param in self._params if param.name == param_name.lstrip("*")) except StopIteration: return False return True def add(self, parameter: Parameter) -> None: """Add a parameter to the container. Parameters: parameter: The function parameter to add. Raises: ValueError: When a parameter with the same name is already present. """ if parameter.name in self: raise ValueError(f"parameter {parameter.name} already present") self._params.append(parameter) class Object(ObjectAliasMixin): """An abstract class representing a Python object.""" kind: Kind """The object kind.""" is_alias: bool = False """Always false for objects.""" is_collection: bool = False """Always false for objects.""" inherited: bool = False """Always false for objects. Only aliases can be marked as inherited. """ def __init__( self, name: str, *, lineno: int | None = None, endlineno: int | None = None, runtime: bool = True, docstring: Docstring | None = None, parent: Module | Class | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> None: """Initialize the object. Parameters: name: The object name, as declared in the code. lineno: The object starting line, or None for modules. Lines start at 1. endlineno: The object ending line (inclusive), or None for modules. runtime: Whether this object is present at runtime or not. docstring: The object docstring. parent: The object parent. lines_collection: A collection of source code lines. modules_collection: A collection of modules. """ self.name: str = name """The object name.""" self.lineno: int | None = lineno """The starting line number of the object. See also: [`endlineno`][griffe.Object.endlineno]. """ self.endlineno: int | None = endlineno """The ending line number of the object. See also: [`lineno`][griffe.Object.lineno]. """ self.docstring: Docstring | None = docstring """The object docstring. See also: [`has_docstring`][griffe.Object.has_docstring], [`has_docstrings`][griffe.Object.has_docstrings]. """ self.parent: Module | Class | None = parent """The parent of the object (none if top module).""" self.members: dict[str, Object | Alias] = {} """The object members (modules, classes, functions, attributes). See also: [`inherited_members`][griffe.Object.inherited_members], [`get_member`][griffe.Object.get_member], [`set_member`][griffe.Object.set_member], [`filter_members`][griffe.Object.filter_members]. """ self.labels: set[str] = set() """The object labels (`property`, `dataclass`, etc.). See also: [`has_labels`][griffe.Object.has_labels].""" self.imports: dict[str, str] = {} """The other objects imported by this object. Keys are the names within the object (`from ... import ... as AS_NAME`), while the values are the actual names of the objects (`from ... import REAL_NAME as ...`). """ self.exports: list[str | ExprName] | None = None """The names of the objects exported by this (module) object through the `__all__` variable. Exports can contain string (object names) or resolvable names, like other lists of exports coming from submodules: ```python from .submodule import __all__ as submodule_all __all__ = ["hello", *submodule_all] ``` Exports get expanded by the loader before it expands wildcards and resolves aliases. See also: [`GriffeLoader.expand_exports`][griffe.GriffeLoader.expand_exports]. """ self.aliases: dict[str, Alias] = {} """The aliases pointing to this object.""" self.runtime: bool = runtime """Whether this object is available at runtime. Typically, type-guarded objects (under an `if TYPE_CHECKING` condition) are not available at runtime. """ self.extra: dict[str, dict[str, Any]] = defaultdict(dict) """Namespaced dictionaries storing extra metadata for this object, used by extensions.""" self.public: bool | None = None """Whether this object is public.""" self.deprecated: bool | str | None = None """Whether this object is deprecated (boolean or deprecation message).""" self._lines_collection: LinesCollection | None = lines_collection self._modules_collection: ModulesCollection | None = modules_collection # Attach the docstring to this object. if docstring: docstring.parent = self 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: """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 def has_docstring(self) -> bool: """Whether this object has a docstring (empty or not). See also: [`docstring`][griffe.Object.docstring], [`has_docstrings`][griffe.Object.has_docstrings]. """ return bool(self.docstring) # NOTE: (pawamoy) I'm not happy with `has_docstrings`. # It currently recurses into submodules, but that doesn't make sense # if downstream projects use it to know if they should render an init module # while not rendering submodules too: the property could tell them there are # docstrings, but they could be in submodules, not in the init module. # Maybe we should derive it into new properties: `has_local_docstrings`, # `has_docstrings`, `has_public_docstrings`... Maybe we should make it a function?` # For now it's used in mkdocstrings-python so we must be careful with changes. @property def has_docstrings(self) -> bool: """Whether this object or any of its members has a docstring (empty or not). Inherited members are not considered. Imported members are not considered, unless they are also public. See also: [`docstring`][griffe.Object.docstring], [`has_docstring`][griffe.Object.has_docstring]. """ if self.has_docstring: return True for member in self.members.values(): try: if (not member.is_imported or member.is_public) and member.has_docstrings: return True except AliasResolutionError: continue return False def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: """Tell if this object is of the given kind. See also: [`is_module`][griffe.Object.is_module], [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], [`is_alias`][griffe.Object.is_alias]. Parameters: kind: An instance or set of kinds (strings or enumerations). Raises: ValueError: When an empty set is given as argument. Returns: True or False. """ if isinstance(kind, set): if not kind: raise ValueError("kind must not be an empty set") return self.kind in (knd if isinstance(knd, Kind) else Kind(knd) for knd in kind) if isinstance(kind, str): kind = Kind(kind) return self.kind is kind @property def inherited_members(self) -> dict[str, Alias]: """Members that are inherited from base classes. This method is part of the consumer API: do not use when producing Griffe trees! See also: [`members`][griffe.Object.members]. """ if not isinstance(self, Class): return {} try: mro = self.mro() except ValueError as error: logger.debug(error) return {} inherited_members = {} for base in reversed(mro): for name, member in base.members.items(): if name not in self.members: inherited_members[name] = Alias(name, member, parent=self, inherited=True) return inherited_members @property def is_module(self) -> bool: """Whether this object is a module. See also: [`is_init_module`][griffe.Object.is_init_module]. [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], [`is_alias`][griffe.Object.is_alias], [`is_kind`][griffe.Object.is_kind]. """ return self.kind is Kind.MODULE @property def is_class(self) -> bool: """Whether this object is a class. See also: [`is_module`][griffe.Object.is_module]. [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], [`is_alias`][griffe.Object.is_alias], [`is_kind`][griffe.Object.is_kind]. """ return self.kind is Kind.CLASS @property def is_function(self) -> bool: """Whether this object is a function. See also: [`is_module`][griffe.Object.is_module]. [`is_class`][griffe.Object.is_class], [`is_attribute`][griffe.Object.is_attribute], [`is_alias`][griffe.Object.is_alias], [`is_kind`][griffe.Object.is_kind]. """ return self.kind is Kind.FUNCTION @property def is_attribute(self) -> bool: """Whether this object is an attribute. See also: [`is_module`][griffe.Object.is_module]. [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_alias`][griffe.Object.is_alias], [`is_kind`][griffe.Object.is_kind]. """ return self.kind is Kind.ATTRIBUTE @property def is_init_module(self) -> bool: """Whether this object is an `__init__.py` module. See also: [`is_module`][griffe.Object.is_module]. """ return False @property def is_package(self) -> bool: """Whether this object is a package (top module). See also: [`is_subpackage`][griffe.Object.is_subpackage]. """ return False @property def is_subpackage(self) -> bool: """Whether this object is a subpackage. See also: [`is_package`][griffe.Object.is_package]. """ return False @property def is_namespace_package(self) -> bool: """Whether this object is a namespace package (top folder, no `__init__.py`). See also: [`is_namespace_subpackage`][griffe.Object.is_namespace_subpackage]. """ return False @property def is_namespace_subpackage(self) -> bool: """Whether this object is a namespace subpackage. See also: [`is_namespace_package`][griffe.Object.is_namespace_package]. """ return False def has_labels(self, *labels: str) -> bool: """Tell if this object has all the given labels. See also: [`labels`][griffe.Object.labels]. Parameters: *labels: Labels that must be present. Returns: True or False. """ return set(labels).issubset(self.labels) def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]: """Filter and return members based on predicates. See also: [`members`][griffe.Object.members]. Parameters: *predicates: A list of predicates, i.e. callables accepting a member as argument and returning a boolean. Returns: A dictionary of members. """ if not predicates: return self.members members: dict[str, Object | Alias] = { name: member for name, member in self.members.items() if all(predicate(member) for predicate in predicates) } return members @property def module(self) -> Module: """The parent module of this object. See also: [`package`][griffe.Object.package]. 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. """ if isinstance(self, Module): return self if self.parent is not None: return self.parent.module raise ValueError(f"Object {self.name} does not have a parent module") @property def package(self) -> Module: """The absolute top module (the package) of this object. See also: [`module`][griffe.Object.module]. 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] return module @property def filepath(self) -> Path | list[Path]: """The file path (or directory list for namespace packages) where this object was defined. See also: [`relative_filepath`][griffe.Object.relative_filepath], [`relative_package_filepath`][griffe.Object.relative_package_filepath]. 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 def relative_package_filepath(self) -> Path: """The file path where this object was defined, relative to the top module path. See also: [`filepath`][griffe.Object.filepath], [`relative_filepath`][griffe.Object.relative_filepath]. Raises: ValueError: When the relative path could not be computed. """ package_path = self.package.filepath # Current "module" is a namespace package. if isinstance(self.filepath, list): # Current package is a namespace package. if isinstance(package_path, list): for pkg_path in package_path: for self_path in self.filepath: with suppress(ValueError): return self_path.relative_to(pkg_path.parent) # Current package is a regular package. # NOTE: Technically it makes no sense to have a namespace package # under a non-namespace one, so we should never enter this branch. else: for self_path in self.filepath: with suppress(ValueError): return self_path.relative_to(package_path.parent.parent) raise ValueError # Current package is a namespace package, # and current module is a regular module or package. if isinstance(package_path, list): for pkg_path in package_path: with suppress(ValueError): return self.filepath.relative_to(pkg_path.parent) raise ValueError # Current package is a regular package, # and current module is a regular module or package, # try to compute the path relative to the parent folder # of the package (search path). return self.filepath.relative_to(package_path.parent.parent) @property def relative_filepath(self) -> Path: """The file path where this object was defined, relative to the current working directory. If this object's file path is not relative to the current working directory, return its absolute path. See also: [`filepath`][griffe.Object.filepath], [`relative_package_filepath`][griffe.Object.relative_package_filepath]. Raises: ValueError: When the relative path could not be computed. """ cwd = Path.cwd() if isinstance(self.filepath, list): for self_path in self.filepath: with suppress(ValueError): return self_path.relative_to(cwd) raise ValueError(f"No directory in {self.filepath!r} is relative to the current working directory {cwd}") try: return self.filepath.relative_to(cwd) except ValueError: return self.filepath @property def path(self) -> str: """The dotted path of this object. On regular objects (not aliases), the path is the canonical path. See also: [`canonical_path`][griffe.Object.canonical_path]. Examples: >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown["core.Markdown.references"].path 'markdown.core.Markdown.references' """ return self.canonical_path @property def canonical_path(self) -> str: """The full dotted path of this object. The canonical path is the path where the object was defined (not imported). See also: [`path`][griffe.Object.path]. """ if self.parent is None: return self.name return f"{self.parent.path}.{self.name}" @property def modules_collection(self) -> ModulesCollection: """The modules collection attached to this object or its parents. Raises: ValueError: When no modules collection can be found in the object or its parents. """ if self._modules_collection is not None: return self._modules_collection if self.parent is None: raise ValueError("no modules collection in this object or its parents") return self.parent.modules_collection @property def lines_collection(self) -> LinesCollection: """The lines collection attached to this object or its parents. See also: [`lines`][griffe.Object.lines], [`source`][griffe.Object.source]. Raises: ValueError: When no modules collection can be found in the object or its parents. """ if self._lines_collection is not None: return self._lines_collection if self.parent is None: raise ValueError("no lines collection in this object or its parents") return self.parent.lines_collection @property def lines(self) -> list[str]: """The lines containing the source of this object. See also: [`lines_collection`][griffe.Object.lines_collection], [`source`][griffe.Object.source]. """ try: filepath = self.filepath except BuiltinModuleError: return [] if isinstance(filepath, list): return [] try: lines = self.lines_collection[filepath] except KeyError: return [] if self.is_module: return lines if self.lineno is None or self.endlineno is None: return [] return lines[self.lineno - 1 : self.endlineno] @property def source(self) -> str: """The source code of this object. See also: [`lines`][griffe.Object.lines], [`lines_collection`][griffe.Object.lines_collection]. """ return dedent("\n".join(self.lines)) def resolve(self, name: str) -> str: """Resolve a name within this object's and parents' scope. Parameters: name: The name to resolve. Raises: NameResolutionError: When the name could not be resolved. 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: return self.members[name].target_path # type: ignore[union-attr] return self.members[name].path # Name unknown and no more parent scope, could be a built-in. if self.parent is None: raise NameResolutionError(f"{name} could not be resolved in the scope of {self.path}") # Name is parent, non-module object. if name == self.parent.name and not self.parent.is_module: return self.parent.path # Recurse in parent. return self.parent.resolve(name) def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: """Return this object's data as a dictionary. See also: [`as_json`][griffe.Object.as_json]. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options. Returns: A dictionary. """ base: dict[str, Any] = { "kind": self.kind, "name": self.name, } if full: base.update( { "path": self.path, "filepath": self.filepath, "relative_filepath": self.relative_filepath, "relative_package_filepath": self.relative_package_filepath, }, ) if self.lineno is not None: base["lineno"] = self.lineno if self.endlineno is not None: base["endlineno"] = self.endlineno if self.docstring: base["docstring"] = self.docstring base["labels"] = self.labels base["members"] = {name: member.as_dict(full=full, **kwargs) for name, member in self.members.items()} return base class Alias(ObjectAliasMixin): """This class represents an alias, or indirection, to an object declared in another module. Aliases represent objects that are in the scope of a module or class, but were imported from another module. They behave almost exactly like regular objects, to a few exceptions: - line numbers are those of the alias, not the target - 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.Kind] """ is_alias: bool = True """Always true for aliases.""" is_collection: bool = False """Always false for aliases. See also: [`ModulesCollection`][griffe.ModulesCollection]. """ def __init__( self, name: str, target: str | Object | Alias, *, lineno: int | None = None, endlineno: int | None = None, runtime: bool = True, parent: Module | Class | Alias | None = None, inherited: bool = False, ) -> None: """Initialize the alias. Parameters: name: The alias name. target: If it's a string, the target resolution is delayed until accessing the target property. If it's an object, or even another alias, the target is immediately set. lineno: The alias starting line number. endlineno: The alias ending line number. runtime: Whether this alias is present at runtime or not. parent: The alias parent. inherited: Whether this alias wraps an inherited member. """ self.name: str = name """The alias name.""" self.alias_lineno: int | None = lineno """The starting line number of the alias.""" self.alias_endlineno: int | None = endlineno """The ending line number of the alias.""" self.runtime: bool = runtime """Whether this alias is available at runtime.""" self.inherited: bool = inherited """Whether this alias represents an inherited member.""" self.public: bool | None = None """Whether this alias is public.""" self.deprecated: str | bool | None = None """Whether this alias is deprecated (boolean or deprecation message).""" self._parent: Module | Class | Alias | None = parent self._passed_through: bool = False self.target_path: str """The path of this alias' target.""" if isinstance(target, str): self._target: Object | Alias | None = None self.target_path = target else: self._target = target self.target_path = target.path self._update_target_aliases() def __repr__(self) -> str: return f"Alias({self.name!r}, {self.target_path!r})" # Prevent using `__len__`. def __bool__(self) -> bool: """An alias is always true-ish.""" return True def __len__(self) -> int: """The length of an alias is always 1.""" return 1 # SPECIAL PROXIES ------------------------------- # The following methods and properties exist on the target(s), # but we must handle them in a special way. @property def kind(self) -> Kind: """The target's kind, or `Kind.ALIAS` if the target cannot be resolved. See also: [`is_kind`][griffe.Alias.is_kind]. """ # custom behavior to avoid raising exceptions try: return self.final_target.kind except (AliasResolutionError, CyclicAliasError): return Kind.ALIAS @property def has_docstring(self) -> bool: """Whether this alias' target has a non-empty docstring. See also: [`has_docstrings`][griffe.Alias.has_docstrings], [`docstring`][griffe.Alias.docstring]. """ try: return self.final_target.has_docstring except (AliasResolutionError, CyclicAliasError): return False @property def has_docstrings(self) -> bool: """Whether this alias' target or any of its members has a non-empty docstring. See also: [`has_docstring`][griffe.Alias.has_docstring], [`docstring`][griffe.Alias.docstring]. """ try: return self.final_target.has_docstrings except (AliasResolutionError, CyclicAliasError): return False @property def parent(self) -> Module | Class | Alias | None: """The parent of this alias.""" return self._parent @parent.setter def parent(self, value: Module | Class | Alias) -> None: self._parent = value self._update_target_aliases() @property def path(self) -> str: """The dotted path / import path of this object. See also: [`canonical_path`][griffe.Alias.canonical_path]. """ return f"{self.parent.path}.{self.name}" # type: ignore[union-attr] @property def modules_collection(self) -> ModulesCollection: """The modules collection attached to the alias parents.""" # No need to forward to the target. return self.parent.modules_collection # type: ignore[union-attr] @property def members(self) -> dict[str, Object | Alias]: """The target's members (modules, classes, functions, attributes). See also: [`inherited_members`][griffe.Alias.inherited_members], [`get_member`][griffe.Alias.get_member], [`set_member`][griffe.Alias.set_member], [`filter_members`][griffe.Alias.filter_members]. """ final_target = self.final_target # We recreate aliases to maintain a correct hierarchy, # and therefore correct paths. The path of an alias member # should be the path of the alias plus the member's name, # not the original member's path. return { name: Alias(name, target=member, parent=self, inherited=False) for name, member in final_target.members.items() } @property def inherited_members(self) -> dict[str, Alias]: """Members that are inherited from base classes. Each inherited member of the target will be wrapped in an alias, to preserve correct object access paths. This method is part of the consumer API: do not use when producing Griffe trees! See also: [`members`][griffe.Alias.members]. """ final_target = self.final_target # We recreate aliases to maintain a correct hierarchy, # and therefore correct paths. The path of an alias member # should be the path of the alias plus the member's name, # not the original member's path. return { name: Alias(name, target=member, parent=self, inherited=True) for name, member in final_target.inherited_members.items() } def as_json(self, *, full: bool = False, **kwargs: Any) -> str: """Return this target's data as a JSON string. See also: [`as_dict`][griffe.Alias.as_dict]. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options passed to encoder. Returns: A JSON string. """ try: return self.final_target.as_json(full=full, **kwargs) except (AliasResolutionError, CyclicAliasError): return super().as_json(full=full, **kwargs) # GENERIC OBJECT PROXIES -------------------------------- # The following methods and properties exist on the target(s). # We first try to reach the final target, triggering alias resolution errors # and cyclic aliases errors early. We avoid recursing in the alias chain. @property def extra(self) -> dict: """Namespaced dictionaries storing extra metadata for this object, used by extensions.""" return self.final_target.extra @property def lineno(self) -> int | None: """The starting line number of the target object. See also: [`endlineno`][griffe.Alias.endlineno]. """ return self.final_target.lineno @property def endlineno(self) -> int | None: """The ending line number of the target object. See also: [`lineno`][griffe.Alias.lineno]. """ return self.final_target.endlineno @property def docstring(self) -> Docstring | None: """The target docstring. See also: [`has_docstring`][griffe.Alias.has_docstring], [`has_docstrings`][griffe.Alias.has_docstrings]. """ return self.final_target.docstring @docstring.setter def docstring(self, docstring: Docstring | None) -> None: self.final_target.docstring = docstring @property def labels(self) -> set[str]: """The target labels (`property`, `dataclass`, etc.). See also: [`has_labels`][griffe.Alias.has_labels]. """ return self.final_target.labels @property def imports(self) -> dict[str, str]: """The other objects imported by this alias' target. Keys are the names within the object (`from ... import ... as AS_NAME`), while the values are the actual names of the objects (`from ... import REAL_NAME as ...`). See also: [`is_imported`][griffe.Alias.is_imported]. """ return self.final_target.imports @property def exports(self) -> list[str | ExprName] | None: """The names of the objects exported by this (module) object through the `__all__` variable. Exports can contain string (object names) or resolvable names, like other lists of exports coming from submodules: ```python from .submodule import __all__ as submodule_all __all__ = ["hello", *submodule_all] ``` Exports get expanded by the loader before it expands wildcards and resolves aliases. See also: [`GriffeLoader.expand_exports`][griffe.GriffeLoader.expand_exports]. """ return self.final_target.exports @property def aliases(self) -> dict[str, Alias]: """The aliases pointing to this object.""" return self.final_target.aliases def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: """Tell if this object is of the given kind. See also: [`is_module`][griffe.Alias.is_module], [`is_class`][griffe.Alias.is_class], [`is_function`][griffe.Alias.is_function], [`is_attribute`][griffe.Alias.is_attribute], [`is_alias`][griffe.Alias.is_alias]. Parameters: kind: An instance or set of kinds (strings or enumerations). Raises: ValueError: When an empty set is given as argument. Returns: True or False. """ return self.final_target.is_kind(kind) @property def is_module(self) -> bool: """Whether this object is a module. See also: [`is_init_module`][griffe.Alias.is_init_module]. [`is_class`][griffe.Alias.is_class], [`is_function`][griffe.Alias.is_function], [`is_attribute`][griffe.Alias.is_attribute], [`is_alias`][griffe.Alias.is_alias], [`is_kind`][griffe.Alias.is_kind]. """ return self.final_target.is_module @property def is_class(self) -> bool: """Whether this object is a class. See also: [`is_module`][griffe.Alias.is_module], [`is_function`][griffe.Alias.is_function], [`is_attribute`][griffe.Alias.is_attribute], [`is_alias`][griffe.Alias.is_alias], [`is_kind`][griffe.Alias.is_kind]. """ return self.final_target.is_class @property def is_function(self) -> bool: """Whether this object is a function. See also: [`is_module`][griffe.Alias.is_module], [`is_class`][griffe.Alias.is_class], [`is_attribute`][griffe.Alias.is_attribute], [`is_alias`][griffe.Alias.is_alias], [`is_kind`][griffe.Alias.is_kind]. """ return self.final_target.is_function @property def is_attribute(self) -> bool: """Whether this object is an attribute. See also: [`is_module`][griffe.Alias.is_module], [`is_class`][griffe.Alias.is_class], [`is_function`][griffe.Alias.is_function], [`is_alias`][griffe.Alias.is_alias], [`is_kind`][griffe.Alias.is_kind]. """ return self.final_target.is_attribute def has_labels(self, *labels: str) -> bool: """Tell if this object has all the given labels. See also: [`labels`][griffe.Alias.labels]. Parameters: *labels: Labels that must be present. Returns: True or False. """ return self.final_target.has_labels(*labels) def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]: """Filter and return members based on predicates. See also: [`members`][griffe.Alias.members], [`get_member`][griffe.Alias.get_member], [`set_member`][griffe.Alias.set_member]. Parameters: *predicates: A list of predicates, i.e. callables accepting a member as argument and returning a boolean. Returns: A dictionary of members. """ return self.final_target.filter_members(*predicates) @property def module(self) -> Module: """The parent module of this object. See also: [`package`][griffe.Alias.package]. Raises: ValueError: When the object is not a module and does not have a parent. """ return self.final_target.module @property def package(self) -> Module: """The absolute top module (the package) of this object. See also: [`module`][griffe.Alias.module]. """ return self.final_target.package @property def filepath(self) -> Path | list[Path]: """The file path (or directory list for namespace packages) where this object was defined. See also: [`relative_filepath`][griffe.Alias.relative_filepath], [`relative_package_filepath`][griffe.Alias.relative_package_filepath]. """ return self.final_target.filepath @property def relative_filepath(self) -> Path: """The file path where this object was defined, relative to the current working directory. If this object's file path is not relative to the current working directory, return its absolute path. See also: [`filepath`][griffe.Alias.filepath], [`relative_package_filepath`][griffe.Alias.relative_package_filepath]. Raises: ValueError: When the relative path could not be computed. """ return self.final_target.relative_filepath @property def relative_package_filepath(self) -> Path: """The file path where this object was defined, relative to the top module path. See also: [`filepath`][griffe.Alias.filepath], [`relative_filepath`][griffe.Alias.relative_filepath]. Raises: ValueError: When the relative path could not be computed. """ return self.final_target.relative_package_filepath @property def canonical_path(self) -> str: """The full dotted path of this object. The canonical path is the path where the object was defined (not imported). See also: [`path`][griffe.Alias.path]. """ return self.final_target.canonical_path @property def lines_collection(self) -> LinesCollection: """The lines collection attached to this object or its parents. See also: [`lines`][griffe.Alias.lines], [`source`][griffe.Alias.source]. Raises: ValueError: When no modules collection can be found in the object or its parents. """ return self.final_target.lines_collection @property def lines(self) -> list[str]: """The lines containing the source of this object. See also: [`source`][griffe.Alias.source], [`lines_collection`][griffe.Alias.lines_collection]. """ return self.final_target.lines @property def source(self) -> str: """The source code of this object. See also: [`lines`][griffe.Alias.lines], [`lines_collection`][griffe.Alias.lines_collection]. """ return self.final_target.source def resolve(self, name: str) -> str: """Resolve a name within this object's and parents' scope. Parameters: name: The name to resolve. Raises: NameResolutionError: When the name could not be resolved. Returns: The resolved name. """ return self.final_target.resolve(name) # SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE PROXIES --------------- # These methods and properties exist on targets of specific kind. # We first try to reach the final target, triggering alias resolution errors # and cyclic aliases errors early. We avoid recursing in the alias chain. @property def _filepath(self) -> Path | list[Path] | None: return cast("Module", self.final_target)._filepath @property def bases(self) -> list[Expr | str]: """The class bases. See also: [`Class`][griffe.Class], [`resolved_bases`][griffe.Alias.resolved_bases], [`mro`][griffe.Alias.mro]. """ return cast("Class", self.final_target).bases @property def decorators(self) -> list[Decorator]: """The class/function decorators. See also: [`Function`][griffe.Function], [`Class`][griffe.Class]. """ return cast("Union[Class, Function]", self.target).decorators @property def imports_future_annotations(self) -> bool: """Whether this module import future annotations.""" return cast("Module", self.final_target).imports_future_annotations @property def is_init_module(self) -> bool: """Whether this module is an `__init__.py` module. See also: [`is_module`][griffe.Alias.is_module]. """ return cast("Module", self.final_target).is_init_module @property def is_package(self) -> bool: """Whether this module is a package (top module). See also: [`is_subpackage`][griffe.Alias.is_subpackage]. """ return cast("Module", self.final_target).is_package @property def is_subpackage(self) -> bool: """Whether this module is a subpackage. See also: [`is_package`][griffe.Alias.is_package]. """ return cast("Module", self.final_target).is_subpackage @property def is_namespace_package(self) -> bool: """Whether this module is a namespace package (top folder, no `__init__.py`). See also: [`is_namespace_subpackage`][griffe.Alias.is_namespace_subpackage]. """ return cast("Module", self.final_target).is_namespace_package @property def is_namespace_subpackage(self) -> bool: """Whether this module is a namespace subpackage. See also: [`is_namespace_package`][griffe.Alias.is_namespace_package]. """ return cast("Module", self.final_target).is_namespace_subpackage @property def overloads(self) -> dict[str, list[Function]] | list[Function] | None: """The overloaded signatures declared in this class/module or for this function.""" return cast("Union[Module, Class, Function]", self.final_target).overloads @overloads.setter def overloads(self, overloads: list[Function] | None) -> None: cast("Union[Module, Class, Function]", self.final_target).overloads = overloads @property def parameters(self) -> Parameters: """The parameters of the current function or `__init__` method for classes. This property can fetch inherited members, and therefore is part of the consumer API: do not use when producing Griffe trees! """ return cast("Union[Class, Function]", self.final_target).parameters @property def returns(self) -> str | Expr | None: """The function return type annotation.""" return cast("Function", self.final_target).returns @returns.setter def returns(self, returns: str | Expr | None) -> None: cast("Function", self.final_target).returns = returns @property def setter(self) -> Function | None: """The setter linked to this function (property).""" return cast("Attribute", self.final_target).setter @property def deleter(self) -> Function | None: """The deleter linked to this function (property).""" return cast("Attribute", self.final_target).deleter @property def value(self) -> str | Expr | None: """The attribute value.""" return cast("Attribute", self.final_target).value @property def annotation(self) -> str | Expr | None: """The attribute type annotation.""" return cast("Attribute", self.final_target).annotation @annotation.setter def annotation(self, annotation: str | Expr | None) -> None: cast("Attribute", self.final_target).annotation = annotation @property def resolved_bases(self) -> list[Object]: """Resolved class bases. This method is part of the consumer API: do not use when producing Griffe trees! """ return cast("Class", self.final_target).resolved_bases def mro(self) -> list[Class]: """Return a list of classes in order corresponding to Python's MRO.""" return cast("Class", self.final_target).mro() # SPECIFIC ALIAS METHOD AND PROPERTIES ----------------- # These methods and properties do not exist on targets, # they are specific to aliases. @property def target(self) -> Object | Alias: """The resolved target (actual object), if possible. Upon accessing this property, if the target is not already resolved, a lookup is done using the modules collection to find the target. See also: [`final_target`][griffe.Alias.final_target], [`resolve_target`][griffe.Alias.resolve_target], [`resolved`][griffe.Alias.resolved]. """ if not self.resolved: self.resolve_target() return self._target # type: ignore[return-value] @target.setter def target(self, value: Object | Alias) -> None: if value is self or value.path == self.path: raise CyclicAliasError([self.target_path]) self._target = value self.target_path = value.path if self.parent is not None: self._target.aliases[self.path] = self @property def final_target(self) -> Object: """The final, resolved target, if possible. This will iterate through the targets until a non-alias object is found. See also: [`target`][griffe.Alias.target], [`resolve_target`][griffe.Alias.resolve_target], [`resolved`][griffe.Alias.resolved]. """ # Here we quickly iterate on the alias chain, # remembering which path we've seen already to detect cycles. # The cycle detection is needed because alias chains can be created # as already resolved, and can contain cycles. # Using a dict as an ordered set. paths_seen: dict[str, None] = {} target = self while target.is_alias: if target.path in paths_seen: raise CyclicAliasError([*paths_seen, target.path]) paths_seen[target.path] = None target = target.target # type: ignore[assignment] return target # type: ignore[return-value] def resolve_target(self) -> None: """Resolve the target. See also: [`target`][griffe.Alias.target], [`final_target`][griffe.Alias.final_target], [`resolved`][griffe.Alias.resolved]. Raises: AliasResolutionError: When the target cannot be resolved. It happens when the target does not exist, or could not be loaded (unhandled dynamic object?), or when the target is from a module that was not loaded and added to the collection. CyclicAliasError: When the resolved target is the alias itself. """ # Here we try to resolve the whole alias chain recursively. # We detect cycles by setting a "passed through" state variable # on each alias as we pass through it. Passing a second time # through an alias will raise a CyclicAliasError. # If a single link of the chain cannot be resolved, # the whole chain stays unresolved. This prevents # bad surprises later, in code that checks if # an alias is resolved by checking only # the first link of the chain. if self._passed_through: raise CyclicAliasError([self.target_path]) self._passed_through = True try: self._resolve_target() finally: self._passed_through = False def _resolve_target(self) -> None: try: resolved = self.modules_collection.get_member(self.target_path) except KeyError as error: raise AliasResolutionError(self) from error if resolved is self: raise CyclicAliasError([self.target_path]) if resolved.is_alias and not resolved.resolved: try: resolved.resolve_target() except CyclicAliasError as error: raise CyclicAliasError([self.target_path, *error.chain]) from error self._target = resolved if self.parent is not None: self._target.aliases[self.path] = self # type: ignore[union-attr] def _update_target_aliases(self) -> None: with suppress(AttributeError, AliasResolutionError, CyclicAliasError): self._target.aliases[self.path] = self # type: ignore[union-attr] @property def resolved(self) -> bool: """Whether this alias' target is resolved.""" return self._target is not None @property def wildcard(self) -> str | None: """The module on which the wildcard import is performed (if any). See also: [`GriffeLoader.expand_wildcards`][griffe.GriffeLoader.expand_wildcards]. """ if self.name.endswith("/*"): return self.target_path return None def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this alias' data as a dictionary. See also: [`as_json`][griffe.Alias.as_json]. Parameters: full: Whether to return full info, or just base info. **kwargs: Additional serialization options. Returns: A dictionary. """ base: dict[str, Any] = { "kind": Kind.ALIAS, "name": self.name, "target_path": self.target_path, } if full: base["path"] = self.path if self.alias_lineno: base["lineno"] = self.alias_lineno if self.alias_endlineno: base["endlineno"] = self.alias_endlineno return base class Module(Object): """The class representing a Python module.""" kind = Kind.MODULE def __init__(self, *args: Any, filepath: Path | list[Path] | None = None, **kwargs: Any) -> None: """Initialize the module. Parameters: *args: See [`griffe.Object`][]. filepath: The module file path (directory for namespace [sub]packages, none for builtin modules). **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 signatures declared in this module.""" def __repr__(self) -> str: try: return f"Module({self.filepath!r})" except BuiltinModuleError: return f"Module({self.name!r})" @property def filepath(self) -> Path | list[Path]: """The file path of this module. Raises: BuiltinModuleError: When the instance filepath is None. """ if self._filepath is None: raise BuiltinModuleError(self.name) return self._filepath @property def imports_future_annotations(self) -> bool: """Whether this module import future annotations.""" return ( "annotations" in self.members and self.members["annotations"].is_alias and self.members["annotations"].target_path == "__future__.annotations" # type: ignore[union-attr] ) @property def is_init_module(self) -> bool: """Whether this module is an `__init__.py` module. See also: [`is_module`][griffe.Module.is_module]. """ if isinstance(self.filepath, list): return False try: return self.filepath.name.split(".", 1)[0] == "__init__" except BuiltinModuleError: return False @property def is_package(self) -> bool: """Whether this module is a package (top module). See also: [`is_subpackage`][griffe.Module.is_subpackage]. """ return not bool(self.parent) and self.is_init_module @property def is_subpackage(self) -> bool: """Whether this module is a subpackage. See also: [`is_package`][griffe.Module.is_package]. """ return bool(self.parent) and self.is_init_module @property def is_namespace_package(self) -> bool: """Whether this module is a namespace package (top folder, no `__init__.py`). See also: [`is_namespace_subpackage`][griffe.Module.is_namespace_subpackage]. """ try: return self.parent is None and isinstance(self.filepath, list) except BuiltinModuleError: return False @property def is_namespace_subpackage(self) -> bool: """Whether this module is a namespace subpackage. See also: [`is_namespace_package`][griffe.Module.is_namespace_package]. """ try: return ( self.parent is not None and isinstance(self.filepath, list) and ( cast("Module", self.parent).is_namespace_package or cast("Module", self.parent).is_namespace_subpackage ) ) except BuiltinModuleError: return False def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this module's data as a dictionary. See also: [`as_json`][griffe.Module.as_json]. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base = super().as_dict(**kwargs) if isinstance(self._filepath, list): base["filepath"] = [str(path) for path in self._filepath] elif self._filepath: base["filepath"] = str(self._filepath) else: base["filepath"] = None return base class Class(Object): """The class representing a Python class.""" kind = Kind.CLASS def __init__( self, *args: Any, bases: Sequence[Expr | str] | None = None, decorators: list[Decorator] | None = None, **kwargs: Any, ) -> None: """Initialize the class. Parameters: *args: See [`griffe.Object`][]. bases: The list of base classes, if any. decorators: The class decorators, if any. **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.bases: list[Expr | str] = list(bases) if bases else [] """The class bases. See also: [`resolved_bases`][griffe.Class.resolved_bases], [`mro`][griffe.Class.mro]. """ self.decorators: list[Decorator] = decorators or [] """The class decorators.""" self.overloads: dict[str, list[Function]] = defaultdict(list) """The overloaded signatures declared in this class.""" @property def parameters(self) -> Parameters: """The parameters of this class' `__init__` method, if any. This property fetches inherited members, and therefore is part of the consumer API: do not use when producing Griffe trees! """ try: return self.all_members["__init__"].parameters # type: ignore[union-attr] except KeyError: return Parameters() @property def resolved_bases(self) -> list[Object]: """Resolved class bases. This method is part of the consumer API: do not use when producing Griffe trees! See also: [`bases`][griffe.Class.bases], [`mro`][griffe.Class.mro]. """ resolved_bases = [] for base in self.bases: base_path = base if isinstance(base, str) else base.canonical_path try: resolved_base = self.modules_collection.get_member(base_path) if resolved_base.is_alias: resolved_base = resolved_base.final_target except (AliasResolutionError, CyclicAliasError, KeyError): logger.debug("Base class %s is not loaded, or not static, it cannot be resolved", base_path) else: resolved_bases.append(resolved_base) return resolved_bases def _mro(self, seen: tuple[str, ...] = ()) -> list[Class]: seen = (*seen, self.path) bases: list[Class] = [base for base in self.resolved_bases if base.is_class] # type: ignore[misc] if not bases: return [self] for base in bases: if base.path in seen: cycle = " -> ".join(seen) + f" -> {base.path}" raise ValueError(f"Cannot compute C3 linearization, inheritance cycle detected: {cycle}") return [self, *c3linear_merge(*[base._mro(seen) for base in bases], bases)] def mro(self) -> list[Class]: """Return a list of classes in order corresponding to Python's MRO. See also: [`bases`][griffe.Class.bases], [`resolved_bases`][griffe.Class.resolved_bases]. """ return self._mro()[1:] # Remove self. def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this class' data as a dictionary. See also: [`as_json`][griffe.Class.as_json]. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base = super().as_dict(**kwargs) base["bases"] = self.bases base["decorators"] = [dec.as_dict(**kwargs) for dec in self.decorators] return base class Function(Object): """The class representing a Python function.""" kind = Kind.FUNCTION def __init__( self, *args: Any, parameters: Parameters | None = None, returns: str | Expr | None = None, decorators: list[Decorator] | None = None, **kwargs: Any, ) -> None: """Initialize the function. Parameters: *args: See [`griffe.Object`][]. parameters: The function parameters. returns: The function return annotation. decorators: The function decorators, if any. **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.parameters: Parameters = parameters or Parameters() """The function parameters.""" self.returns: str | Expr | None = returns """The function return type annotation.""" self.decorators: list[Decorator] = decorators or [] """The function decorators.""" self.overloads: list[Function] | None = None """The overloaded signatures of this function.""" for parameter in self.parameters: parameter.function = self @property def annotation(self) -> str | Expr | None: """The type annotation of the returned value.""" return self.returns def resolve(self, name: str) -> str: """Resolve a name within this object's and parents' scope. Parameters: name: The name to resolve. Raises: NameResolutionError: When the name could not be resolved. Returns: The resolved name. """ # We're in an `__init__` method and name is a parameter name. if self.parent and self.name == "__init__" and name in self.parameters: return f"{self.parent.path}({name})" return super().resolve(name) def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this function's data as a dictionary. See also: [`as_json`][griffe.Function.as_json]. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base = super().as_dict(**kwargs) base["decorators"] = [dec.as_dict(**kwargs) for dec in self.decorators] base["parameters"] = [param.as_dict(**kwargs) for param in self.parameters] base["returns"] = self.returns return base class Attribute(Object): """The class representing a Python module/class/instance attribute.""" kind = Kind.ATTRIBUTE def __init__( self, *args: Any, value: str | Expr | None = None, annotation: str | Expr | None = None, **kwargs: Any, ) -> None: """Initialize the function. Parameters: *args: See [`griffe.Object`][]. value: The attribute value, if any. annotation: The attribute annotation, if any. **kwargs: See [`griffe.Object`][]. """ super().__init__(*args, **kwargs) self.value: str | Expr | None = value """The attribute value.""" self.annotation: str | Expr | None = annotation """The attribute type annotation.""" self.setter: Function | None = None """The setter linked to this property.""" self.deleter: Function | None = None """The deleter linked to this property.""" def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this function's data as a dictionary. See also: [`as_json`][griffe.Attribute.as_json]. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base = super().as_dict(**kwargs) if self.value is not None: base["value"] = self.value if self.annotation is not None: base["annotation"] = self.annotation return base python-griffe-1.6.2/src/_griffe/finder.py0000664000175000017500000005231714767006246020220 0ustar carstencarsten# This module contains the code allowing to find modules. # # Note: It might be possible to replace a good part of this module's logic # with utilities from `importlib` (however the util in question is private): # # ```pycon # >>> from importlib.util import _find_spec # >>> _find_spec("griffe.agents", _find_spec("griffe", None).submodule_search_locations) # ModuleSpec( # name='griffe.agents', # loader=<_frozen_importlib_external.SourceFileLoader object at 0x7fa5f34e8110>, # origin='/media/data/dev/griffe/src/griffe/agents/__init__.py', # submodule_search_locations=['/media/data/dev/griffe/src/griffe/agents'], # ) # ``` from __future__ import annotations import ast import os import re import sys from collections import defaultdict from contextlib import suppress from dataclasses import dataclass from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, ClassVar from _griffe.exceptions import UnhandledEditableModuleError from _griffe.logger import logger if TYPE_CHECKING: from collections.abc import Iterator, Sequence from re import Pattern from _griffe.models import Module _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) @dataclass class Package: """This class is a simple placeholder used during the process of finding packages. Parameters: name: The package name. path: The package path(s). stubs: An optional path to the related stubs file (.pyi). """ name: str """Package name.""" path: Path """Package folder path.""" stubs: Path | None = None """Package stubs file.""" @dataclass class NamespacePackage: """This class is a simple placeholder used during the process of finding packages. Parameters: name: The package name. path: The package paths. """ name: str """Namespace package name.""" path: list[Path] """Namespace package folder paths.""" class ModuleFinder: """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][griffe.GriffeLoader]'s `search_paths` parameter. """ accepted_py_module_extensions: ClassVar[list[str]] = [".py", ".pyc", ".pyo", ".pyd", ".pyi", ".so"] """List of extensions supported by the finder.""" extensions_set: ClassVar[set[str]] = set(accepted_py_module_extensions) """Set of extensions supported by the finder.""" def __init__(self, search_paths: Sequence[str | Path] | None = None) -> None: """Initialize the finder. Parameters: search_paths: Optional paths to search into. """ self._paths_contents: dict[Path, list[Path]] = {} self.search_paths: list[Path] = [] """The finder search paths.""" # Optimization: pre-compute Paths to relieve CPU when joining paths. for path in search_paths or sys.path: self.append_search_path(Path(path)) self._always_scan_for: dict[str, list[Path]] = defaultdict(list) self._extend_from_pth_files() def append_search_path(self, path: Path) -> None: """Append a search path. The path will be resolved (absolute, normalized). The path won't be appended if it is already in the search paths list. Parameters: path: The path to append. """ path = path.resolve() if path not in self.search_paths: self.search_paths.append(path) def insert_search_path(self, position: int, path: Path) -> None: """Insert a search path at the given position. The path will be resolved (absolute, normalized). The path won't be inserted if it is already in the search paths list. Parameters: position: The insert position in the list. path: The path to insert. """ path = path.resolve() if path not in self.search_paths: self.search_paths.insert(position, path) def find_spec( self, module: str | Path, *, try_relative_path: bool = True, find_stubs_package: bool = False, ) -> tuple[str, Package | NamespacePackage]: """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, then look into the search paths. Parameters: module: The module name or path. try_relative_path: Whether to try finding the module as a relative path, when the given module is not already a path. find_stubs_package: Whether to search for stubs-only package. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. Raises: FileNotFoundError: When a Path was passed and the module could not be found: - the directory has no `__init__.py` file in it - the path does not exist ModuleNotFoundError: When a string was passed and the module could not be found: - no `module/__init__.py` - no `module.py` - no `module.pth` - no `module` directory (namespace packages) - or unsupported .pth file Returns: The name of the module, and an instance representing its (namespace) package. """ module_path: Path | list[Path] if isinstance(module, Path): module_name, module_path = self._module_name_path(module) top_module_name = self._top_module_name(module_path) elif try_relative_path: try: module_name, module_path = self._module_name_path(Path(module)) except FileNotFoundError: module_name = module top_module_name = module.split(".", 1)[0] else: top_module_name = self._top_module_name(module_path) else: module_name = module top_module_name = module.split(".", 1)[0] # Only search for actual package, let exceptions bubble up. if not find_stubs_package: return module_name, self.find_package(top_module_name) # Search for both package and stubs-only package. try: package = self.find_package(top_module_name) except ModuleNotFoundError: package = None try: stubs = self.find_package(top_module_name + "-stubs") except ModuleNotFoundError: stubs = None # None found, raise error. if package is None and stubs is None: raise ModuleNotFoundError(top_module_name) # Both found, assemble them to be merged later. if package and stubs: if isinstance(package, Package) and isinstance(stubs, Package): package.stubs = stubs.path elif isinstance(package, NamespacePackage) and isinstance(stubs, NamespacePackage): package.path += stubs.path return module_name, package # Return either one. return module_name, package or stubs # type: ignore[return-value] def find_package(self, module_name: str) -> Package | NamespacePackage: """Find a package or namespace package. Parameters: module_name: The module name. Raises: ModuleNotFoundError: When the module cannot be found. Returns: A package or namespace package wrapper. """ filepaths = [ Path(module_name), # TODO: Handle .py[cod] and .so files? # This would be needed for package that are composed # solely of a file with such an extension. Path(f"{module_name}.py"), ] real_module_name = module_name real_module_name = real_module_name.removesuffix("-stubs") namespace_dirs = [] for path in self.search_paths: path_contents = self._contents(path) if path_contents: for choice in filepaths: abs_path = path / choice if abs_path in path_contents: if abs_path.suffix: stubs = abs_path.with_suffix(".pyi") return Package(real_module_name, abs_path, stubs if stubs.exists() else None) init_module = abs_path / "__init__.py" if init_module.exists() and not _is_pkg_style_namespace(init_module): stubs = init_module.with_suffix(".pyi") return Package(real_module_name, init_module, stubs if stubs.exists() else None) init_module = abs_path / "__init__.pyi" if init_module.exists(): # Stubs package. return Package(real_module_name, init_module, None) namespace_dirs.append(abs_path) if namespace_dirs: return NamespacePackage(module_name, namespace_dirs) raise ModuleNotFoundError(module_name) def iter_submodules( self, path: Path | list[Path], seen: set | None = None, ) -> Iterator[NamePartsAndPathType]: """Iterate on a module's submodules, if any. Parameters: path: The module path. seen: If not none, this set is used to skip some files. The goal is to replicate the behavior of Python by only using the first packages (with `__init__` modules) of the same name found in different namespace packages. As soon as we find an `__init__` module, we add its parent path to the `seen` set, which will be reused when scanning the next namespace packages. Yields: name_parts (tuple[str, ...]): The parts of a submodule name. filepath (Path): A submodule filepath. """ if isinstance(path, list): # We never enter this condition again in recursive calls, # so we just have to set `seen` once regardless of its value. seen = set() for path_elem in path: yield from self.iter_submodules(path_elem, seen) return if path.stem == "__init__": path = path.parent # Optimization: just check if the file name ends with .py[icod]/.so # (to distinguish it from a directory), not if it's an actual file. elif path.suffix in self.extensions_set: return # `seen` is only set when we scan a list of paths (namespace package). # `skip` is used to prevent yielding modules # of a regular subpackage that we already yielded # from another part of the namespace. skip = set(seen or ()) for subpath in self._filter_py_modules(path): rel_subpath = subpath.relative_to(path) if rel_subpath.parent in skip: logger.debug("Skip %s, another module took precedence", subpath) continue py_file = rel_subpath.suffix == ".py" stem = rel_subpath.stem if not py_file: # `.py[cod]` and `.so` files look like `name.cpython-38-x86_64-linux-gnu.ext`. stem = stem.split(".", 1)[0] if stem == "__init__": # Optimization: since it's a relative path, if it has only one part # and is named __init__, it means it's the starting path # (no need to compare it against starting path). if len(rel_subpath.parts) == 1: continue yield rel_subpath.parts[:-1], subpath if seen is not None: seen.add(rel_subpath.parent) elif py_file: yield rel_subpath.with_suffix("").parts, subpath else: yield rel_subpath.with_name(stem).parts, subpath def submodules(self, module: Module) -> list[NamePartsAndPathType]: """Return the list of a module's submodules. Parameters: module: The parent module. Returns: A list of tuples containing the parts of the submodule name and its path. """ return sorted( chain( self.iter_submodules(module.filepath), self.iter_submodules(self._always_scan_for[module.name]), ), key=_module_depth, ) def _module_name_path(self, path: Path) -> tuple[str, Path]: # Always return absolute paths to avoid working-directory-dependent issues. path = path.absolute() if path.is_dir(): for ext in self.accepted_py_module_extensions: module_path = path / f"__init__{ext}" if module_path.exists(): return path.name, module_path return path.name, path if path.exists(): if path.stem == "__init__": return path.parent.name, path return path.stem, path raise FileNotFoundError def _contents(self, path: Path) -> list[Path]: if path not in self._paths_contents: try: self._paths_contents[path] = list(path.iterdir()) except (FileNotFoundError, NotADirectoryError): self._paths_contents[path] = [] return self._paths_contents[path] def _append_search_path(self, path: Path) -> None: if path not in self.search_paths: self.search_paths.append(path) def _extend_from_pth_files(self) -> None: for path in self.search_paths: for item in self._contents(path): if item.suffix == ".pth": for directory in _handle_pth_file(item): if scan := directory.always_scan_for: self._always_scan_for[scan].append(directory.path.joinpath(scan)) self.append_search_path(directory.path) def _filter_py_modules(self, path: Path) -> Iterator[Path]: for root, dirs, files in os.walk(path, topdown=True, followlinks=True): # Optimization: modify dirs in-place to exclude `__pycache__` directories. dirs[:] = [dir for dir in dirs if dir != "__pycache__"] for relfile in files: if os.path.splitext(relfile)[1] in self.extensions_set: # noqa: PTH122 yield Path(root, relfile) def _top_module_name(self, path: Path) -> str: # First find if a parent is in search paths. parent_path = path if path.is_dir() else path.parent # Always resolve parent path to compare for relativeness against resolved search paths. parent_path = parent_path.resolve() for search_path in self.search_paths: with suppress(ValueError, IndexError): rel_path = parent_path.relative_to(search_path.resolve()) return rel_path.parts[0] # If not, get the highest directory with an `__init__` module, # add its parent to search paths and return it. while parent_path.parent != parent_path and (parent_path.parent / "__init__.py").exists(): parent_path = parent_path.parent self.insert_search_path(0, parent_path.parent) return parent_path.name _re_pkgresources = re.compile(r"(?:__import__\([\"']pkg_resources[\"']\).declare_namespace\(__name__\))") _re_pkgutil = re.compile(r"(?:__path__ = __import__\([\"']pkgutil[\"']\).extend_path\(__path__, __name__\))") _re_import_line = re.compile(r"^import[ \t]+\w+$") # TODO: For more robustness, we should load and minify the AST # to search for particular call statements. def _is_pkg_style_namespace(init_module: Path) -> bool: code = init_module.read_text(encoding="utf8") return bool(_re_pkgresources.search(code) or _re_pkgutil.search(code)) def _module_depth(name_parts_and_path: NamePartsAndPathType) -> int: return len(name_parts_and_path[0]) @dataclass class _SP: path: Path always_scan_for: str = "" def _handle_pth_file(path: Path) -> list[_SP]: # Support for .pth files pointing to directories. # From https://docs.python.org/3/library/site.html: # A path configuration file is a file whose name has the form name.pth # and exists in one of the four directories mentioned above; # its contents are additional items (one per line) to be added to sys.path. # Non-existing items are never added to sys.path, # and no check is made that the item refers to a directory rather than a file. # No item is added to sys.path more than once. # Blank lines and lines beginning with # are skipped. # Lines starting with import (followed by space or tab) are executed. directories: list[_SP] = [] try: # It turns out PyTorch recommends its users to use `.pth` as the extension # when saving models on the disk. These model files are not encoded in UTF8. # If UTF8 decoding fails, we skip the .pth file. text = path.read_text(encoding="utf8") except UnicodeDecodeError: return directories for line in text.strip().replace(";", "\n").splitlines(keepends=False): line = line.strip() # noqa: PLW2901 if _re_import_line.match(line): editable_module = path.parent / f"{line[len('import') :].lstrip()}.py" with suppress(UnhandledEditableModuleError): return _handle_editable_module(editable_module) if line and not line.startswith("#") and os.path.exists(line): # noqa: PTH110 directories.append(_SP(Path(line))) return directories def _handle_editable_module(path: Path) -> list[_SP]: if _match_pattern(path.name, (*_editable_editables_patterns, *_editable_scikit_build_core_patterns)): # Support for how 'editables' write these files: # example line: `F.map_module('griffe', '/media/data/dev/griffe/src/griffe/__init__.py')`. # And how 'scikit-build-core' writes these files: # example line: `install({'griffe': '/media/data/dev/griffe/src/griffe/__init__.py'}, {'cmake_example': ...}, None, False, True)`. try: editable_lines = path.read_text(encoding="utf8").strip().splitlines(keepends=False) except FileNotFoundError as error: raise UnhandledEditableModuleError(path) from error new_path = Path(editable_lines[-1].split("'")[3]) if new_path.name.startswith("__init__"): return [_SP(new_path.parent.parent)] return [_SP(new_path)] if _match_pattern(path.name, _editable_setuptools_patterns): # Support for how 'setuptools' writes these files: # example line: `MAPPING = {'griffe': '/media/data/dev/griffe/src/griffe', 'briffe': '/media/data/dev/griffe/src/briffe'}`. # with annotation: `MAPPING: dict[str, str] = {...}`. parsed_module = ast.parse(path.read_text()) for node in parsed_module.body: if isinstance(node, ast.Assign): target = node.targets[0] elif isinstance(node, ast.AnnAssign): target = node.target else: continue if isinstance(target, ast.Name) and target.id == "MAPPING" and isinstance(node.value, ast.Dict): # type: ignore[attr-defined] return [_SP(Path(cst.value).parent) for cst in node.value.values if isinstance(cst, ast.Constant)] # type: ignore[attr-defined] if _match_pattern(path.name, _editable_meson_python_patterns): # Support for how 'meson-python' writes these files: # example line: `install({'package', 'module1'}, '/media/data/dev/griffe/build/cp311', ["path"], False)`. # Compiled modules then found in the cp311 folder, under src/package. parsed_module = ast.parse(path.read_text()) for node in parsed_module.body: if ( isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id == "install" 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) python-griffe-1.6.2/src/_griffe/cli.py0000664000175000017500000005015014767006246017511 0ustar carstencarsten# This module contains all CLI-related things. # Why does this file exist, and why not put this in `__main__`? # # We might be tempted to import things from `__main__` later, # but that will cause problems; the code will get executed twice: # # - When we run `python -m griffe`, Python will execute # `__main__.py` as a script. That means there won't be any # `griffe.__main__` in `sys.modules`. # - When you import `__main__` it will get executed again (as a module) because # there's no `griffe.__main__` in `sys.modules`. from __future__ import annotations import argparse import json import logging import os import sys from datetime import datetime, timezone from pathlib import Path from typing import IO, TYPE_CHECKING, Any, Callable 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 logger if TYPE_CHECKING: from collections.abc import Sequence from _griffe.extensions.base import Extension, Extensions DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper() """The default log level for the CLI. This can be overridden by the `GRIFFE_LOG_LEVEL` environment variable. """ class _DebugInfo(argparse.Action): 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() sys.exit(0) def _print_data(data: str, output_file: str | IO | None) -> None: if isinstance(output_file, str): with open(output_file, "w") as fd: # noqa: PTH123 print(data, file=fd) else: if output_file is None: output_file = sys.stdout print(data, file=output_file) def _load_packages( packages: Sequence[str], *, extensions: Extensions | None = None, search_paths: Sequence[str | Path] | None = None, docstring_parser: Parser | None = None, docstring_options: dict[str, Any] | None = None, resolve_aliases: bool = True, resolve_implicit: bool = False, resolve_external: bool | None = None, allow_inspection: bool = True, force_inspection: bool = False, store_source: bool = True, find_stubs_package: bool = False, ) -> GriffeLoader: # Create a single loader. loader = GriffeLoader( extensions=extensions, search_paths=search_paths, docstring_parser=docstring_parser, docstring_options=docstring_options, allow_inspection=allow_inspection, force_inspection=force_inspection, store_source=store_source, ) # Load each package. for package in packages: if not package: logger.debug("Empty package name, continuing") continue logger.info("Loading package %s", package) try: loader.load(package, try_relative_path=True, find_stubs_package=find_stubs_package) except ModuleNotFoundError as error: logger.error("Could not find package %s: %s", package, error) # noqa: TRY400 except ImportError: logger.exception("Tried but could not import package %s", package) logger.info("Finished loading packages") # Resolve aliases. if resolve_aliases: logger.info("Starting alias resolution") unresolved, iterations = loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external) if unresolved: logger.info("%s aliases were still unresolved after %s iterations", len(unresolved), iterations) else: logger.info("All aliases were resolved after %s iterations", iterations) return loader _level_choices = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL") def _extensions_type(value: str) -> Sequence[str | dict[str, Any]]: try: return json.loads(value) except json.JSONDecodeError: return value.split(",") def get_parser() -> argparse.ArgumentParser: """Return the CLI argument parser. Returns: An argparse parser. """ usage = "%(prog)s [GLOBAL_OPTS...] COMMAND [COMMAND_OPTS...]" description = "Signatures for entire Python programs. " "Extract the structure, the frame, the skeleton of your project, " "to generate API documentation or find breaking changes in your API." parser = argparse.ArgumentParser(add_help=False, usage=usage, description=description, prog="griffe") main_help = "Show this help message and exit. Commands also accept the -h/--help option." subcommand_help = "Show this help message and exit." 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("--debug-info", action=_DebugInfo, help="Print debug information.") def add_common_options(subparser: argparse.ArgumentParser) -> None: common_options = subparser.add_argument_group(title="Common options") common_options.add_argument("-h", "--help", action="help", help=subcommand_help) search_options = subparser.add_argument_group(title="Search options") search_options.add_argument( "-s", "--search", dest="search_paths", action="append", type=Path, help="Paths to search packages into.", ) search_options.add_argument( "-y", "--sys-path", dest="append_sys_path", action="store_true", help="Whether to append `sys.path` to search paths specified with `-s`.", ) loading_options = subparser.add_argument_group(title="Loading options") loading_options.add_argument( "-B", "--find-stubs-packages", dest="find_stubs_package", action="store_true", default=False, help="Whether to look for stubs-only packages and merge them with concrete ones.", ) loading_options.add_argument( "-e", "--extensions", default={}, type=_extensions_type, help="A list of extensions to use.", ) loading_options.add_argument( "-X", "--no-inspection", dest="allow_inspection", action="store_false", default=True, help="Disallow inspection of builtin/compiled/not found modules.", ) loading_options.add_argument( "-x", "--force-inspection", dest="force_inspection", action="store_true", default=False, help="Force inspection of everything, even when sources are found.", ) debug_options = subparser.add_argument_group(title="Debugging options") debug_options.add_argument( "-L", "--log-level", metavar="LEVEL", default=DEFAULT_LOG_LEVEL, choices=_level_choices, type=str.upper, help="Set the log level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`.", ) # ========= SUBPARSERS ========= # subparsers = parser.add_subparsers( dest="subcommand", title="Commands", metavar="COMMAND", prog="griffe", required=True, ) def add_subparser(command: str, text: str, **kwargs: Any) -> argparse.ArgumentParser: return subparsers.add_parser(command, add_help=False, help=text, description=text, **kwargs) # ========= DUMP PARSER ========= # dump_parser = add_subparser("dump", "Load package-signatures and dump them as JSON.") dump_options = dump_parser.add_argument_group(title="Dump options") dump_options.add_argument("packages", metavar="PACKAGE", nargs="+", help="Packages to find, load and dump.") dump_options.add_argument( "-f", "--full", action="store_true", default=False, help="Whether to dump full data in JSON.", ) dump_options.add_argument( "-o", "--output", default=sys.stdout, help="Output file. Supports templating to output each package in its own file, with `{package}`.", ) dump_options.add_argument( "-d", "--docstyle", dest="docstring_parser", default=None, type=Parser, help="The docstring style to parse.", ) dump_options.add_argument( "-D", "--docopts", dest="docstring_options", default={}, type=json.loads, help="The options for the docstring parser.", ) dump_options.add_argument( "-r", "--resolve-aliases", action="store_true", help="Whether to resolve aliases.", ) dump_options.add_argument( "-I", "--resolve-implicit", action="store_true", help="Whether to resolve implicitly exported aliases as well. " "Aliases are explicitly exported when defined in `__all__`.", ) dump_options.add_argument( "-U", "--resolve-external", dest="resolve_external", action="store_true", help="Always resolve aliases pointing to external/unknown modules (not loaded directly)." "Default is to resolve only from one module to its private sibling (`ast` -> `_ast`).", ) dump_options.add_argument( "--no-resolve-external", dest="resolve_external", action="store_false", help="Never resolve aliases pointing to external/unknown modules (not loaded directly)." "Default is to resolve only from one module to its private sibling (`ast` -> `_ast`).", ) dump_options.add_argument( "-S", "--stats", action="store_true", help="Show statistics at the end.", ) add_common_options(dump_parser) # ========= CHECK PARSER ========= # check_parser = add_subparser("check", "Check for API breakages or possible improvements.") check_options = check_parser.add_argument_group(title="Check options") check_options.add_argument("package", metavar="PACKAGE", help="Package to find, load and check, as path.") check_options.add_argument( "-a", "--against", metavar="REF", help="Older Git reference (commit, branch, tag) to check against. Default: load latest tag.", ) check_options.add_argument( "-b", "--base-ref", metavar="BASE_REF", help="Git reference (commit, branch, tag) to check. Default: load current code.", ) check_options.add_argument( "--color", dest="color", action="store_true", default=None, help="Force enable colors in the output.", ) check_options.add_argument( "--no-color", dest="color", action="store_false", default=None, help="Force disable colors in the output.", ) check_options.add_argument("-v", "--verbose", action="store_true", help="Verbose output.") formats = [fmt.value for fmt in ExplanationStyle] check_options.add_argument("-f", "--format", dest="style", choices=formats, default=None, help="Output format.") add_common_options(check_parser) return parser def dump( packages: Sequence[str], *, output: str | IO | None = None, full: bool = False, docstring_parser: Parser | None = None, docstring_options: dict[str, Any] | None = None, extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None, resolve_aliases: bool = False, resolve_implicit: bool = False, resolve_external: bool | None = None, search_paths: Sequence[str | Path] | None = None, find_stubs_package: bool = False, append_sys_path: bool = False, allow_inspection: bool = True, force_inspection: bool = False, stats: bool = False, ) -> int: """Load packages data and dump it as JSON. Parameters: packages: The packages to load and dump. output: Where to output the JSON-serialized data. full: Whether to output full or minimal data. docstring_parser: The docstring parser to use. By default, no parsing is done. docstring_options: Additional docstring parsing options. resolve_aliases: Whether to resolve aliases (indirect objects references). resolve_implicit: Whether to resolve every alias or only the explicitly exported ones. resolve_external: Whether to load additional, unspecified modules to resolve aliases. Default is to resolve only from one module to its private sibling (`ast` -> `_ast`). extensions: The extensions to use. search_paths: The paths to search into. find_stubs_package: Whether to search for stubs-only packages. If both the package and its stubs are found, they'll be merged together. If only the stubs are found, they'll be used as the package itself. append_sys_path: Whether to append the contents of `sys.path` to the search paths. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. force_inspection: Whether to force using dynamic analysis when loading data. stats: Whether to compute and log stats about loading. Returns: `0` for success, `1` for failure. """ # Prepare options. per_package_output = False if isinstance(output, str) and output.format(package="package") != output: per_package_output = True search_paths = list(search_paths) if search_paths else [] if append_sys_path: search_paths.extend(sys.path) try: loaded_extensions = load_extensions(*(extensions or ())) except ExtensionError: logger.exception("Could not load extensions") return 1 # Load packages. loader = _load_packages( packages, extensions=loaded_extensions, search_paths=search_paths, docstring_parser=docstring_parser, docstring_options=docstring_options, resolve_aliases=resolve_aliases, resolve_implicit=resolve_implicit, resolve_external=resolve_external, allow_inspection=allow_inspection, force_inspection=force_inspection, store_source=False, find_stubs_package=find_stubs_package, ) data_packages = loader.modules_collection.members # Serialize and dump packages. started = datetime.now(tz=timezone.utc) if per_package_output: for package_name, data in data_packages.items(): serialized = data.as_json(indent=2, full=full, sort_keys=True) _print_data(serialized, output.format(package=package_name)) # type: ignore[union-attr] else: serialized = json.dumps(data_packages, cls=JSONEncoder, indent=2, full=full, sort_keys=True) _print_data(serialized, output) elapsed = datetime.now(tz=timezone.utc) - started if stats: loader_stats = loader.stats() loader_stats.time_spent_serializing = elapsed.microseconds logger.info(loader_stats.as_text()) return 0 if len(data_packages) == len(packages) else 1 def check( package: str | Path, against: str | None = None, against_path: str | Path | None = None, *, base_ref: str | None = None, extensions: Sequence[str | dict[str, Any] | Extension | type[Extension]] | None = None, search_paths: Sequence[str | Path] | None = None, append_sys_path: bool = False, find_stubs_package: bool = False, allow_inspection: bool = True, force_inspection: bool = False, verbose: bool = False, color: bool | None = None, style: str | ExplanationStyle | None = None, ) -> int: """Check for API breaking changes in two versions of the same package. Parameters: package: The package to load and check. against: Older Git reference (commit, branch, tag) to check against. against_path: Path when the "against" reference is checked out. base_ref: Git reference (commit, branch, tag) to check. extensions: The extensions to use. search_paths: The paths to search into. append_sys_path: Whether to append the contents of `sys.path` to the search paths. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. force_inspection: Whether to force using dynamic analysis when loading data. verbose: Use a verbose output. Returns: `0` for success, `1` for failure. """ # Prepare options. search_paths = list(search_paths) if search_paths else [] if append_sys_path: search_paths.extend(sys.path) try: against = against or get_latest_tag(package) except GitError as error: print(f"griffe: error: {error}", file=sys.stderr) return 2 against_path = against_path or package repository = get_repo_root(against_path) try: loaded_extensions = load_extensions(*(extensions or ())) except ExtensionError: logger.exception("Could not load extensions") return 1 # Load old and new version of the package. old_package = load_git( against_path, ref=against, repo=repository, extensions=loaded_extensions, search_paths=search_paths, allow_inspection=allow_inspection, force_inspection=force_inspection, resolve_aliases=True, resolve_external=None, ) if base_ref: new_package = load_git( package, ref=base_ref, repo=repository, extensions=loaded_extensions, search_paths=search_paths, allow_inspection=allow_inspection, force_inspection=force_inspection, find_stubs_package=find_stubs_package, resolve_aliases=True, resolve_external=None, ) else: new_package = load( package, try_relative_path=True, extensions=loaded_extensions, search_paths=search_paths, allow_inspection=allow_inspection, force_inspection=force_inspection, find_stubs_package=find_stubs_package, resolve_aliases=True, resolve_external=None, ) # Find and display API breakages. breakages = list(find_breaking_changes(old_package, new_package)) if color is None and (force_color := os.getenv("FORCE_COLOR", None)) is not None: color = force_color.lower() in {"1", "true", "y", "yes", "on"} colorama.deinit() colorama.init(strip=color if color is None else not color) if style is None: style = ExplanationStyle.VERBOSE if verbose else ExplanationStyle.ONE_LINE else: style = ExplanationStyle(style) for breakage in breakages: print(breakage.explain(style=style), file=sys.stderr) if breakages: return 1 return 0 def main(args: list[str] | None = None) -> int: """Run the main program. This function is executed when you type `griffe` or `python -m griffe`. Parameters: args: Arguments passed from the command line. Returns: An exit code. """ # Parse arguments. parser = get_parser() opts: argparse.Namespace = parser.parse_args(args) opts_dict = opts.__dict__ opts_dict.pop("debug_info") subcommand = opts_dict.pop("subcommand") # Initialize logging. log_level = opts_dict.pop("log_level", DEFAULT_LOG_LEVEL) try: level = getattr(logging, log_level) except AttributeError: choices = "', '".join(_level_choices) print( f"griffe: error: invalid log level '{log_level}' (choose from '{choices}')", file=sys.stderr, ) return 1 else: logging.basicConfig(format="%(levelname)-10s %(message)s", level=level) # Run subcommand. commands: dict[str, Callable[..., int]] = {"check": check, "dump": dump} return commands[subcommand](**opts_dict) python-griffe-1.6.2/src/_griffe/c3linear.py0000664000175000017500000000672714767006246020455 0ustar carstencarsten# This module contains a single function, `c3linear_merge`. # The function is generic enough to be in its own module. # # - Copyright (c) 2019 Vitaly R. Samigullin # - Adapted from https://github.com/pilosus/c3linear # - Adapted from https://github.com/tristanlatr/pydocspec from __future__ import annotations from collections import deque from itertools import islice from typing import TypeVar _T = TypeVar("_T") class _Dependency(deque[_T]): """A class representing a (doubly-ended) queue of items.""" @property def head(self) -> _T | None: """Head of the dependency.""" try: return self[0] except IndexError: return None @property def tail(self) -> islice: """Tail of the dependency. The `islice` object is sufficient for iteration or testing membership (`in`). """ try: return islice(self, 1, self.__len__()) except (ValueError, IndexError): return islice([], 0, 0) class _DependencyList: """A class representing a list of linearizations (dependencies). The last element of DependencyList is a list of parents. It's needed to the merge process preserves the local precedence order of direct parent classes. """ def __init__(self, *lists: list[_T | None]) -> None: """Initialize the list. Parameters: *lists: Lists of items. """ self._lists = [_Dependency(lst) for lst in lists] 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) def __len__(self) -> int: size = len(self._lists) return (size - 1) if size else 0 def __repr__(self) -> str: return self._lists.__repr__() @property def heads(self) -> list[_T | None]: """Return the heads.""" return [lst.head for lst in self._lists] # type: ignore[misc] @property def tails(self) -> _DependencyList: """Return self so that `__contains__` could be called.""" return self @property 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: """Remove an item from the lists. Once an item removed from heads, the leftmost elements of the tails get promoted to become the new heads. """ for i in self._lists: if i and i.head == item: i.popleft() def c3linear_merge(*lists: list[_T]) -> list[_T]: """Merge lists of lists in the order defined by the C3Linear algorithm. Parameters: *lists: Lists of items. Returns: The merged list of items. """ result: list[_T] = [] linearizations = _DependencyList(*lists) # type: ignore[arg-type] while True: if linearizations.exhausted: return result for head in linearizations.heads: if head and (head not in linearizations.tails): result.append(head) # type: ignore[arg-type] linearizations.remove(head) # Once candidate is found, continue iteration # from the first element of the list. break else: # Loop never broke, no linearization could possibly be found. raise ValueError("Cannot compute C3 linearization") python-griffe-1.6.2/src/_griffe/docstrings/0000775000175000017500000000000014767006246020546 5ustar carstencarstenpython-griffe-1.6.2/src/_griffe/docstrings/google.py0000664000175000017500000007766314767006246022417 0ustar carstencarsten# This module defines functions to parse Google-style docstrings into structured data. from __future__ import annotations import re from contextlib import suppress from typing import TYPE_CHECKING from _griffe.docstrings.models import ( DocstringAttribute, DocstringClass, DocstringFunction, DocstringModule, DocstringParameter, DocstringRaise, DocstringReceive, DocstringReturn, DocstringSection, DocstringSectionAdmonition, DocstringSectionAttributes, DocstringSectionClasses, DocstringSectionExamples, DocstringSectionFunctions, DocstringSectionModules, DocstringSectionOtherParameters, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReceives, DocstringSectionReturns, DocstringSectionText, DocstringSectionWarns, DocstringSectionYields, DocstringWarn, DocstringYield, ) from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation from _griffe.enumerations import DocstringSectionKind, LogLevel if TYPE_CHECKING: from re import Pattern from typing import Any, Literal from _griffe.expressions import Expr from _griffe.models import Docstring _section_kind = { "args": DocstringSectionKind.parameters, "arguments": DocstringSectionKind.parameters, "params": DocstringSectionKind.parameters, "parameters": DocstringSectionKind.parameters, "keyword args": DocstringSectionKind.other_parameters, "keyword arguments": DocstringSectionKind.other_parameters, "other args": DocstringSectionKind.other_parameters, "other arguments": DocstringSectionKind.other_parameters, "other params": DocstringSectionKind.other_parameters, "other parameters": DocstringSectionKind.other_parameters, "raises": DocstringSectionKind.raises, "exceptions": DocstringSectionKind.raises, "returns": DocstringSectionKind.returns, "yields": DocstringSectionKind.yields, "receives": DocstringSectionKind.receives, "examples": DocstringSectionKind.examples, "attributes": DocstringSectionKind.attributes, "functions": DocstringSectionKind.functions, "methods": DocstringSectionKind.functions, "classes": DocstringSectionKind.classes, "modules": DocstringSectionKind.modules, "warns": DocstringSectionKind.warns, "warnings": DocstringSectionKind.warns, } _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.IGNORECASE) _RE_NAME_ANNOTATION_DESCRIPTION: Pattern = re.compile(r"^(?:(?P<name>\w+)?\s*(?:\((?P<type>.+)\))?:\s*)?(?P<desc>.*)$") _RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$") _RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") 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 = [] # Skip first empty lines. while _is_empty_line(lines[new_offset]): new_offset += 1 # Get initial indent. indent = len(lines[new_offset]) - len(lines[new_offset].lstrip()) if indent == 0: # First non-empty line was not indented, abort. return [], new_offset - 1 # Start processing first item. current_item = (new_offset, [lines[new_offset][indent:]]) new_offset += 1 # Loop on next lines. while new_offset < len(lines): line = lines[new_offset] if _is_empty_line(line): # Empty line: preserve it in the current item. current_item[1].append("") elif line.startswith(indent * 2 * " "): # Continuation line. current_item[1].append(line[indent * 2 :]) elif line.startswith((indent + 1) * " "): # Indent between initial and continuation: append but warn. cont_indent = len(line) - len(line.lstrip()) current_item[1].append(line[cont_indent:]) docstring_warning( docstring, new_offset, f"Confusing indentation for continuation line {new_offset + 1} in docstring, " f"should be {indent} * 2 = {indent * 2} spaces, not {cont_indent}", ) elif line.startswith(indent * " "): # Indent equal to initial one: new item. items.append(current_item) current_item = (new_offset, [line[indent:]]) else: # Indent lower than initial one: end of section. break new_offset += 1 if current_item: items.append(current_item) return items, new_offset - 1 def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001 lines = docstring.lines if offset >= len(lines): return "", offset - 1 new_offset = offset block: list[str] = [] # skip first empty lines. while _is_empty_line(lines[new_offset]): new_offset += 1 # Get initial indent. indent = len(lines[new_offset]) - len(lines[new_offset].lstrip()) if indent == 0: # First non-empty line was not indented, abort. return "", offset - 1 # Start processing first item. block.append(lines[new_offset].lstrip()) new_offset += 1 # Loop on next lines. while new_offset < len(lines) and (lines[new_offset].startswith(indent * " ") or _is_empty_line(lines[new_offset])): block.append(lines[new_offset][indent:]) new_offset += 1 return "\n".join(block).rstrip("\n"), new_offset - 1 def _read_parameters( docstring: Docstring, *, offset: int, warn_unknown_params: bool = True, **options: Any, ) -> tuple[list[DocstringParameter], int]: parameters = [] annotation: str | Expr | None block, new_offset = _read_block_items(docstring, offset=offset, **options) for line_number, param_lines in block: # Check the presence of a name and description, separated by a colon. try: name_with_type, description = param_lines[0].split(":", 1) except ValueError: docstring_warning(docstring, line_number, f"Failed to get 'name: description' pair from '{param_lines[0]}'") continue description = "\n".join([description.lstrip(), *param_lines[1:]]).rstrip("\n") # Use the type given after the parameter name, if any. if " " in name_with_type: name, annotation = name_with_type.split(" ", 1) annotation = annotation.strip("()") annotation = annotation.removesuffix(", optional") # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) else: name = name_with_type # Try to use the annotation from the signature. try: annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr] except (AttributeError, KeyError): annotation = None try: default = docstring.parent.parameters[name].default # type: ignore[union-attr] except (AttributeError, KeyError): default = None if annotation is None: docstring_warning(docstring, line_number, f"No type or annotation for parameter '{name}'") if warn_unknown_params: with suppress(AttributeError): # For Parameters sections in objects without parameters. params = docstring.parent.parameters # type: ignore[union-attr] if name not in params: message = f"Parameter '{name}' does not appear in the function signature" for starred_name in (f"*{name}", f"**{name}"): if starred_name in params: message += f". Did you mean '{starred_name}'?" break docstring_warning(docstring, line_number, message) parameters.append(DocstringParameter(name=name, value=default, annotation=annotation, description=description)) return parameters, new_offset def _read_parameters_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionParameters | None, int]: parameters, new_offset = _read_parameters(docstring, offset=offset, **options) return DocstringSectionParameters(parameters), new_offset def _read_other_parameters_section( docstring: Docstring, *, offset: int, warn_unknown_params: bool = True, # noqa: ARG001 **options: Any, ) -> tuple[DocstringSectionOtherParameters | None, int]: parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options) return DocstringSectionOtherParameters(parameters), new_offset def _read_attributes_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionAttributes | None, int]: attributes = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) annotation: str | Expr | None = None for line_number, attr_lines in block: try: name_with_type, description = attr_lines[0].split(":", 1) except ValueError: docstring_warning(docstring, line_number, f"Failed to get 'name: description' pair from '{attr_lines[0]}'") continue description = "\n".join([description.lstrip(), *attr_lines[1:]]).rstrip("\n") if " " in name_with_type: name, annotation = name_with_type.split(" ", 1) annotation = annotation.strip("()") annotation = annotation.removesuffix(", optional") # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) else: name = name_with_type with suppress(AttributeError, KeyError, TypeError): # Use subscript syntax to fetch annotation from inherited members too. annotation = docstring.parent[name].annotation # type: ignore[index] attributes.append(DocstringAttribute(name=name, annotation=annotation, description=description)) return DocstringSectionAttributes(attributes), new_offset def _read_functions_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionFunctions | None, int]: functions = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) signature: str | Expr | None = None for line_number, func_lines in block: try: name_with_signature, description = func_lines[0].split(":", 1) except ValueError: docstring_warning( docstring, line_number, f"Failed to get 'signature: description' pair from '{func_lines[0]}'", ) continue description = "\n".join([description.lstrip(), *func_lines[1:]]).rstrip("\n") if "(" in name_with_signature: name = name_with_signature.split("(", 1)[0] signature = name_with_signature else: name = name_with_signature signature = None functions.append(DocstringFunction(name=name, annotation=signature, description=description)) return DocstringSectionFunctions(functions), new_offset def _read_classes_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionClasses | None, int]: classes = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) signature: str | Expr | None = None for line_number, class_lines in block: try: name_with_signature, description = class_lines[0].split(":", 1) except ValueError: docstring_warning( docstring, line_number, f"Failed to get 'signature: description' pair from '{class_lines[0]}'", ) continue description = "\n".join([description.lstrip(), *class_lines[1:]]).rstrip("\n") if "(" in name_with_signature: name = name_with_signature.split("(", 1)[0] signature = name_with_signature else: name = name_with_signature signature = None classes.append(DocstringClass(name=name, annotation=signature, description=description)) return DocstringSectionClasses(classes), new_offset def _read_modules_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionModules | None, int]: modules = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) for line_number, module_lines in block: try: name, description = module_lines[0].split(":", 1) except ValueError: docstring_warning( docstring, line_number, f"Failed to get 'name: description' pair from '{module_lines[0]}'", ) continue description = "\n".join([description.lstrip(), *module_lines[1:]]).rstrip("\n") modules.append(DocstringModule(name=name, description=description)) return DocstringSectionModules(modules), new_offset def _read_raises_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionRaises | None, int]: exceptions = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) annotation: str | Expr for line_number, exception_lines in block: try: annotation, description = exception_lines[0].split(":", 1) except ValueError: docstring_warning( docstring, line_number, f"Failed to get 'exception: description' pair from '{exception_lines[0]}'", ) else: description = "\n".join([description.lstrip(), *exception_lines[1:]]).rstrip("\n") # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) exceptions.append(DocstringRaise(annotation=annotation, description=description)) return DocstringSectionRaises(exceptions), new_offset def _read_warns_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionWarns | None, int]: warns = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) for line_number, warning_lines in block: try: annotation, description = warning_lines[0].split(":", 1) except ValueError: docstring_warning( docstring, line_number, f"Failed to get 'warning: description' pair from '{warning_lines[0]}'", ) else: description = "\n".join([description.lstrip(), *warning_lines[1:]]).rstrip("\n") warns.append(DocstringWarn(annotation=annotation, description=description)) return DocstringSectionWarns(warns), new_offset def _read_block_items_maybe( docstring: Docstring, *, offset: int, multiple: bool = True, **options: Any, ) -> _ItemsBlock: if multiple: return _read_block_items(docstring, offset=offset, **options) one_block, new_offset = _read_block(docstring, offset=offset, **options) return [(new_offset, one_block.splitlines())], new_offset def _get_name_annotation_description( docstring: Docstring, line_number: int, lines: list[str], *, named: bool = True, ) -> tuple[str | None, Any, str]: if named: match = _RE_NAME_ANNOTATION_DESCRIPTION.match(lines[0]) if not match: docstring_warning( docstring, line_number, f"Failed to get name, annotation or description from '{lines[0]}'", ) raise ValueError name, annotation, description = match.groups() else: name = None if ":" in lines[0]: annotation, description = lines[0].split(":", 1) annotation = annotation.lstrip("(").rstrip(")") else: annotation = None description = lines[0] description = "\n".join([description.lstrip(), *lines[1:]]).rstrip("\n") return name, annotation, description def _annotation_from_parent( docstring: Docstring, *, gen_index: Literal[0, 1, 2], multiple: bool = False, index: int = 0, ) -> str | Expr | None: annotation = None with suppress(Exception): annotation = docstring.parent.annotation # type: ignore[union-attr] if annotation.is_generator: annotation = annotation.slice.elements[gen_index] elif annotation.is_iterator and gen_index == 0: annotation = annotation.slice if multiple and annotation.is_tuple: annotation = annotation.slice.elements[index] return annotation def _read_returns_section( docstring: Docstring, *, offset: int, returns_multiple_items: bool = True, returns_named_value: bool = True, **options: Any, ) -> tuple[DocstringSectionReturns | None, int]: returns = [] block, new_offset = _read_block_items_maybe( docstring, offset=offset, multiple=returns_multiple_items, **options, ) for index, (line_number, return_lines) in enumerate(block): try: name, annotation, description = _get_name_annotation_description( docstring, line_number, return_lines, named=returns_named_value, ) except ValueError: continue if annotation: # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) else: # Try to retrieve the annotation from the docstring parent. annotation = _annotation_from_parent(docstring, gen_index=2, multiple=len(block) > 1, index=index) if annotation is None: returned_value = repr(name) if name else index + 1 docstring_warning(docstring, line_number, f"No type or annotation for returned value {returned_value}") returns.append(DocstringReturn(name=name or "", annotation=annotation, description=description)) return DocstringSectionReturns(returns), new_offset def _read_yields_section( docstring: Docstring, *, offset: int, returns_multiple_items: bool = True, returns_named_value: bool = True, **options: Any, ) -> tuple[DocstringSectionYields | None, int]: yields = [] block, new_offset = _read_block_items_maybe( docstring, offset=offset, multiple=returns_multiple_items, **options, ) for index, (line_number, yield_lines) in enumerate(block): try: name, annotation, description = _get_name_annotation_description( docstring, line_number, yield_lines, named=returns_named_value, ) except ValueError: continue if annotation: # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) else: # Try to retrieve the annotation from the docstring parent. annotation = _annotation_from_parent(docstring, gen_index=0, multiple=len(block) > 1, index=index) if annotation is None: yielded_value = repr(name) if name else index + 1 docstring_warning(docstring, line_number, f"No type or annotation for yielded value {yielded_value}") yields.append(DocstringYield(name=name or "", annotation=annotation, description=description)) return DocstringSectionYields(yields), new_offset def _read_receives_section( docstring: Docstring, *, offset: int, receives_multiple_items: bool = True, receives_named_value: bool = True, **options: Any, ) -> tuple[DocstringSectionReceives | None, int]: receives = [] block, new_offset = _read_block_items_maybe( docstring, offset=offset, multiple=receives_multiple_items, **options, ) for index, (line_number, receive_lines) in enumerate(block): try: name, annotation, description = _get_name_annotation_description( docstring, line_number, receive_lines, named=receives_named_value, ) except ValueError: continue if annotation: # Try to compile the annotation to transform it into an expression. annotation = parse_docstring_annotation(annotation, docstring) else: # Try to retrieve the annotation from the docstring parent. annotation = _annotation_from_parent(docstring, gen_index=1, multiple=len(block) > 1, index=index) if annotation is None: received_value = repr(name) if name else index + 1 docstring_warning(docstring, line_number, f"No type or annotation for received value {received_value}") receives.append(DocstringReceive(name=name or "", annotation=annotation, description=description)) return DocstringSectionReceives(receives), new_offset def _read_examples_section( docstring: Docstring, *, offset: int, trim_doctest_flags: bool = True, **options: Any, ) -> tuple[DocstringSectionExamples | None, int]: text, new_offset = _read_block(docstring, offset=offset, **options) sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = [] in_code_example = False in_code_block = False current_text: list[str] = [] current_example: list[str] = [] for line in text.split("\n"): if _is_empty_line(line): if in_code_example: if current_example: sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) current_example = [] in_code_example = False else: current_text.append(line) elif in_code_example: if trim_doctest_flags: line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901 current_example.append(line) elif line.startswith("```"): in_code_block = not in_code_block current_text.append(line) elif in_code_block: current_text.append(line) elif line.startswith(">>>"): if current_text: sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) current_text = [] in_code_example = True if trim_doctest_flags: line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901 current_example.append(line) else: current_text.append(line) if current_text: sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) elif current_example: sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) return DocstringSectionExamples(sub_sections), new_offset def _is_empty_line(line: str) -> bool: return not line.strip() _section_reader = { DocstringSectionKind.parameters: _read_parameters_section, DocstringSectionKind.other_parameters: _read_other_parameters_section, DocstringSectionKind.raises: _read_raises_section, DocstringSectionKind.warns: _read_warns_section, DocstringSectionKind.examples: _read_examples_section, DocstringSectionKind.attributes: _read_attributes_section, DocstringSectionKind.functions: _read_functions_section, DocstringSectionKind.classes: _read_classes_section, DocstringSectionKind.modules: _read_modules_section, DocstringSectionKind.returns: _read_returns_section, DocstringSectionKind.yields: _read_yields_section, DocstringSectionKind.receives: _read_receives_section, } _sentinel = object() def parse_google( docstring: Docstring, *, ignore_init_summary: bool = False, trim_doctest_flags: bool = True, returns_multiple_items: bool = True, returns_named_value: bool = True, returns_type_in_property_summary: bool = False, receives_multiple_items: bool = True, receives_named_value: bool = True, warn_unknown_params: bool = True, **options: Any, ) -> list[DocstringSection]: """Parse a Google-style docstring. This function iterates on lines of a docstring to build sections. It then returns this list of sections. Parameters: docstring: The docstring to parse. ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings. trim_doctest_flags: Whether to remove doctest flags from Python example blocks. returns_multiple_items: Whether to parse multiple items in `Yields` and `Returns` sections. When true, each item's continuation lines must be indented. When false (single item), no further indentation is required. returns_named_value: Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description. When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. When false, parentheses are optional but the items cannot be named: `int: Description`. receives_multiple_items: Whether to parse multiple items in `Receives` sections. When true, each item's continuation lines must be indented. When false (single item), no further indentation is required. receives_named_value: Whether to parse `Receives` section items as name and description, rather than type and description. When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. When false, parentheses are optional but the items cannot be named: `int: Description`. returns_type_in_property_summary: Whether to parse the return type of properties at the beginning of their summary: `str: Summary of the property`. warn_unknown_params: Warn about documented parameters not appearing in the signature. **options: Additional parsing options. Returns: A list of docstring sections. """ sections: list[DocstringSection] = [] current_section = [] in_code_block = False lines = docstring.lines options = { "ignore_init_summary": ignore_init_summary, "trim_doctest_flags": trim_doctest_flags, "returns_multiple_items": returns_multiple_items, "returns_named_value": returns_named_value, "returns_type_in_property_summary": returns_type_in_property_summary, "receives_multiple_items": receives_multiple_items, "receives_named_value": receives_named_value, "warn_unknown_params": warn_unknown_params, **options, } ignore_summary = ( options["ignore_init_summary"] and docstring.parent is not None and docstring.parent.name == "__init__" and docstring.parent.is_function and docstring.parent.parent is not None and docstring.parent.parent.is_class ) offset = 2 if ignore_summary else 0 while offset < len(lines): line_lower = lines[offset].lower() if in_code_block: if line_lower.lstrip(" ").startswith("```"): in_code_block = False current_section.append(lines[offset]) elif line_lower.lstrip(" ").startswith("```"): in_code_block = True current_section.append(lines[offset]) elif match := _RE_ADMONITION.match(lines[offset]): groups = match.groupdict() title = groups["title"] admonition_type = groups["type"] is_section = admonition_type.lower() in _section_kind has_previous_line = offset > 0 blank_line_above = not has_previous_line or _is_empty_line(lines[offset - 1]) has_next_line = offset < len(lines) - 1 has_next_lines = offset < len(lines) - 2 blank_line_below = has_next_line and _is_empty_line(lines[offset + 1]) blank_lines_below = has_next_lines and _is_empty_line(lines[offset + 2]) indented_line_below = has_next_line and not blank_line_below and lines[offset + 1].startswith(" ") indented_lines_below = has_next_lines and not blank_lines_below and lines[offset + 2].startswith(" ") if not (indented_line_below or indented_lines_below): # Do not warn when there are no contents, # this is most probably not a section or admonition. current_section.append(lines[offset]) offset += 1 continue reasons = [] kind = "section" if is_section else "admonition" if (indented_line_below or indented_lines_below) and not blank_line_above: reasons.append(f"Missing blank line above {kind}") if indented_lines_below and blank_line_below: reasons.append(f"Extraneous blank line below {kind} title") if reasons: reasons_string = "; ".join(reasons) docstring_warning( docstring, offset, f"Possible {kind} skipped, reasons: {reasons_string}", LogLevel.debug, ) current_section.append(lines[offset]) offset += 1 continue if is_section: if current_section: if any(current_section): sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n"))) current_section = [] reader = _section_reader[_section_kind[admonition_type.lower()]] section, offset = reader(docstring, offset=offset + 1, **options) # type: ignore[operator] if section: section.title = title sections.append(section) else: contents, offset = _read_block(docstring, offset=offset + 1) if contents: if current_section: if any(current_section): sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n"))) current_section = [] if title is None: title = admonition_type admonition_type = admonition_type.lower().replace(" ", "-") sections.append(DocstringSectionAdmonition(kind=admonition_type, text=contents, title=title)) else: with suppress(IndexError): current_section.append(lines[offset]) else: current_section.append(lines[offset]) offset += 1 if current_section: sections.append(DocstringSectionText("\n".join(current_section).rstrip("\n"))) if ( returns_type_in_property_summary and sections and docstring.parent and docstring.parent.is_attribute and "property" in docstring.parent.labels ): lines = sections[0].value.lstrip().split("\n") if ":" in lines[0]: annotation, line = lines[0].split(":", 1) lines = [line, *lines[1:]] sections[0].value = "\n".join(lines) sections.append( DocstringSectionReturns( [DocstringReturn("", description="", annotation=parse_docstring_annotation(annotation, docstring))], ), ) return sections �����������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/sphinx.py������������������������������������������������0000664�0001750�0001750�00000036373�14767006246�022445� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module defines functions to parse Sphinx docstrings into structured data. # Credits to Patrick Lannigan ([@plannigan](https://github.com/plannigan)) # who originally added the parser in the [pytkdocs project](https://github.com/mkdocstrings/pytkdocs). # See https://github.com/mkdocstrings/pytkdocs/pull/71. from __future__ import annotations from contextlib import suppress from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable from _griffe.docstrings.models import ( DocstringAttribute, DocstringParameter, DocstringRaise, DocstringReturn, DocstringSection, DocstringSectionAttributes, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReturns, DocstringSectionText, ) from _griffe.docstrings.utils import docstring_warning if TYPE_CHECKING: from _griffe.expressions import Expr from _griffe.models import Docstring # 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")) @dataclass(frozen=True) class _FieldType: """Maps directive names to parser functions.""" names: frozenset[str] reader: Callable[[Docstring, int, _ParsedValues], int] def matches(self, line: str) -> bool: """Check if a line matches the field type. Parameters: line: Line to check against Returns: True if the line matches the field type, False otherwise. """ return any(line.startswith(f":{name}") for name in self.names) @dataclass class _ParsedDirective: """Directive information that has been parsed from a docstring.""" line: str next_index: int directive_parts: list[str] value: str invalid: bool = False @dataclass class _ParsedValues: """Values parsed from the docstring to be used to produce sections.""" description: list[str] = field(default_factory=list) parameters: dict[str, DocstringParameter] = field(default_factory=dict) param_types: dict[str, str] = field(default_factory=dict) attributes: dict[str, DocstringAttribute] = field(default_factory=dict) attribute_types: dict[str, str] = field(default_factory=dict) exceptions: list[DocstringRaise] = field(default_factory=list) return_value: DocstringReturn | None = None return_type: str | None = None def parse_sphinx(docstring: Docstring, *, warn_unknown_params: bool = True, **options: Any) -> list[DocstringSection]: """Parse a Sphinx-style docstring. Parameters: docstring: The docstring to parse. warn_unknown_params: Warn about documented parameters not appearing in the signature. **options: Additional parsing options. Returns: A list of docstring sections. """ parsed_values = _ParsedValues() options = { "warn_unknown_params": warn_unknown_params, **options, } lines = docstring.lines curr_line_index = 0 while curr_line_index < len(lines): line = lines[curr_line_index] 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) break else: parsed_values.description.append(line) curr_line_index += 1 return _parsed_values_to_sections(parsed_values) def _read_parameter( docstring: Docstring, offset: int, parsed_values: _ParsedValues, *, warn_unknown_params: bool = True, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index directive_type = None if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004 # no type info name = parsed_directive.directive_parts[1] elif len(parsed_directive.directive_parts) == 3: # noqa: PLR2004 directive_type = parsed_directive.directive_parts[1] name = parsed_directive.directive_parts[2] else: docstring_warning(docstring, 0, f"Failed to parse field directive from '{parsed_directive.line}'") return parsed_directive.next_index if name in parsed_values.parameters: docstring_warning(docstring, 0, f"Duplicate parameter entry for '{name}'") return parsed_directive.next_index if warn_unknown_params: with suppress(AttributeError): # For Parameters sections in objects without parameters. params = docstring.parent.parameters # type: ignore[union-attr] if name not in params: message = f"Parameter '{name}' does not appear in the function signature" for starred_name in (f"*{name}", f"**{name}"): if starred_name in params: message += f". Did you mean '{starred_name}'?" break docstring_warning(docstring, 0, message) annotation = _determine_param_annotation(docstring, name, directive_type, parsed_values) default = _determine_param_default(docstring, name) parsed_values.parameters[name] = DocstringParameter( name=name, annotation=annotation, description=parsed_directive.value, value=default, ) return parsed_directive.next_index def _determine_param_default(docstring: Docstring, name: str) -> str | None: try: return docstring.parent.parameters[name.lstrip()].default # type: ignore[union-attr] except (AttributeError, KeyError): return None def _determine_param_annotation( docstring: Docstring, name: str, directive_type: str | None, parsed_values: _ParsedValues, ) -> Any: # Annotation precedence: # - in-line directive type # - "type" directive type # - signature annotation # - none annotation: str | Expr | None = None parsed_param_type = parsed_values.param_types.get(name) if parsed_param_type is not None: annotation = parsed_param_type if directive_type is not None: annotation = directive_type if directive_type is not None and parsed_param_type is not None: docstring_warning(docstring, 0, f"Duplicate parameter information for '{name}'") if annotation is None: try: annotation = docstring.parent.parameters[name.lstrip()].annotation # type: ignore[union-attr] except (AttributeError, KeyError): docstring_warning(docstring, 0, f"No matching parameter for '{name}'") return annotation def _read_parameter_type( docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index param_type = _consolidate_descriptive_type(parsed_directive.value.strip()) if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004 param_name = parsed_directive.directive_parts[1] else: docstring_warning(docstring, 0, f"Failed to get parameter name from '{parsed_directive.line}'") return parsed_directive.next_index parsed_values.param_types[param_name] = param_type param = parsed_values.parameters.get(param_name) if param is not None: if param.annotation is None: param.annotation = param_type else: docstring_warning(docstring, 0, f"Duplicate parameter information for '{param_name}'") return parsed_directive.next_index def _read_attribute( docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004 name = parsed_directive.directive_parts[1] else: docstring_warning(docstring, 0, f"Failed to parse field directive from '{parsed_directive.line}'") return parsed_directive.next_index annotation: str | Expr | None = None # Annotation precedence: # - "vartype" directive type # - annotation in the parent # - none parsed_attribute_type = parsed_values.attribute_types.get(name) if parsed_attribute_type is not None: annotation = parsed_attribute_type else: # try to use the annotation from the parent with suppress(AttributeError, KeyError, TypeError): # Use subscript syntax to fetch annotation from inherited members too. annotation = docstring.parent[name].annotation # type: ignore[index] if name in parsed_values.attributes: docstring_warning(docstring, 0, f"Duplicate attribute entry for '{name}'") else: parsed_values.attributes[name] = DocstringAttribute( name=name, annotation=annotation, description=parsed_directive.value, ) return parsed_directive.next_index def _read_attribute_type( docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index attribute_type = _consolidate_descriptive_type(parsed_directive.value.strip()) if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004 attribute_name = parsed_directive.directive_parts[1] else: docstring_warning(docstring, 0, f"Failed to get attribute name from '{parsed_directive.line}'") return parsed_directive.next_index parsed_values.attribute_types[attribute_name] = attribute_type attribute = parsed_values.attributes.get(attribute_name) if attribute is not None: if attribute.annotation is None: attribute.annotation = attribute_type else: docstring_warning(docstring, 0, f"Duplicate attribute information for '{attribute_name}'") return parsed_directive.next_index def _read_exception( docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index if len(parsed_directive.directive_parts) == 2: # noqa: PLR2004 ex_type = parsed_directive.directive_parts[1] parsed_values.exceptions.append(DocstringRaise(annotation=ex_type, description=parsed_directive.value)) else: docstring_warning(docstring, 0, f"Failed to parse exception directive from '{parsed_directive.line}'") return parsed_directive.next_index 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 # Annotation precedence: # - "rtype" directive type # - signature annotation # - None annotation: str | Expr | None if parsed_values.return_type is not None: annotation = parsed_values.return_type else: try: annotation = docstring.parent.annotation # type: ignore[union-attr] except AttributeError: docstring_warning(docstring, 0, f"No return type or annotation at '{parsed_directive.line}'") annotation = None # TODO: maybe support names parsed_values.return_value = DocstringReturn(name="", annotation=annotation, description=parsed_directive.value) return parsed_directive.next_index def _read_return_type( docstring: Docstring, offset: int, parsed_values: _ParsedValues, **options: Any, # noqa: ARG001 ) -> int: parsed_directive = _parse_directive(docstring, offset) if parsed_directive.invalid: return parsed_directive.next_index return_type = _consolidate_descriptive_type(parsed_directive.value.strip()) parsed_values.return_type = return_type return_value = parsed_values.return_value if return_value is not None: return_value.annotation = return_type return parsed_directive.next_index 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: param_values = list(parsed_values.parameters.values()) result.append(DocstringSectionParameters(param_values)) if parsed_values.attributes: attribute_values = list(parsed_values.attributes.values()) result.append(DocstringSectionAttributes(attribute_values)) if parsed_values.return_value is not None: result.append(DocstringSectionReturns([parsed_values.return_value])) if parsed_values.exceptions: result.append(DocstringSectionRaises(parsed_values.exceptions)) return result 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: docstring_warning(docstring, 0, f"Failed to get ':directive: value' pair from '{line}'") return _ParsedDirective(line, next_index, [], "", invalid=True) value = value.strip() return _ParsedDirective(line, next_index, directive.split(" "), value) def _consolidate_continuation_lines(lines: list[str], offset: int) -> tuple[str, int]: curr_line_index = offset block = [lines[curr_line_index].lstrip()] # start processing after first item curr_line_index += 1 while curr_line_index < len(lines) and not lines[curr_line_index].startswith(":"): block.append(lines[curr_line_index].lstrip()) curr_line_index += 1 return " ".join(block).rstrip("\n"), curr_line_index - 1 def _consolidate_descriptive_type(descriptive_type: str) -> str: return descriptive_type.replace(" or ", " | ") def _strip_blank_lines(lines: list[str]) -> list[str]: if not lines: return lines # remove blank lines from the start and end content_found = False initial_content = 0 final_content = 0 for index, line in enumerate(lines): if not line or line.isspace(): if not content_found: initial_content += 1 else: content_found = True final_content = index 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), ] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/__init__.py����������������������������������������������0000664�0001750�0001750�00000000075�14767006246�022661� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# These submodules define models and parsers for docstrings. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/parsers.py�����������������������������������������������0000664�0001750�0001750�00000012270�14767006246�022601� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module imports all the defined parsers # and provides a generic function to parse docstrings. from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable, Literal 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 # This is not our preferred order, but the safest order for proper detection # using heuristics. Indeed, Google style sections sometimes appear in otherwise # plain markup docstrings, which could lead to false positives. Same for Numpy # sections, whose syntax is regular rST markup, and which can therefore appear # in plain markup docstrings too, even more often than Google sections. _default_style_order = [Parser.sphinx, Parser.google, Parser.numpy] DocstringStyle = Literal["google", "numpy", "sphinx", "auto"] """The supported docstring styles (literal values of the Parser enumeration).""" DocstringDetectionMethod = Literal["heuristics", "max_sections"] """The supported methods to infer docstring styles.""" def infer_docstring_style( docstring: Docstring, # noqa: ARG001 *, method: DocstringDetectionMethod = "heuristics", # noqa: ARG001 style_order: list[Parser] | list[DocstringStyle] | None = None, default: Parser | DocstringStyle | None = None, **options: Any, # noqa: ARG001 ) -> tuple[Parser | None, list[DocstringSection] | None]: """Infer the parser to use for the docstring. [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.3.0](../../../insiders/changelog.md#1.3.0). The 'heuristics' method uses regular expressions. The 'max_sections' method parses the docstring with all parsers specified in `style_order` and returns the one who parsed the most sections. If heuristics fail, the `default` parser is returned. If multiple parsers parsed the same number of sections, `style_order` is used to decide which one to return. The `default` parser is never used with the 'max_sections' method. For non-Insiders versions, `default` is returned if specified, else the first parser in `style_order` is returned. If `style_order` is not specified, `None` is returned. Additional options are parsed to the detected parser, if any. Parameters: docstring: The docstring to parse. method: The method to use to infer the parser. style_order: The order of the styles to try when inferring the parser. default: The default parser to use if the inference fails. **options: Additional parsing options. Returns: The inferred parser, and optionally parsed sections (when method is 'max_sections'). """ if default: return default if isinstance(default, Parser) else Parser(default), None if style_order: style = style_order[0] return style if isinstance(style, Parser) else Parser(style), None return None, None def parse_auto( docstring: Docstring, *, method: DocstringDetectionMethod = "heuristics", style_order: list[Parser] | list[DocstringStyle] | None = None, default: Parser | DocstringStyle | None = None, **options: Any, ) -> list[DocstringSection]: """Parse a docstring by automatically detecting the style it uses. [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.3.0](../../../insiders/changelog.md#1.3.0). See [`infer_docstring_style`][griffe.infer_docstring_style] for more information on the available parameters. Parameters: docstring: The docstring to parse. method: The method to use to infer the parser. style_order: The order of the styles to try when inferring the parser. default: The default parser to use if the inference fails. **options: Additional parsing options. Returns: A list of docstring sections. """ style, sections = infer_docstring_style( docstring, method=method, style_order=style_order, default=default, **options, ) if sections is None: return parse(docstring, style, **options) return sections parsers: dict[Parser, Callable[[Docstring], list[DocstringSection]]] = { Parser.auto: parse_auto, Parser.google: parse_google, Parser.sphinx: parse_sphinx, Parser.numpy: parse_numpy, } def parse( docstring: Docstring, parser: DocstringStyle | Parser | None, **options: Any, ) -> list[DocstringSection]: """Parse the docstring. Parameters: docstring: The docstring to parse. parser: The docstring parser to use. If None, return a single text section. **options: The options accepted by the parser. Returns: A list of docstring sections. """ if parser: if not isinstance(parser, Parser): parser = Parser(parser) return parsers[parser](docstring, **options) return [DocstringSectionText(docstring.value)] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/numpy.py�������������������������������������������������0000664�0001750�0001750�00000072431�14767006246�022277� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module defines functions to parse Numpy-style docstrings into structured data. # # Based on https://numpydoc.readthedocs.io/en/latest/format.html, # it seems Numpydoc is a superset of RST. # Since fully parsing RST is a non-goal of this project, # some things are stripped from the Numpydoc specification. # # Rejected as non particularly Pythonic or useful as sections: # # - See also: this section feels too subjective (specially crafted as a standard for Numpy itself), # and there are may ways to reference related items in a docstring, depending on the chosen markup. # # Rejected as naturally handled by the user-chosen markup: # # - Warnings: this is just markup. # - Notes: again, just markup. # - References: again, just markup. from __future__ import annotations import re from contextlib import suppress from textwrap import dedent from typing import TYPE_CHECKING from _griffe.docstrings.models import ( DocstringAttribute, DocstringClass, DocstringFunction, DocstringModule, DocstringParameter, DocstringRaise, DocstringReceive, DocstringReturn, DocstringSection, DocstringSectionAdmonition, DocstringSectionAttributes, DocstringSectionClasses, DocstringSectionDeprecated, DocstringSectionExamples, DocstringSectionFunctions, DocstringSectionModules, DocstringSectionOtherParameters, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReceives, DocstringSectionReturns, DocstringSectionText, DocstringSectionWarns, DocstringSectionYields, DocstringWarn, DocstringYield, ) 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 re import Pattern from typing import Any, Literal from _griffe.expressions import Expr from _griffe.models import Docstring _section_kind = { "deprecated": DocstringSectionKind.deprecated, "parameters": DocstringSectionKind.parameters, "other parameters": DocstringSectionKind.other_parameters, "returns": DocstringSectionKind.returns, "yields": DocstringSectionKind.yields, "receives": DocstringSectionKind.receives, "raises": DocstringSectionKind.raises, "warns": DocstringSectionKind.warns, "examples": DocstringSectionKind.examples, "attributes": DocstringSectionKind.attributes, "functions": DocstringSectionKind.functions, "methods": DocstringSectionKind.functions, "classes": DocstringSectionKind.classes, "modules": DocstringSectionKind.modules, } def _is_empty_line(line: str) -> bool: return not line.strip() def _is_dash_line(line: str) -> bool: return not _is_empty_line(line) and _is_empty_line(line.replace("-", "")) def _read_block_items( docstring: Docstring, *, offset: int, **options: Any, # noqa: ARG001 ) -> tuple[list[list[str]], int]: lines = docstring.lines if offset >= len(lines): return [], offset new_offset = offset items: list[list[str]] = [] # skip first empty lines while _is_empty_line(lines[new_offset]): new_offset += 1 # start processing first item current_item = [lines[new_offset]] new_offset += 1 # loop on next lines while new_offset < len(lines): line = lines[new_offset] if _is_empty_line(line): # empty line: preserve it in the current item current_item.append("") elif line.startswith(4 * " "): # continuation line current_item.append(line[4:]) elif line.startswith(" "): # indent between initial and continuation: append but warn cont_indent = len(line) - len(line.lstrip()) current_item.append(line[cont_indent:]) docstring_warning( docstring, new_offset, f"Confusing indentation for continuation line {new_offset + 1} in docstring, " f"should be 4 spaces, not {cont_indent}", ) elif new_offset + 1 < len(lines) and _is_dash_line(lines[new_offset + 1]): # detect the start of a new section break else: items.append(current_item) current_item = [line] new_offset += 1 if current_item: items.append(current_item) return items, new_offset - 1 def _read_block(docstring: Docstring, *, offset: int, **options: Any) -> tuple[str, int]: # noqa: ARG001 lines = docstring.lines if offset >= len(lines): return "", offset new_offset = offset block: list[str] = [] # skip first empty lines while _is_empty_line(lines[new_offset]): new_offset += 1 while new_offset < len(lines): is_empty = _is_empty_line(lines[new_offset]) if is_empty and new_offset < len(lines) - 1 and _is_dash_line(lines[new_offset + 1]): break # Break if a new unnamed section is reached. if is_empty and new_offset < len(lines) - 2 and _is_dash_line(lines[new_offset + 2]): break # Break if a new named section is reached. block.append(lines[new_offset]) new_offset += 1 return "\n".join(block).rstrip("\n"), new_offset - 1 _RE_OB: str = r"\{" # Opening bracket. _RE_CB: str = r"\}" # Closing bracket. _RE_NAME: str = r"\*{0,2}[_a-z][_a-z0-9]*" _RE_TYPE: str = r".+" _RE_RETURNS: Pattern = re.compile( rf""" (?: (?P<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type | # or (?P<name>{_RE_NAME})\s*:\s* # just name | # or \s*:\s*$ # no name, no type | # or (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type ) """, re.IGNORECASE | re.VERBOSE, ) _RE_YIELDS: Pattern = _RE_RETURNS _RE_RECEIVES: Pattern = _RE_RETURNS _RE_PARAMETER: Pattern = re.compile( rf""" (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*) (?: \s:\s (?: (?:{_RE_OB}(?P<choices>.+){_RE_CB})| (?P<type>{_RE_TYPE}) )? )? """, re.IGNORECASE | re.VERBOSE, ) _RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$") _RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") def _read_parameters( docstring: Docstring, *, offset: int, warn_unknown_params: bool = True, **options: Any, ) -> tuple[list[DocstringParameter], int]: parameters: list[DocstringParameter] = [] annotation: str | Expr | None items, new_offset = _read_block_items(docstring, offset=offset, **options) for item in items: match = _RE_PARAMETER.match(item[0]) if not match: docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") continue names = match.group("names").split(", ") annotation = match.group("type") or None choices = match.group("choices") default = None if choices: annotation = choices default = choices.split(", ", 1)[0] elif annotation: match = re.match(r"^(?P<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation) if match: default = match.group("default") annotation = match.group("annotation") if annotation and annotation.endswith(", optional"): annotation = annotation[:-10] description = "\n".join(item[1:]).rstrip() if len(item) > 1 else "" if annotation is None: # try to use the annotation from the signature for name in names: with suppress(AttributeError, KeyError): annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr] break else: docstring_warning(docstring, new_offset, f"No types or annotations for parameters {names}") else: annotation = parse_docstring_annotation(annotation, docstring, log_level=LogLevel.debug) if default is None: for name in names: with suppress(AttributeError, KeyError): default = docstring.parent.parameters[name].default # type: ignore[union-attr] break if warn_unknown_params: with suppress(AttributeError): # For Parameters sections in objects without parameters. params = docstring.parent.parameters # type: ignore[union-attr] for name in names: if name not in params: message = f"Parameter '{name}' does not appear in the function signature" for starred_name in (f"*{name}", f"**{name}"): if starred_name in params: message += f". Did you mean '{starred_name}'?" break docstring_warning(docstring, new_offset, message) parameters.extend( DocstringParameter(name, value=default, annotation=annotation, description=description) for name in names ) return parameters, new_offset def _read_parameters_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionParameters | None, int]: parameters, new_offset = _read_parameters(docstring, offset=offset, **options) if parameters: return DocstringSectionParameters(parameters), new_offset docstring_warning(docstring, new_offset, f"Empty parameters section at line {offset}") return None, new_offset def _read_other_parameters_section( docstring: Docstring, *, offset: int, warn_unknown_params: bool = True, # noqa: ARG001 **options: Any, ) -> tuple[DocstringSectionOtherParameters | None, int]: parameters, new_offset = _read_parameters(docstring, offset=offset, warn_unknown_params=False, **options) if parameters: return DocstringSectionOtherParameters(parameters), new_offset docstring_warning(docstring, new_offset, f"Empty other parameters section at line {offset}") return None, new_offset def _read_deprecated_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionDeprecated | None, int]: # deprecated # SINCE_VERSION # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty deprecated section at line {offset}") return None, new_offset if len(items) > 1: docstring_warning(docstring, new_offset, f"Too many deprecated items at {offset}") item = items[0] version = item[0] text = dedent("\n".join(item[1:])) return DocstringSectionDeprecated(version=version, text=text), new_offset def _read_returns_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionReturns | None, int]: # (NAME : )?TYPE # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty returns section at line {offset}") return None, new_offset returns = [] for index, item in enumerate(items): match = _RE_RETURNS.match(item[0]) if not match: docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") continue groups = match.groupdict() name = groups["nt_name"] or groups["name"] annotation = groups["nt_type"] or groups["type"] text = dedent("\n".join(item[1:])) if annotation is None: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, KeyError, ValueError): if docstring.parent.is_function: # type: ignore[union-attr] annotation = docstring.parent.returns # type: ignore[union-attr] elif docstring.parent.is_attribute: # type: ignore[union-attr] annotation = docstring.parent.annotation # type: ignore[union-attr] else: raise ValueError if len(items) > 1: if annotation.is_tuple: annotation = annotation.slice.elements[index] else: if annotation.is_iterator: return_item = annotation.slice elif annotation.is_generator: return_item = annotation.slice.elements[2] else: raise ValueError if isinstance(return_item, ExprName): annotation = return_item elif return_item.is_tuple: annotation = return_item.slice.elements[index] else: annotation = return_item else: 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 def _read_yields_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionYields | None, int]: # yields # (NAME : )?TYPE # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty yields section at line {offset}") return None, new_offset yields = [] for index, item in enumerate(items): match = _RE_YIELDS.match(item[0]) if not match: docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") continue groups = match.groupdict() name = groups["nt_name"] or groups["name"] annotation = groups["nt_type"] or groups["type"] text = dedent("\n".join(item[1:])) if annotation is None: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, IndexError, KeyError, ValueError): annotation = docstring.parent.annotation # type: ignore[union-attr] if annotation.is_iterator: yield_item = annotation.slice elif annotation.is_generator: yield_item = annotation.slice.elements[0] else: raise ValueError if isinstance(yield_item, ExprName): annotation = yield_item elif yield_item.is_tuple: annotation = yield_item.slice.elements[index] else: annotation = yield_item else: 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 def _read_receives_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionReceives | None, int]: # receives # (NAME : )?TYPE # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty receives section at line {offset}") return None, new_offset receives = [] for index, item in enumerate(items): match = _RE_RECEIVES.match(item[0]) if not match: docstring_warning(docstring, new_offset, f"Could not parse line '{item[0]}'") continue groups = match.groupdict() name = groups["nt_name"] or groups["name"] annotation = groups["nt_type"] or groups["type"] text = dedent("\n".join(item[1:])) if annotation is None: # try to retrieve the annotation from the docstring parent with suppress(AttributeError, KeyError): annotation = docstring.parent.returns # type: ignore[union-attr] if annotation.is_generator: receives_item = annotation.slice.elements[1] if isinstance(receives_item, ExprName): annotation = receives_item elif receives_item.is_tuple: annotation = receives_item.slice.elements[index] else: annotation = receives_item else: 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 def _read_raises_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionRaises | None, int]: # raises # EXCEPTION # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty raises section at line {offset}") return None, new_offset raises = [] for item in items: 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 def _read_warns_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionWarns | None, int]: # warns # WARNING # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty warns section at line {offset}") return None, new_offset warns = [] for item in items: 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 def _read_attributes_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionAttributes | None, int]: # attributes (for classes) # NAME( : TYPE)? # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty attributes section at line {offset}") return None, new_offset annotation: str | Expr | None attributes = [] for item in items: name_type = item[0] if ":" in name_type: name, annotation = name_type.split(":", 1) name = name.strip() annotation = annotation.strip() or None else: name = name_type annotation = None if annotation is None: with suppress(AttributeError, KeyError, TypeError): # Use subscript syntax to fetch annotation from inherited members too. annotation = docstring.parent[name].annotation # type: ignore[index] else: 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 def _read_functions_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionFunctions | None, int]: # SIGNATURE # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty functions/methods section at line {offset}") return None, new_offset functions = [] signature: str | Expr | None for item in items: name_signature = item[0] if "(" in name_signature: name = name_signature.split("(", 1)[0] name = name.strip() signature = name_signature.strip() else: name = name_signature signature = None text = dedent("\n".join(item[1:])).strip() functions.append(DocstringFunction(name=name, annotation=signature, description=text)) return DocstringSectionFunctions(functions), new_offset def _read_classes_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionClasses | None, int]: # SIGNATURE # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty classes section at line {offset}") return None, new_offset classes = [] signature: str | Expr | None for item in items: name_signature = item[0] if "(" in name_signature: name = name_signature.split("(", 1)[0] name = name.strip() signature = name_signature.strip() else: name = name_signature signature = None text = dedent("\n".join(item[1:])).strip() classes.append(DocstringClass(name=name, annotation=signature, description=text)) return DocstringSectionClasses(classes), new_offset def _read_modules_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionModules | None, int]: # NAME # TEXT? items, new_offset = _read_block_items(docstring, offset=offset, **options) if not items: docstring_warning(docstring, new_offset, f"Empty modules section at line {offset}") return None, new_offset modules = [] signature: str | Expr | None for item in items: name_signature = item[0] if "(" in name_signature: name = name_signature.split("(", 1)[0] name = name.strip() signature = name_signature.strip() else: name = name_signature signature = None text = dedent("\n".join(item[1:])).strip() modules.append(DocstringModule(name=name, annotation=signature, description=text)) return DocstringSectionModules(modules), new_offset def _read_examples_section( docstring: Docstring, *, offset: int, trim_doctest_flags: bool = True, **options: Any, ) -> tuple[DocstringSectionExamples | None, int]: text, new_offset = _read_block(docstring, offset=offset, **options) sub_sections: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = [] in_code_example = False in_code_block = False current_text: list[str] = [] current_example: list[str] = [] for line in text.split("\n"): if _is_empty_line(line): if in_code_example: if current_example: sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) current_example = [] in_code_example = False else: current_text.append(line) elif in_code_example: if trim_doctest_flags: line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901 line = _RE_DOCTEST_BLANKLINE.sub("", line) # noqa: PLW2901 current_example.append(line) elif line.startswith("```"): in_code_block = not in_code_block current_text.append(line) elif in_code_block: current_text.append(line) elif line.startswith(">>>"): if current_text: sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) current_text = [] in_code_example = True if trim_doctest_flags: line = _RE_DOCTEST_FLAGS.sub("", line) # noqa: PLW2901 current_example.append(line) else: current_text.append(line) if current_text: sub_sections.append((DocstringSectionKind.text, "\n".join(current_text).rstrip("\n"))) elif current_example: sub_sections.append((DocstringSectionKind.examples, "\n".join(current_example))) if sub_sections: return DocstringSectionExamples(sub_sections), new_offset docstring_warning(docstring, new_offset, f"Empty examples section at line {offset}") return None, new_offset def _append_section(sections: list, current: list[str], admonition_title: str) -> None: if admonition_title: kind = admonition_title.lower().replace(" ", "-") if kind in ("warnings", "notes"): # NumpyDoc sections are pluralised but admonitions aren't. # We can special-case these explicitly so that it renders # as one would expect. kind = kind[:-1] sections.append( DocstringSectionAdmonition( kind=kind, text="\n".join(current).rstrip("\n"), title=admonition_title, ), ) elif current and any(current): sections.append(DocstringSectionText("\n".join(current).rstrip("\n"))) _section_reader = { DocstringSectionKind.parameters: _read_parameters_section, DocstringSectionKind.other_parameters: _read_other_parameters_section, DocstringSectionKind.deprecated: _read_deprecated_section, DocstringSectionKind.raises: _read_raises_section, DocstringSectionKind.warns: _read_warns_section, DocstringSectionKind.examples: _read_examples_section, DocstringSectionKind.attributes: _read_attributes_section, DocstringSectionKind.functions: _read_functions_section, DocstringSectionKind.classes: _read_classes_section, DocstringSectionKind.modules: _read_modules_section, DocstringSectionKind.returns: _read_returns_section, DocstringSectionKind.yields: _read_yields_section, DocstringSectionKind.receives: _read_receives_section, } def parse_numpy( docstring: Docstring, *, ignore_init_summary: bool = False, trim_doctest_flags: bool = True, warn_unknown_params: bool = True, **options: Any, ) -> list[DocstringSection]: """Parse a Numpydoc-style docstring. This function iterates on lines of a docstring to build sections. It then returns this list of sections. Parameters: docstring: The docstring to parse. ignore_init_summary: Whether to ignore the summary in `__init__` methods' docstrings. trim_doctest_flags: Whether to remove doctest flags from Python example blocks. warn_unknown_params: Warn about documented parameters not appearing in the signature. **options: Additional parsing options. Returns: A list of docstring sections. """ sections: list[DocstringSection] = [] current_section = [] admonition_title = "" in_code_block = False lines = docstring.lines options = { "trim_doctest_flags": trim_doctest_flags, "ignore_init_summary": ignore_init_summary, "warn_unknown_params": warn_unknown_params, **options, } ignore_summary = ( options["ignore_init_summary"] and docstring.parent is not None and docstring.parent.name == "__init__" and docstring.parent.is_function and docstring.parent.parent is not None and docstring.parent.parent.is_class ) offset = 2 if ignore_summary else 0 while offset < len(lines): line_lower = lines[offset].lower() # Code blocks can contain dash lines that we must not interpret. if in_code_block: # End of code block. if line_lower.lstrip(" ").startswith("```"): in_code_block = False # Lines in code block must not be interpreted in any way. current_section.append(lines[offset]) # Start of code block. elif line_lower.lstrip(" ").startswith("```"): in_code_block = True current_section.append(lines[offset]) # Dash lines after empty lines lose their meaning. elif _is_empty_line(lines[offset]): current_section.append("") # End of the docstring, wrap up. elif offset == len(lines) - 1: current_section.append(lines[offset]) _append_section(sections, current_section, admonition_title) admonition_title = "" current_section = [] # Dash line after regular, non-empty line. elif _is_dash_line(lines[offset + 1]): # Finish reading current section. _append_section(sections, current_section, admonition_title) current_section = [] # Start parsing new (known) section. if line_lower in _section_kind: admonition_title = "" reader = _section_reader[_section_kind[line_lower]] section, offset = reader(docstring, offset=offset + 2, **options) # type: ignore[operator] if section: sections.append(section) # Start parsing admonition. else: admonition_title = lines[offset] offset += 1 # Skip next dash line. # Regular line. else: current_section.append(lines[offset]) offset += 1 # Finish current section. _append_section(sections, current_section, admonition_title) return sections ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/utils.py�������������������������������������������������0000664�0001750�0001750�00000005324�14767006246�022264� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module contains utilities for docstrings parsers. from __future__ import annotations from ast import PyCF_ONLY_AST from contextlib import suppress from typing import TYPE_CHECKING from _griffe.enumerations import LogLevel from _griffe.exceptions import BuiltinModuleError from _griffe.expressions import safe_get_annotation from _griffe.logger import logger if TYPE_CHECKING: from _griffe.expressions import Expr from _griffe.models import Docstring def docstring_warning( docstring: Docstring, offset: int, message: str, log_level: LogLevel = LogLevel.warning, ) -> None: """Log a warning when parsing a docstring. This function logs a warning message by prefixing it with the filepath and line number. Parameters: docstring: The docstring object. offset: The offset in the docstring lines. message: The message to log. Returns: A function used to log parsing warnings if `name` was passed, else none. """ def warn(docstring: Docstring, offset: int, message: str, log_level: LogLevel = LogLevel.warning) -> None: try: prefix = docstring.parent.relative_filepath # type: ignore[union-attr] except (AttributeError, ValueError): prefix = "<module>" except BuiltinModuleError: prefix = f"<module: {docstring.parent.module.name}>" # type: ignore[union-attr] log = getattr(logger, log_level.value) log(f"{prefix}:{(docstring.lineno or 0) + offset}: {message}") warn(docstring, offset, message, log_level) def parse_docstring_annotation( annotation: str, docstring: Docstring, log_level: LogLevel = LogLevel.error, ) -> str | Expr: """Parse a string into a true name or expression that can be resolved later. Parameters: annotation: The annotation to parse. docstring: The docstring in which the annotation appears. The docstring's parent is accessed to bind a resolver to the resulting name/expression. log_level: Log level to use to log a message. Returns: The string unchanged, or a new name or expression. """ with suppress( AttributeError, # Docstring has no parent that can be used to resolve names. SyntaxError, # Annotation contains syntax errors. ): code = compile(annotation, mode="eval", filename="", flags=PyCF_ONLY_AST, optimize=2) if code.body: # type: ignore[attr-defined] name_or_expr = safe_get_annotation( code.body, # type: ignore[attr-defined] parent=docstring.parent, # type: ignore[arg-type] log_level=log_level, ) return name_or_expr or annotation return annotation ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/docstrings/models.py������������������������������������������������0000664�0001750�0001750�00000032111�14767006246�022401� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module contains the models for storing docstrings structured data. from __future__ import annotations from typing import TYPE_CHECKING from _griffe.enumerations import DocstringSectionKind if TYPE_CHECKING: from typing import Any, Literal from _griffe.expressions import Expr # Elements ----------------------------------------------- class DocstringElement: """This base class represents annotated, nameless elements.""" def __init__(self, *, description: str, annotation: str | Expr | None = None) -> None: """Initialize the element. Parameters: annotation: The element annotation, if any. description: The element description. """ self.description: str = description """The element description.""" self.annotation: str | Expr | None = annotation """The element annotation.""" def as_dict(self, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this element's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ return { "annotation": self.annotation, "description": self.description, } class DocstringNamedElement(DocstringElement): """This base class represents annotated, named elements.""" def __init__( self, name: str, *, description: str, annotation: str | Expr | None = None, value: str | None = None, ) -> None: """Initialize the element. Parameters: name: The element name. description: The element description. annotation: The element annotation, if any. value: The element value, as a string. """ super().__init__(description=description, annotation=annotation) self.name: str = name """The element name.""" self.value: str | None = value """The element value, if any""" def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this element's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ base = {"name": self.name, **super().as_dict(**kwargs)} if self.value is not None: base["value"] = self.value return base class DocstringAdmonition(DocstringElement): """This class represents an admonition.""" @property def kind(self) -> str | Expr | None: """The kind of this admonition.""" return self.annotation @kind.setter def kind(self, value: str | Expr) -> None: self.annotation = value @property def contents(self) -> str: """The contents of this admonition.""" return self.description @contents.setter def contents(self, value: str) -> None: self.description = value class DocstringDeprecated(DocstringElement): """This class represents a documented deprecated item.""" @property def version(self) -> str: """The version of this deprecation.""" return self.annotation # type: ignore[return-value] @version.setter def version(self, value: str) -> None: self.annotation = value class DocstringRaise(DocstringElement): """This class represents a documented raise value.""" class DocstringWarn(DocstringElement): """This class represents a documented warn value.""" class DocstringReturn(DocstringNamedElement): """This class represents a documented return value.""" class DocstringYield(DocstringNamedElement): """This class represents a documented yield value.""" class DocstringReceive(DocstringNamedElement): """This class represents a documented receive value.""" class DocstringParameter(DocstringNamedElement): """This class represent a documented function parameter.""" @property def default(self) -> str | None: """The default value of this parameter.""" return self.value @default.setter def default(self, value: str) -> None: self.value = value class DocstringAttribute(DocstringNamedElement): """This class represents a documented module/class attribute.""" class DocstringFunction(DocstringNamedElement): """This class represents a documented function.""" @property def signature(self) -> str | Expr | None: """The function signature.""" return self.annotation class DocstringClass(DocstringNamedElement): """This class represents a documented class.""" @property def signature(self) -> str | Expr | None: """The class signature.""" return self.annotation class DocstringModule(DocstringNamedElement): """This class represents a documented module.""" # Sections ----------------------------------------------- class DocstringSection: """This class represents a docstring section.""" kind: DocstringSectionKind """The section kind.""" def __init__(self, title: str | None = None) -> None: """Initialize the section. Parameters: title: An optional title. """ self.title: str | None = title """The section title.""" self.value: Any = 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]: """Return this section's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ if hasattr(self.value, "as_dict"): # noqa: SIM108 serialized_value = self.value.as_dict(**kwargs) else: serialized_value = self.value base = {"kind": self.kind.value, "value": serialized_value} if self.title: base["title"] = self.title return base class DocstringSectionText(DocstringSection): """This class represents a text section.""" kind: DocstringSectionKind = DocstringSectionKind.text def __init__(self, value: str, title: str | None = None) -> None: """Initialize the section. Parameters: value: The section text. title: An optional title. """ super().__init__(title) self.value: str = value class DocstringSectionParameters(DocstringSection): """This class represents a parameters section.""" kind: DocstringSectionKind = DocstringSectionKind.parameters def __init__(self, value: list[DocstringParameter], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section parameters. title: An optional title. """ super().__init__(title) self.value: list[DocstringParameter] = value class DocstringSectionOtherParameters(DocstringSectionParameters): """This class represents an other parameters section.""" kind: DocstringSectionKind = DocstringSectionKind.other_parameters class DocstringSectionRaises(DocstringSection): """This class represents a raises section.""" kind: DocstringSectionKind = DocstringSectionKind.raises def __init__(self, value: list[DocstringRaise], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section exceptions. title: An optional title. """ super().__init__(title) self.value: list[DocstringRaise] = value class DocstringSectionWarns(DocstringSection): """This class represents a warns section.""" kind: DocstringSectionKind = DocstringSectionKind.warns def __init__(self, value: list[DocstringWarn], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section warnings. title: An optional title. """ super().__init__(title) self.value: list[DocstringWarn] = value class DocstringSectionReturns(DocstringSection): """This class represents a returns section.""" kind: DocstringSectionKind = DocstringSectionKind.returns def __init__(self, value: list[DocstringReturn], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section returned items. title: An optional title. """ super().__init__(title) self.value: list[DocstringReturn] = value class DocstringSectionYields(DocstringSection): """This class represents a yields section.""" kind: DocstringSectionKind = DocstringSectionKind.yields def __init__(self, value: list[DocstringYield], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section yielded items. title: An optional title. """ super().__init__(title) self.value: list[DocstringYield] = value class DocstringSectionReceives(DocstringSection): """This class represents a receives section.""" kind: DocstringSectionKind = DocstringSectionKind.receives def __init__(self, value: list[DocstringReceive], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section received items. title: An optional title. """ super().__init__(title) self.value: list[DocstringReceive] = value class DocstringSectionExamples(DocstringSection): """This class represents an examples section.""" kind: DocstringSectionKind = DocstringSectionKind.examples def __init__( self, value: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]], title: str | None = None, ) -> None: """Initialize the section. Parameters: value: The section examples. title: An optional title. """ super().__init__(title) self.value: list[tuple[Literal[DocstringSectionKind.text, DocstringSectionKind.examples], str]] = value class DocstringSectionAttributes(DocstringSection): """This class represents an attributes section.""" kind: DocstringSectionKind = DocstringSectionKind.attributes def __init__(self, value: list[DocstringAttribute], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section attributes. title: An optional title. """ super().__init__(title) self.value: list[DocstringAttribute] = value class DocstringSectionFunctions(DocstringSection): """This class represents a functions/methods section.""" kind: DocstringSectionKind = DocstringSectionKind.functions def __init__(self, value: list[DocstringFunction], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section functions. title: An optional title. """ super().__init__(title) self.value: list[DocstringFunction] = value class DocstringSectionClasses(DocstringSection): """This class represents a classes section.""" kind: DocstringSectionKind = DocstringSectionKind.classes def __init__(self, value: list[DocstringClass], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section classes. title: An optional title. """ super().__init__(title) self.value: list[DocstringClass] = value class DocstringSectionModules(DocstringSection): """This class represents a modules section.""" kind: DocstringSectionKind = DocstringSectionKind.modules def __init__(self, value: list[DocstringModule], title: str | None = None) -> None: """Initialize the section. Parameters: value: The section modules. title: An optional title. """ super().__init__(title) self.value: list[DocstringModule] = value class DocstringSectionDeprecated(DocstringSection): """This class represents a deprecated section.""" kind: DocstringSectionKind = DocstringSectionKind.deprecated def __init__(self, version: str, text: str, title: str | None = None) -> None: """Initialize the section. Parameters: version: The deprecation version. text: The deprecation text. title: An optional title. """ super().__init__(title) self.value: DocstringDeprecated = DocstringDeprecated(annotation=version, description=text) class DocstringSectionAdmonition(DocstringSection): """This class represents an admonition section.""" kind: DocstringSectionKind = DocstringSectionKind.admonition def __init__(self, kind: str, text: str, title: str | None = None) -> None: """Initialize the section. Parameters: kind: The admonition kind. text: The admonition text. title: An optional title. """ super().__init__(title) self.value: DocstringAdmonition = DocstringAdmonition(annotation=kind, description=text) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/_griffe/collections.py������������������������������������������������������0000664�0001750�0001750�00000005016�14767006246�021261� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This module contains collection-related classes, # which are used throughout the API. from __future__ import annotations from typing import TYPE_CHECKING, Any from _griffe.mixins import DelMembersMixin, GetMembersMixin, SetMembersMixin if TYPE_CHECKING: from collections.abc import ItemsView, KeysView, ValuesView from pathlib import Path from _griffe.models import Module class LinesCollection: """A simple dictionary containing the modules source code lines.""" def __init__(self) -> None: """Initialize the collection.""" 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: """Return the collection keys. Returns: The collection keys. """ return self._data.keys() def values(self) -> ValuesView: """Return the collection values. Returns: The collection values. """ return self._data.values() def items(self) -> ItemsView: """Return the collection items. Returns: The collection items. """ return self._data.items() 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.""" self.members: dict[str, Module] = {} """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 def all_members(self) -> dict[str, Module]: """Members of the collection. This property is overwritten to simply return `self.members`, as `all_members` does not make sense for a modules collection. """ return self.members ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/griffe/���������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�016230� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/griffe/__init__.py����������������������������������������������������������0000664�0001750�0001750�00000042545�14767006246�020353� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# This top-level module imports all public names from the package, # and exposes them as public objects. We have tests to make sure # no object is forgotten in this list. """Griffe package. Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. The entirety of the public API is exposed here, in the top-level `griffe` module. All messages written to standard output or error are logged using the `logging` module. Our logger's name is set to `"griffe"` and is public (you can rely on it). You can obtain the logger from the standard `logging` module: `logging.getLogger("griffe")`. Actual logging messages are not part of the public API (they might change without notice). Raised exceptions throughout the package are part of the public API (you can rely on them). Their actual messages are not part of the public API (they might change without notice). The following paragraphs will help you discover the package's content. ## CLI entrypoints Griffe provides a command-line interface (CLI) to interact with the package. The CLI entrypoints can be called from Python code. - [`griffe.main`][]: Run the main program. - [`griffe.check`][]: Check for API breaking changes in two versions of the same package. - [`griffe.dump`][]: Load packages data and dump it as JSON. ## Loaders To load API data, Griffe provides several high-level functions. - [`griffe.load`][]: Load and return a Griffe object. - [`griffe.load_git`][]: Load and return a module from a specific Git reference. - [`griffe.load_pypi`][]: Load and return a module from a specific package version downloaded using pip. ## Models The data loaded by Griffe is represented by several classes. - [`griffe.Module`][]: The class representing a Python module. - [`griffe.Class`][]: The class representing a Python class. - [`griffe.Function`][]: The class representing a Python function or method. - [`griffe.Attribute`][]: The class representing a Python attribute. - [`griffe.Alias`][]: This class represents an alias, or indirection, to an object declared in another module. Additional classes are available to represent other concepts. - [`griffe.Decorator`][]: This class represents a decorator. - [`griffe.Parameters`][]: This class is a container for parameters. - [`griffe.Parameter`][]: This class represent a function parameter. ## Agents Griffe is able to analyze code both statically and dynamically, using the following "agents". However most of the time you will only need to use the loaders above. - [`griffe.visit`][]: Parse and visit a module file. - [`griffe.inspect`][]: Inspect a module. ## Serializers Griffe can serizalize data to dictionary and JSON. - [`griffe.Object.as_json`][griffe.Object.as_json] - [`griffe.Object.from_json`][griffe.Object.from_json] - [`griffe.JSONEncoder`][]: JSON encoder for Griffe objects. - [`griffe.json_decoder`][]: JSON decoder for Griffe objects. ## API checks Griffe can compare two versions of the same package to find breaking changes. - [`griffe.find_breaking_changes`][]: Find breaking changes between two versions of the same API. - [`griffe.Breakage`][]: Breakage classes can explain what broke from a version to another. ## Extensions Griffe supports extensions. You can create your own extension by subclassing the `griffe.Extension` class. - [`griffe.load_extensions`][]: Load configured extensions. - [`griffe.Extension`][]: Base class for Griffe extensions. ## Docstrings Griffe can parse docstrings into structured data. Main class: - [`griffe.Docstring`][]: This class represents docstrings. Docstring section and element classes all start with `Docstring`. Docstring parsers: - [`griffe.parse`][]: Parse the docstring. - [`griffe.parse_auto`][]: Parse a docstring by automatically detecting the style it uses. - [`griffe.parse_google`][]: Parse a Google-style docstring. - [`griffe.parse_numpy`][]: Parse a Numpydoc-style docstring. - [`griffe.parse_sphinx`][]: Parse a Sphinx-style docstring. ## Exceptions Griffe uses several exceptions to signal errors. - [`griffe.GriffeError`][]: The base exception for all Griffe errors. - [`griffe.LoadingError`][]: Exception for loading errors. - [`griffe.NameResolutionError`][]: Exception for names that cannot be resolved in a object scope. - [`griffe.UnhandledEditableModuleError`][]: Exception for unhandled editables modules, when searching modules. - [`griffe.UnimportableModuleError`][]: Exception for modules that cannot be imported. - [`griffe.AliasResolutionError`][]: Exception for aliases that cannot be resolved. - [`griffe.CyclicAliasError`][]: Exception raised when a cycle is detected in aliases. - [`griffe.LastNodeError`][]: Exception raised when trying to access a next or previous node. - [`griffe.RootNodeError`][]: Exception raised when trying to use siblings properties on a root node. - [`griffe.BuiltinModuleError`][]: Exception raised when trying to access the filepath of a builtin module. - [`griffe.ExtensionError`][]: Base class for errors raised by extensions. - [`griffe.ExtensionNotLoadedError`][]: Exception raised when an extension could not be loaded. - [`griffe.GitError`][]: Exception raised for errors related to Git. # Expressions Griffe stores snippets of code (attribute values, decorators, base class, type annotations) as expressions. Expressions are basically abstract syntax trees (AST) with a few differences compared to the nodes returned by [`ast`][]. Griffe provides a few helpers to extract expressions from regular AST nodes. - [`griffe.get_annotation`][]: Get a type annotation as expression. - [`griffe.get_base_class`][]: Get a base class as expression. - [`griffe.get_condition`][]: Get a condition as expression. - [`griffe.get_expression`][]: Get an expression from an AST node. - [`griffe.safe_get_annotation`][]: Get a type annotation as expression, safely (returns `None` on error). - [`griffe.safe_get_base_class`][]: Get a base class as expression, safely (returns `None` on error). - [`griffe.safe_get_condition`][]: Get a condition as expression, safely (returns `None` on error). - [`griffe.safe_get_expression`][]: Get an expression from an AST node, safely (returns `None` on error). The base class for expressions. - [`griffe.Expr`][] Expression classes all start with `Expr`. # Loggers If you want to log messages from extensions, get a logger with `get_logger`. The `logger` attribute is used by Griffe itself. You can use it to temporarily disable Griffe logging. - [`griffe.logger`][]: Our global logger, used throughout the library. - [`griffe.get_logger`][]: Create and return a new logger instance. # Helpers To test your Griffe extensions, or to load API data from code in memory, Griffe provides the following helpers. - [`griffe.temporary_pyfile`][]: Create a Python file containing the given code in a temporary directory. - [`griffe.temporary_pypackage`][]: Create a package containing the given modules in a temporary directory. - [`griffe.temporary_visited_module`][]: Create and visit a temporary module with the given code. - [`griffe.temporary_visited_package`][]: Create and visit a temporary package. - [`griffe.temporary_inspected_module`][]: Create and inspect a temporary module with the given code. - [`griffe.temporary_inspected_package`][]: Create and inspect a temporary package. """ from __future__ import annotations from _griffe.agents.inspector import Inspector, inspect from _griffe.agents.nodes.assignments import get_instance_names, get_name, get_names 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 # YORE: Bump 2: Replace `ExportedName, ` with `` within line. from _griffe.agents.nodes.exports import ExportedName, get__all__, safe_get__all__ from _griffe.agents.nodes.imports import relative_to_absolute from _griffe.agents.nodes.parameters import ParametersType, get_parameters from _griffe.agents.nodes.runtime import ObjectNode from _griffe.agents.nodes.values import get_value, safe_get_value from _griffe.agents.visitor import Visitor, builtin_decorators, stdlib_decorators, typing_overload, visit from _griffe.c3linear import c3linear_merge from _griffe.cli import DEFAULT_LOG_LEVEL, check, dump, get_parser, main from _griffe.collections import LinesCollection, ModulesCollection from _griffe.diff import ( AttributeChangedTypeBreakage, AttributeChangedValueBreakage, Breakage, ClassRemovedBaseBreakage, ObjectChangedKindBreakage, ObjectRemovedBreakage, ParameterAddedRequiredBreakage, ParameterChangedDefaultBreakage, ParameterChangedKindBreakage, ParameterChangedRequiredBreakage, ParameterMovedBreakage, ParameterRemovedBreakage, ReturnChangedTypeBreakage, find_breaking_changes, ) from _griffe.docstrings.google import parse_google from _griffe.docstrings.models import ( DocstringAdmonition, DocstringAttribute, DocstringClass, DocstringDeprecated, DocstringElement, DocstringFunction, DocstringModule, DocstringNamedElement, DocstringParameter, DocstringRaise, DocstringReceive, DocstringReturn, DocstringSection, DocstringSectionAdmonition, DocstringSectionAttributes, DocstringSectionClasses, DocstringSectionDeprecated, DocstringSectionExamples, DocstringSectionFunctions, DocstringSectionModules, DocstringSectionOtherParameters, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReceives, DocstringSectionReturns, DocstringSectionText, DocstringSectionWarns, DocstringSectionYields, DocstringWarn, DocstringYield, ) from _griffe.docstrings.numpy import parse_numpy from _griffe.docstrings.parsers import ( DocstringDetectionMethod, DocstringStyle, infer_docstring_style, parse, parse_auto, parsers, ) from _griffe.docstrings.sphinx import parse_sphinx from _griffe.docstrings.utils import docstring_warning, parse_docstring_annotation from _griffe.encoders import JSONEncoder, json_decoder from _griffe.enumerations import ( BreakageKind, DocstringSectionKind, ExplanationStyle, Kind, LogLevel, ObjectKind, ParameterKind, Parser, ) from _griffe.exceptions import ( AliasResolutionError, BuiltinModuleError, CyclicAliasError, ExtensionError, ExtensionNotLoadedError, GitError, GriffeError, LastNodeError, LoadingError, NameResolutionError, RootNodeError, UnhandledEditableModuleError, UnimportableModuleError, ) from _griffe.expressions import ( Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprCall, ExprCompare, ExprComprehension, ExprConstant, ExprDict, ExprDictComp, ExprExtSlice, ExprFormatted, ExprGeneratorExp, ExprIfExp, ExprJoinedStr, ExprKeyword, ExprLambda, ExprList, ExprListComp, ExprName, ExprNamedExpr, ExprParameter, ExprSet, ExprSetComp, ExprSlice, ExprSubscript, ExprTuple, ExprUnaryOp, ExprVarKeyword, ExprVarPositional, ExprYield, ExprYieldFrom, get_annotation, get_base_class, get_condition, get_expression, safe_get_annotation, safe_get_base_class, safe_get_condition, safe_get_expression, ) from _griffe.extensions.base import ( Extension, Extensions, LoadableExtensionType, builtin_extensions, load_extensions, ) from _griffe.extensions.dataclasses import DataclassesExtension from _griffe.finder import ModuleFinder, NamePartsAndPathType, NamePartsType, NamespacePackage, Package from _griffe.git import assert_git_repo, get_latest_tag, get_repo_root, tmp_worktree from _griffe.importer import dynamic_import, sys_path from _griffe.loader import GriffeLoader, load, load_git, load_pypi from _griffe.logger import Logger, get_logger, logger, patch_loggers from _griffe.merger import merge_stubs from _griffe.mixins import ( DelMembersMixin, GetMembersMixin, ObjectAliasMixin, SerializationMixin, SetMembersMixin, ) from _griffe.models import ( Alias, Attribute, Class, Decorator, Docstring, Function, Module, Object, Parameter, Parameters, ) from _griffe.stats import Stats from _griffe.tests import ( TmpPackage, htree, module_vtree, temporary_inspected_module, temporary_inspected_package, temporary_pyfile, temporary_pypackage, temporary_visited_module, temporary_visited_package, vtree, ) # Regenerate this list with the following Python snippet: # import griffe # names = sorted(n for n in dir(griffe) if not n.startswith("_") and n not in ("annotations", "lazy_importing")) # print('__all__ = [\n "' + '",\n "'.join(names) + '",\n]') __all__ = [ "DEFAULT_LOG_LEVEL", "Alias", "AliasResolutionError", "Attribute", "AttributeChangedTypeBreakage", "AttributeChangedValueBreakage", "Breakage", "BreakageKind", "BuiltinModuleError", "Class", "ClassRemovedBaseBreakage", "CyclicAliasError", "DataclassesExtension", "Decorator", "DelMembersMixin", "Docstring", "DocstringAdmonition", "DocstringAttribute", "DocstringClass", "DocstringDeprecated", "DocstringDetectionMethod", "DocstringElement", "DocstringFunction", "DocstringModule", "DocstringNamedElement", "DocstringParameter", "DocstringRaise", "DocstringReceive", "DocstringReturn", "DocstringSection", "DocstringSectionAdmonition", "DocstringSectionAttributes", "DocstringSectionClasses", "DocstringSectionDeprecated", "DocstringSectionExamples", "DocstringSectionFunctions", "DocstringSectionKind", "DocstringSectionModules", "DocstringSectionOtherParameters", "DocstringSectionParameters", "DocstringSectionRaises", "DocstringSectionReceives", "DocstringSectionReturns", "DocstringSectionText", "DocstringSectionWarns", "DocstringSectionYields", "DocstringStyle", "DocstringWarn", "DocstringYield", "ExplanationStyle", # YORE: Bump 2: Remove line. "ExportedName", "Expr", "ExprAttribute", "ExprBinOp", "ExprBoolOp", "ExprCall", "ExprCompare", "ExprComprehension", "ExprConstant", "ExprDict", "ExprDictComp", "ExprExtSlice", "ExprFormatted", "ExprGeneratorExp", "ExprIfExp", "ExprJoinedStr", "ExprKeyword", "ExprLambda", "ExprList", "ExprListComp", "ExprName", "ExprNamedExpr", "ExprParameter", "ExprSet", "ExprSetComp", "ExprSlice", "ExprSubscript", "ExprTuple", "ExprUnaryOp", "ExprVarKeyword", "ExprVarPositional", "ExprYield", "ExprYieldFrom", "Extension", "ExtensionError", "ExtensionNotLoadedError", "Extensions", "Function", "GetMembersMixin", "GitError", "GriffeError", "GriffeLoader", "Inspector", "JSONEncoder", "Kind", "LastNodeError", "LinesCollection", "LoadableExtensionType", "LoadingError", "LogLevel", "Logger", "Module", "ModuleFinder", "ModulesCollection", "NamePartsAndPathType", "NamePartsType", "NameResolutionError", "NamespacePackage", "Object", "ObjectAliasMixin", "ObjectChangedKindBreakage", "ObjectKind", "ObjectNode", "ObjectRemovedBreakage", "Package", "Parameter", "ParameterAddedRequiredBreakage", "ParameterChangedDefaultBreakage", "ParameterChangedKindBreakage", "ParameterChangedRequiredBreakage", "ParameterKind", "ParameterMovedBreakage", "ParameterRemovedBreakage", "Parameters", "ParametersType", "Parser", "ReturnChangedTypeBreakage", "RootNodeError", "SerializationMixin", "SetMembersMixin", "Stats", "TmpPackage", "UnhandledEditableModuleError", "UnimportableModuleError", "Visitor", "assert_git_repo", "ast_children", "ast_first_child", "ast_kind", "ast_last_child", "ast_next", "ast_next_siblings", "ast_previous", "ast_previous_siblings", "ast_siblings", "builtin_decorators", "builtin_extensions", "c3linear_merge", "check", "docstring_warning", "dump", "dynamic_import", "find_breaking_changes", "get__all__", "get_annotation", "get_base_class", "get_condition", "get_docstring", "get_expression", "get_instance_names", "get_latest_tag", "get_logger", "get_name", "get_names", "get_parameters", "get_parser", "get_repo_root", "get_value", "htree", "infer_docstring_style", "inspect", "json_decoder", "load", "load_extensions", "load_git", "load_pypi", "logger", "main", "merge_stubs", "module_vtree", "parse", "parse_auto", "parse_docstring_annotation", "parse_google", "parse_numpy", "parse_sphinx", "parsers", "patch_loggers", "relative_to_absolute", "safe_get__all__", "safe_get_annotation", "safe_get_base_class", "safe_get_condition", "safe_get_expression", "safe_get_value", "stdlib_decorators", "sys_path", "temporary_inspected_module", "temporary_inspected_package", "temporary_pyfile", "temporary_pypackage", "temporary_visited_module", "temporary_visited_package", "tmp_worktree", "typing_overload", "visit", "vtree", ] �����������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/griffe/py.typed�������������������������������������������������������������0000664�0001750�0001750�00000000000�14767006246�017715� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/src/griffe/__main__.py����������������������������������������������������������0000664�0001750�0001750�00000000525�14767006246�020324� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Entry-point module, in case you use `python -m griffe`. # # Why does this file exist, and why `__main__`? For more info, read: # # - https://www.python.org/dev/peps/pep-0338/ # - https://docs.python.org/3/using/cmdline.html#cmdoption-m import sys from _griffe.cli import main if __name__ == "__main__": sys.exit(main(sys.argv[1:])) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/mkdocs.yml����������������������������������������������������������������������0000664�0001750�0001750�00000022747�14767006246�016216� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������site_name: "Griffe" site_description: "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." site_url: "https://mkdocstrings.github.io/griffe" repo_url: "https://github.com/mkdocstrings/griffe" repo_name: "mkdocstrings/griffe" site_dir: "site" watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src] copyright: Copyright © 2021 Timothée Mazzucotelli edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn not_in_nav: | usage.md nav: - Home: index.md - Getting started: - getting-started.md - Installation: installation.md - Introduction: introduction.md - Playground: playground.md - License: license.md - Community: - community.md - Getting help: getting-help.md - Contributing: contributing.md - Code of conduct: code-of-conduct.md - Alternatives: alternatives.md - Downstream projects: downstream-projects.md - Credits: credits.md - Guide: - guide.md - User guide: - guide/users.md - Manipulating APIs: - Loading: guide/users/loading.md - Navigating: guide/users/navigating.md - Serializing: guide/users/serializing.md - Checking: guide/users/checking.md - Extending: guide/users/extending.md - Recommendations: - Public APIs: guide/users/recommendations/public-apis.md - Python code: guide/users/recommendations/python-code.md - Docstrings: guide/users/recommendations/docstrings.md - How-to: - Parse docstrings: guide/users/how-to/parse-docstrings.md - Support custom decorators: guide/users/how-to/support-decorators.md - Selectively inspect objects: guide/users/how-to/selectively-inspect.md - Set objects' docstring style: guide/users/how-to/set-docstring-styles.md - Contributor guide: - guide/contributors.md - Environment setup: guide/contributors/setup.md - Management commands: guide/contributors/commands.md - Development workflow: guide/contributors/workflow.md - Project architecture: guide/contributors/architecture.md - Coverage report: guide/contributors/coverage.md - Extensions: - extensions.md - Built-in: - extensions/built-in.md - dataclasses: extensions/built-in/dataclasses.md - Official: - extensions/official.md - autodocstringstyle: extensions/official/autodocstringstyle.md - inherited-docstrings: extensions/official/inherited-docstrings.md - public-redundant-aliases: extensions/official/public-redundant-aliases.md - public-wildcard-imports: extensions/official/public-wildcard-imports.md - pydantic: extensions/official/pydantic.md - runtime-objects: extensions/official/runtime-objects.md - sphinx: extensions/official/sphinx.md - typingdoc: extensions/official/typingdoc.md - warnings-deprecated: extensions/official/warnings-deprecated.md - Third-party: - extensions/third-party.md - docstring-inheritance: extensions/third-party/docstring-inheritance.md - fieldz: extensions/third-party/fieldz.md - generics: extensions/third-party/generics.md - inherited-method-crossrefs: extensions/third-party/inherited-method-crossrefs.md - modernized-annotations: extensions/third-party/modernized-annotations.md - Reference: - reference.md - Command-line interface: reference/cli.md - Docstring parsers: reference/docstrings.md - Python API: - reference/api.md - CLI entrypoints: reference/api/cli.md - Loaders: reference/api/loaders.md - Finder: reference/api/finder.md - Models: - reference/api/models.md - Module: reference/api/models/module.md - Class: reference/api/models/class.md - Function: reference/api/models/function.md - Attribute: reference/api/models/attribute.md - Alias: reference/api/models/alias.md - Agents: reference/api/agents.md - Serializers: reference/api/serializers.md - API checks: reference/api/checks.md - Extensions: reference/api/extensions.md - Docstrings: - reference/api/docstrings.md - Models: reference/api/docstrings/models.md - Parsers: reference/api/docstrings/parsers.md - Exceptions: reference/api/exceptions.md - Expressions: reference/api/expressions.md - Git utilities: reference/api/git.md - Loggers: reference/api/loggers.md - Helpers: reference/api/helpers.md - Changelog: changelog.md - Insiders: - insiders/index.md - Getting started: - Installation: insiders/installation.md - Changelog: insiders/changelog.md - Author's website: https://pawamoy.github.io/ theme: name: material custom_dir: docs/.overrides logo: logo.svg favicon: img/favicon.ico features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.expand - navigation.footer - navigation.instant.preview - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky - navigation.top - search.highlight - search.suggest - toc.follow palette: - media: "(prefers-color-scheme)" toggle: icon: material/brightness-auto name: Switch to light mode - media: "(prefers-color-scheme: light)" scheme: default primary: teal accent: purple toggle: icon: material/weather-sunny name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate primary: black accent: lime toggle: icon: material/weather-night name: Switch to system preference extra_css: - css/custom.css - css/material.css - css/mkdocstrings.css - css/insiders.css extra_javascript: - js/feedback.js - https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js markdown_extensions: - attr_list - admonition - callouts: strip_period: no - footnotes - md_in_html - pymdownx.blocks.tab: alternate_style: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.keys - pymdownx.magiclink - pymdownx.snippets: base_path: [!relative $config_dir] check_paths: true - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - toc: permalink: "¤" plugins: - search - autorefs - markdown-exec: ansi: required - gen-files: scripts: - scripts/gen_griffe_json.py - section-index - coverage: page_path: guide/contributors/coverage - mkdocstrings: enabled: !ENV [MKDOCSTRINGS_ENABLED, true] handlers: python: inventories: # YORE: BOL 3.13: Replace `3.13` with `3` within line. - url: https://docs.python.org/3.13/objects.inv domains: [std, py] - https://typing-extensions.readthedocs.io/en/latest/objects.inv paths: [src, scripts, .] options: backlinks: tree docstring_options: ignore_init_summary: true docstring_style: google docstring_section_style: list extensions: - griffe_inherited_docstrings heading_level: 2 inherited_members: true merge_init_into_class: true parameter_headings: true separate_signature: true show_bases: true show_inheritance_diagram: true show_root_heading: true show_root_full_path: false show_source: false show_signature_annotations: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - llmstxt: files: - output: llms-full.txt inputs: - index.md - reference/**.md - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true type: timeago - minify: minify_html: !ENV [DEPLOY, false] - redirects: redirect_maps: cli_reference.md: reference/cli.md checking.md: guide/users/checking.md dumping.md: guide/users/serializing.md loading.md: guide/users/loading.md expressions.md: guide/users/navigating.md#expressions best_practices.md: guide/users/recommendations/python-code.md docstrings.md: reference/docstrings.md parsing_docstrings.md: guide/users/how-to/parse-docstrings.md try_it_out.md: playground.md reference/griffe.md: reference/api.md code_of_conduct.md: code-of-conduct.md - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: - typeset extra: social: - icon: fontawesome/brands/github link: https://github.com/pawamoy - icon: fontawesome/brands/mastodon link: https://fosstodon.org/@pawamoy - icon: fontawesome/brands/twitter link: https://twitter.com/pawamoy - icon: fontawesome/brands/gitter link: https://gitter.im/griffe/community - icon: fontawesome/brands/python link: https://pypi.org/project/griffe/ analytics: feedback: title: Was this page helpful? ratings: - icon: material/emoticon-happy-outline name: This page was helpful data: 1 note: Thanks for your feedback! - icon: material/emoticon-sad-outline name: This page could be improved data: 0 note: Let us know how we can improve this page. �������������������������python-griffe-1.6.2/.envrc��������������������������������������������������������������������������0000664�0001750�0001750�00000000021�14767006246�015306� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PATH_add scripts ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/README.md�����������������������������������������������������������������������0000664�0001750�0001750�00000006146�14767006246�015465� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Griffe [![ci](https://github.com/mkdocstrings/griffe/workflows/ci/badge.svg)](https://github.com/mkdocstrings/griffe/actions?query=workflow%3Aci) [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/griffe/) [![pypi version](https://img.shields.io/pypi/v/griffe.svg)](https://pypi.org/project/griffe/) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im) <img src="logo.svg" alt="Griffe logo, created by François Rozet <francois.rozet@outlook.com>" style="float: right; max-width: 200px; margin: 0 15px;"> Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. Griffe, pronounced "grif" (`/ɡʁif/`), is a french word that means "claw", but also "signature" in a familiar way. "On reconnaît bien là sa griffe." - [User guide](https://mkdocstrings.github.io/griffe/guide/users/) - [Contributor guide](https://mkdocstrings.github.io/griffe/guide/contributors/) - [API reference](https://mkdocstrings.github.io/griffe/reference/api/) ## Installation ```bash pip install griffe ``` With [`uv`](https://docs.astral.sh/uv/): ```bash uv tool install griffe ``` ## Usage ### Dump JSON-serialized API **On the command line**, pass the names of packages to the `griffe dump` command: ```console $ griffe dump httpx fastapi { "httpx": { "name": "httpx", ... }, "fastapi": { "name": "fastapi", ... } } ``` See the [Serializing chapter](https://mkdocstrings.github.io/griffe/guide/users/serializing/) for more examples. ### Check for API breaking changes Pass a relative path to the `griffe check` command: ```console $ griffe check mypackage --verbose mypackage/mymodule.py:10: MyClass.mymethod(myparam): Parameter kind was changed: Old: positional or keyword New: keyword-only ``` For `src` layouts: ```console $ griffe check --search src mypackage --verbose src/mypackage/mymodule.py:10: MyClass.mymethod(myparam): Parameter kind was changed: Old: positional or keyword New: keyword-only ``` It's also possible to directly **check packages from PyPI.org** (or other indexes configured through `PIP_INDEX_URL`). This feature is [available to sponsors only](https://mkdocstrings.github.io/griffe/insiders/) and requires that you install Griffe with the `pypi` extra: ```bash pip install griffe[pypi] ``` The command syntax is: ```bash griffe check package_name -b project-name==2.0 -a project-name==1.0 ``` See the [Checking chapter](https://mkdocstrings.github.io/griffe/guide/users/checking/) for more examples. ### Load and navigate data with Python **With Python**, loading a package: ```python import griffe fastapi = griffe.load("fastapi") ``` Finding breaking changes: ```python import griffe previous = griffe.load_git("mypackage", ref="0.2.0") current = griffe.load("mypackage") for breakage in griffe.find_breaking_changes(previous, current): ... ``` See the [Loading chapter](https://mkdocstrings.github.io/griffe/guide/users/loading/) for more examples. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/pyproject.toml������������������������������������������������������������������0000664�0001750�0001750�00000007236�14767006246�017123� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [project] name = "griffe" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] license = "ISC" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" keywords = ["api", "signature", "breaking-changes", "static-analysis", "dynamic-analysis"] dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", # YORE: EOL 3.9: Remove line. "Programming Language :: Python :: 3.9", # YORE: EOL 3.10: Remove line. "Programming Language :: Python :: 3.10", # YORE: EOL 3.11: Remove line. "Programming Language :: Python :: 3.11", # YORE: EOL 3.12: Remove line. "Programming Language :: Python :: 3.12", # YORE: EOL 3.13: Remove line. "Programming Language :: Python :: 3.13", # YORE: EOL 3.14: Remove line. "Programming Language :: Python :: 3.14", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ # YORE: EOL 3.8: Remove line. "astunparse>=1.6; python_version < '3.9'", "colorama>=0.4", ] [project.urls] Homepage = "https://mkdocstrings.github.io/griffe" Documentation = "https://mkdocstrings.github.io/griffe" Changelog = "https://mkdocstrings.github.io/griffe/changelog" Repository = "https://github.com/mkdocstrings/griffe" Issues = "https://github.com/mkdocstrings/griffe/issues" Discussions = "https://github.com/mkdocstrings/griffe/discussions" Gitter = "https://gitter.im/mkdocstrings/griffe" Funding = "https://github.com/sponsors/pawamoy" [project.scripts] griffe = "griffe:main" [tool.pdm.version] source = "call" getter = "scripts.get_version:get_version" [tool.pdm.build] # Include as much as possible in the source distribution, to help redistributors. excludes = ["**/.pytest_cache"] source-includes = [ "config", "docs", "scripts", "share", "tests", "duties.py", "mkdocs.yml", "*.md", "LICENSE", ] [tool.pdm.build.wheel-data] # Manual pages can be included in the wheel. # Depending on the installation tool, they will be accessible to users. # pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. data = [ {path = "share/**/*", relative-to = "."}, ] [dependency-groups] maintain = [ "build>=1.2", "git-changelog>=2.5", "twine>=5.1", "yore>=0.3.3", ] ci = [ "duty>=1.6", "ruff>=0.4", "jsonschema>=4.17", "pysource-codegen>=0.4", "pysource-minimize>=0.5", "pytest>=8.2", "pytest-cov>=5.0", "pytest-randomly>=3.15", "pytest-xdist>=3.6", "mypy>=1.10", "types-markdown>=3.6", "types-pyyaml>=6.0", ] docs = [ "code2flow>=2.5", "griffe-inherited-docstrings>=1.0", "markdown-callouts>=0.4", "markdown-exec[ansi]>=1.8", "mkdocs>=1.6", "mkdocs-coverage>=1.0", "mkdocs-gen-files>=0.5", "mkdocs-git-revision-date-localized-plugin>=1.2", "mkdocs-llmstxt>=0.1", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-section-index>=0.3", "mkdocs-redirects>=1.2", "mkdocstrings[python]>=0.29", "pydeps>=1.12", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] [tool.uv] default-groups = ["maintain", "ci", "docs"] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015666� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/gen_griffe_json.py������������������������������������������������������0000664�0001750�0001750�00000000461�14767006246�021365� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Generate the credits page.""" import mkdocs_gen_files import griffe with mkdocs_gen_files.open("griffe.json", "w") as fd: griffe.dump( ["griffe", "_griffe"], docstring_parser=griffe.Parser.google, docstring_options={"ignore_init_summary": True}, output=fd, ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/insiders.py�������������������������������������������������������������0000664�0001750�0001750�00000013632�14767006246�020065� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Functions related to Insiders funding goals. from __future__ import annotations import json import logging import os import posixpath from dataclasses import dataclass from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path from typing import TYPE_CHECKING, cast from urllib.error import HTTPError from urllib.parse import urljoin from urllib.request import urlopen import yaml if TYPE_CHECKING: from collections.abc import Iterable logger = logging.getLogger(f"mkdocs.logs.{__name__}") def human_readable_amount(amount: int) -> str: str_amount = str(amount) if len(str_amount) >= 4: # noqa: PLR2004 return f"{str_amount[: len(str_amount) - 3]},{str_amount[-3:]}" return str_amount @dataclass class Project: name: str url: str @dataclass class Feature: name: str ref: str | None since: date | None project: Project | None def url(self, rel_base: str = "..") -> str | None: # noqa: D102 if not self.ref: return None if self.project: rel_base = self.project.url return posixpath.join(rel_base, self.ref.lstrip("/")) def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102 new = "" if badge: recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011 if recent: ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr] new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}' project = f"[{self.project.name}]({self.project.url}) — " if self.project else "" feature = f"[{self.name}]({self.url(rel_base)})" if self.ref else self.name print(f"- [{'x' if self.since else ' '}] {project}{feature}{new}") @dataclass class Goal: name: str amount: int features: list[Feature] complete: bool = False @property def human_readable_amount(self) -> str: # noqa: D102 return human_readable_amount(self.amount) def render(self, rel_base: str = "..") -> None: # noqa: D102 print(f"#### $ {self.human_readable_amount} — {self.name}\n") if self.features: for feature in self.features: feature.render(rel_base) print() else: print("There are no features in this goal for this project. ") print( "[See the features in this goal **for all Insiders projects.**]" f"(https://pawamoy.github.io/insiders/#{self.amount}-{self.name.lower().replace(' ', '-')})", ) def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]: goals_data = yaml.safe_load(data)["goals"] return { amount: Goal( name=goal_data["name"], amount=amount, complete=funding >= amount, features=[ Feature( name=feature_data["name"], ref=feature_data.get("ref"), since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 project=project, ) for feature_data in goal_data["features"] ], ) for amount, goal_data in goals_data.items() } def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]: project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".") try: data = Path(project_dir, path).read_text() except OSError as error: raise RuntimeError(f"Could not load data from disk: {path}") from error return load_goals(data, funding) def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: project_name, project_url, data_fragment = source_data data_url = urljoin(project_url, data_fragment) try: with urlopen(data_url) as response: # noqa: S310 data = response.read() except HTTPError as error: raise RuntimeError(f"Could not load data from network: {data_url}") from error return load_goals(data, funding, project=Project(name=project_name, url=project_url)) def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]: if isinstance(source, str): return _load_goals_from_disk(source, funding) return _load_goals_from_url(source, funding) def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]: if isinstance(source, str): return _load_goals_from_disk(source, funding) goals = {} for src in source: source_goals = _load_goals(src, funding) for amount, goal in source_goals.items(): if amount not in goals: goals[amount] = goal else: goals[amount].features.extend(goal.features) return {amount: goals[amount] for amount in sorted(goals)} def feature_list(goals: Iterable[Goal]) -> list[Feature]: return list(chain.from_iterable(goal.features for goal in goals)) def load_json(url: str) -> str | list | dict: with urlopen(url) as response: # noqa: S310 return json.loads(response.read().decode()) data_source = globals()["data_source"] sponsor_url = "https://github.com/sponsors/pawamoy" data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main" numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment] sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment] current_funding = numbers["total"] sponsors_count = numbers["count"] goals = funding_goals(data_source, funding=current_funding) ongoing_goals = [goal for goal in goals.values() if not goal.complete] unreleased_features = sorted( (ft for ft in feature_list(ongoing_goals) if ft.since), key=lambda ft: cast("date", ft.since), reverse=True, ) ������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/gen_structure_docs.py���������������������������������������������������0000664�0001750�0001750�00000006234�14767006246�022146� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Render docs for our program structure.""" import os import subprocess from io import StringIO from pathlib import Path from textwrap import dedent from unittest.mock import MagicMock from code2flow import code2flow, engine, model engine.logging = MagicMock() model.TRUNK_COLOR = "#fca311" model.LEAF_COLOR = "#98c1d9" model.EDGE_COLORS = ["#b8b8ff"] * 8 model.NODE_COLOR = "#e5e5e5" def _render_call_graph(module: Path) -> None: buffer = StringIO() code2flow(str(module), buffer) try: svg = subprocess.check_output(["dot", "-Tsvg"], input=buffer.getvalue(), text=True) # noqa: S603, S607 except subprocess.CalledProcessError: # The subprocess dies with SIGSEGV in GHA... return if 'class="node"' not in svg: print() else: print(f'<div class="interactiveSVG code2flow">{svg}</div>') def _comment_block(module: Path) -> str: lines = [] with module.open() as file: for line in file: if line.startswith("#"): lines.append(line[1:]) else: break return dedent("".join(lines)) def _render_api(path: Path, root: Path, heading_level: int = 4) -> None: for module in sorted(path.iterdir()): if module.name in ("__main__.py", "__init__.py"): continue rel_path = str(module.relative_to(root).with_suffix("")).replace("/", "-") if module.suffix == ".py": print(f"{'#' * heading_level} `{module.name}` {{#{rel_path}}}\n") print(_comment_block(module)) _render_call_graph(module) elif module.is_dir() and module.joinpath("__init__.py").exists(): print(f"{'#' * heading_level} `{module.name}` {{#{rel_path}}}\n") print(_comment_block(module / "__init__.py")) _render_api(module, root, heading_level + 1) def render_internal_api(heading_level: int = 4) -> None: """Render Griffe's internal API's structure docs. This function prints Markdown headings, and the contents of the first comment block of a module, for all modules in our internal API. Parameters: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) src = root / "src" internal_api = src / "_griffe" print(_comment_block(internal_api / "__init__.py")) _render_api(internal_api, internal_api, heading_level) def render_public_api(heading_level: int = 4) -> None: """Render Griffe's main module's docs. Parameters: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) src = root / "src" public_api = src / "griffe" print(f"{'#' * heading_level} `griffe`\n") print(_comment_block(public_api / "__init__.py")) def render_entrypoint(heading_level: int = 4) -> None: """Render Griffe's main entrypoint's docs. Parameters: heading_level: The initial level of Markdown headings. """ root = Path(os.environ["MKDOCS_CONFIG_DIR"]) src = root / "src" public_api = src / "griffe" print(f"{'#' * heading_level} `griffe.__main__`\n") print(_comment_block(public_api / "__main__.py")) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/make��������������������������������������������������������������������0000777�0001750�0001750�00000000000�14767006246�020010� 2make.py���������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/get_version.py����������������������������������������������������������0000664�0001750�0001750�00000002006�14767006246�020562� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Get current project version from Git tags or changelog. import re from contextlib import suppress from pathlib import Path from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm _root = Path(__file__).parent.parent _changelog = _root / "CHANGELOG.md" _changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") _default_scm_version = SCMVersion(Version("0.0.0"), None, False, None, None) # noqa: FBT003 def get_version() -> str: scm_version = get_version_from_scm(_root) or _default_scm_version if scm_version.version <= Version("0.1"): # Missing Git tags? with suppress(OSError, StopIteration): # noqa: SIM117 with _changelog.open("r", encoding="utf8") as file: match = next(filter(None, map(_changelog_version_re.match, file))) scm_version = scm_version._replace(version=Version(match.group(1))) return default_version_formatter(scm_version) if __name__ == "__main__": print(get_version()) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/scripts/make.py�����������������������������������������������������������������0000775�0001750�0001750�00000024715�14767006246�017171� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python3 from __future__ import annotations import os import shutil import subprocess import sys from contextlib import contextmanager from pathlib import Path from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from collections.abc import Iterator PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() _commands = [] # ----------------------------------------------------------------------------- # Helper functions ------------------------------------------------------------ # ----------------------------------------------------------------------------- def _shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: if capture_output: return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 return None @contextmanager def _environ(**kwargs: str) -> Iterator[None]: original = dict(os.environ) os.environ.update(kwargs) try: yield finally: os.environ.clear() os.environ.update(original) def _uv_install(venv: Path) -> None: with _environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): if "CI" in os.environ: _shell("uv sync --no-editable") else: _shell("uv sync") def _run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: kwargs = {"check": True, **kwargs} uv_run = ["uv", "run", "--no-sync"] if version == "default": with _environ(UV_PROJECT_ENVIRONMENT=".venv"): subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 else: with _environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"): subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 def _command(name: str) -> Callable[[Callable[..., None]], Callable[..., None]]: def wrapper(func: Callable[..., None]) -> Callable[..., None]: func.__cmdname__ = name # type: ignore[attr-defined] _commands.append(func) return func return wrapper # ----------------------------------------------------------------------------- # Commands -------------------------------------------------------------------- # ----------------------------------------------------------------------------- @_command("help") def help(*args: str) -> None: """Print this help. Add task name to print help. ```bash make help [TASK] ``` When the Python dependencies are not installed, this command just print the available commands. When the Python dependencies are installed, [duty](https://github.com/pawamoy/duty) is available so the command can also print the available tasks. If you add a task name after the command, it will print help for this specific task. """ if len(args) > 1: _run("default", "duty", "--help", args[1]) else: print("Available commands", flush=True) for cmd in _commands: print(f" {cmd.__cmdname__:21} {cmd.__doc__.splitlines()[0]}", flush=True) # type: ignore[attr-defined,union-attr] if Path(".venv").exists(): print("\nAvailable tasks", flush=True) run("duty", "--list") @_command("setup") def setup() -> None: """Setup all virtual environments (install dependencies). ```bash make setup ``` The `setup` command installs all the Python dependencies required to work on the project. Virtual environments and dependencies are managed by [uv](https://github.com/astral-sh/uv). Development dependencies are listed in the `devdeps.txt` file. The command will create a virtual environment in the `.venv` folder, as well as one virtual environment per supported Python version in the `.venvs/3.x` folders. Supported Python versions are listed in the `scripts/make` file, and can be overridden by setting the `PYTHON_VERSIONS` environment variable. If you cloned the repository on the same file-system as uv's cache, everything will be hard linked from the cache, so don't worry about wasting disk space. Once dependencies are installed, try running `make` or `make help` again, to show additional tasks. ```console exec="1" source="console" $ alias make="$PWD/scripts/make" # markdown-exec: hide $ make ``` These tasks are written using [duty](https://github.com/pawamoy/duty) (a task runner), and located in the `duties.py` module in the repository root. Some of these tasks will run in the default virtual environment (`.venv`), while others will run in all the supported Python version environments (`.venvs/3.x`). """ if not shutil.which("uv"): raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") print("Installing dependencies (default environment)") default_venv = Path(".venv") if not default_venv.exists(): _shell("uv venv --python python") _uv_install(default_venv) if PYTHON_VERSIONS: for version in PYTHON_VERSIONS: print(f"\nInstalling dependencies (python{version})") venv_path = Path(f".venvs/{version}") if not venv_path.exists(): _shell(f"uv venv --python {version} {venv_path}") with _environ(VIRTUAL_ENV=str(venv_path.resolve())): _uv_install(venv_path) @_command("run") def run(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in the default virtual environment. ```bash make run <CMD> [ARG...] ``` This command runs an arbitrary command inside the default virtual environment (`.venv`). It is especially useful to start a Python interpreter without having to first activate the virtual environment: `make run python`. """ _run("default", cmd, *args, **kwargs) @_command("multirun") def multirun(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command for all configured Python versions. ```bash make multirun <CMD> [ARG...] ``` This command runs an arbitrary command inside the environments for all supported Python versions. It is especially useful for running tests. """ if PYTHON_VERSIONS: for version in PYTHON_VERSIONS: run3x(version, cmd, *args, **kwargs) else: run(cmd, *args, **kwargs) @_command("allrun") def allrun(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in all virtual environments. ```bash make multirun <CMD> [ARG...] ``` This command runs an arbitrary command inside the default environment, as well as the environments for all supported Python versions. This command is especially useful to install, remove or update dependencies in all environments at once. For example, if you want to install a dependency in editable mode, from a local source: ```bash make allrun uv pip install -e ../other-project ``` """ run(cmd, *args, **kwargs) if PYTHON_VERSIONS: multirun(cmd, *args, **kwargs) @_command("3.x") def run3x(version: str, cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in the virtual environment for Python 3.x. ```bash make 3.x <CMD> [ARG...] ``` This command runs an arbitrary command inside the environment of the selected Python version. It can be useful if you want to run a task that usually runs in the default environment with a different Python version. """ _run(version, cmd, *args, **kwargs) @_command("clean") def clean() -> None: """Delete build artifacts and cache files. ```bash make clean ``` This command simply deletes build artifacts and cache files and folders such as `build/`, `.cache/`, etc.. The virtual environments (`.venv` and `.venvs/*`) are not removed by this command. """ paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] for path in paths_to_clean: _shell(f"rm -rf {path}") cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] for dirpath in Path().rglob("*/"): if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs: shutil.rmtree(dirpath, ignore_errors=True) @_command("vscode") def vscode() -> None: """Configure VSCode to work on this project. ```bash make vscode ``` This command configures the [VSCode editor](https://code.visualstudio.com/) by copying the following files into the `.vscode` directory: - `launch.json`, for run configurations (to run debug sessions) - `settings.json`, for various editor settings like linting tools and their configuration - `tasks.json`, for running tasks directly from VSCode's interface Warning: These files will be overwritten every time the command is run. """ Path(".vscode").mkdir(parents=True, exist_ok=True) _shell("cp -v config/vscode/* .vscode") # ----------------------------------------------------------------------------- # Main ------------------------------------------------------------------------ # ----------------------------------------------------------------------------- def main(args: list[str]) -> int: """Main entry point.""" if not args or args[0] == "help": help(*args) return 0 while args: cmd = args.pop(0) if cmd == "run": run(*args) return 0 if cmd == "multirun": multirun(*args) return 0 if cmd == "allrun": allrun(*args) return 0 if cmd.startswith("3."): run3x(cmd, *args) return 0 opts = [] while args and (args[0].startswith("-") or "=" in args[0]): opts.append(args.pop(0)) if cmd == "clean": clean() elif cmd == "setup": setup() elif cmd == "vscode": vscode() elif cmd == "check": multirun("duty", "check-quality", "check-types", "check-docs") run("duty", "check-api") elif cmd in {"check-quality", "check-docs", "check-types", "test"}: multirun("duty", cmd, *opts) else: run("duty", cmd, *opts) return 0 if __name__ == "__main__": try: sys.exit(main(sys.argv[1:])) except subprocess.CalledProcessError as process: if process.output: print(process.output, file=sys.stderr) sys.exit(process.returncode) ���������������������������������������������������python-griffe-1.6.2/scripts/gen_credits.py����������������������������������������������������������0000664�0001750�0001750�00000014744�14767006246�020540� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Script to generate the project's credits. from __future__ import annotations import os import sys from collections import defaultdict from collections.abc import Iterable from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent from typing import Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment from packaging.requirements import Requirement # YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", ".")) with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] devdeps = [dep for group in pyproject["dependency-groups"].values() for dep in group if not dep.startswith("-e")] PackageMetadata = dict[str, Union[str, Iterable[str]]] Metadata = dict[str, PackageMetadata] def _merge_fields(metadata: dict) -> PackageMetadata: fields = defaultdict(list) for header, value in metadata.items(): fields[header.lower()].append(value.strip()) return { field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0] for field, value in fields.items() } def _norm_name(name: str) -> str: return name.replace("_", "-").replace(".", "-").lower() def _requirements(deps: list[str]) -> dict[str, Requirement]: return {_norm_name((req := Requirement(dep)).name): req for dep in deps} def _extra_marker(req: Requirement) -> str | None: if not req.marker: return None try: return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra") except StopIteration: return None def _get_metadata() -> Metadata: metadata = {} for pkg in distributions(): name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore] metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type] metadata[name]["spec"] = set() metadata[name]["extras"] = set() metadata[name].setdefault("summary", "") _set_license(metadata[name]) return metadata def _set_license(metadata: PackageMetadata) -> None: license_field = metadata.get("license-expression", metadata.get("license", "")) license_name = license_field if isinstance(license_field, str) else " + ".join(license_field) check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n") if check_classifiers: license_names = [ classifier.rsplit("::", 1)[1].strip() for classifier in metadata["classifier"] if classifier.startswith("License ::") ] license_name = " + ".join(license_names) metadata["license"] = license_name or "?" def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} for dep_name, dep_req in base_deps.items(): if dep_name not in metadata or dep_name == "griffe": continue metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator] metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator] deps[dep_name] = metadata[dep_name] again = True while again: again = False for pkg_name in metadata: if pkg_name in deps: for pkg_dependency in metadata[pkg_name].get("requires-dist", []): requirement = Requirement(pkg_dependency) dep_name = _norm_name(requirement.name) extra_marker = _extra_marker(requirement) if ( dep_name in metadata and dep_name not in deps and dep_name != project["name"] and (not extra_marker or extra_marker in deps[pkg_name]["extras"]) ): metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator] deps[dep_name] = metadata[dep_name] again = True return deps def _render_credits() -> str: metadata = _get_metadata() dev_dependencies = _get_deps(_requirements(devdeps), metadata) prod_dependencies = _get_deps( _requirements( chain( # type: ignore[arg-type] project.get("dependencies", []), chain(*project.get("optional-dependencies", {}).values()), ), ), metadata, ) template_data = { "project_name": project_name, "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( """ # Credits These projects were used to build *{{ project_name }}*. **Thank you!** [Python](https://www.python.org/) | [uv](https://github.com/astral-sh/uv) | [copier-uv](https://github.com/pawamoy/copier-uv) {% macro dep_line(dep) -%} [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} {% if prod_dependencies -%} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in prod_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% endif -%} {% if dev_dependencies -%} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in dev_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% endif -%} {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} """, ) jinja_env = SandboxedEnvironment(undefined=StrictUndefined) return jinja_env.from_string(template_text).render(**template_data) print(_render_credits()) ����������������������������python-griffe-1.6.2/config/�������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015444� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/git-changelog.toml�������������������������������������������������������0000664�0001750�0001750�00000000350�14767006246�021047� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "perf", "refactor"] template = "keepachangelog" versioning = "pep440" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/vscode/������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�016727� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/vscode/tasks.json��������������������������������������������������������0000664�0001750�0001750�00000004605�14767006246�020754� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "version": "2.0.0", "tasks": [ { "label": "changelog", "type": "process", "command": "scripts/make", "args": ["changelog"] }, { "label": "check", "type": "process", "command": "scripts/make", "args": ["check"] }, { "label": "check-quality", "type": "process", "command": "scripts/make", "args": ["check-quality"] }, { "label": "check-types", "type": "process", "command": "scripts/make", "args": ["check-types"] }, { "label": "check-docs", "type": "process", "command": "scripts/make", "args": ["check-docs"] }, { "label": "check-api", "type": "process", "command": "scripts/make", "args": ["check-api"] }, { "label": "clean", "type": "process", "command": "scripts/make", "args": ["clean"] }, { "label": "docs", "type": "process", "command": "scripts/make", "args": ["docs"] }, { "label": "docs-deploy", "type": "process", "command": "scripts/make", "args": ["docs-deploy"] }, { "label": "format", "type": "process", "command": "scripts/make", "args": ["format"] }, { "label": "release", "type": "process", "command": "scripts/make", "args": ["release", "${input:version}"] }, { "label": "setup", "type": "process", "command": "scripts/make", "args": ["setup"] }, { "label": "test", "type": "process", "command": "scripts/make", "args": ["test", "coverage"], "group": "test" }, { "label": "vscode", "type": "process", "command": "scripts/make", "args": ["vscode"] } ], "inputs": [ { "id": "version", "type": "promptString", "description": "Version" } ] }���������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/vscode/launch.json�������������������������������������������������������0000664�0001750�0001750�00000002662�14767006246�021102� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false, "args": "${command:pickArgs}" }, { "name": "run", "type": "debugpy", "request": "launch", "module": "griffe", "console": "integratedTerminal", "justMyCode": false, "args": "${command:pickArgs}" }, { "name": "docs", "type": "debugpy", "request": "launch", "module": "mkdocs", "justMyCode": false, "args": [ "serve", "-v" ] }, { "name": "test", "type": "debugpy", "request": "launch", "module": "pytest", "justMyCode": false, "args": [ "-c=config/pytest.ini", "-vvv", "--no-cov", "--dist=no", "tests", "-k=${input:tests_selection}" ] } ], "inputs": [ { "id": "tests_selection", "type": "promptString", "description": "Tests selection", "default": "" } ] }������������������������������������������������������������������������������python-griffe-1.6.2/config/vscode/settings.json�����������������������������������������������������0000664�0001750�0001750�00000001704�14767006246�021464� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "files.watcherExclude": { "**/.venv*/**": true, "**/.venvs*/**": true, "**/venv*/**": true }, "mypy-type-checker.args": [ "--config-file=config/mypy.ini" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestArgs": [ "--config-file=config/pytest.ini" ], "ruff.enable": true, "ruff.format.args": [ "--config=config/ruff.toml" ], "ruff.lint.args": [ "--config=config/ruff.toml" ], "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, "yaml.customTags": [ "!ENV scalar", "!ENV sequence", "!relative scalar", "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" ] }������������������������������������������������������������python-griffe-1.6.2/config/coverage.ini�������������������������������������������������������������0000664�0001750�0001750�00000000610�14767006246�017735� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[coverage:run] branch = true parallel = true source = src/_griffe tests/ [coverage:paths] equivalent = src/ .venv/lib/*/site-packages/ .venvs/*/lib/*/site-packages/ [coverage:report] precision = 2 omit = src/*/__init__.py src/*/__main__.py tests/__init__.py tests/tmp/* exclude_lines = pragma: no cover if TYPE_CHECKING [coverage:json] output = htmlcov/coverage.json ������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/pytest.ini���������������������������������������������������������������0000664�0001750�0001750�00000000532�14767006246�017475� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[pytest] python_files = test_*.py addopts = --cov --cov-config config/coverage.ini testpaths = tests # action:message_regex:warning_class:module_regex:line filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist ignore:.*slated for removal in Python:DeprecationWarning:.* ����������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/mypy.ini�����������������������������������������������������������������0000664�0001750�0001750�00000000162�14767006246�017142� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[mypy] ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/config/ruff.toml����������������������������������������������������������������0000664�0001750�0001750�00000005732�14767006246�017312� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������target-version = "py39" line-length = 120 [lint] exclude = [ "tests/fixtures/*.py", ] select = ["ALL"] ignore = [ "A001", # Variable is shadowing a Python builtin "A005", # Module shadows a Python standard-library module "ANN101", # Missing type annotation for self "ANN102", # Missing type annotation for cls "ANN204", # Missing return type annotation for special method __str__ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ARG005", # Unused lambda argument "C901", # Too complex "D105", # Missing docstring in magic method "D417", # Missing argument description in the docstring "E501", # Line too long "EM", # Raw strings in exceptions "FIX001", # Line contains FIXME "FIX002", # Line contains TODO "ERA001", # Commented out code "PD", # Pandas-related "PERF203", # Try-except block in for loop (zero-cost with Python 3.11+) "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "SLF001", # Private member accessed "TD001", # Invalid TODO tag: FIXME "TD002", # Missing author in TODO "TD003", # Missing issue link on the line following this TODO "TRY003", # Avoid specifying long messages outside the exception class ] logger-objects = ["_griffe.logger.logger"] [lint.per-file-ignores] "src/griffe/__main__.py" = [ "D100", # Missing module docstring ] "src/_griffe/cli.py" = [ "T201", # Print statement ] "src/_griffe/git.py" = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path ] "src/_griffe/agents/nodes/*.py" = [ "ARG001", # Unused function argument "N812", # Lowercase `keyword` imported as non-lowercase `NodeKeyword` ] "src/_griffe/debug.py" = [ "T201", # Print statement ] "src/_griffe/**.py" = [ "D100", # Missing docstring in public module ] "scripts/*.py" = [ "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D103", # Missing docstring in public function "INP001", # File is part of an implicit namespace package "T201", # Print statement ] "tests/test_git.py" = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path ] "tests/**.py" = [ "ARG005", # Unused lambda argument "FBT001", # Boolean positional arg in function definition "PLC1901", # a == "" can be simplified to not a "PLR2004", # Magic value used in comparison "S101", # Use of assert detected ] [lint.flake8-quotes] docstring-quotes = "double" [lint.flake8-tidy-imports] ban-relative-imports = "all" [lint.isort] known-first-party = ["griffe"] [lint.pydocstyle] convention = "google" [format] exclude = [ "tests/fixtures/*.py", ] docstring-code-format = true docstring-code-line-length = 80 ��������������������������������������python-griffe-1.6.2/CONTRIBUTING.md�����������������������������������������������������������������0000664�0001750�0001750�00000001556�14767006246�016437� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Contributing Thank you for considering contributing to this project! We provide a guide for contributors. If you are reading this locally or directly on GitHub, check the following documents: - Contributors guide: [docs/guide/contributors.md](docs/guide/contributors.md) - Environment setup: [docs/guide/contributors/setup.md](docs/guide/contributors/setup.md) - Management commands: [docs/guide/contributors/commands.md](docs/guide/contributors/commands.md) - Development workflow: [docs/guide/contributors/workflow.md](docs/guide/contributors/workflow.md) - Project architecture: [docs/guide/contributors/architecture.md](docs/guide/contributors/architecture.md) We strongly recommend reading the online version at https://mkdocstrings.github.io/griffe/guide/contributors/, as some content is dynamically generated when building the documentation pages. ��������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/���������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015127� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/changelog.md���������������������������������������������������������������0000664�0001750�0001750�00000000103�14767006246�017372� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Changelog hide: - navigation --- --8<-- "CHANGELOG.md" �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/community.md���������������������������������������������������������������0000664�0001750�0001750�00000003575�14767006246�017507� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Community Griffe is part of is the [mkdocstrings](https://mkdocstrings.github.io/) ecosystem, and therefore part of the [MkDocs](https://www.mkdocs.org/) ecosystem too. These two ecosystems have wonderful communities and we invite you to join them :octicons-heart-fill-24:{ .pulse } Make sure to read and follow our [code of conduct](code-of-conduct.md) when engaging with the community. You can start new discussions on GitHub, in the following repositories, depending on the specificity of the discussion: [griffe](https://github.com/mkdocstrings/griffe), [mkdocstrings-python](https://github.com/mkdocstrings/python), [mkdocstrings](https://github.com/mkdocstrings/mkdocstrings), and [MkDocs](https://github.com/mkdocs/mkdocs). You can also join our dedicated Gitter channels: [Griffe channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ target="_blank" }, [mkdocstrings-python channel](https://app.gitter.im/#/room/#mkdocstrings_python:gitter.im){ target="_blank" }, [mkdocstrings channel](https://app.gitter.im/#/room/#mkdocstrings_community:gitter.im){ target="_blank" }, and [MkDocs channel](https://app.gitter.im/#/room/#mkdocs_community:gitter.im){ target="_blank" }. The best place to share about Griffe is of course our Gitter channel. [:simple-gitter: Join Griffe's Gitter channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ .md-button target="_blank" } More generally, Griffe is also related to **API documentation** and **API analysis** (static or dynamic): if your project is related to these two domains, but in different ecosystems (other programming languages, static site generators, or environments), feel free to drop us a message! We are always happy to share with other actors in these domains :material-handshake: - [Getting help](getting-help.md) - [Contributing](contributing.md) - [Code of conduct](code-of-conduct.md) - [Credits](credits.md) �����������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/getting-help.md������������������������������������������������������������0000664�0001750�0001750�00000002137�14767006246�020043� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Getting help If you have a quick question regarding Griffe, ask on our [Gitter channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ target="_blank" }. For more complex questions, or actual issues that require showing code and configuration, please create [new Q/A discussions](https://github.com/mkdocstrings/griffe/discussions/categories/q-a) or [new bug reports](https://github.com/mkdocstrings/griffe/issues) respectively. Make sure to search previous discussions and issues to avoid creating duplicates. Also make sure to read our documentation before asking questions or opening bug reports. Don't hesitate to report unclear or missing documentation, we will do our best to improve it. In any case (quick or complex questions) please remember to be **kind**, and to follow our [code of conduct](code-of-conduct.md). The people helping you do so voluntarily, in their free time. Be respectful of their time, and of your own. Help them help you by providing all the necessary information in minimal, reproducible examples. When creating a bug report, make sure to fill out the issue template. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/downstream-projects.md�����������������������������������������������������0000664�0001750�0001750�00000005166�14767006246�021473� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Downstream projects Griffe is used by various projects in the Python ecosystem. ## griffe2md [griffe2md](https://mkdocstrings.github.io/griffe2md/) outputs API docs in Markdown. It uses Griffe to load the data, and then use Jinja templates to render documentation in Markdown, just like [mkdocstrings-python](https://mkdocstrings.github.io/python/), but in Markdown instead of HTML. ## Griffe TUI [Griffe TUI](https://mkdocstrings.github.io/griffe-tui/) is a textual user interface for Griffe. It offers 100% offline, beautiful Python API docs, in your terminal, thanks to Griffe and [Textual](https://textual.textualize.io/). ## mkdocstrings-python Of course, Griffe is what powers [the Python handler of mkdocstrings](https://mkdocstrings.github.io/python/). mkdocstrings is a plugin for [MkDocs](https://www.mkdocs.org/) that allows rendering API docs easily. ## OpenAI Agents SDK The [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) is a lightweight yet powerful framework for building multi-agent workflows. It was inspired by Pydantic AI and uses Griffe the same way, to parse docstrings in order to generate function schemas. ## pydanclick [Pydanclick](https://pypi.org/project/pydanclick/) allows to use [Pydantic](https://docs.pydantic.dev/latest/) models as [Click](https://click.palletsprojects.com/en/8.1.x/) options. It uses Griffe to parse docstrings and find Attributes sections, to help itself build Click options. ## PydanticAI [PydanticAI](https://ai.pydantic.dev/) is a Python Agent Framework designed to make it less painful to build production grade applications with Generative AI. It uses Griffe to extract tool and parameter descriptions from docstrings. ## quartodoc [quartodoc](https://machow.github.io/quartodoc/) lets you quickly generate Python package API reference documentation using Markdown and [Quarto](https://quarto.org/). quartodoc is designed as an alternative to [Sphinx](https://www.sphinx-doc.org/en/master/). It uses Griffe to load API data and parse docstrings in order to render HTML documentation, just like [mkdocstrings-python](https://mkdocstrings.github.io/python/), but for Quarto instead of Mkdocs. ## rafe [rafe](https://pypi.org/project/rafe/) is a tool for inspecting Python environments and building packages (irrespective of language) in a reproducible manner. It wraps Griffe to provide a CLI command to check for API breaking changes. ## Yapper [Yapper](https://pypi.org/project/yapper/) converts Python docstrings to `astro` files for use by the [Astro](https://astro.build/) static site generator. It uses Griffe to parse Python modules and extracts Numpydoc-style docstrings. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide.md�������������������������������������������������������������������0000664�0001750�0001750�00000001500�14767006246�016542� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Guide Welcome to the Griffe guide! Here, you’ll find a series of topics on using Griffe, along with an explanation of the project’s inner workings. Although the more detailed explanations are primarily intended for contributors, users are encouraged to read them as well, since this can deepen their understanding of Griffe and help them make the most of it. <div class="grid cards" markdown> - :fontawesome-solid-user:{ .lg .middle } **User guide** --- A collection of topics for Griffe users. [:octicons-arrow-right-24: Browse the user guide](guide/users.md) - :fontawesome-solid-helmet-safety:{ .lg .middle } **Contributor guide** --- Explanation of the project and more, for Griffe contributors. [:octicons-arrow-right-24: Browse the contributor guide](guide/contributors.md) </div> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/credits.md�����������������������������������������������������������������0000664�0001750�0001750�00000000135�14767006246�017105� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Credits hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/license.md�����������������������������������������������������������������0000664�0001750�0001750�00000000115�14767006246�017070� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: License hide: - feedback --- # License ``` --8<-- "LICENSE" ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/schema-docstrings-options.json���������������������������������������������0000664�0001750�0001750�00000002070�14767006246�023127� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "$schema": "https://json-schema.org/draft-07/schema", "title": "Griffe docstrings parsing options.", "type": "object", "properties": { "ignore_init_summary": { "title": "Whether to discard the summary line in `__init__` methods' docstrings.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/docstrings/google/#griffe.docstrings.google.parse", "type": "boolean", "default": false }, "trim_doctest_flags": { "title": "Whether to remove doctest flags from Python example blocks.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/docstrings/google/#griffe.docstrings.google.parse", "type": "boolean", "default": true }, "returns_multiple_items": { "title": "Whether the `Returns` section has multiple items.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/docstrings/google/#griffe.docstrings.google.parse", "type": "boolean", "default": true } }, "additionalProperties": false }������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/code-of-conduct.md���������������������������������������������������������0000664�0001750�0001750�00000000034�14767006246�020417� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--8<-- "CODE_OF_CONDUCT.md" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/css/�����������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015717� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/css/custom.css�������������������������������������������������������������0000664�0001750�0001750�00000000237�14767006246�017745� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Prevent selection of prompts in pycon code blocks. */ .language-pycon .gp, .language-pycon .go { /* Generic.Prompt, Generic.Output */ user-select: none; }�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/css/mkdocstrings.css�������������������������������������������������������0000664�0001750�0001750�00000005420�14767006246�021141� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Indentation. */ div.doc-contents:not(.first) { padding-left: 25px; border-left: .05rem solid var(--md-typeset-table-color); } /* Mark external links as such. */ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>'); -webkit-mask-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.25 15.5a.75.75 0 00.75-.75v-9a.75.75 0 00-.75-.75h-9a.75.75 0 000 1.5h7.19L6.22 16.72a.75.75 0 101.06 1.06L17.5 7.56v7.19c0 .414.336.75.75.75z"></path></svg>'); content: ' '; display: inline-block; vertical-align: middle; position: relative; height: 1em; width: 1em; background-color: currentColor; } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } /* Light blue color for `param` symbols`. */ [data-md-color-scheme="default"] { --doc-symbol-parameter-fg-color: #829bd1; --doc-symbol-parameter-bg-color: #829bd11a; } [data-md-color-scheme="slate"] { --doc-symbol-parameter-fg-color: #829bd1; --doc-symbol-parameter-bg-color: #829bd11a; } /* Hide parameters in ToC. */ li.md-nav__item:has(> a[href*="("]) { display: none; } /* Tree-like output for backlinks. */ .doc-backlink-list { --tree-clr: var(--md-default-fg-color); --tree-font-size: 1rem; --tree-item-height: 1; --tree-offset: 1rem; --tree-thickness: 1px; --tree-style: solid; display: grid; list-style: none !important; } .doc-backlink-list li > span:first-child { text-indent: .3rem; } .doc-backlink-list li { padding-inline-start: var(--tree-offset); border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); position: relative; margin-left: 0 !important; &:last-child { border-color: transparent; } &::before{ content: ''; position: absolute; top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness)); left: calc(var(--tree-thickness) * -1); width: calc(var(--tree-offset) + var(--tree-thickness) * 2); height: calc(var(--tree-item-height) * var(--tree-font-size)); border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr); } &::after{ content: ''; position: absolute; border-radius: 50%; background-color: var(--tree-clr); top: calc(var(--tree-item-height) / 2 * 1rem); left: var(--tree-offset) ; translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/css/insiders.css�����������������������������������������������������������0000664�0001750�0001750�00000003731�14767006246�020255� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@keyframes heart { 0%, 40%, 80%, 100% { transform: scale(1); } 20%, 60% { transform: scale(1.15); } } @keyframes vibrate { 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% { -webkit-transform: translate3d(-2px, 0, 0); transform: translate3d(-2px, 0, 0); } 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% { -webkit-transform: translate3d(2px, 0, 0); transform: translate3d(2px, 0, 0); } 20%, 100% { -webkit-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); } } .heart { color: #e91e63; } .pulse { animation: heart 1000ms infinite; } .vibrate { animation: vibrate 2000ms infinite; } .new-feature svg { fill: var(--md-accent-fg-color) !important; } a.insiders { color: #e91e63; } .sponsorship-list { width: 100%; } .sponsorship-item { border-radius: 100%; display: inline-block; height: 1.6rem; margin: 0.1rem; overflow: hidden; width: 1.6rem; } .sponsorship-item:focus, .sponsorship-item:hover { transform: scale(1.1); } .sponsorship-item img { filter: grayscale(100%) opacity(75%); height: auto; width: 100%; } .sponsorship-item:focus img, .sponsorship-item:hover img { filter: grayscale(0); } .sponsorship-item.private { background: var(--md-default-fg-color--lightest); color: var(--md-default-fg-color); font-size: .6rem; font-weight: 700; line-height: 1.6rem; text-align: center; } .mastodon { color: #897ff8; border-radius: 100%; box-shadow: inset 0 0 0 .05rem currentcolor; display: inline-block; height: 1.2rem !important; padding: .25rem; transition: all .25s; vertical-align: bottom !important; width: 1.2rem; } .premium-sponsors { text-align: center; } #silver-sponsors img { height: 140px; } #bronze-sponsors img { height: 140px; } #bronze-sponsors p { display: flex; flex-wrap: wrap; justify-content: center; } #bronze-sponsors a { display: block; flex-shrink: 0; } .sponsors-total { font-weight: bold; }���������������������������������������python-griffe-1.6.2/docs/css/material.css�����������������������������������������������������������0000664�0001750�0001750�00000015767�14767006246�020247� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* More space at the bottom of the page. */ .md-main__inner { margin-bottom: 1.5rem; } /* Invert logo colors in navbar. */ .md-logo img { filter: invert(100%); } /* Add space between Material buttons on mobile. */ .md-button { margin-bottom: 0.2rem; } /* Add space above bold ToC entries. */ .md-sidebar--secondary .md-nav__item:not(:first-child):has(strong) { margin-top: 1rem; } /* Griffe admonitions. */ :root { --md-admonition-icon--griffe: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Copyright François Rozet 2022 --><path d="M 16.353555,0.02028172 C 6.0845792,0.53792196 -3.5255624,10.91814 1.2695074,20.365706 14.431688,46.305932 16.211565,67.483102 28.239923,79.141311 c 17.000421,16.474673 32.343015,12.942792 49.700142,39.295259 16.528952,25.09049 13.749525,47.45141 25.558475,72.47241 1.23303,2.61266 5.4807,9.67734 -10.834959,25.22786 -0.11279,0.10749 -0.181871,0.23544 -0.290586,0.34534 1.366563,-0.97244 2.865238,-2.16253 4.182783,-3.07171 15.455442,-10.66525 34.227902,-23.66232 41.716832,-28.88307 21.73425,-15.15067 51.49749,-31.65817 76.66698,-42.70499 -0.0198,0.006 -0.0346,-6.1e-4 -0.0543,0.006 -16.34949,5.52402 -16.58735,-2.05825 -14.36058,-13.16142 6.20633,-30.946611 18.12289,-43.704229 31.56677,-48.759275 10.22168,-3.840752 18.99735,-0.849552 25.29684,5.777574 8.19073,8.616619 6.9156,29.044281 -26.62556,49.138301 6.42826,-3.04004 13.75526,-6.83521 19.21133,-10.24973 7.39318,-4.62704 21.18623,-10.17731 30.98201,-12.46717 9.71163,-2.27015 28.76227,-9.04167 42.33418,-15.047915 19.96147,-8.833889 37.95922,-14.273638 62.42215,-17.421801 -0.0179,0 -0.0352,-0.0117 -0.0531,-0.0099 -55.87264,6.172011 -65.2272,-2.800912 -72.80731,-15.294565 -0.95381,-1.57208 -1.97867,-3.132391 -3.07712,-4.677918 -20.01357,-28.158447 -57.10949,-45.871256 -91.50019,-32.947374 -25.85067,9.717875 -51.82802,39.030059 -60.16253,92.696353 -1.45725,9.62571 -2.2737,22.02605 -4.80013,30.86948 -5.94816,20.82043 -15.02806,17.61739 -21.12011,7.00844 -7.12513,-12.41135 -18.44996,-54.97033 -23.2567,-70.78076 C 102.27144,64.584981 96.758639,48.981053 87.955063,33.648479 79.214334,18.426172 39.706987,5.1302774 20.755683,0.43406126 19.301889,0.07399851 17.820437,-0.05382529 16.353494,0.02015851 Z M 390.70187,105.7023 c -52.7833,15.12024 -97.37611,30.94691 -119.42441,43.17426 -7.87805,4.36905 -25.33321,11.5294 -38.78801,15.91416 -1.58517,0.51659 -3.47357,1.29271 -5.17875,1.90947 23.43739,-4.22361 15.43952,10.37406 4.36002,28.78919 -8.96131,14.89452 -18.32722,30.42718 -26.48691,45.94752 -6.23182,11.85349 -14.1416,20.30778 -51.42081,41.30212 -0.013,0.007 -0.0463,0.097 -0.0605,0.10707 3.10668,-1.46896 6.06545,-2.90403 9.51945,-4.45166 46.45678,-20.81649 81.78955,-40.67798 95.08255,-53.44564 11.2263,-10.78272 47.73629,-26.16088 89.84829,-37.84546 24.24587,-6.72747 57.37854,-17.24106 73.62727,-23.36415 43.62475,-16.43938 53.2912,-19.17912 76.3788,-21.48001 -21.09111,0.47804 -50.20989,4.17334 -69.99189,-3.1295 -20.1205,-7.83366 -42.9282,-7.9005 -64.18493,3.4579 -20.46788,10.93611 -34.51701,24.61924 -44.36113,40.18917 -2.21825,3.50852 -8.45125,13.15457 -16.84997,16.4629 -8.39866,3.30838 -13.50968,3.96629 -9.5694,-3.60956 4.5882,-8.82163 9.21165,-17.96759 13.41888,-27.28887 7.23069,-16.01987 11.64159,-36.60911 84.08121,-62.63897 z M 512,159.01919 c -22.27702,9.29035 -44.35643,19.12299 -58.8748,26.51164 -4.57758,2.32966 -10.05266,4.8111 -15.29747,7.25509 6.939,6.75095 11.99144,15.78288 12.91012,26.84368 0.20298,2.44597 0.27774,4.83759 0.23391,7.17927 -0.38893,20.7911 -29.4382,48.51316 -63.42417,71.90813 8.62399,-3.76844 17.3696,-7.90445 26.20719,-12.43347 19.84269,-10.16886 36.35498,-16.34617 60.06005,-22.47266 9.47056,-2.44757 20.92589,-5.17788 32.58928,-7.87345 -0.0849,0.013 -0.10644,-3.1e-4 -0.19293,0.0136 -26.49956,4.1426 -19.63773,-20.25758 -19.43684,-30.3293 0.0574,-2.88469 -0.0327,-5.83252 -0.28337,-8.85046 -0.6158,-7.41963 -2.18374,-14.61621 -4.56134,-21.46682 -3.4082,-9.81989 -5.56842,-16.84767 30.07037,-36.2849 z M 211.20296,173.2141 c -11.53781,5.09262 -24.17938,11.36024 -36.93121,18.53106 -18.56521,10.43979 -44.30476,23.74658 -57.19876,29.57011 -5.48465,2.47714 -10.82465,4.96702 -16.0631,7.41989 29.65489,-9.94198 34.31056,-5.83826 40.0348,-5.3577 8.88735,0.74606 19.12225,-1.79237 30.85175,-8.82637 3.16774,-2.42188 5.91963,-5.47222 8.32334,-8.98401 6.81942,-9.963 12.06238,-21.3161 30.98318,-32.35298 z m 136.58115,55.053 c -1.71654,0.56754 -3.5195,1.24774 -5.2161,1.79514 -12.22826,3.94529 -23.15175,9.34456 -29.20108,14.43429 -5.31593,4.47249 -23.17527,14.57531 -39.68755,22.45099 -0.11594,0.0555 -0.22601,0.1081 -0.34245,0.16367 24.03672,-8.64834 20.37158,21.25121 19.03774,34.34543 -0.19397,1.90436 -0.39375,3.80847 -0.6005,5.71258 -1.13135,10.41841 -11.18395,25.5283 -20.00234,33.96763 12.14306,-6.91487 22.31246,-12.97065 22.95648,-14.12272 0.5019,-0.89766 10.95886,-3.54204 23.23737,-5.8762 12.90475,-2.45323 26.55871,-6.2338 40.69438,-11.12566 -35.08608,6.36988 -29.62044,-9.7881 -27.009,-25.58761 3.26769,-19.77038 7.79576,-39.03457 16.13305,-56.15754 z m -79.43541,41.23713 c -15.58163,7.44292 -34.24495,16.38615 -43.41819,20.82556 -33.75724,16.33649 -58.68873,27.14546 -84.31994,36.59173 0.0216,0.006 0.0198,0.07 0.0445,0.0638 25.26277,-6.39692 37.93496,-3.25682 39.86603,16.91038 1.57942,16.49445 9.13441,33.66979 -17.9212,48.93256 -0.0346,0.0198 -0.0426,0.0733 -0.0748,0.0963 3.53358,-1.55766 7.14698,-3.1258 11.49207,-4.87283 15.69695,-6.31098 36.70835,-15.20013 46.6918,-19.75466 7.2594,-3.31165 19.60711,-9.8322 32.08527,-16.61685 -13.89028,4.34829 -14.84662,-6.5564 -15.94253,-15.72054 -0.0804,-0.67201 -0.15113,-1.3505 -0.21225,-2.03576 -0.80687,-9.0745 0.13057,-18.10151 2.27049,-27.05903 3.36572,-14.08884 9.35128,-24.82037 29.43857,-37.3606 z m 210.37356,15.05278 c -26.60773,15.7001 -52.05971,32.55239 -82.38227,42.30069 -11.35131,3.98928 -21.38526,7.94855 -31.44495,12.26025 17.65586,-3.2296 10.19403,7.91855 2.55619,24.91264 -7.88737,17.54926 -13.23299,37.88267 -13.87349,62.40796 -3.38406,129.40649 145.03678,86.88022 148.4832,35.42346 -47.51955,52.36372 -125.16428,47.81461 -123.19117,-27.61013 0.6847,-26.25636 8.21111,-47.84701 18.99915,-66.75379 12.52742,-21.95514 25.53508,-44.06495 80.85334,-82.94108 z m -129.1066,61.45862 c -9.38259,4.42831 -19.11953,9.26109 -30.52141,15.32469 -35.15355,18.69467 -38.28437,20.10234 -91.56773,41.16013 -19.28202,7.62033 -33.72464,13.45088 -47.83006,19.18318 28.28505,-9.83239 39.14354,-6.43838 48.67287,-1.74456 41.93093,20.65584 66.287,1.71019 80.51095,-28.95767 6.34814,-13.68695 16.37221,-29.63615 40.73538,-44.96577 z"/></svg>') } .md-typeset .admonition.griffe, .md-typeset details.griffe { border-color: rgb(166, 226, 55); } .md-typeset .griffe > .admonition-title, .md-typeset .griffe > summary { background-color: rgba(43, 155, 70, 0.1); } .md-typeset .griffe > .admonition-title::before, .md-typeset .griffe > summary::before { background-color: rgb(166, 226, 55); -webkit-mask-image: var(--md-admonition-icon--griffe); mask-image: var(--md-admonition-icon--griffe); } /* Don't uppercase H5 headings. */ .md-typeset h5 { text-transform: none; } ���������python-griffe-1.6.2/docs/guide/���������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�016224� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/contributors/��������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�020761� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/contributors/workflow.md���������������������������������������������0000664�0001750�0001750�00000015640�14767006246�023163� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Development workflow This document describes our workflow when developing features, fixing bugs and updating the documentation. It also includes guidelines for pull requests on GitHub. ## Features and bug fixes The development worklow is rather usual. **For a new feature:** 1. create a new branch: `git switch -c feat-summary` 2. edit the code and the documentation 3. write new tests **For a bug fix:** 1. create a new branch: `git switch -c fix-summary` 2. write tests that fail but are expected to pass once the bug is fixed 3. run [`make test`][task-test] to make sure the new tests fail 4. fix the code **For a docs update:** <div class="annotate" markdown> 1. create a new branch: `git switch -c docs-summary` 2. start the live reloading server: `make docs` (1) 3. update the documentation 4. preview changes at http://localhost:8000 </div> 1. To speed-up the live reloading, disable mkdocstrings with `MKDOCSTRINGS_ENABLED=false make docs`. **Before committing:** 1. run [`make format`][task-format] to auto-format the code 2. run [`make check`][task-check] to check everything (fix any warning) 3. run [`make test`][task-test] to run the tests (fix any issue) 4. if you updated the documentation or the project dependencies: 1. run [`make docs`][task-docs] 2. go to http://localhost:8000 and check that everything looks good Once you are ready to commit, follow our [commit message convention](#commit-message-convention). NOTE: **Occasional contributors** If you are unsure about how to fix or ignore a warning, just let the continuous integration fail, and we will help you during review. Don't bother updating the changelog, we will take care of this. ## Breaking changes and deprecations Breaking changes should generally be avoided. If we decide to add a breaking change anyway, we should first allow a deprecation period. To deprecate parts of the API, check [Griffe's hints on how to deprecate things](../users/checking.md). Use [`make check-api`][task-check-api] to check if there are any breaking changes. All of them should allow deprecation periods. Run this command again until no breaking changes are detected. Deprecated code should also be marked as legacy code. We use [Yore](https://pawamoy.github.io/yore/) to mark legacy code. Similarly, code branches made to support older version of Python should be marked as legacy code using Yore too. Examples: ```python title="Remove function when we bump to 2.0" # YORE: Bump 2: Remove block. def deprecated_function(): ... ``` ```python title="Simplify imports when Python 3.9 is EOL" # YORE: EOL 3.9: Replace block with line 4. try: import ... except ImportError: import ... ``` Check [Yore's docs](https://pawamoy.github.io/yore/), and Yore-comments in our own code base (`git grep -A1 YORE`) to learn how to use it. NOTE: **Occasional contributors** If you are unsure about how to deprecate something or mark legacy code, let us do it during review. ## Commit message convention Commit messages must follow our convention based on the [Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): ``` type(scope): Subject Body. ``` **Subject and body must be valid Markdown.** Subject must have proper casing (uppercase for first letter if it makes sense), but no dot at the end, and no punctuation in general. Example: ``` feat: Add CLI option to run in verbose mode ``` Scope and body are optional. Type can be: - `build`: About packaging, building wheels, etc. - `chore`: About packaging or repo/files management. - `ci`: About Continuous Integration. - `deps`: Dependencies update. - `docs`: About documentation. - `feat`: New feature. - `fix`: Bug fix. - `perf`: About performance. - `refactor`: Changes that are not features or bug fixes. - `style`: A change in code style/format. - `tests`: About tests. If you write a body, please add trailers at the end (for example issues and PR references, or co-authors), without relying on GitHub's flavored Markdown: ``` This is the body of the commit message. Issue-10: https://github.com/namespace/project/issues/10 Related-to-PR-namespace/other-project#15: https://github.com/namespace/other-project/pull/15 ``` These "trailers" must appear at the end of the body, without any blank lines between them. The trailer title can contain any character except colons `:`. We expect a full URI for each trailer, not just GitHub autolinks (for example, full GitHub URLs for commits and issues, not the hash or the #issue-number). We do not enforce a line length on commit messages summary and body. NOTE: **Occasional contributors** If this convention seems unclear to you, just write the message of your choice, and we will rewrite it ourselves before merging. ## Pull requests guidelines Link to any related issue in the Pull Request message. During the review, we recommend using fixups: ```bash # SHA is the SHA of the commit you want to fix git commit --fixup=SHA ``` Once all the changes are approved, you can squash your commits: ```bash git rebase -i --autosquash main ``` And force-push: ```bash git push -f ``` NOTE: **Occasional contributors** If this seems all too complicated, you can push or force-push each new commit, and we will squash them ourselves if needed, before merging. ## Release process Occasional or even regular contributors don't *have* to read this, but can anyway if they are interested in our release process. Once we are ready for a new release (a few bugfixes and/or features merged in the main branch), maintainers should update the changelog. If our [commit message convention](workflow.md#commit-message-convention) was properly followed, the changelog can be automatically updated from the messages in the Git history with [`make changelog`][task-changelog]. This task updates the changelog in place to add a new version entry. Once the changelog is updated, maintainers should review the new version entry, to: - (optionally) add general notes for this new version, like highlights - insert **Breaking changes** and **Deprecations** sections if needed, before other sections - add links to the relevant parts of the documentation - fix typos or markup if needed Once the changelog is ready, a new release can be made with [`make release`][task-release]. If the version wasn't passed on the command-line with `make release version=x.x.x`, the task will prompt you for it. **Use the same version as the one that was just added to the changelog.** For example if the new version added to the changelog is `7.8.9`, use `make release version=7.8.9`. The [release task][task-release] will stage the changelog, commit, tag, push, then build distributions and upload them to PyPI.org, and finally deploy the documentation. If any of these steps fail, you can manually run each step with Git commands, then [`make build`][task-build], [`make publish`][task-publish] and [`make docs-deploy`][task-docs-deploy]. ������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/contributors/architecture.md�����������������������������������������0000664�0001750�0001750�00000025626�14767006246�024000� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Project architecture This document describes how the project is architectured, both regarding boilerplate and actual code. We start by giving an overview of the project's contents: ```python exec="1" session="filetree" from fnmatch import fnmatch from pathlib import Path exclude = {"dist", "*cache*", ".devbox", ".hypothesis", ".pdm*", ".coverage*", "profile.*", ".gitpod*"} no_recurse = {".venv*", "site", "htmlcov", ".git", "fixtures"} descriptions = { ".github": "GitHub workflows, issue templates and other configuration.", ".venv": "The default virtual environment (git-ignored). See [`make setup`][command-setup] command.", ".venvs": "The virtual environments for all supported Python versions (git-ignored). See [`make setup`][command-setup] command.", ".vscode": "The configuration for VSCode (git-ignored). See [`make vscode`][command-vscode] command.", "docs": "Documentation sources (Markdown pages). See [`make docs`][task-docs] task.", "docs/.overrides": "Customization of [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)' templates.", "docs/reference/api": "Python API reference, injected with [mkdocstrings](https://mkdocstrings.github.io/).", "config": "Contains our tooling configuration. See [Scripts, configuration](#scripts-configuration).", "htmlcov": "HTML report for Python code coverage (git-ignored), integrated in the [Coverage report](../coverage/) page. See [`make coverage`][task-coverage] task.", "scripts": "Our different scripts. See [Scripts, configuration](#scripts-configuration).", "site": "Documentation site, built with `make run mkdocs build` (git-ignored).", "src": "The source of our Python package(s). See [Sources](#sources) and [Program structure](#program-structure).", "src/_griffe": "Our internal API, hidden from users. See [Program structure](#program-structure).", "src/griffe": "Our public API, exposed to users. See [Program structure](#program-structure).", "tests": "Our test suite. See [Tests](#tests).", ".copier-answers.yml": "The answers file generated by [Copier](https://copier.readthedocs.io/en/stable/). See [Boilerplate](#boilerplate).", "devdeps.txt": "Our development dependencies specification. See [`make setup`][command-setup] command.", "duties.py": "Our project tasks, written with [duty](https://pawamoy.github.io/duty). See [Tasks][tasks].", ".envrc": "The environment configuration, automatically sourced by [direnv](https://direnv.net/). See [commands](../commands/).", "Makefile": "A dummy makefile, only there for auto-completion. See [commands](../commands/).", "mkdocs.yml": "The build configuration for our docs. See [`make docs`][task-docs] task.", "pyproject.toml": "The project metadata and production dependencies.", } def exptree(path): files = [] dirs = [] for node in Path(path).iterdir(): if any(fnmatch(node.name, pattern) for pattern in exclude): continue if node.is_dir(): dirs.append(node) else: files.append(node) annotated = [] recurse = set() print("```tree") for directory in sorted(dirs): if not any(fnmatch(directory.name, pattern) for pattern in no_recurse): recurse.add(directory) annotated.append(directory) print(f"{directory.name}/ # ({len(annotated)})!") elif str(directory) in descriptions: annotated.append(directory) print(f"{directory.name}/ # ({len(annotated)})!") else: print(f"{directory.name}/") for file in sorted(files): if str(file) in descriptions: annotated.append(file) print(f"{file.name} # ({len(annotated)})!") else: print(file.name) print("```\n") for index, node in enumerate(annotated, 1): print(f"{index}. {descriptions.get(str(node), '')}\n") if node.is_dir() and node in recurse: print(' ```python exec="1" session="filetree" idprefix=""') print(f' exptree("{node}")') print(" ```\n") ``` ```python exec="1" session="filetree" idprefix="" exptree(".") ``` ## Boilerplate This project's skeleton (the file-tree shown above) is actually generated from a [Copier](https://copier.readthedocs.io/en/stable/) template called [copier-uv](https://pawamoy.github.io/copier-uv/). When generating the project, Copier asks a series of questions (configured by the template itself), and the answers are used to render the file and directory names, as well as the file contents. Copier also records answers in the `.copier-answers.yml` file, allowing to update the project with latest changes from the template while reusing previous answers. To update the project (in order to apply latest changes from the template), we use the following command: ```bash copier update --trust --skip-answered ``` ## Scripts, configuration We have a few scripts that let us manage the various maintenance aspects for this project. The entry-point is the `make` script located in the `scripts` folder. It doesn't need any dependency to be installed to run. See [Management commands](commands.md) for more information. The `make` script can also invoke what we call "[tasks][]". Tasks need our development dependencies to be installed to run. These tasks are written in the `duties.py` file, and the development dependencies are listed in `devdeps.txt`. The tools used in tasks have their configuration files stored in the `config` folder, to unclutter the root of the repository. The tasks take care of calling the tools with the right options to locate their respective configuration files. ## Sources Sources are located in the `src` folder, following the [src-layout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). We use [PDM-Backend](https://backend.pdm-project.org/) to build source and wheel distributions, and configure it in `pyproject.toml` to search for packages in the `src` folder. ## Tests Our test suite is located in the `tests` folder. It is located outside of the sources as to not pollute distributions (it would be very wrong to publish a `tests` package as part of our distributions, since this name is extremely common), or worse, the public API. The `tests` folder is however included in our source distributions (`.tar.gz`), alongside most of our metadata and configuration files. Check out `pyproject.toml` to get the full list of files included in our source distributions. The test suite is based on [pytest](https://docs.pytest.org/en/8.2.x/). Test modules reflect our internal API structure, and except for a few test modules that test specific aspects of our API, each test module tests the logic from the corresponding module in the internal API. For example, `test_finder.py` tests code of the `_griffe.finder` internal module, while `test_functions` tests our ability to extract correct information from function signatures, statically. The general rule of thumb when writing new tests is to mirror the internal API. If a test touches to many aspects of the loading process, it can be added to the `test_loader` test module. ## Program structure Griffe is composed of two packages: - `_griffe`, which is our internal API, hidden from users - `griffe`, which is our public API, exposed to users When installing the `griffe` distribution from PyPI.org (or any other index where it is published), both the `_griffe` and `griffe` packages are installed. Users then import `griffe` directly, or import objects from it. The top-level `griffe/__init__.py` module exposes all the public API, by importing the internal objects from various submodules of `_griffe`. We'll be honest: our code organization is not the most elegant, but it works :shrug: Have a look at the following module dependency graph, which will basically tell you nothing except that we have a lot of inter-module dependencies. Arrows read as "imports from". The code base is generally pleasant to work with though. ```python exec="true" html="true" from pydeps import cli, colors, dot, py2depgraph from pydeps.pydeps import depgraph_to_dotsrc from pydeps.target import Target cli.verbose = cli._not_verbose options = cli.parse_args(["src/griffe", "--noshow", "--reverse"]) colors.START_COLOR = 128 target = Target(options["fname"]) with target.chdir_work(): dep_graph = py2depgraph.py2dep(target, **options) dot_src = depgraph_to_dotsrc(target, dep_graph, **options) svg = dot.call_graphviz_dot(dot_src, "svg").decode() svg = "".join(svg.splitlines()[6:]) svg = svg.replace('fill="white"', 'fill="transparent"') print(f'<div class="interactiveSVG pydeps">{svg}</div>') ``` <small><i>You can zoom and pan all diagrams on this page with mouse inputs.</i></small> The following sections are generated automatically by iterating on the modules of our public and internal APIs respectively, and extracting the comment blocks at the top of each module. The comment blocks are addressed to readers of the code (maintainers, contributors), while module docstrings are addressed to users of the API. Module docstrings in our internal API are never written, because our [module layout][module-layout] is hidden, and therefore modules aren't part of the public API, so it doesn't make much sense to write "user documentation" in them. ```python exec="1" session="comment_blocks" --8<-- "scripts/gen_structure_docs.py" ``` ### CLI entrypoint ```python exec="1" idprefix="entrypoint-" session="comment_blocks" render_entrypoint(heading_level=4) ``` ### Public API ```python exec="1" idprefix="public-" session="comment_blocks" render_public_api(heading_level=4) ``` ### Internal API ```python exec="1" idprefix="internal-" session="comment_blocks" render_internal_api(heading_level=4) ``` <style> .interactiveSVG svg { min-height: 200px; } .graph > polygon { fill-opacity: 0.0; } /* pydeps dependency graph. */ [data-md-color-scheme="default"] .pydeps .edge > path, [data-md-color-scheme="default"] .pydeps .edge > polygon { stroke: black; } [data-md-color-scheme="slate"] .pydeps .edge > path, [data-md-color-scheme="slate"] .pydeps .edge > polygon { stroke: white; } /* Code2Flow call graphs. */ [data-md-color-scheme="default"] .code2flow .cluster > polygon { stroke: black; } [data-md-color-scheme="default"] .code2flow .cluster > text { fill: black; } [data-md-color-scheme="slate"] .code2flow .cluster > polygon { stroke: white; } [data-md-color-scheme="slate"] .code2flow .cluster > text { fill: white; } </style> <script> document.addEventListener("DOMContentLoaded", function(){ const divs = document.getElementsByClassName("interactiveSVG"); for (let i = 0; i < divs.length; i++) { if (!divs[i].firstElementChild.id) { divs[i].firstElementChild.id = `interactiveSVG-${i}` } svgPanZoom(`#${divs[i].firstElementChild.id}`, {}); } }); </script> ����������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/contributors/commands.md���������������������������������������������0000664�0001750�0001750�00000014736�14767006246�023117� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Management commands The entry-point to run commands to manage the project is our Python `make` script, located in the `scripts` folder. You can either call it directly with `./scripts/make`, or you can use [direnv](https://direnv.net/) to add the script to your command line path. Once direnv is installed and hooked into your shell, allow it once for this directory with `direnv allow`. Now you can directly call the Python script with `make`. The `Makefile` is just here to provide auto-completion. Try typing `make` or `make help` to show the available commands. ```console exec="1" source="console" $ alias make="$PWD/scripts/make"; cd # markdown-exec: hide $ make ``` ## Commands Commands are always available: they don't require any Python dependency to be installed. [](){#command-setup} ### `setup` ::: make.setup options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-help} ### `help` ::: make.help options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-run} ### `run` ::: make.run options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-multirun} ### `multirun` ::: make.multirun options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-allrun} ### `allrun` ::: make.allrun options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-3.x} ### `3.x` ::: make.run3x options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-clean} ### `clean` ::: make.clean options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#command-vscode} ### `vscode` ::: make.vscode options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false ## Tasks Tasks require the Python dependencies to be installed. They use various tools and libraries to assert code quality, run tests, serve the documentation locally, or build and publish distributions of your project. There are multiple ways to run tasks: - `make TASK`, the main, configured way to run a task - `make run duty TASK`, to run a task in the default environment - `make multirun duty TASK`, to run a task on all supported Python versions - `make allrun duty TASK`, to run a task in *all* environments - `make 3.x duty TASK`, to run a task on a specific Python version [](){#task-build} ### `build` ::: duties.build options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-changelog} ### `changelog` ::: duties.changelog options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-check} ### `check` ::: duties.check options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-check-api} ### `check-api` ::: duties.check_api options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-check-docs} ### `check-docs` ::: duties.check_docs options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-check-quality} ### `check-quality` ::: duties.check_quality options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-check-types} ### `check-types` ::: duties.check_types options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-coverage} ### `coverage` ::: duties.coverage options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-docs} ### `docs` ::: duties.docs options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-docs-deploy} ### `docs-deploy` ::: duties.docs_deploy options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-format} ### `format` ::: duties.format options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-fuzz} ### `fuzz` ::: duties.fuzz options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-publish} ### `publish` ::: duties.publish options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-release} ### `release` ::: duties.release options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false [](){#task-test} ### `test` ::: duties.test options: heading_level: 3 show_root_heading: false show_root_toc_entry: false separate_signature: false parameter_headings: false ����������������������������������python-griffe-1.6.2/docs/guide/contributors/setup.md������������������������������������������������0000664�0001750�0001750�00000006254�14767006246�022452� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Environment setup To work on the project, whether to update the code or the documentation, you will have to setup a development environment. ## Requirements The only requirement is that you have [Python](https://www.python.org/) and [uv](https://docs.astral.sh/uv/) installed and available on your command line path. === ":simple-astral: official installer" ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` <div class="result" markdown> See [Installation methods](https://docs.astral.sh/uv/getting-started/installation/). </div> === ":simple-python: pip" ```bash pip install --user uv ``` <div class="result" markdown> [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. </div> === ":simple-pipx: pipx" ```bash pipx install uv ``` <div class="result" markdown> [pipx](https://pipx.pypa.io/stable/) allows to install and run Python applications in isolated environments. </div> === ":simple-rye: rye" ```bash rye install uv ``` <div class="result" markdown> [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. </div> Optionally, we recommend using [direnv](https://direnv.net/), which will add our `scripts` folder to your path when working on the project, allowing to call our `make` Python script with the usual `make` command. ## Fork and clone [Fork the repository on GitHub](https://github.com/mkdocstrings/griffe/fork), then clone it locally: === "GitHub CLI" ```bash gh repo clone griffe ``` <div class="result"> The [`gh` GitHub CLI](https://cli.github.com/) allows you to interact with GitHub on the command line. </div> === "Git + SSH" ```bash git clone git@github.com:your-username/griffe ``` <div class="result"> See the documentation on GitHub for [Connecting with SSH](https://docs.github.com/en/authentication/connecting-to-github-with-ssh) and for [Cloning a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository). </div> === "Git + HTTPS" ```bash git clone https://github.com/your-username/griffe ``` <div class="result"> See the documentation on GitHub for [Cloning a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository). </div> ## Install dependencies First, enter the repository. If you installed [direnv](https://direnv.net/): - run `direnv allow` - run `make setup` If you didn't install [direnv](https://direnv.net/), just run `./scripts/make setup`. The setup command will install all the Python dependencies required to work on the project. This command will create a virtual environment in the `.venv` folder, as well as one virtual environment per supported Python version in the `.venvs/3.x` folders. If you cloned the repository on the same file-system as [uv](https://docs.astral.sh/uv/)'s cache, everything will be hard linked from the cache, so don't worry about wasting disk space. ## IDE setup If you work in VSCode, we provide [a command to configure VSCode](commands.md/#vscode) for the project. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/contributors.md������������������������������������������������������0000664�0001750�0001750�00000002271�14767006246�021305� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Contributor guide Welcome to the Griffe contributor guide! If you are familiar with Python tooling, development, and contributions to open-source projects, see the [TL;DR](#tldr) at the end, otherwise we recommend you walk through the following pages: - [Environment setup](contributors/setup.md) - [Management commands](contributors/commands.md) - [Development workflow](contributors/workflow.md) Regular contributors might be interested in the following documents that explain Griffe's design and inner workings: - [Architecture](contributors/architecture.md) If you are unsure about what to contribute to, you can check out [our issue tracker](https://github.com/mkdocstrings/griffe/issues) to see if some issues are interesting to you, or you could check out [our coverage report](contributors/coverage.md) to help us cover more of the codebase with tests. ## TL;DR - Install [Python](https://www.python.org/), [uv](https://docs.astral.sh/uv/) and [direnv](https://direnv.net/) - Fork, clone, and enter repository - Run `direnv allow` and `make setup` - Checkout a new branch - Edit code, tests and/or documentation - Run `make format check test docs` to check everything - Commit, push, open PR ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/���������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017365� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/how-to/��������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�020602� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/how-to/support-decorators.md�����������������������������������0000664�0001750�0001750�00000006041�14767006246�025004� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Supporting custom decorators Griffe aims to support the Python language itself, as well as its standard library. It means that built-in objects and objects from the standard library that can be used or are often used as decorators, should be supported natively or through official extensions, for example `@property`, `@functools.cache`, `@warnings.deprecated`, etc. Custom decorators however (the ones you define in your code-base) won't be supported by default, at least statically (dynamic analysis might be able to support them), because Griffe doesn't try to infer anything more than the obvious. Griffe is not a type-checker and so doesn't have the same inference abilities. Therefore, to support your own decorators (at least statically), you have to write Griffe extensions. Don't worry, extensions that support custom decorators are generally super easy to write. --- Lets assume we have a decorator whose path is `my_package.utils.enhance`. It is used throughout our code base like so: ```python from my_package.utils import enhance @enhance def my_function() -> ...: ... ``` Start by creating an extensions module (a simple Python file) somewhere in your repository, if you don't already have one. Within it, create an extension class: ```python import griffe class MyDecorator(griffe.Extension): """An extension to suport my decorator.""" ``` Now we can declare the [`on_instance`][griffe.Extension.on_instance] hook, which receives any kind of Griffe object ([`Module`][griffe.Module], [`Class`][griffe.Class], [`Function`][griffe.Function], [`Attribute`][griffe.Attribute]), or we could use a kind-specific hook such as [`on_module_instance`][griffe.Extension.on_module_instance], [`on_class_instance`][griffe.Extension.on_class_instance], [`on_function_instance`][griffe.Extension.on_function_instance] and [`on_attribute_instance`][griffe.Extension.on_attribute_instance]. For example, if you know your decorator is only ever used on class declarations, it would make sense to use `on_class_instance`. For the example, lets use the `on_function_instance` hook, which receives `Function` instances. ```python hl_lines="7-8" import griffe class MyDecorator(griffe.Extension): """An extension to suport my decorator.""" def on_function_instance(self, *, func: griffe.Function, **kwargs) -> None: ... ``` In this hook, we check if our function is decorated with our custom decorator: ```python hl_lines="8-10" import griffe class MyDecorator(griffe.Extension): """An extension to suport my decorator.""" def on_function_instance(self, *, func: griffe.Function, **kwargs) -> None: for decorator in func.decorators: if decorator.callable_path == "my_package.utils.enhance": ... # Update the function attributes. ``` Now all that is left to do is to actually write the code that updates the function according to what the decorator is doing. We could update the function's docstring, or its return type, or its parameters: it all depends on your decorator and what it does to the objects it decorates. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/how-to/set-docstring-styles.md���������������������������������0000664�0001750�0001750�00000011171�14767006246�025233� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Setting the right docstring style for every docstring Griffe attaches the specified docstring style and parsing options to each object in the tree of the package(s) you load. If your package(s) use several docstring styles, some of these objects will have the wrong style attached to them. This is problematic because other Griffe extensions rely on this attached style to parse docstrings and modify them. We plan to alleviate this limitation in the future (see [issue-340](https://github.com/mkdocstrings/griffe/issues/340)), but the most robust thing you can do is to make sure each object has the *right style* attached, as easly as possible, so that other extensions can work without issue. There are currently two ways to make sure objects have the right docstring style attached as early as possible: 1. Use the [`auto` docstring style](https://mkdocstrings.github.io/griffe/reference/docstrings/#auto-style) (currently only available to sponsors). Griffe will use regular expressions to infer the docstring style used. 100% accuracy is impossible to achieve, so it's possible that you get incorrect styles for some objects. 2. Write and use a custom Griffe extension. This how-to provides a few extension-based solutions to correctly set docstring styles in your packages. **Just make sure to enable these extensions in first position.** ## Markup comment Depending on the markup you use in docstrings, you can add a comment that tells Griffe which docstring style to use. === "Markdown" ```python def function(): """Summary. Body. <!-- style: google --> """ ``` === "reStructuredText" ```python def function(): """Summary. Body. .. style: google """ ``` Your Griffe extension can then use regular expressions to search for such comments. For example with Markdown (HTML) comments: ```python import re import griffe class ApplyDocstringStyle(griffe.Extension): def __init__(self, regex: str = "<!-- style: (google|numpy|sphinx) -->") -> None: self.regex = re.compile(regex) def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.docstring: if match := self.regex.search(obj.docstring.value): obj.docstring.parser = match.group(1) ``` ## Python comment You could also decide to add a trailing comment to your docstrings to indicate which style to use. ```python def function(): """Summary. Body. """ # style: google ``` Your extension can then pick up this comment to assign the right style: ```python import re import griffe class ApplyDocstringStyle(griffe.Extension): def __init__(self, regex: str = ".*# style: (google|numpy|sphinx)$") -> None: self.regex = re.compile(regex) def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.docstring: if match := self.regex.search(obj.docstring.source): obj.docstring.parser = match.group(1) ``` ## Explicit configuration Finally, you could decide to map a list of objects to the docstring style they should use. Your extension can either accept options, or it could hard-code that list: ```python import griffe from fnmatch import fnmatch class ApplyDocstringStyle(griffe.Extension): def __init__(self, config: dict[str, str]): self.instances = {} self.globs = {} for key, value in config.items(): if "*" in key: self.globs[key] = value else: self.instances[key] = value def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path in self.instances: if obj.docstring: obj.docsring.parser = self.instances[obj.path] else: for pattern, style in self.globs: if fnmatch(obj.path, pattern): if obj.docstring: obj.docstring.parser = style ``` Example configuration in MkDocs: ```yaml plugins: - mkdocstrings: handlers: python: options: extensions: - your_griffe_extension.py: config: path.to.obj1: google path.to.obj2: numpy path.to.obj3.*: sphinx path.to.obj4*: google ``` The benefit of this last solution is that it works for code you don't have control over. An alternative solution is to use the [griffe-autodocstringstyle extension](https://mkdocstrings.github.io/griffe/extensions/official/autodocstringstyle/) (sponsors only), which automatically assigns the `auto` style to all objects coming from sources found in a virtual environment. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/how-to/parse-docstrings.md�������������������������������������0000664�0001750�0001750�00000002434�14767006246�024416� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Using Griffe as a docstring-parsing library You can use Griffe to parse arbitrary docstrings. You don't have to load anything through the Griffe loader. You just need to import the [`Docstring`][griffe.Docstring] class. Then you can build a `Docstring` instance and call its `parse` method, choosing the parsing-style to use: ```python from griffe import Docstring text = "Hello I'm a docstring!" docstring = Docstring(text, lineno=1) parsed = docstring.parse("google") ``` If you want to take advantage of the parsers ability to fetch annotations from the object from which the docstring originates, you can manually create the parent objects and link them to the docstring: ```python from griffe import Docstring, Function, Parameters, Parameter, ParameterKind function = Function( "func", parameters=Parameters( Parameter("param1", annotation="str", kind=ParameterKind.positional_or_keyword), Parameter("param2", annotation="int", kind=ParameterKind.keyword_only), ), ) text = """ Hello I'm a docstring! Parameters: param1: Description. param2: Description. """ docstring = Docstring(text, lineno=1, parent=function) parsed = docstring.parse("google") ``` With this the parser will fetch the `str` and `int` annotations from the parent function's parameters. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/how-to/selectively-inspect.md����������������������������������0000664�0001750�0001750�00000017327�14767006246�025131� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Inspecting specific objects Griffe by default parses and visits your code (static analysis) instead of importing it and inspecting objects in memory (dynamic analysis). There are various reasons why static analysis is generally a better approach, but sometimes it is insufficient to handle particularly dynamic objects. When this happpens and Griffe cannot handle specific objects, you have a few solutions: 1. enable dynamic analysis for the whole package 2. write a Griffe extension that dynamically handles just the problematic objects 3. write a Griffe extension that statically handles the objects This document will help you achieve point 2. <!-- TODO: > NOTE: **Try static analysis first?** > We always recommend to try and handle things statically, so if you're interested, please check out these other documents: > > - link to doc > - link to doc --> Enabling dynamic analysis for whole packages is [not recommended][forcing-dynamic-analysis-not-recommended], but it can be useful to do it once and check the results, to see if our dynamic analysis agent is able to handle your code natively. Whether it is or not is not very important, you will be able to move onto creating an extension that will selectively inspect the relevant objects in any case. It could just be a bit more difficult in the latter case, and if you have trouble writing the extension we invite you to create a [Q&A discussion](https://github.com/mkdocstrings/griffe/discussions/categories/q-a) to get guidance. --- Start by creating an extensions module (a simple Python file) somewhere in your repository, if you don't already have one. Within it, create an extension class: ```python import griffe class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" ``` Make it accept configuration options by declaring an `__init__` method: ```python hl_lines="7-8" import griffe class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects ``` Here we choose to store a list of strings, where each string is an object path, like `module.Class.method`. Feel free to store different values to help you filter objects according to your needs. For example, maybe you want to inspect all functions with a given label, in that case you could accept a single string which is the label name. Or you may want to inspect all functions decorated with a specific decorator, etc. With this `__init__` method, users (or simply yourself) will be able to configure the extension by passing a list of object paths. You could also hard-code everything in the extension if you don't want or need to configure it. Now that our extension accepts options, we implement its core functionality. We assume that the static analysis agent is able to see the objects we are interested in, and will actually create instances that represent them (Griffe objects). Therefore we hook onto the `on_instance` event, which runs each time a Griffe object is created. ```python hl_lines="10-11" import griffe class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: ... ``` Check out the [available hooks][griffe.Extension] to see if there more appropriate hooks for your needs. Lets now use our configuration option to decide whether to do something or skip: ```python hl_lines="11-12" import griffe class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path not in self.objects: return ``` Now we know that only the objects we're interested in will be handled, so lets handle them. ```python hl_lines="3 16-20" import griffe logger = griffe.get_logger("griffe_inspect_specific_objects") # (1)! class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path not in self.objects: return try: runtime_obj = griffe.dynamic_import(obj.path) except ImportError as error: logger.warning(f"Could not import {obj.path}: {error}") # (2)! return ``` 1. We integrate with Griffe's logging (which also ensures integration with MkDocs' logging) by creating a logger. The name should look like a package name, with underscores. 2. We decide to log the exception as a warning (causing MkDocs builds to fail in `--strict` mode), but you could also log an error, or a debug message. Now that we have a reference to our runtime object, we can use it to alter the Griffe object. For example, we could use the runtime object's `__doc__` attribute, which could have been declared dynamically, to fix the Griffe object docstring: ```python hl_lines="22-25" import griffe logger = griffe.get_logger("griffe_inspect_specific_objects") class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path not in self.objects: return try: runtime_obj = griffe.dynamic_import(obj.path) except ImportError as error: logger.warning(f"Could not import {obj.path}: {error}") return if obj.docstring: obj.docstring.value = runtime_obj.__doc__ else: obj.docstring = griffe.Docstring(runtime_obj.__doc__) ``` Or we could alter the Griffe object parameters in case of functions, which could have been modified by a signature-changing decorator: ```python hl_lines="1 23-27" import inspect import griffe logger = griffe.get_logger("griffe_inspect_specific_objects") class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path not in self.objects: return try: runtime_obj = griffe.dynamic_import(obj.path) except ImportError as error: logger.warning(f"Could not import {obj.path}: {error}") return # Update default values modified by decorator. signature = inspect.signature(runtime_obj) for param in signature.parameters: if param.name in obj.parameters: obj.parameters[param.name].default = repr(param.default) ``` We could also entirely replace the Griffe object obtained from static analysis by the same one obtained from dynamic analysis: ```python hl_lines="14-25" import griffe class InspectSpecificObjects(griffe.Extension): """An extension to inspect just a few specific objects.""" def __init__(self, objects: list[str]) -> None: self.objects = objects def on_instance(self, *, obj: griffe.Object, **kwargs) -> None: if obj.path not in self.objects: return inspected_module = griffe.inspect(obj.module.path, filepath=obj.filepath) obj.parent.set_member(obj.name, inspected_module[obj.name]) # (1)! ``` 1. This assumes the object we're interested in is declared at the module level. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/loading.md�����������������������������������������������������0000664�0001750�0001750�00000040640�14767006246�021330� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Loading APIs Griffe can load API data from both source code (static analysis) and objects at runtime through introspection (dynamic analysis). Both static and dynamic analysis can be used at the same time: Griffe will first try to find sources, and will fall back to introspection if it cannot find any. When Griffe finds compiled modules within a packages, it uses introspection again to extract API information. There are various options to configure how Griffe loads data, for example to force or disallow dynamic analysis, but first let see the interface. ## The `load` function The main interface to load API data is Griffe's [`load`][griffe.load] function: ```python import griffe my_package = griffe.load("my_package") ``` You can ask to load a specific object rather than a package: ```python import griffe my_method = griffe.load("my_package.MyClass.my_method") ``` Griffe will load the whole package anyway, but return the specified object directly, so that you don't have to access it manually. To manually access the object representing the method called `my_method`, you would have used the `my_package` variable instantiated before, like this: ```python my_method = my_package["MyClass.my_method"] ``` The [Navigating](navigating.md) topic will show you all the ways Griffe objects can be navigated. Finally, you can even load packages or modules by passing absolute or relative file paths. This is useful when the module or package is not installed within the current Python environment and therefore cannot be found in the default search paths (see [Search paths](#search-paths) below). ```python import griffe griffe.load("src/my_package") griffe.load("some_script.py") ``` In case of ambiguity, you can instruct Griffe to ignore existing relative file paths with `try_relative_paths=False`. For example, when using [the flat layout (in contrast to the src-layout)](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/), your Python package is in the root of the repository. ```tree ./ my_package/ __init__.py pyproject.toml ``` Here if you ask Griffe to load `my_package`, it will find it as a relative path, in `./my_package`. If you want Griffe to use the version installed in your environment's site packages instead, set `try_relative_path` to false: ```python import griffe my_installed_package = griffe.load("my_package", try_relative_path=False) ``` ## The `GriffeLoader` class The [`load`][griffe.load] function is a shortcut for instantiating the [`GriffeLoader`][griffe.GriffeLoader] class and calling its [`load`][griffe.GriffeLoader.load] method. Calling the [`load`][griffe.load] function multiple times will instantiate a new Griffe loader each time. If you care about efficiency, it is better to instantiate the loader yourself and use its `load` method: ```python import griffe loader = GriffeLoader() my_package = loader.load("my_package") my_other_package = loader.load("my_other_package") ``` Keeping a reference to the loader will reduce the number of IO operations on the file-system, as the contents of the directories that the loader searches into will be cached (only the lists of files and directories will be cached, not the file contents). Reusing the same loader will also help resolving aliases across different packages. See [Alias resolution](#alias-resolution) below. ## Search paths To specify in which directories Griffe should search for packages and modules, you can use the `search_paths` parameter on both the [`load` function][griffe.load] and the [`GriffeLoader` class][griffe.GriffeLoader]. === "`load`" ```python import griffe my_package = griffe.load("my_package", search_paths=["src"]) ``` === "`GriffeLoader`" ```python import griffe loader = GriffeLoader(search_paths=["src"]) my_package = loader.load("my_package") ``` By default it will search in the paths found in [`sys.path`][sys.path], which can be influenced through the [`PYTHONPATH`][PYTHONPATH] environment variable. If Griffe cannot find sources for the specified object in the given search paths, it will try to import the specified object and use dynamic analysis on it (introspection). See [Forcing dynamic analysis](#forcing-dynamic-analysis) and [Disallowing dynamic analysis](#disallowing-dynamic-analysis). ## Forcing dynamic analysis Griffe always tries first to find sources for the specified object. Then, unless told otherwise, it uses static analysis to load API data, i.e. it parses the sources and visits the AST (Abstract Syntax Tree) to extract information. If for some reason you want Griffe to use dynamic analysis instead (importing and inspecting runtime objects), you can pass the `force_inspection=True` argument: ```python import griffe my_package = griffe.load("my_package", force_inspection=True) ``` [](){#forcing-dynamic-analysis-not-recommended} Forcing inspection can be useful when your code is highly dynamic, and static analysis has trouble keeping up. **However we don't recommend forcing inspection**, for a few reasons: - dynamic analysis requires that you either mock your dependencies, or install them - dynamic analysis will **execute code**, possibly ***arbitrary code*** if you import third-party dependencies, putting you at risk - dynamic analysis will potentially consume more resources (CPU, RAM) since it executes code - dynamic analysis will sometimes give you less precise or incomplete information - it's possible to write Griffe extensions that will *statically handle* the highly dynamic parts of your code (like custom decorators) that Griffe doesn't understand by default - if really needed, it's possible to [handle only a subset of objects with dynamic analysis](how-to/selectively-inspect.md), while the rest is loaded with static analysis, again thanks to Griffe extensions The [Extending](extending.md) topic will explain how to write and use extensions for Griffe. ## Disallowing dynamic analysis If you want to be careful about what gets executed in the current Python process, you can choose to disallow dynamic analysis by passing the `allow_inspection=False` argument. If Griffe cannot find sources for a package, it will not try to import it and will instead fail with a `ModuleNotFoundError` directly. ```python import griffe # Here Griffe will fall back on dynamic analysis and import `itertools`. griffe.load("itertools") # While here it will raise `ModuleNotFoundError`. griffe.load("itertools", allow_inspection=False) ``` ## Alias resolution >? QUESTION: **What's that?** > In Griffe, indirections to objects are called *aliases*. These indirections, or aliases, represent two kinds of objects: imported objects and inherited objects. Indeed, an imported object is "aliased" in the module that imports it, while its true location is in the module it was imported from. Similarly, a method inherited from a parent class is "aliased" in the subclass, while its true location is in the parent class. > > The name "alias" comes from the fact that imported objects can be aliased under a different name: `from X import A as B`. In the case of inherited members, this doesn't really apply, but we reuse the concept for conciseness. > > An [`Alias`][griffe.Alias] instance is therefore a pointer to another object. It has its own name, parent, line numbers, and stores the path to the target object. Thanks to this path, we can access the actual target object and all its metadata, such as name, parent, line numbers, docstring, etc.. Obtaining a reference to the target object is what we call "alias resolution". > > **To summarize, alias resolution is a post-process task that resolves imports after loading everything.** To resolve an alias, i.e. obtain a reference to the object it targets, we have to wait for this object to be loaded. Indeed, during analysis, objects are loaded in breadth-first order (in the object hierarchy, highest objects are loaded first, deepest ones are loaded last), so when we encounter an imported object, we often haven't loaded this object yet. Once a whole package is loaded, we are ready to try and resolve all aliases. But we don't *have* to resolve them. First, because the user might not need aliases to be resolved, and second, because each alias can be resolved individually and transparently when accessing its target object properties. Therefore, alias resolution is optional and enabled with the `resolve_aliases` parameter. Lets take an example. ```tree title="File layout" ./ my_package/ __init__.py my_module.py ``` ```python title="my_package/__init__.py" from my_package.my_module import my_function ``` ```python title="my_package/my_module.py" def my_function(): print("hello") ``` When loading this package, `my_package.my_function` will be an alias pointing at `my_package.my_module.my_function`: ```python import griffe my_package = griffe.load("my_package") my_package["my_function"].resolved # False ``` ```python import griffe my_package = griffe.load("my_package", resolve_aliases=True) my_package["my_function"].resolved # True my_package["my_function"].target is my_package["my_module.my_function"] # True ``` The [Navigating](navigating.md) topic will tell you more about aliases and how they behave. ### Modules collection In the first section of this page, we briefly mentioned that Griffe always loads the entire package containing the object you requested. One of the reasons it always loads entire packages and not just single, isolated objects, is that alias resolution requires all objects of a package to be loaded. Which means that if an alias points to an object that is part of *another* package, it can only be resolved if the *other* package is *also loaded*. For example: ```tree title="File layout" ./ package1/ __init__.py package2/ __init__.py ``` ```python title="package1/__init__.py" X = 0 ``` ```python title="package2/__init__.py" from package1 import X ``` ```pycon >>> import griffe >>> package2 = griffe.load("package2", resolve_aliases=True) >>> package2["X"].target_path 'package1.X' >>> package2["X"].resolved False >>> package2["X"].target Traceback (most recent call last): File "_griffe/dataclasses.py", line 1375, in _resolve_target resolved = self.modules_collection.get_member(self.target_path) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "_griffe/mixins.py", line 84, in get_member return self.members[parts[0]].get_member(parts[1:]) # type: ignore[attr-defined] ~~~~~~~~~~~~^^^^^^^^^^ KeyError: 'package1' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<stdin>", line 1, in <module> File "_griffe/dataclasses.py", line 1310, in target self.resolve_target() File "_griffe/dataclasses.py", line 1369, in resolve_target self._resolve_target() File "_griffe/dataclasses.py", line 1377, in _resolve_target raise AliasResolutionError(self) from error _griffe.exceptions.AliasResolutionError: Could not resolve alias package2.X pointing at package1.X (in package2/__init__.py:1) ``` As you can see in the interpreter session above, Griffe did not resolve the `X` alias. When we tried to access its target object anyway, it failed with a `KeyError`, which was raised again as an [`AliasResolutionError`][griffe.AliasResolutionError]. Lets try again, but this time by loading both packages. ```pycon >>> import griffe >>> package1 = griffe.load("package1") # nothing to resolve >>> package2 = griffe.load("package2", resolve_aliases=True) >>> package2["X"].target_path 'package1.X' >>> package2["X"].resolved False # Hmm? >>> package2["X"].target Traceback (most recent call last): ... _griffe.exceptions.AliasResolutionError: Could not resolve alias package2.X pointing at package1.X (in package2/__init__.py:1) ``` The same exception again? What happened here? We loaded both packages, but Griffe still failed to resolve the alias. That is expected; here is the explanation. If you look closely at the first exception traceback, you will see that Griffe searched the target path in `self.modules_collection`. So what is this modules collection? Each instance of [`GriffeLoader`][griffe.GriffeLoader] holds a reference to an instance of [`ModulesCollection`][griffe.ModulesCollection]. If you don't create such a collection manually to pass it to the loader, it will instantiate one itself. All objects loaded with this loader are added to this very modules collection, and gain a reference to it. Since the [`load` function][griffe.load] is just a shortcut for creating a loader and calling its [`load` method][griffe.GriffeLoader.load], when we called `griffe.load(...)` twice, it actually created two distinct collections of modules. When Griffe tried to resolve aliases of `package2`, it looked for `package1` in `package2`'s collection, and couldn't find it. Indeed, `package1` was in another modules collection. Therefore, to resolve aliases *across different packages*, these packages must be loaded within the same modules collection. In order to do that, you have a few options: - instantiate a single loader, and use it to load both packages - create your own modules collection, and pass it to the [`load` function][griffe.load] each time you call it - create your own modules collection, and pass it to the different instances of [`GriffeLoader`][griffe.GriffeLoader] you create === "Same loader" ```pycon >>> import griffe >>> loader = griffe.GriffeLoader() >>> package1 = loader.load("package1") >>> package2 = loader.load("package2") >>> loader.resolve_aliases() >>> package2["X"].resolved True >>> package2["X"].target Attribute('X', lineno=1, endlineno=1) ``` === "Same collection with `load`" ```pycon >>> import griffe >>> collection = griffe.ModulesCollection() >>> package1 = griffe.load("package1", modules_collection=collection) >>> package2 = griffe.load("package2", modules_collection=collection, resolve_aliases=True) >>> package2["X"].resolved True >>> package2["X"].target Attribute('X', lineno=1, endlineno=1) ``` === "Same collection, different loaders" ```pycon >>> import griffe >>> collection = griffe.ModulesCollection() >>> loader1 = griffe.GriffeLoader(modules_collection=collection, ...) >>> package1 = loader1.load("package1") >>> loader2 = griffe.GriffeLoader(modules_collection=collection, ...) # different parameters >>> package2 = loader2.load("package2") >>> package2["X"].resolved True >>> package2["X"].target Attribute('X', lineno=1, endlineno=1) ``` There is no preferred way, it depends on whether you need to instantiate different loaders with different parameters (search paths for example) while keeping every loaded module in the same collection, or if a single loader is enough, or if you explicitly need a reference to the collection, etc.. ### Loading external packages automatically By default, when resolving aliases, Griffe loaders will not be able to resolve aliases pointing at objects from "external" packages. By external, we mean that these packages are external to the current modules collection: they are not loaded. But sometimes users don't know in advance which packages need to be loaded in order to resolve aliases (and compute class inheritance). For these cases, Griffe loaders can be instructed to automatically load external packages. If we take the previous example again: ```python import griffe package2 = griffe.load("package2", resolve_aliases=True, resolve_external=True) print(package2["X"].target.name) # X ``` Here Griffe automatically loaded `package1` while resolving aliases, even though we didn't explicitly load it ourselves. While automatically resolving aliases pointing at external packages can be convenient, we advise cautiousness: this can trigger the loading of *a lot* of external packages, *recursively*. One special case that we must mention is that Griffe will by default automatically load *private sibling packages*. For example, when resolving aliases for the `ast` module, Griffe will automatically try and load `_ast` too (if dynamic analysis is allowed, since this is a builtin module), even without `resolve_external=True`. If you want to prevent this behavior, you can pass `resolve_external=False` (it is `None` by default). ## Next steps Now that the API is loaded, you can start [navigating it](navigating.md), [serializing it](serializing.md) or [checking for API breaking changes](checking.md). If you find out that the API data is incorrect or incomplete, you might want to learn how to [extend it](extending.md). ������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/serializing.md�������������������������������������������������0000664�0001750�0001750�00000010560�14767006246�022231� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Serializing APIs Griffe can be used to load API data and output it as JSON on standard output or in writable files. An example of what real data looks like can be found here: [Griffe's full JSON dump](../../griffe.json). We also provide a [JSON schema](../../schema.json). ## Command-line The easiest way to load and serialize API data is to use the command-line tool: ```console $ griffe dump httpx fastapi { "httpx": { "name": "httpx", ... }, "fastapi": { "name": "fastapi", ... } } ``` It will output a JSON-serialized version of the package's API data. Try it out on Griffe itself: ```console $ griffe dump griffe { "griffe": { "name": "griffe", ... } } ``` To output in a file instead of standard output, use the `-o`, `--output` option: ```console $ griffe dump griffe -o griffe.json ``` If you load multiple packages' signatures, you can dump each in its own file with a templated filepath: ```console $ griffe dump griffe -o './dumps/{package}.json' ``` By default, Griffe will search in `sys.path`, so if you installed it through *pipx*, there are few chances it will find your packages. To explicitly specify search paths, use the `-s, --search <PATH>` option. You can use it multiple times. You can also add the search paths to the `PYTHONPATH` environment variable. If Griffe can't find the packages, it will fail with a `ModuleNotFoundError`. See all the options for the `dump` command in the [CLI reference](../../reference/cli.md). ## Python API If you have read through the [Navigating](navigating.md) chapter, you know about our five data models for modules, classes, functions, attributes and aliases. Each one of these model provide the two following methods: - [`as_json`][griffe.Object.as_json], which allows to serialize an object into JSON, - [`from_json`][griffe.Object.from_json], which allows loading JSON back into a model instance. These two methods are convenient wrappers around our [JSON encoder][griffe.JSONEncoder] and [JSON decoder][griffe.json_decoder]. The JSON encoder and decoder will give you finer-grain control over what you serialize or load, as the methods above are only available on data models, and not on sub-structures like decorators or parameters. Under the hood, `as_json` just calls [`as_dict`][griffe.Object.as_dict], which converts the model instance into a dictionary, and then serializes this dictionary to JSON. When serializing an object, by default the JSON will only contain the fields required to load it back to a Griffe model instance. If you are not planning on loading back the data into our data models, or if you want to load them in a different implementation which is not able to infer back all the other fields, you can choose to serialize every possible field. We call this a full dump, and it is enabled with the `full` option of the [encoder][griffe.JSONEncoder] or the [`as_json`][griffe.Object.as_json] method. ## Schema For anything automated, we suggest relying on our [JSON schema](../../schema.json). When serializing multiple packages with the `dump` command, you get a map with package names as keys. Map values are the serialized objects (modules, classes, functions, etc.). They are maps too, with field names and values as key-value pairs. For example: ```json { "kind": "class", "name": "Expr", "lineno": 82, "endlineno": 171, "docstring": { "value": "Base class for expressions.", "lineno": 84, "endlineno": 84 }, "labels": [ "dataclass" ], "members": [ ... ], "bases": [], "decorators": [ { "value": { "name": "dataclass", "cls": "ExprName" }, "lineno": 82, "endlineno": 82 } ] } ``` The `members` value, truncated here, just repeats the pattern: it's an array of maps. We use an array for members instead of a map to preserve order, which could be important to downstream tools. The other fields do not require explanations, except maybe for expressions. You will sometimes notice deeply nested structures with `cls` keys. These are serialized Griffe [expressions](../../reference/api/expressions.md). They represent actual code. ## Next steps That's it! There is not much to say about serialization. We are interested in getting your feedback regarding serialization as we didn't see it being used a lot. Next you might be interested in learning how to [check](checking.md) or [extend](extending.md) your API data. ������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/recommendations/�����������������������������������������������0000775�0001750�0001750�00000000000�14767006246�022554� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/recommendations/python-code.md���������������������������������0000664�0001750�0001750�00000035572�14767006246�025343� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Python code best practices This document describes some best practices to adopt when using Griffe, or when writing Python code that will be analyzed by Griffe. ## Avoid member-submodule name shadowing Sometimes we find that an `__init__` module defines or imports an object which has the same name as a submodule of the parent package. **Case 1** ```tree package/ __init__.py subpackage/ __init__.py thing.py ``` ```python title="package/subpackage/__init__.py" thing = "thing from init module" ``` ```python title="package/subpackage/thing.py" other_thing = "other thing from thing submodule" ``` We recommend not doing that. Why? Because the `package.subpackage.thing` submodule can eventually **shadow** the `package.subpackage.thing` attribute. Try this: ```bash # Replicate the file tree from above. mkdir -p package/subpackage echo 'thing = "thing from init module"' > package/subpackage/__init__.py echo 'other_thing = "other thing from thing submodule"' > package/subpackage/thing.py # Run a Python interpreter. python ``` ```pycon >>> from package import subpackage >>> subpackage.thing 'thing from init module' >>> # OK, but... >>> from package.subpackage.thing import other_thing >>> subpackage.thing <module 'package.subpackage.thing' from 'package/subpackage/thing.py'> ``` By simply importing from the `thing` submodule, the `thing` attribute of `subpackage` was overwritten by the `thing` submodule. **Case 2** In a particular case though, the situation improves: if we *import* `thing` in the init module instead of declaring it, then further imports will not overwrite anything: ```python title="package/subpackage/__init__.py" from package.subpackage.thing import thing ``` ```python title="package/subpackage/thing.py" thing = "thing from thing submodule" ``` ```bash # Update the modules. echo 'from package.subpackage.thing import thing' > package/subpackage/__init__.py echo 'thing = "thing from thing submodule"' > package/subpackage/thing.py # Run a Python interpreter. python ``` ```pycon >>> from package import subpackage >>> subpackage.thing 'thing from thing' >>> # OK >>> from package.subpackage.thing import thing >>> subpackage.thing 'thing from thing' >>> # Still OK ``` From an API perspective, and given that both cases are very similar but differ in behavior, we recommend not doing that either. If the goal is to isolate a single object into its own module, to then expose it in the parent module, then it would make sense that this object is the only object of the submodule to be exposed in the public API, and therefore the submodule could be marked as private by prefixing its name with an underscore: ```tree package/ __init__.py subpackage/ __init__.py _thing.py ``` With this, there is no ambiguity as to what `subpackage.thing` points to. For the reasons mentioned above, **Griffe does not support this kind of name shadowing.** During static analysis, the submodule will take precedence over the attribute. During dynamic analysis, Griffe's behavior is undefined. ## Avoid wildcard imports Wildcard imports allow importing from a module all objects that do not start with an underscore `_`, or all objects that are listed in the module's `__all__` attribute, if it is defined. ```tree package/ __init__.py module.py ``` **Explicitly exposed to wildcard imports** ```python title="package/module.py" __all__ = [ "SomeClass", "some_function", "some_attribute", ] class SomeClass: ... class SomeOtherClass: ... def some_function(): ... def some_other_function(): ... some_attribute = 0 some_other_attribute = 1 ``` **Implicitly exposed to wildcard imports** ```python title="package/module.py" class SomeClass: ... class _SomeOtherClass: ... def some_function(): ... def _some_other_function(): ... some_attribute = 0 _some_other_attribute = 1 ``` In both cases, using a wildcard import will only import `SomeClass`, `some_function` and `some_attribute`, and not their "other" counterparts: ```python title="package/__init__.py" from package.module import * ``` While we recommend declaring your public API with `__all__` lists, we do not recommend using wildcard imports. In the implicit case, any other object imported in `module.py` will also be exported by the wildcard: ```python title="package/module.py" from somewhere_else import this, that class SomeClass: ... class _SomeOtherClass: ... def some_function(): ... def _some_other_function(): ... some_attribute = 0 _some_other_attribute = 1 ``` Here, `this` and `that` will also be imported when we do `from package.module import *`. To prevent that, we would have to alias these names as such: ```python title="package/module.py" from somewhere_else import this as _this, that as _that ``` ...which is not ideal. It gets even worse if `module.py` itself uses wildcard imports: ```python title="package/module.py" from somewhere_else import * ``` Now using `from package.module import *` will import all objects that do not start with an underscore declared in the module, but also all the objects imported by it that do not start with an underscore, and also all the objects imported by the modules of the imported objects that do not start with an underscore, etc., recursively. Soon enough, we end up with dozens and dozens of objects exposed in `package`, while just a few of them are useful/meaningful to users. Not only that, but it also increases the risk of creating cycles in imports. Python can handle some of these cycles, but static analysis tools such as Griffe can have a much harder time trying to resolve them. In the explicit case, the situation improves, as only the objects listed in `__all__` will be exported to the modules that wildcard imports them. It effectively stops namespace pollution, but it does not remove the risk of cyclic imports, only decreases it. We have seen code bases where parent modules wildcard imports from submodules, while these submodules also wildcard imports from the parent modules... Python somehow handles this, but it is *hell* to handle statically, and it is just too error prone (cyclic imports, name shadowing, namespaces become dependent on the order of imports, etc.). For these reasons, we recommend not using wildcard imports. Instead, we recommend declaring your public API explicitly with `__all__`, and combining `__all__` lists together if needed: ```tree package/ __init__.py module.py other_module.py ``` Completely explicit: ```python title="package/__init__.py" from package.module import only, needed, objects from package.other_module import some, more __all__ = [ "only", "needed", "some", "function", ] def function(): ... ``` Combining `__all__` lists: ```python title="package/__init__.py" from package.module import only, needed, objects, __all__ as module_all from package.other_module import some, more, __all__ as other_module_all __all__ = [ *module_all, *other_module_all, "function", ] def function(): ... ``` Most Python linting tools allow you to forbid the use of wildcard imports. ## Prefer canonical imports Within your own code base, we recommend using canonical imports. By canonical, we mean importing objects from the module they are declared in, and not from another module that also imports them. Given the following tree: ```tree package/ __init__.py module_a.py module_b.py ``` ```python title="package/module_a.py" from package.module_b import thing ``` ```python title="package/module_b.py" thing = True ``` Don't do that: ```python title="package/__init__.py" from package.module_a import thing # Indirect import, bad. ``` Instead, do this: ```python title="package/__init__.py" from package.module_b import thing # Canonical import, good. ``` --- We especially recommend canonical imports over indirect imports from sibling modules passing through the parent: ```python title="package/__init__.py" from package.module_a import thing # Canonical import, good. ``` ```python title="package/module_a.py" thing = True ``` ```python title="package/module_b.py" from package import thing # Indirect import passing through parent, bad. # Do this instead: from package.module_a import thing # Canonical import, good. ``` --- Similarly, avoid exposing the API of external packages from your own package and recommending to use this indirect API. ```python title="package.py" import numpy as np __all__ = ["np"] # Bad. # Recommending users to do `from package import np` # or `import package; package.np.etc`: bad. ``` Instead, let users import Numpy themselves, with `import numpy as np`. This will help other analysis tools, for example to detect that Numpy is used directly and should therefore be listed as a dependency. To quote [PEP 8](https://peps.python.org/pep-0008/#public-and-internal-interfaces): > Imported names should always be considered an implementation detail. Other modules must not rely on indirect access to such imported names unless they are an explicitly documented part of the containing module’s API, such as os.path or a package’s `__init__` module that exposes functionality from submodules. Emphasis on *exposes functionality from submodules*: PEP 8 does not state *exposing functionality from external packages*. --- Using canonical imports provides several benefits: - it can reduce the risk of cyclic imports - it can increase performance by reducing hoops and importing less things (for example by not passing through a parent module that imports many things from siblings modules) - it makes the code more readable and easier to refactor (less indirections) - it makes the life of static analysis tools easier (less indirections) We recommend using the [canonical-imports](https://github.com/15r10nk/canonical-imports) tool to automatically rewrite your imports as canonical. Note however that we recommend using public imports (importing from the "public" locations rather than the canonical ones) when: - importing from other packages - importing from your own package within your tests suite Apply these recommendations at your discretion: there may be other special cases where it might not make sense to use canonical imports. ## Make your compiled objects tell their true location Python modules can be written in other languages (C, C++, Rust) and compiled. To extract information from such compiled modules, we have to use dynamic analysis, since sources are not available. A practice that seems common in projects including compiled modules in their distributions is to make the compiled modules private (prefix their names with an underscore), and to expose their objects from a public module higher-up in the module layout, for example by wildcard importing everything from it. ```tree package/ __init__.py module.py _module.cpython-312-x86_64-linux-gnu.so ``` ```python title="package/module.py" from package._module import * ``` Since the objects are exposed in `package.module` instead of `package._module`, developers sometimes decide to make their compiled objects lie about their location, and make them say that they are defined in `package.module` instead of `package._module`. Example: ```pycon >>> from package._module import MyObject >>> MyObject.__module__ 'package.module' ``` **Don't do that.** When using dynamic analysis and inspecting modules, Griffe must distinguish objects that were declared in the inspected module from objects that were imported from other modules. The reason is that if we didn't care where objects come from, we could end up inspecting the same objects and their members again and again, since they can be imported in many places. This could lead to infinite loops, recursivity errors, and would generally decrease performance. So, when Griffe inspects a member of the compiled `_module`, and this member lies and says it comes from `package.module`, Griffe thinks it was imported. It means that Griffe will record the object as an indirection, or alias, instead of visiting it in-place. But that is wrong: the object was actually declared in the module, and should not have been recorded as an indirection. Fortunately, we were able to put some guard-rails in place, which means that the case above where the compiled and public modules have the same name, except for the leading underscore, is supported, and will not trigger errors. But other cases where modules have different names will trigger issues, and we have to special case them in Griffe itself, after issues are reported. Please, use your framework features to correctly set the `__module__` attribute of your objects (functions, classes and their methods too) as their *canonical location*, not their public location or any other location in the final package. For example with [PyO3](https://github.com/PyO3/pyo3): ```rust // Your module is compiled and added as `_module` into `package`, // but its objects are exposed in `package` or `package.module`. // Set `module = "package._module"`, not `module = "package"` or `module = "package.module"`! #[pyclass(name = "MyClass", module = "package._module")] struct MyClass { // ... } ``` Some modules of the standard library are guilty of this too, and do so inconsistently (`ast` and `_ast`, `io` and `_io`, depending on the Python version...). For this reason, when checking if an object was declared in the currently inspected module, Griffe ultimately considers that any qualified name is equal to itself with each component stripped from leading underscores: ``` a.b.c == _a.b.c a.b.c == _a._b._c a.__b._c == __a.b.c ... ``` When the qualified name of the object's parent module and the currently inspected module match like above, the object is inspected in-place (added as a member of the currently inspected module) instead of created as an alias. ## Avoid forward references in base classes Python's type system will let you use forward references in generic types when they are used as base classes. For example: === "Before Python 3.12" ```python from typing import TypeVar, Generic T = TypeVar('T') class Foo(Generic[T]): ... class FooBar(Foo['Bar']): ... class Bar: ... ``` === "Python 3.12+" ```python class Foo[T]: ... class FooBar(Foo['Bar']): ... class Bar: ... ``` While Griffe will load this code without error, the `'Bar'` forward reference won't be resolved to the actual `Bar` class. As a consequence, downstream tools like documentation renderers won't be able to output a link to the `Bar` class. We therefore recommend to avoid using forward references in base classes, if possible. Instead, you can try one of the following approach: - declare or import the `Bar` class earlier - declare a proper type: ```python class Foo[T]: ... type TBar = Bar class FooBar(Foo[TBar]): ... class Bar: ... ``` - make `FooBar` generic again but with a default type: ```python class Foo[T]: ... class FooBar[T=Bar](Foo[T]): ... class Bar: ... ``` ��������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/recommendations/public-apis.md���������������������������������0000664�0001750�0001750�00000106021�14767006246�025306� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Public APIs ## What is a public API? An API (Application Programming Interface) in the interface with which developers interact with your software. In the Python world, the API of your Python library is the set of modules, classes, functions and other attributes made available to your users. For example, users can do `from your_library import this_function`: `this_function` is part of the API of `your_library`. Oftentimes, when you develop a library, you create functions, classes, etc. that are only useful internally: they are not supposed to be used by your users. Python does not provide easy or standard ways to actually *prevent* users from using internal objects, so, to distinguish public objects from internal objects, we usually rely on conventions, such as prefixing internal objects' names with an underscore, for example `def _internal_function(): ...`, to mark them as "internal". Prefixing an object's name with an underscore still does not prevent users from importing and using this object, but it *informs* them that they are not supposed to import and use it, and that this object might change or even disappear in the future, *without notice*. On the other hand, public objects are supposed to stay compatible with previous versions of your library for at least a definite amount of time, to prevent downstream code from breaking. Any change that could break downstream code is supposed to be communicated *before* it is actually released. Maintainers of the library usually allow a period of time where the public object can still be used as before, but will emit deprecation warnings when doing so, hinting users that they should upgrade their use of the object (or use another object that will replace it). This period of time is usually called a deprecation period. So, how do we mark an object as public? How do we inform our users which objects can safely be used, and which ones are subject to unnotified changes? Usually, we rely again on the underscore prefix convention: if an object isn't prefixed with an underscore, it means that it is public. But essentially, your public API is what you say it is. If you clearly document that a single function of your package is public, and that all others are subject to unnotified changes and whose usage by users is not supported, then your public API is composed of this single function, and nothing else. **Public APIs are a matter of communication.** Concretely, it's about deciding what parts of your code base are public, and communicating that clearly. Some components are obviously considered for the public API of a Python package: - the module layout - functions and their signature - classes (their inheritance), their methods and signatures - the rest of the module or class attributes, their types and values Other components *should* be considered for the public API but are often forgotten: - CLI options: see [The CLI is API too](#the-cli-is-api-too) section - logger names: users might rely on them to filter logs (see [Logger names](#logger-names)) - exceptions raised: users definitely rely on them to catch errors Other components *could* be considered for the public API, but usually require too much maintenance: - logging messages: users might rely on them to grep the logs - exception messages: users might rely on them for various things Besides, logging and exception messages simply cannot allow deprecation periods where both old and new messages are emitted. Maintainers could however consider adding unique, short codes to messages for more robust consumption. > GRIFFE: **Our recommendation — Communicate your public API, verify what you can.** > Take the time to learn about and use ways to declare, communicate and deprecate your public API. Your users will have an easier time using your library. On the maintenance side, you won't get bug reports for uses that are not supported, or you will be able to quickly close them by pointing at the documentation explaining what your public API is, or why something was deprecated, for how long, and how to upgrade. > > Automate verifications around your public API with tools like Griffe. Currently Griffe doesn't support checking CLI configuration options, logger names or raised exceptions. If you have the capacity to, verify these manually before each release. [Griffe checks](../checking.md) and [API rules enforcement](#ensuring-api-rules) are a very good starting point. ## Conventions Python does not provide any standard way to declare public APIs. However we do have official recommendations and a few conventions. ### Underscore prefix In the Python ecosystem we very often prefix objects with an underscore to mark them as internal, or private. Objects that are not prefixed are then implicitly considered public. For example: ```python def public_function(): ... def _internal_function(): ... ``` The exception to this rule is that imported objects are not considered public. For example: ```python from elsewhere import something ``` Even though `something` doesn't start with an underscore, it was imported so it is not considered public. ### `__all__` list There is another convention that lets you do the opposite: explicitly mark objects as public. This convention uses the `__all__` module-level attribute, which is a list of strings containing the names of the public objects. ```python title="package/module.py" __all__ [ "this_function", "ThisClass", ] def this_function(): ... def this_other_function(): ... class ThisClass: ... class ThisOtherClass: ... ``` Here, even though `this_other_function` and `ThisOtherClass` are *not* prefixed with underscores, they are not considered public, because we explicitly and only marked `this_function` and `ThisClass` as public. Declaring `__all__` has another beneficial effect: it affects wildcard imports. When your users use wildcard imports to import things from one of your modules, Python will only import the objects that are listed in `__all__`. Without `__all__`, it would import all objects that are not prefixed with an underscore, *including objects already imported from elsewhere*. This can cause serious namespace pollution, and even slow down Python code when wildcard imports are chained. [We actually recommend avoiding wildcard imports](python-code.md#avoid-wildcard-imports). By declaring `__all__`, your public API becomes explicit, and explicit is better than implicit. But `__all__` only works for module-level objects. Within classes, you will still have to rely on the underscore prefix convention to mark methods or attributes as internal/private. ```python class Thing: def public_method(self): ... def _internal_method(self): ... ``` ### Redundant aliases When you expose your public API in `__init__` modules by importing most objects from the underlying modules, it can be a bit tedious to import everything, and then list everything again in the `__all__` list attribute. For this reason, another convention emerged where objects imported and aliased with the same name are considered public. ```python title="my_package/__init__.py" from elsewhere import something as something from my_package._internal_module import Thing as Thing ``` Here `Thing` and `something` are considered public even though they were imported. If `__all__` was defined, it would take precedence and redundant aliases wouldn't apply. ### Wildcard imports Same as for redundant aliases, this convention says that all objects imported thanks to wildcard imports are public. This can again be useful in `__init__` modules where you expose lots of objects declared in submodules. ```python title="my_package/__init__.py" from my_package._internal_module1 import * from my_package._internal_module2 import * ``` Note that the wildcard imports logic stays the same, and imports either all objects that do not start with an underscore (imported objects included!), or all objects listed in `__all__` if it is defined. It doesn't care about other conventions such as redundant aliases, or the wildcard imports convention itself. --- > GRIFFE: **Our recommendation — Use the underscore prefix and `__all__` conventions.** > Use both the underscore prefix convention for consistent naming at module and class levels, and the `__all__` convention for declaring your public API. We do not recommend using the redundant aliases convention, because it doesn't provide any information at runtime. We do not recommend the wildcard import convention either, for the same reason and [for additional reasons mentioned here](python-code.md#avoid-wildcard-imports). We still provide the [`griffe-public-redundant-aliases`](https://mkdocstrings.github.io/griffe-public-redundant-aliases/) and [`griffe-public-wildcard-imports`](https://mkdocstrings.github.io/griffe-public-wildcard-imports/) extensions for those who would still like to rely on these conventions. > > Our recommendation matches [PEP 8](https://peps.python.org/pep-0008/#public-and-internal-interfaces): > > > To better support introspection, modules should explicitly declare the names in their public API using the `__all__` attribute. Setting `__all__` to an empty list indicates that the module has no public API. > > > Even with `__all__` set appropriately, internal interfaces (packages, modules, classes, functions, attributes or other names) should still be prefixed with a single leading underscore. > TIP: **Concatenating `__all__` for easier maintenance of `__init__` modules.** > If you worry about maintenance of your `__init__` modules, know that you can very well concatenate `__all__` lists from submodules into the current one: > > ```tree > my_package/ > __init__.py > module.py > subpackage1/ > __init__.py > _module1a.py > subpackage2/ > __init__.py > _module2a.py > ``` > > ```python title="my_package/subpackage1/__init__.py" > from my_package.subpackage1.module1a import this1a, that1a > > __all__ = ["this1a", "that1a"] > ``` > > ```python title="my_package/subpackage2/__init__.py" > from my_package.subpackage2.module2a import this2a, that2a > > __all__ = ["this2a", "that2a"] > ``` > > ```python title="my_package/__init__.py" > from my_package.module import this > from my_package.subpackage1 import this1a, that1a, __all__ as subpackage1_all > from my_package.subpackage2 import this2a, that2a, __all__ as subpackage2_all > > __all__ = ["this", *subpackage1_all, *subpackage2_all] > > # Griffe supports the `+` and `+=` operators too: > # __all__ = ["this"] + subpackage1_all + subpackage2_all > # __all__ = ["this"]; __all__ += subpackage1_all; __all__ += subpackage2_all > ``` > > However we would argue that `this1a`, `that1a`, `this2a` and `that2a` should not be exposed publicly in more than one location. See our section on [unique names and public locations](#unique-names-and-public-locations). ## Module layout We usually split the code of our packages into different modules. The code can be split according to domains, types of objects, logic, etc.: we don't have any recommendation on that. However, your package layout is part of your API, so it should be taken into account when deciding what you expose as your public API. Most of the time, packages implicitly expose their module layout in their public API. Indeed, when you start a new project, you create new modules but don't immediately think about making them private. Then the project grows organically, you add more modules, and users start actually relying on their layout, importing specific objects from specific modules. Now when you want to move objects around, to reorganize your layout, you introduce breaking changes. So you have to create a deprecation period where objects that moved around are still importable in the old locations, but emit deprecation warnings. A module-level `__getattr__` function is commonly used for that. ```python title="package/old_module.py" import warnings from typing import Any def __getattr__(name: str) -> Any: if name == "my_object": warnings.warn( "Importing `my_object` from `old_module` is deprecated, import it from `new_module` instead.", DeprecationWarning, stacklevel=2, ) from package.new_module import my_object return my_object raise AttributeError(f"module 'old_module' has no attribute '{name}'") ``` Such changes sometimes go unnoticed before the breaking change is released, because users don't enable deprecation warnings. These changes can also be confusing to users when they do notice the warnings: maybe they don't use the deprecated import themselves, and are not sure where to report the deprecated use. These changes also require time to upgrade, and time to maintain. What if we could make this easier? By hiding your module layout from your public API, you're removing all these pain points at once. Any object can freely move around without ever impacting users. Maintainers do not need to set deprecation periods where old and new uses are supported, or bump the major part of their semantic version when they stop supporting the old use. Hiding the module layout also removes the ambiguity of whether a submodule is considered public or not: [PEP 8](https://peps.python.org/pep-0008/#public-and-internal-interfaces) doesn't mention anything about it, and it doesn't look like the `__all__` convention expects developers to list their submodules too. In the end it looks like submodules are only subject to the underscore prefix convention. So, how do we hide the module layout from the public API? The most common way to hide the module layout is to make all your modules private, by prefixing their name with an underscore: ```tree my_package/ __init__.py _combat.py _exploration.py _sorcery.py ``` Then, you expose public objects in the top-level `__init__` module thanks to its `__all__` attribute: ```python title="my_package/__init__.py" from my_package._combat import Combat from my_package._exploration import navigate from my_package._sorcery import cast_spell __all__ [ "Combat", "navigate", "cast_spell", ] ``` Now, if you want to move `cast_spell` into the `_combat` module, you can do so without impacting users. You can even rename your modules. All you have to do when doing so is update your top-level `__init__` module to import the objects from the right locations. If you have more than one layer of submodules, you don't have to make the next layer private: only the first one is enough, as it informs users that they shouldn't import from this layer anyway: ```tree my_package/ __init__.py _combat.py _exploration.py _sorcery/ __init__.py dark.py light.py ``` If you don't want to bother prefixing every module with an underscore, you could go one step further and do one of these two things: - move everything into an `_internal` directory: ```tree my_package/ __init__.py _internal/ __init__.py combat.py exploration.py sorcery/ __init__.py dark.py light.py ``` - or move everything into a private package: ```tree my_package/ __init__.py _my_package/ __init__.py combat.py exploration.py sorcery/ __init__.py dark.py light.py ``` Whatever *hidden* layout you choose (private modules, internals, private package), it is not very important, as you will be able to switch from one to another easily. In Griffe we chose to experiment and go with the private package approach. This highlighted a few shortcomings that we were able to address in both Griffe and mkdocstrings-python, so we are happy with the result. WARNING: **Top-level-only exposition doesn't play well with large packages.** The *fully* hidden layout plays well with small to medium projects. If you maintain a large project, it can become very impractical for both you and your users to expose every single object in the top-level `__init__` module. For large projects, it therefore makes sense to keep at least one or two additional public layers in your module layout. Sometimes packages also implement many variations of the same abstract class, using the same name in many different modules: in these cases, the modules are effective namespaces that could be kept in the public API. GRIFFE: **Our recommendation — Hide your module layout early.** Start hiding your module layout early! It is much easier to (partially) expose the layout later than to hide it after your users started relying on it. It will also make code reorganizations much easier. ## Unique names and public locations Whether or not you are planning to hide your module layout, as recommended in the previous section, one thing that will help both you and your users is making sure your object names are unique across your code base. Having unique names ensures that you can expose everything at the top-level module of your package without having to alias objects (using `from ... import x as y`). It will also ensure that your users don't end up importing multiple different objects with the same name, again having to alias them. Finally, it forces you to use meaningful names for your objects, names that don't need the context of the above namespaces (generally modules) to understand what they mean. For example, in Griffe we previously exposed `griffe.docstrings.utils.warning`. Exposing `warning` at the top-level made it very vague: what does it do? So we renamed it `docstring_warning`, which is much clearer. Ensuring unique names across a code base is sometimes not feasible, or not desirable; in this case, try to use namespacing while still hiding the module layout the best you can. In accordance with our recommendation on module layouts, it is also useful to ensure that a single public object is exposed in a single location. Ensuring a unique public location for each object removes any ambiguity on the user side as to where to import the object from. It also helps documentation generators that try to cross-reference objects: with several locations, they cannot know for sure which one is the best to reference (which path is best to use and display in the generated documentation). With a fully hidden layout, all objects are *only* exposed in the top-level module, so there is no ambiguity. With partially hidden layouts, or completely public layouts, make sure to declare your public API so that each object is only exposed in a single location. Example: ```tree my_package/ __init__.py module.py ``` === "Multiple locations, bad" Here the `Hello` class is exposed in both `my_package.module` and `my_package`. ```python title="my_package/module.py" __all__ ["Hello"] class Hello: ... ``` ```python title="my_package/__init__" from my_package.module import Hello __all__ = ["Hello"] ``` === "Single location, good" Here the `Hello` class is only exposed in `my_package.module`. ```python title="my_package/module.py" __all__ ["Hello"] class Hello: ... ``` ```python title="my_package/__init__" # Nothing to see here. ``` If you wanted to expose it in the top-level `__init__` module instead, then you should hide your module layout by making `module.py` private, renaming it `_module.py`, or using other hiding techniques such as described in the [Module layout](#module-layout) section. === "Single location (top-level), good" Here the `Hello` class is only exposed in `package`. ```python title="my_package/module.py" __all__ = [] class Hello: ... ``` ```python title="my_package/__init__" from my_package.module import Hello __all__ = ["Hello"] ``` It feels weird to "unpublicize" the `Hello` class in `my_package.module` by declaring an empty `__all__`, so maybe the module should be made private instead: `my_package/_module.py`. See other hiding techniques in the [Module layout](#module-layout) section. GRIFFE: **Our recommendation — Expose public objects in single locations, use meaningful names.** We recommend making sure that each public object is exposed in a single location. Ensuring unique names might be more tricky depending on the code base, so we recommend ensuring meaningful names at least, not requiring the context of modules above to understand what the objects are for. ## Logger names The documentation of the standard `logging` library recommends to use `__name__` as logger name when obtaining a logger with `logging.getLogger()`, *unless we have a specific reason for not doing that*. Unfortunately, no examples of such specific reasons are given. So let us give one. Using `__name__` as logger names means that your loggers have the same name as your module paths. For example, the module `package/module.py`, whose path and `__name__` value are `package.module`, will have a logger with the same name, i.e. `package.module`. If your module layout is public, that's fine: renaming the module or moving it around is already a breaking change that you must document. However if your module layout is hidden, or if this particular module is private, then even though renaming it or moving it around is *not* breaking change, the change of name of its logger *is*. Indeed, by renaming your module (or moving it), you changed its `__name__` value, and therefore you changed its logger name. Now, users that were relying on this name (for example to silence WARNING-level logs and below coming from this particular module) will see their logic break without any error and without any deprecation warning. ```python # For example, the following would have zero effect if `_module` was renamed `_other_module`. package_module_logger = logging.getLogger("package._module") package_module_logger.setLevel(logging.ERROR) ``` Could we emit a deprecation warning when users obtain the logger with the old name? Unfortunately, there is no standard way to do that. This would require patching `logging.getLogger`, which means it would only work when users actually use this method, in a Python interpreter, and not for all the other ways logging can be configured (configuration files, configuration dicts, etc.). Since it is essentially impossible to deprecate a logger name, we recommend to avoid using `__name__` as logger name, at the very least in private modules. GRIFFE: **Our recommendation — Use a single logger.** Absolutely avoid using `__name__` as logger name in private modules. If your module layout is hidden, or does not matter for logging purposes, just use the same logger everywhere by using your package name as logger name. Example: `logger = logging.getLogger("griffe")`. Show your users how to temporarily alter your global logger (typically with context managers) so that altering subloggers becomes unnecessary. Maybe even provide the utilities to do that. ## Documentation Obviously, your public API should be documented. Each object should have a docstring that explains why the object is useful and how it is used. More on that in our [docstrings recommendations](docstrings.md). Docstrings work well for offline documentation; we recommend exposing your public API online too, for example with [MkDocs](https://www.mkdocs.org/) and [mkdocstrings' Python handler](https://mkdocstrings.github.io/python/), or with other SSGs (Static Site Generators). Prefer a tool that is able to create a [Sphinx-like](https://sphobjinv.readthedocs.io/en/stable/syntax.html) inventory of objects (an `objects.inv` file) that will allow other projects to easily cross-reference your API from their own documentation. Make sure each and every object of your public API is documented in your web docs and therefore added to the objects inventory (and maybe that nothing else is added to this inventory as "public API"). > GRIFFE: **Our recommendation — Document your public API extensively.** > Write docstrings for each and every object of your public API. Deploy online documentation where each object is documented and added to an object inventory that can be consumed by third-party projects. If you find yourself reluctant to document a public object, it means that this object should maybe be internal instead. > > Our documentation framework of choice is of course [MkDocs](https://www.mkdocs.org) combined with our [mkdocstrings](https://mkdocstrings.github.io/) plugin. ## Ensuring API rules If you already follow some of these recommendations, or if you decide to start following them, it might be a good idea to make sure that these recommendations keep being followed as your code base evolves. The intent of these recommendations, or "rules", can be captured in tests relatively easily thanks to Griffe. We invite you to check out our own test file: [`test_internals.py`](https://github.com/mkdocstrings/griffe/blob/main/tests/test_internals.py). This test module asserts several things: - all public objects are exposed in the top-level `griffe` module - all public objects have unique names - all public objects have single locations - all public objects are added to the inventory (which means they are documented in our API docs) - no private object is added to the inventory GRIFFE: **Our recommendation — Test your API declaration early.** The sooner you test your API declaration, the better your code base will evolve. This will force you to really think about how your API is exposed to your users. This will prevent mistakes like leaving a new object as public while you don't want users to start relying on it, or forgetting to expose a public object in your top-level module or to document it in your API docs. ## Linters Depending on their configuration, many popular Python linters will warn you that you access or import private objects. This doesn't play well with hidden module layouts, where modules are private or moved under a private (sub-)package. Sometimes it doesn't even play well with private methods > GRIFFE: **Our recommendation — Ignore "protected access" warnings for your own package, or make the warnings smarter.** > To users of linters, we recommend adding `# noqa` comments on the relevant code lines, or globally disabling warnings related to "private object access" if per-line exclusion requires too much maintenance. > > To authors of linters, we recommend (if possible) making these warnings smarter: they shouldn't be triggered when private objects are accessed from within the *same package*. Marking objects as private is meant to prevent downstream code from using them, not to prevent the developers of the current package themselves from using them: they know what they are doing and should be allowed to use their own private objects without warnings. At the same time, they don't want to disable these warnings *globally*, so the warnings should be derived in multiple versions, or made smarter. ## The CLI is API too This section deserves an entire article, but we will try to stay succinct here. Generally, we distinguish the API (Application Programming Interface) from the CLI (Command Line Interface), TUI (Textual User Interface) or GUI (Graphical User Interface). Contrary to TUIs or GUIs which are not likely to be controlled programmatically (they typically work with keyboard and mouse inputs), the CLI can easily be called by various scripts or programs, including from Python programs. Even if a project was not designed to be used programmatically (doesn't expose a public API), it is *a certainty* that with enough popularity, it *will* be used programmatically. And the CLI will even more so be used programmatically if there is no API. Even if there is an API, sometimes it makes more sense to hook into the CLI rather than the API (cross-language integrations, wrappers, etc.). Therefore, we urge everyone to consider their CLI as an API too. We urge everyone to always design their project as library-first APIs rather than CLI-first tools. The first user of your CLI as API is... you. When you declare your project's CLI entrypoint in pyproject.toml: ```toml [project.scripts] griffe = "griffe:main" ``` ...this entrypoint ends up as a Python script in the `bin` directory of your virtual environment: ```python #!/media/data/dev/griffe/.venv/bin/python # -*- coding: utf-8 -*- import re import sys from griffe import main if __name__ == "__main__": sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) sys.exit(main()) ``` In this script, we find our entrypoint, `griffe.main`, used programmatically. --- The second user of your CLI as API is... you again. When you write tests for your CLI, you import your entrypoints and call them by passing CLI options and arguments, maybe asserting the exit code raised with a `SystemExit` or the standard output/error thanks to [pytest's capture fixtures](https://docs.pytest.org/en/6.2.x/capture.html). Some simplified examples from our own test suite: ```python title="tests/test_cli.py" import pytest import griffe def test_main() -> None: assert griffe.main(["dump", "griffe", "-s", "src", "-o/dev/null"]) == 0 def test_show_help(capsys: pytest.CaptureFixture) -> None: with pytest.raises(SystemExit): griffe.main(["-h"]) captured = capsys.readouterr() assert "griffe" in captured.out def test_show_version(capsys: pytest.CaptureFixture) -> None: with pytest.raises(SystemExit): griffe.main(["-V"]) captured = capsys.readouterr() assert griffe.get_version() in captured.out ``` Now, when you start testing the logic of your CLI subcommands, such as our `dump` subcommand above, you might feel like passing again and again through the command-line arguments parser (here `argparse`) is wasteful and redundant. It is important to test that your arguments are parsed correctly (as you expect them to be parsed), but they shouldn't *have* to be parsed when you are testing the underlying logic. It's a hint that your command-line arguments parsing (and command-line handling generally) should be *decoupled* from the logic below it: write functions with proper parameters! Then call these functions from your main CLI entrypoint, with the arguments obtained from parsing the command-line arguments and options. It will make testing and debugging much, much easier: ```python import argparse import sys def dump(...): ... def main(args: list[str] | None = None) -> int: parser = argparse.ArgumentParser(...) opts = parser.parse_args(args) if opts.subcommand == "dump": return dump(opts.arg1, opts.arg2, ...) elif ... print(f"Unknown subcommand {opts.subcommand}", file=sys.stderr) return 1 ``` Now instead of having to call `main(["dump", "..."])` in your tests, you can directly call `dump(...)`, with all the benefits from static-typing and your IDE features, such as autocompletion, linting, etc.. --- The third and next users of your CLI as API are your users: just as you made your own life easier, you made their life easier for when they want to call some subcommands of your tool programmatically. No more messing with lists of strings without autocompletion or linting, no more patching of `sys.argv`, no more following the maze of transformations applied by this fancy CLI framework before finally reaching the crux of the subcommand you want to call, no more trying to replicate these transformations yourself with the CLI framework's API to avoid copy-pasting the dozens of lines you're only interested in. > GRIFFE: **Our recommendation — Decouple command-line parsing from your CLI entrypoints.** > Do not tie the command parsing logic with your program's logic. Create functions early, make them accept arguments using basic types (`int`, `str`, `list`, etc.) so that your users can call your main command or subcommands with a single import and single statement. Do not encode all the logic in a single big `main` function. Decoupling the CLI-parsing logic from your entrypoints will make them much easier to test and use programmatically. Consider your entrypoints part of your API! > > Our CLI framework of choice is [Cappa](https://pypi.org/project/cappa/). ## Deprecations With time, the code base of your project evolves. You add features, you fix bugs, and you generally reorganize code. Some of these changes might make your project's public API incompatible with previous versions. In that case, you usually have to "deprecate" previous usage in favor of the new usage. That means you have to support both, and emit deprecation warnings when old usage is detected. There are many different ways of deprecating previous usage of code, which depend on the change itself. We invite you to read our [Checking APIs](../checking.md) chapter, which describes all the API changes Griffe is able to detect, and provides hints on how to allow deprecation periods for each kind of change. In addition to emitting deprecation warnings, you should also update the docstrings and documentation for the old usage to point at the new usage, add "deprecated" labels where possible, and mark objects as deprecated when possible. GRIFFE: **Our recommendation — Allow a deprecation period, document deprecations.** Try allowing deprecation periods for every breaking change. Most changes can be made backward-compatible at the cost of writing legacy code. Use tools like [Yore](https://pawamoy.github.io/yore) to manage legacy code, and standard utilities like [`warnings.deprecated`][] to mark objects as deprecated. Griffe extensions such as [griffe-warnings-deprecated](https://mkdocstrings.github.io/griffe-warnings-deprecated/) can help you by dynamically augmenting docstrings for your API documentation. ## Third-party libraries A few third-party libraries directly or indirectly related to public APIs deserve to be mentioned here. [public](https://pypi.org/project/public/) lets you decorate objects with `@public.add` to dynamically add them to `__all__`, so that you don't have to build a list of strings yourself. The "public visibility" marker is closer to each object, and might help avoid mistakes like forgetting to update `__all__` when an object is removed or renamed. [modul](https://pypi.org/project/modul/), from Frost Ming, the author of [PDM](https://pdm-project.org/en/latest/), goes one step further and actually hides attributes that are not marked "exported" from users: they won't be able to access un-exported attributes, leaving *only* the public API visible. [Deprecated](https://pypi.org/project/Deprecated/), which was probably a source of inspiration for [PEP 702](https://peps.python.org/pep-0702/), allows decorating objects with `@deprecated` to mark them as deprecated. Such decorated callables will emit deprecation warnings when called. PEP 702's `warnings.deprecated` could be seen as its successor, bringing the feature directly into the standard library so that type checkers and other static analysis tools can converge on this way to mark objects as deprecated. [slothy](https://pypi.org/project/slothy/), which is less directly related to public APIs, but useful for the case where you are hiding your modules layout and exposing all your public API from the top-level `__init__` module. Depending on the size of your public API, and the time it takes to import everything (memory initializations, etc.), it might be interesting to make all these imports *lazy*. With a lazily imported public API, users who are only interested in a few objects of your public API won't have to pay the price of importing everything. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/recommendations/docstrings.md����������������������������������0000664�0001750�0001750�00000035333�14767006246�025264� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstrings Here are explanations on what docstrings are, and a few recommendations on how to write them. This guide uses the [Google-style](../../../reference/docstrings.md#google-style), because that is our preferred and recommended style, but you can also use any other supported style. Skip to the [Styles](#styles) section to learn about the existing docstring styles. We invite you to read their own style guides as they are full of examples and good advice. ## Definition A docstring is a line or block of text describing objects such as modules, classes, functions and attributes. They are written below the object signature or assignment, or appear as first expression in a module: ```python title="module.py" """This is the module docstring.""" a = 0 """This is an attribute docstring.""" def b(): """This is a function docstring.""" class C: """This is a class docstring.""" def d(self): """This is a method docstring.""" ``` ## Multi-line docstrings Each docstring can span multiple lines if it is wrapped in triple double-quotes (which is generally the case and the official recommendation even for single-line docstrings): ```python def function(): """This is a longer docstring. It spans on multiple lines. Blank lines are allowed, too. """ ``` When writing multi-line docstrings, it is recommended to write a short description on the first line, then separate the rest of the docstring with a blank line. The first line is called the **summary**, and the rest of docstring is called the **body**. The summary is useful to documentation generators and other tools to show the short description of an object. ## Markup Docstrings are just text, so you can use any markup you want. The markup you choose will generally depend on what you decide to do with your docstrings: if you generate API documentation from your docstrings, and the documentation renderer expects Markdown, then you should write your docstrings in Markdown. Examples of markups are [Markdown](https://daringfireball.net/projects/markdown/) (which has many different implementations and many different "flavors"), [reStructuredText](https://docutils.sourceforge.io/rst.html), [AsciiDoc](https://asciidoc.org/), and [Djot](https://djot.net/). For example, if you are using [MkDocs](https://www.mkdocs.org) and [mkdocstrings](https://mkdocstrings.github.io/) to generate your API documentation, you should write your docstrings in Markdown. If you are using [Sphinx](https://www.sphinx-doc.org/en/master/), you should probably write your docstrings in reStructuredText, unless you are also using the [MyST](https://myst-parser.readthedocs.io/en/latest/index.html) extension. Whatever markup you choose, try to stay consistent within your code base. ## Styles Docstrings can be written for modules, classes, functions, and attributes. But there are other aspects of a Python API that need to be documented, such as function parameters, returned values, and raised exceptions, to name a few. We could document everything in natural language, but that would make it hard for downstream tools such as documentation generators to extract information in a structured way, to allow dedicated rendering such as tables for parameters. To compensate for the lack of structure in natural languages, docstring "styles" emerged. A docstring style is a micro-format for docstrings, allowing to structure the information by following a specific format. With the most popular Google and Numpydoc styles, information in docstrings is decomposed into **sections** of different kinds, for example "parameter" sections or "return" sections. Some kinds of section then support documenting multiple items, or support a single block of markup. For example, we can document multiple parameters in "parameter" sections, but a "note" section is only composed of a text block. Structuring the information in sections and items allows documentation-related tools to extract and provide this information in a structured way, by parsing the docstrings according to the style they follow. Griffe has parsers for [Google-style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings), [Numpydoc-style](https://numpydoc.readthedocs.io/en/latest/format.html), and [Sphinx-style](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html) docstrings. See the complete reference for these parsers and styles in the [Docstrings reference](../../../reference/docstrings.md). We recommend that you read the style guides mentioned here as they are full of examples and good advice too. <div class="grid cards" markdown> <div markdown> ```python title="Google-style" def greet(name: str, end: str = "!") -> None: """Greet someone. Parameters: name: The name to greet. end: The punctuation mark at the end. Note: Greetings are cool! """ print(f"Hey {name}{end}") ‎ ``` <!-- The invisible character above is here on purpose, to make both divs the same height. --> </div> <div markdown> ```python title="Numpydoc-style" def greet(name: str, end: str = "!") -> None: """Greet someone. Parameters ---------- name The name to greet. end The punctuation mark at the end. Note ---- Greetings are cool! """ print(f"Hey {name}{end}") ``` </div> </div> Our preferred style for docstrings is the **Google-style**, because it is in our opinion the most markup-agnostic style: it is based on any kind of markup or documentation generator. Our second choice would be the Numpydoc-style, for its readability. For the adventurers, have a look at [PEP 727](https://peps.python.org/pep-0727/) (draft) and [griffe-typingdoc](https://mkdocstrings.github.io/griffe-typingdoc/), a Griffe extension to support PEP 727. PEP 727 proposes an alternative way to provide information in a structured way, that does not rely on a docstring micro-format. It takes advantage of `typing.Annotated` to attach documentation to any type-annotated object, like attributes, parameters and returned values. With PEP 727, docstrings styles and their sections aren't required anymore, and docstrings can be written in plain markup, without following any particular style. This makes it easier for tools like Griffe who then don't have to parse docstrings *at all*. The PEP is a bit controversial (lots of different opinions), so we invite you to make your own opinion by looking at real-world projects using it, such as [FastAPI](https://github.com/tiangolo/fastapi/blob/master/fastapi/applications.py), or by reading the (very-long) [discussion on discuss.python.org](https://discuss.python.org/t/pep-727-documentation-metadata-in-typing/32566/17). The PEP was actually written by FastAPI's author, Sebastián Ramírez. ```python title="PEP 727" from typing_extensions import Annotated, Doc def greet( name: Annotated[str, Doc("The name to greet."), end: Annotated[str, Doc("The punctuation mark at the end.")] = "!", ) -> None: """Greet someone. > [!NOTE] > Greetings are cool! """ # (1)! print(f"Hey {name}{end}") ``` 1. Here we use the [GitHub syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) for a "note" callout. It assumes our documentation renderer supports this syntax. The point is that we rely purely on Markdown rather than docstrings styles. ## General tips Your docstrings will typically be used to document your API, either on a deployed (static) website, or locally, on the command line or in a Python interpreter. Therefore, when writing your docstrings, you should address the right audience: the users of your code. Try to stay succinct and give clear examples. Docstrings are not really the place to explain architectural or technical decisions made while developing the project: this information, while extremely valuable, is better written in *code comments*, where the audience is other developers working on the code base. Your docstrings will typically again be read online (HTML) or other types of documents such as manual pages or PDFs. Make sure to write complete sentences, with correct punctuation. That means for example, to start each parameter description with a capital letter, and to end it with a period. When documenting objects acting as namespaces (modules, classes, enumerations), prefer documenting each attribute separately than with an Attributes section in the namespace object docstring. For example, add a docstring to each enumeration value rather than describing each value in the docstring of the enumeration class. ## Modules Module docstrings should briefly explain what the module contains, and for what purposes these objects can be used. If the documentation generator you chose does not support generating member summaries automatically, you might want to add docstrings sections for attributes, functions, classes and submodules. ```python title="package/__init__.py" """A generic package to demonstrate docstrings. This package does not really exist, and is only used as a demonstration purpose for docstrings. Anyway, this package contains the following API, exposed directly at the top-level, meaning you can import everything from `package` directly. Attributes: ghost: A ghost wandering in this desolated land. dummy: A dummy you can practice on. Not for ghosts. Classes: Ghost: Ah, so this is where our ghost comes from. Maybe create some additional ghosts so they can pass the time together? Functions: deploy(): Deploy something on the web (we're not sure what exactly). """ ``` Do the same thing for every other module of the package, except if you are [hiding your module layout](public-apis.md#module-layout). ## Classes, methods, properties Class docstrings follow the same logic as module docstrings. Explain what the class is used for, and maybe show a few of its attributes and methods thanks to sections of the same name. A class is already more concrete than a module, so we can maybe start adding usage examples too. Such examples should only show how to create instances of the class. Examples of use for methods can be written in each respective method. ```python class Ghost: """Ghosts that wander the earth. Ghosts are meant to... we're actually unsure. All we know is that, as a user, you might find it amusing to instantiate a few of them and put them together to see what they do. Methods: wander: Wander the earth. spook: Spook living organisms. pass_through: Pass through anything. Examples: Create a new ghost with a cool nickname: >>> ghost = Ghost(nickname="Rattlesnake") """ def wander(self) -> None: """Wander the earth. That's it, really. Examples: >>> ghost.wander() """ ... @property def weight(self) -> int: """The ghost's weight (spoiler: it's close to 0).""" ... ``` Note that blocks of lines starting with `>>>` or `...` are automatically parsed as code blocks by Griffe, until a blank line is found. This only works in Examples (plural!) sections. If you rely on [Python-Markdown](https://python-markdown.github.io/) to convert Markdown to HTML (which is the case for MkDocs), you can use the [markdown-pycon](https://pawamoy.github.io/markdown-pycon/) extension to recognize such `pycon` code blocks anywhere, without having to wrap them in fences. You can also choose to use explicit fences everywhere: ````python """ Examples: Create a new ghost with a cool nickname: ```pycon >>> ghost = Ghost(nickname="Rattlesnake") ``` """ ```` ## Functions Function and method docstrings will typically describe their parameters and return values. For generators, it's also possible to describe what the generator yields and what it can receive, though the latter is not often used. ```python import datetime from typing import Generator, Iterator class GhostOlympicGames: ... class GOGTicket: ... def organize_gog(date: datetime.date) -> GhostOlympicGames: """Organize Olympic Games for Ghosts. The ghost world is much simpler than the living world, so it's super easy to organize big events like this. Parameters: date: The date of the games. Returns: The prepared games. """ ... def yield_athletes(quantity: int) -> Iterator[Ghost]: """Yield a certain quantity of athletes. Parameters: quantity: How many ghost athletes you want. Yields: Ghost athletes. They're just regular ghosts. """ ... def gog_tickets_factory() -> Generator[GOGTicket, int, None]: """Generate tickets for the GOG. We value fairness: tickets are priced randomly. Unless we send a specific price to the generator. Yields: Tickets for the games. Receives: Price for the next ticket, in ghost money (???). """ ... ``` ## Attributes Attribute docstrings are written below their assignment. As usual, they should have a short summary, and an optional, longer body. ```python GHOST_MASS_CONSTANT: float = 1e-100 """The ghost mass constant. This is a very small number. Use it scientifically for all ghost-related things. Note: There is actually nothing scientific about any of this. """ # (1)! ``` 1. Our `Note` section here is parsed as an admonition. See [Google-style admonitions](../../../reference/docstrings.md#google-admonitions) for reference. Class and instance attributes can be documented the same way: ```python class GhostTown: instances: str """All the existing towns.""" def __init__(self, name: str, size: int) -> None: self.name = name """The town's name.""" self.size = size """The town's size.""" ``` ## Exceptions, warnings Callables that raise exceptions or emit warnings can document each of these exceptions and warnings. Documenting them informs your users that they could or should catch the raised exceptions, or that they could filter or configure warnings differently. The description next to each exception or warning should explain how or when they are raised or emitted. ```python def verify_spirit_chest(): """Do a verification routine on the spirit chest. Raises: OverflowError: When the verification failed and all the ghosts escaped from the spirit chest. """ ... def negotiate_return_to_the_spirit_chest(): """Negotiate with ghost for them to return in the spirit chest. Warns: ResourceWarning: When the ghosts refuse to go back in the chest because the chest is too tight. """ ... ``` ## Going further There are more sections and more features to discover and use. For a complete reference on docstring styles syntax, see our [reference](../../../reference/docstrings.md). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/extending.md���������������������������������������������������0000664�0001750�0001750�00000070653�14767006246�021707� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extending APIs Griffe has an extension system that can be used to enhance or customize the data that Griffe collects. Extensions are written in Python. ## Using extensions Extensions can be specified both on the command-line (in the terminal), and programmatically (in Python). ### On the command-line On the command-line, you can specify extensions to use with the `-e`, `--extensions` option. This option accepts a single positional argument which can take two forms: - a comma-separated list of extensions - a JSON list of extensions Extensions can accept options: the comma-separated list does not allow to specify options, while the JSON list does. See examples below. With both forms, each extension refers to one of these three things: - the name of a built-in extension's module, for example `dynamic_docstrings` (this is just an example, this built-in extension does not exist) - the Python dotted-path to a module containing one or more extensions, or to an extension directly, for example `package.module` and `package.module.ThisExtension` - the file path to a Python script, and an optional extension name, separated by a colon, for example `scripts/griffe_exts.py` and `scripts/griffe_exts.py:ThisExtension` The specified extension modules can contain more than one extension: Griffe will pick up and load every extension declared or imported within the modules. If options are specified for a module that contains multiple extensions, the same options will be passed to all the extensions, so extension writers must make sure that all extensions within a single module accept the same options. If they don't, Griffe will abort with an error. To specify options in the JSON form, use a dictionary instead of a string: the dictionary's only key is the extension identifier (built-in name, Python path, file path) and its value is a dictionary of options. Some examples: ```bash griffe dump griffe -e pydantic,scripts/exts.py:DynamicDocstrings,griffe_attrs ``` ```bash griffe check --search src griffe -e '[ {"pydantic": {"schema": true}}, { "scripts/exts.py:DynamicDocstrings": { "paths": ["mypkg.mymod.myobj"] } }, "griffe_attrs" ]' ``` In the above two examples, `pydantic` would be a built-in extension, `scripts/exts.py:DynamicDocstrings` the file path plus name of a local extension, and `griffe_attrs` the name of a third-party package that exposes one or more extensions. ### Programmatically Within Python code, extensions can be specified with the `extensions` parameter of the [`GriffeLoader` class][griffe.GriffeLoader] or [`load` function][griffe.load]. The parameter accepts an instance of the [`Extensions` class][griffe.Extensions]. Such an instance is created with the help of the [`load_extensions` function][griffe.load_extensions], which itself accepts a list of strings, dictionaries, extension classes and extension instances. Strings and dictionaries are used the same way as [on the command-line](#on-the-command-line). Extension instances are used as such, and extension classes are instantiated without any options. Example: ```python import griffe from mypackage.extensions import ThisExtension, ThisOtherExtension extensions = griffe.load_extensions( {"pydantic": {"schema": true}}, {"scripts/exts.py:DynamicDocstrings": {"paths": ["mypkg.mymod.myobj"]}}, "griffe_attrs", ThisExtension(option="value"), ThisOtherExtension, ) data = griffe.load("mypackage", extensions=extensions) ``` ### In MkDocs MkDocs and its mkdocstrings plugin can be configured to use Griffe extensions: ```yaml title="mkdocs.yml" plugins: - mkdocstrings: handlers: python: options: extensions: - pydantic: {schema: true} - scripts/exts.py:DynamicDocstrings: paths: [mypkg.mymod.myobj] - griffe_attrs ``` The `extensions` key accepts a list that is passed to the [`load_extensions` function][griffe.load_extensions]. See [how to use extensions programmatically](#programmatically) to learn more. ## Writing extensions In the next section we give a bit of context on how Griffe works, to show how extensions can integrate into the data collection process. Feel free to skip to the [Events and hooks](#events-and-hooks) section or the [Full example](#full-example) section if you'd prefer to see concrete examples first. ### How it works To extract information from your Python sources, Griffe tries to build Abstract Syntax Trees by parsing the sources with [`ast`][] utilities. If the source code is not available (the modules are built-in or compiled), Griffe imports the modules and builds object trees instead. Griffe then follows the [Visitor pattern](https://www.wikiwand.com/en/Visitor_pattern) to walk the tree and extract information. For ASTs, Griffe uses its [Visitor agent][griffe.Visitor] and for object trees, it uses its [Inspector agent][griffe.Inspector]. Sometimes during the walk through the tree (depth-first order), both the visitor and inspector agents will trigger events. These events can be hooked on by extensions to alter or enhance Griffe's behavior. Some hooks will be passed just the current node being visited, others will be passed both the node and an instance of an [Object][griffe.Object] subclass, such as a [Module][griffe.Module], a [Class][griffe.Class], a [Function][griffe.Function], or an [Attribute][griffe.Attribute]. Extensions will therefore be able to modify these instances. The following flow chart shows an example of an AST visit. The tree is simplified: actual trees have a lot more nodes like `if/elif/else` nodes, `try/except/else/finally` nodes, [and many more][ast.AST]. ```mermaid flowchart TB M(Module definition) --- C(Class definition) & F(Function definition) C --- m(Function definition) & A(Variable assignment) ``` The following flow chart shows an example of an object tree inspection. The tree is simplified as well: [many more types of objects are handled][griffe.ObjectKind]. ```mermaid flowchart TB M(Module) --- C(Class) & F(Function) C --- m(Method) & A(Attribute) ``` For a more concrete example, let say that we visit (or inspect) an AST (or object tree) for a given module, and that this module contains a single class, which itself contains a single method: - the agent (visitor or inspector) will walk through the tree by starting with the module node - it will instantiate a [Module][griffe.Module], then walk through its members, continuing with the class node - it will instantiate a [Class][griffe.Class], then walk through its members, continuing with the function node - it will instantiate a [Function][griffe.Function] - then it will go back up and finish walking since there are no more nodes to walk through Every time the agent enters a node, creates an object instance, or finishes handling members of an object, it will trigger an event. The flow of events is drawn in the following flowchart: ```mermaid flowchart TB visit_mod{{enter module node}} event_mod_node{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_node'><b><code style='color: var(--md-accent-fg-color)'>on_node</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_module_node'><b><code style='color: var(--md-accent-fg-color)'>on_module_node</code></b></a> event"}} create_mod{{create module instance}} event_mod_instance{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_instance'><b><code style='color: var(--md-accent-fg-color)'>on_instance</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_module_instance'><b><code style='color: var(--md-accent-fg-color)'>on_module_instance</code></b></a> event"}} visit_mod_members{{visit module members}} visit_cls{{enter class node}} event_cls_node{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_node'><b><code style='color: var(--md-accent-fg-color)'>on_node</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_class_node'><b><code style='color: var(--md-accent-fg-color)'>on_class_node</code></b></a> event"}} create_cls{{create class instance}} event_cls_instance{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_instance'><b><code style='color: var(--md-accent-fg-color)'>on_instance</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_class_instance'><b><code style='color: var(--md-accent-fg-color)'>on_class_instance</code></b></a> event"}} visit_cls_members{{visit class members}} visit_func{{enter func node}} event_func_node{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_node'><b><code style='color: var(--md-accent-fg-color)'>on_node</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_function_node'><b><code style='color: var(--md-accent-fg-color)'>on_function_node</code></b></a> event"}} create_func{{create function instance}} event_func_instance{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_instance'><b><code style='color: var(--md-accent-fg-color)'>on_instance</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_function_instance'><b><code style='color: var(--md-accent-fg-color)'>on_function_instance</code></b></a> event"}} event_cls_members{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_members'><b><code style='color: var(--md-accent-fg-color)'>on_members</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_class_members'><b><code style='color: var(--md-accent-fg-color)'>on_class_members</code></b></a> event"}} event_mod_members{{"<a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_members'><b><code style='color: var(--md-accent-fg-color)'>on_members</code></b></a> event<br><a href='/griffe/reference/griffe/extensions/#griffe.Extension.on_module_members'><b><code style='color: var(--md-accent-fg-color)'>on_module_members</code></b></a> event"}} start{start} --> visit_mod visit_mod --> event_mod_node event_mod_node --> create_mod create_mod --> event_mod_instance event_mod_instance --> visit_mod_members visit_mod_members --1--> visit_cls visit_cls --> event_cls_node event_cls_node --> create_cls create_cls --> event_cls_instance event_cls_instance --> visit_cls_members visit_cls_members --1--> visit_func visit_func --> event_func_node event_func_node --> create_func create_func --> event_func_instance event_func_instance --> visit_cls_members visit_cls_members --2--> event_cls_members event_cls_members --> visit_mod_members visit_mod_members --2--> event_mod_members event_mod_members --> finish{finish} class event_mod_node event class event_mod_instance event class event_cls_node event class event_cls_instance event class event_func_node event class event_func_instance event class event_cls_members event class event_mod_members event classDef event stroke:#3cc,stroke-width:2 ``` Hopefully this flowchart gives you a pretty good idea of what happens when Griffe collects data from a Python module. The next section will explain in more details the different events that are triggered, and how to hook onto them in your extensions. ### Events and hooks There are two kinds of events in Griffe: **load events** and **analysis events**. Load events are scoped to the Griffe loader. Analysis events are scoped to the visitor and inspector agents (triggered during static and dynamic analysis). #### Load events There are two **load events**: - [`on_package_loaded`][griffe.Extension.on_package_loaded]: The "on package loaded" event is triggered when the loader has finished loading a package entirely, i.e. when all its submodules were scanned and loaded. This event can be hooked by extensions which require the whole package to be loaded, to be able to navigate the object tree without raising lookup errors or alias resolution errors. - [`on_wildcard_expansion`][griffe.Extension.on_wildcard_expansion]: The "on wildcard expansion" event is triggered for each alias that is created by expanding wildcard imports (`from ... import *`). #### Analysis events There are 3 generic **analysis events**: - [`on_node`][griffe.Extension.on_node]: The "on node" events are triggered when the agent (visitor or inspector) starts handling a node in the tree (AST or object tree). - [`on_instance`][griffe.Extension.on_instance]: The "on instance" events are triggered when the agent just created an instance of [Module][griffe.Module], [Class][griffe.Class], [Function][griffe.Function], or [Attribute][griffe.Attribute], and added it as a member of its parent. The "on instance" event is **not** triggered when an [Alias][griffe.Alias] is created. - [`on_members`][griffe.Extension.on_members]: The "on members" events are triggered when the agent just finished handling all the members of an object. Functions and attributes do not have members, so there are no "on members" events for these two kinds. There are also specific **analysis events** for each object kind: - [`on_module_node`][griffe.Extension.on_module_node] - [`on_module_instance`][griffe.Extension.on_module_instance] - [`on_module_members`][griffe.Extension.on_module_members] - [`on_class_node`][griffe.Extension.on_class_node] - [`on_class_instance`][griffe.Extension.on_class_instance] - [`on_class_members`][griffe.Extension.on_class_members] - [`on_function_node`][griffe.Extension.on_function_node] - [`on_function_instance`][griffe.Extension.on_function_instance] - [`on_attribute_node`][griffe.Extension.on_attribute_node] - [`on_attribute_instance`][griffe.Extension.on_attribute_instance] And a special event for aliases: - [`on_alias`][griffe.Extension.on_alias]: The "on alias" event is triggered when an [Alias][griffe.Alias] was just created and added as a member of its parent object. --- **Hooks** are methods that are called when a particular event is triggered. To target a specific event, the hook must be named after it. **Extensions** are classes that inherit from [Griffe's Extension base class][griffe.Extension] and define some hooks as methods: ```python import ast import griffe class MyExtension(griffe.Extension): def on_instance( self, node: ast.AST | griffe.ObjectNode, obj: griffe.Object, agent: griffe.Visitor | griffe.Inspector, **kwargs, ) -> None: """Do something with `node` and/or `obj`.""" ``` Hooks are always defined as methods of a class inheriting from [Extension][griffe.Extension], never as standalone functions. IDEs should autocomplete the signature when you start typing `def` followed by a hook name. Since hooks are declared in a class, feel free to also declare state variables (or any other variable) in the `__init__` method: ```python import ast from griffe import Extension, Object, ObjectNode class MyExtension(Extension): def __init__(self) -> None: super().__init__() self.state_thingy = "initial stuff" self.list_of_things = [] def on_instance( self, node: ast.AST | griffe.ObjectNode, obj: griffe.Object, agent: griffe.Visitor | griffe.Inspector, **kwargs, ) -> None: """Do something with `node` and/or `obj`.""" ``` ### Static/dynamic support Extensions can support both static and dynamic analysis of modules. If a module is scanned statically, your extension hooks will receive AST nodes (from the [ast][] module of the standard library). If the module is scanned dynamically, your extension hooks will receive [object nodes][griffe.ObjectNode]. Similarly, your hooks will receive a reference to the analysis agent that calls them, either a [Visitor][griffe.Visitor] or an [Inspector][griffe.Inspector]. To support static analysis, dynamic analysis, or both, you can therefore check the type of the received node or agent: ```python import ast import griffe class MyExtension(griffe.Extension): def on_instance( self, node: ast.AST | griffe.ObjectNode, obj: griffe.Object, agent: griffe.Visitor | griffe.Inspector, **kwargs, ) -> None: """Do something with `node` and/or `obj`.""" if isinstance(node, ast.AST): ... # Apply logic for static analysis. else: ... # Apply logic for dynamic analysis. ``` ```python import ast import griffe class MyExtension(Extension): def on_instance( self, node: ast.AST | griffe.ObjectNode, obj: griffe.Object, agent: griffe.Visitor | griffe.Inspector, **kwargs, ) -> None: """Do something with `node` and/or `obj`.""" if isinstance(agent, griffe.Visitor): ... # Apply logic for static analysis. else: ... # Apply logic for dynamic analysis. ``` The preferred method is to check the type of the received node rather than the agent. Since hooks also receive instantiated modules, classes, functions and attributes, most of the time you will not need to use the `node` argument other than for checking its type and deciding what to do based on the result. And since we always add `**kwargs` to the hooks' signatures, you can drop any parameter you don't use from the signature: ```python import griffe class MyExtension(Extension): def on_instance(self, obj: griffe.Object, **kwargs) -> None: """Do something with `obj`.""" ... ``` ### Visiting trees Extensions provide basic functionality to help you visit trees: - [`visit`][griffe.Extension.visit]: call `self.visit(node)` to start visiting an abstract syntax tree. - [`generic_visit`][griffe.Extension.generic_visit]: call `self.generic_visit(node)` to visit each subnode of a given node. - [`inspect`][griffe.Extension.inspect]: call `self.inspect(node)` to start visiting an object tree. Nodes contain references to the runtime objects, see [`ObjectNode`][griffe.ObjectNode]. - [`generic_inspect`][griffe.Extension.generic_inspect]: call `self.generic_inspect(node)` to visit each subnode of a given node. Calling `self.visit(node)` or `self.inspect(node)` will do nothing unless you actually implement methods that handle specific types of nodes: - for ASTs, methods must be named `visit_<node_type>` where `<node_type>` is replaced with the lowercase name of the node's class. For example, to allow visiting [`ClassDef`][ast.ClassDef] nodes, you must implement the `visit_classdef` method: ```python import ast from griffe import Extension class MyExtension(Extension): def visit_classdef(node: ast.ClassDef) -> None: # Do something with the node... ... # ...then visit the subnodes # (it only makes sense if you implement other methods # such as visit_functiondef or visit_assign for example). self.generic_visit(node) ``` See the [list of existing AST classes](#ast-nodes) to learn what method you can implement. - for object trees, methods must be named `inspect_<node_type>`, where `<node_type>` is replaced with the string value of the node's kind. The different kinds are listed in the [`ObjectKind`][griffe.ObjectKind] enumeration. For example, to allow inspecting coroutine nodes, you must implement the `inspect_coroutine` method: ```python from griffe import Extension, ObjectNode class MyExtension(Extension): def inspect_coroutine(node: ObjectNode) -> None: # Do something with the node... ... # ...then visit the subnodes if it makes sense. self.generic_inspect(node) ``` ### Triggering other extensions If your extension creates new objects, you might want to trigger the other enabled extensions on these object instances. To do this you can use [`agent.extensions.call`][griffe.Extensions.call]: ```python import ast import griffe class MyExtension(griffe.Extension): def on_node(self, node: ast.AST | griffe.ObjectNode, agent: griffe.Visitor | griffe.Inspector, **kwargs) -> None: # New object created for whatever reason. function = griffe.Function(...) # Trigger other extensions. agent.extensions.call("on_function_instance", node=node, agent=agent, func=function, **kwargs) ``` ### Extra data All Griffe objects (modules, classes, functions, attributes) can store additional (meta)data in their `extra` attribute. This attribute is a dictionary of dictionaries. The first layer is used as namespacing: each extension writes into its own namespace, or integrates with other projects by reading/writing in their namespaces, according to what they support and document. ```python import griffe self_namespace = "my_extension" class MyExtension(griffe.Extension): def on_instance(self, obj: griffe.Object, **kwargs) -> None: obj.extra[self_namespace]["some_key"] = "some_value" ``` For example, [mkdocstrings-python](https://mkdocstrings.github.io/python) looks into the `mkdocstrings` namespace for a `template` key. Extensions can therefore provide a custom template value by writing into `extra["mkdocstrings"]["template"]`: ```python import griffe self_namespace = "my_extension" mkdocstrings_namespace = "mkdocstrings" class MyExtension(griffe.Extension): def on_class_instance(self, cls: griffe.Class, **kwargs) -> None: obj.extra[mkdocstrings_namespace]["template"] = "my_custom_template" ``` [Read more about mkdocstrings handler extensions.](https://mkdocstrings.github.io/usage/handlers/#handler-extensions) ### Options Extensions can be made to support options. These options can then be passed from the [command-line](#on-the-command-line) using JSON, from Python directly, or from other tools like MkDocs, in `mkdocs.yml`. ```python import griffe class MyExtension(griffe.Extension): def __init__(self, option1: str, option2: bool = False) -> None: super().__init__() self.option1 = option1 self.option2 = option2 def on_attribute_instance(self, attr: griffe.Attribute, **kwargs) -> None: if self.option2: ... # Do something. ``` ### Logging To better integrate with Griffe and other tools in the ecosystem (notably MkDocs), use Griffe loggers to log messages: ```python import griffe logger = griffe.get_logger(__name__) class MyExtension(griffe.Extension): def on_module_members(self, mod: griffe.Module, **kwargs) -> None: logger.info("Doing some work on module %s and its members", mod.path) ``` ### Full example The following example shows how one could write a "dynamic docstrings" extension that dynamically imports objects that declare their docstrings dynamically, to improve support for such docstrings. The extension is configurable to run only on user-selected objects. Package structure (or just write your extension in a local script): ```tree ./ pyproject.toml src/ dynamic_docstrings/ __init__.py extension.py ``` ```python title="./src/dynamic_docstrings/extension.py" import ast import inspect import griffe logger = griffe.get_logger(__name__) class DynamicDocstrings(griffe.Extension): def __init__(self, object_paths: list[str] | None = None) -> None: self.object_paths = object_paths def on_instance( self, node: ast.AST | griffe.ObjectNode, obj: griffe.Object, agent: griffe.Visitor | griffe.Inspector, **kwargs, ) -> None: if isinstance(node, griffe.ObjectNode): return # Skip runtime objects, their docstrings are already right. if self.object_paths and obj.path not in self.object_paths: return # Skip objects that were not selected. # Import object to get its evaluated docstring. try: runtime_obj = griffe.dynamic_import(obj.path) docstring = runtime_obj.__doc__ except ImportError: logger.debug(f"Could not get dynamic docstring for {obj.path}") return except AttributeError: logger.debug(f"Object {obj.path} does not have a __doc__ attribute") return # Update the object instance with the evaluated docstring. docstring = inspect.cleandoc(docstring) if obj.docstring: obj.docstring.value = docstring else: obj.docstring = griffe.Docstring( docstring, parent=obj, docstring_parser=agent.docstring_parser, docstring_options=agent.docstring_options, ) ``` You can then expose this extension in the top-level module of your package: ```python title="./src/dynamic_docstrings/__init__.py" from dynamic_docstrings.extension import DynamicDocstrings __all__ = ["DynamicDocstrings"] ``` This will allow users to load and use this extension by referring to it as `dynamic_docstrings` (your Python package name). See [how to use extensions](#using-extensions) to learn more about how to load and use your new extension. ## AST nodes > <table style="border: none; background-color: unset;"><tbody><tr><td> > > - [`Add`][ast.Add] > - [`alias`][ast.alias] > - [`And`][ast.And] > - [`AnnAssign`][ast.AnnAssign] > - [`arg`][ast.arg] > - [`arguments`][ast.arguments] > - [`Assert`][ast.Assert] > - [`Assign`][ast.Assign] > - [`AsyncFor`][ast.AsyncFor] > - [`AsyncFunctionDef`][ast.AsyncFunctionDef] > - [`AsyncWith`][ast.AsyncWith] > - [`Attribute`][ast.Attribute] > - [`AugAssign`][ast.AugAssign] > - [`Await`][ast.Await] > - [`BinOp`][ast.BinOp] > - [`BitAnd`][ast.BitAnd] > - [`BitOr`][ast.BitOr] > - [`BitXor`][ast.BitXor] > - [`BoolOp`][ast.BoolOp] > - [`Break`][ast.Break] > - `Bytes`[^1] > - [`Call`][ast.Call] > - [`ClassDef`][ast.ClassDef] > - [`Compare`][ast.Compare] > - [`comprehension`][ast.comprehension] > - [`Constant`][ast.Constant] > - [`Continue`][ast.Continue] > - [`Del`][ast.Del] > - [`Delete`][ast.Delete] > > </td><td> > > - [`Dict`][ast.Dict] > - [`DictComp`][ast.DictComp] > - [`Div`][ast.Div] > - `Ellipsis`[^1] > - [`Eq`][ast.Eq] > - [`ExceptHandler`][ast.ExceptHandler] > - [`Expr`][ast.Expr] > - `Expression`[^1] > - `ExtSlice`[^2] > - [`FloorDiv`][ast.FloorDiv] > - [`For`][ast.For] > - [`FormattedValue`][ast.FormattedValue] > - [`FunctionDef`][ast.FunctionDef] > - [`GeneratorExp`][ast.GeneratorExp] > - [`Global`][ast.Global] > - [`Gt`][ast.Gt] > - [`GtE`][ast.GtE] > - [`If`][ast.If] > - [`IfExp`][ast.IfExp] > - [`Import`][ast.Import] > - [`ImportFrom`][ast.ImportFrom] > - [`In`][ast.In] > - `Index`[^2] > - `Interactive`[^3] > - [`Invert`][ast.Invert] > - [`Is`][ast.Is] > - [`IsNot`][ast.IsNot] > - [`JoinedStr`][ast.JoinedStr] > - [`keyword`][ast.keyword] > > </td><td> > > - [`Lambda`][ast.Lambda] > - [`List`][ast.List] > - [`ListComp`][ast.ListComp] > - [`Load`][ast.Load] > - [`LShift`][ast.LShift] > - [`Lt`][ast.Lt] > - [`LtE`][ast.LtE] > - [`Match`][ast.Match] > - [`MatchAs`][ast.MatchAs] > - [`match_case`][ast.match_case] > - [`MatchClass`][ast.MatchClass] > - [`MatchMapping`][ast.MatchMapping] > - [`MatchOr`][ast.MatchOr] > - [`MatchSequence`][ast.MatchSequence] > - [`MatchSingleton`][ast.MatchSingleton] > - [`MatchStar`][ast.MatchStar] > - [`MatchValue`][ast.MatchValue] > - [`MatMult`][ast.MatMult] > - [`Mod`][ast.Mod] > - `Module`[^3] > - [`Mult`][ast.Mult] > - [`Name`][ast.Name] > - `NameConstant`[^1] > - [`NamedExpr`][ast.NamedExpr] > - [`Nonlocal`][ast.Nonlocal] > - [`Not`][ast.Not] > - [`NotEq`][ast.NotEq] > - [`NotIn`][ast.NotIn] > - `Num`[^1] > > </td><td> > > - [`Or`][ast.Or] > - [`Pass`][ast.Pass] > - `pattern`[^3] > - [`Pow`][ast.Pow] > - `Print`[^4] > - [`Raise`][ast.Raise] > - [`Return`][ast.Return] > - [`RShift`][ast.RShift] > - [`Set`][ast.Set] > - [`SetComp`][ast.SetComp] > - [`Slice`][ast.Slice] > - [`Starred`][ast.Starred] > - [`Store`][ast.Store] > - `Str`[^1] > - [`Sub`][ast.Sub] > - [`Subscript`][ast.Subscript] > - [`Try`][ast.Try] > - `TryExcept`[^5] > - `TryFinally`[^6] > - [`Tuple`][ast.Tuple] > - [`UAdd`][ast.UAdd] > - [`UnaryOp`][ast.UnaryOp] > - [`USub`][ast.USub] > - [`While`][ast.While] > - [`With`][ast.With] > - [`withitem`][ast.withitem] > - [`Yield`][ast.Yield] > - [`YieldFrom`][ast.YieldFrom] > > </td></tr></tbody></table> [^1]: Deprecated since Python 3.8. [^2]: Deprecated since Python 3.9. [^3]: Not documented. [^4]: `print` became a builtin (instead of a keyword) in Python 3. [^5]: Now `ExceptHandler`, in the `handlers` attribute of `Try` nodes. [^6]: Now a list of expressions in the `finalbody` attribute of `Try` nodes. ## Next steps Extensions are a powerful mechanism to customize or enhance the data loaded by Griffe. But sometimes, all you need to do to improve the data is to make Griffe happy by following a few conventions. We therefore invite you to read our recommendations on [public APIs](recommendations/public-apis.md), [Python code best practices](recommendations/python-code.md) and [docstrings](recommendations/docstrings.md). �������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/checking.md����������������������������������������������������0000664�0001750�0001750�00000063222�14767006246�021467� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Checking APIs Griffe is able to compare two snapshots of your project to detect API breakages between the old and the new snapshot. By snapshot we mean a specific point in your Git history. For example, you can ask Griffe to compare your current code against a specific tag. ## Command-line ### Using Git By default, Griffe will compare the current code to the latest tag: ```console $ griffe check mypackage ``` To specify another Git reference to check against, use the `--against` or `-a` option: ```console $ griffe check mypackage -a 0.2.0 ``` You can specify a Git tag, commit (hash), or even a branch: Griffe will create a worktree at this reference in a temporary directory, and clean it up after finishing. If you want to also specify the *base* reference to use (instead of the current code), use the `--base` or `-b` option. Some examples: ```console $ griffe check mypackage -b HEAD -a 2.0.0 $ griffe check mypackage -b 2.0.0 -a 1.0.0 $ griffe check mypackage -b fix-issue-90 -a 1.2.3 $ griffe check mypackage -b 8afcfd6e ``` TIP: **Important:** Remember that the base is the most recent reference, and the one we compare it against is the oldest one. The package name you pass to `griffe check` must be found relative to the repository root. For Griffe to find packages in subfolders, pass the parent subfolder to the `--search` or `-s` option. Example for `src`-layouts: ```console $ griffe check -s src griffe ``` Example in a monorepo, within a deeper file tree: ```console $ griffe check -s back/services/identity-provider/src identity_provider ``` ### Using PyPI [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.1.0](../../insiders/changelog.md#1.1.0) It's also possible to directly **check packages from PyPI.org** (or other indexes configured through `PIP_INDEX_URL`). This feature is [available to sponsors only](../../insiders/index.md) and requires that you install Griffe with the `pypi` extra: ```console $ pip install griffe[pypi] ``` The command syntax is: ```console $ griffe check package_name -b project-name==2.0 -a project-name==1.0 ``` You can let Griffe guess the package name by passing an empty string: ```console $ griffe check "" -b project-name==2.0 -a project-name==1.0 ``` [PEP 508 version specifiers](https://peps.python.org/pep-0508/) are supported (`<`, `<=`, `!=`, `==`, `>=`, `>`, `~=`). For example, to compare v2 against the version just before it: ```console $ griffe check "" -b project-name==2.0 -a project-name<2.0 ``` Without a version specifier on the base reference, or without a base reference at all, Griffe will use the latest available version. The two following commands compare the latest version against v1: ```console $ griffe check "" -b project-name -a project-name==1.0 $ griffe check "" -a project-name==1.0 ``` Griffe will actually install packages in a cache directory. It means a few things: source distributions are supported, and only packages that are compatible with your current environment can be checked. ## Python API To programmatically check for API breaking changes, you have to load two snapshots of your code base, for example using our [`load_git()`][griffe.load_git] utility, and then passing them both to the [`find_breaking_changes()`][griffe.find_breaking_changes] function. This function will yield instances of [`Breakage`][griffe.Breakage]. It's up to you how you want to use these breakage instances. ```python import griffe my_pkg_v1 = griffe.load_git("my_pkg", ref="v1") my_pkg_v2 = griffe.load_git("my_pkg", ref="v2") for breaking_change in find_breaking_changes(my_pkg_v1, my_pkg_v2): print(breaking_change.explain()) ``` ## In CI It is of course possible to Griffe in CI (Continuous Integration) to make sure no breaking changes are introduced in pull/merge requests. ### GitHub {#ci-github} Here is a quick example on how to use Griffe in a GitHub workflow: ```yaml jobs: check-api: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Griffe requires that Git tags are available. - run: git fetch --depth=1 --tags - uses: actions/setup-python@v5 with: python-version: "3.11" # Install Griffe (use your preferred dependency manager). - run: pip install griffe - run: griffe check -ssrc your_package ``` The last step will fail the workflow if any breaking change is found. If you are part of [Insiders](../../insiders/index.md), you can format the output for GitHub, to enjoy GitHub annotations in PRs. See [GitHub format](#github) below. ## Detected breakages In this section, we will describe the breakages that Griffe detects, giving some code examples and hints on how to properly communicate breakages with deprecation messages before actually releasing them. Obviously, these explanations and the value of the hints we provide depend on your definition of what is a public Python API. There is no clear and generally agreed upon definition of "public Python API". A public Python API might vary from one project to another. In essence, your public API is what you say it is. However, we do have conventions like prefixing objects with an underscore to tell users these objects are part of the private API, or internals, and therefore should not be used. For the rest, Griffe can detect changes that *will* trigger immediate errors in your users code', and changes that *might* cause issues in your users' code. Although the latter sound less impactful, they do have a serious impact, because they can *silently* change the behavior of your users' code, leading to issues that are hard to detect, understand and fix. [Knowing that every change is a breaking change](https://xkcd.com/1172/), the more we detect and document (potentially) breaking changes in our changelogs, the better. ### Parameter moved > Positional parameter was moved. Moving the order of positional parameters can *silently* break your users' code. ```python title="before" # your code def greet(prefix, name): print(prefix + " " + name) # user's code greet("hello", "world") ``` ```python title="after" # your code def greet(name, prefix): print(prefix + " " + name) # user's code: no immediate error, broken behavior greet("hello", "world") ``` NOTE: Moving required parameters around is not really an API breakage, depending on our definition of API, since this won't raise immediate errors like `TypeError`. The function expects a number of arguments, and the developer passes it this same number of arguments: the contract is fulfilled. But parameters very often have specific meaning, and changing their order will *silently lead* (no immediate error) to incorrect behavior, potentially making it difficult to detect, understand and fix the issue. That is why it is important to warn developers about such changes. > TIP: **Hint** > If you often add, move or remove parameters, consider making them keyword-only, so that their order doesn't matter. > > ```python title="before" > def greet(*, prefix, name): > print(prefix + " " + name) > > greet(prefix="hello", name="world") > ``` > > ```python title="after" > def greet(*, name, prefix): > print(prefix + " " + name) > > # still working as expected > greet(prefix="hello", name="world") > ``` ### Parameter removed > Parameter was removed. Removing a parameter can immediately break your users' code. ```python title="before" # your code def greet(prefix, name): print(prefix + " " + name) # user's code greet("hello", "world") ``` ```python title="after" # your code def greet(name): print("hello " + name) # user's code: immediate error greet("hello", "world") # even with keyword parameters: immediate error greet(prefix="hello", name="world") ``` > TIP: **Hint** > Allow a deprecation period for the removed parameter by swallowing it in a variadic positional parameter, a variadic keyword parameter, or both. > > === "positional-only" > ```python title="before" > # your parameters are positional-only parameters (difficult deprecation) > def greet(prefix, name, /): > print(prefix + " " + name) > > greet("hello", "world") > ``` > > ```python title="after" > # swallow prefix using a variadic positional parameter > def greet(*args): > if len(args) == 2: > prefix, name = args > elif len(args) == 1: > prefix = None > name = args[0] > else: > raise ValueError("missing parameter 'name'") > if prefix is not None: > warnings.warn(DeprecationWarning, "prefix is deprecated") > print("hello " + name) > > # still working as expected > greet("hello", "world") > ``` > > === "keyword-only" > ```python title="before" > # your parameters are keyword-only parameters (easy deprecation) > def greet(*, prefix, name): > print(prefix + " " + name) > > greet(prefix="hello", name="world") > ``` > > ```python title="after" > # swallow prefix using a variadic keyword parameter > def greet(name, **kwargs): > prefix = kwargs.get("prefix", None) > if prefix is not None: > warnings.warn(DeprecationWarning, "prefix is deprecated") > print("hello " + name) > > # still working as expected > greet(prefix="hello", name="world") > ``` > > === "positional or keyword" > ```python title="before" > # your parameters are positional or keyword parameters (very difficult deprecation) > def greet(prefix, name): > print(prefix + " " + name) > > greet("hello", name="world") > ``` > > ```python title="after" > # no other choice than swallowing both forms... > # ignoring the deprecated parameter becomes quite complex > def greet(*args, **kwargs): > if len(args) == 2: > prefix, name = args > elif len(args) == 1: > prefix = None > name = args[0] > if "name" in kwargs: > name = kwargs["name"] > if "prefix" in kwargs: > prefix = kwargs["prefix"] > if prefix is not None: > warnings.warn(DeprecationWarning, "prefix is deprecated") > print("hello " + name) > > # still working as expected > greet("hello", "world") > greet("hello", name="world") > greet(prefix="hello", name="world") > ``` ### Parameter changed kind > Parameter kind was changed Changing the kind of a parameter to another (positional-only, keyword-only, positional or keyword, variadic positional, variadic keyword) can immediately break your users' code. ```python title="before" # your code def greet(name): print("hello " + name) def greet2(name): print("hello " + name) # user's code: all working fine greet("tim") greet(name="tim") greet2("tim") greet2(name="tim") ``` ```python title="after" # your code def greet(name, /): print("hello " + name) def greet2(*, name): print("hello " + name) # user's code: working as expected greet("tim") greet2(name="tim") # immediate error greet(name="tim") greet2("tim") ``` > TIP: **Hint** > Although it actually is a breaking change, changing your positional or keyword parameters' kind to keyword-only makes your public function more robust to future changes (forward-compatibility). > > For functions with lots of optional parameters, and a few (one or two) required parameters, it can be a good idea to accept the required parameters as positional or keyword, while accepting the optional parameters as keyword-only parameters: > > ```python > def greet(name, *, punctuation=False, bold=False, italic=False): > ... > > # simple cases are easy to write > greet("tim") > greet("tiff") > > # complex cases are never ambiguous > greet("tim", italic=True, bold=True) > greet(name="tiff", bold=True, punctuation=True) > ``` > > Positional-only parameters are useful in some specific cases, such as when a function takes two or more numeric values, and their order does not matter, and naming the parameters would not make sense: > > ```python > def multiply3(a, b, c, /): > return a * b * c > > # all the following are equivalent > multiply3(4, 2, 3) > multiply3(4, 3, 2) > multiply3(2, 3, 4) > # etc. > ``` ### Parameter changed default > Parameter default was changed Changing the default value of a parameter can *silently* break your users' code. ```python title="before" # your code def compute_something(value: int, to_float=True): value = ... if to_float: return float(value) return value # user's code: condition is entered if isinstance(compute_something(7), float): ... ``` ```python title="after" # your code def compute_something(value: int, to_float=False): value = ... if to_float: return float(value) return value # user's code: condition is not entered anymore if isinstance(compute_something(7), float): ... ``` NOTE: Changing default value of parameters is not really an API breakage, depending on our definition of API, since this won't raise immediate errors like `TypeError`. Not using the parameter still provides the argument with a default value: the contract is fulfilled. But default values very often have specific meaning, and changing them will *silently lead* (no immediate error) to incorrect behavior, potentially making it difficult to detect, understand and fix the issue. That is why it is important to warn developers about such changes. > TIP: **Hint** > Allow a deprecation period for the old default value by using a sentinel value to detect when the parameter wasn't used by the user: > > ```python title="in the coming release" > _sentinel = object() > > def compute_something(value: int, to_float=_sentinel): > value = ... > if to_float is _sentinel: > to_float = True > warnings.warn( > DeprecationWarning, > "default value of 'to_float' will change from True to False, " > "please provide 'to_float=True' if you want to retain the current behavior" > ) > if to_float: > return float(value) > return value > ``` > > In a later release you can remove the sentinel, the deprecation warning, and set `False` as default to `to_float`. > > ```python title="in a later release" > def compute_something(value: int, to_float=False): > value = ... > if to_float: > return float(value) > return value > ``` ### Parameter changed required > Parameter is now required Changing an optional parameter to a required one (by removing its default value) can immediately break your users' code. ```python title="before" # your code def greet(name, prefix="hello"): print(prefix + " " + name) # user's code greet("tiff") ``` ```python title="after" # your code def greet(name, prefix): print(prefix + " " + name) # user's code: immediate error greet("tiff") ``` > TIP: **Hint** > Allow a deprecation period for the default value by using a sentinel value to detect when the parameter wasn't used by the user: > > ```python title="in the coming release" > _sentinel = object() > > def greet(name, prefix=_sentinel): > if prefix is _sentinel: > prefix = "hello" > warnings.warn(DeprecationWarning, "'prefix' will become required in the next release") > print(prefix + " " + name) > ``` > > In a later release you can remove the sentinel, the deprecation warning, and the default value of `prefix`. > > ```python title="in a later release" > def greet(name, prefix): > print(prefix + " " + name) > ``` ### Parameter added required > Parameter was added as required Adding a new, required parameter can immediately break your users' code. ```python title="before" # your code def greet(name): print("hello " + name) # user's code greet("tiff") ``` ```python title="after" # your code def greet(name, prefix): print(prefix + " " + name) # user's code: immediate error greet("tiff") ``` > TIP: **Hint** > You can delay (or avoid) and inform your users about the upcoming breakage by temporarily (or permanently) providing a default value for the new parameter: > > ```python title="in the coming release" > def greet(name, prefix="hello"): > print(prefix + " " + name) > ``` ### Return changed type > Return types are incompatible WARNING: **Not yet supported!** Telling if a type construct is compatible with another one is not trivial, especially statically. Support for this will be implemented later. ### Object removed > Public object was removed Removing a public object from a module can immediately break your users' code. ```python title="before" # your/module.py special_thing = "hey" # user/module.py from your.module import special_thing # other/user/module.py from your import module print(module.special_thing) ``` ```python title="after" # user/module.py: import error from your.module import special_thing # other/user/module.py: attribute error from your import module print(module.special_thing) ``` > TIP: **Hint** > Allow a deprecation period by declaring a module-level `__getattr__` function that returns the given object while warning about its deprecation: > > ```python > def __getattr__(name): > if name == "special_thing": > warnings.warn(DeprecationWarning, "'special_thing' is deprecated and will be removed") > return "hey" > ``` ### Object changed kind > Public object points to a different kind of object Changing the kind (attribute, function, class, module) of a public object can *silently* break your users' code. ```python title="before" # your code class Factory: def __call__(self, ...): ... factory = Factory(...) # user's code: condition is entered if isinstance(factory, Factory): ... ``` ```python title="after" # your code class Factory: ... def factory(...): ... # user's code: condition is not entered anymore if isinstance(factory, Factory): ... ``` NOTE: Changing the kind of an object is not really an API breakage, depending on our definition of API, since this won't always raise immediate errors like `TypeError`. The object is still here and accessed: the contract is fulfilled. But developers sometimes rely on the kind of an object, so changing it will lead to incorrect behavior, potentially making it difficult to detect, understand and fix the issue. That is why it is important to warn developers about such changes. ### Attribute changed type > Attribute types are incompatible WARNING: **Not yet supported!** Telling if a type construct is compatible with another one is not trivial, especially statically. Support for this will be implemented later. ### Attribute changed value > Attribute value was changed Changing the value of an attribute can *silently* break your users' code. ```python title="before" # your code PY_VERSION = os.getenv("PY_VERSION") # user's code: condition is entered if PY_VERSION is None: ... ``` ```python title="after" # your code PY_VERSION = os.getenv("PY_VERSION", "3.8") # user's code: condition is not entered anymore if PY_VERSION is None: ... ``` NOTE: Changing the value of an attribute is not really an API breakage, depending on our definition of API, since this won't raise immediate errors like `TypeError`. The attribute is still here and accessed: the contract is fulfilled. But developers heavily rely on the value of public attributes, so changing it will lead to incorrect behavior, potentially making it difficult to detect, understand and fix the issue. That is why it is important to warn developers about such changes. TIP: **Hint** Make sure to document the change of value of the attribute in your changelog, particularly the previous and new range of values it can take. ### Class removed base > Base class was removed Removing a class from another class' bases can *silently* break your users' code. ```python title="before" # your code class A: ... class B: ... class C(A, B): ... # user's code: condition is entered if B in klass.__bases__: ... ``` ```python title="after" # your code class A: ... class B: ... class C(A): ... # user's code: condition is not entered anymore if B in klass.__bases__: ... ``` NOTE: Unless inherited members are lost in the process, removing a class base is not really an API breakage, depending on our definition of API, since this won't raise immediate errors like `TypeError`. The class is here, its members as well: the contract is fulfilled. But developers sometimes rely on the actual bases of a class, so changing them will lead to incorrect behavior, potentially making it difficult to detect, understand and fix the issue. That is why it is important to warn developers about such changes. ## Output style Griffe supports writing detected breakages in multiple formats, or styles. [](){#format-oneline} ### One-line - **CLI**: `-f oneline` / no flags - **API**: `check(...)` / `check(..., style="oneline")` / `check(..., style=ExplanationStyle.ONE_LINE)` This is the default format. Griffe will print each detected breakage on a single line: ```console exec="1" source="console" result="ansi" returncode="1" $ export FORCE_COLOR=1 # markdown-exec: hide $ griffe check griffe -ssrc -b0.46.0 -a0.45.0 ``` [](){#format-verbose} ### Verbose - **CLI**: `-f verbose` / `-v` - **API**: `check(..., style="verbose")` / `check(..., style=ExplanationStyle.VERBOSE)` / `check(..., verbose=True)` Depending on the detected breakages, the lines might be hard to read (being too compact), so `griffe check` also accepts a `--verbose` or `-v` option to add some space to the output: ```console exec="1" source="console" result="ansi" returncode="1" $ export FORCE_COLOR=1 # markdown-exec: hide $ griffe check griffe -ssrc -b0.46.0 -a0.45.0 --verbose ``` [](){#format-markdown} ### Markdown [:octicons-tag-24: Insiders 1.0.0](../../insiders/changelog.md#1.0.0) - **CLI**: `-f markdown` - **API**: `check(..., style="markdown")` / `check(..., style=ExplanationStyle.MARKDOWN)` The Markdown format is adapted for changelogs. It doesn't show the file and line number, and instead prints out the complete path of your API objects. With a bit of automation, you will be able to automatically insert a summary of breaking changes in your changelog entries. ```md exec="1" source="tabbed-left" tabs="Output|Result" - `griffe.loader.GriffeLoader.resolve_aliases(only_exported)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.loader.GriffeLoader.resolve_aliases(only_exported)`: *Parameter default was changed*: `True` -> `None` - `griffe.loader.GriffeLoader.resolve_aliases(only_known_modules)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.loader.GriffeLoader.resolve_aliases(only_known_modules)`: *Parameter default was changed*: `True` -> `None` - `griffe.loader.GriffeLoader.resolve_aliases(max_iterations)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.loader.GriffeLoader.resolve_module_aliases(only_exported)`: *Parameter was removed* - `griffe.loader.GriffeLoader.resolve_module_aliases(only_known_modules)`: *Parameter was removed* - `griffe.git.tmp_worktree(commit)`: *Parameter was removed* - `griffe.git.tmp_worktree(repo)`: *Positional parameter was moved*: position: from 2 to 1 (-1) - `griffe.git.load_git(commit)`: *Parameter was removed* - `griffe.git.load_git(repo)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(submodules)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(try_relative_path)`: *Parameter was removed* - `griffe.git.load_git(extensions)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(search_paths)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(docstring_parser)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(docstring_options)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(lines_collection)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(modules_collection)`: *Parameter kind was changed*: positional or keyword -> keyword-only - `griffe.git.load_git(allow_inspection)`: *Parameter kind was changed*: positional or keyword -> keyword-only ``` [](){#format-github} ### GitHub [:octicons-tag-24: Insiders 1.0.0](../../insiders/changelog.md#1.0.0) - **CLI**: `-f github` - **API**: `check(..., style="github")` / `check(..., style=ExplanationStyle.GITHUB)` When running `griffe check` in CI, you can enable GitHub's annotations thanks to the GitHub output style. Annotations are displayed on specific lines of code. They are visible in the Checks tab. When you create an annotation for a file that is part of the pull request, the annotations are also shown in the Files changed tab. /// tab | Files changed tab ![gha_annotations_2](../../img/gha_annotations_2.png) /// /// tab | Checks tab ![gha_annotations_1](../../img/gha_annotations_1.png) /// ```console % python -m griffe check -fgithub -ssrc griffe ::warning file=src/griffe/finder.py,line=58,title=Package.name::Attribute value was changed: `name` -> unset ::warning file=src/griffe/finder.py,line=60,title=Package.path::Attribute value was changed: `path` -> unset ::warning file=src/griffe/finder.py,line=62,title=Package.stubs::Attribute value was changed: `stubs` -> `None` ::warning file=src/griffe/finder.py,line=75,title=NamespacePackage.name::Attribute value was changed: `name` -> unset ::warning file=src/griffe/finder.py,line=77,title=NamespacePackage.path::Attribute value was changed: `path` -> unset ``` ## Next steps If you are using a third-party library to mark objects as public, or if you follow conventions different than the one Griffe understands, you might get false-positives, or breaking changes could go undetected. In that case, you might be interested in [extending](extending.md) how Griffe loads API data to support these third-party libraries or other conventions. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users/navigating.md��������������������������������������������������0000664�0001750�0001750�00000066237�14767006246�022054� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Navigating APIs Griffe loads API data into data models. These models provide various attributes and methods to access or update specific fields. The different models are: - [`Module`][griffe.Module], representing Python modules; - [`Class`][griffe.Class], representing Python classes; - [`Function`][griffe.Function], representing Python functions and class methods; - [`Attribute`][griffe.Attribute], representing object attributes that weren't identified as modules, classes or functions; - [`Alias`][griffe.Alias], representing indirections such as imported objects or class members inherited from parent classes. When [loading an object](loading.md), Griffe will give you back an instance of one of these models. A few examples: ```python >>> import griffe >>> type(griffe.load("markdown")) <class '_griffe.models.Module'> >>> type(griffe.load("markdown.core.Markdown")) <class '_griffe.models.Class'> >>> type(griffe.load("markdown.Markdown")) <class '_griffe.models.Alias'> >>> type(griffe.load("markdown.core.markdown")) <class '_griffe.models.Function'> >>> type(griffe.load("markdown.markdown")) <class '_griffe.models.Alias'> >>> type(griffe.load("markdown.Markdown.references")) <class '_griffe.models.Attribute'> ``` However deep the object is, Griffe loads the entire package. It means that in all the cases above, Griffe loaded the whole `markdown` package. The model instance Griffe gives you back is therefore part of a tree that you can navigate. ## Moving up: parents Each object holds a reference to its [`parent`][griffe.Object.parent] (except for the top-level module, for which the parent is `None`). Shortcuts are provided to climb up directly to the parent [`module`][griffe.Object.module], or the top-level [`package`][griffe.Object.package]. As we have seen in the [Loading chapter](loading.md), Griffe stores all loaded modules in a modules collection; this collection can be accessed too, through the [`modules_collection`][griffe.Object.modules_collection] attribute. ## Moving down: members To access an object's members, there are a few options: - Access to regular members through the [`members`][griffe.Object.members] attribute, which is a dictionary. The keys are member names, the values are Griffe models. ```pycon >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown.members["Markdown"] Alias('Markdown', 'markdown.core.Markdown') >>> markdown.members["core"].members["Markdown"] Class('Markdown', 46, 451) ``` - Access to both regular and inherited members through the [`all_members`][griffe.Object.all_members] attribute, which is a dictionary again. See [Inherited members](#inherited-members). - Convenient dictionary-like item access, thanks to the subscript syntax `[]`. With this syntax, you will not only be able to chain accesses, but also merge them into a single access by using dot-separated paths to objects: ```pycon >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown["core"]["Markdown"] # chained access Class('Markdown', 46, 451) >>> markdown["core.Markdown"] # merged access Class('Markdown', 46, 451) ``` The dictionary-like item access also accepts tuples of strings. So if for some reason you don't have a string equal to `"core.Markdown"` but a tuple equal to `("core", "Markdown")` (for example obtained from splitting another string), you can use it too: ```pycon >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown[("core", "Markdown")] # tuple access Class('Markdown', 46, 451) >>> # Due to the nature of the subscript syntax, >>> # you can even use implicit tuples. >>> markdown["core", "Markdown"] Class('Markdown', 46, 451) ``` - Less convenient, but safer access to members while the object tree is being built (while a package is still being loaded), using the [`get_member()`][griffe.GetMembersMixin.get_member] method. ```pycon >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown.get_member("core.Markdown") Class('Markdown', 46, 451) ``` In particular, Griffe extensions should always use `get_member` instead of the subscript syntax `[]`. The `get_member` method only looks into regular members, while the subscript syntax looks into inherited members too (for classes), which cannot be correctly computed until a package is fully loaded (which is generally not the case when an extension is running). - In addition to this, models provide the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes] or [`modules`][griffe.Object.modules] attributes, which return only members of the corresponding kind. These attributes are computed dynamically each time (they are Python properties). The same way members are accessed, they can also be set: - Dictionary-like item assignment: `markdown["thing"] = ...`, also supporting dotted-paths and string tuples. This will (re)assign only regular members: inherited members (classes only) are re-computed everytime they are accessed. - Safer method for extensions: `markdown.set_member("thing", ...)`, also supporting dotted-paths and string tuples. - Regular member assignment: `markdown.members["thing"] = ...`. **This is not recommended, as the assigned member's `parent` attribute will not be automatically updated.** ...and deleted: - Dictionary-like item deletion: `del markdown["thing"]`, also supporting dotted-paths and string tuples. This will delete only regular members: inherited members (classes only) are re-computed everytime they are accessed. - Safer method for extensions: `markdown.del_member("thing")`, also supporting dotted-paths and string tuples. - Regular member deletion: `del markdown.members["thing"]`. **This is not recommended, as the [`aliases`][griffe.Object.aliases] attribute of other objects in the tree will not be automatically updated.** ### Inherited members Griffe supports class inheritance, both when visiting and inspecting modules. To access members of a class that are inherited from base classes, use the [`inherited_members`][griffe.Object.inherited_members] attribute. Everytime you access inherited members, the base classes of the given class will be resolved, then the MRO (Method Resolution Order) will be computed for these base classes, and a dictionary of inherited members will be built. Make sure to store the result in a variable to avoid re-computing it everytime (you are responsible for the caching part). Also make sure to only access `inherited_members` once everything is loaded by Griffe, to avoid computing things too early. Don't try to access inherited members in extensions, while visiting or inspecting modules. Inherited members are aliases that point at the corresponding members in parent classes. These aliases will have their [`inherited`][griffe.Alias.inherited] attribute set to true. **Important:** only classes from already loaded packages will be used when computing inherited members. This gives users control over how deep into inheritance to go, by pre-loading packages from which you want to inherit members. For example, if `package_c.ClassC` inherits from `package_b.ClassB`, itself inheriting from `package_a.ClassA`, and you want to load `ClassB` members only: ```python import griffe loader = griffe.GriffeLoader() # note that we don't load package_a loader.load("package_b") loader.load("package_c") ``` If a base class cannot be resolved during computation of inherited members, Griffe logs a DEBUG message. If you want to access all members at once (both declared and inherited), use the [`all_members`][griffe.Object.all_members] attribute. If you want to access only declared members, use the [`members`][griffe.Object] attribute. Accessing the [`attributes`][griffe.Object.attributes], [`functions`][griffe.Object.functions], [`classes`][griffe.Object.classes] or [`modules`][griffe.Object.modules] attributes will trigger inheritance computation, so make sure to only access them once everything is loaded by Griffe. Don't try to access inherited members in extensions, while visiting or inspecting modules. #### Limitations Currently, there are three limitations to our class inheritance support: 1. when visiting (static analysis), some objects are not yet properly recognized as classes, for example named tuples. If you inherit from a named tuple, its members won't be added to the inherited members of the inheriting class. ```python MyTuple = namedtuple("MyTuple", "attr1 attr2") class MyClass(MyTuple): ... ``` 2. when visiting (static analysis), subclasses using the same name as one of their parent classes will prevent Griffe from computing the MRO and therefore the inherited members. To circumvent that, give a different name to your subclass: ```python from package import SomeClass # instead of class SomeClass(SomeClass): ... # do class SomeOtherClass(SomeClass): ... ``` 3. when inspecting (dynamic analysis), ephemeral base classes won't be resolved, and therefore their members won't appear in child classes. To circumvent that, assign these dynamic classes to variables: ```python # instead of class MyClass(namedtuple("MyTuple", "attr1 attr2")): ... # do MyTuple = namedtuple("MyTuple", "attr1 attr2") class MyClass(MyTuple): ... ``` We will try to lift these limitations in the future. ## Aliases Aliases represent indirections, such as objects imported from elsewhere, attributes, or methods inherited from parent classes. They are pointers to the object they represent. The path of the object they represent is stored in their [`target_path`][griffe.Alias.target_path] attribute. Once they are resolved, the target object can be accessed through their [`target`][griffe.Alias.target] attribute. Aliases can be found in objects' members. Each object can also access its own aliases (the aliases pointing at it) through its [`aliases`][griffe.Object.aliases] attribute. This attribute is a dictionary whose keys are the aliases paths and values are the aliases themselves. Most of the time, aliases simply act as proxies to their target objects. For example, accessing the `docstring` of an alias will simply return the docstring of the object it targets. Accessing fields on aliases will trigger their resolution. If they are already resolved (their `target` attribute is set to the target object), the field is returned. If they are not resolved, their target path will be looked up in the modules collection, and if it is found, the object at this location will be assigned to the alias' `target` attribute. If it isn't found, an [`AliasResolutionError`][griffe.AliasResolutionError] exception will be raised. Since merely accessing an alias field can raise an exception, it is often useful to check if an object is an alias before accessing its fields. There are multiple ways to check if an object is an alias: - using the `is_alias` boolean ([`Object.is_alias`][griffe.Object.is_alias], [`Alias.is_alias`][griffe.Alias.is_alias]), which won't trigger resolution - using `isinstance` to check if the object is an instance of [`Alias`][griffe.Alias] ```pycon >>> import griffe >>> load = griffe.load("griffe.load") >>> load.is_alias True >>> isinstance(load, griffe.Alias) True ``` The [`kind`][griffe.Alias.kind] of an alias will only return [`ALIAS`][griffe.Kind.ALIAS] if the alias is not resolved and cannot be resolved within the current modules collection. You can of course also catch any raised exception with a regular try/except block: ```python try: print(obj.source) except griffe.AliasResolutionError: pass ``` To check if an alias is already resolved, you can use its [`resolved`][griffe.Alias.resolved] attribute. ### Alias chains Aliases can be chained. For example, if module `a` imports `X` from module `b`, which itself imports `X` from module `c`, then `a.X` is an alias to `b.X` which is an alias to `c.X`: `a.X` -> `b.X` -> `c.X`. To access the final target directly, you can use the [`final_target`][griffe.Alias.final_target] attribute. Most alias properties that act like proxies actually fetch the final target rather than the next one to return the final field. Sometimes, when a package makes use of complicated imports (wildcard imports from parents and submodules), or when runtime objects are hard to inspect, it is possible to end up with a cyclic chain of aliases. You could for example end up with a chain like `a.X` -> `b.X` -> `c.X` -> `a.X`. In this case, the alias *cannot* be resolved, since the chain goes in a loop. Griffe will raise a [`CyclicAliasError`][griffe.CyclicAliasError] when trying to resolve such cyclic chains. Aliases chains are never partially resolved: either they are resolved down to their final target, or none of their links are resolved. ## Object kind The kind of an object (module, class, function, attribute or alias) can be obtained in several ways. - With the [`kind`][griffe.Object.kind] attribute and the [`Kind`][griffe.Kind] enumeration: `obj.kind is Kind.MODULE`. - With the [`is_kind()`][griffe.Object.is_kind] method: - `obj.is_kind(Kind.MODULE)` - `obj.is_kind("class")` - `obj.is_kind({"function", Kind.ATTRIBUTE})` When given a set of kinds, the method returns true if the object is of one of the given kinds. - With the [`is_module`][griffe.Object.is_module], [`is_class`][griffe.Object.is_class], [`is_function`][griffe.Object.is_function], [`is_attribute`][griffe.Object.is_attribute], and [`is_alias`][griffe.Object.is_alias] attributes. Additionally, it is possible to check if an object is a sub-kind of module, with the following attributes: - [`is_init_module`][griffe.Object.is_init_module], for `__init__.py` modules - [`is_package`][griffe.Object.is_package], for top-level packages - [`is_subpackage`][griffe.Object.is_subpackage], for non-top-level packages - [`is_namespace_package`][griffe.Object.is_namespace_package], for top-level [namespace packages](https://packaging.python.org/en/latest/guides/packaging-namespace-packages/) - [`is_namespace_subpackage`][griffe.Object.is_namespace_subpackage], for non-top-level namespace packages Finally, additional [`labels`][griffe.Object.labels] are attached to objects to further specify their kind. The [`has_labels()`][griffe.Object.has_labels] method can be used to check if an object has several specific labels. ## Object location An object is identified by its [`path`][griffe.Object.path], which is its location in the object tree. The path is composed of all the parent names and the object name, separated by dots, for example `mod.Class.meth`. This `path` is the [`canonical_path`][griffe.Object.canonical_path] on regular objects. For aliases however, the `path` is *where they are imported* while the canonical path is *where they come from*. Example: ```python # pkg1.py from pkg2 import A as B ``` ```pycon >>> import griffe >>> B = griffe.load("pkg1.B") >>> B.path 'pkg1.B' >>> B.canonical_path 'pkg2.A' ``` ### Source Information on the actual source code of objects is available through the following attributes: - [`filepath`][griffe.Object.filepath], the absolute path to the module the object appears in, for example `~/project/src/pkg/mod.py` - [`relative_filepath`][griffe.Object.relative_filepath], the relative path to the module, compared to the current working directory, for example `src/pkg/mod.py` - [`relative_package_filepath`][griffe.Object.relative_package_filepath], the relative path to the module, compared to the parent of the top-level package, for example `pkg/mod.py` - [`lineno`][griffe.Object.lineno] and [`endlineno`][griffe.Object.endlineno], the starting and ending line numbers of the object in the source - [`lines`][griffe.Object.lines], the lines of code defining the object (or importing the alias) - [`source`][griffe.Object.source], the source lines concatenated as a single multiline string Each object holds a reference to a [`lines_collection`][griffe.Object.lines_collection]. Similar to the modules collection, this lines collection is a dictionary whose keys are module file-paths and values are their contents as list of lines. The lines collection is populated by the loader. ## Object visibility Each object has fields that are related to their visibility within the API. - [`is_public`][griffe.Object.is_public]: whether this object is public (destined to be consumed by your users). For module-level objects, Griffe considers that the object is public if: - it is listed in its parent module's `__all__` attribute - or if its parent module does not declare `__all__`, and the object doesn't have a private name, and the object is not imported from elsewhere ```python # package1/__init__.py from package2 import A # not public from package1 import submodule # not public b = 0 # public _c = 1 # not public __d = 2 # not public def __getattr__(name: str): # public ... ``` For class-level objects, Griffe considers that the object is public if the object doesn't have a private name, and the object is not imported from elsewhere. ```python # package1/__init__.py class A: from package1.module import X # not public from package2 import Y # not public b = 0 # public _c = 1 # not public __d = 2 # not public def __eq__(self, other): # public ... ``` - [`is_deprecated`][griffe.Object.is_deprecated]: whether this object is deprecated and shouldn't be used. - [`is_special`][griffe.Object.is_special]: whether this object has a special name like `__special__` - [`is_private`][griffe.Object.is_private]: whether this object has a private name like `_private` or `__private`, but not `__special__` - [`is_class_private`][griffe.Object.is_class_private]: whether this object has a class-private name like `__private` and is a member of a class Since `is_private` only checks the name of the object, it is not mutually exclusive with `is_public`. It means an object can return true for both `is_public` and `is_private`. We invite Griffe users to mostly rely on `is_public` and `not is_public`. It is possible to force `is_public` and `is_deprecated` to return true or false by setting the [`public`][griffe.Object.public] and [`deprecated`][griffe.Object.deprecated] fields respectively. These fields are typically set by extensions that support new ways of marking objects as public or deprecated. ## Imports/exports Modules and classes populate their [`imports`][griffe.Object.imports] field with names that were imported from other modules. Similarly, modules populate their [`exports`][griffe.Object.exports] field with names that were exported by being listed into the module's `__all__` attribute. Each object then provides then [`is_imported`][griffe.Object.is_imported] and [`is_exported`][griffe.Object.is_exported] fields, which tell if an object was imported or exported respectively. Additionally, objects also provide an [`is_wildcard_exposed`][griffe.Object.is_wildcard_exposed] field that tells if an object is exposed to wildcard imports, i.e. will be imported when another module does `from this_module import *`. ## Docstrings Each object has an optional [`docstring`][griffe.Object.docstring] attached to it. To check whether it has one without comparing against `None`, the two following fields can be used: - [`has_docstring`][griffe.Object.has_docstring]: whether this object has a docstring (even empty) - [`has_docstrings`][griffe.Object.has_docstrings]: same thing, but recursive; whether this object or any of its members has a docstring (even empty) [Docstrings][griffe.Docstring] provide their cleaned-up [`value`][griffe.Docstring.value] (de-indented string, stripped from leading and trailing newlines), as well as their starting and ending line numbers with [`lineno`][griffe.Docstring.lineno] and [`endlineno`][griffe.Docstring.endlineno]. Docstrings can be parsed against several [docstring-styles](../../reference/docstrings.md), which are micro-formats that allow documenting things such as parameters, returned values, raised exceptions, etc.. When loading a package, it is possible to specify the docstring style to attach to every docstring (see the `docstring_parser` parameter of [`griffe.load`][griffe.load]). Accessing the [`parsed`][griffe.Docstring.parsed] field of a docstring will use this style to parse the docstring and return a list of [docstring sections][advanced-api-sections]. Each section has a `value` whose shape depends on the section kind. For example, parameter sections have a list of parameter representations as value, while a text section only has a string as value. After a package is loaded, it is still possible to change the style used for specific docstrings by either overriding their [`parser`][griffe.Docstring.parser] and [`parser_options`][griffe.Docstring.parser_options] attributes, or by calling their [`parse()`][griffe.Docstring.parse] method with a different style: ```pycon >>> import griffe >>> markdown = griffe.load("markdown", docstring_parser="google") >>> markdown["Markdown"].docstring.parse("numpy") [...] ``` Do note, however, that the `parsed` attribute is cached, and won't be reset when overriding the `parser` or `parser_options` values. Docstrings have a [`parent`][griffe.Docstring.parent] field too, that is a reference to their respective module, class, function or attribute. ## Model-specific fields Models have most fields in common, but also have specific fields. ### Modules - [`imports_future_annotations`][griffe.Module.imports_future_annotations]: Whether the module imports [future annotations](https://peps.python.org/pep-0563/), which changes the way we parse type annotations. - [`overloads`][griffe.Module.overloads]: A dictionary to store overloads for module-level functions. ### Classes - [`bases`][griffe.Class.bases]: A list of class bases in the form of [expressions][griffe.Expr]. - [`resolved_bases`][griffe.Class.resolved_bases]: A list of class bases, in the form of [Class][griffe.Class] objects. Only the bases that were loaded are returned, the others are discarded. - [`mro()`][griffe.Class.mro]: A method to compute the Method Resolution Order in the form of a list of [Class][griffe.Class] objects. - [`overloads`][griffe.Class.overloads]: A dictionary to store overloads for class-level methods. - [`decorators`][griffe.Class.decorators]: The [decorators][griffe.Decorator] applied to the class. - [`parameters`][griffe.Class.parameters]: The [parameters][griffe.Parameters] of the class' `__init__` method, if any. ### Functions - [`decorators`][griffe.Function.decorators]: The [decorators][griffe.Decorator] applied to the function. - [`overloads`][griffe.Function.overloads]: The overloaded signatures of the function. - [`parameters`][griffe.Function.parameters]: The [parameters][griffe.Parameters] of the function. - [`returns`][griffe.Function.returns]: The type annotation of the returned value, in the form of an [expression][griffe.Expr]. The `annotation` field can also be used, for compatibility with attributes. ### Attributes - [`annotation`][griffe.Attribute.annotation]: The type annotation of the attribute, in the form of an [expression][griffe.Expr]. - [`value`][griffe.Attribute.value]: The value of the attribute, in the form of an [expression][griffe.Expr]. - [`deleter`][griffe.Attribute.deleter]: The property deleter. - [`setter`][griffe.Attribute.setter]: The property setter. ### Alias - [`alias_lineno`][griffe.Alias.alias_lineno]: The alias line number (where the object is imported). - [`alias_endlineno`][griffe.Alias.alias_endlineno]: The alias ending line number (where the object is imported). - [`target`][griffe.Alias.target]: The alias target (a module, class, function or attribute). - [`target_path`][griffe.Alias.target_path]: The path of the alias target, as a string. - [`wildcard`][griffe.Alias.wildcard]: Whether this alias represents a wildcard import, and if so from which module. - [`resolve_target()`][griffe.Alias.resolve_target]: A method that resolves the target when called. ## Expressions When parsing source code, Griffe builds enhanced ASTs for type annotations, decorators, parameter defaults, attribute values, etc. These "expressions" are very similar to what Python's [ast][] module gives you back when parsing source code, with a few differences: attributes like `a.b.c.` are flattened, and names like `a` have a parent object attached to them, a Griffe object, allowing to resolve this name to its full path given the scope of its parent. You can write some code below and print annotations or attribute values with [Rich]'s pretty printer to see how expressions look like. ```pyodide install="griffe,rich" theme="tomorrow,dracula" from griffe import temporary_visited_module from rich.pretty import pprint code = """ from dataclasses import dataclass from random import randint @dataclass class Bar: baz: int def get_some_baz() -> int: return randint(0, 10) foo: Bar = Bar(baz=get_some_baz()) """ with temporary_visited_module(code) as module: pprint(module["foo"].annotation) pprint(module["foo"].value) ``` Ultimately, these expressions are what allow downstream tools such as [mkdocstrings' Python handler][mkdocstrings-python] to render cross-references to every object it knows of, coming from the current code base or loaded from object inventories (objects.inv files). During static analysis, these expressions also allow analyzing decorators, dataclass fields, and many more things in great detail, and in a robust manner, to build third-party libraries support in the form of [Griffe extensions](extending.md). To learn more about expressions, read their [API reference](../../reference/api/expressions.md). ### Modernization [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.2.0](../../insiders/changelog.md#1.2.0) The Python language keeps evolving, and often library developers must continue supporting a few minor versions of Python. Therefore they cannot use some features that were introduced in the latest versions. Yet this doesn't mean they can't enjoy latest features in their own docs: Griffe allows to "modernize" expressions, for example by replacing `typing.Union` with PEP 604 type unions `|`. Thanks to this, downstream tools like [mkdocstrings][mkdocstrings-python] can automatically transform type annotations into their modern equivalent. This improves consistency in your docs, and shows users how to use your code with the latest features of the language. To modernize an expression, simply call its [`modernize()`][griffe.Expr.modernize] method. It returns a new, modernized expression. Some parts of the expression might be left unchanged, so be careful if you decide to mutate them. Modernizations applied: - `typing.Dict[A, B]` becomes `dict[A, B]` - `typing.List[A]` becomes `list[A]` - `typing.Set[A]` becomes `set[A]` - `typing.Tuple[A]` becomes `tuple[A]` - `typing.Union[A, B]` becomes `A | B` - `typing.Optional[A]` becomes `A | None` ## Next steps In this chapter we saw many of the fields that compose our models, and how and why to use them. Now you might be interested in [extending](extending.md) or [serializing](serializing.md) the API data, or [checking for API breaking changes](checking.md). [mkdocstrings-python]: https://mkdocstrings.github.io/python [rich]: https://rich.readthedocs.io/en/stable/ �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/guide/users.md�������������������������������������������������������������0000664�0001750�0001750�00000007054�14767006246�017715� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# User guide Welcome to the Griffe user guide! ## Manipulating APIs The following topics will guide you through the various methods Griffe offers for exploring and exploiting Python APIs. <div class="grid cards" markdown> - :material-cube-scan:{ .lg .middle } **Loading** --- Griffe can find packages and modules to scan them statically or dynamically and extract API-related information. [:octicons-arrow-right-24: Learn how to load data](users/loading.md) - :material-navigation-variant-outline:{ .lg .middle } **Navigating** --- Griffe exposes the extracted API information into data models, making it easy to navigate your API. [:octicons-arrow-right-24: Learn how to navigate data](users/navigating.md) - :material-code-json:{ .lg .middle } **Serializing** --- Griffe can serialize your API data into JSON, for other tools to navigate or manipulate it. [:octicons-arrow-right-24: Learn how to serialize data](users/serializing.md) - :material-target:{ .lg .middle } **Checking** --- Griffe can compare snapshots of the same API to find breaking changes. [:octicons-arrow-right-24: Learn how to detect and handle breaking changes](users/checking.md) - :material-puzzle-plus:{ .lg .middle } **Extending** --- API data can be augmented or modified thanks to Griffe's extension system. [:octicons-arrow-right-24: Learn how to write and use extensions](users/extending.md) </div> ## Recommendations These topics explore the user side: how to write code to better integrate with Griffe. <div class="grid cards" markdown> - :material-gift-open:{ .lg .middle } **Public API** --- See our recommendations for exposing public APIs to your users. [:octicons-arrow-right-24: See our public API recommendations](users/recommendations/public-apis.md) - :material-star-face:{ .lg .middle } **Python code best practices** --- See our best practices for writing Python code. [:octicons-arrow-right-24: See our best practices](users/recommendations/python-code.md) - :material-text:{ .lg .middle } **Docstrings** --- Griffe supports multiple docstring styles. Learn about these different styles, and see our recommendations to write docstrings. [:octicons-arrow-right-24: See our docstring recommendations](users/recommendations/docstrings.md) </div> ## How-to These how-tos will show you how to achieve specific things with Griffe. <div class="grid cards" markdown> - :octicons-ai-model-24:{ .lg .middle } **Parse docstrings** --- Griffe can be used as a docstring-parsing library. [:octicons-arrow-right-24: See how to parse docstrings](users/how-to/parse-docstrings.md) - **@ Support custom decorators** --- Griffe will rarely support custom decorators through static analysis, but you can easily write extensions to do so. [:octicons-arrow-right-24: See how to support custom decorators](users/how-to/support-decorators.md) - :material-select:{ .lg .middle } **Selectively inspect objects** --- Sometimes static analysis is not enough, so you might want to use dynamic analysis (inspection) on certain objects. [:octicons-arrow-right-24: See how to selectively inspect objects](users/how-to/selectively-inspect.md) - :material-select:{ .lg .middle } **Set objects' docstring style** --- Sometimes the wrong docstring styles are attached to objects. You can fix this with a few different methods. [:octicons-arrow-right-24: See how to set the correct docstring styles on objects](users/how-to/set-docstring-styles.md) </div> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference.md���������������������������������������������������������������0000664�0001750�0001750�00000000547�14767006246�017415� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Reference Our reference pages contain comprehensive information on various aspects of Griffe, such as its Command Line Interface (CLI), its Application Programming Interface (API), or its docstring parsers. - [Command Line Interface](reference/cli.md) - [Application Programming Interface](reference/api.md) - [Docstring parsers](reference/docstrings.md) ���������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/alternatives.md������������������������������������������������������������0000664�0001750�0001750�00000006444�14767006246�020162� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Alternatives Similar projects exist in the ecosystem. They generally allow to extract API data from Python source, or to build a representation of the Python source or Python API. Some of them also allow to compare representations to find breaking changes. ## Docspec [Docspec](https://github.com/NiklasRosenstein/python-docspec) is a JSON object specification for representing API documentation of programming languages. While in it's current form it is targeting Python APIs, it is intended to be able to represent other programming languages in the future as well. The repository contains two projects, docspec and docspec-python. **docspec** is the reference implementation for reading/writing the JSON format and API for representing API objects in memory. **docspec-python** is a parser for Python packages and modules based on lib2to3 producing docspec API object representations. ## Frappucino [Frappucino](https://github.com/Carreau/frappuccino) allows you to make sure you haven't broken your API, by first taking an imprint of your API at one point in time and then compare it to the current project state. The goal is to warn you when incompatible changes have been introduced, and to list these changes. ## Other related projects The work of [@tristanlatr](https://github.com/tristanlatr) is worth checking out, notably his [ast-nodes](https://github.com/tristanlatr/ast-nodes) and [astuce](https://github.com/tristanlatr/astuce) projects, which aim at providing lower-level Python AST utilities to help implementing API data extraction with powerful inference. Tristan is [advocating for more interoperability](https://github.com/mkdocstrings/griffe/discussions/287) between Docspec, Griffe and his own projects. We should also mention our own simplified "Griffe" variants for other programming languages, such as [Griffe TypeDoc](https://mkdocstrings.github.io/griffe-typedoc/), which extracts API data from TypeScript sources thanks to [TypeDoc](https://typedoc.org/), and builds Python objects from it. --- The following projects are more than API data extraction tools, but deserve being mentioned. ### Papyri [Papyri](https://github.com/jupyter/papyri) is a set of tools to build, publish (future functionality - to be done), install and render documentation within IPython and Jupyter. Papyri [has considered using Griffe in the past](https://github.com/jupyter/papyri/issues/249) :heart:, but eventually went with their own solution, trying to be compatible with Griffe serialization format. ### pdoc [pdoc](https://github.com/mitmproxy/pdoc) is a documentation renderer for Python projects. pdoc's main feature is a focus on simplicity: pdoc aims to do one thing and do it well. ### Sphinx AutoAPI [Sphinx AutoAPI](https://github.com/readthedocs/sphinx-autoapi) is a new approach to API documentation in Sphinx. It is a Sphinx extension for generating complete API documentation without needing to load, run, or import the project being documented. In contrast to the traditional [Sphinx autodoc](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html), which requires manual authoring and uses code imports, AutoAPI finds and generates documentation by parsing source code. Sphinx AutoAPI is [considering Griffe as a data extraction backend](https://github.com/readthedocs/sphinx-autoapi/issues/444) :heart: ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions.md��������������������������������������������������������������0000664�0001750�0001750�00000001124�14767006246�017646� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extensions Griffe has an extension system that allows Python developers to enhance or modify the data that Griffe collects. To learn more about how to use or develop extensions, please see the [Extending APIs topic](guide/users/extending.md). We group extensions under three categories: - [built-in extensions](extensions/built-in.md): maintained directly within Griffe's codebase - [official extensions](extensions/official.md): maintained in separated repositories, by the authors/maintainers of Griffe - [third-party extensions](extensions/third-party.md): maintained by other developers ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/insiders/������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�016747� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/insiders/changelog.md������������������������������������������������������0000664�0001750�0001750�00000001262�14767006246�021221� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog ## Griffe Insiders [](){#insiders-1.3.0} ### 1.3.0 <small>August 09, 2024</small> { id="1.3.0" } - [Automatic docstring style detection](../reference/docstrings.md#auto-style) [](){#insiders-1.2.0} ### 1.2.0 <small>March 11, 2024</small> { id="1.2.0" } - [Expressions modernization](../guide/users/navigating.md#modernization) [](){#insiders-1.1.0} ### 1.1.0 <small>March 02, 2024</small> { id="1.1.0" } - Check API of Python packages by [downloading them from PyPI](../guide/users/checking.md#using-pypi) [](){#insiders-1.0.0} ### 1.0.0 <small>January 16, 2024</small> { id="1.0.0" } - Add [Markdown][markdown] and [GitHub][github] output formats to the check command ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/insiders/goals.yml���������������������������������������������������������0000664�0001750�0001750�00000001550�14767006246�020600� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������goals: 500: name: PlasmaVac User Guide features: [] 1000: name: GraviFridge Fluid Renewal features: - name: "Markdown output format for the `griffe check` command" ref: /guide/users/checking/#markdown since: 2024/01/16 - name: "GitHub output format for the `griffe check` command" ref: /guide/users/checking/#github since: 2024/01/16 1500: name: HyperLamp Navigation Tips features: - name: "Check API of Python packages from PyPI" ref: /guide/users/checking/#using-pypi since: 2024/03/02 - name: "Expressions modernization" ref: /guide/users/navigating/#modernization since: 2024/03/11 - name: "Automatic detection of docstring style" ref: /reference/docstrings/#auto-style since: 2024/08/09 2000: name: FusionDrive Ejection Configuration features: [] ��������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/insiders/installation.md���������������������������������������������������0000664�0001750�0001750�00000005405�14767006246�021776� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Getting started with Insiders --- # Getting started with Insiders *Griffe Insiders* is a compatible drop-in replacement for *Griffe*, and can be installed similarly using `pip` or `git`. Note that in order to access the Insiders repository, you need to [become an eligible sponsor][] of @pawamoy on GitHub. ## Installation ### with the `insiders` tool [`insiders`][insiders-tool] is a tool that helps you keep up-to-date versions of Insiders projects in the PyPI index of your choice (self-hosted, Google registry, Artifactory, etc.). **We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use][].** ### with pip (ssh/https) *Griffe Insiders* can be installed with `pip` [using SSH][install-pip-ssh]: ```bash pip install git+ssh://git@github.com/pawamoy-insiders/griffe.git ``` Or using HTTPS: ```bash pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/griffe.git ``` >? NOTE: **How to get a GitHub personal access token?** The `GH_TOKEN` environment variable is a GitHub token. It can be obtained by creating a [personal access token][github-pat] for your GitHub account. It will give you access to the Insiders repository, programmatically, from the command line or GitHub Actions workflows: > > 1. Go to https://github.com/settings/tokens > 2. Click on [Generate a new token][github-pat-new] > 3. Enter a name and select the [`repo`][scopes] scope > 4. Generate the token and store it in a safe place > > Note that the personal access token must be kept secret at all times, as it allows the owner to access your private repositories. ### with Git Of course, you can use *Griffe Insiders* directly using Git: ``` git clone git@github.com:pawamoy-insiders/griffe ``` When cloning with Git, the package must be installed: ``` pip install -e griffe ``` ## Upgrading When upgrading Insiders, you should always check the version of *Griffe* which makes up the first part of the version qualifier. For example, a version like `8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`. If the major version increased, it's a good idea to consult the [changelog][] and go through the steps to ensure your configuration is up to date and all necessary changes have been made. [become an eligible sponsor]: ./index.md#how-to-become-a-sponsor [changelog]: ./changelog.md [github-pat]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token [github-pat-new]: https://github.com/settings/tokens/new [insiders-tool]: https://pawamoy.github.io/insiders-project/ [install-pip-ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes [terms of use]: ./index.md#terms �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/insiders/index.md����������������������������������������������������������0000664�0001750�0001750�00000026365�14767006246�020414� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Insiders --- # Insiders *Griffe* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][]. Read on to learn [what sponsorships achieve][sponsorship], [how to become a sponsor][sponsors] to get access to Insiders, and [what's in it for you][features]! ## What is Insiders? *Griffe Insiders* is a private fork of *Griffe*, hosted as a private GitHub repository. Almost[^1] [all new features][features] are developed as part of this fork, which means that they are immediately available to all eligible sponsors, as they are granted access to this private repository. [^1]: In general, every new feature is first exclusively released to sponsors, but sometimes upstream dependencies enhance existing features that must be supported by *Griffe*. Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a funding goal is hit, the features that are tied to it are merged back into *Griffe* and released for general availability, making them available to all users. Bugfixes are always released in tandem. Sponsorships start as low as [**$10 a month**][sponsors].[^2] [^2]: Note that $10 a month is the minimum amount to become eligible for Insiders. While GitHub Sponsors also allows to sponsor lower amounts or one-time amounts, those can't be granted access to Insiders due to technical reasons. Such contributions are still very much welcome as they help ensuring the project's sustainability. ## What sponsorships achieve Sponsorships make this project sustainable, as they buy the maintainers of this project time – a very scarce resource – which is spent on the development of new features, bug fixing, stability improvement, issue triage and general support. The biggest bottleneck in Open Source is time.[^3] [^3]: Making an Open Source project sustainable is exceptionally hard: maintainers burn out, projects are abandoned. That's not great and very unpredictable. The sponsorware model ensures that if you decide to use *Griffe*, you can be sure that bugs are fixed quickly and new features are added regularly. If you're unsure if you should sponsor this project, check out the list of [completed funding goals][goals completed] to learn whether you're already using features that were developed with the help of sponsorships. You're most likely using at least a handful of them, [thanks to our awesome sponsors][sponsors]! ## What's in it for me? ```python exec="1" session="insiders" data_source = [ "docs/insiders/goals.yml", ("mkdocstrings/griffe2md", "https://mkdocstrings.github.io/griffe2md/", "insiders/goals.yml"), ("mkdocstrings/griffe-autodocstringstyle", "https://mkdocstrings.github.io/griffe-autodocstringstyle/", "insiders/goals.yml"), ("mkdocstrings/griffe-inherited-docstrings", "https://mkdocstrings.github.io/griffe-inherited-docstrings/", "insiders/goals.yml"), ("mkdocstrings/griffe-public-redundant-aliases", "https://mkdocstrings.github.io/griffe-public-redundant-aliases/", "insiders/goals.yml"), ("mkdocstrings/griffe-public-wildcard-imports", "https://mkdocstrings.github.io/griffe-public-wildcard-imports/", "insiders/goals.yml"), ("mkdocstrings/griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), ("mkdocstrings/griffe-runtime-objects", "https://mkdocstrings.github.io/griffe-runtime-objects/", "insiders/goals.yml"), ("mkdocstrings/griffe-sphinx", "https://mkdocstrings.github.io/griffe-sphinx/", "insiders/goals.yml"), ("mkdocstrings/griffe-tui", "https://mkdocstrings.github.io/griffe-tui/", "insiders/goals.yml"), ("mkdocstrings/griffe-warnings-deprecated", "https://mkdocstrings.github.io/griffe-warnings-deprecated/", "insiders/goals.yml"), ] ``` ```python exec="1" session="insiders" idprefix="" --8<-- "scripts/insiders.py" if unreleased_features: print( "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get **immediate " f"access to {len(unreleased_features)} additional features** that you can start using right away, and " "which are currently exclusively available to sponsors:\n" ) for feature in unreleased_features: feature.render(badge=True) print( "\n\nThese are just the features related to this project. " "[See the complete feature list on the author's main Insiders page](https://pawamoy.github.io/insiders/#whats-in-it-for-me)." ) else: print( "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get immediate " "access to all released features that you can start using right away, and " "which are exclusively available to sponsors. At this moment, there are no " "Insiders features for this project, but checkout the [next funding goals](#goals) " "to see what's coming, as well as **[the feature list for all Insiders projects](https://pawamoy.github.io/insiders/#whats-in-it-for-me).**" ) ``` Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management][backlog]. ## How to become a sponsor Thanks for your interest in sponsoring! In order to become an eligible sponsor with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. Sponsorships lower than $10 a month are also very much appreciated, and useful. They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. Every sponsorship helps us implementing new features and releasing them to the public. **Important:** By default, when you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, all the publicly visible members of the organization will be invited to join our private repositories. If you wish to only grant access to a subset of users, please send a short email to insiders@pawamoy.fr with the name of your organization and the GitHub accounts of the users that should be granted access. **Tip:** to ensure that access is not tied to a particular individual GitHub account, you can create a bot account (i.e. a GitHub account that is not tied to a specific individual), and use this account for the sponsoring. After being granted access to our private repositories, the bot account can create private forks of our private repositories into your own organization, which all members of your organization will have access to. You can cancel your sponsorship anytime.[^5] [^5]: If you cancel your sponsorship, GitHub schedules a cancellation request which will become effective at the end of the billing cycle. This means that even though you cancel your sponsorship, you will keep your access to Insiders as long as your cancellation isn't effective. All charges are processed by GitHub through Stripe. As we don't receive any information regarding your payment, and GitHub doesn't offer refunds, sponsorships are non-refundable. [:octicons-heart-fill-24:{ .pulse }   Join our <span id="sponsors-count"></span> awesome sponsors][github sponsor profile]{ .md-button .md-button--primary } <hr> <div class="premium-sponsors"> <div id="gold-sponsors"></div> <div id="silver-sponsors"></div> <div id="bronze-sponsors"></div> </div> <hr> <div id="sponsors"></div> <small> If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *Griffe*. Alternatively, if you wish to keep your sponsorship private, you'll be a silent +1. You can select visibility during checkout and change it afterwards. </small> ## Funding <span class="sponsors-total"></span> ### Goals The following section lists all funding goals. Each goal contains a list of features prefixed with a checkmark symbol, denoting whether a feature is :octicons-check-circle-fill-24:{ style="color: #00e676" } already available or :octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, but not yet implemented. When the funding goal is hit, the features are released for general availability. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): if not goal.complete: goal.render() ``` ### Goals completed This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): if goal.complete: goal.render() ``` ## Frequently asked questions ### Compatibility > We're building an open source project and want to allow outside collaborators to use *Griffe* locally without having access to Insiders. Is this still possible? Yes. Insiders is compatible with *Griffe*. Almost all new features and configuration options are either backward-compatible or implemented behind feature flags. Most Insiders features enhance the overall experience, though while these features add value for the users of your project, they shouldn't be necessary for previewing when making changes to content. ### Payment > We don't want to pay for sponsorship every month. Are there any other options? Yes. You can sponsor on a yearly basis by [switching your GitHub account to a yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). If you have any problems or further questions, please reach out to insiders@pawamoy.fr. ### Terms > Are we allowed to use Insiders under the same terms and conditions as *Griffe*? Yes. Whether you're an individual or a company, you may use *Griffe Insiders* precisely under the same terms as *Griffe*, which are given by the [ISC license][license]. However, we kindly ask you to respect our **fair use policy**: - Please **don't distribute the source code** of Insiders. You may freely use it for public, private or commercial projects, privately fork or mirror it, but please don't make the source code public, as it would counteract the sponsorware strategy. - If you cancel your subscription, your access to the private repository is revoked, and you will miss out on all future updates of Insiders. However, you may **use the latest version** that's available to you **as long as you like**. Just remember that [GitHub deletes private forks][private forks]. [backlog]: https://pawamoy.github.io/backlog/ [billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle [insiders]: #what-is-insiders [sponsorship]: #what-sponsorships-achieve [sponsors]: #how-to-become-a-sponsor [features]: #whats-in-it-for-me [funding]: #funding [github sponsor profile]: https://github.com/sponsors/pawamoy [goals completed]: #goals-completed [insiders]: #what-is-insiders [license]: ../license.md [private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository [sponsors]: #how-to-become-a-sponsor [sponsorship]: #what-sponsorships-achieve ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/contributing.md������������������������������������������������������������0000664�0001750�0001750�00000004555�14767006246�020171� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Contributing --- # Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. There are multiple ways to contribute to this project: - with **feature requests**: we are always happy to receive feedback and new ideas! If you have any, you can create [new feature requests](https://github.com/mkdocstrings/griffe/issues/new?assignees=pawamoy&labels=feature&projects=&template=feature_request.md&title=feature%3A+) on our issue tracker. Make sure to search issues first, to avoid creating duplicate requests. - with **bug reports**: only you (the users) can help us find and fix bugs! We greatly appreciate if you can give us a bit of your time to create a proper [bug report](https://github.com/mkdocstrings/griffe/issues/new?assignees=pawamoy&labels=unconfirmed&projects=&template=bug_report.md&title=bug%3A+) on our issue tracker. Same as for feature requests, make sure the bug is not already reported, by searching through issues first. - with **user support**: watch activity on the [Github repository](https://github.com/mkdocstrings/griffe) and our [Gitter channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ target="_blank" } to answer issues and discussions created by users. Answering questions from users can take a lot of time off maintenance and new features: helping us with user support means more time for us to work on the project. - with **documentation**: spotted a mistake in the documentation? Found a paragraph unclear or a section missing? Reporting those already helps a lot, and if you can, sending pull requests is even better. - with **code**: if you are interested in a feature request, or are experiencing a reported bug, you can contribute a feature or a fix. You can simply drop a comment on the relevant issue, and we will do our best to guide you. For easy documentation fixes, you can edit a file and send a pull request [directly from the GitHub web interface](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files#editing-files-in-another-users-repository). For more complex fixes or improvements, please read our contributor guide. The guide will show you how to set up a development environment to run tests or serve the documentation locally. [:fontawesome-solid-helmet-safety: Contributor guide](guide/contributors.md){ .md-button } ���������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/����������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017326� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/����������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021575� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/inherited-method-crossrefs.md�����������������������0000664�0001750�0001750�00000002071�14767006246�027357� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-inherited-method-crossrefs` - **PyPI**: [`griffe-inherited-method-crossrefs`](https://pypi.org/project/griffe-inherited-method-crossrefs/) - **GitHub**: [mlprt/griffe-inherited-method-crossrefs](https://github.com/mlprt/griffe-inherited-method-crossrefs) - **Extension name:** `griffe_inherited_method_crossrefs` --- This extension replaces docstrings of inherited methods with cross-references to parent methods. For example, if a class `foo.Child` inherits the method `do_something` from `bar.Parent`, then in the generated documentation, the docstring of `Child.do_something` will appear similar to > Inherited from [bar.Parent](https://example.com/link/to/bar.Parent.do_something) whereas the docstring of `bar.Parent.do_something` will be unaffected. This is contrast to the official [`inherited-docstrings`](../official/inherited-docstrings.md) extension which simply attaches the docstring of the parent method to the subclass method, which means that modifying the subclass method docstring also modifies the parent method docstring (it's the same object). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/generics.md�����������������������������������������0000664�0001750�0001750�00000001072�14767006246�023716� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-generics` - **PyPI**: [`griffe-generics`](https://pypi.org/project/griffe-generics/) - **GitHub**: [jonghwanhyeon/griffe-generics](https://github.com/jonghwanhyeon/griffe-generics) - **Extension name:** `griffe_generics` --- This extension resolves generic type parameters as bound types in subclasses. For example, if a parent class inherits from `Generics[L]`, and a subclass specifies `L` as `Hashable`, then all type annotations using `L` in the class methods or attributes inherited from the parent class will be transformed to use `Hashable` instead. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/modernized-annotations.md���������������������������0000664�0001750�0001750�00000001065�14767006246�026614� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-modernized-annotations` - **PyPI**: [`griffe-modernized-annotations`](https://pypi.org/project/griffe-modernized-annotations/) - **GitHub**: [jonghwanhyeon/griffe-modernized-annotations](https://github.com/jonghwanhyeon/griffe-modernized-annotations) - **Extension name:** `griffe_modernized_annotations` --- This extension modernizes type annotations by adopting [PEP 585](https://peps.python.org/pep-0585/) and [PEP 604](https://peps.python.org/pep-0604/). For example, it will transform `Union[A, B]` into `A | B`, and `List[str]` into `list[str]`. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/fieldz.md�������������������������������������������0000664�0001750�0001750�00000001502�14767006246�023372� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-fieldz` - **PyPI**: [`griffe-fieldz`](https://pypi.org/project/griffe-fieldz/) - **GitHub**: [pyapp-kit/griffe-fieldz](https://github.com/pyapp-kit/griffe-fieldz) - **Extension name:** `griffe_fieldz` --- This extension adds support for data-class like things (pydantic, attrs, etc...). This extension will inject the fields of the data-class into the documentation, preventing you from duplicating field metadata in your docstrings. It supports anything that [fieldz](https://github.com/pyapp-kit/fieldz) supports, which is currently: - [`dataclasses.dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass) - [`pydantic.BaseModel`](https://docs.pydantic.dev/latest/) - [`attrs.define`](https://www.attrs.org/en/stable/overview.html) - [`msgspec.Struct`](https://jcristharif.com/msgspec/) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party/docstring-inheritance.md����������������������������0000664�0001750�0001750�00000001576�14767006246�026413� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `docstring-inheritance` - **PyPI**: [`docstring-inheritance`](https://pypi.org/project/docstring-inheritance/) - **GitHub**: [AntoineD/docstring-inheritance](https://github.com/AntoineD/docstring-inheritance) - **Extension name:** `docstring_inheritance.griffe` --- `docstring-inheritance` is a Python package that allows to avoid writing and maintaining duplicated Python docstrings. The typical usage is to enable the inheritance of the docstrings from a base class such that its derived classes fully or partially inherit the docstrings. It provides a Griffe extension and recommends to use it alongside the official [`inherited-docstrings`](../official/inherited-docstrings.md) extension in MkDocs: ```yaml plugins: - mkdocstrings: handlers: python: options: extensions: - griffe_inherited_docstrings - docstring_inheritance.griffe ``` ����������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/third-party.md��������������������������������������������������0000664�0001750�0001750�00000002441�14767006246�022120� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Third-party extensions Third-party extensions are developed and maintained outside of the mkdocstrings organization, by various developers. They generally bring support for third-party libraries. Extension | Description --------- | ----------- [`docstring-inheritance`](third-party/docstring-inheritance.md) | A more advanced docstring inheritance utility that also provides a Griffe extension. [`fieldz`](third-party/fieldz.md) | Support for data-class like objects (dataclasses, pydantic, attrs, etc.) using [fieldz](https://github.com/pyapp-kit/fieldz). [`generics`](third-party/generics.md) | Resolve generic type parameters as bound types in subclasses. [`inherited-method-crossrefs`](third-party/inherited-method-crossrefs.md) | Replace docstrings of inherited methods with cross-references to parents. [`modernized-annotations`](third-party/modernized-annotations.md) | Modernize type annotations by adopting PEP 585 and PEP 604. You can find more third-party extensions by exploring the [`griffe-extension` topic on GitHub](https://github.com/topics/griffe-extension). You can also check out the "in-project" extensions (not published to PyPI) used in various projects on GitHub by [searching for "griffe extension" in code](https://github.com/search?q=griffe+Extension+language%3Apython&type=code). �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/�������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021102� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/public-redundant-aliases.md����������������������������0000664�0001750�0001750�00000001413�14767006246�026302� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-public-redundant-aliases` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } - **PyPI**: [`griffe-public-redundant-aliases`](https://pypi.org/project/griffe-public-redundant-aliases/) - **GitHub**: [mkdocstrings/griffe-public-redundant-aliases](https://github.com/mkdocstrings/griffe-public-redundant-aliases) - **Documentation:** [mkdocstrings.github.io/griffe-public-redundant-aliases](https://mkdocstrings.github.io/griffe-public-redundant-aliases) - **Extension name:** `griffe_public_redundant_aliases` --- This extension marks every object that was imported with a redundant alias as public. See our documentation on the [redundant aliases convention](../../guide/users/recommendations/public-apis.md#redundant-aliases). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/public-wildcard-imports.md�����������������������������0000664�0001750�0001750�00000001401�14767006246�026160� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-public-wildcard-imports` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } - **PyPI**: [`griffe-public-wildcard-imports`](https://pypi.org/project/griffe-public-wildcard-imports/) - **GitHub**: [mkdocstrings/griffe-public-wildcard-imports](https://github.com/mkdocstrings/griffe-public-wildcard-imports) - **Documentation:** [mkdocstrings.github.io/griffe-public-wildcard-imports](https://mkdocstrings.github.io/griffe-public-wildcard-imports) - **Extension name:** `griffe_public_wildcard_imports` --- This extension marks every object that was imported with a wildcard import as public. See our documentation on the [wildcard imports convention](../../guide/users/recommendations/public-apis.md#wildcard-imports). ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/autodocstringstyle.md����������������������������������0000664�0001750�0001750�00000001573�14767006246�025400� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-autodocstringstyle` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } - **PyPI**: [`griffe-autodocstringstyle`](https://pypi.org/project/griffe-autodocstringstyle/) - **GitHub**: [mkdocstrings/griffe-autodocstringstyle](https://github.com/mkdocstrings/griffe-autodocstringstyle) - **Documentation:** [mkdocstrings.github.io/griffe-autodocstringstyle](https://mkdocstrings.github.io/griffe-autodocstringstyle) - **Extension name:** `griffe_autodocstringstyle` --- This extension sets the docstring parser to `auto` for all the docstrings of external packages. Packages are considered "external" when their sources are found in a virtual environment instead of a folder under the current working directory. Setting their docstring style to `auto` is useful if you plan on rendering the docstring of these objects in your own documentation. �������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/warnings-deprecated.md���������������������������������0000664�0001750�0001750�00000002133�14767006246�025351� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-warnings-deprecated` - **PyPI**: [`griffe-warnings-deprecated`](https://pypi.org/project/griffe-warnings-deprecated/) - **GitHub**: [mkdocstrings/griffe-warnings-deprecated](https://github.com/mkdocstrings/griffe-warnings-deprecated) - **Documentation:** [mkdocstrings.github.io/griffe-warnings-deprecated](https://mkdocstrings.github.io/griffe-warnings-deprecated) - **Extension name:** `griffe_warnings_deprecated` --- This extension adds support for functions and classes decorated with [`@warnings.deprecated(...)`][warnings.deprecated], as implemented thanks to [PEP 702](https://peps.python.org/pep-0702/). The message provided in the decorator call will be stored in the corresponding Griffe object's [`deprecated`][griffe.Object.deprecated] attribute (usable by downstream rendering templates), and will also add an admonition to the object's docstring with the provided message as text. ```python from warnings import deprecated @deprecated("This function is **deprecated**. Use [another one][package.another_func] instead.") def deprecated_func(): ... def another_func(): ... ``` �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/typingdoc.md�������������������������������������������0000664�0001750�0001750�00000003745�14767006246�023435� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-typingdoc` - **PyPI**: [`griffe-typingdoc`](https://pypi.org/project/griffe-typingdoc/) - **GitHub**: [mkdocstrings/griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc) - **Documentation:** [mkdocstrings.github.io/griffe-typingdoc](https://mkdocstrings.github.io/griffe-typingdoc) - **Extension name:** `griffe_typingdoc` --- This extension reads docstrings for parameters, return values and more from type annotations using [`Annotated`][typing.Annotated] and the [`Doc`][typing_extensions.Doc] class suggested in [PEP 727](https://peps.python.org/pep-0727/). Documenting parameters and return values this way makes it possible to completely avoid relying on a particular "docstring style" (Google, Numpydoc, Sphinx, etc.) and just use plain markup in module/classes/function docstrings. Docstrings therefore do not have to be parsed at all. ```python from typing import Annotated as An from typing_extensions import Doc def function( param1: An[int, Doc("Some integer value.")], param2: An[ str, Doc( """ Summary of the parameter. Multi-line docstrings can be used, as usual. Any **markup** is supported, as usual. """ ) ] ) -> An[bool, Doc("Whether you like PEP 727.")]: """Summary of the function. No more "Args", "Parameters" or "Returns" sections. Just plain markup. """ ... ``` PEP 727 is likely to be withdrawn or rejected, but the `Doc` class will remain in `typing_extensions`, [as told by Jelle Zijlstra](https://discuss.python.org/t/pep-727-documentation-metadata-in-typing/32566/183): > We’ll probably keep it in `typing_extensions` indefinitely even if the PEP gets withdrawn or rejected, for backwards compatibility reasons. > > You are free to use it in your own code using the typing-extensions version. If usage of `typing_extensions.Doc` becomes widespread, that will be a good argument for accepting the PEP and putting it in the standard library. ���������������������������python-griffe-1.6.2/docs/extensions/official/inherited-docstrings.md��������������������������������0000664�0001750�0001750�00000002076�14767006246�025561� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-inherited-docstrings` - **PyPI**: [`griffe-inherited-docstrings`](https://pypi.org/project/griffe-inherited-docstrings/) - **GitHub**: [mkdocstrings/griffe-inherited-docstrings](https://github.com/mkdocstrings/griffe-inherited-docstrings) - **Documentation:** [mkdocstrings.github.io/griffe-inherited-docstrings](https://mkdocstrings.github.io/griffe-inherited-docstrings) - **Extension name:** `griffe_inherited_docstrings` --- This extension, when enabled, iterates over the declared members of all classes found within a package, and if they don't have a docstring, but do have a parent member with a docstring, sets their docstring to that parent's docstring. ```python class Base: attr = "hello" """Hello.""" def hello(self): """Hello again.""" ... class Derived(Base): attr = "bye" def hello(self): ... ``` In the example above, *without* the extension `Derived.attr` and `Derived.hello` have no docstrings, while *with* the extension they will have the `Base.attr` and `Base.hello` docstrings attached, respectively. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/pydantic.md��������������������������������������������0000664�0001750�0001750�00000001273�14767006246�023242� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-pydantic` - **PyPI**: [`griffe-pydantic`](https://pypi.org/project/griffe-pydantic/) - **GitHub**: [mkdocstrings/griffe-pydantic](https://github.com/mkdocstrings/griffe-pydantic) - **Documentation:** [mkdocstrings.github.io/griffe-pydantic](https://mkdocstrings.github.io/griffe-pydantic) - **Extension name:** `griffe_pydantic` --- This extension adds support for [Pydantic](https://docs.pydantic.dev/latest/) models. It extracts useful information from them, stores this information into the `extra` attribute of objects, and binds custom mkdocstrings templates to the objects for better rendering. The extension works both statically and dynamically, and supports model inheritance. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/runtime-objects.md�������������������������������������0000664�0001750�0001750�00000002454�14767006246�024543� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-runtime-objects` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } - **PyPI**: [`griffe-runtime-objects`](https://pypi.org/project/griffe-runtime-objects/) - **GitHub**: [mkdocstrings/griffe-runtime-objects](https://github.com/mkdocstrings/griffe-runtime-objects) - **Documentation:** [mkdocstrings.github.io/griffe-runtime-objects](https://mkdocstrings.github.io/griffe-runtime-objects) - **Extension name:** `griffe_runtime_objects` --- This extension stores runtime objects corresponding to each loaded Griffe object into its `extra` attribute, under the `runtime-objects` namespace. ```pycon >>> import griffe >>> griffe_data = griffe.load("griffe", extensions=griffe.load_extensions("griffe_runtime_objects"), resolve_aliases=True) >>> griffe_data["parse"].extra defaultdict(<class 'dict'>, {'runtime-objects': {'object': <function parse at 0x78685c951260>}}) >>> griffe_data["Module"].extra defaultdict(<class 'dict'>, {'runtime-objects': {'object': <class '_griffe.models.Module'>}}) ``` It can be useful in combination with mkdocstrings-python and custom templates, to iterate over object values or their attributes that couldn't be loaded by Griffe itself (for example, objects built dynamically and loaded as attributes won't have "members" to iterate over). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official/sphinx.md����������������������������������������������0000664�0001750�0001750�00000002267�14767006246�022744� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `griffe-sphinx` [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } - **PyPI**: [`griffe-sphinx`](https://pypi.org/project/griffe-sphinx/) - **GitHub**: [mkdocstrings/griffe-sphinx](https://github.com/mkdocstrings/griffe-sphinx) - **Documentation:** [mkdocstrings.github.io/griffe-sphinx](https://mkdocstrings.github.io/griffe-sphinx) - **Extension name:** `griffe_sphinx` --- This extension reads Sphinx comments placed above attribute assignments and uses them as docstrings. ```python #: Summary of `module_attr`. module_attr = "hello" class Hello: #: Summary of `class_attr`. #: #: Description of the class attribute. #: *Markup* and [cross-references][] are __supported__, #: just like in regular docstrings. class_attr = "hello" def __init__(self): #: Summary of `instance_attr`. self.instance_attr = "hello" ``` Comments are treated exactly like regular docstrings: they are "cleaned" (dedented and stripped of leading and trailing newlines) and can contain any markup you want, be it Markdown, rST, AsciiDoc, etc. Trailing comments are not supported: ```python module_attr #: This is not supported. ``` �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/official.md�����������������������������������������������������0000664�0001750�0001750�00000003621�14767006246�021426� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Official extensions Official extensions are developed and maintained within the mkdocstrings organization on GitHub, in separate repositories. They generally bring support for various third-party libraries or other documentation-related features that are part of Python's standard library. Extension | Description | Sponsors only? --------- | ----------- | -------------- [`autodocstringstyle`](official/autodocstringstyle.md) | Set docstring style to `auto` for external packages. | [:octicons-heart-fill-24:{ .pulse }](../insiders/index.md){ .insiders } [`inherited-docstrings`](official/inherited-docstrings.md) | Inherit docstrings from parent classes. [`public-redundant-aliases`](official/public-redundant-aliases.md) | Mark objects imported with redundant aliases as public. | [:octicons-heart-fill-24:{ .pulse }](../insiders/index.md){ .insiders } [`public-wildcard-imports`](official/public-wildcard-imports.md) | Mark wildcard imported objects as public. | [:octicons-heart-fill-24:{ .pulse }](../insiders/index.md){ .insiders } [`pydantic`](official/pydantic.md) | Support for [Pydantic](https://docs.pydantic.dev/latest/) models. [`runtime-objects`](official/runtime-objects.md) | Access runtime objects corresponding to each loaded Griffe object through their `extra` attribute. | [:octicons-heart-fill-24:{ .pulse }](../insiders/index.md){ .insiders } [`sphinx`](official/sphinx.md) | Parse [Sphinx](https://www.sphinx-doc.org/)-comments above attributes (`#:`) as docstrings. | [:octicons-heart-fill-24:{ .pulse }](../insiders/index.md){ .insiders } [`typing-doc`](official/typingdoc.md) | Support for [PEP 727](https://peps.python.org/pep-0727/)'s [`typing.Doc`][typing_extensions.Doc], "Documentation in Annotated Metadata". [`warnings-deprecated`](official/warnings-deprecated.md) | Support for [PEP 702](https://peps.python.org/pep-0702/)'s [`warnings.deprecated`][], "Marking deprecations using the type system". ���������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/built-in.md�����������������������������������������������������0000664�0001750�0001750�00000000453�14767006246�021375� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Built-in extensions Built-in extensions are maintained in Griffe's code base. They generally bring support for core features of the Python language or its standard library. Extension | Description --------- | ----------- [`dataclasses`](built-in/dataclasses.md) | Support for [`dataclasses`][]. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/built-in/�������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021051� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/extensions/built-in/dataclasses.md�����������������������������������������0000664�0001750�0001750�00000000711�14767006246�023661� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# `dataclasses` The `dataclasses` extension adds support for [dataclasses][] from the standard library. It works both statically and dynamically. When used statically, it re-creates the `__init__` methods and their signatures (as Griffe objects), that would otherwise be created at runtime. When used dynamically, it does nothing since `__init__` methods are created by the library and can be inspected normally. **This extension is enabled by default.** �������������������������������������������������������python-griffe-1.6.2/docs/reference/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017065� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/�������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017636� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/finder.md����������������������������������������������������0000664�0001750�0001750�00000000261�14767006246�021426� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Finder ## **Advanced API** ::: griffe.ModuleFinder ::: griffe.Package ::: griffe.NamespacePackage ## **Types** ::: griffe.NamePartsType ::: griffe.NamePartsAndPathType �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/exceptions.md������������������������������������������������0000664�0001750�0001750�00000000620�14767006246�022337� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Exceptions ::: griffe.GriffeError ::: griffe.LoadingError ::: griffe.NameResolutionError ::: griffe.UnhandledEditableModuleError ::: griffe.UnimportableModuleError ::: griffe.AliasResolutionError ::: griffe.CyclicAliasError ::: griffe.LastNodeError ::: griffe.RootNodeError ::: griffe.BuiltinModuleError ::: griffe.ExtensionError ::: griffe.ExtensionNotLoadedError ::: griffe.GitError ����������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/loaders.md���������������������������������������������������0000664�0001750�0001750�00000000400�14767006246�021603� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Loaders ## **Main API** ::: griffe.load ::: griffe.load_git ::: griffe.load_pypi ## **Advanced API** ::: griffe.GriffeLoader ::: griffe.ModulesCollection ::: griffe.LinesCollection ## **Additional API** ::: griffe.Stats ::: griffe.merge_stubs ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/serializers.md�����������������������������������������������0000664�0001750�0001750�00000000324�14767006246�022513� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Serializers ## **Main API** See the [`as_json()`][griffe.Object.as_json] and [`from_json()`][griffe.Object.from_json] methods of objects. ## **Advanced API** ::: griffe.JSONEncoder ::: griffe.json_decoder ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021121� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/attribute.md������������������������������������������0000664�0001750�0001750�00000000027�14767006246�023445� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe.Attribute ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/alias.md����������������������������������������������0000664�0001750�0001750�00000000023�14767006246�022527� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe.Alias �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/function.md�������������������������������������������0000664�0001750�0001750�00000000216�14767006246�023267� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe.Function ::: griffe.Parameters ::: griffe.Parameter ::: griffe.ParameterKind ::: griffe.ParametersType ::: griffe.Decorator ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/class.md����������������������������������������������0000664�0001750�0001750�00000000100�14767006246�022537� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe.Class ## **Utilities** ::: griffe.c3linear_merge ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models/module.md���������������������������������������������0000664�0001750�0001750�00000000024�14767006246�022724� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe.Module ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/expressions.md�����������������������������������������������0000664�0001750�0001750�00000002320�14767006246�022537� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Expressions ::: _griffe.expressions options: members: false show_root_heading: false show_root_toc_entry: false summary: classes: true ## **Helpers** ::: griffe.get_annotation ::: griffe.get_base_class ::: griffe.get_condition ::: griffe.get_expression ::: griffe.safe_get_annotation ::: griffe.safe_get_base_class ::: griffe.safe_get_condition ::: griffe.safe_get_expression ## **Expression nodes** ::: griffe.Expr ::: griffe.ExprAttribute ::: griffe.ExprBinOp ::: griffe.ExprBoolOp ::: griffe.ExprCall ::: griffe.ExprCompare ::: griffe.ExprComprehension ::: griffe.ExprConstant ::: griffe.ExprDict ::: griffe.ExprDictComp ::: griffe.ExprExtSlice ::: griffe.ExprFormatted ::: griffe.ExprGeneratorExp ::: griffe.ExprIfExp ::: griffe.ExprJoinedStr ::: griffe.ExprKeyword ::: griffe.ExprVarPositional ::: griffe.ExprVarKeyword ::: griffe.ExprLambda ::: griffe.ExprList ::: griffe.ExprListComp ::: griffe.ExprName ::: griffe.ExprNamedExpr ::: griffe.ExprParameter ::: griffe.ExprSet ::: griffe.ExprSetComp ::: griffe.ExprSlice ::: griffe.ExprSubscript ::: griffe.ExprTuple ::: griffe.ExprUnaryOp ::: griffe.ExprYield ::: griffe.ExprYieldFrom ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/helpers.md���������������������������������������������������0000664�0001750�0001750�00000000465�14767006246�021627� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Helpers ::: griffe.TmpPackage ::: griffe.temporary_pyfile ::: griffe.temporary_pypackage ::: griffe.temporary_visited_module ::: griffe.temporary_visited_package ::: griffe.temporary_inspected_module ::: griffe.temporary_inspected_package ::: griffe.vtree ::: griffe.htree ::: griffe.module_vtree �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/cli.md�������������������������������������������������������0000664�0001750�0001750�00000000203�14767006246�020722� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# CLI entrypoints ## **Main API** ::: griffe.main ::: griffe.check ::: griffe.dump ## **Advanced API** ::: griffe.get_parser ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/extensions.md������������������������������������������������0000664�0001750�0001750�00000000465�14767006246�022364� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extensions ## **Main API** ::: griffe.load_extensions ::: griffe.Extension ## **Advanced API** ::: griffe.Extensions ## **Types** ::: griffe.LoadableExtensionType ## **Builtin extensions** ::: griffe.builtin_extensions ::: griffe.DataclassesExtension options: inherited_members: false �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/docstrings.md������������������������������������������������0000664�0001750�0001750�00000000214�14767006246�022334� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstrings Docstrings are [parsed](docstrings/parsers.md) and the extracted information is structured in [models](docstrings/models.md). ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/agents.md����������������������������������������������������0000664�0001750�0001750�00000002040�14767006246�021435� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Agents Griffe is able to analyze code both statically and dynamically. ## **Main API** ::: griffe.visit ::: griffe.inspect ## **Advanced API** ::: griffe.Visitor ::: griffe.Inspector ## **Dynamic analysis helpers** ::: griffe.sys_path ::: griffe.dynamic_import ::: griffe.ObjectNode ::: griffe.ObjectKind ## **Static analysis helpers** ::: griffe.builtin_decorators ::: griffe.stdlib_decorators ::: griffe.typing_overload ::: griffe.ast_kind ::: griffe.ast_children ::: griffe.ast_previous_siblings ::: griffe.ast_next_siblings ::: griffe.ast_siblings ::: griffe.ast_previous ::: griffe.ast_next ::: griffe.ast_first_child ::: griffe.ast_last_child ::: griffe.get_docstring ::: griffe.get_name ::: griffe.get_names ::: griffe.get_instance_names ::: griffe.get__all__ ::: griffe.safe_get__all__ ::: griffe.relative_to_absolute ::: griffe.get_parameters ::: griffe.get_value ::: griffe.safe_get_value <!-- YORE: Bump 2: Remove line. --> ## **Deprecated API** <!-- YORE: Bump 2: Remove line. --> ::: griffe.ExportedName ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/git.md�������������������������������������������������������0000664�0001750�0001750�00000000172�14767006246�020743� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Git utilities ::: griffe.assert_git_repo ::: griffe.get_latest_tag ::: griffe.get_repo_root ::: griffe.tmp_worktree ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/loggers.md���������������������������������������������������0000664�0001750�0001750�00000000346�14767006246�021625� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Loggers ## **Main API** ::: griffe.logger ::: griffe.get_logger ::: griffe.Logger ::: griffe.LogLevel ::: griffe.DEFAULT_LOG_LEVEL options: annotations_path: full ## **Advanced API** ::: griffe.patch_loggers ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/models.md����������������������������������������������������0000664�0001750�0001750�00000002110�14767006246�021435� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Models Griffe stores information extracted from Python source code into data models. 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. The 5 models: - [`Module`][griffe.Module] - [`Class`][griffe.Class] - [`Function`][griffe.Function] - [`Attribute`][griffe.Attribute] - [`Alias`][griffe.Alias] ## **Model kind enumeration** ::: griffe.Kind ## **Models base classes** ::: griffe.GetMembersMixin ::: griffe.SetMembersMixin ::: griffe.DelMembersMixin ::: griffe.SerializationMixin ::: griffe.ObjectAliasMixin ::: griffe.Object ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/docstrings/��������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�022015� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/docstrings/parsers.md����������������������������������������0000664�0001750�0001750�00000000573�14767006246�024023� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstring parsers ## **Main API** ::: griffe.parse ::: griffe.parse_auto ::: griffe.parse_google ::: griffe.parse_numpy ::: griffe.parse_sphinx ::: griffe.DocstringStyle ## **Advanced API** ::: griffe.Parser ::: griffe.parsers ::: griffe.parse_docstring_annotation ::: griffe.docstring_warning ::: griffe.DocstringDetectionMethod ::: griffe.infer_docstring_style �������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/docstrings/models.md�����������������������������������������0000664�0001750�0001750�00000002237�14767006246�023626� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstring models ## **Main API** ::: griffe.Docstring ## **Advanced API: Sections** ::: griffe.DocstringSectionKind ::: griffe.DocstringSectionText ::: griffe.DocstringSectionParameters ::: griffe.DocstringSectionOtherParameters ::: griffe.DocstringSectionRaises ::: griffe.DocstringSectionWarns ::: griffe.DocstringSectionReturns ::: griffe.DocstringSectionYields ::: griffe.DocstringSectionReceives ::: griffe.DocstringSectionExamples ::: griffe.DocstringSectionAttributes ::: griffe.DocstringSectionFunctions ::: griffe.DocstringSectionClasses ::: griffe.DocstringSectionModules ::: griffe.DocstringSectionDeprecated ::: griffe.DocstringSectionAdmonition ## **Advanced API: Section items** ::: griffe.DocstringAdmonition ::: griffe.DocstringDeprecated ::: griffe.DocstringRaise ::: griffe.DocstringWarn ::: griffe.DocstringReturn ::: griffe.DocstringYield ::: griffe.DocstringReceive ::: griffe.DocstringParameter ::: griffe.DocstringAttribute ::: griffe.DocstringFunction ::: griffe.DocstringClass ::: griffe.DocstringModule ## **Models base classes** ::: griffe.DocstringElement ::: griffe.DocstringNamedElement ::: griffe.DocstringSection �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api/checks.md����������������������������������������������������0000664�0001750�0001750�00000001125�14767006246�021417� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# API checks ::: griffe.find_breaking_changes ::: griffe.ExplanationStyle ::: griffe.Breakage ::: griffe.BreakageKind ::: griffe.AttributeChangedTypeBreakage ::: griffe.AttributeChangedValueBreakage ::: griffe.ClassRemovedBaseBreakage ::: griffe.ObjectChangedKindBreakage ::: griffe.ObjectRemovedBreakage ::: griffe.ParameterAddedRequiredBreakage ::: griffe.ParameterChangedDefaultBreakage ::: griffe.ParameterChangedKindBreakage ::: griffe.ParameterChangedRequiredBreakage ::: griffe.ParameterMovedBreakage ::: griffe.ParameterRemovedBreakage ::: griffe.ReturnChangedTypeBreakage �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/cli.md�����������������������������������������������������������0000664�0001750�0001750�00000003426�14767006246�020163� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# CLI reference ```python exec="true" idprefix="cli-" import argparse import sys from griffe import get_parser parser = get_parser() def render_parser(parser: argparse.ArgumentParser, title: str, heading_level: int = 2) -> str: """Render the parser help documents as a string.""" result = [f"{'#' * heading_level} {title}\n"] if parser.description and title != "pdm": result.append("> " + parser.description + "\n") for group in sorted(parser._action_groups, key=lambda g: g.title.lower(), reverse=True): if not any( bool(action.option_strings or action.dest) or isinstance(action, argparse._SubParsersAction) for action in group._group_actions ): continue result.append(f"{group.title.title()}:\n") for action in group._group_actions: if isinstance(action, argparse._SubParsersAction): for name, subparser in action._name_parser_map.items(): result.append(render_parser(subparser, name, heading_level + 1)) continue opts = [f"`{opt}`" for opt in action.option_strings] if not opts: line = f"- `{action.dest}`" else: line = f"- {', '.join(opts)}" if action.metavar: line += f" `{action.metavar}`" line += f": {action.help}" if action.default and action.default != argparse.SUPPRESS: if action.default is sys.stdout: default = "sys.stdout" else: default = str(action.default) line += f" Default: `{default}`." result.append(line) result.append("") return "\n".join(result) print(render_parser(parser, "griffe")) ``` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/docstrings.md����������������������������������������������������0000664�0001750�0001750�00000122062�14767006246�021571� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstrings Griffe provides different docstring parsers allowing to extract even more structured data from source code. The available parsers are: - `google`, to parse Google-style docstrings, see [Napoleon's documentation][napoleon] - `numpy`, to parse Numpydoc docstrings, see [Numpydoc's documentation][numpydoc] - `sphinx`, to parse Sphinx-style docstrings, see [Sphinx's documentation][sphinx] - `auto` (sponsors only), to automatically detect the docstring style, see [Auto-style](#auto-style) Most of the time, the syntax specified in the aforementioned docs is supported. In some cases, the original syntax is not supported, or is supported but with subtle differences. We will try to document these differences in the following sections. No assumption is made on the markup used in docstrings: it's retrieved as regular text. Tooling making use of Griffe can then choose to render the text as if it is Markdown, or AsciiDoc, or reStructuredText, etc.. ## Google-style Google-style docstrings, see [Napoleon's documentation][napoleon]. ### Syntax {#google-syntax} Sections are written like this: ``` section identifier: optional section title section contents ``` All sections identifiers are case-insensitive. All sections support multiple lines in descriptions, as well as blank lines. The first line must not be blank. Each section must be separated from contents above by a blank line. ❌ This is **invalid** and will be parsed as regular markup: ```python Some text. Note: # (1)! Some information. Blank lines allowed. ``` 1. Missing blank line above. ❌ This is **invalid** and will be parsed as regular markup: ```python Some text. Note: # (1)! Some information. Blank lines allowed. ``` 1. Extraneous blank line below. ✅ This is **valid** and will parsed as a text section followed by a note admonition: ```python Some text. Note: Some information. Blank lines allowed. ``` Find out possibly invalid section syntax by grepping for "reasons" in Griffe debug logs: ```bash griffe dump -Ldebug -o/dev/null -fdgoogle your_package 2>&1 | grep reasons ``` Some sections support documenting multiple items (attributes, parameters, etc.). When multiple items are supported, each item description can use multiple lines, and continuation lines must be indented once more so that the parser is able to differentiate items. ```python def foo(a, b): """Foo. Parameters: a: Here's a. Continuation line 1. Continuation line 2. b: Here's b. """ ``` It's possible to start a description with a newline if you find it less confusing: ```python def foo(a, b): """Foo. Parameters: a: Here's a. Continuation line 1. Continuation line 2. b: Here's b. """ ``` ### Admonitions {#google-admonitions} When a section identifier does not match one of the [supported sections](#google-sections), the section is parsed as an "admonition" (or "callout"). Identifiers are case-insensitive, however singular and plural forms are distinct. For example, `Note:` is not the same as `Notes:`. In particular, `Examples` is parsed as the [Examples section](#google-section-examples), while `Example` is parsed as an admonition whose kind is `example`. The kind is obtained by lower-casing the identifier and replacing spaces with dashes. For example, an admonition whose identifier is `See also:` will have a kind equal to `see-also`. Custom section titles are preserved in admonitions: `Tip: Check this out:` is parsed as a `tip` admonition with `Check this out:` as title. It is up to any downstream documentation renderer to make use of these kinds and titles. ### Parser options {#google-options} The parser accepts a few options: - `ignore_init_summary`: Ignore the first line in `__init__` methods' docstrings. Useful when merging `__init__` docstring into class' docstrings with mkdocstrings-python's [`merge_init_into_class`][merge_init] option. Default: false. - `returns_multiple_items`: Parse [Returns sections](#google-section-returns) and [Yields sections](#google-section-yields) as if they contain multiple items. It means that continuation lines must be indented. Default: true. - `returns_named_value`: Whether to parse `thing: Description` in [Returns sections](#google-section-returns) and [Yields sections](#google-section-yields) as a name and description, rather than a type and description. When true, type must be wrapped in parentheses: `(int): Description.`. When false, parentheses are optional but the items cannot be named: `int: Description`. Default: true. - `receives_multiple_items`: Parse [Receives sections](#google-section-receives) as if they contain multiple items. It means that continuation lines must be indented. Default: true. - `receives_named_value`: Whether to parse `thing: Description` in [Receives sections](#google-section-receives) as a name and description, rather than a type and description. When true, type must be wrapped in parentheses: `(int): Description.`. When false, parentheses are optional but the items cannot be named: `int: Description`. Default: true. - `returns_type_in_property_summary`: Whether to parse the return type of properties at the beginning of their summary: `str: Summary of the property`. Default: false. - `trim_doctest_flags`: Remove the [doctest flags] written as comments in `pycon` snippets within a docstring. These flags are used to alter the behavior of [doctest] when testing docstrings, and should not be visible in your docs. Default: true. - `warn_unknown_params`: Warn about parameters documented in docstrings that do not appear in the signature. Default: true. ### Sections {#google-sections} The following sections are supported. #### Attributes {#google-section-attributes} - Multiple items allowed Attributes sections allow to document attributes of a module, class, or class instance. They should be used in modules and classes docstrings only. ```python """My module. Attributes: foo: Description for `foo`. bar: Description for `bar`. """ foo: int = 0 bar: bool = True class MyClass: """My class. Attributes: foofoo: Description for `foofoo`. barbar: Description for `barbar`. """ foofoo: int = 0 def __init__(self): self.barbar: bool = True ``` Type annotations are fetched from the related attributes definitions. You can override those by adding types between parentheses before the colon: ```python """My module. Attributes: foo (Integer): Description for `foo`. bar (Boolean): Description for `bar`. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** When documenting an attribute with `attr_name (attr_type): Attribute description`, `attr_type` will be resolved using the scope of the docstrings' parent object (class or module). For example, a type of `list[str]` will be parsed just as if it was an actual Python annotation. You can therefore use complex types (available in the current scope) in docstrings, for example `Optional[Union[int, Tuple[float, float]]]`. #### Functions/Methods {#google-section-functions} - Multiple items allowed Functions or Methods sections allow to document functions of a module, or methods of a class. They should be used in modules and classes docstrings only. ```python """My module. Functions: foo: Description for `foo`. bar: Description for `bar`. """ def foo(): return "foo" def bar(baz: int) -> int: return baz * 2 class MyClass: """My class. Methods: foofoo: Description for `foofoo`. barbar: Description for `barbar`. """ def foofoo(self): return "foofoo" @staticmethod def barbar(): return "barbar" ``` It's possible to write the function/method signature as well as its name: ```python """ Functions: foo(): Description for `foo`. bar(baz=1): Description for `bar`. """ ``` The signatures do not have to match the real ones: you can shorten them to only show the important parameters. #### Classes {#google-section-classes} - Multiple items allowed Classes sections allow to document classes of a module or class. They should be used in modules and classes docstrings only. ```python """My module. Classes: Foo: Description for `foo`. Bar: Description for `bar`. """ class Foo: ... class Bar: def __init__(self, baz: int) -> int: return baz * 2 class MyClass: """My class. Classes: FooFoo: Description for `foofoo`. BarBar: Description for `barbar`. """ class FooFoo: ... class BarBar: ... ``` It's possible to write the class signature as well as its name: ```python """ Functions: Foo(): Description for `Foo`. Bar(baz=1): Description for `Bar`. """ ``` The signatures do not have to match the real ones: you can shorten them to only show the important initialization parameters. #### Modules {#google-section-modules} - Multiple items allowed Modules sections allow to document submodules of a module. They should be used in modules docstrings only. ```tree my_pkg/ __init__.py foo.py bar.py ``` ```python title="my_pkg/__init__.py" """My package. Modules: foo: Description for `foo`. bar: Description for `bar`. """ ``` #### Examples {#google-section-examples} Examples sections allow to add examples of Python code without the use of markup code blocks. They are a mix of prose and interactive console snippets. They can be used in every docstring. ```python """My module. Examples: Some explanation of what is possible. >>> print("hello!") hello! Blank lines delimit prose vs. console blocks. >>> a = 0 >>> a += 1 >>> a 1 """ ``` WARNING: **Not the same as *Example* sections.** *Example* (singular) sections are parsed as admonitions. Console code blocks will only be understood in *Examples* (plural) sections. #### Parameters {#google-section-parameters} - Aliases: Args, Arguments, Params - Multiple items allowed Parameters sections allow to document parameters of a function. They are typically used in functions docstrings, but can also be used in dataclasses docstrings. ```python def foo(a: int, b: str): """Foo. Parameters: a: Here's a. b: Here's b. """ ``` ```python from dataclasses import dataclass @dataclass class Foo: """Foo. Parameters: a: Here's a. b: Here's b. """ foo: int bar: str ``` Type annotations are fetched from the related parameters definitions. You can override those by adding types between parentheses before the colon: ```python """My function. Parameters: foo (Integer): Description for `foo`. bar (String): Description for `bar`. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** When documenting a parameter with `param_name (param_type): Parameter description`, `param_type` will be resolved using the scope of the function (or class). For example, a type of `list[str]` will be parsed just as if it was an actual Python annotation. You can therefore use complex types (available in the current scope) in docstrings, for example `Optional[Union[int, Tuple[float, float]]]`. #### Other Parameters {#google-section-other-parameters} - Aliases: Keyword Args, Keyword Arguments, Other Args, Other Arguments, Other Params - Multiple items allowed Other parameters sections allow to document secondary parameters such as variadic keyword arguments, or parameters that should be of lesser interest to the user. They are used the same way Parameters sections are, but can also be useful in decorators / to document returned callables. ```python def foo(a, b, **kwargs): """Foo. Parameters: a: Here's a. b: Here's b. Other parameters: c (int): Here's c. d (bool): Here's d. """ ``` ```python def foo(a, b): """Returns a callable. Parameters: a: Here's a. b: Here's b. Other parameters: Parameters of the returned callable: c (int): Here's c. d (bool): Here's d. """ def inner(c, d): ... return inner ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See the same tip for parameters. #### Raises {#google-section-raises} - Aliases: Exceptions - Multiple items allowed Raises sections allow to document exceptions that are raised by a function. They are usually only used in functions docstrings. ```python def foo(a: int): """Foo. Parameters: a: A value. Raises: ValueError: When `a` is less than 0. """ if a < 0: raise ValueError("message") ``` TIP: **Exceptions names are resolved using the function's scope.** `ValueError` and other built-in exceptions are resolved as such. You can document custom exception, using the names available in the current scope, for example `my_exceptions.MyCustomException` or `MyCustomException` directly, depending on what you imported/defined in the current module. #### Warns {#google-section-warns} - Aliases: Warnings - Multiple items allowed Warns sections allow to document warnings emitted by the following code. They are usually only used in functions docstrings. ```python import warnings def foo(): """Foo. Warns: UserWarning: To annoy users. """ warnings.warn("Just messing with you.", UserWarning) ``` TIP: **Warnings names are resolved using the function's scope.** `UserWarning` and other built-in warnings are resolved as such. You can document custom warnings, using the names available in the current scope, for example `my_warnings.MyCustomWarning` or `MyCustomWarning` directly, depending on what you imported/defined in the current module. #### Yields {#google-section-yields} - Multiple items allowed Yields sections allow to document values that generator yield. They should be used only in generators docstrings. Documented items can be given a name when it makes sense. ```python from typing import Iterator def foo() -> Iterator[int]: """Foo. Yields: Integers from 0 to 9. """ for i in range(10): yield i ``` Type annotations are fetched from the function return annotation when the annotation is `typing.Generator` or `typing.Iterator`. If your generator yields tuples, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python from datetime import datetime def foo() -> Iterator[tuple[float, float, datetime]]: """Foo. Yields: x: Absissa. y: Ordinate. t: Time. ... """ ... ``` You have to indent each continuation line when documenting yielded values, even if there's only one value yielded: ```python """Foo. Yields: partial_result: Some partial result. A longer description of details and other information for this partial result. """ ``` If you don't want to indent continuation lines for the only yielded value, use the [`returns_multiple_items=False`](#google-options) parser option. Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Yields: x (int): Absissa. y (int): Ordinate. t (int): Timestamp. """ ``` If you want to specify the type without a name, you still have to wrap the type in parentheses: ```python """Foo. Yields: (int): Absissa. (int): Ordinate. (int): Timestamp. """ ``` If you don't want to wrap the type in parentheses, use the [`returns_named_value=False`](#google-options) parser option. Setting it to false will disallow specifying a name. TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See previous tips for types in docstrings. #### Receives {#google-section-receives} - Multiple items allowed Receives sections allow to document values that can be sent to generators using their `send` method. They should be used only in generators docstrings. Documented items can be given a name when it makes sense. ```python from typing import Generator def foo() -> Generator[int, str, None]: """Foo. Receives: reverse: Reverse the generator if `"reverse"` is received. Yields: Integers from 0 to 9. Examples: >>> gen = foo() >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 2 >>> gen.send("reverse") 2 >>> next(gen) 1 >>> next(gen) 0 >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration """ for i in range(10): received = yield i if received == "reverse": for j in range(i, -1, -1): yield j break ``` Type annotations are fetched from the function return annotation when the annotation is `typing.Generator`. If your generator is able to receive tuples, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python def foo() -> Generator[int, tuple[str, bool], None]: """Foo. Receives: mode: Some mode. flag: Some flag. ... """ ... ``` You have to indent each continuation line when documenting received values, even if there's only one value received: ```python """Foo. Receives: data: Input data. A longer description of what this data actually is, and what it isn't. """ ``` If you don't want to indent continuation lines for the only received value, use the [`receives_multiple_items=False`](#google-options) parser option. Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Receives: mode (ModeEnum): Some mode. flag (int): Some flag. """ ``` If you want to specify the type without a name, you still have to wrap the type in parentheses: ```python """Foo. Receives: (ModeEnum): Some mode. (int): Some flag. """ ``` If you don't want to wrap the type in parentheses, use the [`receives_named_value=False`](#google-options) parser option. Setting it to false will disallow specifying a name. TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See previous tips for types in docstrings. #### Returns {#google-section-returns} - Multiple items allowed Returns sections allow to document values returned by functions. They should be used only in functions docstrings. Documented items can be given a name when it makes sense. ```python import random def foo() -> int: """Foo. Returns: A random integer. """ return random.randint(0, 100) ``` Type annotations are fetched from the function return annotation. If your function returns tuples of values, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python def foo() -> tuple[bool, float]: """Foo. Returns: success: Whether it succeeded. precision: Final precision. ... """ ... ``` You have to indent each continuation line when documenting returned values, even if there's only one value returned: ```python """Foo. Returns: success: Whether it succeeded. A longer description of what is considered success, and what is considered failure. """ ``` If you don't want to indent continuation lines for the only returned value, use the [`returns_multiple_items=False`](#google-options) parser option. Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Returns: success (int): Whether it succeeded. precision (Decimal): Final precision. """ ``` If you want to specify the type without a name, you still have to wrap the type in parentheses: ```python """Foo. Returns: (int): Whether it succeeded. (Decimal): Final precision. """ ``` If you don't want to wrap the type in parentheses, use the [`returns_named_value=False`](#google-options) parser option. Setting it to false will disallow specifying a name. TIP: **Types in docstrings are resolved using the docstrings' function scope.** See previous tips for types in docstrings. ## Numpydoc-style Numpydoc docstrings, see [Numpydoc's documentation][numpydoc] ### Syntax {#numpydoc-syntax} Sections are written like this: ``` section identifier ------------------ section contents ``` All sections identifiers are case-insensitive. All sections support multiple lines in descriptions. Some sections support documenting items items. Item descriptions start on a new, indented line. When multiple items are supported, each item description can use multiple lines. ```python def foo(a, b): """Foo. Parameters ---------- a Here's a. Continuation line 1. Continuation line 2. b Here's b. """ ``` For items that have an optional name and type, several syntaxes are supported: - specifying both the name and type: ```python """ name : type description """ ``` - specifying just the name: ```python """ name description """ ``` or ```python """ name : description """ ``` - specifying just the type: ```python """ : type description """ ``` - specifying neither the name nor type: ```python """ : description """ ``` ### Admonitions {#numpydoc-admonitions} When a section identifier does not match one of the [supported sections](#numpydoc-sections), the section is parsed as an "admonition" (or "callout"). Identifiers are case-insensitive, however singular and plural forms are distinct, except for notes and warnings. In particular, `Examples` is parsed as the [Examples section](#numpydoc-section-examples), while `Example` is parsed as an admonition whose kind is `example`. The kind is obtained by lower-casing the identifier and replacing spaces with dashes. For example, an admonition whose identifier is `See also` will have a kind equal to `see-also`. It is up to any downstream documentation renderer to make use of these kinds. ### Parser options {#numpydoc-options} The parser accepts a few options: - `ignore_init_summary`: Ignore the first line in `__init__` methods' docstrings. Useful when merging `__init__` docstring into class' docstrings with mkdocstrings-python's [`merge_init_into_class`][merge_init] option. Default: false. - `trim_doctest_flags`: Remove the [doctest flags] written as comments in `pycon` snippets within a docstring. These flags are used to alter the behavior of [doctest] when testing docstrings, and should not be visible in your docs. Default: true. - `warn_unknown_params`: Warn about parameters documented in docstrings that do not appear in the signature. Default: true. ### Sections {#numpydoc-sections} The following sections are supported. #### Attributes {#numpydoc-section-attributes} - Multiple items allowed Attributes sections allow to document attributes of a module, class, or class instance. They should be used in modules and classes docstrings only. ```python """My module. Attributes ---------- foo Description for `foo`. bar Description for `bar`. """ foo: int = 0 bar: bool = True class MyClass: """My class. Attributes ---------- foofoo Description for `foofoo`. barbar Description for `barbar`. """ foofoo: int = 0 def __init__(self): self.barbar: bool = True ``` Type annotations are fetched from the related attributes definitions. You can override those by adding types between parentheses before the colon: ```python """My module. Attributes ---------- foo : Integer Description for `foo`. bar : Boolean Description for `bar`. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** When documenting an attribute with `attr_name : attr_type`, `attr_type` will be resolved using the scope of the docstrings' parent object (class or module). For example, a type of `list[str]` will be parsed just as if it was an actual Python annotation. You can therefore use complex types (available in the current scope) in docstrings, for example `Optional[Union[int, Tuple[float, float]]]`. #### Functions/Methods {#numpydoc-section-functions} - Multiple items allowed Functions or Methods sections allow to document functions of a module, or methods of a class. They should be used in modules and classes docstrings only. ```python """My module. Functions --------- foo Description for `foo`. bar Description for `bar`. """ def foo(): return "foo" def bar(baz: int) -> int: return baz * 2 class MyClass: """My class. Methods ------- foofoo Description for `foofoo`. barbar Description for `barbar`. """ def foofoo(self): return "foofoo" @staticmethod def barbar(): return "barbar" ``` It's possible to write the function/method signature as well as its name: ```python """ Functions --------- foo() Description for `foo`. bar(baz=1) Description for `bar`. """ ``` The signatures do not have to match the real ones: you can shorten them to only show the important parameters. #### Classes {#numpydoc-section-classes} - Multiple items allowed Classes sections allow to document classes of a module or class. They should be used in modules and classes docstrings only. ```python """My module. Classes ------- Foo Description for `foo`. Bar Description for `bar`. """ class Foo: ... class Bar: def __init__(self, baz: int) -> int: return baz * 2 class MyClass: """My class. Classes ------- FooFoo Description for `foofoo`. BarBar Description for `barbar`. """ class FooFoo: ... class BarBar: ... ``` It's possible to write the class signature as well as its name: ```python """ Functions --------- Foo() Description for `Foo`. Bar(baz=1) Description for `Bar`. """ ``` The signatures do not have to match the real ones: you can shorten them to only show the important initialization parameters. #### Modules {#numpydoc-section-modules} - Multiple items allowed Modules sections allow to document submodules of a module. They should be used in modules docstrings only. ```tree my_pkg/ __init__.py foo.py bar.py ``` ```python title="my_pkg/__init__.py" """My package. Modules ------- foo Description for `foo`. bar Description for `bar`. """ ``` #### Examples {#numpydoc-section-examples} Examples sections allow to add examples of Python code without the use of markup code blocks. They are a mix of prose and interactive console snippets. They can be used in every docstring. ```python """My module. Examples -------- Some explanation of what is possible. >>> print("hello!") hello! Blank lines delimit prose vs. console blocks. >>> a = 0 >>> a += 1 >>> a 1 """ ``` #### Parameters {#numpydoc-section-parameters} - Aliases: Args, Arguments, Params - Multiple items allowed Parameters sections allow to document parameters of a function. They are typically used in functions docstrings, but can also be used in dataclasses docstrings. ```python def foo(a: int, b: str): """Foo. Parameters ---------- a Here's a. b Here's b. """ ``` ```python from dataclasses import dataclass @dataclass class Foo: """Foo. Parameters ---------- a Here's a. b Here's b. """ foo: int bar: str ``` Type annotations are fetched from the related parameters definitions. You can override those by adding types between parentheses before the colon: ```python """My function. Parameters ---------- foo : Integer Description for `foo`. bar : String Description for `bar`. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** When documenting a parameter with `param_name : param_type`, `param_type` will be resolved using the scope of the function (or class). For example, a type of `list[str]` will be parsed just as if it was an actual Python annotation. You can therefore use complex types (available in the current scope) in docstrings, for example `Optional[Union[int, Tuple[float, float]]]`. #### Other Parameters {#numpydoc-section-other-parameters} - Aliases: Keyword Args, Keyword Arguments, Other Args, Other Arguments, Other Params - Multiple items allowed Other parameters sections allow to document secondary parameters such as variadic keyword arguments, or parameters that should be of lesser interest to the user. They are used the same way Parameters sections are. ```python def foo(a, b, **kwargs): """Foo. Parameters ---------- a Here's a. b Here's b. Other parameters ---------------- c : int Here's c. d : bool Here's d. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See the same tip for parameters. #### Raises {#numpydoc-section-raises} - Aliases: Exceptions - Multiple items allowed Raises sections allow to document exceptions that are raised by a function. They are usually only used in functions docstrings. ```python def foo(a: int): """Foo. Parameters ---------- a A value. Raises ------ ValueError When `a` is less than 0. """ if a < 0: raise ValueError("message") ``` TIP: **Exceptions names are resolved using the function's scope.** `ValueError` and other built-in exceptions are resolved as such. You can document custom exception, using the names available in the current scope, for example `my_exceptions.MyCustomException` or `MyCustomException` directly, depending on what you imported/defined in the current module. #### Warns {#numpydoc-section-warns} - Multiple items allowed Warns sections allow to document warnings emitted by the following code. They are usually only used in functions docstrings. ```python import warnings def foo(): """Foo. Warns ----- UserWarning To annoy users. """ warnings.warn("Just messing with you.", UserWarning) ``` TIP: **Warnings names are resolved using the function's scope.** `UserWarning` and other built-in warnings are resolved as such. You can document custom warnings, using the names available in the current scope, for example `my_warnings.MyCustomWarning` or `MyCustomWarning` directly, depending on what you imported/defined in the current module. #### Yields {#numpydoc-section-yields} - Multiple items allowed Yields sections allow to document values that generator yield. They should be used only in generators docstrings. Documented items can be given a name when it makes sense. ```python from typing import Iterator def foo() -> Iterator[int]: """Foo. Yields ------ : Integers from 0 to 9. """ for i in range(10): yield i ``` Type annotations are fetched from the function return annotation when the annotation is `typing.Generator` or `typing.Iterator`. If your generator yields tuples, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python from datetime import datetime def foo() -> Iterator[tuple[float, float, datetime]]: """Foo. Yields ------ x Absissa. y Ordinate. t Time. """ ... ``` Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Yields ------ x : int Absissa. y : int Ordinate. t : int Timestamp. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See previous tips for types in docstrings. #### Receives {#numpydoc-section-receives} - Multiple items allowed Receives sections allow to document values that can be sent to generators using their `send` method. They should be used only in generators docstrings. Documented items can be given a name when it makes sense. ```python from typing import Generator def foo() -> Generator[int, str, None]: """Foo. Receives -------- reverse Reverse the generator if `"reverse"` is received. Yields ------ : Integers from 0 to 9. Examples -------- >>> gen = foo() >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 2 >>> gen.send("reverse") 2 >>> next(gen) 1 >>> next(gen) 0 >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration """ for i in range(10): received = yield i if received == "reverse": for j in range(i, -1, -1): yield j break ``` Type annotations are fetched from the function return annotation when the annotation is `typing.Generator`. If your generator is able to receive tuples, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python def foo() -> Generator[int, tuple[str, bool], None]: """Foo. Receives -------- mode Some mode. flag Some flag. """ ... ``` Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Receives -------- mode : ModeEnum Some mode. flag : int Some flag. """ ``` TIP: **Types in docstrings are resolved using the docstrings' parent scope.** See previous tips for types in docstrings. #### Returns {#numpydoc-section-returns} - Multiple items allowed Returns sections allow to document values returned by functions. They should be used only in functions docstrings. Documented items can be given a name when it makes sense. ```python import random def foo() -> int: """Foo. Returns ------- : A random integer. """ return random.randint(0, 100) ``` Type annotations are fetched from the function return annotation. If your function returns tuples of values, you can document each item of the tuple separately, and the type annotation will be fetched accordingly: ```python def foo() -> tuple[bool, float]: """Foo. Returns ------- success Whether it succeeded. precision Final precision. """ ... ``` Type annotations can as usual be overridden using types in parentheses in the docstring itself: ```python """Foo. Returns ------- success : int Whether it succeeded. precision : Decimal Final precision. """ ``` TIP: **Types in docstrings are resolved using the docstrings' function scope.** See previous tips for types in docstrings. ## Auto-style [:octicons-heart-fill-24:{ .pulse } Sponsors only](../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.3.0](../insiders/changelog.md#1.3.0). Automatic style detection. This parser will first try to detect the style used in the docstring, and call the corresponding parser on it. ### Parser options {#auto-options} The parser accepts a few options: - `method`: The method to use to detect the style and infer the parser. Method 'heuristics' will use regular expressions, while method 'max_sections' will parse the docstring with all parsers specified in `style_order` and return the one who parsed the most sections. Default: `"heuristics"`. - `style_order`: If multiple parsers parsed the same number of sections, `style_order` is used to decide which one to return. Default: `["sphinx", "google", "numpy"]`. - `default`: If heuristics fail, the `default` parser is returned. The `default` parser is never used with the 'max_sections' method. Default: `None`. - Any other option is passed down to the detected parser, if any. For non-Insiders versions, `default` is returned if specified, else the first parser in `style_order` is returned. If `style_order` is not specified, `None` is returned. ## Parsers features !!! tip "Want to contribute?" Each red cross is a link to an issue on the bugtracker. You will find some guidance on how to add support for the corresponding item. The sections are easier to deal in that order: - Raises, Warns (multiple items, no names, single type each) - Attributes, Other Parameters, Parameters (multiple items, one name and one optional type each) - Returns (multiple items, optional name and/or type each, annotation to split when multiple names) - Receives, Yields (multiple items, optional name and/or type each, several types of annotations to split when multiple names) "Examples" section are a bit different as they require to parse the examples. But you can probably reuse the code in the Google parser. We can probably even factorize the examples parsing into a single function. You can tackle several items at once in a single PR, as long as they relate to a single parser or a single section (a line or a column of the following tables). ### Sections Section | Google | Numpy | Sphinx ---------------- | ------ | ----- | ------ Attributes | ✅ | ✅ | ✅ Functions | ✅ | ✅ | ❌ Methods | ✅ | ✅ | ❌ Classes | ✅ | ✅ | ❌ Modules | ✅ | ✅ | ❌ Examples | ✅ | ✅ | [❌][issue-section-sphinx-examples] Parameters | ✅ | ✅ | ✅ Other Parameters | ✅ | ✅ | [❌][issue-section-sphinx-other-parameters] Raises | ✅ | ✅ | ✅ Warns | ✅ | ✅ | [❌][issue-section-sphinx-warns] Yields | ✅ | ✅ | [❌][issue-section-sphinx-yields] Receives | ✅ | ✅ | [❌][issue-section-sphinx-receives] Returns | ✅ | ✅ | ✅ ### Getting annotations/defaults from parent Section | Google | Numpy | Sphinx ---------------- | ------ | ----- | ------ Attributes | ✅ | ✅ | [❌][issue-parent-sphinx-attributes] Functions | / | / | / Methods | / | / | / Classes | / | / | / Modules | / | / | / Examples | / | / | / Parameters | ✅ | ✅ | ✅ Other Parameters | ✅ | ✅ | [❌][issue-parent-sphinx-other-parameters] Raises | / | / | / Warns | / | / | / Yields | ✅ | ✅ | [❌][issue-parent-sphinx-yields] Receives | ✅ | ✅ | [❌][issue-parent-sphinx-receives] Returns | ✅ | ✅ | ✅ ### Cross-references for annotations in docstrings Section | Google | Numpy | Sphinx ---------------- | ------ | ----- | ------ Attributes | ✅ | ✅ | [❌][issue-xref-sphinx-attributes] Functions | [❌][issue-xref-google-func-cls] | [❌][issue-xref-numpy-func-cls] | / Methods | [❌][issue-xref-google-func-cls] | [❌][issue-xref-numpy-func-cls] | / Classes | [❌][issue-xref-google-func-cls] | [❌][issue-xref-numpy-func-cls] | / Modules | / | / | / Examples | / | / | / Parameters | ✅ | ✅ | [❌][issue-xref-sphinx-parameters] Other Parameters | ✅ | ✅ | [❌][issue-xref-sphinx-other-parameters] Raises | ✅ | ✅ | [❌][issue-xref-sphinx-raises] Warns | ✅ | ✅ | [❌][issue-xref-sphinx-warns] Yields | ✅ | ✅ | [❌][issue-xref-sphinx-yields] Receives | ✅ | ✅ | [❌][issue-xref-sphinx-receives] Returns | ✅ | ✅ | [❌][issue-xref-sphinx-returns] [doctest]: https://docs.python.org/3/library/doctest.html#module-doctest [doctest flags]: https://docs.python.org/3/library/doctest.html#option-flags [issue-parent-sphinx-attributes]: https://github.com/mkdocstrings/griffe/issues/33 [issue-parent-sphinx-other-parameters]: https://github.com/mkdocstrings/griffe/issues/34 [issue-parent-sphinx-receives]: https://github.com/mkdocstrings/griffe/issues/35 [issue-parent-sphinx-yields]: https://github.com/mkdocstrings/griffe/issues/36 [issue-section-sphinx-examples]: https://github.com/mkdocstrings/griffe/issues/7 [issue-section-sphinx-other-parameters]: https://github.com/mkdocstrings/griffe/issues/27 [issue-section-sphinx-receives]: https://github.com/mkdocstrings/griffe/issues/8 [issue-section-sphinx-warns]: https://github.com/mkdocstrings/griffe/issues/9 [issue-section-sphinx-yields]: https://github.com/mkdocstrings/griffe/issues/10 [issue-xref-google-func-cls]: https://github.com/mkdocstrings/griffe/issues/199 [issue-xref-numpy-func-cls]: https://github.com/mkdocstrings/griffe/issues/200 [issue-xref-sphinx-attributes]: https://github.com/mkdocstrings/griffe/issues/19 [issue-xref-sphinx-other-parameters]: https://github.com/mkdocstrings/griffe/issues/20 [issue-xref-sphinx-parameters]: https://github.com/mkdocstrings/griffe/issues/21 [issue-xref-sphinx-raises]: https://github.com/mkdocstrings/griffe/issues/22 [issue-xref-sphinx-receives]: https://github.com/mkdocstrings/griffe/issues/23 [issue-xref-sphinx-returns]: https://github.com/mkdocstrings/griffe/issues/24 [issue-xref-sphinx-warns]: https://github.com/mkdocstrings/griffe/issues/25 [issue-xref-sphinx-yields]: https://github.com/mkdocstrings/griffe/issues/26 [merge_init]: https://mkdocstrings.github.io/python/usage/configuration/docstrings/#merge_init_into_class [napoleon]: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html [numpydoc]: https://numpydoc.readthedocs.io/en/latest/format.html [sphinx]: https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/reference/api.md�����������������������������������������������������������0000664�0001750�0001750�00000000217�14767006246�020160� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: API reference hide: - navigation --- # ::: griffe options: summary: functions: true members: false ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/img/�����������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015703� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/img/gha_annotations_2.png��������������������������������������������������0000664�0001750�0001750�00000041627�14767006246�022020� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��������':���sBITO�� �IDATxgX�ovOwAQ@@`Ck5ͨѨijLILcػ" J9(*ENogf 4%] h]� 6 1 mcm?qfO6{򴙃D�w2q R<7WryCfMދ[W^ Gu)g{h��@sEځg1-|L)�:}xeSL y#SV p[;B=x:=Fx9c7QzbS1g xmZxnOx׾~>䪛M5 Db2rJ9#U m*+L*%bVVVrC<˷Yˋo)c_δ+mC,4rrDz&^\R>q,m R?PbPN:ɜ-d^vYVHjcV.38erI T.U%Vr#J!f5$O޿hg-e*g߅?Zph2zgX�-(ǝqԚFi Dz;_dB 2 WoY)Lә�u[nBYALth=KWPeۿiw}O9tOes쾦D^^ow ^6١uW3զ:TYX!yb7".L<͘4u�/s>��` wJg+rc\Z+5^i\R%H&U%m)DhVͰH ]bLŴ/�d9e'^_[6W,rjԏ,FO]B �y}=wŊX{�� \>䦷|yہ)@zVs`Fr>>ٍnvgYn~z',ə5fyo=<ޡmʫ-u% iL j夕f{,VRRP,{LƸȀGcrU%VhKΦtvFl둒eqi^dׯ+Z~[Cs*`~5(ݎ=fL6LX{WeQ}~)U�Ĭ\3>F{f�"1X,Y16trݵJ7B � Ѐmӵk*/X�ϩO#V�%N_�㝩tN10DސcB(CC jʫ [#*RL#hK^mCy7Co՘=eҴ8옅\_sY{vTnimrUCoJN[X#?(dʪJ��6cr�Q(]WCXUQeO'(~~e/d4HώW�# a ۉl&Gzmo*,E�uA(V� YO>h�skQ��3k 7�V�3j �-6!0J)�V˛IxcEيuE!_TYd��es_;Z?:o}ODߺ��60#`�N$/�VδK>MWw�'5g59G��V== ��:?j͋ KAh;˄kWtOŀڰ y@<=A ;r|>,y>e|_'!�(kf񱲊ҪU҆L'D�H_G$D<N<>� <@F{��qL9!�հ5Ft^Mj)/^] y,vdb&eUף1� {m3eU՘;oP\sNO~~yw]1sg޽~K4S72鷕~?2nkW.+\syr?Xv>.?:+"6 �,f.'H=u{mR7MX yaHdW"77L>/ִ1Ahr@f{K p<8g'lĿxUpyXę!S6 7 /&cĿMHߐ mBbAڄ, YAh&$fMH"BwJyZʲ+ Er�@:>~LȈZ 5 e9cv_|V ]˃k:``+%{/V7_U]eʆlb *W6!&/sw|`:zw6�@߾Ҿ}Xbջo'K/+5*ZZEҼ <Kv:{"A/=f:[UJ*V`,/;pV9 rNK-Qb,OM`{yEÿ߱dخ!��dSQb.u.��pV9w,^Nȹ_zX>__ݳ2ԧ24Y=7yUUomS7a[ K/}2^dS?;{w[IH95A<>矾R5dBAJB55%UXouY{OzbRuW :-vؕ!^:Q�NiC>?%͵-+eģZP`6T1NQ9'WBյT5b�HYtjԬ+>}MDdzx|Yƹk8pL@a�℈ NKiRR0@V6?&)yn *GȢGnSd"V �``h49 lB́OY{1ae^ʽJ% l<ZC i\jڌ(42``Iu4Lӥ\2b‰fHXG[ni$yF]3Ѻ?"VRY{`,�8Z5syP+ha#V/} IM-e�X�@�5!<9@TNcӯ[<X~cĉ3/¾~%T-ZK/'l]U۾{% vecY^V9"tqA5 LQv�!AAfQ\E�@g@3(] j0sȁ6LjoX+j3{;�G@SnOOE19� E* }iRi#c$ /B/ƒ˥SLM�Uε~&Ov.QEݕMݿ>~pX?jU�wt3ᶭt361y745[#uvhMwL�>�@Mj_]~9u'ӯBdοp\?>Q57m3 0NL6!1 mBAh"BE6!1 m{Ν\^}9 E1k~x+, AD+Hߐ mBbAڄ, YAh&$fMH"BE6!1 mBbAڄ, YAh&$fMH"BE6!1 m<?kP�9,/# L/5ņr>ƢBƿkxN[SNO2./-{m8ۼemOywyWԣf)'}uKot׊ĬZ^3'V'eW_62}H)p~^›BS'2}Lr*}ȔÃtDG8?þѝ.e~Hbvhً~<WRŞ�@v~(/&E1ҿ/GsV$FΥK*dVߍ+e;c;ĿG'3J|ϰ{ m~HVy2"ѾEF'M2tnh)ȍӕ4p;{{ƫUj6߁G~ۖi^-x=h YCߢ-8O&]Z=NrmYj%Rm(~mk*ӴY:;5 C4%oIngU15*3l:3:V-V�(˳2<j/8f %f31)x�*1=wFkM-ڡgۼi2��ein-�� AY]-t&+Xصb5�׼{PNeESgS+4�g=0_gK]\9G{isx{Ǘj��(s3<Q)(o>tG<Q?o 7+C!(I<s=)6iAbỌ#O3Lzt0KoE={OiwGvDWچ-Orn qG$^|eEF|:8q۩FWm4xR] OzJ/3{a+)Swr>7;|Bp4Ew"_ۨu]uuW\n]L�(Ύ;k4JsvCWΖQ-9y[r<-bD<^' L;p %,P]2MWZALtyP.ݛf=;6!L_-vB;fӃ#.W>}@CF5&wڽ7OmfNn.6 �$ETLCz9uN˺nu42'Y[[-ɉ;{:;oub< �p8b1i5EƬف\K#ۃr�d]?=B]@F(vKdOmƮᕔvI{Gi7ߒjE'L[r%?vN?.}N- 6uIZzj}�"�S9eUbٖ{'_`7ڑkjYɊ+@F^ܮ޲DhCxp/zn#LɎx#q!lsۓMT~l,cyU 0;zkx$t<cbS1SOmehy*N¬߄On@ה.f̓Ԩ2ki#cQMj}O]Y!161@DlE�se+i+}2'�eI +a�t4ʢ Ʌc[U ��TyW|_kj T2)"} 0Ң}|E]~;Ѿ5um n ڲIP� htvnIC #.]?<G6xdxȼ\nN#$`5=7 }9f6L>:|r,ɸ}BXU+m]KԀ*vYGD۸wd,Q0R��`ZGncsl^XaFv(?"/L`&;BGOTuRZZ��0I R{:Of$>,X/Buh_%e:F%K[%J ƭ- �^[,,`E-VewpT{=1ե*ݱ(Y=uneB4 =>VGG M8SXh]pVL<9XFz@S&NN̫Iej`%7x5,{1ZQԧyH΂"%! whRy'Mںv< !/5/G$=XPW7R/ЗBS +*k2̲Btt ;�P(/ $H˞xE3{2g*?Aٮ<|n~Qb \S>;0P&TNgؚʚFca&˲dž5b_WHKBX.Ǟ"]\,/)+HB ��Ke ;PwrY/Z!ΏpIմ 5:FjL[stOFnCf\Rkʩ>J(N>[;`s9"jWfq8XB UUV+A eԬ4d GJ-@s5 VWW?V|YX7ɕS;Nd9/&fY }9a74 ]Ja74Ol<$ȼ㧆8B&}p-*tE:j靰=:O6KѩtI4ybo{meNF' Yע~;ryҮ}GԔI3 /HKȬJ`uut`�?L*1ԥX�Dz yM..nٝ(ilmX(ҡ@aX4UbC# JY�^M^sGU~@<rqDZQv.*-u ǡQF-~_r@O;R4�pWѫcJd*�צ&tsvV;Ԥ\}z~3@HHmxv/xHH 5:Z|Kؾ.<Wh`fm"hf[]goO >@ ,M�LaF6dz)!a]D)H.eX5len>cyVyBo;q~ @(PU?Rp\Z=3U^VJϖ%8 q33垓eՐ�8:FtuW,NnN ÖeR]{pҧ݃xMtLMAzԁںu7MZg9y,6`+4|p�ͽsR ϶ǰ]MϱR1킳1�"~N5i5ռβ2B!oqĪhC5֋X?U3M-?3{ )FZy7;CCƿa'NW(TYG₂}dcU5VDx'4īЭ7s~^㔬 ;ͯb)y)oX輙JvVĩgO5w!GU_Џ7.z2\8d䇽heyڕÑy};{@<Bފ<|])?z> ylu'2�Ptd|H܏Frw.<%vGQ)Eր 8p̝3fJ=ϙsFeԖcN_<}?Hn'@55,�乹ciqN鋹j��yfJQ$d�,e~xiI|1ϝ\zBtxR gL{vgxe4n?É'v 5s?7`-XmuK [A"+{~mAE Jbhj1Ʒi{iفǬ*vC7[6~!FRt2<&{*v z"䱧A{}C WG6!1 mBbAڄ, YAh&$fMH"B 4M :|CsucXh \.c9hRYBP__HT`B|@Gd`jjV[[+ADQ B07Sh%a B!k̭-xCEӴa̙tpp@^xqǎkT)Ey6U*$ě|GZOORJ+J~rVZhԦj_]|JU?E%+_MQ xsC'?~|ٲeO,�r .  맻kgmn ŧ'Ŭn+Mu'33סPc6%%l@<XҮ΀g×|Jš9JeAKpp7֭[W^^x_r% R-urKvvը{-hE� z:u{O`ڌBFfjRp@|p4-3/r ZZff鶺5m?p·|Lrt :u�� �IDAT&W-z(zPk��Q/Q uk/1 �=%;w.�>}zŝ;wuwwܹ… ?�ӦM�qoE�2?ݫ1./(d`,�(.۽ Y�-Y7x{ٸ\*Dl>O>Sygv 6eOWvD˟Qs̯" Y� i}AG~bVS���RRRd2\.OKKkvWƭ$|N8~ӽb$��?߸y֎A9n~׍K[Oߝua?R22S7F�Lޙȇm%Fv=};%1'"ғOb=ZݕkO#IKN@+KE0ц|&#hݺu<��:m'ܾxKB2]>8ep6t'% VAgwo۩)7im'e>dS'>ȡدz=r!?+�v $no.!�HmwJs'9{<YռuV曆x?8yL"bP:fɑKk{hv@Voܸm6oެP(� (([llNgVʺ +<>25,}×r/`dWS^c7-q@r*/t@}tú)2o%ͭZHQ��[rСGXf]kfwDI۵&zgu) ò@�wec .8.4:#'dyf>4 6TX^{cw 4Us%#Vmޱ{GNw hYeԵ\f%}W233311{6YYYYrr2�ش m @s9^y])ؒ6_|[}eۗ?jWbKiuL=Э 7uG0m5: +תGb> �0}#3oIC yv^``"g?;Ib5|ʤVH)sjSzзO|{ES^ (w#}Ɍa.z,:%xAR{0K;\\^vvư*zGNs?js_5sr:T7i0س8L Yofʐ+[9V70nzbiʊܴ9mi~id�lٕthc;/NbpȮEK,B 2cA4u ��3׹#4YW�lY•L ttPXv# �sr@\.Craӗ¶}6E9]F^9Qx(s~^jFÊ|̻9m;s֣g/^p1r̎4MqqJs/t{` i@Wѹ4M#=W*++h4�dja;w�qIII J {:eĨx[f,3|#.��zt(DH !ee!Tc6uPQ3WD\*}qM욱}5=9*ⱋO�0诬m 7"~OkjJP�Aswf&9h1Y~67KYJ211�8w}f|M]*55ʅkrL[^n,� O* @[[�['8=Жpo%#6$ԛ8^"1 CqQ^#ݷ� {Gs.�h4�$l{itkFPf;r@uzB-'n K;bاlY^ʶKdž]jj܆)S{;= �Mui[* @Z<5e; A]x}�ozٽ{Ǎuֈ�Y``YnJrl6Zң}X}+[@SM-$@]SqzCJKP8fOElӝJ,];:ۍ.1!$+4~>qK�@9/lWhÇϕ i7yn)9!-nǣk檬FyxkĔCyĈlp꬇842X܌O-vELafc˯>(/u/Q7ĔzLpUn<#^&]?;�@SZ@׳NLڲG ѝ %EV}wV3b [|dQG:7Nig֭[CCC,Yҽ{]v4{~Y](eֹvfLF*N9LSa W_s<5ȗ[19>_u8v&w^񓭩?pF6!CݸvY[q.U\s_lw\ˑsB-8jʽ-k ozuܘ]K/=^rKsr!&oQŇo4JPv<ײ$1k'GƜ<roBCѤߟr%Cr,ںrkՈW/lزDeKS_۶$%*W�pYĩ>gf/q MLMJ }A'**?ذa[nVUUe``�� bȐ!.]�@haXQ^jJ�L=ք_8<oM֛ Ƞk׮L2%!!iY[Ç X�#2P*^DA!tX. Xor@P&@ll޽{PtӦMuFfrUxxVݧ}6*z׉<De aZskEyu=E-Zfd7kSˮ=B:Vkene#c22AYK;\.WTBYְ(//'-,x� H$bu vA< &$fMH"BE6!1 mBbAڄ, YAh&$fM1ˤZݨS+o4pn]hw9[N��ͼNy{y8T`?sO^@"gY 6(oک/^`̼aAvV(@r]} 9UolJP8_T�@!>Y1*|/Uqr\j,qD׮G1X� }җcC7\2_8ML�@rzZ!MNlS=> e6g\~h ǁ&_W g4Ԉ`ްNٻN`_16@3Z'v4(UBw8=M@\;ܟ۶Y _ah."�� FrTMtX8qLzr��: n lS /O\\zQ04\j 0頋++'a?N47g ݇xySHJ1z`e_D`5[Szbm-)rCH$!Q ;S\G+C&6y; Aj()C$wZ�3ѧE@8_,1jzT/J�8|EP���aW`W-?[䏭,]a1P8TP;Pn˛6B V1ҟ/ꊃ8ҐrAM,5?…WOsUC,{ ;{r.sXv#˵CNWteUc �4�@8'ZVcݴOuчە k1KdBu  ?ϩK0�OE/`0Jr`؄,�=(:hXK̰!޶> �pi3QJ��9{B���!$8Ԑ2�^ ^&�g 0dDYRO u@!��-? 2 �jq<G?oUĿN{YqKo<6WEU1VݒSk59hcBQJy NM)N;qmI ǝt4fG�QEmO5 Q]P]ʋLnko2`7[FkYY .TRJJ�թ~ >W[<^yK})QB sZ[Sa];P|�P],s�F`ƇML&&Es3)TpP׮9z8o�� '--!ccԑYl  as] 'vӊ*XӶwT;nZÜId `. UO]��ɫa6cz叿/)MHZP_4I.~mL>f/]dr;l5$؜T'P iNթuЄYBRob4371GM2r$ K?:KU hg\V 9srTˣM%+W0d`g8!}TiꇫQs:k?ԂkXG|/eTT+"|ԩ2Ovթ,Vqa/32 26WˊpyÓJR QzjZreiչbĝ+~z2{2Y9X# @D+ךjџ.coo+~O׽Q^_ M|o ŸBEv(5J_wxYeLFsBoP WgA3$fՓ樗n}fY? xF1OQ, YAh&$fMH"BE6!1 mBbAڄ, YAh7=wbOWq%ʼ]qJO=ݦQɐѸͿO2{bbݿ~ƒgL&lKg]C+.~YhS^3I:V.]!kƯ|haڔ[Ss5qyvBh^{Y {L{ xT4N:wrlqZ 5NLsN.j^/:}?PJY~U؊k|<_xINz}uBYػ?cpy]ٸ~+tlKgr�Of5.wp��9 �qظjZ}s*xͭQsyQ}l9y;]u}όv#狨5B�"`_g.DG_8X]lΆv<gPv[~.m}_ou)/[mAY/q"|Xc~ԻyԊ=/^NWlt?w۱&g9wa΃珯 �?.߶d}_p�}{0*…g-oQWX]Gc.F wחc'8؃_b]O6Cb>^͜Y_9riN9ubӧ[[ޔϺs�h_L{F^w4 z#v~p<}z7ZYvw"v( XԱ+C>;'}_^MP�9h szw��u~^YHn],Q�sL>>^N~vqG |cm&BF_ ft@Pԝyki�%gOX>ﭐ@8vE ^5<`@K[CٽzGA睱 M&?+$001Pq`}s ];=�qI%CBvɢo_8]f>}E/];ccv⽐&x,Փ̋ewy3ǏYuoq�hޡw=(x{>!]��3$ `ħ{,y!#ftz,j+kJ+%55P>͝1a6|MfC@:=zP[ ]&\Q#Y?7E,C{<|3;�Pc>Svtw"vt3ws;dt60#*k�ӶvYqL_a_`{2rT9]гބJbn '(�AaaU�+ZsU HFm:Ƞ瀮)H1rIrS5rk{S#.:ڣ9L홅&,Ne`i�,,{ ULj*zd @qwǼAwd׭ {k,Ho[ M�g�{iƖf�Ͻ[nj 9x�ж}[\9p8G O=x"78N=W"�9a_B? |o 0qr�\y5.s77^[&)ՉsL_efFtRğRfH&a]8}K-͸+157�@)}E87+gtzzoSPݝ|ԃA1#˼ϴ%�SU^�`q#P>sF|=w�P|AA# WGtNνUf�ua]շөHn[F"#��oRPD YV] SpI&?+WEY3@AF{�>wxI=])AՉ $U 3aR �05Ph:/�dj��̲ ttV�ʲJ�@/s��N4"J+)��D,yqClmMMݹ! ö/D� Aϱ|%e?uܹ�&ّF3gǃ32:3.V帮\E�gߟm䷏[CX-+׎bSP3qE�@0С-�<'(=== 'HJ c麛"/hTSTU֤l<u{3ȱ҉/?߱_Y �pM&)T~4cLu9 TV+wTZlUnA1[]z ?BlҘ tŽ+~,wJ1h>I ZhTՕ5"g&ގ_S"mD Dz1R��[UQYMKFrRH_D0@NՐ,ib%nzC �V uQޝhzc.o>ba3F藦WwX`O Oxﰸ-~ k{x{~k!�`Rmfo'�pJ nm7K.0)z (vrpKuzĤn@<~ŷ:WN^0au@$2Ы;@ <a<Qdq+C[80F�F:fBg!�%z0~NA꫗Kū:egO_$(˼Wŀu.4G }yq}0^0y.;܎ p`fJt^MSuߒMqb0bim�_&vn!"Dwj{Tk e7>0)FMceeer�0ӹvg뎶���7IDATjGH=ϖj'1W4`EIZzeNmnbcPCgJ:F@sWnF_;ܶ=n޺za2FɨW.a5k�4ͤ\ѳ{'lH N{jk˕L^w#Vp™uCbZ'#T%Z޿6@l0nqax&FpƞWb~dvl}7`_g9jUȨ_w/:w{"~ʕ 2$?ŵѻi~Kw<9'->p|XDئ)75�Фn]sc.>({<4Wg<::3[Qyu;ݹx}.Dc* Z6q;U]S Q< &3Q<lѳNl^{mx=ycr^ݱֻ,l~w9׶C*$ans&?swXĹsO*vEdMVx,>/Sʫ|BYn7M; .svXhDe`_;d]nv4O��L?/Q_2 kDI^wv &oH6!1 mBbAڄ, YAh&$fMH"BE6!1 mBbAڄ, YAh&$fMH"BE6!1 mBbAڄ, YAh&$fMH"BE6!1 m?jo`8����IENDB`���������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/img/favicon.ico������������������������������������������������������������0000664�0001750�0001750�00000012466�14767006246�020035� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� �h��&��� ��� �����(������ ���� �����������������������������������������������������������������������O������k���c���*������������������������������������������� ������-������� ���!������������������������������N���U��� �����������������������������������������������(���������y���������&�����������������������������������O������6���!���[���������y����������������������������������������r������2������9������5������������������������������B������s���������b����������3��� ������������������������������$��� ���������x����������_���^������������������������ ���p���t������B���b����������2������P��������������B���e���Z���,���������2������������� ������@��������������������I��� ���~���������������b���3���\���)�����������F������e������;������e������S���������{���%�������������������!������t������u���_���8������ �������������7���������\���������������������U��� �����������������������������������6������������a��������������������������������I���������������&���W���2������������������������������������������������������������������������������(��� ���@���� �����������������������������������������������������������������������������������������������������������������������&������������������M�����������������������������������������������������������������������������������������������������������X���!���!���I������i����������������������������������������������������������������������������������������������t������H����������������������P���.������������������������������������������������������������������������������������������������ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������%���a���������{��������������������������������������������������������������������������������������������������������n������������������ ������� �������������������������������������������������������������������������������������������������`������������R�����������������<��������������������������������������������������������������������������������9���������� ���@����������������Z��������������������������������������������������������������������������������������������"��������������A���O��� ������������;������������������������������������������������������������������������������������������������������ ����������������������������������������������������������������������������a����������������������A���o���7������������)���z���������������������������������������������������������������� ���H����������������������������������5�����������������P���[�����������������������������������������������������������������5���������C���������������������������������������� ���#��� ����������������������������������������������������� �����������/���N��������������������������������=���t���<��� ������������������������������������������������������ ���>���K����������������,�������������������������������I������������C��� ��������������������������������������������������r������H�������������,���������O�����������������������a�������������������������������������������������������������������������������������O���J��������������������������������f���������������������������������)�������������������T������������I��� ��������������������������������������������f�������������������������������g���_������P������y���������������������j���;���-��������������������������J���������@��������������������������������������� ��� ���N���)�������7���������������<���������{���>�����������������������9���������������������������"���������������d��� ���������� ���������������;���Z������������������>��� ����������8��������������������������M���������6���{���������J���������)���{������������ ���O���������������������]��������������������������������������������J����������������������������m���������������h���������h���7���&�����������������������������3���������o�������1������������������6���Z���"������ ���9���N���*��������������������������������������������������1������������4�������������������(������� ������������`��������� ��� �����������������������������������������|�������������������������������������&���(������������������e���"�����������������������������������������������������������������������E������������������������������B��������������������������������������������*������������������:������������������������������������������C���������������������������������������������������w����������������������������������������������������������L����������������������������������������������������������������������&��������������������������� ���L���������b�����������������������������������������������������������������������R��� ������������������������������������������������������������������������������������������������������������������8��?���������������������������������������������������������?���?���?��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/img/gha_annotations_1.png��������������������������������������������������0000664�0001750�0001750�00000152746�14767006246�022024� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR����>���r���sBITO�� �IDATxg|W?̨ږ,+mFojB =$[޻w{b0c{dKVl0|/if9g4B"w ~d!IW�!0B0B0B0B0B0B0B0B0B0B0B0BxcYVDzP ŬQ7>銠?˰hǦP8GI$jIW=Mhdbt:BMqmu:luAOHgOo_aFFa=}tEd,Yj+50sh2JmOi2q@=h Ѩu!E!E!E!E!E!E!E!E!E!E!Eczld�C�&ЛmQZtBh,p,[;oim_4C(ulW&!1!=㺻� O;C.]i(@禱Ҡ;;"љsgLxsT.8iVޫVgd\miݦ9~dd.-){ŋV'~ FhJg DsP¡PTNϞ;\ߓ&O\Cxxx^;7!e7_,;mmVVVhommmiHTݏݏC#<;4v$SJcmyvگ?]z葤m۷�흞q=!12mze˗�Io}{/]N=˗߿/; �qqq� JjzVq}i}q?Q4=lF6ajx|�[8,"lܴ%vnU׿9!yIRw_VR0toF貱�bιUQ^rQ/]MKWJP&Nw;M&_z NOST/ }p _lmkVXTTYQ1?[^}l׬Y2/z�DEG9:9oHdt:]>DĒ+r'EKTU3/I^^vvvך�74׹yx N &L|`.hggՌYC,  bw6Sc=Ur{a'Js�};vnUAAAåBR�@UUM߸'mXoeeUׯP8PoƬٙ}OhS)z�Z[[Ί  X,Wuo2fKPhXIqRxMA/l9+К FZ{ ;<FDqIQCK[z.^:z(�D�wqGQd\|ܹh d171zE@'eYbh \ێ=6d,?rH&�PN�Msh~0 �en C?P$Ww+ǣ_0 wvvf\HINNQ!H"6>7;+GshXVj]JKWI|&enTq<Y�w�탦Ή�A}utrƦ~~`udtswhmi�;vdUUU 7$3b8o<>k`>~��R_@@G{;{�t^^zqh2T7Bw:>jGYk6T[NOwիW_ZFխR*/20٥ZlYiIͷ%0Ã?emkk 7WUUuvvffd(:?Ѷ=2;YPp�so <޾3gR<Ѡ())r2$g͉HinGJ;Ϩx=?_:? {eO?e=l{>f]] ŗqgxH׮];s˗,YI̕+WXqّ=RJ՗>' }1{^tq45 ^b4~G!$rNˈ=3e+FrʳrϨF|={o6Y~�v?ZzJd?{7;@.Q OB NJLzp+탦Gk^^_:WRRzp.PWojMlmj6*Dc>*ޭpάُ �}d3:! Lg"Lg"Lg"Lg"Lg"Lg⢱HgLOгQI=M"=j!OF&='] 4tn%HB(ĀF)PHDc=>?;UW=8sQVVV<>S#t1φV[\+x)'b1{kF? Q!<!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!hDlmlwUB Q:j{]BFZ,;99? C #uN66l2=j!sG#HRT$z|B!MgBcϨC!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.tF!.jj\n#TB}F|l2uuvZ'B#Mgg^[XkBψƝerdhF13tHzzzwUB Q: "kFg!a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#a:#i:S}WZY^UU^z@j,W^s \pz2h(P>0 򃒒.)-ߗW sXW|Ҏ\Kp{D=3Bà|C.L&'U<S~燎8�$3lLWnӻY!0vL$%YsޅONг@@-\~[Z^vSB7 Kr/`mʟʼϖL{o/g<ub`jfQI~_vD �t' ioiUyU � Mz?s%0?Ԯ^dO(`w[UVUU|Plǎ>ϵ{~u<VAqAߚ'{xqUW# %͒�Q,z:S\Zp+‘/~iBw=|GIJⳖ[ƄIɁW~Ģ|{ќ˚ _};C&# F9ǿ/k>=#'Yl2[9YXWu>Sg{�X]Ϛ �ВqWК~n^q9jF$�: !~BM&F5a'5w՘ڗ_D&=C|gSCYl CgmU+lvJWBy6V67)9ʴKՋf矜xށ|nZLt]հ@Ov{Le7!zZ͚fO�glyw;&-{/bcϜu贈#Zhɢj�sS&&$.s%&$̰g[Y�ruK-ZZ/ENz !; ҁjo'(iQQST`b Y~PSb]_/`:#0FL%%x5=-gK]7U4SHTB~鼰He{n^f�DB/`9 L]G ,)ڳ%&;@+&AHmF،& r]*L=YK)<#jI.}b  @{,MEJHe<R~g،lP~IɁ|�{p_MJ h`p5(��l2�@w3-mX�@Hm%$�RFRÂJIhJB"j{{"%R,Rv3��(JȤ;EKe.@o;,%KߟnƣAcw:g瓔r̐n5@9+�L?;𣏕߆}s|\.)rJnji`G�je:]_�VS/%oߝYB|3o,ҙOJ s'3&] 𼓒Gw,4 x Ri"5d;⤭- ԆO N]J/Ӌ쥛&tl"29,ƿnk<`4ٙEvSݬ]?^Ա@8:Z[By6#T@rR�X]ku&ҍe ^BUJ03~k"B>Un�DD9KJo*KWʫMl p_SIO rRIXoG_묻dZ;ߊز;c 3?!*{66(,r+5taAmL&&5ݸml= <Ϥy?̴7q$Ne(&_^]Kʾ Ǘ"M%E :Kfu؉W~vLϡfXuݭ=y7_]*iӱ$m)<׽v坿RArcyG<zC'  !a:#a:#a:#a:#a:#a:#a:#=L:L}eSSdY0&w%lcmMsٺ|2]vEЎWG#RcVִm_P�Ǣ~ c2Fl%ֵ2LHcּy_GK ]fuw)>OZ87U& mNOԲ6^Zk{+]0-9JVuӊگӂŴJtsmܔ$kt!t V ^3/ie,4x}~d7/q55t^w,>ؤ@ }g|е8}Nnlocc$O pU8+O/r#(�Uow%㗾?:Gdže6Fâ]/ai%'+Ҡ\LJUV|ҋkcJNWf/1nX#rL]@: W`82ڱpBh(crTRv}sz9+<ne}#xxHϻ֍V�/!!v6׶�@tOMƦdwśjB�U]�jb${co͹j`VX˥P!⛥u7 i��FwRMB7\F=1c\A֬�0k&{ɦ[&ӭu-Vն>CZ>"sg} 9}vLimO/QqhVHRf ]kj Ð�b+S7):eZM M'dϨ#(fgjllw bjjuQ-Γ$;5ڻo>x@}ӲQl3 mSgٕ_uL\76.X3!S>{+j|H h{b 'T "ݡ2Cٓ薊:QĄ )mB{~VnBѥ##!)J\bg6w?Q7gW3�gƷԵ�Д{a7^6-GU%SՕc <1"�R2yՋ$m2t^<U"O�5o{x1B+VN>@='G7u!y^{<s^pu?VЍg 8G6Bۑ 4Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg")5ȍ|w��xتbHB(۫GX^R"̓:kyKF#By'ʄ0"A{1A:L\e:U^f��&`uK&N+ +Ԏ^()vI7uB<g,~ 3-6I/Na+Jی#/xu~ٕ= p;Q��^)Nӣ_͸5?IXFK f��B>i;nvd)G\o潶n`Y6v5V޼fz{N36mrޑh -z�`пTLDũoU[(K\B2iu;v趏Io<ծF -NP_:Q[R T94YJ(7ۥ`apB~\;gf7wn?[G.}{8*pz7Ԝm6s*W i40k]? N,Qkh9pBφ9*TR^Lʭ5�(/;9w6,ҵ-J&55]Pji�+o99 {89rT;󼦯K6w@XY[SiN`͚gX o_'LZ/? Qc4juf\dVm\ܜH_W;5eQwTe]>٤c 7NJr3DM -&8 ʆӧjt��4`VqNQ\qV ɡ25v7n;73Ğۜw̥n�Νh'撬 ~\̍ Yz;n?b!='%Κ"н59/ݨt؞ߜkEZ- **a �|3B}l^ѐе;?&&dɚe2C幝a*p?o'jv²iz rM)2ڢg3*ripE3gzؒ-eWO\k7N|kc6Lqɫa)1rٓ7Z,�gΝhME.t:tƥlቯOVѱd!3#EƩ##3)CsS' lE{9��.:Sg8.a~\5Ϳ\ ?Z;qFwfY6ɝ|?uAjٯDys'(q#ܥZCXSsviˌ ɗ~E\eMu7ZǬz3PG�G+FTzR�! Im mS}a�� �IDATaF9 Q(06W4-tey#$B&ohw+FRzBB9}to|o]f}--�*Wv@ةӺNim?\ $6EKsGs2:jV VLP��:aUCo+8Lg^Zm̼(9GA"w LZۏ��Hi9,@3lSh]]g.^х9w -JtqWZ@<uigFYURevvS$A Vx: .ޓ/ؕłp4Ӷ>VX4q7$d`7kh^':4~-λZGP|I�05Z]j&UK^dhax^8ZFf<aZ"Eݡ&I;6r@V;9mM}(}ʦNg伥͟gt2V��HYTj+OM,nPݝC;Ɋ֬ZKi) ^;8 b/&HgQpéTjyRG{S7@SzJo|rje:= k~dj�\rzJsB҂([͍5tXfYwm=/?嫯2�p1>VF!KW<h4aб�@DdTh}m] _h>ϳ4�t`J=n!fɭ,:{Խ_xN­Z,P�%.syjWV5C S"Zd2 e%uICRj,�@yTБ/dAIle$E02r MUlo��aem'K`i.J\,i:oJ?|@v\0}xSDžԥ%FΙS{W2YߴnÞ*m훕vqOxkT{G*LvS^~+MlM4'SLKB Ě~QE ++�:RV Db7mH'ߜf#4]XBånRY0>G K;vv3@`f)q"lCTdkfDg]Qa9o/Hl@+Ja[6%Ȇ`ڮr>2372gR5Um [r<ė7̰𢡊VloS[h(VK wTo S;T6o‡Npk&P\mZ;(ͽ.58;;'t§J sՕ+nJ  9]]yt™m8A0A=탯E-_VX�&צMֲ�]] #Ň,� t.E_I Z;ҀH)rYW *�[xlCGY!l~�47j'':EZpf|Ԧ_!恅UOs4wI=yWҫ�uօfFy.q<A]ZΠBw}%`O��(C?-ZRy@�/�VNS筵|~tfnй5wȭanЫf[zG}Pϧzr/f411&{y@NKq)��`(`YKS}Vt3`m[n<i }Ϋob "S63(ڕƁC(EهƥL W( (;LWu�f�B`M0kkmcE@:SWƢWT_Ȭ7Xю"؞. ��k6a)2B_tB0U%ld"ֺz�XZZ ��<';HI7 6܏Qj�P* 8<z` bKsCG|H{G x ^ /龷/֔ҜܲNCٖچ {;mV4�vUu촢CK1ɞ<{˻OD6kFJUyxwUw)ݠMf I�[0f\(�0u5UtǬWl+š!^58PPZ HgWGu뵑ux YPsi��{{E=2vjO"&]Re髭U3x? @gF95>4R%)/jҎ_p&��Xc!m5({X1mi&Ey7M@AܞX� '/?ŝ.M;xQ4yQ40DDdOk<;Fy}s-wKXqR*̞^uދaI� (Χ cXuC&c7k �e`Mݱ'�FSvb`'շ{f# So2!uN��6x[i*7zܱ#_lmeQ##pw|,;+ C0VT¼0o^)0PTROyſ~$)}�H`z!Gk n9' ��R�qfNX=׉ϊt FXby bW?R2G>*3!%1.(zrZ ,F .l]~f͋d2/ +z`z=,!wrVQ|5HӪn 2� Z.U] em#6W bV@ Y5Q;P4";Rmό׭KhnWgVUY;{z + q 鮎n6ښm)SVbJ!ts7U6ib|kꡫJI>b *;{[Qz �_̶V׫33m/=)%b=o*mh{GyE_,sv@�KIgA*x}nGKmG=*ɚW�@BwSF�aneh6�憢rfIWXs�tåOp+}^^ط'ڰ4�XM',?V=̺;8H=Q6t~5A _^zxW`7}7-z? C3=nYOO󎉴67;g:N^r0}')n-JEKÀ��HH`j,,Q2,T1<lLSM$?>��M3w[azY͎K aaKT0"`ktƲe 3=|y3]C:I麾JDZЖ);OT SUJKȚB'{oEG B^Si\d>>kKl隓.o.^<+-=j ˵*eS4.oqг="DhPjY4C ^k 5g͟RbJmhsCoXj ύT\E'~z(��,Y1y”J3^_HD^iӭE=fMhK-UQac\m{PW!Y1:s<��Fϙ^{V;m0ݭ\o%KcayY46f`HHtiY :K ѱ$֤VX�1fU #fJE:+9@Y>}c,UN̛;p|OR=pͼj& }fTguտXV]x`SJ.琢x1nt 4ث533"b=-E&`5MAAQ~$cP\tj`#bTKb|fUݭ,@W]M 4͡)ճ mh?WWQUWaڻ7sBGH'z;jxcƲ9Qqq]mL:KbN] |uOԢO WU0^h@gޢL],XUUM tKFަ1 m3>5kbg:S^\^`wudlk<ebj,<zV7{GԐ04g+VAQ~H_.#d$K\u)S_x=7lU[t�;uņdkҬ˺THH#; wk,-W$Ůݒ�ږk2d gn ,k(6i)<z\ZFf[vz;A;٩ڻh핍w^qnahA3e]íwL|l}mUDЉ++~pR8+ZgوC_-`I#go碌l=K}Yg?u̪w&7؞ ^:H|w܇J򃗿5=gl0}g9ѱ/?tJuu)WxTsmJs'heqW<{';Ж6O7fKz+a4hc0ipSӹM|x*7=-:w�W+>AzZK^MKoxFtHז7+uϔsC:ߏ YjWnc-̊?C%O3?=Z<>L{O\Úhgɬ`tդ9{/VQKZixƳ=<Vb!hQB0B0B09KԌq6crXNg%&ޱq_a݌ Uxel`oһڏ]Y?ٯiֈk)~j՘+[hhnO\4gdQƲbϨ#Cd,) d8wt {D-IN*G`}o 6qdFW֏Aקn[׼ cQ�V[qeYk'yr}QOs:NM99VBmoiX�7%w0hB|Ӽ0^?Y�2{י Uc@;+l+-9 }X$8euD5|ЉfȪ�e1y"$>G9!왜O'o +j׍�(Ϥ Nq" 8)DNa.!$7r 1^h(YHbJ\-gKCJ%h<jG io,+HV]vąR5@HƆH`-Wꇿ!*nl{!eVU];~! <rI>v"cݥr4�e_YtΕjJ;tM[> Y>Jv|lƬ(pZ e+zukqV{/wf wCaQo0dtlG7�坰y\Uƻ.=RnPv$$vffe6ita¶M47_v n.`5UO]( k0_:4Oq:NA\+%yj3O>,5}jwgKooJul>6 �"gs(w]/NܐYM}wt,B Xy˽śOkڙ6XuYas@YV+$*՘ Hqt-aX҃壹$smV|b K%QI(vw*XQ}]۲`Jyztxby! '-k*p�MƾON[7,eXI)oǸ_ja&MWZ�e8o>UW&u|}^7;ŽYi/3߮۫3`YWzy3&ԗ^XOz߿h>:]1ҲcYoFy kGmrbf0hʼY9 .E͍TZ3%;KVQdܼҡx*G+�t VWVBG�h4o؁OWWTe(T8<ƴ|4E7t,0*_p!8XN�#,%E4�]wך� ձ"Bږw@ ZDC.5f܊1b{(:ProvX� ]"�DK|C<+E3�! -,RY�,>ytGQNSsS_"�C�tcqY7 `jWYI$$�á,TB'+PX�mJw H{w7s]y(b6ҶmkX%MF�ViXî/VikFEƢ՛"ohkf謼~dg_]iwݸ8wRxhb6 =B섬"%3Sw&B䭷5,[^;)&7t#,˚:FvRFR('O$:L�x[Cg#�&~j<+[/N�!v7;IҬH݅"��@XEn(%vNb&{?GmwB;)aV=fʎY�ve= &-޸Z[w3tZm>Rl%2n͘!dͦ04!�߽e�� a-Y!�"+0$͟0#| DBQ7˚r@l of` * K�AfaҦ.�qYqbӧs-1ꋫoۛ8ėdK.Qmht&Cy7ߋ�Au~~Ì7$!A(2&�X=4ɞ5& rQ9w|�1mt/-y!NHe` :܎7+Svڪc5 hJU㗆Qcl׼?ʫ>(a+k1zA RbUh!*k1A(�+k1a�V/]wf'M&PԿGcF,6v�`Lw`O91غ(܎᳙ IFSu>os#AP_ !_{2!}gfY,\ bF[hNJP"pT��@ �(yp}kc+ z R;�) tIWRU#."�ϋq~P L5%U3'xv?YS$(yx;.,ÒYCے�}@3lVqN|�B6-%9vtb`NXo Ww)ޮN=|`gǣ�QPʊ8?�kn<`OǪj4~r 0iˋ=wȲfhv @نD=KG(�Ƣx?+Ζ6O[Tˤ<檉G\_w1B,^W=Ȑ؝qp�NE%(` �9{Mb�֬nP}@~ut[~P?"-ӔջtݻX.[ 5T̟϶z{D3~\1@g_TO}t$&CWٰ-W�tsK7lV+t^d7DYs%n~O�47UH֎<yt]nfϢy;3U,tn}Х %kߟh6\piMf+6;*Y�X3g-;bCkœ=sXږʩ2swB]}f T=-9Yzqj�twم0mG/͝Y(S2t Cm̾{UE*:/X,gS�� �IDATu,ʹ/.J!J`e%?ʐkw-12"N[|Vd-V 9XZKJ"l,u�`&KCm q:��4y)V-*fxZG6�`:3*&FK+.Yql Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3Bq3BqHXBN#MΎZBw‘ "Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg"Lg⢑A r#] �?4'R2J(yDv*ANZшPމW2!m>M{Gqr4Ѻfeg+N:L\e:U^f��&`uK&N+ +Ԏ^()vI7Vb|qyܸpA';y\iMQv5غί5;Q��^)Nӣ_͸5?IXFK f��B>i;nvd)G\o潶n`Y6v5V޼fz{lN›U'+ӌM\o~|0w0ڶBK�</ *:,$2>Cq}LZztݎ7LQ s_Գ4Rr7f)-)vdfng,% Wv00a5n͛  Rz zO~@P<Ş@:PЦbN�ju XFv|sYq,Qkh9pB4"Ln̩R߱~1L&9*TR^1*楦rkMmic�Nmy]- tm?b(7;њ^k�^h}CƎIؕÀIԕ;~՗YH ttCs;3zN(9ɿN^4֣h:k̸ȂymBqqs"}]֔EQu|f%go+9x7O7(*Ϧxϛ$4+rNJ�PҀY:IxFUsō+[ ,\&zD`(ڪnS޸9NV~p_{^os3Ji�:wjTLzK.o3wxW}sRrV+"H < 8wg}۝gޝ=c0d9IdsΡꜪ$®=u�8Y$)<۬s7H-N{H`Gw,ʉFj9?@r—r 42hi%'),�0YLҦ>zՄy(oX9Kho=Ł*-gҬe*Y6USmK Y{QO)ܭã+"roW(u70 f;gN�Cyy)pwեLe+ K_z/{Fs㒔UyJ m{N3~tѦQlt*.$Np۝V|7g-i9J{YB}xmw5�춳6@x)ΟYkoܵ^/T7I<θoU@x~Lf$n]gEzrݽ/>Gx'ό,w.Ҙ8f"B&7bí}~O\W#~ڷ[ (S_Õt$��!R/Yjo5ÃG[<7!Ћa׶߹rf.ǵŹ>[(UQ@x1g,?erb)tKi#��\֐ڿyS�BԐ@bJ;yFl9SG&>iB?QuޚϪ K_.e_N:+;- X˿LJ:k>rq~K_Gp%vkt�IV C:O̢.dU\68{Hf5/eHC- VQd\5|C59+&P6;;H."ccE.YW5�yPڪeni8(c JYڭ~%ڸ0CYNTtH ds֭r-7xV ^ӽՏ# HS :G,S?ge,!9x*- e 6?NF<(y(mꩭb3VK/_{246rcsWtۏ`I<NwdTdm<r� BcL1- ?i伥Ρ6:Ro/6 .uDPOK �b Gݭ7 kKxJ346o=V,]g[<Ug_}m½M$�WXúc;x"˧߶X�h[-&څ^` �|�ff|畳{0iR%YY"vʾ ų0[vWԟ̀Khbˍ��+eN5d1iƝLRwOIatnWUQ;�ನpIώ%8oo.}|i�0hg׏'sZ-L!�DMu7[.>suFgy]W Q;{6B�ۖ_dxKQ!joE忳ajv-Nqwr}2> KMSWjMb Miy< ]16gʫHh(wߥ|!0VԒ_Y츼  .D'12-8q1t"6@"`mj#'-l9&=T:$\��(+qf^��Lޑh!c?dclޘ%͏lvJ8:@BAѱ~}ݏM[O}q )x$26T{ *|X`8!oݜP7",[3gz��0Xs N^o+9 ?ި>W:a8aYI[LWZ� EWh �f&vX|7�!OHne@�KQSa�TQ@-)kbBX-�76~A ^�nL[uP%o3�:o\ۘ��ܨ{CGXnT'mLTT07R|6RuF0޽xK++b"8 v%QJEOuOֺ×7uv�� )w SYg.6QAVt<?*;ȵY7g0Hť[[rS jD+" /!G&0 �" }}.�̠5P Uwt:7epT0oַ?F<LQ޺ׯ1Ba�̽=z*=19XVH KP.#�@F}#)\{0΄@6@k]Fz5n��r=ph1~LA]4$X{k)a�0b pg/x �7D1= y) !1 �EOiuq�]1#�V-Nzc&}hgDҷ=~k$�0bM58n|R2[yѿ1s0p FpL4z{Qut8�S( {+~x,[o^,b �ɯHяuYdU >^rl.fR_p8x}j_yѩ SG''YY !蟭l'?b|}bڔ9*-|)+i(9qmcG^N� D�C_0 @*Tvr;JIN0@^ {4 �%i+aƒwz-ik+9&l9Pv~} /Jwq&RX ƲrWVzZVU4ݟz;8DYy by{ǯ7hū2|1^1@.?A<g+_P� u>ҏ?題 7^5ޜca/ԦsԞ�+,!mTIMV>T7o.n} a;r'b�ȁ&Feq@!mAD7_.�'02UDӊK3,_ۧvsZ+akgמ/'ޫ-ԙr,B4l/5[$/!I*mS i?cSzƉ0ejnjmnjmnS`)cWŹ/wn?nB  �30X5A[J`H__Z=w2cH�{��_$ FkEyp]ݭcr5O OZXjD�I ��#wj)cӱ$1I' �P.ڤf؀�@Xzf[<qIP�X C R CTgժA<XI` 擎DmD&8(/>4 B)N)9=oQ;j\'݂DBweitDa3M. ak+\#z+W=ԘHDFuD4=jv>5lIEx: ?]o# U #Q6#ǿ{#gwy_HΧ%X!)ɒԮlxN|v;8Ǯ|vYۚ?T,׻x0qH$eg3yӞeosO1Way{ ]r9M1Q ��8e;nO_ : :=䢘ys*ej(�jTs}ly'Fmc=.wW)YGk3kv}q]=iN'-,24U!-:3]tZdIi_l$@46cg{ƭx]gd{KplwZR(_ܮv@a.e[EvCU͊oz#)c}] ^D Npr~$ۉ2FțPYd@ƣG'o-L:g>wu;B8%K3 'Z'X~,58rIO%2Bj<2|mʛ=xQKbsR}14Lz^KڒSu.<Xbŕq#׾ 2rfdܼdՕF=!IR�XjsVHw]i�9tV<q7-bo��fX9D3}Gj~h3$'d>W*4MAh]cLL&iԎ`،hXcыɮxB%3^w=hacP^w%gW^({86 YAi޺{/wM;lrp.' ..N5:: LȄD=Psȹw YeKleZޙ+25#N}u՘7@n:pߌ GOK o;:\9+.&%}O?[ۣ#=~ !j=<}ہ 5e]mB)wv/ 0:zFc{NlTDeh2L.Z>"NP"-KGw:�ͥg̈́bܤ�[ 5CV'f8Om@dS v~Ph!IX۷^l;v0AM:{J|H^olې1eѢ_c'yx㗿ܰ@[Gl;vL;/ew~2@a1 ~͉&n+˹˷ӳwD^ڶ?ش6? c"Pr[ں6xkcsqQh~PK[Ґ@H}$Ǽ^<0o7iI,�`Cԓ]9 D>ϼ),";5XblOŎ[ZO~Gl6zN=1Y,Xle%"WvE,s/~ #j{G?:lQzHDQ¿5*Vf<Ȇ\,34Y;3<CD0O4ҙn4<Vډ2nix6)`O~��oD8z3mA<IޒlSdf4hd11>2XZZr糅3'9ܭ19Tn{^X6͓uhyD6hh~ #rJ=i\x:ʕ﬊}Nw{֙f&BBCCC33 L44443:L<!+1L|hf"/uOzH1n⺗?,g_=Zp<U"dHZWӓ} {ݯdO9w3 FK6b۪8qSeOm'xB70AʦGM-W֏?Q^eQJ!…Q{pL?qD-C-uSMt^w=mzd]X-yH�du;?>-J]1W<9p4V#55*'�!J g'gjA'nR礥+> fL/{䵯,_.,oQ$y۫d&YkNEaa,OZܜT?oҿgUԫ\˔p*Ueay/*OKMAī0osk��D_U 2wyiݝC ^IΆQ_T{yif-PhŚes[_fUojzƗ`”oƎu1ׯX6'=ݩq �L:ESByɷ`JWY8?{Nr5d�dʑ5pʂ9 {O 'dai7%yYs};`f}yuAr\'!Zih3"`x^fm9ɳ#x^�paW~0#1ʟ8ܡۍc@E5 k?w[P'>33=;3h7Q�5wVME-Zh~zަ62`#/^" M53OMQv#LiR[n̟R$fΎb I�Y(7#955RdW'{NG3^`TݭoTUg?:T߿a}Mヵm^o]v3*;`�J[ODX{& y!\qQ|'y\ƍ.\;پnGt}_L26Ύ-# zD%c@ k<<50iQgݢu늒ޛh ʠ 1p[H,2 _ޱÌח7\&qEZnH_>EJ~O$#'xO z��7θ N >AM$BXw;` B3?I|y�eRe[9 kD�Ӌ{>bukz )>b/f~wnl}  a_8Z x5n��3 ۿg7rs+Z ~ޛwW“mz*޾ת�!W=Y5+@ ??"D 2-u:4zOh ��\6 }&'rReG�ۋZZ- Z?Nx/<To�� �IDATwNJ2TqCý&xKs}4*J0 u$�uo/t:\ýַ"PUU:�H}mՠOD gCs E<lbr%Bꬔ'_@��tHO5tĵL3�&ꬭӻ[G>ؾp]yo)d e�[d \!=O rpsT6& tVh�.UE.8̏�\jp-}o?T7gFZ\�`zʉ_]5�jx?TDvuod!ybwRxi u }�dR;<< �.'?F(f#}Su:jzM3^XT+M277SaӋ#ᩝKY-2pq6 0H.[:g[��#ʉm#�cn(&V �/%/'ޛH2ͪ)��}\ & nP:A {s媪>IL2h` 'fk�ֱSZi uʙNnqr#s$NP�P1( ��cX6e5�0VOg\&`6a" "}& rZE9#&8ir]SkD_sarϨ fNj|sK zrZM3 ^T˔(_�N0 2_]m~sAQSCblͦ� 6$0OHb{ sQv\/ 34|A�@g,d_P. \ٲ[mV?3:![Hv3b_<ծV+1kac gZK1$2'Z'|kQ6-uZ+1,ZdMYl \ �f/x#jt9cOoT06{a6r:\m 2|I3�@9: MW>ؠerی�0|5&2u=}4+4sժq݇Qoi@QJaϙ??mSrI q]#$%$�=��^!J?�! �j{J �S&E ux%q0�$qqQrhhM6ZB`0D!!n  $>و̓YL�Q(k2Di#(5Z 6ӬQ(`|p�8B9�iPMOy!}[)4>ND�0ś&;q #<q agiSجX!òa@T`?0oB4o6Yl+$&=A)}pb(l-mtC\??KfuxcWf(8�[>b��0r6( Q5ltQ=E1H\8UvzxQC#+2'^S?B#,{I~ܤXAeC�6yRbmj'`\Yy=1^mIq)}YN*$-\87WN3<\cVƪADnMcvpP:F֔)ȍ8Ur� \楾@I:F7HC$K??{NJT43bya"=?|QM9z Kg&̸A+zع-:ʎ vrf1jEyIJ~Ջʅ9syO᣻Sr,N>Z ag߽f-KYKYnB܄Hok�:ۍe4?{NrNu0lX0'%n 4z׸X=K8PU=9(,Ea&/kyDM^O!wn6m`Smǀ> 7q^~jdx0wX}e# K uV°0g]eZ⼬D^{NG3=h~" 6/S;/} ƽnoJC/jd��N{(LYxwxg>O ��պt[o-DұiБ ٠֙f&B[gmihhhf"u֙f&B[gmihhhf"u֙f&B[gmihhhf"u֙f&B[gTwߗʼyZgz#44443:DhLCCC33 L44443:DhLCCC33 L44443:DhLCCC33 L44443:DK/�z䁆f&CϼI,uPK<�LLy`l].kPS?jV~>0×n]lA�8pKΛrdzuEYrSCðsJ bby&߲uqf9 Ҍ-KT8khh~`~9'J�~KO#q?- %�(M?Y}ҍ.m`3[<܌��;_ފ#3坹8CylǞ!9MOW1ufljvydN푿ђ1?Ki`u[C*vkbn d0^↷3?Z5\z '�p sS#}<Yi¥VzGο\R$�lͧ>X*}Đ]?3R_bj74}~p{TyR=byhF�@=:ӳc*c.` E<SgcL�4ݸ:r<?p0v#O))ȶ_r��]��ªovsX;_fIdB.ח?rr'zP_oD{xs̉ =s *��2?3)R!Α֒ӧh"}y޾;\ҸP˃MY.]f� -\Dk.ID9Iٹ9!CrڥA#^ &ٴ-S~-.79\q:]>uӌ�pψy~2! v]O Z7+"zy >9SwTZ7ꯟ<]q�prSlʪ+=wD /h);Pv8ԢQ>LʪpJ]UIx/t1|sDn� b₴)�-I{:ecv(3"]ax߮ 6$* 7Prf%7Q"㸵ݕ.56+Ud?{4?&#d'!5KsM[@^'g% vm n��7 pqjt?P@㟞.Q# i�) h?f'�?iۑm 3'o_{+KhkZ<ѷmN�ɼx�{pL8IXNq(=FA{?%uޚϪ K_.e_N:+;- X˿ �3`�"Ay#ݤWpDPnn'sK|I߇<}BݯWL@ۣ;l�%tڛ:ޑQkU1FaTKXXFSa,XR=R`E%.^w>TD=V"C*o$"gijǷ@N\+tݍ O-^mWE��"$o]mxPc(ġ)+LD^v}&/~uQS[9g_n:(x09\oθ<h'GIח Obh nQ󹃗l}9�x/H~xHj^5ݩ1n^;F�%oǹ.�WG<qz ׮n Q��pKM5'#:{A ~1qjQS?;e\V٥> <U{j~ x b��,_b|ೖf@ֲs\q�.K_?puߍ~@ktr%37xj3eׇ)�`(CmG~9onN8 {G'<y<Bb"ƅ-z7PƮmFRjXsI'kMOj3"l�;6oE_ngzeoܔ񒊸rtԶ?`+j/zzƠ,>9` D,EўKG׌Dȇy EtE0Jg]%o7m$!Ԛ�6ί4?w0X\ܸo.(DxBow4#FSa/gW{y׼W W?ꆚ\D\Y2;%WO_ ʂXҾ{$6 p 6a5\p6|y枆�\i/}�xKc+G�?2!oP�FjΕEğ0)#(-ѳ̥R{"e[bGtlagfV<LܭE }~d|xR`t=,z��IL/z�-%Rt��ƃ#Go>&@_Ey7 Fy (`Fڏu(Wߗ2֯]yiH4w^FӞ�WŃFG~wn*=-kKi2kWjOg=,FeYUtP,nm#R ej)+Z1�It^@0�� xR1rk<.7�`c)f/֙Cc 1?/%T!8�pHs�n��P*ea7o[H< �VkF "�l~b>=Nvw] 4H�J5Qř8�C-�*~{}Q�95mkRݾTx㯢+ޮ1ҍ5&ၡOO|v�p/S[.Zc XS*Lʋ?{]j.�gLp͹)$4w! A`{I@z?N@��XR.`3V>�5FʤՍ;wAJ #$:l9=ULEpcFխk}cj* ~n|62;3y &O-v<?6!?4渵<O:w]ŘLCWpI~^wښJ�<hX"1tS4/H"V_oQWe(yhP� x]7/9ˋRPok7 y!pۍFx �@i.\i_EIyy䌴M9ܨwM?=$k?Qxz6e0�JW[LrH�\3Pqx*yȉ°G4gk/>PPutHD �0u΍)yDZ1~7 C QJpGQ&}óK6 [o>X:n嶥 wlOs 6q}_N>2@)w?vwyK iK˚)Y#\Eh(ތ!:zĨ}+tH p0Du_r(F X Ő˽Ǘ\RzƉd,<<ہҴui||K#꫸Qf/;&z<6)[�q - Z鮹^ںkuLu<VM4=4݋h;ˤ��\L[H ^>wю@LF �D^u4#` R??@*eà �PxcVZU vqrU}?Uֿ.N^↭޷zsF'x=O,嚆 $!r cjHcO+c{ Vrog gX?s.:\|k>{B#:=0Q̼_^zju$𣖽ڦ6ϯ/wjڙr ^<YF ^DQѸdo#\_uI^=mظ~eѲի_[+b9㗯[f_&9 :+|vNܜ/gnpŝ3p_۲|i~-Kn}MD81I,eԋx6LJPK^</O${ńЄFKKm;&q<dS8OyR08/R!>!IJ)".Q}Z[:¥@773[rN(sI,?@xpyk dܵ!2 ^|rA~,VlB�`okhf![Uk( d>cas'yI B\#zVe+*(420`V$͋`2|ŬɊgyxB%3^w=?wuڹ.:L)/!:u={eO{O 3⃥~MYoCWrt^:W&508gOi2YYso<rY��ԟ>.f sEIuHA|̩u]9Ϛ=7), .#uN@aS=2miArPtfo{H^ c=Hn{82Xw%rTͷ5T=ZDOe+;ǎ7|PxgmsXN]w!|zYrܼ 7�#(6~{MEE[簝JW)G;z9G{a >D��PCwaK__$e;ԭ73օɻ%㵄HdEB}t%x%.ۖ8HWpróWf,nX�pҢh+L yMa-Yag`{G?.vza fw^Iع3g| ӏ$}l&^zG{iI3m,a&+z9H.<i!f*#=_L/R|L*|De1~R1pm+ihyXO:Ug[gԶ1A.mߐkT��MW}?-ߔ<k@g'Da9 "=3w$b g9<D<s�k)LSztW鏝rQ^ 10qs?$׳'*t?*xqgBBCCC33 L44443:L<!+1|hf"/uOzH1n⺗?,g_=*Zp<U"dHZWӓ} {ݯdO9w3 FK6q½3b۪8qSesL!<兮:\"\)(gKdR#jRoj¶K_ڟsLO쾲K?i4�,ݷtxY|_ٮQKy~'ʋlq(r^btGY !.-"7B֚;'nՖF[(]پoS)~/@т15D$۔bqJyك-0ATẸ)yA0_VYȕI`mbY 8g&:w|x1W4�"`KYgvlu "/T lݥGWk&Y R6wcnIpcF�� �IDATn ʂ¼h!Nm7Nkj7/JJ S^$*ۅ^aL#cӅ�&[ں+'u[&5LiB^QNMmO\VmJ s]В� Ygڦu>5XHKH9h�ؾ1z/(~6\<v݄�palꅡlq~_,aqKaN�"e3;Al&I-_m ӂ") ޭ3=mMq5s}$`k냉9ˊeL�djtbӨ<QI:tx3)[ !0" ;~t$m *zJ9e>>mKi+|oޣdҽ;O;ęk6.oS1~¸хKt'?m.޲8 e@Ʀ»ezD2Zw$R{l7ua&-*Rx[n]QR  @HՎ/Z8Q ?:̈(z}Izs˅nW尛Z4O2}R?Z^]- �0|C<n;`ԠD(D!Axu"d;͗89oQ/UFϸ囓F�<8_ -]fGmМB;/"iK1rvVw2!W=Y5+@ zHD1|==fyuS{N`vMQ7ysﵪyLX.&,7P. Є=q/d��piR862jUFȣ q\TѣOB[Ъ?m' ƒMuz(SKu74k4wKD�#(.PO]B5; i}+8 UUV Dr&L9:Pc &W"J|e߯ �@N')dQCN\4`p:u/OוZ8O�P�M!7( MUm R1aXFp*ua~?_WW oyq'z]=LCjM^HV;lC6-6'bLC73_ڐ<wS;)<\v91@1ϟQSoθwR4XlBٜ 󨨞^! OmRj#ÎL?.[:g[ɣ4 [Ķ�^1RtLaj�뗒A$fՔep��@>.-t@*UU}Pe)!&IOP� ͭc #xޕ3%lsyu|K$帿 !r9` 0e�so)��0&9lc[6�㰕oӨx? rXF!.8߸p?[$)�1ݘL~\9@_}A<+JLO57^vO `l:j/Ԡ'4EθL!ae.�"=,z5z4 f)�: *e؆n{g*o\)K _j�2?@2>|Ff1~VܛYu</Bfi;4,$f "y]VcֆnwZKϴb,Idy;NL|xe HİhMg&5Af.9,��s17� jq5"?4F{ 5|Ҍ�Pα# ^isSwLn�Vd>"PfzfZ p(D#iRsf?{o3nC$b,.{\H7{= �aO+.��Wҏ@%$�R1�TF='dQmq~ )I\\8;a?{ŕWU9g �gfvvv>ww<' D$@sV:sD  |T8S+d&ʛUGkRkiD1,uO }/,2H$v"(qH;9g76�J+^tb qd8=دD5r,`{Ą,@Š�E->HTݽڠvI^uE#/䝱Ye[X# SD\vDw6xA 3/J>\ ^qaI$-3ǶBlF^uF+�@cg+|ՆE3�cy" yd��`f\@�j2A1;/ދw wh( ʭAv zfnի'"ơCu �zVnli¾33ĮRGg}$aӂW8ۣM)[jF=o#' uϟv]^Ƈ@y3=˴5w:?~ꇩk+'}WT\\*_Յ~ ��d|7IVk/o뭼pmY�r!�iΞu.Y 1&IS|_*ҪvNTM1 Z jA-aњK^Z$ri fk"�Z~T#{iz%΁i8W냊lTyO%>veW">[m@�kC֬{7։ʮ>'y7(@gh]73.0￙ĺ[uE[ތVz$zzIZ[cnys1 @[ �EN=~F3Azo]v^z+0l>xQG60 �tKyplא`lTxQG60 �oZwtlNWcؼG60 #<a6t0 p:cG81 #aNg ð3a|0l>a6t0 p:cG81 #aNg ð3a|4ۧﻸM0 lY>.av/<a6t0 p:cG81 #aNg ð3a|0l>a6t0 p:cG81 #aNg ðhOqy/�9bO2a?q"gXTM0oRHDRgjm?bsG%l`tq' p}AY;G8vȲu=r*޴nͲE~oV/_mmYfU aUˊsҽ c4 Jz3dVJ0I M�/yTY;_ �!Unk_{dj8["] oɯs8iwhe_k =��Cr mKD:-4͹؎ch[ƶmU=`I_k:-͘)?f=?۸=vDZ"j/NvـĿn}S)iBi:&=Kz )}_YO.HU�0,唵0dRIT&w=7?|&!Qc!Tv@tyw �F?I{ggC!�Lm}JΔ{\CN=�O(hچ:�չ/Cc6 =jc.dks{Oʴ@F�kњ+t J׮2|ux\Hy49ixdX3RՑF圱o�ǛroQ�WgG>ii}B �@%-NOIXWS {cynڒGW}{|%&+79ɞe.^2� X^"t#[q�cHVnv|ˬuRt߾Dfoy#Oq1K7RV5 ˉ tvlY* 0dhA?2_^,dY b;o;Y5lB?}]ef)3$?5ݑN^2Uy%='?:#rbȠv �!֖C6��ORPWESuued]% d󁏮pWWl׸/{X>oK/6%%7}-ɾ N+.tu.wj>Cgm]ft䟾S罹wG;mO dbF\mRT\8us؈�햐_eϨݳ1OjAAZj-?}Yn`?N,�v  ~Я 3șmw՜?Y9$H}Ȯ/׿Sd9�xlygjV@ٝzk-ktUoP6`x鼽SK.�_NZzm5O}� Yl ��`f}\Y#S@RZC6=u9^,F;>2݅YYH8k Ev Z~|Nˏ*])�FG.[",훿~]6Yjė>\~Ip–WI&_5u+jyxýC&od@ljP8QV+hZG��#qJ'8۳y|7Ig#$Gq2') (�5uy7&tj /RwXbjj!!R�a/e10tq_:<] 哾Ip^@_ugw5۷4wK�L 8O�#㥵'g ^vu7�`EG]{,[&6&ytk o:'U:1]p%_^f8yJ}N[I,^Oj%_%\| o򕛲5]ƒdvHvɝtkWvhoIt>&c�dndIR$�"P?jL$šܭzꡆgkh 9ҍhr<0L; "x鼭!"p�^Sh ػ.y靵yK EħG�l×>"B@Hi<٠o;V=�f!*@ ThqR;Q-B@X}kUg =o3*/89AL17N]i9D~AP\]Cx&UvKǤX%U&goS%B@l_W}Th8!h:wV�@9� 2)-L{"/wbH=T~`8mJ$b4FcΟN]|b >+�(ƧW^1�`GϷJ�CݭҺL,[?%ޱ̥qFmQcc pcC->qt 4;d DW}=0lw_ 5"д_/ ��{|Hg F�,Oʋ6:ui�En<V&2 2ֲP.J۴^py1Qkw^c&~ Z{8peV!tAF$q=ؐk{kVL]/!uMghy#ƪwV]@�82xQ؞C��MQg>2h �F:Sͱ!K[)Q$A��b( HH"et` �)��ŅCwwr x(I2"@74BABgGNiyqܩyg(6V4Ţ�"n?4RoDܮlLeit꟤{ Ǩ8/Y9%34qH5دct|NJ§q>�PCWY }!lbdX(9T=2eJ4W!;Ϳ+.;�ݿNb tGQHHhۆ"oH%�Tb{:ɚ p<[).v]9_7l[ƒ'羕b"2X|zs]wG'-?o`ͽLF=!)KS u[ّ[Cz<�3FpD΄ l棍 [[-0T*'Kפy%`A�2_?V+^@dRNt!aj'#&�e e'nvO/\" kbL<Ȇt}wLLKy׉� zg b=Gźw9uӶ@�0ab#TmWg5̊J<3=`h9ݏmө{ɽT�@ܟ^vv/UD$?�$Ia$=z3;m2VaRdAo�@@ԸLzq%xN)"6d{w/<ei"7@ c'd>@zՄ3(ȮElgRj&?G}sxhkć/>RD.b3;[̄ZM#W'{pQq;&G|Ic3#`9zr! 'gq L(&Ha @~Ĺ!@�,x+5Jຄ:1xyAzV+=nN}XW t6��o:EZ&dN e||:&p �r;0 �`U'be..\�XnK�2t5z l~7=*?:'y[3v|rR "'bgi1( �a4!f*yN鼽'xQ�^h/_^$ pa#ʘ%4T1CiG${N&eT8|;')lWC*'Ciƕkgj6&LJRY'/ [hMM-](W6{o[U㡙)/ovՑ?srci0&9i|e6G/W6XZ6JM# 8 ͇+ѧU 5WkB ʷWA|6nJt{OpݡgBjkl7oWQ>Z6CD z/'w6/=~cl盚',hnl}[3W.Itϛ;\׉"c%uGg<%k_C8n={Av1-5p‰U}'vPnw Hj=l��ZaZ^Mu"�0uglC(b\�.Ohx%MiJ!O,.Y<~j'-`w4cjMOw Reu祸K<SD$]>/ !QC׭2_^<[:S|QHXrpzVO~d]GU}OZ5LSNcmNz^ �-; b]mYC`aTچ֐C1ўf($J_*MXf *!kUם;S@.^<zuwKySKo4OAQ͘Tk襊[XA-u } cu祶HN{KJKXı(j*<=渝D<x{9݁ØU#僤k\v|a||Aq˿^AO7\$jHYu�fysLH=e ~s��`nąkydԬ6ӷFÙ9v"<]oO;QGNd/.,ڔHF;\j7��Rל~IK[ 6u#U/=S#DO(84ܬhj҉�� �IDAT=8= %z%nem-o,v3T멁{^!]ҶWk?-%lx/ud7fK|齌_lQl^x}g52כ5 9[qs wlL7ٸ^a`iMAg"d _X#6a9z[1=t^n+xr:KUG'H*Ǭ }}yXᆛ7TN7)<M{DCOgb8a ɭ3c8HQfl [ُ8?2Ns,¬z\לUY Ibk<yV\w0 0l>a6t0 p:c<2Őϳ.l>zәXuS=-o\ M+#g{]†x=I8]*Xij]WsYPyoezV?58QiM w_4 gᐴ僕3g'vzՑBD 4SW3bWWԲ\Dr/2=Y[]ςJ(xa�GsjPWGa){~;ON龰hB,R �Pſ^, X~go,w ](%5k#`G0;H%nx ?U,T28qӻk `=47; `EŜ [mukV$'Ey>~[WښF��oD*:!,w✴n恾1ÝHЄ Kkd_dSM.wS5 (aĒUgGý/O ~ƻqP?=G¤oGOTjJ7Z(5I3'7#�!hKKrSG?Wv}EΖ1kT2.\i�%~.1e;vDNFZVfKnB�\ /-L q<2Vk5Xn P(qa@7��).zբp*dwP%=|#5A{lzzjVz05=e�׌^ 6&|%ScDnŏ 0lq鲬@Z+k,Qtݲ%\^ۣA@foѐK_x&f- H44�X&N7#v:l.^3&]+P_A@4 >9X_mwohU*%S(jg \^<mvN_yIl;s]?xi#Wl+Mj!M{HPj/" Z \8^U#s&,)<MqcI;gbrq'lr;_{೫2VhɛKS;:/ ФgJnd6{rI`#8H=_45ckG�+оgloMo8TkA5W<:."ļETkcVnM �+j~Q <s7/}]]=ph!䒶u:H "|<k[%q{jԤ k�l_?SL"yNC7u IٜM ޮ7%"}~Ehڃ-Fj9gD~f%yϼasBV��!uOuui"B!3w^ܥghR{{Ps 6W 0Άn~PӣVe%<\D�cBl#4�/Y�!8 [ I�^ǛdV)B哹lX 1l_`X�,!02ǑÌUh D!N}M*m_UM=5 XFelG�.�Z4U>88�,YGF^=yC*! roT�e@71j)Z:sM_53f_Au{1# 3qdH.p|Ge߁L]7<rx6ʞa3�Ҏ�جOT3ZΟjV2sk\}g-,B$"FѡKmH1B"qA?y^ffuYɷ[P,c�`i](BtZjlz�C4 :(��Ba>3 n0Jٍ!prC`rAVM3 pf7� yűSbR;}Օ3e}}$_3 ;Z&_4@�\�0֩-0��pq,Ơ3".�<Qo'd#|6<.ǬE8 L5Vd۠紽opNش-Ӄewl#<)=5KvU6gXSg vvreK*zN5I׈P1K�@R, kh={`پ˙|5 r �ALvX}ՠ7ؽlԷ$+Cn'G�@-O^r_fE, 3KNu]̖`uCȠT!ޡ˂hJգCū-Ǿe`~6uWr#+X7D׌G-h0q]S;-XzvG5ڬV8�V�B`'l0bTSO{FMZ,V.oj# vX;Fc,f\׳B? �+N)Iv͕w ޡf?!SO׬ɏ8`KE=q ??Wj#B&̲h{ @rFhّx<%��t �PdHB  #BɇbF{cy�[_Z9vg&ʛUGkRkiD1,uO tȤ#؉!ԋ;$ �+xIӣ҉Q(&đ`fh% x\N>ndNX� +^@t\#RujbcD�%yᏼwƺffIna$PNqDݽB X8H@�3>*DpzŅ[&. (_TېKlgm3clF^uF+�@cg+|ՆE3�cy" yd��`f\@�j2A1o /5EէdSk8a鍬ଥ VMg$'D9ں-@, Vv= oR(5C INJKdĐCBQQf}:n^䮓j܄e6и‚҂ԔXfά9-7\~T�Z"$=?=24iv+(aWM� P8kQR}L!BsRCEQm1 ]Kf.$nw52csT ̎љ!ꚫJ)Ht2%%y vW/֌?jnt蛵 ;3!R0rD5[-R+�)Pjfi!npU4Rա` j~\^in\gT 5=F?6d^V(1ya@CӖe'hڦ^+N:Gd-7 Rexin,2^Cׂk6JڛzGUdQ9a!|y&@dn6" Ia#mSy+2R+ռg鰹Ϩ~&HKz} ,UfgNT {:a��[+ `e*ŋ �0-]˖N5*p a|G60 #aNg ð3a|0l>a6t0 p:cG81 #aNg ð3a|0l>a6t0 f}W|`am:e0 G60 #aNg ð3a|0l>a6t0 p:cG81 #aNg ð3a|0l>a6 ?0/�?GI0l>|'N,M(_LG,yl_6^:2{/":k/tIztMj x+6m*)Yh.[qma^F8{bv vKX5yќqjco~3D~FE/OwΎQ8lYˎZcoQ�1ը5ܸt_b(<9AR[3.ᐼvRz3ziGm̈�tYX':DZK,{vI}l kq~[:.#Y(nޮ>987ba[ke:hz`r̜p?W{n}iSpjfFBMk/TILv8RsK9e-Ƨ- Tx3w ,DӍ٧3)J)*SpF55n<<ORdک�@)YcTtZJiFۘ �(yEy^(WwWN!R=rO_B]Ņ. O4�S,PX)P}q�{~w:&R"#^&Ra=wHWGMsƦ oE\eb R3��i8=!S'-c]eO5-噺iK5^V OW'{. _x[�7cyaJҍ4^n >GP!Yn,jֵKvm8qR"}<0k[W.8d.NL͇xFx O @j|ߎKr^&_[O]ס P?Sʺ$G/  'ٓ6b\ҶbmH؊XwCŮxy)!,FR!�.Q1n#m�SD~ގ~m"ON JaK}-^:ǟ#78ZR[6ꝲ*.07|m1ގ^v¹ѩfC8D+.˝[S)dS~+{}%lx7Wnrr�!0sY^bmodglS2)zn]9wsP.aûͻ]KJ|](_1+Yrx hAZF]hF'=龛  �)ts5nY׶aY='+eG&ȮS=N^p�rRKo眮y3��bS@� �3cʚBڔQ/b1a.z*@@:EؘѶ^#�a戽-Ҟ>[XxXzuZ~TH5R?rNouibXg]A_κpn&nV%3n2#�2ut,#tKȷK:{МKջ5.!8"wgrs#aq X®ؿI v.^ @�}Pj׾_r$N]RԜ:rZF۹['Ìt�閱jef}a^iQ7]}1 +#8C7ˬG\ �DĖm !�.blNn+V/CNQqcg ő=7^V+�xˉa9+g^U. +V.]"8Ь'DIkW%>CΑ"+E-~?jE-ٰjiρ �P>k$ ^☛&+W\mׄq[M�@-e[# H@ov$I~6~k3m\N@j+z[)̙ZɃC|{h9z鼭!"p�^Sh ػ.>'13;kO!soS؆/}E 8K)ˑ'yAvz�B֩U㝷$:t ��! HJھ5׃ń Fuk V^ҷ_m 4i"^^ (.Į:,ʞ˷zb6iT̛v8�\m5UykKPl]L{OQ"&jO i�P^)\>6e+:sۈ�(x~5Ж] ߚ|{J5025\{A5y3i}T KdHmۓ4D`幆!#�h+nyFQDnWeלțM*i�}q!4�aoiwa3v[�hn_x+3ʹ˪P^k HqxHЩOOr\?Z|ͷ{�i8:s= 'su'9#q:δfRKg{>!UKk{^zC 8aT'tGU'.Z;�(qd^h?="��( .|hI��h &��^YK#)FZqa-K ( ��1P$b2:0l� ��R!;g;9MgzZZׯU=Ԓa 2\NBGyih{˃_Uk[U+FS͌O/Y`j2HDergT2= 3!NoiFl7OcIx Haoe,VaF[6$H$eEz��H# x&[(Iq ;;4�H5?(1RȧGݑA*qݝxtsK`AsI,m:vvn0:\םڣd�Lׯ/ <uE:_:o`9?LF?:iCkg.M&meGn R֗F�nK9+=rb6j�',oma<Ruz /]yߗvH �@j LyѲow ƥe%/]XӁ/. ?rWo6E<Itŏ-|iu멽=))[nu�f '8."FC ms]wm!cc={fu  k sPBh7&?c60�6sQpw�Ѧv`~hDX>.Ymu@>OSci4T0lx ڠ7sӵ7 8|4ױtؐ:Scs_NZrrwQLmCS`!rsj),,H0#<��@^Ӄ*$/)^L.b#L̄ZM#�jdTrlspX6#`9zEDphk#P@_! 0 �f9b26;:r=z'? M'` dVU9~c[J=ᾪƶQ4*M0}gܒH)Wb{V`en1fWZ\\G(b&dJF,vhV<>hjJmB@KZ'|CBBG5[6*Ke7~D4�<|7W,i4 �vA ?ts^<=w^q_^{_h)Il/O��P!ɴH#ӝ\[s2*Eד6|W}3Wj{ ؘ2q*JeAĂl'6ADL ޖ.SHs+= ^Nwm ��VIDATuj<45.:Rc.۹bWn, T72KF&q\eQ+ r'!UU_3"WAxΆվj(wN}Aylf=Idl6K.ݭݡg#|v`T<,m :-i_$Kyi{U2kw.%My %Wڔ8:;ً@gڌ3eb 64ƅ2';%+ڛFR3 SN5$��@^ݔpUJD9z@_] ~-K*_��9}h؂t#4�0ĸ hs�%vsR+`;( a\]]?h]u+4l_eC།ug^�rLc9s<SD$]>p+ nݐfƫΔ{,_2\껞ScOEi.>q~xߕw\MdY{T;.\4Vq��Hr309H!Ֆ5&zL,ۆ֐C1ўf($J_*12ܧ "fsf�P7],3Ӄht^;V/Jl;dwQU*ȸ8ʤuT G߻H.62:.|[<|6Enp+ݔaOYT׏J\/ -);vW[EA\Q ��随"rݼĎj/7cL_>qy2ʝVL~d? Sa3ٌA>pyg Y;+ZYhӱM!!EK*7׳wL::kr^nbL@۾mIlǙҬ5oqmGn`&{D;xOgzTl"DO(84ܬhj=8qwWۍع _� _w9'Sm`Aޥ|Ƿ<"Bk@/e8ր<~5n)ܱ1dzEكuㅽIF ]]I} r;!++47pG3ԞG:toq<9*棓TdU Y(U 7o^%?DpZl<Ϥd펹Aqy\GrA,| 3 G:w2kguIe]?\#fed,A>_(;51<vi`4yy;casߍa6t0 p:cG81lxq?URj ~v^t&=oݔtB7KxesƦB.a?$. ,zEٮ,(w۲f=w +r?m<z9QiMSDig<+)gnO搴僕n-H?%w)^ TCum7��HwfHy. y't "RP]aVB>ZTζ`c}C}zTWF �8:0 gn?z_>v^p]J$DUO3sENg-4iFl_]E�T`W.Q_ C㉝if[I=+.FYJl)u貑č[9X٧G� ‹7Ƅ#7%+rvt�@}|G3E8d/_fsgkd3?ƇpL|m[ToM �|~sf.CXΊEq G<jpHw':l@%':l@ # "$mt8uM9M֔WC_˦&ET C¨4N^l0�CPު! ȦhroddU}ņwؒ-<sck4�%~.1eFF"IH =Ѧf�^ikJS<xW;* ,YcU^<vG�Hatڢ Q#iܷ%Yv˯*s?"=Kyr@?tFpM �홴6;[4_<zK�(QLqIf`Ԟ>Q#y0sʂBu[MڧzX 3ϊ6vC`֊1 ":r^l Τ[dpv `kXC}w/`g"Sw{97?nU*%S �0YCse;Oo^۱V=LX,^<&Jӆ*=pH40TxZV2)y17H\t KJ<|r@b%mX0ƽ\+` (Nqx쪌ZԎ 4陒8tr>\c73=yEf *pXڪ/+� 8k23 9v{PM.ˣ91oU/ǘ[6�G<ڽ_*܍d_|ae{yq{ei;w6Ng<3LF.<+/H"glcB ^fQfGߙ^hsk럙ɑ?-ơv׺$dl&koWVum9ZMqf3BU4d컝G-eo,}ah5vΤKXP=1եkl̝W&vkCsK1ӻ 6W 0Άn~PӣVe%<\D�cBl#4�/Y�!8  I�^ೢ'!Ji,̭l.gBP( ge6�Ȉ�da>fĵGE3�! qkjV�lG?j fȀ22,c;8r�Ќu�=Ү1ۻ&;ݕ!˲C9L3h %fJHRH'G#ɛE4d24i@.;,ٖvMhDJg|?<Hwrd 8iuX^pκk(QdJ|{@-ov?Bu7>eSo~|O1>d~Y| C}=''PXZpV>UyAWc_ >~qUމkC ު yǚ1G0??'s@4|N:peiJ=J}rbXxd8J?B /Fė6Hru17vlX^d]T4='畮_$Gb*2ێ%I g`2G FGiY9r`M[~vhĥ-XfT52g\ DDl|IZ*şY "[,33FEDZdrziGDM&.Ď| ̒r |l$NM!¡jȡkז=F7jNLvi-}T&'<vM= U.&}VHuprnqG]1MSu^6@I``r0e#%Jbt6u'"^0P=>4-YN4ŶFDQ¡1F w!#2ձwINB ˊr=YvoFDœ;׈5?~ͫ;_\'g淺 (Ϊ'꿳x^ 0þ>O<>foؽ8\Ѷ/'?UWUs&[Ѧ'{mqISA1K' j'Ok+Ng(BęSd.JDLMgqU>I4$qWJ_g9vpϱol@8SCDd (C0#"v5VX9KwQFUrIj>z96 ƢhJS\UG+Veў+f0rQXQT)E(8,]*8q4s+YŎ7_}W_y?[;(V7%FUޒ. $&U ""޲g"|L(-ّ^"O∌۶><n=w4;S쏭;pcr7InׄJuqI; 2YgV'lɍ[X,HĉWo\b.4{̶X,52H7R42 <>_"bak.w:qvL/) DƬˋ}i8n[E9Etc=$svLؗ-ڝiAqcH8{iQn<rҽe D5 ilj;aSCD-*j}=^8{2ighDѴ(ţ~Q""޶dl_{ء" ?ں.5iKinOɌCsMSBunFvoS/`ቮNֈXb}鮃\ 9l2g^yL=Use?{<3W0ƈԾC{v?};> c|7Nes/Gk*]w2"iN~4ծ+c;84n:_Ӻko#ў/F" ~ׅcm,'"6~̭{~@㹓c]lMZ'A{~mO(_Pӵߪn.EFzo=>C6X[m(vVUԍjL:~ǎDDu-ϾTkmlTنO*tRڿ*س/Gƺj:#`2eLU4WkcpHtn4x9{ mgr,Q"kk޵CF\x铏ʇ|Lp4""^YS7/HSz>YӧRMb֨ ~ƃ; 0�[ pt! �""tmÇsIkl��*��zt�#3�!� �GHg��=B:��@��zt�#3�!� �GHg��=B:�QB %Y]�[JgĄb]�[Jgc4227��#rj"S%feYUh4z�#оdXii(ݿ��%��7�� �GHg��=B:��@��zt�#3�!� �GHg��=B:��@��zt�#3�!� �GHg��=B:�ѿVvu(����IENDB`��������������������������python-griffe-1.6.2/docs/schema.json����������������������������������������������������������������0000664�0001750�0001750�00000031633�14767006246�017270� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "$schema": "http://json-schema.org/draft-07/schema", "title": "Griffe object.", "oneOf": [ { "type": "object", "properties": { "name": { "title": "The name of the alias.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.name", "type": "string" }, "kind": { "title": "The 'alias' kind.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.kind", "const": "alias" }, "path": { "title": "The alias path.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.path", "type": "string" }, "target_path": { "title": "For aliases, the Python path of their target.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.target_path", "type": "string" }, "lineno": { "title": "For aliases, the import starting line number in their own module.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.lineno", "type": "integer" }, "endlineno": { "title": "For aliases, the import ending line number in their own module.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/alias/#griffe.Alias.endlineno", "type": [ "integer", "null" ] } }, "additionalProperties": false, "required": [ "name", "kind", "path", "target_path", "lineno" ] }, { "type": "object", "properties": { "name": { "title": "The name of the object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.name", "type": "string" }, "kind": { "title": "The kind of object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.kind", "enum": [ "module", "class", "function", "attribute" ] }, "path": { "title": "The path of the object (dot-separated Python path).", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.path", "type": "string" }, "filepath": { "title": "The file path of the object's parent module.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.filepath", "type": "string" }, "relative_filepath": { "title": "The file path of the object's parent module, relative to the (at the time) current working directory.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.relative_filepath", "type": "string" }, "relative_package_filepath": { "title": "The file path of the object's package, as found in the explored directories of the Python paths.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.relative_package_filepath", "type": "string" }, "labels": { "title": "The labels of the object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/#griffe.Object.labels", "type": "array" }, "docstring": { "title": "The docstring of the object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring", "type": "object", "properties": { "value": { "title": "The actual string.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.value", "type": "string" }, "lineno": { "title": "The docstring starting line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.lineno", "type": "integer" }, "endlineno": { "title": "The docstring ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.endlineno", "type": [ "integer", "null" ] }, "parsed": { "title": "The parsed docstring (list of docstring sections).", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.parsed", "type": "array", "items": { "type": "object", "properties": { "kind": { "title": "The docstring section kind.", "enum": [ "text", "parameters", "other parameters", "raises", "warns", "returns", "yields", "receives", "examples", "attributes", "deprecated", "admonition" ] }, "value": { "title": "The docstring section value", "type": [ "string", "array" ] } }, "required": [ "kind", "value" ] } } }, "required": [ "value", "lineno", "endlineno" ] }, "members": { "type": "object", "additionalProperties": { "$ref": "#" } }, "lineno": { "title": "The docstring starting line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.lineno", "type": "integer" }, "endlineno": { "title": "The docstring ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/docstrings/models/#griffe.Docstring.endlineno", "type": [ "integer", "null" ] }, "bases": true, "decorators": true, "parameters": true, "returns": true, "value": true, "annotation": true }, "additionalProperties": false, "required": [ "name", "kind", "path", "filepath", "relative_filepath", "relative_package_filepath", "labels", "members" ], "allOf": [ { "if": { "properties": { "kind": { "const": "class" } } }, "then": { "properties": { "bases": { "title": "For classes, their bases classes.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/class/#griffe.Class.bases", "type": "array", "items": { "$ref": "#/$defs/annotation" } }, "decorators": { "title": "For classes, their decorators.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/class/#griffe.Class.decorators", "type": "array", "items": { "type": "object", "properties": { "value": { "title": "The decorator value (string, name or expression).", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Decorator.value", "$ref": "#/$defs/annotation" }, "lineno": { "title": "The decorator starting line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Decorator.lineno", "type": "integer" }, "endlineno": { "title": "The decorator ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Decorator.endlineno", "type": [ "integer", "null" ] } }, "additionalProperties": false, "required": [ "value", "lineno", "endlineno" ] } } }, "required": [ "bases", "decorators" ] } }, { "if": { "properties": { "kind": { "const": "function" } } }, "then": { "properties": { "parameters": { "title": "For functions, their parameters.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Function.parameters", "type": "array", "items": { "type": "object", "properties": { "name": { "title": "The name of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Parameter.name", "type": "string" }, "annotation": { "title": "The annotation of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Parameter.annotation", "$ref": "#/$defs/annotation" }, "kind": { "title": "The kind of parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Parameter.kind", "type": "string" }, "default": { "title": "The default value of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Parameter.default", "$ref": "#/$defs/annotation" } }, "required": [ "name", "kind" ] } }, "returns": { "title": "For functions, their return annotation.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/function/#griffe.Function.returns", "$ref": "#/$defs/annotation" } }, "required": [ "parameters", "returns" ] } }, { "if": { "properties": { "kind": { "const": "attribute" } } }, "then": { "properties": { "value": { "title": "For attributes, their value.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/attribute/#griffe.Attribute.value", "$ref": "#/$defs/annotation" }, "annotation": { "title": "For attributes, their type annotation.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/api/models/attribute/#griffe.Attribute.annotation", "$ref": "#/$defs/annotation" } } } } ] } ], "$defs": { "expression": { "type": "object", "additionalProperties": true }, "annotation": { "oneOf": [ { "type": "null" }, { "type": "string" }, { "$ref": "#/$defs/expression" } ] } } }�����������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/introduction.md������������������������������������������������������������0000664�0001750�0001750�00000004473�14767006246�020202� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Introduction Griffe is able to read Python source code and inspect objects at runtime to extract information about the API of a Python package. This information is then stored into data models (Python classes), and these model instances together form a tree that statically represent the package's API: starting with the top-level module, then descending into submodules, classes, functions and attributes. From there, it's possible to explore and exploit this API representation in various ways. ## Command line tool Griffe is both a command line tool and a Python library. The command line tool offers a few commands to, for example, serialize API data to JSON and check for API breaking changes between two versions of your project. ```bash # Load API of `my_package`, serialize it to JSON, # print it to standard output. griffe dump my_package ``` ```bash # Check for API breaking changes # between current version and version 1.0 (Git reference). griffe check my_package --against 1.0 ``` Both commands accept a `-h`, `--help` argument to show all the available options. For a complete reference of the command line interface, see [Reference / Command line interface](reference/cli.md). ## Python library As a library, Griffe exposes all its public API directly in the top-level module. It means you can simply import `griffe` to access all its API. ```python import griffe griffe.load(...) griffe.find_breaking_changes(...) griffe.main(...) griffe.visit(...) griffe.inspect(...) ``` To start exploring your API within Griffe data models, use the [`load`][griffe.load] function to load your package and access its various objects: ```python import griffe my_package = griffe.load("my_package") some_method = my_package["some_module.SomeClass.some_method"] print(some_method.docstring.value) print(f"Is `some_method` public? {'yes' if some_method.is_public else 'no'}") ``` Use the [`load_git`][griffe.load_git] function to load your API at a particular moment in time, specified with a Git reference (commit hash, branch name, tag name): ```python import griffe my_package_v2_1 = griffe.load_git("my_package", ref="2.1") ``` For more advanced usage, see our guide on [loading and navigating data](guide/users/loading.md). For a complete reference of the application programming interface, see [Reference / Python API](reference/api.md). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/js/������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015543� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/js/insiders.js�������������������������������������������������������������0000664�0001750�0001750�00000005473�14767006246�017732� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������function humanReadableAmount(amount) { const strAmount = String(amount); if (strAmount.length >= 4) { return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`; } return strAmount; } function getJSON(url, callback) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'json'; xhr.onload = function () { var status = xhr.status; if (status === 200) { callback(null, xhr.response); } else { callback(status, xhr.response); } }; xhr.send(); } function updatePremiumSponsors(dataURL, rank) { let capRank = rank.charAt(0).toUpperCase() + rank.slice(1); getJSON(dataURL + `/sponsors${capRank}.json`, function (err, sponsors) { const sponsorsDiv = document.getElementById(`${rank}-sponsors`); if (sponsors.length > 0) { let html = ''; html += `<b>${capRank} sponsors</b><p>` sponsors.forEach(function (sponsor) { html += ` <a href="${sponsor.url}" target="_blank" title="${sponsor.name}"> <img alt="${sponsor.name}" src="${sponsor.image}" style="height: ${sponsor.imageHeight}px;"> </a> ` }); html += '</p>' sponsorsDiv.innerHTML = html; } }); } function updateInsidersPage(author_username) { const sponsorURL = `https://github.com/sponsors/${author_username}` const dataURL = `https://raw.githubusercontent.com/${author_username}/sponsors/main`; getJSON(dataURL + '/numbers.json', function (err, numbers) { document.getElementById('sponsors-count').innerHTML = numbers.count; Array.from(document.getElementsByClassName('sponsors-total')).forEach(function (element) { element.innerHTML = '$ ' + humanReadableAmount(numbers.total); }); getJSON(dataURL + '/sponsors.json', function (err, sponsors) { const sponsorsElem = document.getElementById('sponsors'); const privateSponsors = numbers.count - sponsors.length; sponsors.forEach(function (sponsor) { sponsorsElem.innerHTML += ` <a href="${sponsor.url}" class="sponsorship-item" title="@${sponsor.name}"> <img src="${sponsor.image}&size=72"> </a> `; }); if (privateSponsors > 0) { sponsorsElem.innerHTML += ` <a href="${sponsorURL}" class="sponsorship-item private"> +${privateSponsors} </a> `; } }); }); updatePremiumSponsors(dataURL, "gold"); updatePremiumSponsors(dataURL, "silver"); updatePremiumSponsors(dataURL, "bronze"); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/js/feedback.js�������������������������������������������������������������0000664�0001750�0001750�00000000775�14767006246�017636� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������const feedback = document.forms.feedback; feedback.hidden = false; feedback.addEventListener("submit", function(ev) { ev.preventDefault(); const commentElement = document.getElementById("feedback"); commentElement.style.display = "block"; feedback.firstElementChild.disabled = true; const data = ev.submitter.getAttribute("data-md-value"); const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']"); if (note) { note.hidden = false; } }) ���python-griffe-1.6.2/docs/logo.svg�������������������������������������������������������������������0000777�0001750�0001750�00000000000�14767006246�020477� 2../logo.svg�����������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/.overrides/����������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017207� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/.overrides/main.html�������������������������������������������������������0000664�0001750�0001750�00000001104�14767006246�021015� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{% extends "base.html" %} {% block announce %} <strong>Fund this project</strong> through <a href="{{ 'insiders/#how-to-become-a-sponsor' | url }}"><strong>sponsorship</strong></a> <span class="twemoji heart pulse"> {% include ".icons/octicons/heart-fill-16.svg" %} </span> — Follow <strong>@pawamoy</strong> on <a rel="me" href="https://fosstodon.org/@pawamoy"> <span class="twemoji mastodon"> {% include ".icons/fontawesome/brands/mastodon.svg" %} </span> <strong>Fosstodon</strong> </a> for updates {% endblock %} ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/.overrides/partials/�������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021026� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/.overrides/partials/path-item.html�����������������������������������������0000664�0001750�0001750�00000001211�14767006246�023577� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{# Fix breadcrumbs for when mkdocs-section-index is used. #} {# See https://github.com/squidfunk/mkdocs-material/issues/7614. #} <!-- Render navigation link content --> {% macro render_content(nav_item) %} <span class="md-ellipsis"> {{ nav_item.title }} </span> {% endmacro %} <!-- Render navigation item --> {% macro render(nav_item, ref=nav_item) %} {% if nav_item.is_page %} <li class="md-path__item"> <a href="{{ nav_item.url | url }}" class="md-path__link"> {{ render_content(ref) }} </a> </li> {% elif nav_item.children %} {{ render(nav_item.children | first, ref) }} {% endif %} {% endmacro %} ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/.overrides/partials/comments.html������������������������������������������0000664�0001750�0001750�00000004136�14767006246�023545� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Giscus --> <!-- https://squidfunk.github.io/mkdocs-material/setup/adding-a-comment-system/#giscus-integration --> <div id="feedback" style="display: none;"> <h2 id="__comments">Feedback</h2> <script src="https://giscus.app/client.js" data-repo="mkdocstrings/griffe" data-repo-id="MDEwOlJlcG9zaXRvcnk0MDQ4NzkwODY=" data-category="Documentation" data-category-id="DIC_kwDOGCH27s4CgiFt" data-mapping="pathname" data-strict="1" data-reactions-enabled="0" data-emit-metadata="0" data-input-position="top" data-theme="preferred_color_scheme" data-lang="en" data-loading="lazy" crossorigin="anonymous" async> </script> <!-- Synchronize Giscus theme with palette --> <script> var giscus = document.querySelector("script[src*=giscus]") // Set palette on initial load var palette = __md_get("__palette") if (palette && typeof palette.color === "object") { var theme = palette.color.scheme === "slate" ? "transparent_dark" : "light" // Instruct Giscus to set theme giscus.setAttribute("data-theme", theme) } // Register event handlers after documented loaded document.addEventListener("DOMContentLoaded", function() { var ref = document.querySelector("[data-md-component=palette]") ref.addEventListener("change", function() { var palette = __md_get("__palette") if (palette && typeof palette.color === "object") { var theme = palette.color.scheme === "slate" ? "transparent_dark" : "light" // Instruct Giscus to change theme var frame = document.querySelector(".giscus-frame") frame.contentWindow.postMessage( { giscus: { setConfig: { theme } } }, "https://giscus.app" ) } }) }) </script> </div>����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/getting-started.md���������������������������������������������������������0000664�0001750�0001750�00000001345�14767006246�020561� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Getting started To begin using Griffe, refer to [Installation](installation.md) and take a look at our [short introduction](introduction.md). If you'd like to experiment with Griffe without installing it, try our [playground](playground.md) directly in your browser. If you have questions, need help, or want to contribute, feel free to reach out to the community! You can open [new discussions on GitHub](https://github.com/mkdocstrings/griffe/discussions) or join our [Gitter channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ target="_blank" } for a quick chat. - [Installation](installation.md) - [Introduction (short tour)](introduction.md) - [Guide (advanced tour)](guide.md) - [Getting help](getting-help.md) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/installation.md������������������������������������������������������������0000664�0001750�0001750�00000004463�14767006246�020161� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Installation Griffe is a Python package, so you can install it with your favorite Python package installer or dependency manager. TIP: **[Griffe Insiders](insiders/index.md), a version with [*more features*](insiders/index.md#whats-in-it-for-me), is also available to sponsors :octicons-heart-fill-24:{ .heart .pulse }** ## Install as a tool & library === ":simple-python: pip" ```bash pip install griffe ``` <div class="result" markdown> [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. </div> === ":simple-pdm: pdm" ```bash pdm add griffe ``` <div class="result" markdown> [PDM](https://pdm-project.org/en/latest/) is an all-in-one solution for Python project management. </div> === ":simple-poetry: poetry" ```bash poetry add griffe ``` <div class="result" markdown> [Poetry](https://python-poetry.org/) is an all-in-one solution for Python project management. </div> === ":simple-rye: rye" ```bash rye add griffe ``` <div class="result" markdown> [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. </div> === ":simple-astral: uv" ```bash uv add griffe ``` <div class="result" markdown> [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. </div> ## Install as a tool only === ":simple-python: pip" ```bash pip install --user griffe ``` <div class="result" markdown> [pip](https://pip.pypa.io/en/stable/) is the main package installer for Python. </div> === ":simple-pipx: pipx" ```bash pipx install griffe ``` <div class="result" markdown> [pipx](https://pipx.pypa.io/stable/) allows to install and run Python applications in isolated environments. </div> === ":simple-rye: rye" ```bash rye install griffe ``` <div class="result" markdown> [Rye](https://rye.astral.sh/) is an all-in-one solution for Python project management, written in Rust. </div> === ":simple-astral: uv" ```bash uv tool install griffe ``` <div class="result" markdown> [uv](https://docs.astral.sh/uv/) is an extremely fast Python package and project manager, written in Rust. </div> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/index.md�������������������������������������������������������������������0000664�0001750�0001750�00000004614�14767006246�016565� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: Overview hide: - feedback - navigation - toc --- # Welcome <img src="logo.svg" alt="Griffe logo, created by François Rozet <francois.rozet@outlook.com>" title="Griffe logo, created by François Rozet <francois.rozet@outlook.com>" style="float: right; max-width: 200px; margin: 0 15px;"> > Griffe, pronounced "grif" (`/ɡʁif/`), is a french word that means "claw", but also "signature" in a familiar way. "On reconnaît bien là sa griffe." <div class="grid cards" markdown> - :material-run-fast:{ .lg .middle } **Getting started** --- Learn how to quickly install and use Griffe. [:octicons-download-16: Installation](installation.md){ .md-button .md-button--primary } [:material-book-open-variant: Introduction](introduction.md){ .md-button .md-button--primary } - :material-diving-scuba:{ .lg .middle } **Deep dive** --- Learn everything you can do with Griffe. [:fontawesome-solid-book: Guide](guide/users.md){ .md-button .md-button--primary } [:material-code-parentheses: API reference](reference/api.md){ .md-button .md-button--primary } </div> ## What is Griffe? Griffe is a Python tool and library that gives you signatures for entire Python programs. It extracts the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. Griffe can be used as a Python library. For example, the [Python handler](https://mkdocstrings.github.io/python) of [mkdocstrings](https://mkdocstrings.github.io/) uses Griffe to collect API data and render API documentation in HTML. Griffe can also be used on the command-line, to load and serialize your API data to JSON, or find breaking changes in your API since the previous version of your library. <div class="grid cards" markdown> <div markdown> ```console exec="1" source="console" result="json" title="Serializing as JSON" $ export FORCE_COLOR=1 # markdown-exec: hide $ griffe dump griffe -ssrc -r 2>/dev/null | head -n29 ``` </div> <div markdown> ```console exec="1" source="console" result="ansi" returncode="1" title="Checking for API breaking changes" $ export FORCE_COLOR=1 # markdown-exec: hide $ griffe check griffe -ssrc -b0.46.0 -a0.45.0 --verbose ``` </div> </div> [:material-play: Playground](playground.md){ .md-button } [:simple-gitter: Join our Gitter channel](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im){ .md-button target="_blank" } ��������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/docs/playground.md��������������������������������������������������������������0000664�0001750�0001750�00000001001�14767006246�017625� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- hide: - toc --- # Playground Play with Griffe's API directly in your browser thanks to [Pyodide](https://pyodide.org/en/stable/). You can click the **:material-play: Run** button in the top-right corner of the editor, or hit ++ctrl+enter++ to run the code. ```pyodide install="griffe" theme="tomorrow,dracula" import griffe, micropip # Install your favorite Python package... await micropip.install("cowsay") # And load it with Griffe! cowsay = griffe.load("cowsay") cowsay.as_json(indent=2)[:1000] ``` �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/Makefile������������������������������������������������������������������������0000664�0001750�0001750�00000000760�14767006246�015642� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# If you have `direnv` loaded in your shell, and allow it in the repository, # the `make` command will point at the `scripts/make` shell script. # This Makefile is just here to allow auto-completion in the terminal. actions = \ allrun \ changelog \ check \ check-api \ check-docs \ check-quality \ check-types \ clean \ coverage \ docs \ docs-deploy \ format \ help \ multirun \ release \ run \ setup \ test \ vscode .PHONY: $(actions) $(actions): @python scripts/make "$@" ����������������python-griffe-1.6.2/tests/��������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015341� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/__init__.py���������������������������������������������������������������0000664�0001750�0001750�00000000303�14767006246�017446� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests suite for `griffe`.""" from __future__ import annotations from pathlib import Path TESTS_DIR = Path(__file__).parent TMP_DIR = TESTS_DIR / "tmp" FIXTURES_DIR = TESTS_DIR / "fixtures" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_nodes.py�������������������������������������������������������������0000664�0001750�0001750�00000020372�14767006246�020066� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test nodes utilities.""" from __future__ import annotations import logging from ast import PyCF_ONLY_AST import pytest from griffe import Expr, ExprName, module_vtree, relative_to_absolute, temporary_visited_module syntax_examples = [ # Operations. "b + c", "b - c", "b * c", "b / c", "b // c", "b ** c", "b ^ c", "b & c", "b | c", "b @ c", "b % c", "b >> c", "b << c", # Unary operations. "+b", "-b", "~b", # Comparisons. "b == c", "b >= c", "b > c", "b <= c", "b < c", "b != c", # Boolean logic. "b and c", "b or c", "not b", # Identify. "b is c", "b is not c", # Membership. "b in c", "b not in c", # Calls. "call()", "call(something)", "call(something=something)", # Strings. "f'a {round(key, 2)} {z}'", # Slices. "o[x]", "o[x, y]", "o[x:y]", "o[x:y, z]", "o[x, y(z)]", # Walrus operator. "a if (a := b) else c", # Starred. "a(*b, **c)", # Structs. "(a, b, c)", "{a, b, c}", "{a: b, c: d}", "[a, b, c]", # Yields. "yield", "yield a", "yield from a", # Lambdas. "lambda a: a", "lambda a, b: a", "lambda *a, **b: a", "lambda a, b=0: a", "lambda a, /, b, c: a", "lambda a, *, b, c: a", "lambda a, /, b, *, c: a", ] @pytest.mark.parametrize( ("code", "path", "is_package", "expected"), [ ("from . import b", "a", False, "a.b"), ("from . import b", "a", True, "a.b"), ("from . import c", "a.b", False, "a.c"), ("from . import c", "a.b", True, "a.b.c"), ("from . import d", "a.b.c", False, "a.b.d"), ("from .c import d", "a", False, "a.c.d"), ("from .c import d", "a.b", False, "a.c.d"), ("from .b import c", "a.b", True, "a.b.b.c"), ("from .. import e", "a.c.d.i", False, "a.c.e"), ("from ..d import e", "a.c.d.i", False, "a.c.d.e"), ("from ... import f", "a.c.d.i", False, "a.f"), ("from ...b import f", "a.c.d.i", False, "a.b.f"), ("from ...c.d import e", "a.c.d.i", False, "a.c.d.e"), ("from .c import *", "a", False, "a.c.*"), ("from .c import *", "a.b", False, "a.c.*"), ("from .b import *", "a.b", True, "a.b.b.*"), ("from .. import *", "a.c.d.i", False, "a.c.*"), ("from ..d import *", "a.c.d.i", False, "a.c.d.*"), ("from ... import *", "a.c.d.i", False, "a.*"), ("from ...b import *", "a.c.d.i", False, "a.b.*"), ("from ...c.d import *", "a.c.d.i", False, "a.c.d.*"), ], ) def test_relative_to_absolute_imports(code: str, path: str, is_package: bool, expected: str) -> None: """Check if relative imports are correctly converted to absolute ones. Parameters: code: The parametrized module code. path: The parametrized module path. is_package: Whether the module is a package (or subpackage) (parametrized). expected: The parametrized expected absolute path. """ node = compile(code, mode="exec", filename="<>", flags=PyCF_ONLY_AST).body[0] # type: ignore[attr-defined] module = module_vtree(path, leaf_package=is_package, return_leaf=True) for name in node.names: assert relative_to_absolute(node, name, module) == expected def test_multipart_imports() -> None: """Assert that a multipart path like `a.b.c` imported as `x` points to the right target.""" with temporary_visited_module( """ import pkg.b.c import pkg.b.c as alias """, ) as module: pkg = module["pkg"] alias = module["alias"] assert pkg.target_path == "pkg" assert alias.target_path == "pkg.b.c" @pytest.mark.parametrize( "expression", [ "A", "A.B", "A[B]", "A.B[C.D]", "~A", "A | B", "A[[B, C], D]", "A(b=c, d=1)", "A[-1, +2.3]", "A[B, C.D(e='syntax error')]", ], ) def test_building_annotations_from_nodes(expression: str) -> None: """Test building annotations from AST nodes. Parameters: expression: An expression (parametrized). """ class_defs = "\n\n".join(f"class {letter}: ..." for letter in "ABCD") with temporary_visited_module(f"{class_defs}\n\nx: {expression}\ny: {expression} = 0") as module: assert "x" in module.members assert "y" in module.members assert str(module["x"].annotation) == expression assert str(module["y"].annotation) == expression @pytest.mark.parametrize("code", syntax_examples) def test_building_expressions_from_nodes(code: str) -> None: """Test building annotations from AST nodes. Parameters: code: An expression (parametrized). """ with temporary_visited_module(f"__z__ = {code}") as module: assert "__z__" in module.members # Make space after comma non-significant. value = str(module["__z__"].value).replace(", ", ",") assert value == code.replace(", ", ",") @pytest.mark.parametrize( ("code", "has_name"), [ ("import typing\nclass A: ...\na: typing.Literal['A']", False), ("from typing import Literal\nclass A: ...\na: Literal['A']", False), ("import typing_extensions\nclass A: ...\na: typing.Literal['A']", False), ("from typing_extensions import Literal\nclass A: ...\na: Literal['A']", False), ("from mod import A\na: 'A'", True), ("from mod import A\na: list['A']", True), ], ) def test_forward_references(code: str, has_name: bool) -> None: """Check that we support forward references (type names as strings). Parameters: code: Parametrized code. has_name: Whether the annotation should contain a Name rather than a string. """ with temporary_visited_module(code) as module: annotation = list(module["a"].annotation.iterate(flat=True)) if has_name: assert any(isinstance(item, ExprName) and item.name == "A" for item in annotation) assert all(not (isinstance(item, str) and item == "A") for item in annotation) else: assert "'A'" in annotation assert all(not (isinstance(item, ExprName) and item.name == "A") for item in annotation) @pytest.mark.parametrize( "default", [ "1", "'test_string'", "dict(key=1)", "{'key': 1}", "DEFAULT_VALUE", "None", ], ) def test_default_value_from_nodes(default: str) -> None: """Test getting default value from AST nodes. Parameters: default: A default value (parametrized). """ module_defs = f"def f(x={default}):\n return x" with temporary_visited_module(module_defs) as module: assert "f" in module.members params = module.members["f"].parameters # type: ignore[union-attr] assert len(params) == 1 assert str(params[0].default) == default # https://github.com/mkdocstrings/griffe/issues/159 def test_parsing_complex_string_annotations() -> None: """Test parsing of complex, stringified annotations.""" with temporary_visited_module( """ class ArgsKwargs: def __init__(self, args: 'tuple[Any, ...]', kwargs: 'dict[str, Any] | None' = None) -> None: ... @property def args(self) -> 'tuple[Any, ...]': ... @property def kwargs(self) -> 'dict[str, Any] | None': ... """, ) as module: init_args_annotation = module["ArgsKwargs.__init__"].parameters["args"].annotation assert isinstance(init_args_annotation, Expr) assert init_args_annotation.is_tuple kwargs_return_annotation = module["ArgsKwargs.kwargs"].annotation assert isinstance(kwargs_return_annotation, Expr) def test_parsing_dynamic_base_classes(caplog: pytest.LogCaptureFixture) -> None: """Assert parsing dynamic base classes does not trigger errors. Parameters: caplog: Pytest fixture to capture logs. """ with ( caplog.at_level(logging.ERROR), temporary_visited_module( """ from collections import namedtuple class Thing(namedtuple('Thing', 'attr1 attr2')): ... """, ), ): pass assert not caplog.records ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/helpers.py����������������������������������������������������������������0000664�0001750�0001750�00000002411�14767006246�017353� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Helpers for tests.""" from __future__ import annotations import os import sys from tempfile import gettempdir from _griffe.tests import _TMPDIR_PREFIX def clear_sys_modules(name: str | None = None) -> None: """Clear `sys.modules` of a module and its submodules. Use this function after having used `temporary_pypackage` and `inspect` together. Better yet, use `temporary_inspected_package` and `temporary_inspected_module` which will automatically clear `sys.modules` when exiting. Parameters: name: A top-level module name. If None, clear all temporary inspected modules (located in the OS' default temporary directory). """ if name: for module_name in tuple(sys.modules.keys()): if module_name == name or module_name.startswith(f"{name}."): sys.modules.pop(module_name, None) else: prefix = os.path.join(gettempdir(), _TMPDIR_PREFIX) # noqa: PTH118 for module_name, module in tuple(sys.modules.items()): if ((file := getattr(module, "__file__", "")) and file.startswith(prefix)) or ( (paths := getattr(module, "__path__", ())) and any(path.startswith(prefix) for path in paths) ): sys.modules.pop(module_name, None) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_cli.py���������������������������������������������������������������0000664�0001750�0001750�00000002543�14767006246�017525� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the CLI.""" from __future__ import annotations import sys import pytest from _griffe import cli, debug def test_main() -> None: """Basic CLI test.""" if sys.platform == "win32": assert cli.main(["dump", "griffe", "-s", "src", "-oNUL"]) == 0 else: assert cli.main(["dump", "griffe", "-s", "src", "-o/dev/null"]) == 0 def test_show_help(capsys: pytest.CaptureFixture) -> None: """Show help. Parameters: capsys: Pytest fixture to capture output. """ with pytest.raises(SystemExit): cli.main(["-h"]) captured = capsys.readouterr() assert "griffe" in captured.out def test_show_version(capsys: pytest.CaptureFixture) -> None: """Show version. Parameters: capsys: Pytest fixture to capture output. """ with pytest.raises(SystemExit): cli.main(["-V"]) captured = capsys.readouterr() assert debug._get_version() in captured.out def test_show_debug_info(capsys: pytest.CaptureFixture) -> None: """Show debug information. Parameters: capsys: Pytest fixture to capture output. """ with pytest.raises(SystemExit): cli.main(["--debug-info"]) captured = capsys.readouterr().out.lower() assert "python" in captured assert "system" in captured assert "environment" in captured assert "packages" in captured �������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_git.py���������������������������������������������������������������0000664�0001750�0001750�00000006617�14767006246�017547� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for creating a griffe Module from specific commits in a git repository.""" from __future__ import annotations import shutil from subprocess import run from typing import TYPE_CHECKING import pytest from griffe import Module, check, load_git from tests import FIXTURES_DIR if TYPE_CHECKING: from pathlib import Path REPO_NAME = "my-repo" REPO_SOURCE = FIXTURES_DIR / "_repo" MODULE_NAME = "my_module" def _copy_contents(src: Path, dst: Path) -> None: """Copy *contents* of src into dst. Parameters: src: the folder whose contents will be copied to dst dst: the destination folder """ dst.mkdir(exist_ok=True, parents=True) for src_path in src.iterdir(): dst_path = dst / src_path.name if src_path.is_dir(): _copy_contents(src_path, dst_path) else: shutil.copy(src_path, dst_path) @pytest.fixture def git_repo(tmp_path: Path) -> Path: """Fixture that creates a git repo with multiple tagged versions. For each directory in `tests/test_git/_repo/` - the contents of the directory will be copied into the temporary repo - all files will be added and commited - the commit will be tagged with the name of the directory To add to these tests (i.e. to simulate change over time), either modify one of the files in the existing `v0.1.0`, `v0.2.0` folders, or continue adding new version folders following the same pattern. Parameters: tmp_path: temporary directory fixture Returns: Path: path to temporary repo. """ repo_path = tmp_path / REPO_NAME repo_path.mkdir() run(["git", "-C", str(repo_path), "init"], check=True) run(["git", "-C", str(repo_path), "config", "user.name", "Name"], check=True) run(["git", "-C", str(repo_path), "config", "user.email", "my@email.com"], check=True) for tagdir in REPO_SOURCE.iterdir(): ver = tagdir.name _copy_contents(tagdir, repo_path) run(["git", "-C", str(repo_path), "add", "."], check=True) run(["git", "-C", str(repo_path), "commit", "-m", f"feat: {ver} stuff"], check=True) run(["git", "-C", str(repo_path), "tag", ver], check=True) return repo_path def test_load_git(git_repo: Path) -> None: """Test that we can load modules from different commits from a git repo. Parameters: git_repo: temporary git repo """ v1 = load_git(MODULE_NAME, ref="v0.1.0", repo=git_repo) v2 = load_git(MODULE_NAME, ref="v0.2.0", repo=git_repo) assert isinstance(v1, Module) assert isinstance(v2, Module) assert v1.attributes["__version__"].value == "'0.1.0'" assert v2.attributes["__version__"].value == "'0.2.0'" def test_load_git_errors(git_repo: Path) -> None: """Test that we get informative errors for various invalid inputs. Parameters: git_repo: temporary git repo """ with pytest.raises(OSError, match="Not a git repository"): load_git(MODULE_NAME, ref="v0.2.0", repo="not-a-repo") with pytest.raises(RuntimeError, match="Could not create git worktre"): load_git(MODULE_NAME, ref="invalid-tag", repo=git_repo) with pytest.raises(ImportError, match="ModuleNotFoundError: No module named 'not_a_real_module'"): load_git("not_a_real_module", ref="v0.2.0", repo=git_repo) def test_git_failures(tmp_path: Path) -> None: """Test failures to use Git.""" assert check(tmp_path) == 2 �����������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_loader.py������������������������������������������������������������0000664�0001750�0001750�00000047160�14767006246�020230� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `loader` module.""" from __future__ import annotations import logging from textwrap import dedent from typing import TYPE_CHECKING import pytest from griffe import ( ExprName, GriffeLoader, temporary_inspected_package, temporary_pyfile, temporary_pypackage, temporary_visited_package, ) if TYPE_CHECKING: from pathlib import Path from griffe import Alias def test_has_docstrings_does_not_try_to_resolve_alias() -> None: """Assert that checkins presence of docstrings does not trigger alias resolution.""" with temporary_pyfile("""from abc import abstractmethod""") as (module_name, path): loader = GriffeLoader(search_paths=[path.parent]) module = loader.load(module_name) loader.resolve_aliases() assert "abstractmethod" in module.members assert not module.has_docstrings def test_recursive_wildcard_expansion() -> None: """Assert that wildcards are expanded recursively.""" with temporary_pypackage("package", ["mod_a/mod_b/mod_c.py"]) as tmp_package: mod_a_dir = tmp_package.path / "mod_a" mod_b_dir = mod_a_dir / "mod_b" mod_a = mod_a_dir / "__init__.py" mod_b = mod_b_dir / "__init__.py" mod_c = mod_b_dir / "mod_c.py" mod_c.write_text("CONST_X = 'X'\nCONST_Y = 'Y'") mod_b.write_text("from .mod_c import *") mod_a.write_text("from .mod_b import *") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) assert "CONST_X" in package["mod_a.mod_b.mod_c"].members assert "CONST_Y" in package["mod_a.mod_b.mod_c"].members assert "CONST_X" not in package.members assert "CONST_Y" not in package.members loader.expand_wildcards(package) # type: ignore[arg-type] assert "CONST_X" in package["mod_a"].members assert "CONST_Y" in package["mod_a"].members assert "CONST_X" in package["mod_a.mod_b"].members assert "CONST_Y" in package["mod_a.mod_b"].members def test_dont_shortcut_alias_chain_after_expanding_wildcards() -> None: """Assert public aliases paths are not resolved to canonical paths when expanding wildcards.""" with temporary_pypackage("package", ["mod_a.py", "mod_b.py", "mod_c.py"]) as tmp_package: mod_a = tmp_package.path / "mod_a.py" mod_b = tmp_package.path / "mod_b.py" mod_c = tmp_package.path / "mod_c.py" mod_a.write_text("from package.mod_b import *\nclass Child(Base): ...\n") mod_b.write_text("from package.mod_c import Base\n__all__ = ['Base']\n") mod_c.write_text("class Base: ...\n") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() child = package["mod_a.Child"] assert child.bases base = child.bases[0] assert isinstance(base, ExprName) assert base.name == "Base" assert base.canonical_path == "package.mod_b.Base" def test_dont_overwrite_lower_member_when_expanding_wildcard() -> None: """Check that we don't overwrite a member defined after the import when expanding a wildcard.""" with temporary_pypackage("package", ["mod_a.py", "mod_b.py"]) as tmp_package: mod_a = tmp_package.path / "mod_a.py" mod_b = tmp_package.path / "mod_b.py" mod_a.write_text("overwritten = 0\nfrom package.mod_b import *\nnot_overwritten = 0\n") mod_b.write_text("overwritten = 1\nnot_overwritten = 1\n") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() assert package["mod_a.overwritten"].value == "1" assert package["mod_a.not_overwritten"].value == "0" def test_load_data_from_stubs() -> None: """Check that the loader is able to load data from stubs / `*.pyi` files.""" with temporary_pypackage("package", ["_rust_notify.pyi"]) as tmp_package: # Code taken from samuelcolvin/watchfiles project. code = ''' from typing import List, Literal, Optional, Protocol, Set, Tuple, Union __all__ = ['RustNotify'] class AbstractEvent(Protocol): def is_set(self) -> bool: ... class RustNotify: """ Interface to the Rust [notify](https://crates.io/crates/notify) crate which does the heavy lifting of watching for file changes and grouping them into a single event. """ def __init__(self, watch_paths: List[str], debug: bool) -> None: """ Create a new RustNotify instance and start a thread to watch for changes. `FileNotFoundError` is raised if one of the paths does not exist. Args: watch_paths: file system paths to watch for changes, can be directories or files debug: if true, print details about all events to stderr """ ''' tmp_package.path.joinpath("_rust_notify.pyi").write_text(dedent(code)) tmp_package.path.joinpath("__init__.py").write_text( "from ._rust_notify import RustNotify\n__all__ = ['RustNotify']", ) loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() assert "_rust_notify" in package.members assert "RustNotify" in package.members assert package["RustNotify"].resolved def test_load_from_both_py_and_pyi_files() -> None: """Check that the loader is able to merge data loaded from `*.py` and `*.pyi` files.""" with temporary_pypackage("package", ["mod.py", "mod.pyi"]) as tmp_package: tmp_package.path.joinpath("mod.py").write_text( dedent( """ CONST = 0 class Class: class_attr = True def function1(self, arg1): pass def function2(self, arg1=2.2): pass """, ), ) tmp_package.path.joinpath("mod.pyi").write_text( dedent( """ from typing import Sequence, overload CONST: int class Class: class_attr: bool @overload def function1(self, arg1: str) -> Sequence[str]: ... @overload def function1(self, arg1: bytes) -> Sequence[bytes]: ... def function2(self, arg1: float) -> float: ... """, ), ) loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() assert "mod" in package.members mod = package["mod"] assert mod.filepath.suffix == ".py" assert "CONST" in mod.members const = mod["CONST"] assert const.value == "0" assert const.annotation.name == "int" assert "Class" in mod.members class_ = mod["Class"] assert "class_attr" in class_.members class_attr = class_["class_attr"] assert class_attr.value == "True" assert class_attr.annotation.name == "bool" assert "function1" in class_.members function1 = class_["function1"] assert len(function1.overloads) == 2 assert "function2" in class_.members function2 = class_["function2"] assert function2.returns.name == "float" assert function2.parameters["arg1"].annotation.name == "float" assert function2.parameters["arg1"].default == "2.2" def test_overwrite_module_with_attribute() -> None: """Check we are able to overwrite a module with an attribute.""" with temporary_pypackage("package", ["mod.py"]) as tmp_package: tmp_package.path.joinpath("mod.py").write_text("mod: list = [0, 1, 2]") tmp_package.path.joinpath("__init__.py").write_text("from package.mod import *") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) loader.load(tmp_package.name) loader.resolve_aliases() def test_load_package_from_both_py_and_pyi_files() -> None: """Check that the loader is able to merge a package loaded from `*.py` and `*.pyi` files. This is a special case of the previous test: where the package itself has a top level `__init__.pyi` (not so uncommon). """ with temporary_pypackage("package", ["__init__.py", "__init__.pyi"]) as tmp_package: tmp_package.path.joinpath("__init__.py").write_text("globals()['f'] = lambda x: str(x)") tmp_package.path.joinpath("__init__.pyi").write_text("def f(x: int) -> str: ...") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) assert "f" in package.members def test_load_single_module_from_both_py_and_pyi_files() -> None: """Check that the loader is able to merge a single-module package loaded from `*.py` and `*.pyi` files. This is a special case of the previous test: where the package is a single module distribution that also drops a `.pyi` file in site-packages. """ with temporary_pypackage("just_a_folder", ["mod.py", "mod.pyi"]) as tmp_folder: tmp_folder.path.joinpath("__init__.py").unlink() tmp_folder.path.joinpath("mod.py").write_text("globals()['f'] = lambda x: str(x)") tmp_folder.path.joinpath("mod.pyi").write_text("def f(x: int) -> str: ...") loader = GriffeLoader(search_paths=[tmp_folder.path]) package = loader.load("mod") assert "f" in package.members def test_unsupported_item_in_all(caplog: pytest.LogCaptureFixture) -> None: """Check that unsupported items in `__all__` log a warning. Parameters: caplog: Pytest fixture to capture logs. """ item_name = "XXX" with temporary_pypackage("package", ["mod.py"]) as tmp_folder: tmp_folder.path.joinpath("__init__.py").write_text(f"from .mod import {item_name}\n__all__ = [{item_name}]") tmp_folder.path.joinpath("mod.py").write_text(f"class {item_name}: ...") loader = GriffeLoader(search_paths=[tmp_folder.tmpdir]) loader.expand_exports(loader.load("package")) # type: ignore[arg-type] assert any(item_name in record.message and record.levelname == "WARNING" for record in caplog.records) def test_skip_modules_with_dots_in_filename(caplog: pytest.LogCaptureFixture) -> None: """Check that modules with dots in their filenames are skipped. Parameters: caplog: Pytest fixture to capture logs. """ caplog.set_level(logging.DEBUG) with temporary_pypackage("package", ["gunicorn.conf.py"]) as tmp_folder: loader = GriffeLoader(search_paths=[tmp_folder.tmpdir]) loader.load("package") assert any("gunicorn.conf.py" in record.message and record.levelname == "DEBUG" for record in caplog.records) def test_nested_namespace_packages() -> None: """Load a deeply nested namespace package.""" with temporary_pypackage("a/b/c/d", ["mod.py"]) as tmp_folder: loader = GriffeLoader(search_paths=[tmp_folder.tmpdir]) a_package = loader.load("a") assert "b" in a_package.members b_package = a_package.members["b"] assert "c" in b_package.members c_package = b_package.members["c"] assert "d" in c_package.members d_package = c_package.members["d"] assert "mod" in d_package.members def test_multiple_nested_namespace_packages() -> None: """Load a deeply nested namespace package appearing in several places.""" with temporary_pypackage("a/b/c/d", ["mod1.py"], init=False) as tmp_ns1: # noqa: SIM117 with temporary_pypackage("a/b/c/d", ["mod2.py"], init=False) as tmp_ns2: with temporary_pypackage("a/b/c/d", ["mod3.py"], init=False) as tmp_ns3: tmp_namespace_pkgs = [tmp_ns.tmpdir for tmp_ns in (tmp_ns1, tmp_ns2, tmp_ns3)] loader = GriffeLoader(search_paths=tmp_namespace_pkgs) a_package = loader.load("a") for tmp_ns in tmp_namespace_pkgs: assert tmp_ns.joinpath("a") in a_package.filepath # type: ignore[operator] assert "b" in a_package.members b_package = a_package.members["b"] for tmp_ns in tmp_namespace_pkgs: assert tmp_ns.joinpath("a/b") in b_package.filepath # type: ignore[operator] assert "c" in b_package.members c_package = b_package.members["c"] for tmp_ns in tmp_namespace_pkgs: assert tmp_ns.joinpath("a/b/c") in c_package.filepath # type: ignore[operator] assert "d" in c_package.members d_package = c_package.members["d"] for tmp_ns in tmp_namespace_pkgs: assert tmp_ns.joinpath("a/b/c/d") in d_package.filepath # type: ignore[operator] assert "mod1" in d_package.members assert "mod2" in d_package.members assert "mod3" in d_package.members def test_stop_at_first_package_inside_namespace_package() -> None: """Stop loading similar paths once we found a non-namespace package.""" with temporary_pypackage("a/b/c/d", ["mod1.py"], init=True) as tmp_ns1: # noqa: SIM117 with temporary_pypackage("a/b/c/d", ["mod2.py"], init=True) as tmp_ns2: tmp_namespace_pkgs = [tmp_ns.tmpdir for tmp_ns in (tmp_ns1, tmp_ns2)] loader = GriffeLoader(search_paths=tmp_namespace_pkgs) a_package = loader.load("a") assert "b" in a_package.members b_package = a_package.members["b"] assert "c" in b_package.members c_package = b_package.members["c"] assert "d" in c_package.members d_package = c_package.members["d"] assert d_package.is_subpackage assert d_package.filepath == tmp_ns1.tmpdir.joinpath("a/b/c/d/__init__.py") assert "mod1" in d_package.members assert "mod2" not in d_package.members def test_load_builtin_modules() -> None: """Assert builtin/compiled modules can be loaded.""" loader = GriffeLoader() loader.load("_ast") loader.load("_collections") loader.load("_operator") assert "_ast" in loader.modules_collection assert "_collections" in loader.modules_collection assert "_operator" in loader.modules_collection def test_resolve_aliases_of_builtin_modules() -> None: """Assert builtin/compiled modules can be loaded.""" loader = GriffeLoader() loader.load("io") loader.load("_io") unresolved, _ = loader.resolve_aliases(external=True, implicit=True, max_iterations=1) io_unresolved = {un for un in unresolved if un.startswith(("io", "_io"))} assert len(io_unresolved) < 5 @pytest.mark.parametrize("namespace", [False, True]) def test_loading_stubs_only_packages(tmp_path: Path, namespace: bool) -> None: """Test loading and merging of stubs-only packages. Parameters: tmp_path: Pytest fixture. namespace: Whether the package and stubs are namespace packages. """ # Create package. package_parent = tmp_path / "pkg_parent" package_parent.mkdir() package = package_parent / "package" package.mkdir() if not namespace: package.joinpath("__init__.py").write_text("a: int = 0") package.joinpath("module.py").write_text("a: int = 0") # Create stubs. stubs_parent = tmp_path / "stubs_parent" stubs_parent.mkdir() stubs = stubs_parent / "package-stubs" stubs.mkdir() if not namespace: stubs.joinpath("__init__.pyi").write_text("b: int") stubs.joinpath("module.pyi").write_text("b: int") # Exposing stubs first, to make sure order doesn't matter. loader = GriffeLoader(search_paths=[stubs_parent, package_parent]) # Loading package and stubs, checking their contents. top_module = loader.load("package", try_relative_path=False, find_stubs_package=True) if not namespace: assert "a" in top_module.members assert "b" in top_module.members assert "a" in top_module["module"].members assert "b" in top_module["module"].members @pytest.mark.parametrize( "init", [ "from package.thing import thing", "thing = False", ], ) def test_submodule_shadowing_member(init: str, caplog: pytest.LogCaptureFixture) -> None: """Warn when a submodule shadows a member of the same name. Parameters: init: Contents of the top-level init module. """ caplog.set_level(logging.DEBUG) with temporary_visited_package( "package", {"__init__.py": init, "thing.py": "thing = True"}, init=True, ): assert "shadowing" in caplog.text @pytest.mark.parametrize("wildcard", [True, False]) @pytest.mark.parametrize(("external", "foo_is_resolved"), [(None, True), (True, True), (False, False)]) def test_side_loading_sibling_private_module(wildcard: bool, external: bool | None, foo_is_resolved: bool) -> None: """Automatically load `_a` when `a` (wildcard) imports from it. Parameters: wildcard: Whether the import is a wildcard import. external: Value for the `external` parameter when resolving aliases. foo_is_resolved: Whether the `foo` alias should be resolved. """ with temporary_pypackage("_a", {"__init__.py": "def foo():\n '''Docstring.'''"}) as pkg_a: # noqa: SIM117 with temporary_pypackage("a", {"__init__.py": f"from _a import {'*' if wildcard else 'foo'}"}) as pkg_a_private: loader = GriffeLoader(search_paths=[pkg_a.tmpdir, pkg_a_private.tmpdir]) package = loader.load("a") loader.resolve_aliases(external=external, implicit=True) if foo_is_resolved: assert "foo" in package.members assert package["foo"].is_alias assert package["foo"].resolved assert package["foo"].docstring.value == "Docstring." elif wildcard: assert "foo" not in package.members else: assert "foo" in package.members assert package["foo"].is_alias assert not package["foo"].resolved def test_forcing_inspection() -> None: """Load a package with forced dynamic analysis.""" modules = {"__init__.py": "a = 0", "mod.py": "b = 1"} with ( temporary_visited_package("static_pkg", modules) as static_package, temporary_inspected_package("dynamic_pkg", modules) as dynamic_package, ): for name in static_package.members: assert name in dynamic_package.members for name in static_package["mod"].members: assert name in dynamic_package["mod"].members def test_relying_on_modules_path_attribute(monkeypatch: pytest.MonkeyPatch) -> None: """Load a package that relies on the `__path__` attribute of a module.""" def raise_module_not_found_error(*args, **kwargs) -> None: # noqa: ARG001,ANN002,ANN003 raise ModuleNotFoundError loader = GriffeLoader() monkeypatch.setattr(loader.finder, "find_spec", raise_module_not_found_error) assert loader.load("griffe") def test_not_calling_package_loaded_hook_on_something_else_than_package() -> None: """Always call the `on_package_loaded` hook on a package, not any other object.""" with temporary_pypackage("pkg", {"__init__.py": "from typing import List as L"}) as pkg: loader = GriffeLoader(search_paths=[pkg.tmpdir]) alias: Alias = loader.load("pkg.L") # type: ignore[assignment] assert alias.is_alias assert not alias.resolved ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_functions.py���������������������������������������������������������0000664�0001750�0001750�00000012456�14767006246�020772� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test functions loading.""" from __future__ import annotations import pytest from griffe import ParameterKind, temporary_visited_module def test_visit_simple_function() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(foo='<>'): ...") as module: function = module["f"] assert len(function.parameters) == 1 param = function.parameters[0] assert param is function.parameters["foo"] assert param.name == "foo" assert param.kind is ParameterKind.positional_or_keyword assert param.default == "'<>'" def test_visit_function_positional_only_param() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(posonly, /): ...") as module: function = module["f"] assert len(function.parameters) == 1 param = function.parameters[0] assert param is function.parameters["posonly"] assert param.name == "posonly" assert param.kind is ParameterKind.positional_only assert param.default is None def test_visit_function_positional_only_param_with_default() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(posonly=0, /): ...") as module: function = module["f"] assert len(function.parameters) == 1 param = function.parameters[0] assert param is function.parameters["posonly"] assert param.name == "posonly" assert param.kind is ParameterKind.positional_only assert param.default == "0" def test_visit_function_positional_or_keyword_param() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(posonly, /, poskw): ...") as module: function = module["f"] assert len(function.parameters) == 2 param = function.parameters[1] assert param is function.parameters["poskw"] assert param.name == "poskw" assert param.kind is ParameterKind.positional_or_keyword assert param.default is None def test_visit_function_positional_or_keyword_param_with_default() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(posonly, /, poskw=0): ...") as module: function = module["f"] assert len(function.parameters) == 2 param = function.parameters[1] assert param is function.parameters["poskw"] assert param.name == "poskw" assert param.kind is ParameterKind.positional_or_keyword assert param.default == "0" def test_visit_function_keyword_only_param() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(*, kwonly): ...") as module: function = module["f"] assert len(function.parameters) == 1 param = function.parameters[0] assert param is function.parameters["kwonly"] assert param.name == "kwonly" assert param.kind is ParameterKind.keyword_only assert param.default is None def test_visit_function_keyword_only_param_with_default() -> None: """Test functions parameters loading.""" with temporary_visited_module("def f(*, kwonly=0): ...") as module: function = module["f"] assert len(function.parameters) == 1 param = function.parameters[0] assert param is function.parameters["kwonly"] assert param.name == "kwonly" assert param.kind is ParameterKind.keyword_only assert param.default == "0" def test_visit_function_syntax_error() -> None: """Test functions parameters loading.""" with pytest.raises(SyntaxError), temporary_visited_module("def f(/, poskw=0): ..."): ... def test_visit_function_variadic_params() -> None: """Test functions variadic parameters visit.""" with temporary_visited_module("def f(*args: str, kw=1, **kwargs: int): ...") as module: function = module["f"] assert len(function.parameters) == 3 param = function.parameters[0] assert param.name == "args" assert param.annotation.name == "str" assert param.annotation.canonical_path == "str" param = function.parameters[1] assert param.annotation is None param = function.parameters[2] assert param.name == "kwargs" assert param.annotation.name == "int" assert param.annotation.canonical_path == "int" def test_visit_function_params_annotations() -> None: """Test functions parameters loading.""" with temporary_visited_module( """ import typing from typing import Any def f_annorations( a: str, b: Any, c: typing.Optional[typing.List[int]], d: float | None): ... """, ) as module: function = module["f_annorations"] assert len(function.parameters) == 4 param = function.parameters[0] assert param.annotation.name == "str" assert param.annotation.canonical_path == "str" param = function.parameters[1] assert param.annotation.name == "Any" assert param.annotation.canonical_path == "typing.Any" param = function.parameters[2] assert str(param.annotation) == "typing.Optional[typing.List[int]]" param = function.parameters[3] assert str(param.annotation) == "float | None" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_encoders.py����������������������������������������������������������0000664�0001750�0001750�00000003255�14767006246�020561� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `encoders` module.""" from __future__ import annotations import json import pytest from jsonschema import ValidationError, validate from griffe import Function, GriffeLoader, Module, Object def test_minimal_data_is_enough() -> None: """Test serialization and de-serialization. This is an end-to-end test that asserts we can load back a serialized tree and infer as much data as within the original tree. """ loader = GriffeLoader() module = loader.load("griffe") minimal = module.as_json(full=False) full = module.as_json(full=True) reloaded = Module.from_json(minimal) assert reloaded.as_json(full=False) == minimal assert reloaded.as_json(full=True) == full # Also works (but will result in a different type hint). assert Object.from_json(minimal) # Won't work if the JSON doesn't represent the type requested. with pytest.raises(TypeError, match="provided JSON object is not of type"): Function.from_json(minimal) # use this function in test_json_schema to ease schema debugging def _validate(obj: dict, schema: dict) -> None: if "members" in obj: for member in obj["members"]: _validate(member, schema) try: validate(obj, schema) except ValidationError: print(obj["path"]) # noqa: T201 raise def test_json_schema() -> None: """Assert that our serialized data matches our JSON schema.""" loader = GriffeLoader() module = loader.load("griffe") loader.resolve_aliases() data = json.loads(module.as_json(full=True)) with open("docs/schema.json") as f: # noqa: PTH123 schema = json.load(f) validate(data, schema) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_models.py������������������������������������������������������������0000664�0001750�0001750�00000041075�14767006246�020244� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `dataclasses` module.""" from __future__ import annotations from copy import deepcopy from textwrap import dedent import pytest from griffe import ( Attribute, Docstring, GriffeLoader, Module, NameResolutionError, Parameter, Parameters, module_vtree, temporary_inspected_module, temporary_pypackage, temporary_visited_module, temporary_visited_package, ) def test_submodule_exports() -> None: """Check that a module is exported depending on whether it was also imported.""" root = Module("root") sub = Module("sub") private = Attribute("_private") root["sub"] = sub root["_private"] = private assert not sub.is_wildcard_exposed root.imports["sub"] = "root.sub" assert sub.is_wildcard_exposed assert not private.is_wildcard_exposed root.exports = ["_private"] assert private.is_wildcard_exposed def test_has_docstrings() -> None: """Assert the `.has_docstrings` method is recursive.""" with temporary_visited_module("class A:\n '''Hello.'''") as module: assert module.has_docstrings def test_has_docstrings_submodules() -> None: """Assert the `.has_docstrings` method descends into submodules.""" module = module_vtree("a.b.c.d") module["b.c.d"].docstring = Docstring("Hello.") assert module.has_docstrings def test_handle_aliases_chain_in_has_docstrings() -> None: """Assert the `.has_docstrings` method can handle aliases chains in members.""" with temporary_pypackage("package", ["mod_a.py", "mod_b.py"]) as tmp_package: mod_a = tmp_package.path / "mod_a.py" mod_b = tmp_package.path / "mod_b.py" mod_a.write_text("from .mod_b import someobj") mod_b.write_text("from somelib import someobj") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) assert not package.has_docstrings loader.resolve_aliases(implicit=True) assert not package.has_docstrings def test_has_docstrings_does_not_trigger_alias_resolution() -> None: """Assert the `.has_docstrings` method does not trigger alias resolution.""" with temporary_pypackage("package", ["mod_a.py", "mod_b.py"]) as tmp_package: mod_a = tmp_package.path / "mod_a.py" mod_b = tmp_package.path / "mod_b.py" mod_a.write_text("from .mod_b import someobj") mod_b.write_text("from somelib import someobj") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) assert not package.has_docstrings assert not package["mod_a.someobj"].resolved def test_deepcopy() -> None: """Assert we can deep-copy object trees.""" loader = GriffeLoader() mod = loader.load("griffe") deepcopy(mod) deepcopy(mod.as_dict()) def test_dataclass_properties_and_class_variables() -> None: """Don't return properties or class variables as parameters of dataclasses.""" code = """ from dataclasses import dataclass from functools import cached_property from typing import ClassVar @dataclass class Point: x: float y: float # These definitions create class variables. r: ClassVar[float] s: float = 3 t: ClassVar[float] = 3 @property def a(self): return 0 @cached_property def b(self): return 0 """ with temporary_visited_package("package", {"__init__.py": code}) as module: params = module["Point"].parameters assert [p.name for p in params] == ["self", "x", "y", "s"] @pytest.mark.parametrize( "code", [ """ @dataclass class Dataclass: x: float y: float = field(kw_only=True) class Class: def __init__(self, x: float, *, y: float): ... """, """ @dataclass class Dataclass: x: float = field(kw_only=True) y: float class Class: def __init__(self, y: float, *, x: float): ... """, """ @dataclass class Dataclass: x: float _: KW_ONLY y: float class Class: def __init__(self, x: float, *, y: float): ... """, """ @dataclass class Dataclass: _: KW_ONLY x: float y: float class Class: def __init__(self, *, x: float, y: float): ... """, """ @dataclass(kw_only=True) class Dataclass: x: float y: float class Class: def __init__(self, *, x: float, y: float): ... """, ], ) def test_dataclass_parameter_kinds(code: str) -> None: """Check dataclass and equivalent non-dataclass parameters. The parameter kinds for each pair should be the same. Parameters: code: Python code to visit. """ code = f"from dataclasses import dataclass, field, KW_ONLY\n\n{dedent(code)}" with temporary_visited_package("package", {"__init__.py": code}) as module: for dataclass_param, regular_param in zip(module["Dataclass"].parameters, module["Class"].parameters): assert dataclass_param == regular_param def test_regular_class_inheriting_dataclass_dont_get_its_own_params() -> None: """A regular class inheriting from a dataclass don't have its attributes added to `__init__`.""" code = """ from dataclasses import dataclass @dataclass class Base: a: int b: str @dataclass class Derived1(Base): c: float class Derived2(Base): d: float """ with temporary_visited_package("package", {"__init__.py": code}) as module: params1 = module["Derived1"].parameters params2 = module["Derived2"].parameters assert [p.name for p in params1] == ["self", "a", "b", "c"] assert [p.name for p in params2] == ["self", "a", "b"] def test_regular_class_inheriting_dataclass_is_labelled_dataclass() -> None: """A regular class inheriting from a dataclass is labelled as a dataclass too.""" code = """ from dataclasses import dataclass @dataclass class Base: pass class Derived(Base): pass """ with temporary_visited_package("package", {"__init__.py": code}) as module: obj = module["Derived"] assert "dataclass" in obj.labels def test_fields_with_init_false() -> None: """Fields marked with `init=False` are not added to the `__init__` method.""" code = """ from dataclasses import dataclass, field @dataclass class PointA: x: float y: float z: float = field(init=False) @dataclass(init=False) class PointB: x: float y: float @dataclass(init=False) class PointC: x: float y: float = field(init=True) # `init=True` has no effect. """ with temporary_visited_package("package", {"__init__.py": code}) as module: params_a = module["PointA"].parameters params_b = module["PointB"].parameters params_c = module["PointC"].parameters assert "z" not in params_a assert "x" not in params_b assert "y" not in params_b assert "x" not in params_c assert "y" not in params_c def test_parameters_are_reorderd_to_match_their_kind() -> None: """Keyword-only parameters in base class are pushed back to the end of the signature.""" code = """ from dataclasses import dataclass @dataclass(kw_only=True) class Base: a: int b: str @dataclass class Reordered(Base): b: float c: float """ with temporary_visited_package("package", {"__init__.py": code}) as module: params_base = module["Base"].parameters params_reordered = module["Reordered"].parameters assert [p.name for p in params_base] == ["self", "a", "b"] assert [p.name for p in params_reordered] == ["self", "b", "c", "a"] assert str(params_reordered["b"].annotation) == "float" def test_parameters_annotated_as_initvar() -> None: """Don't return InitVar annotated fields as class members. But if __init__ is defined, InitVar has no effect. """ code = """ from dataclasses import dataclass, InitVar @dataclass class PointA: x: float y: float z: InitVar[float] @dataclass class PointB: x: float y: float z: InitVar[float] def __init__(self, r: float): ... """ with temporary_visited_package("package", {"__init__.py": code}) as module: point_a = module["PointA"] assert [p.name for p in point_a.parameters] == ["self", "x", "y", "z"] assert list(point_a.members) == ["x", "y", "__init__"] point_b = module["PointB"] assert [p.name for p in point_b.parameters] == ["self", "r"] assert list(point_b.members) == ["x", "y", "z", "__init__"] def test_visited_module_source() -> None: """Check the source property of a module.""" code = "print('hello')\nprint('world')" with temporary_visited_package("package", {"__init__.py": code}) as module: assert module.source == code def test_visited_class_source() -> None: """Check the source property of a class.""" code = """ class A: def __init__(self, x: int): self.x = x """ with temporary_visited_package("package", {"__init__.py": code}) as module: assert module["A"].source == dedent(code).strip() def test_visited_object_source_with_missing_line_number() -> None: """Check the source property of an object with missing line number.""" code = """ class A: def __init__(self, x: int): self.x = x """ with temporary_visited_package("package", {"__init__.py": code}) as module: module["A"].endlineno = None assert not module["A"].source module["A"].endlineno = 3 module["A"].lineno = None assert not module["A"].source def test_inspected_module_source() -> None: """Check the source property of a module.""" code = "print('hello')\nprint('world')" with temporary_inspected_module(code) as module: assert module.source == code def test_inspected_class_source() -> None: """Check the source property of a class.""" code = """ class A: def __init__(self, x: int): self.x = x """ with temporary_inspected_module(code) as module: assert module["A"].source == dedent(code).strip() def test_inspected_object_source_with_missing_line_number() -> None: """Check the source property of an object with missing line number.""" code = """ class A: def __init__(self, x: int): self.x = x """ with temporary_inspected_module(code) as module: module["A"].endlineno = None assert not module["A"].source module["A"].endlineno = 3 module["A"].lineno = None assert not module["A"].source def test_dataclass_parameter_docstrings() -> None: """Class parameters should have a docstring attribute.""" code = """ from dataclasses import dataclass, InitVar @dataclass class Base: a: int "Parameter a" b: InitVar[int] = 3 "Parameter b" @dataclass class Derived(Base): c: float d: InitVar[float] "Parameter d" """ with temporary_visited_package("package", {"__init__.py": code}) as module: base = module["Base"] param_self = base.parameters[0] param_a = base.parameters[1] param_b = base.parameters[2] assert param_self.docstring is None assert param_a.docstring.value == "Parameter a" assert param_b.docstring.value == "Parameter b" derived = module["Derived"] param_self = derived.parameters[0] param_a = derived.parameters[1] param_b = derived.parameters[2] param_c = derived.parameters[3] param_d = derived.parameters[4] assert param_self.docstring is None assert param_a.docstring.value == "Parameter a" assert param_b.docstring.value == "Parameter b" assert param_c.docstring is None assert param_d.docstring.value == "Parameter d" def test_attributes_that_have_no_annotations() -> None: """Dataclass attributes that have no annotatations are not parameters.""" code = """ from dataclasses import dataclass, field @dataclass class Base: a: int b: str = field(init=False) c = 3 # Class attribute. @dataclass class Derived(Base): a = 1 # No effect on the parameter status of `a`. b = "b" # Inherited non-parameter. d: float = 4 """ with temporary_visited_package("package", {"__init__.py": code}) as module: base_params = [p.name for p in module["Base"].parameters] derived_params = [p.name for p in module["Derived"].parameters] assert base_params == ["self", "a"] assert derived_params == ["self", "a", "d"] def test_name_resolution() -> None: """Name are correctly resolved in the scope of an object.""" code = """ module_attribute = 0 class Class: import imported class_attribute = 0 def __init__(self): self.instance_attribute = 0 def method(self): local_variable = 0 """ with temporary_visited_module(code) as module: assert module.resolve("module_attribute") == "module.module_attribute" assert module.resolve("Class") == "module.Class" assert module["module_attribute"].resolve("Class") == "module.Class" with pytest.raises(NameResolutionError): module["module_attribute"].resolve("class_attribute") assert module["Class"].resolve("module_attribute") == "module.module_attribute" assert module["Class"].resolve("imported") == "imported" assert module["Class"].resolve("class_attribute") == "module.Class.class_attribute" assert module["Class"].resolve("instance_attribute") == "module.Class.instance_attribute" assert module["Class"].resolve("method") == "module.Class.method" assert module["Class.class_attribute"].resolve("module_attribute") == "module.module_attribute" assert module["Class.class_attribute"].resolve("Class") == "module.Class" assert module["Class.class_attribute"].resolve("imported") == "imported" assert module["Class.class_attribute"].resolve("instance_attribute") == "module.Class.instance_attribute" assert module["Class.class_attribute"].resolve("method") == "module.Class.method" assert module["Class.instance_attribute"].resolve("module_attribute") == "module.module_attribute" assert module["Class.instance_attribute"].resolve("Class") == "module.Class" assert module["Class.instance_attribute"].resolve("imported") == "imported" assert module["Class.instance_attribute"].resolve("class_attribute") == "module.Class.class_attribute" assert module["Class.instance_attribute"].resolve("method") == "module.Class.method" assert module["Class.method"].resolve("module_attribute") == "module.module_attribute" assert module["Class.method"].resolve("Class") == "module.Class" assert module["Class.method"].resolve("imported") == "imported" assert module["Class.method"].resolve("class_attribute") == "module.Class.class_attribute" assert module["Class.method"].resolve("instance_attribute") == "module.Class.instance_attribute" def test_set_parameters() -> None: """We can set parameters.""" parameters = Parameters() # Does not exist yet. parameters["x"] = Parameter(name="x") assert "x" in parameters # Already exists, by name. parameters["x"] = Parameter(name="x") assert "x" in parameters assert len(parameters) == 1 # Already exists, by index. parameters[0] = Parameter(name="y") assert "y" in parameters assert len(parameters) == 1 def test_delete_parameters() -> None: """We can delete parameters.""" parameters = Parameters() # By name. parameters["x"] = Parameter(name="x") del parameters["x"] assert "x" not in parameters assert len(parameters) == 0 # By index. parameters["x"] = Parameter(name="x") del parameters[0] assert "x" not in parameters assert len(parameters) == 0 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_merger.py������������������������������������������������������������0000664�0001750�0001750�00000003102�14767006246�020227� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `merger` module.""" from __future__ import annotations from griffe import temporary_visited_package def test_dont_trigger_alias_resolution_when_merging_stubs() -> None: """Assert that we don't trigger alias resolution when merging stubs.""" with temporary_visited_package( "package", { "mod.py": "import pathlib\n\ndef f() -> pathlib.Path:\n return pathlib.Path()", "mod.pyi": "import pathlib\n\ndef f() -> pathlib.Path: ...", }, ) as pkg: assert not pkg["mod.pathlib"].resolved def test_merge_stubs_on_wildcard_imported_objects() -> None: """Assert that stubs can be merged on wildcard imported objects.""" with temporary_visited_package( "package", { "mod.py": "class A:\n def hello(value: int | str) -> int | str:\n return value", "__init__.py": "from .mod import *", "__init__.pyi": """ from typing import overload class A: @overload def hello(value: int) -> int: ... @overload def hello(value: str) -> str: ... """, }, ) as pkg: assert pkg["A.hello"].overloads def test_merge_imports() -> None: """Assert that imports are merged correctly.""" with temporary_visited_package( "package", { "mod.py": "import abc", "mod.pyi": "import collections", }, ) as pkg: assert set(pkg["mod"].imports) == {"abc", "collections"} ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/conftest.py���������������������������������������������������������������0000664�0001750�0001750�00000000057�14767006246�017542� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Configuration for the pytest test suite.""" ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/����������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�020557� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/__init__.py�����������������������������������������������0000664�0001750�0001750�00000000034�14767006246�022665� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for docstrings.""" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/helpers.py������������������������������������������������0000664�0001750�0001750�00000004222�14767006246�022573� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains helpers for testing docstring parsing.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Protocol, Union from griffe import ( Attribute, Class, Docstring, DocstringSection, Function, LogLevel, Module, ) if TYPE_CHECKING: from collections.abc import Iterator from types import ModuleType ParentType = Union[Module, Class, Function, Attribute, None] ParseResultType = tuple[list[DocstringSection], list[str]] class ParserType(Protocol): # noqa: D101 def __call__( # noqa: D102 self, docstring: str, parent: ParentType | None = None, **parser_opts: Any, ) -> ParseResultType: ... def parser(parser_module: ModuleType) -> Iterator[ParserType]: """Wrap a parser to help testing. Parameters: parser_module: The parser module containing a `parse` function. Yields: The wrapped function. """ original_warn = parser_module.docstring_warning def parse(docstring: str, parent: ParentType | None = None, **parser_opts: Any) -> ParseResultType: """Parse a docstring. Parameters: docstring: The docstring to parse. parent: The docstring's parent object. **parser_opts: Additional options accepted by the parser. Returns: The parsed sections, and warnings. """ docstring_object = Docstring(docstring, lineno=1, endlineno=None) docstring_object.endlineno = len(docstring_object.lines) + 1 if parent is not None: docstring_object.parent = parent parent.docstring = docstring_object warnings = [] parser_module.docstring_warning = ( # type: ignore[attr-defined] lambda _docstring, _offset, message, log_level=LogLevel.warning: warnings.append(message) ) func_name = f"parse_{parser_module.__name__.split('.')[-1]}" func = getattr(parser_module, func_name) sections = func(docstring_object, **parser_opts) return sections, warnings yield parse parser_module.docstring_warning = original_warn # type: ignore[attr-defined] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/test_warnings.py������������������������������������������0000664�0001750�0001750�00000001450�14767006246�024020� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the docstrings utility functions.""" from __future__ import annotations from griffe import Docstring, Function, Parameter, ParameterKind, Parameters, Parser, parse def test_can_warn_without_parent_module() -> None: """Assert we can parse a docstring even if it does not have a parent module.""" function = Function( "func", parameters=Parameters( Parameter("param1", annotation=None, kind=ParameterKind.positional_or_keyword), Parameter("param2", annotation="int", kind=ParameterKind.keyword_only), ), ) text = """ Hello I'm a docstring! Parameters: param1: Description. param2: Description. """ docstring = Docstring(text, lineno=1, parent=function) assert parse(docstring, Parser.google) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/test_numpy.py���������������������������������������������0000664�0001750�0001750�00000105637�14767006246�023354� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the [Numpy-style parser][griffe.docstrings.numpy].""" from __future__ import annotations import logging from typing import TYPE_CHECKING import pytest from griffe import ( Attribute, Class, Docstring, DocstringSectionKind, ExprName, Function, Module, Parameter, Parameters, parse_docstring_annotation, ) if TYPE_CHECKING: from tests.test_docstrings.helpers import ParserType # ============================================================================================= # Markup flow (multilines, indentation, etc.) def test_simple_docstring(parse_numpy: ParserType) -> None: """Parse a simple docstring. Parameters: parse_numpy: Fixture parser. """ sections, warnings = parse_numpy("A simple docstring.") assert len(sections) == 1 assert not warnings def test_multiline_docstring(parse_numpy: ParserType) -> None: """Parse a multi-line docstring. Parameters: parse_numpy: Fixture parser. """ sections, warnings = parse_numpy( """ A somewhat longer docstring. Blablablabla. """, ) assert len(sections) == 1 assert not warnings def test_code_blocks(parse_numpy: ParserType) -> None: """Parse code blocks. Parameters: parse_numpy: Fixture parser. """ docstring = """ This docstring contains a code block! ```python print("hello") ``` """ sections, warnings = parse_numpy(docstring) assert len(sections) == 1 assert not warnings def test_indented_code_block(parse_numpy: ParserType) -> None: """Parse indented code blocks. Parameters: parse_numpy: Fixture parser. """ docstring = """ This docstring contains a docstring in a code block o_O! \"\"\" This docstring is contained in another docstring O_o! Parameters: s: A string. \"\"\" """ sections, warnings = parse_numpy(docstring) assert len(sections) == 1 assert not warnings def test_empty_indented_lines_in_section_with_items(parse_numpy: ParserType) -> None: """In sections with items, don't treat lines with just indentation as items. Parameters: parse_numpy: Fixture parser. """ docstring = "Returns\n-------\nonly_item : type\n Description.\n \n \n\nSomething." sections, _ = parse_numpy(docstring) assert len(sections) == 1 assert len(sections[0].value) == 2 def test_doubly_indented_lines_in_section_items(parse_numpy: ParserType) -> None: """In sections with items, don't remove all spaces on the left of indented lines. Parameters: parse_numpy: Fixture parser. """ docstring = "Returns\n-------\nonly_item : type\n Description:\n\n - List item.\n - Sublist item." sections, _ = parse_numpy(docstring) assert len(sections) == 1 lines = sections[0].value[0].description.split("\n") assert lines[-1].startswith(4 * " " + "- ") # ============================================================================================= # Admonitions def test_admonition_see_also(parse_numpy: ParserType) -> None: """Test a "See Also" admonition. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary text. See Also -------- some_function more text """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].value == "Summary text." assert sections[1].title == "See Also" assert sections[1].value.description == "some_function\n\nmore text" def test_admonition_empty(parse_numpy: ParserType) -> None: """Test an empty "See Also" admonition. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary text. See Also -------- """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].value == "Summary text." assert sections[1].title == "See Also" assert sections[1].value.description == "" def test_isolated_dash_lines_do_not_create_sections(parse_numpy: ParserType) -> None: """An isolated dash-line (`---`) should not be parsed as a section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary text. --- Text. Note ---- Note contents. --- Text. """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].value == "Summary text.\n\n---\nText." assert sections[1].title == "Note" assert sections[1].value.description == "Note contents.\n\n---\nText." def test_admonition_warnings_special_case(parse_numpy: ParserType) -> None: """Test that the "Warnings" section renders as a warning admonition. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary text. Warnings -------- Be careful!!! more text """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].value == "Summary text." assert sections[1].title == "Warnings" assert sections[1].value.description == "Be careful!!!\n\nmore text" assert sections[1].value.kind == "warning" def test_admonition_notes_special_case(parse_numpy: ParserType) -> None: """Test that the "Warnings" section renders as a warning admonition. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary text. Notes ----- Something noteworthy. more text """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].value == "Summary text." assert sections[1].title == "Notes" assert sections[1].value.description == "Something noteworthy.\n\nmore text" assert sections[1].value.kind == "note" # ============================================================================================= # Annotations def test_prefer_docstring_type_over_annotation(parse_numpy: ParserType) -> None: """Prefer the type written in the docstring over the annotation in the parent. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- a : int """ sections, _ = parse_numpy( docstring, parent=Function("func", parameters=Parameters(Parameter("a", annotation="str"))), ) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "a" assert param.description == "" assert param.annotation.name == "int" def test_parse_complex_annotations(parse_numpy: ParserType) -> None: """Check the type regex accepts all the necessary characters. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- a : typing.Tuple[str, random0123456789] b : int | float | None c : Literal['hello'] | Literal["world"] """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param_a, param_b, param_c = sections[0].value assert param_a.name == "a" assert param_a.description == "" assert param_a.annotation == "typing.Tuple[str, random0123456789]" assert param_b.name == "b" assert param_b.description == "" assert param_b.annotation == "int | float | None" assert param_c.name == "c" assert param_c.description == "" assert param_c.annotation == "Literal['hello'] | Literal[\"world\"]" @pytest.mark.parametrize( ("docstring", "name"), [ ("Attributes\n---\na : {name}\n Description.\n", "int"), ("Parameters\n---\na : {name}\n Description.\n", "int"), ("Other Parameters\n---\na : {name}\n Description.\n", "int"), ("Yields\n---\na : {name}\n Description.\n", "int"), ("Receives\n---\na : {name}\n Description.\n", "int"), ("Returns\n---\na : {name}\n Description.\n", "int"), ("Raises\n---\n{name}\n Description.\n", "RuntimeError"), ("Warns\n---\n{name}\n Description.\n", "UserWarning"), ], ) def test_parse_annotations_in_all_sections(parse_numpy: ParserType, docstring: str, name: str) -> None: """Assert annotations are parsed in all relevant sections. Parameters: parse_numpy: Fixture parser. docstring: Parametrized docstring. name: Parametrized name in annotation. """ docstring = docstring.format(name=name) sections, _ = parse_numpy(docstring, parent=Function("f")) assert len(sections) == 1 assert sections[0].value[0].annotation.name == name def test_dont_crash_on_text_annotations(parse_numpy: ParserType, caplog: pytest.LogCaptureFixture) -> None: """Don't crash while parsing annotations containing unhandled nodes. Parameters: parse_numpy: Fixture parser. caplog: Pytest fixture used to capture logs. """ docstring = """ Attributes ---------- region : str, list-like, geopandas.GeoSeries, geopandas.GeoDataFrame, geometric Description. Parameters ---------- region : str, list-like, geopandas.GeoSeries, geopandas.GeoDataFrame, geometric Description. Returns ------- str or bytes Description. Receives -------- region : str, list-like, geopandas.GeoSeries, geopandas.GeoDataFrame, geometric Description. Yields ------ str or bytes Description. """ caplog.set_level(logging.DEBUG) assert parse_numpy(docstring, parent=Function("f")) assert all(record.levelname == "DEBUG" for record in caplog.records if "Failed to parse" in record.message) # ============================================================================================= # Sections def test_parameters_section(parse_numpy: ParserType) -> None: """Parse parameters section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- a b : int c : str, optional d : float, default=1.0 e, f g, h : bytes, optional, default=b'' i : {0, 1, 2} j : {"a", 1, None, True} k K's description. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 def test_parse_starred_parameters(parse_numpy: ParserType) -> None: """Parse parameters names with stars in them. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- *a : str **b : int ***c : float """ sections, warnings = parse_numpy(docstring) assert len(sections) == 1 assert len(warnings) == 1 def test_other_parameters_section(parse_numpy: ParserType) -> None: """Parse other parameters section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Other Parameters ---------------- a b : int c : str, optional d : float, default=1.0 e, f g, h : bytes, optional, default=b'' i : {0, 1, 2} j : {"a", 1, None, True} k K's description. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 def test_retrieve_annotation_from_parent(parse_numpy: ParserType) -> None: """Retrieve parameter annotation from the parent object. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- a """ sections, _ = parse_numpy( docstring, parent=Function("func", parameters=Parameters(Parameter("a", annotation="str"))), ) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "a" assert param.description == "" assert param.annotation == "str" def test_deprecated_section(parse_numpy: ParserType) -> None: """Parse deprecated section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Deprecated ---------- 1.23.4 Deprecated. Sorry. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 assert sections[0].value.version == "1.23.4" assert sections[0].value.description == "Deprecated.\nSorry." def test_returns_section(parse_numpy: ParserType) -> None: """Parse returns section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Returns ------- list of int A list of integers. flag : bool Some kind of flag. x : Name only : No name or annotation : int Only annotation """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "" assert param.description == "A list of integers." assert param.annotation == "list of int" param = sections[0].value[1] assert param.name == "flag" assert param.description == "Some kind\nof flag." assert param.annotation == "bool" param = sections[0].value[2] assert param.name == "x" assert param.description == "Name only" assert param.annotation is None param = sections[0].value[3] assert param.name == "" assert param.description == "No name or annotation" assert param.annotation is None param = sections[0].value[4] assert param.name == "" assert param.description == "Only annotation" assert param.annotation == "int" def test_yields_section(parse_numpy: ParserType) -> None: """Parse yields section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Yields ------ list of int A list of integers. flag : bool Some kind of flag. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "" assert param.description == "A list of integers." assert param.annotation == "list of int" param = sections[0].value[1] assert param.name == "flag" assert param.description == "Some kind\nof flag." assert param.annotation == "bool" def test_receives_section(parse_numpy: ParserType) -> None: """Parse receives section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Receives -------- list of int A list of integers. flag : bool Some kind of flag. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "" assert param.description == "A list of integers." assert param.annotation == "list of int" param = sections[0].value[1] assert param.name == "flag" assert param.description == "Some kind\nof flag." assert param.annotation == "bool" def test_raises_section(parse_numpy: ParserType) -> None: """Parse raises section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Raises ------ RuntimeError There was an issue. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.description == "There was an issue." assert param.annotation == "RuntimeError" def test_warns_section(parse_numpy: ParserType) -> None: """Parse warns section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Warns ----- ResourceWarning Heads up. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.description == "Heads up." assert param.annotation == "ResourceWarning" def test_attributes_section(parse_numpy: ParserType) -> None: """Parse attributes section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Attributes ---------- a Hello. m z : int Bye. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 param = sections[0].value[0] assert param.name == "a" assert param.description == "Hello." assert param.annotation is None param = sections[0].value[1] assert param.name == "m" assert param.description == "" assert param.annotation is None param = sections[0].value[2] assert param.name == "z" assert param.description == "Bye." assert param.annotation == "int" def test_parse_functions_section(parse_numpy: ParserType) -> None: """Parse Functions/Methods sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Functions --------- f(a, b=2) Hello. g Hi. Methods ------- f(a, b=2) Hello. g Hi. """ sections, warnings = parse_numpy(docstring) assert len(sections) == 2 for section in sections: assert section.kind is DocstringSectionKind.functions func_f = section.value[0] assert func_f.name == "f" assert func_f.signature == "f(a, b=2)" assert func_f.description == "Hello." func_g = section.value[1] assert func_g.name == "g" assert func_g.signature is None assert func_g.description == "Hi." assert not warnings def test_parse_classes_section(parse_numpy: ParserType) -> None: """Parse Classes sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Classes ------- C(a, b=2) Hello. D Hi. """ sections, warnings = parse_numpy(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.classes class_c = sections[0].value[0] assert class_c.name == "C" assert class_c.signature == "C(a, b=2)" assert class_c.description == "Hello." class_d = sections[0].value[1] assert class_d.name == "D" assert class_d.signature is None assert class_d.description == "Hi." assert not warnings def test_parse_modules_section(parse_numpy: ParserType) -> None: """Parse Modules sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Modules ------- m Hello. n Hi. """ sections, warnings = parse_numpy(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.modules module_m = sections[0].value[0] assert module_m.name == "m" assert module_m.description == "Hello." module_n = sections[0].value[1] assert module_n.name == "n" assert module_n.description == "Hi." assert not warnings def test_examples_section(parse_numpy: ParserType) -> None: """Parse examples section. Parameters: parse_numpy: Fixture parser. """ docstring = """ Examples -------- Hello. >>> 1 + 2 3 ```pycon >>> print("Hello again.") ``` >>> a = 0 # doctest: +SKIP >>> b = a + 1 >>> print(b) 1 Bye. -------- Not in the section. """ sections, _ = parse_numpy(docstring, trim_doctest_flags=False) assert len(sections) == 2 examples = sections[0] assert len(examples.value) == 5 assert examples.value[0] == (DocstringSectionKind.text, "Hello.") assert examples.value[1] == (DocstringSectionKind.examples, ">>> 1 + 2\n3") assert examples.value[3][1].startswith(">>> a = 0 # doctest: +SKIP") def test_examples_section_when_followed_by_named_section(parse_numpy: ParserType) -> None: """Parse examples section followed by another section. Parameters: parse_numpy: Parse function (fixture). """ docstring = """ Examples -------- Hello, hello. Parameters ---------- foo : int """ sections, _ = parse_numpy(docstring, trim_doctest_flags=False) assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.examples assert sections[1].kind is DocstringSectionKind.parameters def test_examples_section_as_last(parse_numpy: ParserType) -> None: """Parse examples section being last in the docstring. Parameters: parse_numpy: Parse function (fixture). """ docstring = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit... Examples -------- ```pycon >>> LoremIpsum.from_string("consectetur") <foofoo: Ipsum.Lorem> ``` """ sections, _ = parse_numpy(docstring) assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text assert sections[1].kind is DocstringSectionKind.examples def test_blank_lines_in_section(parse_numpy: ParserType) -> None: """Support blank lines in the middle of sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Examples -------- Line 1. Line 2. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 # ============================================================================================= # Attributes sections def test_retrieve_attributes_annotation_from_parent(parse_numpy: ParserType) -> None: """Retrieve the annotations of attributes from the parent object. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary. Attributes ---------- a : Whatever. b : Whatever. """ parent = Class("cls") parent["a"] = Attribute("a", annotation=ExprName("int")) parent["b"] = Attribute("b", annotation=ExprName("str")) sections, _ = parse_numpy(docstring, parent=parent) attributes = sections[1].value assert attributes[0].name == "a" assert attributes[0].annotation.name == "int" assert attributes[1].name == "b" assert attributes[1].annotation.name == "str" # ============================================================================================= # Parameters sections def test_warn_about_unknown_parameters(parse_numpy: ParserType) -> None: """Warn about unknown parameters in "Parameters" sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- x : int Integer. y : int Integer. """ _, warnings = parse_numpy( docstring, parent=Function( "func", parameters=Parameters( Parameter("a"), Parameter("y"), ), ), ) assert len(warnings) == 1 assert "'x' does not appear in the function signature" in warnings[0] def test_never_warn_about_unknown_other_parameters(parse_numpy: ParserType) -> None: """Never warn about unknown parameters in "Other parameters" sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Other Parameters ---------------- x : int Integer. z : int Integer. """ _, warnings = parse_numpy( docstring, parent=Function( "func", parameters=Parameters( Parameter("a"), Parameter("y"), ), ), ) assert not warnings def test_unknown_params_scan_doesnt_crash_without_parameters(parse_numpy: ParserType) -> None: """Assert we don't crash when parsing parameters sections and parent object does not have parameters. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- this : str This. that : str That. """ _, warnings = parse_numpy(docstring, parent=Module("mod")) assert not warnings def test_class_uses_init_parameters(parse_numpy: ParserType) -> None: """Assert we use the `__init__` parameters when parsing classes' parameters sections. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- x : X value. """ parent = Class("c") parent["__init__"] = Function("__init__", parameters=Parameters(Parameter("x", annotation="int"))) sections, warnings = parse_numpy(docstring, parent=parent) assert not warnings argx = sections[0].value[0] assert argx.name == "x" assert argx.annotation == "int" assert argx.description == "X value." def test_detect_optional_flag(parse_numpy: ParserType) -> None: """Detect the optional part of a parameter docstring. Parameters: parse_numpy: Fixture parser. """ docstring = """ Parameters ---------- a : str, optional g, h : bytes, optional, default=b'' """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 assert sections[0].value[0].annotation == "str" assert sections[0].value[1].annotation == "bytes" assert sections[0].value[1].default == "b''" assert sections[0].value[2].annotation == "bytes" assert sections[0].value[2].default == "b''" @pytest.mark.parametrize("newlines", [1, 2, 3]) def test_blank_lines_in_item_descriptions(parse_numpy: ParserType, newlines: int) -> None: """Support blank lines in the middle of item descriptions. Parameters: parse_numpy: Fixture parser. newlines: Number of new lines between item summary and its body. """ nl = "\n" nlindent = "\n" + " " * 12 docstring = f""" Parameters ---------- a : str Summary.{nlindent * newlines}Body. """ sections, _ = parse_numpy(docstring) assert len(sections) == 1 assert sections[0].value[0].annotation == "str" assert sections[0].value[0].description == f"Summary.{nl * newlines}Body." # ============================================================================================= # Yields sections @pytest.mark.parametrize( "return_annotation", [ "Iterator[tuple[int, float]]", "Generator[tuple[int, float], ..., ...]", ], ) def test_parse_yields_tuple_in_iterator_or_generator(parse_numpy: ParserType, return_annotation: str) -> None: """Parse Yields annotations in Iterator or Generator types. Parameters: parse_numpy: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Yields ------ a : Whatever. b : Whatever. """ sections, _ = parse_numpy( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) yields = sections[1].value assert yields[0].name == "a" assert yields[0].annotation.name == "int" assert yields[1].name == "b" assert yields[1].annotation.name == "float" @pytest.mark.parametrize( "return_annotation", [ "Iterator[int]", "Generator[int, None, None]", ], ) def test_extract_yielded_type_with_single_return_item(parse_numpy: ParserType, return_annotation: str) -> None: """Extract main type annotation from Iterator or Generator. Parameters: parse_numpy: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Yields ------ a : A number. """ sections, _ = parse_numpy( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) yields = sections[1].value assert yields[0].annotation.name == "int" def test_yield_section_in_property(parse_numpy: ParserType) -> None: """No warnings when parsing Yields section in a property. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary. Yields ------ : A number. """ sections, warnings = parse_numpy( docstring, parent=Attribute( "prop", annotation=parse_docstring_annotation("Iterator[int]", Docstring("d", parent=Attribute("a"))), ), ) assert not warnings yields = sections[1].value assert yields[0].annotation.name == "int" # ============================================================================================= # Receives sections def test_parse_receives_tuple_in_generator(parse_numpy: ParserType) -> None: """Parse Receives annotations in Generator type. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary. Receives -------- a : Whatever. b : Whatever. """ sections, _ = parse_numpy( docstring, parent=Function( "func", returns=parse_docstring_annotation( "Generator[..., tuple[int, float], ...]", Docstring("d", parent=Function("f")), ), ), ) receives = sections[1].value assert receives[0].name == "a" assert receives[0].annotation.name == "int" assert receives[1].name == "b" assert receives[1].annotation.name == "float" @pytest.mark.parametrize( "return_annotation", [ "Generator[int, float, None]", ], ) def test_extract_received_type_with_single_return_item(parse_numpy: ParserType, return_annotation: str) -> None: """Extract main type annotation from Iterator or Generator. Parameters: parse_numpy: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Receives -------- a : A floating point number. """ sections, _ = parse_numpy( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) receives = sections[1].value assert receives[0].annotation.name == "float" # ============================================================================================= # Returns sections def test_parse_returns_tuple_in_generator(parse_numpy: ParserType) -> None: """Parse Returns annotations in Generator type. Parameters: parse_numpy: Fixture parser. """ docstring = """ Summary. Returns ------- a : Whatever. b : Whatever. """ sections, _ = parse_numpy( docstring, parent=Function( "func", returns=parse_docstring_annotation( "Generator[..., ..., tuple[int, float]]", Docstring("d", parent=Function("f")), ), ), ) returns = sections[1].value assert returns[0].name == "a" assert returns[0].annotation.name == "int" assert returns[1].name == "b" assert returns[1].annotation.name == "float" # ============================================================================================= # Parser special features @pytest.mark.parametrize( "docstring", [ "", "\n", "\n\n", "Summary.", "Summary.\n\n\n", "Summary.\n\nParagraph.", "Summary\non two lines.", "Summary\non two lines.\n\nParagraph.", ], ) def test_ignore_init_summary(parse_numpy: ParserType, docstring: str) -> None: """Correctly ignore summary in `__init__` methods' docstrings. Parameters: parse_numpy: Fixture parser. docstring: The docstring to parse (parametrized). """ sections, _ = parse_numpy(docstring, parent=Function("__init__", parent=Class("C")), ignore_init_summary=True) for section in sections: assert "Summary" not in section.value if docstring.strip(): sections, _ = parse_numpy(docstring, parent=Function("__init__", parent=Module("M")), ignore_init_summary=True) assert "Summary" in sections[0].value sections, _ = parse_numpy(docstring, parent=Function("f", parent=Class("C")), ignore_init_summary=True) assert "Summary" in sections[0].value sections, _ = parse_numpy(docstring, ignore_init_summary=True) assert "Summary" in sections[0].value @pytest.mark.parametrize( "docstring", [ """ Examples -------- Base case 1. We want to skip the following test. >>> 1 + 1 == 3 # doctest: +SKIP True """, r""" Examples -------- Base case 2. We have a blankline test. >>> print("a\n\nb") a <BLANKLINE> b """, ], ) def test_trim_doctest_flags_basic_example(parse_numpy: ParserType, docstring: str) -> None: """Correctly parse simple example docstrings when `trim_doctest_flags` option is turned on. Parameters: parse_numpy: Fixture parser. docstring: The docstring to parse_numpy (parametrized). """ sections, warnings = parse_numpy(docstring, trim_doctest_flags=True) assert len(sections) == 1 assert len(sections[0].value) == 2 assert not warnings # Verify that doctest flags have indeed been trimmed. example_str = sections[0].value[1][1] assert "# doctest: +SKIP" not in example_str assert "<BLANKLINE>" not in example_str def test_trim_doctest_flags_multi_example(parse_numpy: ParserType) -> None: """Correctly parse multiline example docstrings when `trim_doctest_flags` option is turned on. Parameters: parse_numpy: Fixture parser. """ docstring = r""" Examples -------- Test multiline example blocks. We want to skip the following test. >>> 1 + 1 == 3 # doctest: +SKIP True And then a few more examples here: >>> print("a\n\nb") a <BLANKLINE> b >>> 1 + 1 == 2 # doctest: +SKIP >>> print(list(range(1, 100))) # doctest: +ELLIPSIS [1, 2, ..., 98, 99] """ sections, warnings = parse_numpy(docstring, trim_doctest_flags=True) assert len(sections) == 1 assert len(sections[0].value) == 4 assert not warnings # Verify that doctest flags have indeed been trimmed. example_str = sections[0].value[1][1] assert "# doctest: +SKIP" not in example_str example_str = sections[0].value[3][1] assert "<BLANKLINE>" not in example_str assert "\n>>> print(list(range(1, 100)))\n" in example_str def test_parsing_choices(parse_numpy: ParserType) -> None: """Correctly parse choices. Parameters: parse_numpy: Fixture parser. """ docstring = r""" Parameters -------- order : {'C', 'F', 'A'} Description of `order`. """ sections, warnings = parse_numpy(docstring, trim_doctest_flags=True) assert sections[0].value[0].annotation == "'C', 'F', 'A'" assert not warnings �������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/conftest.py�����������������������������������������������0000664�0001750�0001750�00000001554�14767006246�022763� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Pytest fixture for docstrings tests.""" from __future__ import annotations from typing import TYPE_CHECKING import pytest from _griffe.docstrings import google, numpy, sphinx from tests.test_docstrings.helpers import ParserType, parser if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture def parse_google() -> Iterator[ParserType]: """Yield a function to parse Google docstrings. Yields: A parser function. """ yield from parser(google) @pytest.fixture def parse_numpy() -> Iterator[ParserType]: """Yield a function to parse Numpy docstrings. Yields: A parser function. """ yield from parser(numpy) @pytest.fixture def parse_sphinx() -> Iterator[ParserType]: """Yield a function to parse Sphinx docstrings. Yields: A parser function. """ yield from parser(sphinx) ����������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/test_google.py��������������������������������������������0000664�0001750�0001750�00000140523�14767006246�023451� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the [Google-style parser][griffe.docstrings.google].""" from __future__ import annotations import inspect from typing import TYPE_CHECKING import pytest from griffe import ( Attribute, Class, Docstring, DocstringReceive, DocstringReturn, DocstringSectionKind, DocstringYield, ExprName, Function, Module, Parameter, Parameters, parse_docstring_annotation, ) if TYPE_CHECKING: from tests.test_docstrings.helpers import ParserType # ============================================================================================= # Markup flow (multilines, indentation, etc.) def test_simple_docstring(parse_google: ParserType) -> None: """Parse a simple docstring. Parameters: parse_google: Fixture parser. """ sections, warnings = parse_google("A simple docstring.") assert len(sections) == 1 assert not warnings def test_multiline_docstring(parse_google: ParserType) -> None: """Parse a multi-line docstring. Parameters: parse_google: Fixture parser. """ sections, warnings = parse_google( """ A somewhat longer docstring. Blablablabla. """, ) assert len(sections) == 1 assert not warnings def test_parse_partially_indented_lines(parse_google: ParserType) -> None: """Properly handle partially indented lines. Parameters: parse_google: Fixture parser. """ docstring = """ The available formats are: - JSON The unavailable formats are: - YAML """ sections, warnings = parse_google(docstring) assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.admonition assert sections[1].kind is DocstringSectionKind.admonition assert not warnings def test_multiple_lines_in_sections_items(parse_google: ParserType) -> None: """Parse multi-line item description. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: p (int): This parameter has a description spawning on multiple lines. It even has blank lines in it. Some of these lines are indented for no reason. q (int): What if the first line is blank? """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert len(sections[0].value) == 2 assert warnings for warning in warnings: assert "should be 4 * 2 = 8 spaces, not" in warning def test_code_blocks(parse_google: ParserType) -> None: """Parse code blocks. Parameters: parse_google: Fixture parser. """ docstring = """ This docstring contains a code block! ```python print("hello") ``` """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert not warnings def test_indented_code_block(parse_google: ParserType) -> None: """Parse indented code blocks. Parameters: parse_google: Fixture parser. """ docstring = """ This docstring contains a docstring in a code block o_O! \"\"\" This docstring is contained in another docstring O_o! Parameters: s: A string. \"\"\" """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert not warnings def test_different_indentation(parse_google: ParserType) -> None: """Parse different indentations, warn on confusing indentation. Parameters: parse_google: Fixture parser. """ docstring = """ Raises: StartAt5: this section's items starts with 5 spaces of indentation. Well indented continuation line. Badly indented continuation line (will trigger a warning). Empty lines are preserved, as well as extra-indentation (this line is a code block). AnyOtherLine: ...starting with exactly 5 spaces is a new item. AnyLine: ...indented with less than 5 spaces signifies the end of the section. """ sections, warnings = parse_google(docstring) assert len(sections) == 2 assert len(sections[0].value) == 2 assert sections[0].value[0].description == ( "this section's items starts with 5 spaces of indentation.\n" "Well indented continuation line.\n" "Badly indented continuation line (will trigger a warning).\n" "\n" " Empty lines are preserved, as well as extra-indentation (this line is a code block)." ) assert sections[1].value == " AnyLine: ...indented with less than 5 spaces signifies the end of the section." assert len(warnings) == 1 assert "should be 5 * 2 = 10 spaces, not 6" in warnings[0] def test_empty_indented_lines_in_section_with_items(parse_google: ParserType) -> None: """In sections with items, don't treat lines with just indentation as items. Parameters: parse_google: Fixture parser. """ docstring = "Returns:\n only_item: Description.\n \n \n\nSomething." sections, _ = parse_google(docstring) assert len(sections) == 2 assert len(sections[0].value) == 1 @pytest.mark.parametrize( "section", [ "Attributes", "Other Parameters", "Parameters", "Raises", "Receives", "Returns", "Warns", "Yields", ], ) def test_starting_item_description_on_new_line(parse_google: ParserType, section: str) -> None: """In sections with items, allow starting item descriptions on a new (indented) line. Parameters: parse_google: Fixture parser. section: A parametrized section name. """ docstring = f"\n{section}:\n only_item:\n Description." sections, _ = parse_google(docstring) assert len(sections) == 1 assert len(sections[0].value) == 1 assert sections[0].value[0].description.strip() == "Description." # ============================================================================================= # Annotations def test_parse_without_parent(parse_google: ParserType) -> None: """Parse a docstring without a parent function. Parameters: parse_google: Fixture parser. """ sections, warnings = parse_google( """ Parameters: void: SEGFAULT. niet: SEGFAULT. nada: SEGFAULT. rien: SEGFAULT. Keyword Args: keywd: SEGFAULT. Exceptions: GlobalError: when nothing works as expected. Returns: Itself. """, ) assert len(sections) == 4 assert len(warnings) == 6 # Missing annotations for parameters and return. for warning in warnings[:-1]: assert "parameter" in warning assert "return" in warnings[-1] def test_parse_without_annotations(parse_google: ParserType) -> None: """Parse a function docstring without signature annotations. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x: X value. Keyword Args: y: Y value. Returns: Sum X + Y + Z. """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x"), Parameter("y"), ), ), ) assert len(sections) == 3 assert len(warnings) == 3 for warning in warnings[:-1]: assert "parameter" in warning assert "return" in warnings[-1] def test_parse_with_annotations(parse_google: ParserType) -> None: """Parse a function docstring with signature annotations. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x: X value. Keyword Parameters: y: Y value. Returns: Sum X + Y. """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x", annotation="int"), Parameter("y", annotation="int"), ), returns="int", ), ) assert len(sections) == 3 assert not warnings # ============================================================================================= # Sections def test_parse_attributes_section(parse_google: ParserType) -> None: """Parse Attributes sections. Parameters: parse_google: Fixture parser. """ docstring = """ Attributes: hey: Hey. ho: Ho. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert not warnings def test_parse_functions_section(parse_google: ParserType) -> None: """Parse Functions/Methods sections. Parameters: parse_google: Fixture parser. """ docstring = """ Functions: f(a, b=2): Hello. g: Hi. Methods: f(a, b=2): Hello. g: Hi. """ sections, warnings = parse_google(docstring) assert len(sections) == 2 for section in sections: assert section.kind is DocstringSectionKind.functions func_f = section.value[0] assert func_f.name == "f" assert func_f.signature == "f(a, b=2)" assert func_f.description == "Hello." func_g = section.value[1] assert func_g.name == "g" assert func_g.signature is None assert func_g.description == "Hi." assert not warnings def test_parse_classes_section(parse_google: ParserType) -> None: """Parse Classes sections. Parameters: parse_google: Fixture parser. """ docstring = """ Classes: C(a, b=2): Hello. D: Hi. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.classes class_c = sections[0].value[0] assert class_c.name == "C" assert class_c.signature == "C(a, b=2)" assert class_c.description == "Hello." class_d = sections[0].value[1] assert class_d.name == "D" assert class_d.signature is None assert class_d.description == "Hi." assert not warnings def test_parse_modules_section(parse_google: ParserType) -> None: """Parse Modules sections. Parameters: parse_google: Fixture parser. """ docstring = """ Modules: m: Hello. n: Hi. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.modules module_m = sections[0].value[0] assert module_m.name == "m" assert module_m.description == "Hello." module_n = sections[0].value[1] assert module_n.name == "n" assert module_n.description == "Hi." assert not warnings def test_parse_examples_sections(parse_google: ParserType) -> None: """Parse a function docstring with examples. Parameters: parse_google: Fixture parser. """ docstring = """ Examples: Some examples that will create a unified code block: >>> 2 + 2 == 5 False >>> print("examples") "examples" This is just a random comment in the examples section. These examples will generate two different code blocks. Note the blank line. >>> print("I'm in the first code block!") "I'm in the first code block!" >>> print("I'm in other code block!") "I'm in other code block!" We also can write multiline examples: >>> x = 3 + 2 # doctest: +SKIP >>> y = x + 10 >>> y 15 This is just a typical Python code block: ```python print("examples") return 2 + 2 ``` Even if it contains doctests, the following block is still considered a normal code-block. ```pycon >>> print("examples") "examples" >>> 2 + 2 4 ``` The blank line before an example is optional. >>> x = 3 >>> y = "apple" >>> z = False >>> l = [x, y, z] >>> my_print_list_function(l) 3 "apple" False """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x", annotation="int"), Parameter("y", annotation="int"), ), returns="int", ), trim_doctest_flags=False, ) assert len(sections) == 1 examples = sections[0] assert len(examples.value) == 9 assert examples.value[6][1].startswith(">>> x = 3 + 2 # doctest: +SKIP") assert not warnings def test_parse_yields_section(parse_google: ParserType) -> None: """Parse Yields section. Parameters: parse_google: Fixture parser. """ docstring = """ Yields: x: Floats. (int): Integers. y (int): Same. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 annotated = sections[0].value[0] assert annotated.name == "x" assert annotated.annotation is None assert annotated.description == "Floats." annotated = sections[0].value[1] assert annotated.name == "" assert annotated.annotation == "int" assert annotated.description == "Integers." annotated = sections[0].value[2] assert annotated.name == "y" assert annotated.annotation == "int" assert annotated.description == "Same." assert len(warnings) == 1 assert "'x'" in warnings[0] def test_invalid_sections(parse_google: ParserType) -> None: """Warn on invalid sections. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: Exceptions: Exceptions: Returns: Note: Important: """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert not warnings # ============================================================================================= # Parameters sections def test_parse_args_and_kwargs(parse_google: ParserType) -> None: """Parse args and kwargs. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: a (str): a parameter. *args (str): args parameters. **kwargs (str): kwargs parameters. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 expected_parameters = {"a": "a parameter.", "*args": "args parameters.", "**kwargs": "kwargs parameters."} for parameter in sections[0].value: assert parameter.name in expected_parameters assert expected_parameters[parameter.name] == parameter.description assert not warnings def test_parse_args_kwargs_keyword_only(parse_google: ParserType) -> None: """Parse args and kwargs. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: a (str): a parameter. *args (str): args parameters. Keyword Args: **kwargs (str): kwargs parameters. """ sections, warnings = parse_google(docstring) assert len(sections) == 2 expected_parameters = {"a": "a parameter.", "*args": "args parameters."} for parameter in sections[0].value: assert parameter.name in expected_parameters assert expected_parameters[parameter.name] == parameter.description expected_parameters = {"**kwargs": "kwargs parameters."} for kwarg in sections[1].value: assert kwarg.name in expected_parameters assert expected_parameters[kwarg.name] == kwarg.description assert not warnings def test_parse_types_in_docstring(parse_google: ParserType) -> None: """Parse types in docstring. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x (int): X value. Keyword Args: y (int): Y value. Returns: s (int): Sum X + Y + Z. """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x"), Parameter("y"), ), ), ) assert len(sections) == 3 assert not warnings assert sections[0].kind is DocstringSectionKind.parameters assert sections[1].kind is DocstringSectionKind.other_parameters assert sections[2].kind is DocstringSectionKind.returns (argx,) = sections[0].value (argy,) = sections[1].value (returns,) = sections[2].value assert argx.name == "x" assert argx.annotation.name == "int" assert argx.annotation.canonical_path == "int" assert argx.description == "X value." assert argx.value is None assert argy.name == "y" assert argy.annotation.name == "int" assert argy.annotation.canonical_path == "int" assert argy.description == "Y value." assert argy.value is None assert returns.annotation.name == "int" assert returns.annotation.canonical_path == "int" assert returns.description == "Sum X + Y + Z." def test_parse_optional_type_in_docstring(parse_google: ParserType) -> None: """Parse optional types in docstring. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x (int): X value. y (int, optional): Y value. Keyword Args: z (int, optional): Z value. """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x", default="1"), Parameter("y", default="None"), Parameter("z", default="None"), ), ), ) assert len(sections) == 2 assert not warnings assert sections[0].kind is DocstringSectionKind.parameters assert sections[1].kind is DocstringSectionKind.other_parameters argx, argy = sections[0].value (argz,) = sections[1].value assert argx.name == "x" assert argx.annotation.name == "int" assert argx.annotation.canonical_path == "int" assert argx.description == "X value." assert argx.value == "1" assert argy.name == "y" assert argy.annotation.name == "int" assert argy.annotation.canonical_path == "int" assert argy.description == "Y value." assert argy.value == "None" assert argz.name == "z" assert argz.annotation.name == "int" assert argz.annotation.canonical_path == "int" assert argz.description == "Z value." assert argz.value == "None" def test_prefer_docstring_types_over_annotations(parse_google: ParserType) -> None: """Prefer the docstring type over the annotation. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x (str): X value. Keyword Args: y (str): Y value. Returns: (str): Sum X + Y + Z. """ sections, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("x", annotation="int"), Parameter("y", annotation="int"), ), returns="int", ), ) assert len(sections) == 3 assert not warnings assert sections[0].kind is DocstringSectionKind.parameters assert sections[1].kind is DocstringSectionKind.other_parameters assert sections[2].kind is DocstringSectionKind.returns (argx,) = sections[0].value (argy,) = sections[1].value (returns,) = sections[2].value assert argx.name == "x" assert argx.annotation.name == "str" assert argx.annotation.canonical_path == "str" assert argx.description == "X value." assert argy.name == "y" assert argy.annotation.name == "str" assert argy.annotation.canonical_path == "str" assert argy.description == "Y value." assert returns.annotation.name == "str" assert returns.annotation.canonical_path == "str" assert returns.description == "Sum X + Y + Z." def test_parameter_line_without_colon(parse_google: ParserType) -> None: """Warn when missing colon. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x is an integer. """ sections, warnings = parse_google(docstring) assert len(sections) == 0 # Empty sections are discarded. assert len(warnings) == 1 assert "pair" in warnings[0] def test_parameter_line_without_colon_keyword_only(parse_google: ParserType) -> None: """Warn when missing colon. Parameters: parse_google: Fixture parser. """ docstring = """ Keyword Args: x is an integer. """ sections, warnings = parse_google(docstring) assert len(sections) == 0 # Empty sections are discarded. assert len(warnings) == 1 assert "pair" in warnings[0] def test_warn_about_unknown_parameters(parse_google: ParserType) -> None: """Warn about unknown parameters in "Parameters" sections. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x (int): Integer. y (int): Integer. """ _, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("a"), Parameter("y"), ), ), ) assert len(warnings) == 1 assert "'x' does not appear in the function signature" in warnings[0] def test_never_warn_about_unknown_other_parameters(parse_google: ParserType) -> None: """Never warn about unknown parameters in "Other parameters" sections. Parameters: parse_google: Fixture parser. """ docstring = """ Other Parameters: x (int): Integer. z (int): Integer. """ _, warnings = parse_google( docstring, parent=Function( "func", parameters=Parameters( Parameter("a"), Parameter("y"), ), ), ) assert not warnings def test_unknown_params_scan_doesnt_crash_without_parameters(parse_google: ParserType) -> None: """Assert we don't crash when parsing parameters sections and parent object does not have parameters. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: this (str): This. that (str): That. """ _, warnings = parse_google(docstring, parent=Module("mod")) assert not warnings def test_class_uses_init_parameters(parse_google: ParserType) -> None: """Assert we use the `__init__` parameters when parsing classes' parameters sections. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: x: X value. """ parent = Class("c") parent["__init__"] = Function("__init__", parameters=Parameters(Parameter("x", annotation="int"))) sections, warnings = parse_google(docstring, parent=parent) assert not warnings argx = sections[0].value[0] assert argx.name == "x" assert argx.annotation == "int" assert argx.description == "X value." # TODO: possible feature # def test_missing_parameter(parse_google: ParserType) -> None: # """Warn on missing parameter in docstring. # # Parameters: # parse_google: Fixture parser. # """ # docstring = """ # Parameters: # x: Integer. # """ # assert not warnings # ============================================================================================= # Attributes sections def test_retrieve_attributes_annotation_from_parent(parse_google: ParserType) -> None: """Retrieve the annotations of attributes from the parent object. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Attributes: a: Whatever. b: Whatever. """ parent = Class("cls") parent["a"] = Attribute("a", annotation=ExprName("int")) parent["b"] = Attribute("b", annotation=ExprName("str")) sections, _ = parse_google(docstring, parent=parent) attributes = sections[1].value assert attributes[0].name == "a" assert attributes[0].annotation.name == "int" assert attributes[1].name == "b" assert attributes[1].annotation.name == "str" # ============================================================================================= # Yields sections def test_parse_yields_section_with_return_annotation(parse_google: ParserType) -> None: """Parse Yields section with a return annotation in the parent function. Parameters: parse_google: Fixture parser. """ docstring = """ Yields: Integers. """ function = Function("func", returns="Iterator[int]") sections, warnings = parse_google(docstring, function) assert len(sections) == 1 annotated = sections[0].value[0] assert annotated.annotation == "Iterator[int]" assert annotated.description == "Integers." assert not warnings @pytest.mark.parametrize( "return_annotation", [ "Iterator[tuple[int, float]]", "Generator[tuple[int, float], ..., ...]", ], ) def test_parse_yields_tuple_in_iterator_or_generator(parse_google: ParserType, return_annotation: str) -> None: """Parse Yields annotations in Iterator or Generator types. Parameters: parse_google: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Yields: a: Whatever. b: Whatever. """ sections, _ = parse_google( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) yields = sections[1].value assert yields[0].name == "a" assert yields[0].annotation.name == "int" assert yields[1].name == "b" assert yields[1].annotation.name == "float" @pytest.mark.parametrize( "return_annotation", [ "Iterator[int]", "Generator[int, None, None]", ], ) def test_extract_yielded_type_with_single_return_item(parse_google: ParserType, return_annotation: str) -> None: """Extract main type annotation from Iterator or Generator. Parameters: parse_google: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Yields: A number. """ sections, _ = parse_google( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) yields = sections[1].value assert yields[0].annotation.name == "int" def test_yield_section_in_property(parse_google: ParserType) -> None: """No warnings when parsing Yields section in a property. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Yields: A number. """ sections, warnings = parse_google( docstring, parent=Attribute( "prop", annotation=parse_docstring_annotation("Iterator[int]", Docstring("d", parent=Attribute("a"))), ), ) assert not warnings yields = sections[1].value assert yields[0].annotation.name == "int" # ============================================================================================= # Receives sections def test_parse_receives_tuple_in_generator(parse_google: ParserType) -> None: """Parse Receives annotations in Generator type. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Receives: a: Whatever. b: Whatever. """ sections, _ = parse_google( docstring, parent=Function( "func", returns=parse_docstring_annotation( "Generator[..., tuple[int, float], ...]", Docstring("d", parent=Function("f")), ), ), ) receives = sections[1].value assert receives[0].name == "a" assert receives[0].annotation.name == "int" assert receives[1].name == "b" assert receives[1].annotation.name == "float" @pytest.mark.parametrize( "return_annotation", [ "Generator[int, float, None]", ], ) def test_extract_received_type_with_single_return_item(parse_google: ParserType, return_annotation: str) -> None: """Extract main type annotation from Iterator or Generator. Parameters: parse_google: Fixture parser. return_annotation: Parametrized return annotation as a string. """ docstring = """ Summary. Receives: A floating point number. """ sections, _ = parse_google( docstring, parent=Function( "func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f"))), ), ) receives = sections[1].value assert receives[0].annotation.name == "float" # ============================================================================================= # Returns sections def test_parse_returns_tuple_in_generator(parse_google: ParserType) -> None: """Parse Returns annotations in Generator type. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Returns: a: Whatever. b: Whatever. """ sections, _ = parse_google( docstring, parent=Function( "func", returns=parse_docstring_annotation( "Generator[..., ..., tuple[int, float]]", Docstring("d", parent=Function("f")), ), ), ) returns = sections[1].value assert returns[0].name == "a" assert returns[0].annotation.name == "int" assert returns[1].name == "b" assert returns[1].annotation.name == "float" # ============================================================================================= # Parser special features def test_parse_admonitions(parse_google: ParserType) -> None: """Parse admonitions. Parameters: parse_google: Fixture parser. """ docstring = """ Important note: Hello. Note: With title. Hello again. Something: Something. """ sections, warnings = parse_google(docstring) assert len(sections) == 3 assert not warnings assert sections[0].title == "Important note" assert sections[0].value.kind == "important-note" assert sections[0].value.contents == "Hello." assert sections[1].title == "With title." assert sections[1].value.kind == "note" assert sections[1].value.contents == "Hello again." assert sections[2].title == "Something" assert sections[2].value.kind == "something" assert sections[2].value.contents == "Something." @pytest.mark.parametrize( "docstring", [ """ ****************************** This looks like an admonition: ****************************** """, """ Warning: this line also looks like an admonition. """, """ Matching but not an admonition: - Multiple empty lines above. """, """Last line:""", ], ) def test_handle_false_admonitions_correctly(parse_google: ParserType, docstring: str) -> None: """Correctly handle lines that look like admonitions. Parameters: parse_google: Fixture parser. docstring: The docstring to parse (parametrized). """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.text assert len(sections[0].value.splitlines()) == len(inspect.cleandoc(docstring).splitlines()) assert not warnings def test_dont_insert_admonition_before_current_section(parse_google: ParserType) -> None: """Check that admonitions are inserted at the right place. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Short description. Info: Something useful. """ sections, _ = parse_google(docstring) assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text assert sections[1].kind is DocstringSectionKind.admonition @pytest.mark.parametrize( "docstring", [ "", "\n", "\n\n", "Summary.", "Summary.\n\n\n", "Summary.\n\nParagraph.", "Summary\non two lines.", "Summary\non two lines.\n\nParagraph.", ], ) def test_ignore_init_summary(parse_google: ParserType, docstring: str) -> None: """Correctly ignore summary in `__init__` methods' docstrings. Parameters: parse_google: Fixture parser. docstring: The docstring to parse_google (parametrized). """ sections, _ = parse_google(docstring, parent=Function("__init__", parent=Class("C")), ignore_init_summary=True) for section in sections: assert "Summary" not in section.value if docstring.strip(): sections, _ = parse_google(docstring, parent=Function("__init__", parent=Module("M")), ignore_init_summary=True) assert "Summary" in sections[0].value sections, _ = parse_google(docstring, parent=Function("f", parent=Class("C")), ignore_init_summary=True) assert "Summary" in sections[0].value sections, _ = parse_google(docstring, ignore_init_summary=True) assert "Summary" in sections[0].value @pytest.mark.parametrize( "docstring", [ """ Examples: Base case 1. We want to skip the following test. >>> 1 + 1 == 3 # doctest: +SKIP True """, r""" Examples: Base case 2. We have a blankline test. >>> print("a\n\nb") a <BLANKLINE> b """, ], ) def test_trim_doctest_flags_basic_example(parse_google: ParserType, docstring: str) -> None: """Correctly parse simple example docstrings when `trim_doctest_flags` option is turned on. Parameters: parse_google: Fixture parser. docstring: The docstring to parse (parametrized). """ sections, warnings = parse_google(docstring, trim_doctest_flags=True) assert len(sections) == 1 assert len(sections[0].value) == 2 assert not warnings # Verify that doctest flags have indeed been trimmed. example_str = sections[0].value[1][1] assert "# doctest: +SKIP" not in example_str assert "<BLANKLINE>" not in example_str def test_trim_doctest_flags_multi_example(parse_google: ParserType) -> None: """Correctly parse multiline example docstrings when `trim_doctest_flags` option is turned on. Parameters: parse_google: Fixture parser. """ docstring = r""" Examples: Test multiline example blocks. We want to skip the following test. >>> 1 + 1 == 3 # doctest: +SKIP True And then a few more examples here: >>> print("a\n\nb") a <BLANKLINE> b >>> 1 + 1 == 2 # doctest: +SKIP >>> print(list(range(1, 100))) # doctest: +ELLIPSIS [1, 2, ..., 98, 99] """ sections, warnings = parse_google(docstring, trim_doctest_flags=True) assert len(sections) == 1 assert len(sections[0].value) == 4 assert not warnings # Verify that doctest flags have indeed been trimmed. example_str = sections[0].value[1][1] assert "# doctest: +SKIP" not in example_str example_str = sections[0].value[3][1] assert "<BLANKLINE>" not in example_str assert "\n>>> print(list(range(1, 100)))\n" in example_str def test_single_line_with_trailing_whitespace(parse_google: ParserType) -> None: """Don't crash on single line docstrings with trailing whitespace. Parameters: parse_google: Fixture parser. """ docstring = "a: b\n " sections, warnings = parse_google(docstring, trim_doctest_flags=True) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.text assert not warnings @pytest.mark.parametrize( ("returns_multiple_items", "return_annotation", "expected"), [ ( False, None, [DocstringReturn("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)], ), ( False, "tuple[int, int]", [DocstringReturn("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")], ), ( True, None, [ DocstringReturn("", description="XXXXXXX\nYYYYYYY", annotation=None), DocstringReturn("", description="ZZZZZZZ", annotation=None), ], ), ( True, "tuple[int,int]", [ DocstringReturn("", description="XXXXXXX\nYYYYYYY", annotation="int"), DocstringReturn("", description="ZZZZZZZ", annotation="int"), ], ), ], ) def test_parse_returns_multiple_items( parse_google: ParserType, returns_multiple_items: bool, return_annotation: str, expected: list[DocstringReturn], ) -> None: """Parse Returns section with and without multiple items. Parameters: parse_google: Fixture parser. returns_multiple_items: Whether the `Returns` section has multiple items. return_annotation: The return annotation of the function to parse. expected: The expected value of the parsed Returns section. """ parent = ( Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f")))) if return_annotation is not None else None ) docstring = """ Returns: XXXXXXX YYYYYYY ZZZZZZZ """ sections, _ = parse_google( docstring, returns_multiple_items=returns_multiple_items, parent=parent, ) assert len(sections) == 1 assert len(sections[0].value) == len(expected) for annotated, expected_ in zip(sections[0].value, expected): assert annotated.name == expected_.name assert str(annotated.annotation) == str(expected_.annotation) assert annotated.description == expected_.description @pytest.mark.parametrize( ("returns_multiple_items", "return_annotation", "expected"), [ ( False, None, [DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)], ), ( False, "Iterator[tuple[int, int]]", [DocstringYield("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")], ), ( True, None, [ DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation=None), DocstringYield("", description="ZZZZZZZ", annotation=None), ], ), ( True, "Iterator[tuple[int,int]]", [ DocstringYield("", description="XXXXXXX\nYYYYYYY", annotation="int"), DocstringYield("", description="ZZZZZZZ", annotation="int"), ], ), ], ) def test_parse_yields_multiple_items( parse_google: ParserType, returns_multiple_items: bool, return_annotation: str, expected: list[DocstringYield], ) -> None: """Parse Returns section with and without multiple items. Parameters: parse_google: Fixture parser. returns_multiple_items: Whether the `Returns` and `Yields` sections have multiple items. return_annotation: The return annotation of the function to parse. Usually an `Iterator`. expected: The expected value of the parsed Yields section. """ parent = ( Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f")))) if return_annotation is not None else None ) docstring = """ Yields: XXXXXXX YYYYYYY ZZZZZZZ """ sections, _ = parse_google( docstring, returns_multiple_items=returns_multiple_items, parent=parent, ) assert len(sections) == 1 assert len(sections[0].value) == len(expected) for annotated, expected_ in zip(sections[0].value, expected): assert annotated.name == expected_.name assert str(annotated.annotation) == str(expected_.annotation) assert annotated.description == expected_.description @pytest.mark.parametrize( ("receives_multiple_items", "return_annotation", "expected"), [ ( False, None, [DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation=None)], ), ( False, "Generator[..., tuple[int, int], ...]", [DocstringReceive("", description="XXXXXXX\n YYYYYYY\nZZZZZZZ", annotation="tuple[int, int]")], ), ( True, None, [ DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation=None), DocstringReceive("", description="ZZZZZZZ", annotation=None), ], ), ( True, "Generator[..., tuple[int, int], ...]", [ DocstringReceive("", description="XXXXXXX\nYYYYYYY", annotation="int"), DocstringReceive("", description="ZZZZZZZ", annotation="int"), ], ), ], ) def test_parse_receives_multiple_items( parse_google: ParserType, receives_multiple_items: bool, return_annotation: str, expected: list[DocstringReceive], ) -> None: """Parse Returns section with and without multiple items. Parameters: parse_google: Fixture parser. receives_multiple_items: Whether the `Receives` section has multiple items. return_annotation: The return annotation of the function to parse. Usually a `Generator`. expected: The expected value of the parsed Receives section. """ parent = ( Function("func", returns=parse_docstring_annotation(return_annotation, Docstring("d", parent=Function("f")))) if return_annotation is not None else None ) docstring = """ Receives: XXXXXXX YYYYYYY ZZZZZZZ """ sections, _ = parse_google( docstring, receives_multiple_items=receives_multiple_items, parent=parent, ) assert len(sections) == 1 assert len(sections[0].value) == len(expected) for annotated, expected_ in zip(sections[0].value, expected): assert annotated.name == expected_.name assert str(annotated.annotation) == str(expected_.annotation) assert annotated.description == expected_.description def test_avoid_false_positive_sections(parse_google: ParserType) -> None: """Avoid false positive when parsing sections. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Modules: Not a modules section. No blank line before title: Not an admonition. Blank line after title: Not an admonition. Modules: Not a modules section. Modules: Not a modules section. No blank line before and blank line after: Not an admonition. Classes: - Text. """ sections, warnings = parse_google(docstring) assert len(sections) == 1 assert "Classes" in sections[0].value assert "Text" in sections[0].value assert len(warnings) == 6 assert warnings == [ "Possible section skipped, reasons: Missing blank line above section", "Possible admonition skipped, reasons: Missing blank line above admonition", "Possible admonition skipped, reasons: Extraneous blank line below admonition title", "Possible section skipped, reasons: Extraneous blank line below section title", "Possible section skipped, reasons: Missing blank line above section; Extraneous blank line below section title", "Possible admonition skipped, reasons: Missing blank line above admonition; Extraneous blank line below admonition title", ] def test_type_in_returns_without_parentheses(parse_google: ParserType) -> None: """Assert we can parse the return type without parentheses. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Returns: int: Description on several lines. """ sections, warnings = parse_google(docstring, returns_named_value=False) assert len(sections) == 2 assert not warnings retval = sections[1].value[0] assert retval.name == "" assert retval.annotation == "int" assert retval.description == "Description\non several lines." docstring = """ Summary. Returns: Description on several lines. """ sections, warnings = parse_google(docstring, returns_named_value=False) assert len(sections) == 2 assert len(warnings) == 1 retval = sections[1].value[0] assert retval.name == "" assert retval.annotation is None assert retval.description == "Description\non several lines." def test_type_in_yields_without_parentheses(parse_google: ParserType) -> None: """Assert we can parse the return type without parentheses. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Yields: int: Description on several lines. """ sections, warnings = parse_google(docstring, returns_named_value=False) assert len(sections) == 2 assert not warnings retval = sections[1].value[0] assert retval.name == "" assert retval.annotation == "int" assert retval.description == "Description\non several lines." docstring = """ Summary. Yields: Description on several lines. """ sections, warnings = parse_google(docstring, returns_named_value=False) assert len(sections) == 2 assert len(warnings) == 1 retval = sections[1].value[0] assert retval.name == "" assert retval.annotation is None assert retval.description == "Description\non several lines." def test_type_in_receives_without_parentheses(parse_google: ParserType) -> None: """Assert we can parse the return type without parentheses. Parameters: parse_google: Fixture parser. """ docstring = """ Summary. Receives: int: Description on several lines. """ sections, warnings = parse_google(docstring, receives_named_value=False) assert len(sections) == 2 assert not warnings retval = sections[1].value[0] assert retval.name == "" assert retval.annotation == "int" assert retval.description == "Description\non several lines." docstring = """ Summary. Receives: Description on several lines. """ sections, warnings = parse_google(docstring, receives_named_value=False) assert len(sections) == 2 assert len(warnings) == 1 retval = sections[1].value[0] assert retval.name == "" assert retval.annotation is None assert retval.description == "Description\non several lines." def test_reading_property_type_in_summary(parse_google: ParserType) -> None: """Assert we can parse the return type of properties in their summary. Parameters: parse_google: Fixture parser. """ docstring = "str: Description of the property." parent = Attribute("prop") parent.labels.add("property") sections, warnings = parse_google(docstring, returns_type_in_property_summary=True, parent=parent) assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text assert sections[1].kind is DocstringSectionKind.returns retval = sections[1].value[0] assert retval.name == "" assert retval.annotation.name == "str" assert retval.description == "" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_docstrings/test_sphinx.py��������������������������������������������0000664�0001750�0001750�00000074470�14767006246�023515� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the [Sphinx-style parser][griffe.docstrings.sphinx].""" from __future__ import annotations import inspect from typing import TYPE_CHECKING, Any import pytest from griffe import ( Attribute, Class, DocstringAttribute, DocstringParameter, DocstringRaise, DocstringReturn, DocstringSectionKind, Function, Module, Parameter, Parameters, ) if TYPE_CHECKING: from tests.test_docstrings.helpers import ParserType SOME_NAME = "foo" SOME_TEXT = "descriptive test text" SOME_EXTRA_TEXT = "more test text" SOME_EXCEPTION_NAME = "SomeException" SOME_OTHER_EXCEPTION_NAME = "SomeOtherException" @pytest.mark.parametrize( "docstring", [ "One line docstring description", """ Multiple line docstring description. With more text. """, ], ) def test_parse__description_only_docstring__single_markdown_section(parse_sphinx: ParserType, docstring: str) -> None: """Parse a single or multiline docstring. Parameters: parse_sphinx: Fixture parser. docstring: A parametrized docstring. """ sections, warnings = parse_sphinx(docstring) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.text assert sections[0].value == inspect.cleandoc(docstring) assert not warnings def test_parse__no_description__single_markdown_section(parse_sphinx: ParserType) -> None: """Parse an empty docstring. Parameters: parse_sphinx: Fixture parser. """ sections, warnings = parse_sphinx("") assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.text assert sections[0].value == "" assert not warnings def test_parse__multiple_blank_lines_before_description__single_markdown_section(parse_sphinx: ParserType) -> None: """Parse a docstring with initial blank lines. Parameters: parse_sphinx: Fixture parser. """ sections, warnings = parse_sphinx( """ Now text""", ) assert len(sections) == 1 assert sections[0].kind is DocstringSectionKind.text assert sections[0].value == "Now text" assert not warnings def test_parse__param_field__param_section(parse_sphinx: ParserType) -> None: """Parse a parameter section. Parameters: parse_sphinx: Fixture parser. """ sections, _ = parse_sphinx( f""" Docstring with one line param. :param {SOME_NAME}: {SOME_TEXT} """, ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__only_param_field__empty_markdown(parse_sphinx: ParserType) -> None: """Parse only a parameter section. Parameters: parse_sphinx: Fixture parser. """ sections, _ = parse_sphinx(":param foo: text") assert len(sections) == 2 assert sections[0].kind is DocstringSectionKind.text assert sections[0].value == "" @pytest.mark.parametrize( "param_directive_name", [ "param", "parameter", "arg", "arguments", "key", "keyword", ], ) def test_parse__all_param_names__param_section(parse_sphinx: ParserType, param_directive_name: str) -> None: """Parse all parameters directives. Parameters: parse_sphinx: Fixture parser. param_directive_name: A parametrized directive name. """ sections, _ = parse_sphinx( f""" Docstring with one line param. :{param_directive_name} {SOME_NAME}: {SOME_TEXT} """, ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() @pytest.mark.parametrize( "docstring", [ f""" Docstring with param with continuation, no indent. :param {SOME_NAME}: {SOME_TEXT} {SOME_EXTRA_TEXT} """, f""" Docstring with param with continuation, with indent. :param {SOME_NAME}: {SOME_TEXT} {SOME_EXTRA_TEXT} """, ], ) def test_parse__param_field_multi_line__param_section(parse_sphinx: ParserType, docstring: str) -> None: """Parse multiline directives. Parameters: parse_sphinx: Fixture parser. docstring: A parametrized docstring. """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, description=f"{SOME_TEXT} {SOME_EXTRA_TEXT}") assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_for_function__param_section_with_kind(parse_sphinx: ParserType) -> None: """Parse parameters. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_docs_type__param_section_with_type(parse_sphinx: ParserType) -> None: """Parse parameters with types. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param str foo: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_type_field__param_section_with_type(parse_sphinx: ParserType) -> None: """Parse parameters with separated types. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} :type foo: str """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_type_field_first__param_section_with_type(parse_sphinx: ParserType) -> None: """Parse parameters with separated types first. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :type foo: str :param foo: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() @pytest.mark.parametrize("union", ["str or None", "None or str", "str or int", "str or int or float"]) def test_parse__param_field_type_field_or_none__param_section_with_optional( parse_sphinx: ParserType, union: str, ) -> None: """Parse parameters with separated union types. Parameters: parse_sphinx: Fixture parser. union: A parametrized union type. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} :type foo: {union} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, annotation=union.replace(" or ", " | "), description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_annotate_type__param_section_with_type(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} """ sections, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() assert not warnings def test_parse__param_field_no_matching_param__result_from_docstring(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param other: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter("other", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__param_field_with_default__result_from_docstring(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} """ sections, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", kind=None, default=repr("")))), ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.parameters actual = sections[1].value[0] expected = DocstringParameter("foo", description=SOME_TEXT, value=repr("")) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() assert not warnings def test_parse__param_field_no_matching_param__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param other: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "No matching parameter for 'other'" in warnings[0] def test_parse__invalid_param_field_only_initial_marker__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair" in warnings[0] def test_parse__invalid_param_field_wrong_part_count__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to parse field directive" in warnings[0] def test_parse__param_twice__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param foo: {SOME_TEXT} :param foo: {SOME_TEXT} again """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), ) assert "Duplicate parameter entry for 'foo'" in warnings[0] def test_parse__param_type_twice_doc__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param str foo: {SOME_TEXT} :type foo: str """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), ) assert "Duplicate parameter information for 'foo'" in warnings[0] def test_parse__param_type_twice_type_directive_first__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :type foo: str :param str foo: {SOME_TEXT} """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", kind=None))), ) assert "Duplicate parameter information for 'foo'" in warnings[0] def test_parse__param_type_twice_annotated__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param str foo: {SOME_TEXT} :type foo: str """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), ) assert "Duplicate parameter information for 'foo'" in warnings[0] def test_warn_about_unknown_parameters(parse_sphinx: ParserType) -> None: """Warn about unknown parameters in "Parameters" sections. Parameters: parse_sphinx: Fixture parser. """ docstring = """ :param str a: {SOME_TEXT} """ _, warnings = parse_sphinx( docstring, parent=Function( "func", parameters=Parameters( Parameter("b"), ), ), ) assert len(warnings) == 1 assert "Parameter 'a' does not appear in the function signature" in warnings[0] def test_parse__param_type_no_type__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param str foo: {SOME_TEXT} :type str """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), ) assert "Failed to get ':directive: value' pair from" in warnings[0] def test_parse__param_type_no_name__error_message(parse_sphinx: ParserType) -> None: """Parse a simple docstring. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Docstring with line continuation. :param str foo: {SOME_TEXT} :type: str """ _, warnings = parse_sphinx( docstring, parent=Function("func", parameters=Parameters(Parameter("foo", annotation="str", kind=None))), ) assert "Failed to get parameter name from" in warnings[0] @pytest.mark.parametrize( "docstring", [ f""" Docstring with param with continuation, no indent. :var {SOME_NAME}: {SOME_TEXT} {SOME_EXTRA_TEXT} """, f""" Docstring with param with continuation, with indent. :var {SOME_NAME}: {SOME_TEXT} {SOME_EXTRA_TEXT} """, ], ) def test_parse__attribute_field_multi_line__param_section(parse_sphinx: ParserType, docstring: str) -> None: """Parse multiline attributes. Parameters: parse_sphinx: Fixture parser. docstring: A parametrized docstring. """ sections, warnings = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.attributes actual = sections[1].value[0] expected = DocstringAttribute(SOME_NAME, description=f"{SOME_TEXT} {SOME_EXTRA_TEXT}") assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() assert not warnings @pytest.mark.parametrize( "attribute_directive_name", [ "var", "ivar", "cvar", ], ) def test_parse__all_attribute_names__param_section(parse_sphinx: ParserType, attribute_directive_name: str) -> None: """Parse all attributes directives. Parameters: parse_sphinx: Fixture parser. attribute_directive_name: A parametrized directive name. """ sections, warnings = parse_sphinx( f""" Docstring with one line attribute. :{attribute_directive_name} {SOME_NAME}: {SOME_TEXT} """, ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.attributes actual = sections[1].value[0] expected = DocstringAttribute(SOME_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() assert not warnings def test_parse__class_attributes__attributes_section(parse_sphinx: ParserType) -> None: """Parse class attributes. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :var foo: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring, parent=Class("klass")) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.attributes actual = sections[1].value[0] expected = DocstringAttribute(SOME_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__class_attributes_with_type__annotation_in_attributes_section(parse_sphinx: ParserType) -> None: """Parse typed class attributes. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :vartype foo: str :var foo: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring, parent=Class("klass")) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.attributes actual = sections[1].value[0] expected = DocstringAttribute(SOME_NAME, annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__attribute_invalid_directive___error(parse_sphinx: ParserType) -> None: """Warn on invalid attribute directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :var {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair from" in warnings[0] def test_parse__attribute_no_name__error(parse_sphinx: ParserType) -> None: """Warn on invalid attribute directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :var: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to parse field directive from" in warnings[0] def test_parse__attribute_duplicate__error(parse_sphinx: ParserType) -> None: """Warn on duplicate attribute directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :var foo: {SOME_TEXT} :var foo: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Duplicate attribute entry for 'foo'" in warnings[0] def test_parse__class_attributes_type_invalid__error(parse_sphinx: ParserType) -> None: """Warn on invalid attribute type directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :vartype str :var foo: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair from " in warnings[0] def test_parse__class_attributes_type_no_name__error(parse_sphinx: ParserType) -> None: """Warn on invalid attribute directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Class docstring with attributes :vartype: str :var foo: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get attribute name from" in warnings[0] def test_parse__return_directive__return_section_no_type(parse_sphinx: ParserType) -> None: """Parse return directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :return: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.returns actual = sections[1].value[0] expected = DocstringReturn(name="", annotation=None, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__return_directive_rtype__return_section_with_type(parse_sphinx: ParserType) -> None: """Parse typed return directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return & rtype directive :return: {SOME_TEXT} :rtype: str """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.returns actual = sections[1].value[0] expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__return_directive_rtype_first__return_section_with_type(parse_sphinx: ParserType) -> None: """Parse typed-first return directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return & rtype directive :rtype: str :return: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.returns actual = sections[1].value[0] expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__return_directive_annotation__return_section_with_type(parse_sphinx: ParserType) -> None: """Parse return directives with return annotation. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with return directive, rtype directive, & annotation :return: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring, parent=Function("func", returns="str")) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.returns actual = sections[1].value[0] expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__return_directive_annotation__prefer_return_directive(parse_sphinx: ParserType) -> None: """Prefer docstring type over return annotation. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with return directive, rtype directive, & annotation :return: {SOME_TEXT} :rtype: str """ sections, _ = parse_sphinx(docstring, parent=Function("func", returns="int")) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.returns actual = sections[1].value[0] expected = DocstringReturn(name="", annotation="str", description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__return_invalid__error(parse_sphinx: ParserType) -> None: """Warn on invalid return directive. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :return {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair from " in warnings[0] def test_parse__rtype_invalid__error(parse_sphinx: ParserType) -> None: """Warn on invalid typed return directive. Parameters: parse_sphinx: Fixture parser. """ docstring = """ Function with only return directive :rtype str """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair from " in warnings[0] def test_parse__raises_directive__exception_section(parse_sphinx: ParserType) -> None: """Parse raise directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :raise SomeException: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.raises actual = sections[1].value[0] expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__multiple_raises_directive__exception_section_with_two(parse_sphinx: ParserType) -> None: """Parse multiple raise directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :raise SomeException: {SOME_TEXT} :raise SomeOtherException: {SOME_TEXT} """ sections, _ = parse_sphinx(docstring) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.raises actual = sections[1].value[0] expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() actual = sections[1].value[1] expected = DocstringRaise(annotation=SOME_OTHER_EXCEPTION_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() @pytest.mark.parametrize( "raise_directive_name", [ "raises", "raise", "except", "exception", ], ) def test_parse__all_exception_names__param_section(parse_sphinx: ParserType, raise_directive_name: str) -> None: """Parse all raise directives. Parameters: parse_sphinx: Fixture parser. raise_directive_name: A parametrized directive name. """ sections, _ = parse_sphinx( f""" Docstring with one line attribute. :{raise_directive_name} {SOME_EXCEPTION_NAME}: {SOME_TEXT} """, ) assert len(sections) == 2 assert sections[1].kind is DocstringSectionKind.raises actual = sections[1].value[0] expected = DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT) assert isinstance(actual, type(expected)) assert actual.as_dict() == expected.as_dict() def test_parse__raise_invalid__error(parse_sphinx: ParserType) -> None: """Warn on invalid raise directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :raise {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to get ':directive: value' pair from " in warnings[0] def test_parse__raise_no_name__error(parse_sphinx: ParserType) -> None: """Warn on invalid raise directives. Parameters: parse_sphinx: Fixture parser. """ docstring = f""" Function with only return directive :raise: {SOME_TEXT} """ _, warnings = parse_sphinx(docstring) assert "Failed to parse exception directive from" in warnings[0] def test_parse__module_attributes_section__expected_attributes_section(parse_sphinx: ParserType) -> None: """Parse attributes section in modules. Parameters: parse_sphinx: Fixture parser. """ docstring = """ Let's describe some attributes. :var A: Alpha. :vartype B: bytes :var B: Beta. :var C: Gamma. :var D: Delta. :var E: Epsilon. :vartype E: float """ module = Module("mod", filepath=None) module["A"] = Attribute("A", annotation="int", value="0") module["B"] = Attribute("B", annotation="str", value=repr("ŧ")) module["C"] = Attribute("C", annotation="bool", value="True") module["D"] = Attribute("D", annotation=None, value="3.0") module["E"] = Attribute("E", annotation=None, value="None") sections, warnings = parse_sphinx(docstring, parent=module) attr_section = sections[1] assert attr_section.kind is DocstringSectionKind.attributes assert len(attr_section.value) == 5 expected_data: list[dict[str, Any]] = [ {"name": "A", "annotation": "int", "description": "Alpha."}, {"name": "B", "annotation": "bytes", "description": "Beta."}, {"name": "C", "annotation": "bool", "description": "Gamma."}, {"name": "D", "annotation": None, "description": "Delta."}, {"name": "E", "annotation": "float", "description": "Epsilon."}, ] for index, expected_kwargs in enumerate(expected_data): actual = attr_section.value[index] expected = DocstringAttribute(**expected_kwargs) assert isinstance(actual, type(expected)) assert actual.name == expected.name assert actual.as_dict() == expected.as_dict() assert not warnings def test_parse__properties_return_type(parse_sphinx: ParserType) -> None: """Parse attributes section in modules. Parameters: parse_sphinx: Fixture parser. """ docstring = """ Property that returns True for explaining the issue. :return: True """ prop = Attribute("example", annotation="bool") sections, warnings = parse_sphinx(docstring, parent=prop) assert not warnings assert sections[1].value[0].annotation == "bool" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_visitor.py�����������������������������������������������������������0000664�0001750�0001750�00000031250�14767006246�020452� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test visit mechanisms.""" from __future__ import annotations from textwrap import dedent import pytest from griffe import GriffeLoader, temporary_pypackage, temporary_visited_module, temporary_visited_package def test_not_defined_at_runtime() -> None: """Assert that objects not defined at runtime are not added to wildcards expansions.""" with temporary_pypackage("package", ["module_a.py", "module_b.py", "module_c.py"]) as tmp_package: tmp_package.path.joinpath("__init__.py").write_text("from package.module_a import *") tmp_package.path.joinpath("module_a.py").write_text( dedent( """ import typing from typing import TYPE_CHECKING from package.module_b import CONST_B from package.module_c import CONST_C if typing.TYPE_CHECKING: # Always false. from package.module_b import TYPE_B if TYPE_CHECKING: # Always false. from package.module_c import TYPE_C """, ), ) tmp_package.path.joinpath("module_b.py").write_text("CONST_B = 'hi'\nTYPE_B = str") tmp_package.path.joinpath("module_c.py").write_text("CONST_C = 'ho'\nTYPE_C = str") loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() assert "CONST_B" in package.members assert "CONST_C" in package.members assert "TYPE_B" not in package.members assert "TYPE_C" not in package.members @pytest.mark.parametrize( ("decorator", "labels"), [ ("property", ("property",)), ("staticmethod", ("staticmethod",)), ("classmethod", ("classmethod",)), ("functools.cache", ("cached",)), ("cache", ("cached",)), ("functools.cached_property", ("cached", "property")), ("cached_property", ("cached", "property")), ("functools.lru_cache", ("cached",)), ("functools.lru_cache(maxsize=8)", ("cached",)), ("lru_cache", ("cached",)), ("lru_cache(maxsize=8)", ("cached",)), ("abc.abstractmethod", ("abstractmethod",)), ("abstractmethod", ("abstractmethod",)), ("dataclasses.dataclass", ("dataclass",)), ("dataclass", ("dataclass",)), ], ) def test_set_function_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None: """Assert decorators are used to set labels on functions. Parameters: decorator: A parametrized decorator. labels: The parametrized tuple of expected labels. """ code = f""" import abc import dataclasses import functools from abc import abstractmethod from dataclasses import dataclass from functools import cache, cached_property, lru_cache class A: @{decorator} def f(self): return 0 """ with temporary_visited_module(code) as module: assert module["A.f"].has_labels(*labels) @pytest.mark.parametrize( ("decorator", "labels"), [ ("dataclasses.dataclass", ("dataclass",)), ("dataclass", ("dataclass",)), ], ) def test_set_class_labels_using_decorators(decorator: str, labels: tuple[str, ...]) -> None: """Assert decorators are used to set labels on classes. Parameters: decorator: A parametrized decorator. labels: The parametrized tuple of expected labels. """ code = f""" import dataclasses from dataclasses import dataclass @{decorator} class A: ... """ with temporary_visited_module(code) as module: assert module["A"].has_labels(*labels) def test_handle_property_setter_and_deleter() -> None: """Assert property setters and deleters are supported.""" code = """ class A: def __init__(self): self._thing = 0 @property def thing(self): return self._thing @thing.setter def thing(self, value): self._thing = value @thing.deleter def thing(self): del self._thing """ with temporary_visited_module(code) as module: assert module["A.thing"].has_labels("property", "writable", "deletable") assert module["A.thing"].setter.is_function assert module["A.thing"].deleter.is_function @pytest.mark.parametrize( "decorator", [ "overload", "typing.overload", ], ) def test_handle_typing_overaload(decorator: str) -> None: """Assert `typing.overload` is supported. Parameters: decorator: A parametrized overload decorator. """ code = f""" import typing from typing import overload from pathlib import Path class A: @{decorator} def absolute(self, path: str) -> str: ... @{decorator} def absolute(self, path: Path) -> Path: ... def absolute(self, path: str | Path) -> str | Path: ... """ with temporary_visited_module(code) as module: overloads = module["A.absolute"].overloads assert len(overloads) == 2 assert overloads[0].parameters["path"].annotation.name == "str" assert overloads[1].parameters["path"].annotation.name == "Path" assert overloads[0].returns.name == "str" assert overloads[1].returns.name == "Path" @pytest.mark.parametrize( "statements", [ """__all__ = ["CONST_INIT"] + moda_all + modb_all + modc_all""", """__all__ = ["CONST_INIT", *moda_all, *modb_all, *modc_all]""", """ __all__ = ["CONST_INIT"] __all__ += moda_all + modb_all + modc_all """, """ __all__ = ["CONST_INIT"] + moda_all + modb_all __all__ += modc_all """, """ __all__ = ["CONST_INIT"] + moda_all + modb_all __all__ += [*modc_all] """, """ __all__ = ["CONST_INIT"] __all__ += moda_all __all__ += modb_all + modc_all """, ], ) def test_parse_complex__all__assignments(statements: str) -> None: """Check our ability to expand exports based on `__all__` [augmented] assignments. Parameters: statements: Parametrized text containing `__all__` [augmented] assignments. """ with temporary_pypackage("package", ["moda.py", "modb.py", "modc.py"]) as tmp_package: tmp_package.path.joinpath("moda.py").write_text("CONST_A = 1\n\n__all__ = ['CONST_A']") tmp_package.path.joinpath("modb.py").write_text("CONST_B = 1\n\n__all__ = ['CONST_B']") tmp_package.path.joinpath("modc.py").write_text("CONST_C = 2\n\n__all__ = ['CONST_C']") code = """ from package.moda import * from package.moda import __all__ as moda_all from package.modb import * from package.modb import __all__ as modb_all from package.modc import * from package.modc import __all__ as modc_all CONST_INIT = 0 """ tmp_package.path.joinpath("__init__.py").write_text(dedent(code) + dedent(statements)) loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) package = loader.load(tmp_package.name) loader.resolve_aliases() assert package.exports == ["CONST_INIT", "CONST_A", "CONST_B", "CONST_C"] def test_dont_crash_on_nested_functions_in_init() -> None: """Assert we don't crash when visiting a nested function in `__init__` methods.""" with temporary_visited_module( """ class C: def __init__(self): def pl(i: int): return i + 1 """, ) as module: assert module def test_get_correct_docstring_starting_line_number() -> None: """Assert we get the correct line numbers for docstring.""" with temporary_visited_module( """ ''' Module docstring. ''' class C: ''' Class docstring. ''' def method(self): ''' Method docstring. ''' """, ) as module: assert module.docstring.lineno == 2 # type: ignore[union-attr] assert module["C"].docstring.lineno == 6 assert module["C.method"].docstring.lineno == 10 def test_visit_properties_as_attributes() -> None: """Assert properties are created as attributes and not functions.""" with temporary_visited_module( """ from functools import cached_property class C: @property def prop(self) -> bool: return True @cached_property def cached_prop(self) -> int: return 0 """, ) as module: assert module["C.prop"].is_attribute assert "property" in module["C.prop"].labels assert module["C.cached_prop"].is_attribute assert "cached" in module["C.cached_prop"].labels def test_forward_docstrings() -> None: """Assert docstrings of class attributes are forwarded to instance assignments. This is a regression test for https://github.com/mkdocstrings/griffe/issues/128. """ with temporary_visited_module( ''' class C: attr: int """This is a non-empty docstring.""" def __init__(self, attr: int) -> None: self.attr = attr ''', ) as module: assert module["C.attr"].docstring def test_classvar_annotations() -> None: """Assert class variable and instance variable annotations are correctly parsed and merged.""" with temporary_visited_module( """ from typing import ClassVar class C: w: ClassVar[str] = "foo" x: ClassVar[int] y: str z: int = 5 def __init__(self) -> None: self.a: ClassVar[float] self.y = "" self.b: bytes """, ) as module: assert module["C.w"].annotation.canonical_path == "str" assert module["C.w"].labels == {"class-attribute"} assert module["C.w"].value == "'foo'" assert module["C.x"].annotation.canonical_path == "int" assert module["C.x"].labels == {"class-attribute"} assert module["C.y"].annotation.canonical_path == "str" assert module["C.y"].labels == {"instance-attribute"} assert module["C.y"].value == "''" assert module["C.z"].annotation.canonical_path == "int" assert module["C.z"].labels == {"class-attribute", "instance-attribute"} assert module["C.z"].value == "5" # This is syntactically valid, but semantically invalid assert module["C.a"].annotation.canonical_path == "typing.ClassVar" assert module["C.a"].annotation.slice.canonical_path == "float" assert module["C.a"].labels == {"instance-attribute"} assert module["C.b"].annotation.canonical_path == "bytes" assert module["C.b"].labels == {"instance-attribute"} def test_visiting_if_statement_in_class_for_type_guards() -> None: """Don't fail on various if statements when checking for type-guards.""" with temporary_visited_module( """ class A: if something("string1 string2"): class B: pass """, ) as module: assert module["A.B"].runtime def test_visiting_relative_imports_triggering_cyclic_aliases() -> None: """Skip specific imports to avoid cyclic aliases.""" with temporary_visited_package( "pkg", { "__init__.py": "from . import a", "a.py": "from . import b", "b.py": "", }, ) as pkg: assert "a" not in pkg.imports assert "b" in pkg["a"].imports assert pkg["a"].imports["b"] == "pkg.b" def test_parse_attributes_in__all__() -> None: """Parse attributes in `__all__`.""" with temporary_visited_package( "package", { "__init__.py": "from package import module\n__all__ = module.__all__", "module.py": "def hello(): ...\n__all__ = ['hello']", }, ) as package: assert "hello" in package.exports # type: ignore[operator] def test_parse_deep_attributes_in__all__() -> None: """Parse deep attributes in `__all__`.""" with temporary_visited_package( "package", { "__init__.py": "from package import subpackage\n__all__ = subpackage.module.__all__", "subpackage/__init__.py": "from package.subpackage import module", "subpackage/module.py": "def hello(): ...\n__all__ = ['hello']", }, ) as package: assert "hello" in package.exports # type: ignore[operator] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_expressions.py�������������������������������������������������������0000664�0001750�0001750�00000006501�14767006246�021336� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test names and expressions methods.""" from __future__ import annotations import ast import pytest from griffe import Module, Parser, get_expression, temporary_visited_module from tests.test_nodes import syntax_examples @pytest.mark.parametrize( ("annotation", "items"), [ ("tuple[int, float] | None", 2), ("None | tuple[int, float]", 2), ("Optional[tuple[int, float]]", 2), ("typing.Optional[tuple[int, float]]", 2), ], ) def test_explode_return_annotations(annotation: str, items: int) -> None: """Check that we correctly split items from return annotations. Parameters: annotation: The return annotation. items: The number of items to write in the docstring returns section. """ newline = "\n " returns = newline.join(f"x{_}: Some value." for _ in range(items)) code = f""" import typing from typing import Optional def function() -> {annotation}: '''This function returns either two ints or None Returns: {returns} ''' """ with temporary_visited_module(code) as module: sections = module["function"].docstring.parse(Parser.google) assert sections[1].value @pytest.mark.parametrize( "annotation", [ "int", "tuple[int]", "dict[str, str]", "Optional[tuple[int, float]]", ], ) def test_full_expressions(annotation: str) -> None: """Assert we can transform expressions to their full form without errors.""" code = f"x: {annotation}" with temporary_visited_module(code) as module: assert str(module["x"].annotation) == annotation def test_resolving_full_names() -> None: """Assert expressions are correctly transformed to their fully-resolved form.""" with temporary_visited_module( """ from package import module attribute1: module.Class from package import module as mod attribute2: mod.Class """, ) as module: assert module["attribute1"].annotation.canonical_path == "package.module.Class" assert module["attribute2"].annotation.canonical_path == "package.module.Class" @pytest.mark.parametrize("code", syntax_examples) def test_expressions(code: str) -> None: """Test building annotations from AST nodes. Parameters: code: An expression (parametrized). """ top_node = compile(code, filename="<>", mode="exec", flags=ast.PyCF_ONLY_AST, optimize=2) expression = get_expression(top_node.body[0].value, parent=Module("module")) # type: ignore[attr-defined] assert str(expression) == code def test_length_one_tuple_as_string() -> None: """Length-1 tuples must have a trailing comma.""" code = "x = ('a',)" with temporary_visited_module(code) as module: assert str(module["x"].value) == "('a',)" def test_resolving_init_parameter() -> None: """Instance attribute values should resolve to matching parameters. They must not resolve to the member of the same name in the same class, or to objects with the same name in higher scopes. """ with temporary_visited_module( """ x = 1 class Class: def __init__(self, x: int): self.x: int = x """, ) as module: assert module["Class.x"].value.canonical_path == "module.Class(x)" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_finder.py������������������������������������������������������������0000664�0001750�0001750�00000026626�14767006246�020235� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `finder` module.""" from __future__ import annotations import os from pathlib import Path from textwrap import dedent import pytest from _griffe.finder import _handle_editable_module, _handle_pth_file from griffe import Module, ModuleFinder, NamespacePackage, Package, temporary_pypackage @pytest.mark.parametrize( ("pypackage", "module", "add_to_search_path", "expected_top_name", "expected_top_path"), [ (("a", ["b.py"]), "a/b.py", True, "a", "a/__init__.py"), (("a", ["b.py"]), "a/b.py", False, "a", "a/__init__.py"), (("a/b", ["c.py"]), "a/b/c.py", True, "a", "a"), (("a/b", ["c.py"]), "a/b/c.py", False, "b", "a/b/__init__.py"), ], ) def test_find_module_with_path( pypackage: tuple[str, list[str]], module: str, add_to_search_path: bool, expected_top_name: str, expected_top_path: str, ) -> None: """Check that the finder can find modules using strings and Paths. Parameters: pypackage: A temporary package (metadata) on the file system (parametrized). module: The module path to load (parametrized). add_to_search_path: Whether to add the temporary package parent path to the finder search paths (parametrized). expected_top_name: Expected top module name (parametrized). expected_top_path: Expected top module path (parametrized). """ with temporary_pypackage(*pypackage) as tmp_package: finder = ModuleFinder(search_paths=[tmp_package.tmpdir] if add_to_search_path else None) _, package = finder.find_spec(tmp_package.tmpdir / module) assert package.name == expected_top_name if isinstance(package, NamespacePackage): assert package.path == [tmp_package.tmpdir / expected_top_path] else: assert package.path == tmp_package.tmpdir / expected_top_path @pytest.mark.parametrize( "statement", [ "__import__('pkg_resources').declare_namespace(__name__)", "__path__ = __import__('pkgutil').extend_path(__path__, __name__)", ], ) def test_find_pkg_style_namespace_packages(statement: str) -> None: """Check that the finder can find pkg-style namespace packages. Parameters: statement: The statement in the `__init__` module allowing to mark the package as namespace. """ with ( temporary_pypackage("namespace/package1") as tmp_package1, temporary_pypackage("namespace/package2") as tmp_package2, ): tmp_package1.path.parent.joinpath("__init__.py").write_text(statement) tmp_package2.path.parent.joinpath("__init__.py").write_text(statement) finder = ModuleFinder(search_paths=[tmp_package1.tmpdir, tmp_package2.tmpdir]) _, package = finder.find_spec("namespace") assert package.name == "namespace" assert isinstance(package, NamespacePackage) assert package.path == [tmp_package1.path.parent, tmp_package2.path.parent] def test_pth_file_handling(tmp_path: Path) -> None: """Assert .pth files are correctly handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "hello.pth" pth_file.write_text( dedent( """ # comment import thing import\tthing /doesnotexist tests """, ), ) paths = [sp.path for sp in _handle_pth_file(pth_file)] assert paths == [Path("tests")] def test_pth_file_handling_with_semi_colon(tmp_path: Path) -> None: """Assert .pth files are correctly handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "hello.pth" pth_file.write_text( dedent( """ # comment import thing; import\tthing; /doesnotexist; tests """, ), ) paths = [sp.path for sp in _handle_pth_file(pth_file)] assert paths == [Path("tests")] @pytest.mark.parametrize("editable_file_name", ["__editables_whatever.py", "_editable_impl_whatever.py"]) def test_editables_file_handling(tmp_path: Path, editable_file_name: str) -> None: """Assert editable modules by `editables` are handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / editable_file_name pth_file.write_text("hello\nF.map_module('griffe', 'src/griffe/__init__.py')") paths = [sp.path for sp in _handle_editable_module(pth_file)] assert paths == [Path("src")] @pytest.mark.parametrize("annotation", ["", ": dict[str, str]"]) def test_setuptools_file_handling(tmp_path: Path, annotation: str) -> None: """Assert editable modules by `setuptools` are handled. Parameters: tmp_path: Pytest fixture. annotation: The type annotation of the MAPPING variable. """ pth_file = tmp_path / "__editable__whatever.py" pth_file.write_text(f"hello\nMAPPING{annotation} = {{'griffe': 'src/griffe'}}") paths = [sp.path for sp in _handle_editable_module(pth_file)] assert paths == [Path("src")] @pytest.mark.parametrize("annotation", ["", ": dict[str, str]"]) def test_setuptools_file_handling_multiple_paths(tmp_path: Path, annotation: str) -> None: """Assert editable modules by `setuptools` are handled when multiple packages are installed in the same editable. Parameters: tmp_path: Pytest fixture. annotation: The type annotation of the MAPPING variable. """ pth_file = tmp_path / "__editable__whatever.py" pth_file.write_text( "hello=1\n" f"MAPPING{annotation} = {{\n'griffe':\n 'src1/griffe', 'briffe':'src2/briffe'}}\n" "def printer():\n print(hello)", ) paths = [sp.path for sp in _handle_editable_module(pth_file)] assert paths == [Path("src1"), Path("src2")] def test_scikit_build_core_file_handling(tmp_path: Path) -> None: """Assert editable modules by `scikit-build-core` are handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "_whatever_editable.py" pth_file.write_text( "hello=1\ninstall({'whatever': '/path/to/whatever'}, {'whatever.else': '/else'}, None, False, True)", ) # The second dict is not handled: scikit-build-core puts these files # in a location that Griffe won't be able to discover anyway # (they don't respect standard package or namespace package layouts, # and rely on dynamic meta path finder stuff). paths = [sp.path for sp in _handle_editable_module(pth_file)] assert paths == [Path("/path/to/whatever")] def test_meson_python_file_handling(tmp_path: Path) -> None: """Assert editable modules by `meson-python` are handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "_whatever_editable_loader.py" pth_file.write_text( # The path in argument 2 suffixed with src must exist, so we pass `.`. "hello=1\ninstall({'griffe', 'hello'}, '.', ['/tmp/ninja'], False)", ) search_paths = _handle_editable_module(pth_file) assert all(sp.always_scan_for in {"griffe", "_griffe"} for sp in search_paths) paths = [sp.path for sp in search_paths] assert paths == [Path("src")] @pytest.mark.parametrize( ("first", "second", "find_stubs", "expect"), [ ("package", "stubs", True, "both"), ("stubs", "package", True, "both"), ("package", None, True, "package"), (None, "package", True, "package"), ("stubs", None, True, "stubs"), (None, "stubs", True, "stubs"), (None, None, True, "none"), ("package", "stubs", False, "package"), ("stubs", "package", False, "package"), ("package", None, False, "package"), (None, "package", False, "package"), ("stubs", None, False, "none"), (None, "stubs", False, "none"), (None, None, False, "none"), ], ) def test_finding_stubs_packages( tmp_path: Path, first: str | None, second: str | None, find_stubs: bool, expect: str, ) -> None: """Find stubs-only packages. Parameters: tmp_path: Pytest fixture. """ search_path1 = tmp_path / "sp1" search_path2 = tmp_path / "sp2" search_path1.mkdir() search_path2.mkdir() if first == "package": package = search_path1 / "package" package.mkdir() package.joinpath("__init__.py").touch() elif first == "stubs": stubs = search_path1 / "package-stubs" stubs.mkdir() stubs.joinpath("__init__.pyi").touch() if second == "package": package = search_path2 / "package" package.mkdir() package.joinpath("__init__.py").touch() elif second == "stubs": stubs = search_path2 / "package-stubs" stubs.mkdir() stubs.joinpath("__init__.pyi").touch() finder = ModuleFinder([search_path1, search_path2]) if expect == "none": with pytest.raises(ModuleNotFoundError): finder.find_spec("package", try_relative_path=False, find_stubs_package=find_stubs) return name, result = finder.find_spec("package", try_relative_path=False, find_stubs_package=find_stubs) assert name == "package" if expect == "both": assert isinstance(result, Package) assert result.path.suffix == ".py" assert not result.path.parent.name.endswith("-stubs") assert result.stubs assert result.stubs.suffix == ".pyi" assert result.stubs.parent.name.endswith("-stubs") elif expect == "package": assert isinstance(result, Package) assert result.path.suffix == ".py" assert not result.path.parent.name.endswith("-stubs") assert result.stubs is None elif expect == "stubs": assert isinstance(result, Package) assert result.path.suffix == ".pyi" assert result.path.parent.name.endswith("-stubs") assert result.stubs is None @pytest.mark.parametrize("namespace_package", [False, True]) def test_scanning_package_and_module_with_same_names(namespace_package: bool) -> None: """The finder correctly scans package and module having same the name. Parameters: namespace_package: Whether the temporary package is a namespace one. """ init = not namespace_package with temporary_pypackage("pkg", ["pkg/mod.py", "mod/mod.py"], init=init, inits=init) as tmp_package: # Here we must make sure that all paths are relative # to correctly assert the finder's behavior, # so we pass `.` and actually enter the temporary directory. path = Path(tmp_package.name) filepath: Path | list[Path] = [path] if namespace_package else path old = Path.cwd() os.chdir(tmp_package.path.parent) try: finder = ModuleFinder(search_paths=[]) found = [path for _, path in finder.submodules(Module("pkg", filepath=filepath))] finally: os.chdir(old) check = ( path / "pkg/mod.py", path / "mod/mod.py", ) for mod in check: assert mod in found def test_not_finding_namespace_package_twice() -> None: """Deduplicate paths when finding namespace packages.""" with temporary_pypackage("pkg", ["pkg/mod.py", "mod/mod.py"], init=False, inits=False) as tmp_package: old = Path.cwd() os.chdir(tmp_package.tmpdir) try: finder = ModuleFinder(search_paths=[Path(), tmp_package.tmpdir]) found = finder.find_package("pkg") finally: os.chdir(old) assert isinstance(found, NamespacePackage) assert len(found.path) == 1 ����������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_diff.py��������������������������������������������������������������0000664�0001750�0001750�00000015174�14767006246�017672� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `diff` module.""" from __future__ import annotations import pytest from griffe import Breakage, BreakageKind, find_breaking_changes, temporary_visited_module, temporary_visited_package @pytest.mark.parametrize( ("old_code", "new_code", "expected_breakages"), [ ( "a = True", "a = False", [BreakageKind.ATTRIBUTE_CHANGED_VALUE], ), ( "class a(int, str): ...", "class a(int): ...", [BreakageKind.CLASS_REMOVED_BASE], ), ( "a = 0", "class a: ...", [BreakageKind.OBJECT_CHANGED_KIND], ), ( "a = True", "", [BreakageKind.OBJECT_REMOVED], ), ( "def a(): ...", "def a(x): ...", [BreakageKind.PARAMETER_ADDED_REQUIRED], ), ( "def a(x=0): ...", "def a(x=1): ...", [BreakageKind.PARAMETER_CHANGED_DEFAULT], ), ( # Positional-only to keyword-only. "def a(x, /): ...", "def a(*, x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Keyword-only to positional-only. "def a(*, x): ...", "def a(x, /): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Positional or keyword to positional-only. "def a(x): ...", "def a(x, /): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Positional or keyword to keyword-only. "def a(x): ...", "def a(*, x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), # To variadic positional. ( # Positional-only to variadic positional. "def a(x, /): ...", "def a(*x): ...", [], ), ( # Positional or keyword to variadic positional. "def a(x): ...", "def a(*x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Keyword-only to variadic positional. "def a(*, x): ...", "def a(*x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Variadic keyword to variadic positional. "def a(**x): ...", "def a(*x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Positional or keyword to variadic positional, with variadic keyword. "def a(x): ...", "def a(*x, **y): ...", [], ), ( # Keyword-only to variadic positional, with variadic keyword. "def a(*, x): ...", "def a(*x, **y): ...", [], ), # To variadic keyword. ( # Positional-only to variadic keyword. "def a(x, /): ...", "def a(**x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Positional or keyword to variadic keyword. "def a(x): ...", "def a(**x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Keyword-only to variadic keyword. "def a(*, x): ...", "def a(**x): ...", [], ), ( # Variadic positional to variadic keyword. "def a(*x): ...", "def a(**x): ...", [BreakageKind.PARAMETER_CHANGED_KIND], ), ( # Positional-only to variadic keyword, with variadic positional. "def a(x, /): ...", "def a(*y, **x): ...", [], ), ( # Positional or keyword to variadic keyword, with variadic positional. "def a(x): ...", "def a(*y, **x): ...", [], ), ( "def a(x=1): ...", "def a(x): ...", [BreakageKind.PARAMETER_CHANGED_REQUIRED], ), ( "def a(x, y): ...", "def a(y, x): ...", [BreakageKind.PARAMETER_MOVED, BreakageKind.PARAMETER_MOVED], ), ( "def a(x, y): ...", "def a(x): ...", [BreakageKind.PARAMETER_REMOVED], ), ( "class a:\n\tb: int | None = None", "class a:\n\tb: int", [BreakageKind.ATTRIBUTE_CHANGED_VALUE], ), ( "def a() -> int: ...", "def a() -> str: ...", [], # Not supported yet: `BreakageKind.RETURN_CHANGED_TYPE`. ), ], ) def test_diff_griffe(old_code: str, new_code: str, expected_breakages: list[Breakage]) -> None: """Test the different incompatibility finders. Parameters: old_code: Parametrized code of the old module version. new_code: Parametrized code of the new module version. expected_breakages: A list of breakage kinds to expect. """ # Check without any alias. with temporary_visited_module(old_code) as old_package, temporary_visited_module(new_code) as new_package: breaking = list(find_breaking_changes(old_package, new_package)) assert len(breaking) == len(expected_breakages) for breakage, expected_kind in zip(breaking, expected_breakages): assert breakage.kind is expected_kind # Check with aliases. import_a = "from ._mod_a import a\n__all__ = ['a']" old_modules = {"__init__.py": import_a, "_mod_a.py": old_code} new_modules = {"__init__.py": new_code and import_a, "_mod_a.py": new_code} with temporary_visited_package("package_old", old_modules) as old_package: # noqa: SIM117 with temporary_visited_package("package_new", new_modules) as new_package: breaking = list(find_breaking_changes(old_package, new_package)) assert len(breaking) == len(expected_breakages) for breakage, expected_kind in zip(breaking, expected_breakages): assert breakage.kind is expected_kind def test_moving_members_in_parent_classes() -> None: """Test that moving an object from a base class to a parent class doesn't trigger a breakage.""" old_code = """ class Parent: ... class Base(Parent): def method(self): ... """ new_code = """ class Parent: def method(self): ... class Base(Parent): ... """ with temporary_visited_module(old_code) as old_package, temporary_visited_module(new_code) as new_package: assert not list(find_breaking_changes(old_package, new_package)) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_inspector.py���������������������������������������������������������0000664�0001750�0001750�00000013507�14767006246�020766� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Test inspection mechanisms.""" from __future__ import annotations import pytest from griffe import inspect, temporary_inspected_module, temporary_inspected_package, temporary_pypackage from tests.helpers import clear_sys_modules def test_annotations_from_builtin_types() -> None: """Assert builtin types are correctly transformed to annotations.""" with temporary_inspected_module("def func(a: int) -> str: pass") as module: func = module["func"] assert func.parameters[0].name == "a" assert func.parameters[0].annotation.name == "int" assert func.returns.name == "str" def test_annotations_from_classes() -> None: """Assert custom classes are correctly transformed to annotations.""" with temporary_inspected_module("class A: pass\ndef func(a: A) -> A: pass") as module: func = module["func"] assert func.parameters[0].name == "a" param = func.parameters[0].annotation assert param.name == "A" assert param.canonical_path == f"{module.name}.A" returns = func.returns assert returns.name == "A" assert returns.canonical_path == f"{module.name}.A" def test_class_level_imports() -> None: """Assert annotations using class-level imports are resolved.""" with temporary_inspected_module( """ class A: from io import StringIO def method(self, p: StringIO): pass """, ) as module: method = module["A.method"] name = method.parameters["p"].annotation assert name.name == "StringIO" assert name.canonical_path == "io.StringIO" def test_missing_dependency() -> None: """Assert missing dependencies are handled during dynamic imports.""" with ( pytest.raises(ImportError, match="ModuleNotFoundError: No module named 'missing'"), temporary_inspected_module("import missing"), ): pass def test_inspect_properties_as_attributes() -> None: """Assert properties are created as attributes and not functions.""" with temporary_inspected_module( """ try: from functools import cached_property except ImportError: from cached_property import cached_property class C: @property def prop(self) -> bool: return True @cached_property def cached_prop(self) -> int: return 0 """, ) as module: assert module["C.prop"].is_attribute assert "property" in module["C.prop"].labels assert module["C.cached_prop"].is_attribute assert "cached" in module["C.cached_prop"].labels def test_inspecting_module_importing_other_module() -> None: """Assert aliases to modules are correctly inspected and aliased.""" with temporary_inspected_module("import itertools as it") as module: assert module["it"].is_alias assert module["it"].target_path == "itertools" def test_inspecting_parameters_with_functions_as_default_values() -> None: """Assert functions as default parameter values are serialized with their name.""" with temporary_inspected_module("def func(): ...\ndef other_func(f=func): ...") as module: default = module["other_func"].parameters["f"].default assert default == "func" def test_inspecting_package_and_module_with_same_names() -> None: """Package and module having same name shouldn't cause issues.""" with temporary_inspected_package("package", {"package.py": "a = 0"}): pass def test_inspecting_module_with_submodules() -> None: """Inspecting a module shouldn't register any of its submodules if they're not imported.""" with temporary_pypackage("pkg", ["mod.py"]) as tmp_package: pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py") assert "mod" not in pkg.members clear_sys_modules("pkg") def test_inspecting_module_with_imported_submodules() -> None: """When inspecting a package on the disk, direct submodules should be skipped entirely.""" with temporary_pypackage( "pkg", { "__init__.py": "from pkg import subpkg\nfrom pkg.subpkg import mod", "subpkg/__init__.py": "a = 0", "subpkg/mod.py": "b = 0", }, ) as tmp_package: pkg = inspect("pkg", filepath=tmp_package.path / "__init__.py") assert "subpkg" not in pkg.members assert "mod" in pkg.members assert pkg["mod"].is_alias assert pkg["mod"].target_path == "pkg.subpkg.mod" clear_sys_modules("pkg") def test_inspecting_objects_from_private_builtin_stdlib_moduless() -> None: """Inspect objects from private built-in modules in the standard library.""" ast = inspect("ast") assert "Assign" in ast.members assert not ast["Assign"].is_alias ast = inspect("_ast") assert "Assign" in ast.members assert not ast["Assign"].is_alias def test_inspecting_partials_as_functions() -> None: """Assert partials are correctly inspected as functions.""" with temporary_inspected_module( """ from functools import partial def func(a: int, b: int) -> int: pass partial_func = partial(func, 1) partial_func.__module__ = __name__ """, ) as module: partial_func = module["partial_func"] assert partial_func.is_function assert partial_func.parameters[0].name == "b" assert partial_func.parameters[0].annotation.name == "int" assert partial_func.returns.name == "int" def test_inspecting_class_instance() -> None: """Assert class instances are correctly inspected.""" with temporary_inspected_package( "pkg", { "__init__.py": "", "foo.py": "from . import bar\nx = bar.X()", "bar.py": "class X: pass", }, ) as tmp_package: assert not tmp_package["foo.x"].is_alias �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_public_api.py��������������������������������������������������������0000664�0001750�0001750�00000001441�14767006246�021061� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for public API handling.""" from griffe import temporary_visited_module def test_not_detecting_imported_objects_as_public() -> None: """Imported objects not listed in `__all__` must not be considered public.""" with temporary_visited_module("from abc import ABC\ndef func(): ...") as module: assert not module["ABC"].is_public assert module["func"].is_public # Control case. def test_detecting_dunder_attributes_as_public() -> None: """Dunder attributes (methods, etc.) must be considered public.""" with temporary_visited_module( """ def __getattr__(name): ... class A: def __init__(self): ... """, ) as module: assert module["__getattr__"].is_public assert module["A.__init__"].is_public �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017212� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/�����������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�020316� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.2.0/����������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021141� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.2.0/my_module/������������������������������������������0000775�0001750�0001750�00000000000�14767006246�023133� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.2.0/my_module/__init__.py�������������������������������0000664�0001750�0001750�00000000026�14767006246�025242� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������__version__ = "0.2.0" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.1.0/����������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�021140� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.1.0/my_module/������������������������������������������0000775�0001750�0001750�00000000000�14767006246�023132� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/fixtures/_repo/v0.1.0/my_module/__init__.py�������������������������������0000664�0001750�0001750�00000000026�14767006246�025241� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������__version__ = "0.1.0" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_stdlib.py������������������������������������������������������������0000664�0001750�0001750�00000003127�14767006246�020236� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Fuzzing on the standard library.""" from __future__ import annotations import sys from contextlib import suppress from typing import TYPE_CHECKING import pytest from griffe import GriffeLoader, LoadingError if TYPE_CHECKING: from collections.abc import Iterator from griffe import Alias, Object def _access_inherited_members(obj: Object | Alias) -> None: try: is_class = obj.is_class except Exception: # noqa: BLE001 return if is_class: assert obj.inherited_members is not None else: for cls in obj.classes.values(): _access_inherited_members(cls) @pytest.fixture(name="stdlib_loader", scope="session") def fixture_stdlib_loader() -> Iterator[GriffeLoader]: """Yield a GriffeLoader instance. During teardown, resolve aliases and access inherited members to make sure that no exception is raised when computing MRO. """ loader = GriffeLoader(allow_inspection=False, store_source=False) yield loader loader.resolve_aliases(implicit=True, external=None) for module in loader.modules_collection.members.values(): _access_inherited_members(module) loader.stats() @pytest.mark.skipif(sys.version_info < (3, 10), reason="Python less than 3.10 does not have sys.stdlib_module_names") @pytest.mark.parametrize("mod", sorted([m for m in getattr(sys, "stdlib_module_names", ()) if not m.startswith("_")])) def test_fuzzing_on_stdlib(stdlib_loader: GriffeLoader, mod: str) -> None: """Run Griffe on the standard library.""" with suppress(ImportError, LoadingError): stdlib_loader.load(mod) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_mixins.py������������������������������������������������������������0000664�0001750�0001750�00000000650�14767006246�020262� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `mixins` module.""" from __future__ import annotations from griffe import module_vtree def test_access_members_using_string_and_tuples() -> None: """Assert wa can access the same members with both strings and tuples.""" module = module_vtree("a.b.c.d") assert module["b"] is module[("b",)] assert module["b.c"] is module[("b", "c")] assert module["b.c.d"] is module[("b", "c", "d")] ����������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_api.py���������������������������������������������������������������0000664�0001750�0001750�00000016651�14767006246�017534� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for our own API exposition.""" from __future__ import annotations from collections import defaultdict from fnmatch import fnmatch from pathlib import Path from typing import TYPE_CHECKING import pytest from mkdocstrings import Inventory import griffe if TYPE_CHECKING: from collections.abc import Iterator @pytest.fixture(name="loader", scope="module") def _fixture_loader() -> griffe.GriffeLoader: loader = griffe.GriffeLoader() loader.load("griffe") loader.resolve_aliases() return loader @pytest.fixture(name="internal_api", scope="module") def _fixture_internal_api(loader: griffe.GriffeLoader) -> griffe.Module: return loader.modules_collection["_griffe"] @pytest.fixture(name="public_api", scope="module") def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: return loader.modules_collection["griffe"] def _yield_public_objects( obj: griffe.Module | griffe.Class, *, modules: bool = False, modulelevel: bool = True, inherited: bool = False, special: bool = False, ) -> Iterator[griffe.Object | griffe.Alias]: for member in obj.all_members.values() if inherited else obj.members.values(): try: if member.is_module: if member.is_alias: continue if modules: yield member yield from _yield_public_objects( member, # type: ignore[arg-type] modules=modules, modulelevel=modulelevel, inherited=inherited, special=special, ) elif member.is_public and (special or not member.is_special): yield member if member.is_class and not modulelevel: yield from _yield_public_objects( member, # type: ignore[arg-type] modules=modules, modulelevel=False, inherited=inherited, special=special, ) except (griffe.AliasResolutionError, griffe.CyclicAliasError): continue @pytest.fixture(name="modulelevel_internal_objects", scope="module") def _fixture_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(internal_api, modulelevel=True)) @pytest.fixture(name="internal_objects", scope="module") def _fixture_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(internal_api, modulelevel=False, special=True)) @pytest.fixture(name="public_objects", scope="module") def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) @pytest.fixture(name="inventory", scope="module") def _fixture_inventory() -> Inventory: inventory_file = Path(__file__).parent.parent / "site" / "objects.inv" if not inventory_file.exists(): raise pytest.skip("The objects inventory is not available.") with inventory_file.open("rb") as file: return Inventory.parse_sphinx(file) def test_alias_proxies(internal_api: griffe.Module) -> None: """The Alias class has all the necessary methods and properties.""" alias_members = set(internal_api["models.Alias"].all_members.keys()) for cls in ( internal_api["models.Module"], internal_api["models.Class"], internal_api["models.Function"], internal_api["models.Attribute"], ): for name in cls.all_members: if not name.startswith("_") or name.startswith("__"): assert name in alias_members def test_exposed_objects(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: """All public objects in the internal API are exposed under `griffe`.""" not_exposed = [ obj.path for obj in modulelevel_internal_objects if obj.name not in griffe.__all__ or not hasattr(griffe, obj.name) ] assert not not_exposed, "Objects not exposed:\n" + "\n".join(sorted(not_exposed)) def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: """All internal objects have unique names.""" names_to_paths = defaultdict(list) for obj in modulelevel_internal_objects: names_to_paths[obj.name].append(obj.path) non_unique = [paths for paths in names_to_paths.values() if len(paths) > 1] assert not non_unique, "Non-unique names:\n" + "\n".join(str(paths) for paths in non_unique) def test_single_locations(public_api: griffe.Module) -> None: """All objects have a single public location.""" def _public_path(obj: griffe.Object | griffe.Alias) -> bool: return obj.is_public and (obj.parent is None or _public_path(obj.parent)) multiple_locations = {} for obj_name in griffe.__all__: obj = public_api[obj_name] if obj.aliases and ( public_aliases := [path for path, alias in obj.aliases.items() if path != obj.path and _public_path(alias)] ): multiple_locations[obj.path] = public_aliases assert not multiple_locations, "Multiple public locations:\n" + "\n".join( f"{path}: {aliases}" for path, aliases in multiple_locations.items() ) def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias]) -> None: """All public objects are added to the inventory.""" ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"} ignore_paths = {"griffe.DataclassesExtension.*"} not_in_inventory = [ obj.path for obj in public_objects if ( obj.name not in ignore_names and not any(fnmatch(obj.path, pat) for pat in ignore_paths) and obj.path not in inventory ) ] msg = "Objects not in the inventory (try running `make run mkdocs build`):\n{paths}" assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) def test_inventory_matches_api( inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias], loader: griffe.GriffeLoader, ) -> None: """The inventory doesn't contain any additional Python object.""" not_in_api = [] public_api_paths = {obj.path for obj in public_objects} public_api_paths.add("griffe") for item in inventory.values(): if item.domain == "py" and "(" not in item.name: obj = loader.modules_collection[item.name] if obj.path not in public_api_paths and not any(path in public_api_paths for path in obj.aliases): not_in_api.append(item.name) msg = "Inventory objects not in public API (try running `make run mkdocs build`):\n{paths}" assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api))) def test_no_module_docstrings_in_internal_api(internal_api: griffe.Module) -> None: """No module docstrings should be written in our internal API. The reasoning is that docstrings are addressed to users of the public API, but internal modules are not exposed to users, so they should not have docstrings. """ def _modules(obj: griffe.Module) -> Iterator[griffe.Module]: for member in obj.modules.values(): yield member yield from _modules(member) for obj in _modules(internal_api): assert not obj.docstring ���������������������������������������������������������������������������������������python-griffe-1.6.2/tests/test_extensions.py��������������������������������������������������������0000664�0001750�0001750�00000011720�14767006246�021152� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for the `extensions` module.""" from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Any import pytest from griffe import Extension, load_extensions, temporary_visited_module if TYPE_CHECKING: import ast from griffe import Attribute, Class, Function, Module, Object, ObjectNode class ExtensionTest(Extension): # noqa: D101 def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 super().__init__() self.records: list[str] = [] self.args = args self.kwargs = kwargs def on_attribute_instance(self, *, node: ast.AST | ObjectNode, attr: Attribute, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_attribute_instance") def on_attribute_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_attribute_node") def on_class_instance(self, *, node: ast.AST | ObjectNode, cls: Class, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_class_instance") def on_class_members(self, *, node: ast.AST | ObjectNode, cls: Class, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_class_members") def on_class_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_class_node") def on_function_instance(self, *, node: ast.AST | ObjectNode, func: Function, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_function_instance") def on_function_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_function_node") def on_instance(self, *, node: ast.AST | ObjectNode, obj: Object, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_instance") def on_members(self, *, node: ast.AST | ObjectNode, obj: Object, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_members") def on_module_instance(self, *, node: ast.AST | ObjectNode, mod: Module, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_module_instance") def on_module_members(self, *, node: ast.AST | ObjectNode, mod: Module, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_module_members") def on_module_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_module_node") def on_node(self, *, node: ast.AST | ObjectNode, **kwargs: Any) -> None: # noqa: D102,ARG002 self.records.append("on_node") @pytest.mark.parametrize( "extension", [ # With module path. "tests.test_extensions", {"tests.test_extensions": {"option": 0}}, # With extension path. "tests.test_extensions.ExtensionTest", {"tests.test_extensions.ExtensionTest": {"option": 0}}, # With filepath. "tests/test_extensions.py", {"tests/test_extensions.py": {"option": 0}}, # With filepath and extension name. "tests/test_extensions.py:ExtensionTest", {"tests/test_extensions.py:ExtensionTest": {"option": 0}}, # With instance. ExtensionTest(option=0), # With class. ExtensionTest, # With absolute paths (esp. important to test for Windows). Path("tests/test_extensions.py").absolute().as_posix(), Path("tests/test_extensions.py:ExtensionTest").absolute().as_posix(), ], ) def test_loading_extensions(extension: str | dict[str, dict[str, Any]] | Extension | type[Extension]) -> None: """Test the extensions loading mechanisms. Parameters: extension: Extension specification (parametrized). """ extensions = load_extensions(extension) loaded: ExtensionTest = extensions._extensions[0] # type: ignore[assignment] # We cannot use isinstance here, # because loading from a filepath drops the parent `tests` package, # resulting in a different object than the present ExtensionTest. assert loaded.__class__.__name__ == "ExtensionTest" if isinstance(extension, (dict, ExtensionTest)): assert loaded.kwargs == {"option": 0} def test_extension_events() -> None: """Test events triggering.""" extension = ExtensionTest() with temporary_visited_module( """ attr = 0 def func(): ... class Class: cattr = 1 def method(self): ... """, extensions=load_extensions(extension), ): pass events = [ "on_attribute_instance", "on_attribute_node", "on_class_instance", "on_class_members", "on_class_node", "on_function_instance", "on_function_node", "on_instance", "on_members", "on_module_instance", "on_module_members", "on_module_node", "on_node", ] assert set(events) == set(extension.records) ������������������������������������������������python-griffe-1.6.2/tests/test_inheritance.py�������������������������������������������������������0000664�0001750�0001750�00000013355�14767006246�021252� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Tests for class inheritance.""" from __future__ import annotations from typing import TYPE_CHECKING, Callable import pytest from griffe import ModulesCollection, temporary_inspected_module, temporary_visited_module if TYPE_CHECKING: from griffe import Class def _mro_paths(cls: Class) -> list[str]: return [base.path for base in cls.mro()] @pytest.mark.parametrize("agent1", [temporary_visited_module, temporary_inspected_module]) @pytest.mark.parametrize("agent2", [temporary_visited_module, temporary_inspected_module]) def test_loading_inherited_members(agent1: Callable, agent2: Callable) -> None: """Test basic class inheritance. Parameters: agent1: A parametrized agent to load a module. agent2: A parametrized agent to load a module. """ code1 = """ class A: attr_from_a = 0 def method_from_a(self): ... class B(A): attr_from_a = 1 attr_from_b = 1 def method_from_b(self): ... """ code2 = """ from module1 import B class C(B): attr_from_c = 2 def method_from_c(self): ... class D(C): attr_from_a = 3 attr_from_d = 3 def method_from_d(self): ... """ inspection_options = {} collection = ModulesCollection() with agent1(code1, module_name="module1", modules_collection=collection) as module1: if agent2 is temporary_inspected_module: inspection_options["import_paths"] = [module1.filepath.parent] with agent2(code2, module_name="module2", modules_collection=collection, **inspection_options) as module2: classa = module1["A"] classb = module1["B"] classc = module2["C"] classd = module2["D"] assert classa in classb.resolved_bases assert classb in classc.resolved_bases assert classc in classd.resolved_bases classd_mro = classd.mro() assert classa in classd_mro assert classb in classd_mro assert classc in classd_mro inherited_members = classd.inherited_members assert "attr_from_a" not in inherited_members # Overwritten. assert "attr_from_b" in inherited_members assert "attr_from_c" in inherited_members assert "attr_from_d" not in inherited_members # Own-declared. assert "method_from_a" in inherited_members assert "method_from_b" in inherited_members assert "method_from_c" in inherited_members assert "method_from_d" not in inherited_members # Own-declared. assert "attr_from_b" in classd.all_members @pytest.mark.parametrize("agent", [temporary_visited_module, temporary_inspected_module]) def test_nested_class_inheritance(agent: Callable) -> None: """Test nested class inheritance. Parameters: agent: A parametrized agent to load a module. """ code = """ class A: class B: attr_from_b = 0 class C(A.B): attr_from_c = 1 """ with agent(code) as module: assert "attr_from_b" in module["C"].inherited_members code = """ class OuterA: class Inner: ... class OuterB(OuterA): class Inner(OuterA.Inner): ... class OuterC(OuterA): class Inner(OuterA.Inner): ... class OuterD(OuterC): class Inner(OuterC.Inner, OuterB.Inner): ... """ with temporary_visited_module(code) as module: assert _mro_paths(module["OuterD.Inner"]) == [ "module.OuterC.Inner", "module.OuterB.Inner", "module.OuterA.Inner", ] @pytest.mark.parametrize( ("classes", "cls", "expected_mro"), [ (["A", "B(A)"], "B", ["A"]), (["A", "B(A)", "C(A)", "D(B, C)"], "D", ["B", "C", "A"]), (["A", "B(A)", "C(A)", "D(C, B)"], "D", ["C", "B", "A"]), (["A(Z)"], "A", []), (["A(str)"], "A", []), (["A", "B(A)", "C(B)", "D(C)"], "D", ["C", "B", "A"]), (["A", "B(A)", "C(B)", "D(C)", "E(A)", "F(B)", "G(F, E)", "H(G, D)"], "H", ["G", "F", "D", "C", "B", "E", "A"]), (["A", "B(A[T])", "C(B[T])"], "C", ["B", "A"]), ], ) def test_computing_mro(classes: list[str], cls: str, expected_mro: list[str]) -> None: """Test computing MRO. Parameters: classes: A list of classes inheriting from each other. cls: The class to compute the MRO of. expected_mro: The expected computed MRO. """ code = "class " + ": ...\nclass ".join(classes) + ": ..." with temporary_visited_module(code) as module: assert _mro_paths(module[cls]) == [f"module.{base}" for base in expected_mro] @pytest.mark.parametrize( ("classes", "cls"), [ (["A", "B(A, A)"], "B"), (["A(D)", "B", "C(A, B)", "D(C)"], "D"), ], ) def test_uncomputable_mro(classes: list[str], cls: str) -> None: """Test uncomputable MRO. Parameters: classes: A list of classes inheriting from each other. cls: The class to compute the MRO of. """ code = "class " + ": ...\nclass ".join(classes) + ": ..." with temporary_visited_module(code) as module, pytest.raises(ValueError, match="Cannot compute C3 linearization"): _mro_paths(module[cls]) def test_dynamic_base_classes() -> None: """Test dynamic base classes.""" code = """ from collections import namedtuple class A(namedtuple("B", "attrb")): attra = 0 """ with temporary_visited_module(code) as module: assert _mro_paths(module["A"]) == [] # Not supported. with temporary_inspected_module(code) as module: assert _mro_paths(module["A"]) == [] # Not supported either. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/CHANGELOG.md��������������������������������������������������������������������0000664�0001750�0001750�00000467170�14767006246�016027� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). <!-- insertion marker --> ## [1.6.2](https://github.com/mkdocstrings/griffe/releases/tag/1.6.2) - 2025-03-20 <small>[Compare with 1.6.1](https://github.com/mkdocstrings/griffe/compare/1.6.1...1.6.2)</small> ### Code Refactoring - Maintain exports order (`__all__`) ([ded36bf](https://github.com/mkdocstrings/griffe/commit/ded36bf94ed4b8b83797e9da4a3d034d0533a5bd) by Timothée Mazzucotelli). ## [1.6.1](https://github.com/mkdocstrings/griffe/releases/tag/1.6.1) - 2025-03-18 <small>[Compare with 1.6.0](https://github.com/mkdocstrings/griffe/compare/1.6.0...1.6.1)</small> ### Bug Fixes - Extend exports from already expanded modules ([7e708cf](https://github.com/mkdocstrings/griffe/commit/7e708cf9ffe1845d310633e8486b99d32d5fca5c) by Timothée Mazzucotelli). [Issue-746](https://github.com/mkdocstrings/mkdocstrings/discussions/746) - Update imports when merging stubs ([5a92379](https://github.com/mkdocstrings/griffe/commit/5a92379e42c5a8bebc9323aabbbc9df881463718) by Timothée Mazzucotelli). [Issue-746](https://github.com/mkdocstrings/mkdocstrings/discussions/746) - Don't alias attributes when inspecting ([8063ba9](https://github.com/mkdocstrings/griffe/commit/8063ba9fd4b3d1f782515b81ba362c63d4ccd2bd) by Timothée Mazzucotelli). [Issue-366](https://github.com/mkdocstrings/griffe/issues/366) - Register top-module in collection earlier ([2c389b5](https://github.com/mkdocstrings/griffe/commit/2c389b57781c3c24a21141ad3d0103458418ec51) by Timothée Mazzucotelli). - Prevent recursion errors by not looking into inherited members when resolving base classes ([87cbaf8](https://github.com/mkdocstrings/griffe/commit/87cbaf87f09103b5972a47fdf5437e00df6e830a) by Timothée Mazzucotelli). ## [1.6.0](https://github.com/mkdocstrings/griffe/releases/tag/1.6.0) - 2025-03-01 <small>[Compare with 1.5.7](https://github.com/mkdocstrings/griffe/compare/1.5.7...1.6.0)</small> ### Features - Allow passing literal docstring styles everywhere in the API, not just `Parser` enumeration values ([053bf20](https://github.com/mkdocstrings/griffe/commit/053bf20e8da49f6bc0171c1755ee4fde1fb401fa) by Timothée Mazzucotelli). ### Bug Fixes - Follow symlinks when finding modules ([087832f](https://github.com/mkdocstrings/griffe/commit/087832f07dfb8dc529cf68438e7051bd8ce2ae1d) by Timothée Mazzucotelli). [Issue-mkdocstrings-python-258](https://github.com/mkdocstrings/python/issues/258) ## [1.5.7](https://github.com/mkdocstrings/griffe/releases/tag/1.5.7) - 2025-02-11 <small>[Compare with 1.5.6](https://github.com/mkdocstrings/griffe/compare/1.5.6...1.5.7)</small> ### Bug Fixes - Don't conflate passed argument with class member (instance attribute) ([4791b0b](https://github.com/mkdocstrings/griffe/commit/4791b0b5739e64ab2d225b299aea723f3cfbdf00) by Timothée Mazzucotelli). [Issue-357](https://github.com/mkdocstrings/griffe/issues/357) ## [1.5.6](https://github.com/mkdocstrings/griffe/releases/tag/1.5.6) - 2025-01-30 <small>[Compare with 1.5.5](https://github.com/mkdocstrings/griffe/compare/1.5.5...1.5.6)</small> ### Bug Fixes - Handle get/set descriptor objects as properties during dynamic analysis ([bc3c75a](https://github.com/mkdocstrings/griffe/commit/bc3c75acd440356fd7b91d221a8fca87231a6eab) by Timothée Mazzucotelli). [Issue-354](https://github.com/mkdocstrings/griffe/issues/354) ### Code Refactoring - Remove Google parser support for Deprecated sections (previously never used) ([425aece](https://github.com/mkdocstrings/griffe/commit/425aeceb9935be446979b669c9d557db84a36873) by Timothée Mazzucotelli). ## [1.5.5](https://github.com/mkdocstrings/griffe/releases/tag/1.5.5) - 2025-01-16 <small>[Compare with 1.5.4](https://github.com/mkdocstrings/griffe/compare/1.5.4...1.5.5)</small> ### Bug Fixes - Fix check command's Markdown output format not displaying parameter names ([5e7af22](https://github.com/mkdocstrings/griffe/commit/5e7af227792f602c8e1c40707bc7c058e272ce12) by Timothée Mazzucotelli). - Don't output empty change for removed objects when using GitHub output format (check command) ([6842372](https://github.com/mkdocstrings/griffe/commit/68423726c625649dd35b5c8018c752dbb72e5be2) by Timothée Mazzucotelli). [Issue-349](https://github.com/mkdocstrings/griffe/issues/349) ## [1.5.4](https://github.com/mkdocstrings/griffe/releases/tag/1.5.4) - 2024-12-26 <small>[Compare with 1.5.3](https://github.com/mkdocstrings/griffe/compare/1.5.3...1.5.4)</small> ### Bug Fixes - Append trailing comma to length-1 tuples ([4fccca7](https://github.com/mkdocstrings/griffe/commit/4fccca7dd8d8a3dd31ccc88930ca89f4f26d26b0) by Timothée Mazzucotelli). [Issue-343](https://github.com/mkdocstrings/griffe/issues/343) ### Performance Improvements - Avoid dictionary creation when accessing members of non-classes with subscript syntax ([0279998](https://github.com/mkdocstrings/griffe/commit/027999881415bea9e890493d3ef20b96b8749c4a) by Timothée Mazzucotelli). ## [1.5.3](https://github.com/mkdocstrings/griffe/releases/tag/1.5.3) - 2024-12-26 <small>[Compare with 1.5.2](https://github.com/mkdocstrings/griffe/compare/1.5.2...1.5.3)</small> ### Code Refactoring - Stop caching objects' inherited members, aliases' members and inherited members, classes' resolved bases ([e8db3a2](https://github.com/mkdocstrings/griffe/commit/e8db3a2d6c5c2a19a1fa3fc924f11c57d8e86a8e) by Timothée Mazzucotelli). [Issue-346](https://github.com/mkdocstrings/griffe/issues/346) ## [1.5.2](https://github.com/mkdocstrings/griffe/releases/tag/1.5.2) - 2024-12-26 <small>[Compare with 1.5.1](https://github.com/mkdocstrings/griffe/compare/1.5.1...1.5.2)</small> ### Bug Fixes - Always resolve aliases when checking APIs ([0b4f0da](https://github.com/mkdocstrings/griffe/commit/0b4f0da1658a3c4877a2519447288c1247694a0d) by Timothée Mazzucotelli). - Don't use same branch name when creating a worktree ([6d6c996](https://github.com/mkdocstrings/griffe/commit/6d6c99679976a18233ccda5e5cbfb4eb176312fd) by Timothée Mazzucotelli). [Issue-337](https://github.com/mkdocstrings/griffe/issues/337) - Fetch attribute annotations from inherited members too when parsing docstrings ([88fb6b6](https://github.com/mkdocstrings/griffe/commit/88fb6b6abd286b5552887023faa1a22f30cb11e7) by Timothée Mazzucotelli). [Issue-mkdocstrings/python#175](https://github.com/mkdocstrings/python/issues/175) ## [1.5.1](https://github.com/mkdocstrings/griffe/releases/tag/1.5.1) - 2024-10-18 <small>[Compare with 1.5.0](https://github.com/mkdocstrings/griffe/compare/1.5.0...1.5.1)</small> ### Bug Fixes - Sort Git tags using `creatordate` field, which works with both lightweight and annotated tags ([3bfa401](https://github.com/mkdocstrings/griffe/commit/3bfa4015c333dd7e56e535aa31bd2296701b6fa5) by Timothée Mazzucotelli). [Issue-327](https://github.com/mkdocstrings/griffe/issues/327) ## [1.5.0](https://github.com/mkdocstrings/griffe/releases/tag/1.5.0) - 2024-10-18 <small>[Compare with 1.4.1](https://github.com/mkdocstrings/griffe/compare/1.4.1...1.5.0)</small> ### Features - Allow setting and deleting parameters within container ([19f354d](https://github.com/mkdocstrings/griffe/commit/19f354da6a331a12d80a61bd3005cdcc30a3c42c) by Timothée Mazzucotelli). ## [1.4.1](https://github.com/mkdocstrings/griffe/releases/tag/1.4.1) - 2024-10-12 <small>[Compare with 1.4.0](https://github.com/mkdocstrings/griffe/compare/1.4.0...1.4.1)</small> ### Code Refactoring - Drop support for Python 3.8 ([f2d39b8](https://github.com/mkdocstrings/griffe/commit/f2d39b8ed40f2b90ac15fd7ad818b3c59b657a43) by Timothée Mazzucotelli). ## [1.4.0](https://github.com/mkdocstrings/griffe/releases/tag/1.4.0) - 2024-10-11 <small>[Compare with 1.3.2](https://github.com/mkdocstrings/griffe/compare/1.3.2...1.4.0)</small> ### Features - Add Markdown and GitHub output formats to the `griffe check` command ([806805c](https://github.com/mkdocstrings/griffe/commit/806805c3970a7cf3f32eec436255ea1323a60e1a) by Timothée Mazzucotelli). ## [1.3.2](https://github.com/mkdocstrings/griffe/releases/tag/1.3.2) - 2024-10-01 <small>[Compare with 1.3.1](https://github.com/mkdocstrings/griffe/compare/1.3.1...1.3.2)</small> ### Bug Fixes - Normalize paths of temporary Git worktrees ([0821e67](https://github.com/mkdocstrings/griffe/commit/0821e6784e5a3aeb56020867c8b46f9477621ed3) by Timothée Mazzucotelli). [Issue-324](https://github.com/mkdocstrings/griffe/issues/324) ## [1.3.1](https://github.com/mkdocstrings/griffe/releases/tag/1.3.1) - 2024-09-12 <small>[Compare with 1.3.0](https://github.com/mkdocstrings/griffe/compare/1.3.0...1.3.1)</small> ### Bug Fixes - Refactor and fix logic again for fetching returns/yields/receives annotation from parents ([a80bd3c](https://github.com/mkdocstrings/griffe/commit/a80bd3c0cc14e5f6efc30fb804b8c7fccb319276) by Timothée Mazzucotelli). [Follow-up-of-PR-322](https://github.com/mkdocstrings/griffe/pull/322) - Don't crash on invalid signature given "Receives" section ([1cb8f51](https://github.com/mkdocstrings/griffe/commit/1cb8f514eae9d588cfce8cbbfc3ef84d7deadb47) by Timothée Mazzucotelli). ## [1.3.0](https://github.com/mkdocstrings/griffe/releases/tag/1.3.0) - 2024-09-10 <small>[Compare with 1.2.0](https://github.com/mkdocstrings/griffe/compare/1.2.0...1.3.0)</small> ### Features - Allow deselecting multiple or named items in Yields and Receives ([344df50](https://github.com/mkdocstrings/griffe/commit/344df50bfcd66ddb3b8d8250babb40012cbc82b5) by Marco Ricci). [Issue-263](https://github.com/mkdocstrings/griffe/issues/263) ### Bug Fixes - Don't crash when trying to merge stubs into a compiled module that has no file path ([e1f3ed9](https://github.com/mkdocstrings/griffe/commit/e1f3ed9ad3b046bf137de22f855bb392a76ca116) by Timothée Mazzucotelli). [Issue-323](https://github.com/mkdocstrings/griffe/issues/323) - Fix identity checks in inspector when handling attributes ([676cfb4](https://github.com/mkdocstrings/griffe/commit/676cfb44a79e059f74514ff492035e930ed57d03) by Timothée Mazzucotelli). ### Code Refactoring - Extract common functionality in Returns, Yields and Receives parsing ([c768356](https://github.com/mkdocstrings/griffe/commit/c768356023e1fedaaa3f896b073457a0af34ce0e) by Marco Ricci). [Issue-263](https://github.com/mkdocstrings/griffe/issues/263) - Remove useless branch in `resolve` method, add tests for it ([aa6c7e4](https://github.com/mkdocstrings/griffe/commit/aa6c7e4d3dbabef384193b778cfdafd05a7102c2) by Timothée Mazzucotelli). ## [1.2.0](https://github.com/mkdocstrings/griffe/releases/tag/1.2.0) - 2024-08-23 <small>[Compare with 1.1.1](https://github.com/mkdocstrings/griffe/compare/1.1.1...1.2.0)</small> ### Features - Support attribute syntax in `__all__` values ([ad99794](https://github.com/mkdocstrings/griffe/commit/ad997940b136d315787fcb11c03fc70a40c7e8c2) by Timothée Mazzucotelli). [Issue-316](https://github.com/mkdocstrings/griffe/issues/316) ## [1.1.1](https://github.com/mkdocstrings/griffe/releases/tag/1.1.1) - 2024-08-20 <small>[Compare with 1.1.0](https://github.com/mkdocstrings/griffe/compare/1.1.0...1.1.1)</small> ### Bug Fixes - Preemptively expand `__all__` values and wildcard imports before firing the `on_package_loaded` event ([21b3780](https://github.com/mkdocstrings/griffe/commit/21b3780b1a3f7ac62a3380089857a720b646dc4a) by Timothée Mazzucotelli). ## [1.1.0](https://github.com/mkdocstrings/griffe/releases/tag/1.1.0) - 2024-08-17 <small>[Compare with 1.0.0](https://github.com/mkdocstrings/griffe/compare/1.0.0...1.1.0)</small> ### Features - Add `on_wildcard_expansion` event ([c6bc6fa](https://github.com/mkdocstrings/griffe/commit/c6bc6fa858a43ea2180f97fd270075d7ee7169e3) by Timothée Mazzucotelli). [Issue-282](https://github.com/mkdocstrings/griffe/issues/282) - Add `on_alias` event ([a760a8c](https://github.com/mkdocstrings/griffe/commit/a760a8c684cae0da6b6cc83e37d1d374bfeed662) by Timothée Mazzucotelli). [Issue-282](https://github.com/mkdocstrings/griffe/issues/282) - Pass `loader` to `on_package_loaded` hooks ([7f82dc3](https://github.com/mkdocstrings/griffe/commit/7f82dc382f1f20ee9e5f58a9ef7a775563894056) by Timothée Mazzucotelli). ## [1.0.0](https://github.com/mkdocstrings/griffe/releases/tag/1.0.0) - 2024-08-15 <small>[Compare with 0.49.0](https://github.com/mkdocstrings/griffe/compare/0.49.0...1.0.0)</small> **V1!** :rocket: :fire: :rainbow: ### Breaking changes Highlights: - Extensions inherit from `Extension`, (`VisitorExtension` and `InspectorExtension` are removed) - Members are serialized (`as_dict`/JSON) as a dictionary instead of a list - All objects are available in the top-level `griffe` module, nowhere else Removed objects: - all modules under the `griffe` package - the `griffe.DocstringWarningCallable` class - the `griffe.When` class - the `griffe.ExtensionType` type - the `griffe.InspectorExtension` class - the `griffe.VisitorExtension` class - the `griffe.HybridExtension` extension - the `griffe.patch_logger` function - the `griffe.JSONEncoder.docstring_parser` attribute - the `griffe.JSONEncoder.docstring_options` attribute - the `griffe.Extensions.attach_visitor` method - the `griffe.Extensions.attach_inspector` method - the `griffe.Extensions.before_visit` method - the `griffe.Extensions.before_children_visit` method - the `griffe.Extensions.after_children_visit` method - the `griffe.Extensions.after_visit` method - the `griffe.Extensions.before_inspection` method - the `griffe.Extensions.before_children_inspection` method - the `griffe.Extensions.after_children_inspection` method - the `griffe.Extensions.after_inspection` method - the `griffe.GriffeLoader.load_module` method - the `has_special_name` and `has_private_name` properties on objects - the `is_explicitely_exported` and `is_implicitely_exported` properties on objects - the `member_is_exported` method on objects Renamed/moved objects: - `griffe.Function.setter` -> `griffe.Attribute.setter` - `griffe.Function.deleter` -> `griffe.Attribute.deleter` Signatures: - `griffe.docstring_warning(name)` parameter was removed - `griffe.GriffeLoader.load(module)` parameter was removed - `griffe.load(module)` parameter was removed - `griffe.load_git(module)` parameter was removed - `griffe.find_breaking_changes(ignore_private)` parameter was removed - see previous deprecations ### Code Refactoring - Remove all legacy code for v1 ([86d321e](https://github.com/mkdocstrings/griffe/commit/86d321ed1303f7bde28950f14ea75412be1d6888) and [fd72083](https://github.com/mkdocstrings/griffe/commit/fd72083fa06c3eb4ef76fe74c5126eef308766c0)by Timothée Mazzucotelli). [PR-314](https://github.com/mkdocstrings/griffe/pull/314) ## [0.49.0](https://github.com/mkdocstrings/griffe/releases/tag/0.49.0) - 2024-08-14 <small>[Compare with 0.48.0](https://github.com/mkdocstrings/griffe/compare/0.48.0...0.49.0)</small> WARNING: **⚡ Imminent v1! ⚡🚀 See [v0.46](#0460-2024-06-16).** ### Deprecations - Cancel deprecation of `get_logger` and `patch_loggers` (and deprecate `patch_logger` instead). Extensions need loggers too, distinct ones, and they were forgotten... Sorry for the back and forth 🙇 - Attributes `setter` and `deleter` on `Function` are deprecated. They were moved into the `Attribute` class since properties are instantiated as attributes, not functions. - Extension hooks must accept `**kwargs` in their signature, to allow forward-compatibility. Accepting `**kwargs` also makes it possible to remove unused arguments from the signature. - In version 1, Griffe will serialize object members as dictionaries instead of lists. Lists were initially used to preserve source order, but source order can be re-obtained thanks to the line number attributes (`lineno`, `endlineno`). Version 0.49 is able to load both lists and dictionaries from JSON dumps, and version 1 will maintain this ability. However external tools loading JSON dumps will need to be updated. ### Features - Add `temporary_inspected_package` helper ([3c4ba16](https://github.com/mkdocstrings/griffe/commit/3c4ba160ca4c3407bc60d9125e0d93ae5e08d8f3) by Timothée Mazzucotelli). - Accept alias resolution related parameters in `temporary_visited_package` ([7d5408a](https://github.com/mkdocstrings/griffe/commit/7d5408a3bf81d64841bbe620b883bc16cb633f82) by Timothée Mazzucotelli). - Accept `inits` parameter in `temporary_visited_package` ([a4859b7](https://github.com/mkdocstrings/griffe/commit/a4859b74bf52ca29cbb46c147a2b6df4532297e1) by Timothée Mazzucotelli). - Warn (DEBUG) when an object coming from a sibling, parent or external module instead of the current module or a submodule is exported (listed in `__all__`) ([f82317a](https://github.com/mkdocstrings/griffe/commit/f82317a00333e1b8971625f14e4452e93e9840ff) by Timothée Mazzucotelli). [Issue-249](https://github.com/mkdocstrings/griffe/issues/249), [Related-to-PR-251](https://github.com/mkdocstrings/griffe/pull/251) - Pass down agent to extension hooks ([71acb01](https://github.com/mkdocstrings/griffe/commit/71acb018716031331bc26d79bc27fd45f67735c1) by Timothée Mazzucotelli). [Issue-312](https://github.com/mkdocstrings/griffe/issues/312) - Add `source` property to docstrings, which return the docstring lines as written in the source ([3f6a71a](https://github.com/mkdocstrings/griffe/commit/3f6a71a34f503e95fad55038292e3c8ab2ce30b6) by Timothée Mazzucotelli). [Issue-90](https://github.com/mkdocstrings/griffe/issues/90) ### Bug Fixes - Move `setter` and `deleter` to `Attribute` class instead of `Function`, since that's how properties are instantiated ([309c6e3](https://github.com/mkdocstrings/griffe/commit/309c6e34aded516dcfeab0dd81c2fbcecd2691ac) by Timothée Mazzucotelli). [Issue-311](https://github.com/mkdocstrings/griffe/issues/311) - Reduce risk of recursion errors by excluding imported objects from `has_docstrings`, unless they're public ([9296ca7](https://github.com/mkdocstrings/griffe/commit/9296ca7273eb1e6b7255b92793a09b82fd3bc4a9) by Timothée Mazzucotelli). [Issue-302](https://github.com/mkdocstrings/griffe/issues/302) - Fix retrieval of annotations from parent for Yields section in properties ([8a21f4d](https://github.com/mkdocstrings/griffe/commit/8a21f4db1743902c56875980a4aa2366609642c1) by Timothée Mazzucotelli). [Issue-298](https://github.com/mkdocstrings/griffe/issues/298) - Fix parsing Yields section (Google-style) when yielded values are tuples, and the description has more lines than tuple values ([9091776](https://github.com/mkdocstrings/griffe/commit/90917761ef7ea71ccda8147b3e1ebbc4675d9685) by Timothée Mazzucotelli). - Fix condition on objects kinds when merging stubs ([727f99b](https://github.com/mkdocstrings/griffe/commit/727f99b084c703937393d52e930aba4ee5739c3b) by Timothée Mazzucotelli). ### Code Refactoring - Sort keys when dumping JSON from the command line ([8cdffe9](https://github.com/mkdocstrings/griffe/commit/8cdffe9a68383369f6598820ec867740bee58207) by Timothée Mazzucotelli). [Issue-310](https://github.com/mkdocstrings/griffe/issues/310) - Handle both lists and dicts for members when loading JSON data in preparation of v1 ([f89050c](https://github.com/mkdocstrings/griffe/commit/f89050c3dced88d5295971ab019e5c9a5706f6cc) by Timothée Mazzucotelli). [Issue-310](https://github.com/mkdocstrings/griffe/issues/310) - Accept `**kwargs` in extension hooks to allow forward-compatibility ([2621d52](https://github.com/mkdocstrings/griffe/commit/2621d52e4d1e89e043e022efb8eba087df5d321e) by Timothée Mazzucotelli). [Issue-312](https://github.com/mkdocstrings/griffe/issues/312) - Revert deprecation of `patch_loggers` in favor of `patch_logger` ([a20796a](https://github.com/mkdocstrings/griffe/commit/a20796ac821ac72b22082fde2a68ad9dac735076) by Timothée Mazzucotelli). - Expose dummy `load_pypi` in non-Insiders version ([a69cffd](https://github.com/mkdocstrings/griffe/commit/a69cffd89215dbe629cec892ccda3c259d5572ef) by Timothée Mazzucotelli). - Don't emit deprecation warnings through own usage of deprecated API ([9922d74](https://github.com/mkdocstrings/griffe/commit/9922d741dc1f9538e5e5f00dd115b297665ac6f8) by Timothée Mazzucotelli). [Issue-mkdocstrings#676](https://github.com/mkdocstrings/mkdocstrings/issues/676) - Finish preparing docstring style auto-detection feature ([03bdec6](https://github.com/mkdocstrings/griffe/commit/03bdec61bbba86b1fa1b98cb890c034bbfcd44c3) by Timothée Mazzucotelli). [Issue-5](https://github.com/mkdocstrings/griffe/issues/5) - Add DocstringStyle literal type to prepare docstring style auto detection feature ([b7aaf64](https://github.com/mkdocstrings/griffe/commit/b7aaf6487f04876b498237726b36d08f8e35b905) by Timothée Mazzucotelli). [Issue-5](https://github.com/mkdocstrings/griffe/issues/5) - Inherit from `str, Enum` instead of `StrEnum` which needs a backport ([77f1544](https://github.com/mkdocstrings/griffe/commit/77f15443540acd2d279e08675b41bd69470f76d9) by Timothée Mazzucotelli). [Issue-307](https://github.com/mkdocstrings/griffe/issues/307) ## [0.48.0](https://github.com/mkdocstrings/griffe/releases/tag/0.48.0) - 2024-07-15 <small>[Compare with 0.47.0](https://github.com/mkdocstrings/griffe/compare/0.47.0...0.48.0)</small> WARNING: **⚡ Imminent v1! ⚡🚀 See [v0.46](#0460-2024-06-16).** ### Deprecations - All submodules are deprecated. All objects are now exposed in the top-level `griffe` module. - All logger names are deprecated, and will be replaced with `"griffe"` in v1. In v1 our single `"griffe"` logger will provide a method to temporarily disable logging, [`logger.disable()`][griffe.Logger.disable], since that's the most common third-party use. - The `get_logger` function is deprecated. Instead, we'll use a global `logger` internally, and users are welcome to use it too. - The `patch_loggers` function is renamed `patch_logger`. - Following the logging changes, the [`docstring_warning`][griffe.docstring_warning] function can now directly log a warning message instead of returning a callable that does. Passing it a logger name (to get a callable) is deprecated in favor of passing it a docstring, message and offset directly. ### Features - Support `FORCE_COLOR` environment variable ([e1b7bd9](https://github.com/mkdocstrings/griffe/commit/e1b7bd9c3a5be585815dc972a86a51cb1b63bfe7) by Timothée Mazzucotelli). ### Bug Fixes - Don't take a shortcut to the end of an alias chain when getting/setting/deleting alias members ([1930609](https://github.com/mkdocstrings/griffe/commit/193060908aa1cecb9931553abbb0f9fa182c66a1) by Timothée Mazzucotelli). - Short-circuit `__all__` convention when checking if a module is public ([5abf4e3](https://github.com/mkdocstrings/griffe/commit/5abf4e3343410dbd41760415cff7c5f9e8c2b6b8) by Timothée Mazzucotelli). - Reuse existing loggers, preventing overwriting issues ([3c2825f](https://github.com/mkdocstrings/griffe/commit/3c2825f9cf34eb8b0dbedd9fb542e14af3d24c33) by Timothée Mazzucotelli). - Ignore .pth files that are not utf-8 encoded ([ea299dc](https://github.com/mkdocstrings/griffe/commit/ea299dcb38ad78c9b3de961e88da214ccadd31be) by Andrew Sansom). [Issue-300](https://github.com/mkdocstrings/griffe/issues/300), [PR-301](https://github.com/mkdocstrings/griffe/pull/301) - Attributes without annotations cannot be dataclass parameters ([c9b2e09](https://github.com/mkdocstrings/griffe/commit/c9b2e09344538778426c446dad306c4881a873b2) by Hassan Kibirige). [PR-297](https://github.com/mkdocstrings/griffe/pull/297) - When deciding to alias an object or not during inspection, consider module paths to be equivalent even with arbitrary private components ([8c9f6e6](https://github.com/mkdocstrings/griffe/commit/8c9f6e609a1bb93d0c8c41962bb5a9f410862769) by Timothée Mazzucotelli). [Issue-296](https://github.com/mkdocstrings/griffe/issues/296) - Fix target path computation: use qualified names to maintain classes in the path ([6e17def](https://github.com/mkdocstrings/griffe/commit/6e17def0759409c7d5148c1a2f7747d029f17594) by Timothée Mazzucotelli). [Issue-296](https://github.com/mkdocstrings/griffe/issues/296) ### Code Refactoring - Prepare loggers for simplification ([381f10f](https://github.com/mkdocstrings/griffe/commit/381f10f9cc3c2e8b7e9f54db23c13334dacc1203) by Timothée Mazzucotelli). - Add all previous modules for backward compatibility ([a86e44e](https://github.com/mkdocstrings/griffe/commit/a86e44e14b8f7be5b6fa9fb2e6a1614da65a3918) by Timothée Mazzucotelli). - Add main public modules ([fb860b3](https://github.com/mkdocstrings/griffe/commit/fb860b3200699ae85fed52289f3a6136ea522618) by Timothée Mazzucotelli). - Simplify "is imported" check in `is_public` property ([c2bbc10](https://github.com/mkdocstrings/griffe/commit/c2bbc10082da8e3b11d2fe4576db9719b25054e0) by Timothée Mazzucotelli). - Use string and integer enumerations ([06b383b](https://github.com/mkdocstrings/griffe/commit/06b383b5d61bc5083c53745e2c19d0da75e55481) by Timothée Mazzucotelli). - Renamed agents nodes modules ([ddc5b0c](https://github.com/mkdocstrings/griffe/commit/ddc5b0cc5bba3e0901fe6c7e9f9fe5b70bd2883c) by Timothée Mazzucotelli). - Clean up and document internal API, mark legacy code ([92594a9](https://github.com/mkdocstrings/griffe/commit/92594a99fed42eb2daa3bbeb797edbf3507f3068) by Timothée Mazzucotelli). - Renamed `dataclasses` internal modules to `models` ([5555de6](https://github.com/mkdocstrings/griffe/commit/5555de62426063483196888f1bc73757e7492ce8) by Timothée Mazzucotelli). - Move sources under `_griffe` internal package ([cbce6a5](https://github.com/mkdocstrings/griffe/commit/cbce6a5c4740a5964f9b0eb605adbd6f554e99bc) by Timothée Mazzucotelli). ## [0.47.0](https://github.com/mkdocstrings/griffe/releases/tag/0.47.0) - 2024-06-18 <small>[Compare with 0.46.1](https://github.com/mkdocstrings/griffe/compare/0.46.1...0.47.0)</small> WARNING: **⚡ Imminent v1! ⚡🚀 See [v0.46](#0460-2024-06-16).** ### Deprecations - The `has_private_name` and `has_special_name` properties on objects and aliases have been renamed `is_private` and `is_special`. The `is_private` property now only returns true if the name is *not* special. ### Features - Add `deprecated` attribute and `is_deprecated` property to objects/aliases ([2a75d84](https://github.com/mkdocstrings/griffe/commit/2a75d84265b40983ce4a1eb148677efb803f78c6) by Timothée Mazzucotelli). - Add `is_imported` property to objects/aliases ([de926cc](https://github.com/mkdocstrings/griffe/commit/de926cc4782d53b9b28a2f887890d7711dfbc667) by Timothée Mazzucotelli). - Add `is_class_private` property to objects/aliases ([491b6c4](https://github.com/mkdocstrings/griffe/commit/491b6c4da086a68e8e1eee13f2d4b7840390b6b9) by Timothée Mazzucotelli). ### Code Refactoring - Rename `has_private_name` and `has_special_name` to `is_private` and `is_special` ([ae7c7e7](https://github.com/mkdocstrings/griffe/commit/ae7c7e73e7bf7f02b86fc58503888113d98e8e39) by Timothée Mazzucotelli). ## [0.46.1](https://github.com/mkdocstrings/griffe/releases/tag/0.46.1) - 2024-06-17 <small>[Compare with 0.46.0](https://github.com/mkdocstrings/griffe/compare/0.46.0...0.46.1)</small> WARNING: **⚡ Imminent v1! ⚡🚀 See [v0.46](#0460-2024-06-16).** ### Bug Fixes - Always consider special objects ("dunder" attributes/methods/etc.) to be public ([3319410](https://github.com/mkdocstrings/griffe/commit/331941029decd9d400b30ea1471b6bcc384fd54f) by Timothée Mazzucotelli). [Issue-294](https://github.com/mkdocstrings/griffe/issues/294), [Issue-295](https://github.com/mkdocstrings/griffe/issues/295) - Don't consider imported objects as public ([ea90952](https://github.com/mkdocstrings/griffe/commit/ea909526f3a637849364544daff74cd49ccaf428) by Timothée Mazzucotelli). [Discussion-169](https://github.com/mkdocstrings/python/discussions/169) ## [0.46.0](https://github.com/mkdocstrings/griffe/releases/tag/0.46.0) - 2024-06-16 <small>[Compare with 0.45.3](https://github.com/mkdocstrings/griffe/compare/0.45.3...0.46.0)</small> WARNING: **⚡ Imminent v1! ⚡🚀** We are working on v1, and it will come soon, so we recommend that you consider adding an upper bound on Griffe. Version 1 will remove all legacy code! There will be a couple more v0 before so that you get all the deprecation warnings needed to upgrade your code using Griffe before upgrading to v1. See breaking changes and deprecations for v0.46 below. ### Breaking Changes We are still in v0, so no major bump yet. - Calling objects' [`has_labels()`][griffe.Object.has_labels] method with a `labels` keyword argument is not supported anymore. The parameter became a variadic positional parameter, so it cannot be used as a keyword argument anymore. Passing a sequence instead of multiple positional arguments still works but will emit a deprecation warning. - Calling the [`load_extensions()`][griffe.load_extensions] function with an `exts` keyword argument is not supported anymore. The parameter became a variadic positional parameter, so it cannot be used as a keyword argument anymore. Passing a sequence instead of multiple positional arguments still works but will emit a deprecation warning. ### Deprecations - As seen above in the breaking changes section, the only parameters of [`Object.has_labels()`][griffe.Object.has_labels] and [`load_extensions()`][griffe.load_extensions] both became variadic positional parameters. Passing a sequence as single argument is deprecated in favor of passing multiple arguments. This is an ergonomic change: I myself often forgot to wrap extensions in a list. Passing sequences of labels (lists, sets, tuples) is also difficult from Jinja templates. - The following methods and properties on objects and aliases are deprecated: `member_is_exported()`, `is_explicitely_exported`, `is_implicitely_exported`. Use the [`is_exported`][griffe.ObjectAliasMixin.is_exported] property instead. See [issue 281](https://github.com/mkdocstrings/griffe/issues/281). - The [`is_exported()`][griffe.ObjectAliasMixin.is_exported] and [`is_public()`][griffe.ObjectAliasMixin.is_public] methods became properties. They can still be called like methods, but will emit deprecation warnings when doing so. See [issue 281](https://github.com/mkdocstrings/griffe/issues/281). - The `ignore_private` parameter of the [`find_breaking_changes()`][griffe.find_breaking_changes] function is now deprecated and unused. With the reworked "exported" and "public" API, this parameter became useless. See [issue 281](https://github.com/mkdocstrings/griffe/issues/281). - Using `stats()` instead of [`Stats`][griffe.Stats] will now emit a deprecation warning. ### Features - Add `docstring` attribute to parameters ([e21eabe](https://github.com/mkdocstrings/griffe/commit/e21eabe8c48e3650d04fec805804683cb743ce12) by Hassan Kibirige). [Issue-286](https://github.com/mkdocstrings/griffe/issues/286), [Related-to-mkdocstrings/griffe#252](https://github.com/mkdocstrings/griffe/pull/252), [PR-288](https://github.com/mkdocstrings/griffe/pull/288), Co-authored-by: Timothée Mazzucotelli <dev@pawamoy.fr> - Provide line numbers for classes and functions when inspecting ([b6ddcc4](https://github.com/mkdocstrings/griffe/commit/b6ddcc4e6da42318961bb7cb7be59041a43c6451) by Timothée Mazzucotelli). [Issue-272](https://github.com/mkdocstrings/griffe/issues/272) - Populate lines collection within helpers ([ab2e947](https://github.com/mkdocstrings/griffe/commit/ab2e9479c2b94dc7b6736e40024db87fb87b4e62) by Timothée Mazzucotelli). [GitHub-issue-270](https://github.com/mkdocstrings/griffe/issues/270), [Radicle-issue-0d6a513](https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z23ZVuA1DWS99PDJ1rcarCtJi99x1/issues/0d6a51328f554f235c38a2a652b844c4ba21bba5) ### Bug Fixes - Handle partials as functions while inspecting ([be29c32](https://github.com/mkdocstrings/griffe/commit/be29c3214680dc20c9c776d12a2a15ca690fa8d0) by Timothée Mazzucotelli). - Populate lines collection before visiting/inspecting modules within helpers ([08c3f40](https://github.com/mkdocstrings/griffe/commit/08c3f409f3fc130f07b2d717cddff38d47d4dbca) by Timothée Mazzucotelli). [Issue-272](https://github.com/mkdocstrings/griffe/issues/272) - Don't return all lines when line numbers are missing ([9e6dcaa](https://github.com/mkdocstrings/griffe/commit/9e6dcaa8f30132ebef59eb27b1f2f3ff7bc03bae) by Timothée Mazzucotelli). [Issue-271](https://github.com/mkdocstrings/griffe/issues/271) ### Code Refactoring - Emit deprecation warning when accessing `stats` instead of `Stats` ([e5572d2](https://github.com/mkdocstrings/griffe/commit/e5572d2eb1dd8dbe8f9b43b33119bd9becc4a4d9) by Timothée Mazzucotelli). - Rework "exported" and "public" logic ([b327b90](https://github.com/mkdocstrings/griffe/commit/b327b908d9546c8eb8f4ce5d3a216309937a6552) by Timothée Mazzucotelli). [Issue-281](https://github.com/mkdocstrings/griffe/issues/281) - Allow passing multiple extensions to `load_extensions` instead of a sequence ([fadb72b](https://github.com/mkdocstrings/griffe/commit/fadb72b4b693f418ebc11aefba3be188a2522c7e) by Timothée Mazzucotelli). [Issue-268](https://github.com/mkdocstrings/griffe/issues/268) - Allow passing multiple labels to `Object.has_labels` instead of set ([c4e3bf2](https://github.com/mkdocstrings/griffe/commit/c4e3bf2c1a6ff7a1a66f203ae7abec859cbdea44) by Timothée Mazzucotelli). [Issue-267](https://github.com/mkdocstrings/griffe/issues/267) ## [0.45.3](https://github.com/mkdocstrings/griffe/releases/tag/0.45.3) - 2024-06-09 <small>[Compare with 0.45.2](https://github.com/mkdocstrings/griffe/compare/0.45.2...0.45.3)</small> ### Bug Fixes - Always call `on_package_loaded` hook on a package, and not any other object ([40db38d](https://github.com/mkdocstrings/griffe/commit/40db38d6d55c5a7926d39408e7fd51ec198b62b9) by Timothée Mazzucotelli). [Issue-283](https://github.com/mkdocstrings/griffe/issues/283) ## [0.45.2](https://github.com/mkdocstrings/griffe/releases/tag/0.45.2) - 2024-05-23 <small>[Compare with 0.45.1](https://github.com/mkdocstrings/griffe/compare/0.45.1...0.45.2)</small> ### Bug Fixes - Support setuptools' new editable modules using type annotations ([14d45e8](https://github.com/mkdocstrings/griffe/commit/14d45e83d4a48c67b2347965351145cc78d7abe9) by Timothée Mazzucotelli). [Issue-273](https://github.com/mkdocstrings/griffe/issues/273) ## [0.45.1](https://github.com/mkdocstrings/griffe/releases/tag/0.45.1) - 2024-05-18 <small>[Compare with 0.45.0](https://github.com/mkdocstrings/griffe/compare/0.45.0...0.45.1)</small> ### Bug Fixes - Fix loading of importable modules thanks to their `__path__` attribute ([56f5363](https://github.com/mkdocstrings/griffe/commit/56f5363063b54bc43a7e61da7ac6b177db2f158f) by Timothée Mazzucotelli). [Issue-269](https://github.com/mkdocstrings/griffe/issues/269) ## [0.45.0](https://github.com/mkdocstrings/griffe/releases/tag/0.45.0) - 2024-05-12 <small>[Compare with 0.44.0](https://github.com/mkdocstrings/griffe/compare/0.44.0...0.45.0)</small> ### Features - Implement `-x`, `--force-inspection` CLI option ([776063d](https://github.com/mkdocstrings/griffe/commit/776063d971b059576c62f62fdd2e1199de033711) by Timothée Mazzucotelli). - Implement `force_inspection` option in the loader API ([3266f22](https://github.com/mkdocstrings/griffe/commit/3266f2290637d3f46d782fe7ce222ff29f549043) by Timothée Mazzucotelli). - Support inspecting packages (`__init__` modules) ([3f74f67](https://github.com/mkdocstrings/griffe/commit/3f74f679de15df098482fead505d0402bff84401) by Timothée Mazzucotelli). - Add parameters for resolving aliases to `load` functions ([e418dee](https://github.com/mkdocstrings/griffe/commit/e418dee1563e2a02ec61c920842e8b8a13419448) by Timothée Mazzucotelli). - Load private sibling modules by default when resolving aliases ([4806189](https://github.com/mkdocstrings/griffe/commit/4806189111572495466638bb7899cf906eeebfe9) by Timothée Mazzucotelli). ### Bug Fixes - Pass down modules collection when inspecting ([bc0f74b](https://github.com/mkdocstrings/griffe/commit/bc0f74bef40a812e00765a7ab17507b0bfbd62c3) by Timothée Mazzucotelli). - Catch loading errors when loading additional modules during wildcard expansion and alias resolution ([964e0d2](https://github.com/mkdocstrings/griffe/commit/964e0d2b78d3bc3530601009148fb4a5905b8721) by Timothée Mazzucotelli). ### Code Refactoring - Improve stats code and performance ([eeb497f](https://github.com/mkdocstrings/griffe/commit/eeb497fa41acf50801cc6a7a240d079cc1592e79) by Timothée Mazzucotelli). - Recurse immediately into non-discoverable submodules (no path on disk) during dynamic analysis ([d0b7a1d](https://github.com/mkdocstrings/griffe/commit/d0b7a1d96a4dd7513f34673b0ef6cd02aa7d0fca) by Timothée Mazzucotelli). - Simplify the code that checks if an object should be aliased or not during dynamic analysis ([fc794c2](https://github.com/mkdocstrings/griffe/commit/fc794c24c578fe868900483b20601937db3f3d05) by Timothée Mazzucotelli). - Avoid side-effect in inspector by checking early if an object is a cached property ([a6bfcfd](https://github.com/mkdocstrings/griffe/commit/a6bfcfdb9e2a0740d72abbd1480e0aa7e23c9af1) by Timothée Mazzucotelli). ## [0.44.0](https://github.com/mkdocstrings/griffe/releases/tag/0.44.0) - 2024-04-19 <small>[Compare with 0.43.0](https://github.com/mkdocstrings/griffe/compare/0.43.0...0.44.0)</small> ### Features - Add `resolved` property on expression names, returning the corresponding Griffe object ([9b5ca45](https://github.com/mkdocstrings/griffe/commit/9b5ca4574250f847fd33a8cb92af56806db50c1b) by Timothée Mazzucotelli). ### Bug Fixes - Fix enumeration properties on expression names ([6f22256](https://github.com/mkdocstrings/griffe/commit/6f22256ad02439d961bce2bb1afa32d4e9e10b10) by Timothée Mazzucotelli). ## [0.43.0](https://github.com/mkdocstrings/griffe/releases/tag/0.43.0) - 2024-04-18 <small>[Compare with 0.42.2](https://github.com/mkdocstrings/griffe/compare/0.42.2...0.43.0)</small> ### Features - Add properties telling whether an expression name resolves to an enumeration class, instance or value ([fdb21d9](https://github.com/mkdocstrings/griffe/commit/fdb21d943f72fb10a4406930bf3e3bf7aceff6b0) by Timothée Mazzucotelli). [Issue-mkdocstrings/python#124](https://github.com/mkdocstrings/python/issues/124) ## [0.42.2](https://github.com/mkdocstrings/griffe/releases/tag/0.42.2) - 2024-04-15 <small>[Compare with 0.42.1](https://github.com/mkdocstrings/griffe/compare/0.42.1...0.42.2)</small> ### Bug Fixes - Fix target path of aliases for multipart imports (`import a.b.c as x`) ([ee27ad9](https://github.com/mkdocstrings/griffe/commit/ee27ad97669a7321d18e6724e6c155cef601a289) by Timothée Mazzucotelli). [Issue-259](https://github.com/mkdocstrings/griffe/issues/259) ## [0.42.1](https://github.com/mkdocstrings/griffe/releases/tag/0.42.1) - 2024-03-19 <small>[Compare with 0.42.0](https://github.com/mkdocstrings/griffe/compare/0.42.0...0.42.1)</small> ### Bug Fixes - Don't return class variables as parameters of dataclasses ([2729c22](https://github.com/mkdocstrings/griffe/commit/2729c22505d87b771ab7a70c91c9f8301275aa8c) by Hassan Kibirige). [PR-253](https://github.com/mkdocstrings/griffe/pull/253) - Don't turn items annotated as InitVar into dataclass members ([6835ea3](https://github.com/mkdocstrings/griffe/commit/6835ea361325a205c0af69acabc66ca5193156c5) by Hassan Kibirige). [PR-252](https://github.com/mkdocstrings/griffe/pull/252) ## [0.42.0](https://github.com/mkdocstrings/griffe/releases/tag/0.42.0) - 2024-03-11 <small>[Compare with 0.41.3](https://github.com/mkdocstrings/griffe/compare/0.41.3...0.42.0)</small> ### Features - Better support for dataclasses ([82a9d57](https://github.com/mkdocstrings/griffe/commit/82a9d5798b2eebddfd640b918415a0e3de2ca739) by Timothée Mazzucotelli). [Issue-33](https://github.com/mkdocstrings/griffe/issues/233), [Issue-34](https://github.com/mkdocstrings/griffe/issues/234), [Issue-38](https://github.com/mkdocstrings/griffe/issues/238), [Issue-39](https://github.com/mkdocstrings/griffe/issues/239), [PR-240](https://github.com/mkdocstrings/griffe/pull/240) ### Bug Fixes - Don't return properties as parameters of dataclasses (again) ([8c48397](https://github.com/mkdocstrings/griffe/commit/8c48397e7301bbb296e2f2630405f2d22f7222e3) by Hassan Kibirige). [Issue-232](https://github.com/mkdocstrings/griffe/issues/232), [PR-248](https://github.com/mkdocstrings/griffe/pull/248) - Fix getting return type from parent property when parsing Sphinx docstrings ([f314957](https://github.com/mkdocstrings/griffe/commit/f314957c9da7805a9eb1a23d1a7f3d47b0b1e4c0) by Timothée Mazzucotelli). [Issue-125](https://github.com/mkdocstrings/griffe/issues/125) ### Code Refactoring - Warn (debug) when a submodule shadows a member with the same name ([cdc9e1c](https://github.com/mkdocstrings/griffe/commit/cdc9e1c5ee92a4c621314a9d9c6c465bfdd2ad92) by Timothée Mazzucotelli). [Issue-124](https://github.com/mkdocstrings/griffe/issues/124) ## [0.41.3](https://github.com/mkdocstrings/griffe/releases/tag/0.41.3) - 2024-03-04 <small>[Compare with 0.41.2](https://github.com/mkdocstrings/griffe/compare/0.41.2...0.41.3)</small> ### Code Refactoring - Catch index errors when finding top module in case of search path misconfiguration ([46c56c7](https://github.com/mkdocstrings/griffe/commit/46c56c7ff505531f5422f526ad38095ed463cc1b) by Timothée Mazzucotelli). [Issue-#246](https://github.com/mkdocstrings/griffe/issues/246) ## [0.41.2](https://github.com/mkdocstrings/griffe/releases/tag/0.41.2) - 2024-03-03 <small>[Compare with 0.41.1](https://github.com/mkdocstrings/griffe/compare/0.41.1...0.41.2)</small> ### Bug Fixes - Fix discovery of packages in the current working directory ([44f9617](https://github.com/mkdocstrings/griffe/commit/44f96173df188568bb1db54a20270ff0a08298c6) by Timothée Mazzucotelli). [Discussion-mkdocstrings#654](https://github.com/mkdocstrings/mkdocstrings/discussions/654) ## [0.41.1](https://github.com/mkdocstrings/griffe/releases/tag/0.41.1) - 2024-03-01 <small>[Compare with 0.41.0](https://github.com/mkdocstrings/griffe/compare/0.41.0...0.41.1)</small> ### Deprecations - The `load_git` function moved from `griffe.git` to `griffe.loader`. It is still importable from `griffe.git`, but will emit a deprecation warning. ### Code Refactoring - Expose Git utilities, move `load_git` into the `loader` module ([327cc5b](https://github.com/mkdocstrings/griffe/commit/327cc5b0f28f7236eaaf1c028674b6e0006611da) by Timothée Mazzucotelli). ## [0.41.0](https://github.com/mkdocstrings/griffe/releases/tag/0.41.0) - 2024-02-26 <small>[Compare with 0.40.1](https://github.com/mkdocstrings/griffe/compare/0.40.1...0.41.0)</small> ### Features - Add option to append `sys.path` to search paths to the check command too ([d153fa0](https://github.com/mkdocstrings/griffe/commit/d153fa0aeaa248ae13101f189f887f9bfee27f04) by Timothée Mazzucotelli). ### Bug Fixes - Special case NumpyDoc "warnings" and "notes" sections (plural) ([3b47cdb](https://github.com/mkdocstrings/griffe/commit/3b47cdb889e08106404bfcdbd3ce651f7eee6cdf) by Ethan Henderson). [PR #236](https://github.com/mkdocstrings/griffe/pull/236) - Serialize line numbers even if zero ([55e6e0e](https://github.com/mkdocstrings/griffe/commit/55e6e0e6c01351aa832aaf934d001442f66c8598) by Timothée Mazzucotelli). - Fix handling of lambda expressions ([598d08a](https://github.com/mkdocstrings/griffe/commit/598d08ae0dcd7d266194237211e6431ee65aee67) by Timothée Mazzucotelli). - Fix building expressions (and string values) for `yield` and `yield from` statements ([439f65e](https://github.com/mkdocstrings/griffe/commit/439f65e3703c5cad7d68aa3b2da371599236f58b) by Timothée Mazzucotelli). - Do not create aliases pointing to themselves ([356305f](https://github.com/mkdocstrings/griffe/commit/356305f69664c1d955f4dbf7c865cb4f553488fc) by Timothée Mazzucotelli). ### Code Refactoring - Remove `get_call_keyword_arguments` utility function, as it is implemented with a single line and creates a cyclic dependency with expressions ([35cf170](https://github.com/mkdocstrings/griffe/commit/35cf170cc91ba740e6f997d76f99d6a07e8d4437) by Timothée Mazzucotelli). - Further prevent cyclic dependency between node utils and expressions ([9614c83](https://github.com/mkdocstrings/griffe/commit/9614c83c037637d7823a4c06f115c3e2e4b6e10f) by Timothée Mazzucotelli). - Avoid cyclic dependency between node utils and expressions ([aedf39c](https://github.com/mkdocstrings/griffe/commit/aedf39c3795197deb6067e039da8bdec182bd363) by Timothée Mazzucotelli). - Move arguments node-parsing logic into its own module (used by visitor and lambda expressions) ([ad68e65](https://github.com/mkdocstrings/griffe/commit/ad68e65363c4338d7f38ccade2f9cc05d41f8100) by Timothée Mazzucotelli). - Use canonical imports ([3091660](https://github.com/mkdocstrings/griffe/commit/3091660ae1b6253e481cedbdcc31b73c0ab334df) by Timothée Mazzucotelli). - Use `ast.unparse` instead of our own unparser ([6fe1316](https://github.com/mkdocstrings/griffe/commit/6fe1316807870cbf93bba79f3d400cae6630ea73) by Timothée Mazzucotelli). - Only return 0 for the line number of removed objects when the location is reworked as relative ([3a4d054](https://github.com/mkdocstrings/griffe/commit/3a4d054e993e8a53cce9e53057e81479ab5f6034) by Timothée Mazzucotelli). ## [0.40.1](https://github.com/mkdocstrings/griffe/releases/tag/0.40.1) - 2024-02-08 <small>[Compare with 0.40.0](https://github.com/mkdocstrings/griffe/compare/0.40.0...0.40.1)</small> ### Bug Fixes - Don't return properties as parameters of dataclasses ([5a5c03b](https://github.com/mkdocstrings/griffe/commit/5a5c03b38366049f19fc2b65f09153e7df5748ce) by Timothée Mazzucotelli). [Issue #232](https://github.com/mkdocstrings/griffe/issues/232) ## [0.40.0](https://github.com/mkdocstrings/griffe/releases/tag/0.40.0) - 2024-01-30 <small>[Compare with 0.39.1](https://github.com/mkdocstrings/griffe/compare/0.39.1...0.40.0)</small> ### Features - Store reference to function call in keyword expressions ([d72f9d3](https://github.com/mkdocstrings/griffe/commit/d72f9d3a425fee11f23f9f7b44814b6fda458e6e) by Timothée Mazzucotelli). [PR #231](https://github.com/mkdocstrings/griffe/pull/231) ## [0.39.1](https://github.com/mkdocstrings/griffe/releases/tag/0.39.1) - 2024-01-18 <small>[Compare with 0.39.0](https://github.com/mkdocstrings/griffe/compare/0.39.0...0.39.1)</small> ### Bug Fixes - De-duplicate search paths in finder as they could lead to the same modules being yielded twice or more when scanning namespace packages ([80a158a](https://github.com/mkdocstrings/griffe/commit/80a158a2de8d53a054405c3e14113b09d73335a3) by Timothée Mazzucotelli). - Fix logic for skipping already encountered modules when scanning namespace packages ([21a48d0](https://github.com/mkdocstrings/griffe/commit/21a48d0b9248467fe3c36440bee649ce8879f295) by Timothée Mazzucotelli). [Issue mkdocstrings#646](https://github.com/mkdocstrings/mkdocstrings/issues/646) ## [0.39.0](https://github.com/mkdocstrings/griffe/releases/tag/0.39.0) - 2024-01-16 <small>[Compare with 0.38.1](https://github.com/mkdocstrings/griffe/compare/0.38.1...0.39.0)</small> ### Features - Support editable installs dynamically exposing modules from other directories ([2c4ba75](https://github.com/mkdocstrings/griffe/commit/2c4ba751d7d47eb48b47179d316722315e5d4647) by Timothée Mazzucotelli). [Issue #229](https://github.com/mkdocstrings/griffe/issues/229) - Support meson-python editable modules ([9123897](https://github.com/mkdocstrings/griffe/commit/9123897ad8d85e48bd3c435ffabcf9a36a0ed355) by Timothée Mazzucotelli). - Support admonitions in Numpydoc docstrings ([1e311a4](https://github.com/mkdocstrings/griffe/commit/1e311a4eb935c58d488c928a86493ab3f3368f06) by Michael Chow). [Issue #214](https://github.com/mkdocstrings/griffe/issues/214), [PR #219](https://github.com/mkdocstrings/griffe/pull/219), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Expose module properties on all objects ([123f8c5](https://github.com/mkdocstrings/griffe/commit/123f8c5ba1826435e90dafffbfe304bd6ab8e187) by Timothée Mazzucotelli). [Issue #226](https://github.com/mkdocstrings/griffe/issues/226) ### Bug Fixes - Consider space-only lines to be empty, never break Numpydoc sections on blank lines ([8c57354](https://github.com/mkdocstrings/griffe/commit/8c5735497578417e1dd723625590539016e7b7a5) by Timothée Mazzucotelli). [PR #220](https://github.com/mkdocstrings/griffe/pull/220), [Related to PR #219](https://github.com/mkdocstrings/griffe/pull/219), [Numpydoc discussion](https://github.com/numpy/numpydoc/issues/463) - Allow merging stubs into alias targets ([3cf7958](https://github.com/mkdocstrings/griffe/commit/3cf795871a0549b901d9374705d6a1eb84700128) by Timothée Mazzucotelli). - Insert the right directory in front of import paths before inspecting a module (dynamically imported) ([7d75c71](https://github.com/mkdocstrings/griffe/commit/7d75c71477ccb208e071bfe3c3204a0490274b44) by Timothée Mazzucotelli). ### Code Refactoring - Set lineno to 0 for removed objects when checking API ([b660c34](https://github.com/mkdocstrings/griffe/commit/b660c346feb3a95fbe54a6dad460e988a9a41774) by Timothée Mazzucotelli). - Prepare support for new output formats (styles) of the check command ([f2ece1e](https://github.com/mkdocstrings/griffe/commit/f2ece1e602b0fb3d888a60d892089a55fdcf60f0) by Timothée Mazzucotelli). - Transform finder's package and namespace package classes into dataclasses ([16be6a4](https://github.com/mkdocstrings/griffe/commit/16be6a4a7660d8ed13ccdcf9c571eda647e078f0) by Timothée Mazzucotelli). ## [0.38.1](https://github.com/mkdocstrings/griffe/releases/tag/0.38.1) - 2023-12-06 <small>[Compare with 0.38.0](https://github.com/mkdocstrings/griffe/compare/0.38.0...0.38.1)</small> ### Bug Fixes - Support absolute Windows paths for extensions ([4e67d8f](https://github.com/mkdocstrings/griffe/commit/4e67d8fa5f0e9f23c1df2e1d772fc0f1e4e6c2e0) by Timothée Mazzucotelli). [Issue mkdocstrings-python#116](https://github.com/mkdocstrings/python/issues/116) ## [0.38.0](https://github.com/mkdocstrings/griffe/releases/tag/0.38.0) - 2023-11-13 <small>[Compare with 0.37.0](https://github.com/mkdocstrings/griffe/compare/0.37.0...0.38.0)</small> ### Features - Allow passing load parameters to the temporary package visit helper ([3a7854f](https://github.com/mkdocstrings/griffe/commit/3a7854fb180e34392fd520d9d25a6298d4b80830) by Timothée Mazzucotelli). ## [0.37.0](https://github.com/mkdocstrings/griffe/releases/tag/0.37.0) - 2023-11-12 <small>[Compare with 0.36.9](https://github.com/mkdocstrings/griffe/compare/0.36.9...0.37.0)</small> ### Deprecations - The loader `load_module` method was renamed `load`, Its `module` parameter was renamed `objspec` and is now positional-only. This method always returned the specified object, not just modules, so it made more sense to rename it `load` and to rename the parameter specifying the object. Old usages (`load_module` and `module=...`) will continue to work for some time (a few months, a year, more), and will emit deprecation warnings. ### Features - Add option to warn about unknown parameters in Sphinx docstrings ([8b11d77](https://github.com/mkdocstrings/griffe/commit/8b11d77315ca7a5e15da519db1663d05805dd075) by Ashwin Vinod). [Issue #64](https://github.com/mkdocstrings/griffe/issues/64), [PR #210](https://github.com/mkdocstrings/griffe/pull/210), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Add `on_package_loaded` event ([a5cf654](https://github.com/mkdocstrings/griffe/commit/a5cf6543b43db06c4d0f24d2631ddc86b1fee41e) by Timothée Mazzucotelli). - Add option to find, load and merge stubs-only packages ([6e55f3b](https://github.com/mkdocstrings/griffe/commit/6e55f3bd0838e3f229fcd37d3aeced0146d33ff1) by Romain). [PR #221](https://github.com/mkdocstrings/griffe/pull/221), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Bug Fixes - Report attributes who lost their value as "unset" ([dfffa4b](https://github.com/mkdocstrings/griffe/commit/dfffa4b96a8a70f93b899bd41aefeaa9939819e9) by Geethakrishna-Puligundla). [Issue #218](https://github.com/mkdocstrings/griffe/issues/218), [PR #225](https://github.com/mkdocstrings/griffe/pull/225) - Don't crash when computing MRO for a class that is named after its parent ([a2dd8a6](https://github.com/mkdocstrings/griffe/commit/a2dd8a6bc3f95679e1c2e79ce05d175fb8f89ccc) by Timothée Mazzucotelli). ### Code Refactoring - Rename loader `load_module` method to `load` ([2bfe206](https://github.com/mkdocstrings/griffe/commit/2bfe206b57f607b56f7bcb5a85a7e2a25fe3bf47) by Timothée Mazzucotelli). ## [0.36.9](https://github.com/mkdocstrings/griffe/releases/tag/0.36.9) - 2023-10-27 <small>[Compare with 0.36.8](https://github.com/mkdocstrings/griffe/compare/0.36.8...0.36.9)</small> ### Bug Fixes - Fix accessing alias members with `__getitem__` ([8929409](https://github.com/mkdocstrings/griffe/commit/8929409d4703c6b684084e88aae0d99423e05dbf) by Timothée Mazzucotelli). [Issue mkdocstrings-python#111](https://github.com/mkdocstrings/python/issues/111) ### Code Refactoring - Expose parser enumeration and parser functions in top-level module ([785baa0](https://github.com/mkdocstrings/griffe/commit/785baa04e3081fcf80756f56dddb95a00cb9b025) by Timothée Mazzucotelli). ## [0.36.8](https://github.com/mkdocstrings/griffe/releases/tag/0.36.8) - 2023-10-25 <small>[Compare with 0.36.7](https://github.com/mkdocstrings/griffe/compare/0.36.7...0.36.8)</small> ### Bug Fixes - Use already parsed docstring sections when dumping full data ([311807b](https://github.com/mkdocstrings/griffe/commit/311807b8fa1716dabe5ba18d3e12c947286afd8e) by Timothée Mazzucotelli). [Discussion griffe-typingdoc#6](https://github.com/mkdocstrings/griffe-typingdoc/discussions/6) ## [0.36.7](https://github.com/mkdocstrings/griffe/releases/tag/0.36.7) - 2023-10-17 <small>[Compare with 0.36.6](https://github.com/mkdocstrings/griffe/compare/0.36.6...0.36.7)</small> ### Bug Fixes - Add missing proxies (methods/properties) to aliases ([7320640](https://github.com/mkdocstrings/griffe/commit/7320640d42ebb4546f787fe458d5032a67ea20b7) by Timothée Mazzucotelli). ### Code Refactoring - Use final target in alias proxies ([731d662](https://github.com/mkdocstrings/griffe/commit/731d66237252e754b7a935ca4d0344f554edb5ff) by Timothée Mazzucotelli). ## [0.36.6](https://github.com/mkdocstrings/griffe/releases/tag/0.36.6) - 2023-10-16 <small>[Compare with 0.36.5](https://github.com/mkdocstrings/griffe/compare/0.36.5...0.36.6)</small> ### Code Refactoring - Only consider presence/absence for docstrings truthiness, not emptiness of their value ([4c49611](https://github.com/mkdocstrings/griffe/commit/4c496117880d2166bfc2bc8c40a235c23cef8527) by Timothée Mazzucotelli). ## [0.36.5](https://github.com/mkdocstrings/griffe/releases/tag/0.36.5) - 2023-10-09 <small>[Compare with 0.36.4](https://github.com/mkdocstrings/griffe/compare/0.36.4...0.36.5)</small> ### Bug Fixes - Force extension import path to be a string (coming from MkDocs' `!relative` tag) ([34e21a9](https://github.com/mkdocstrings/griffe/commit/34e21a9545a38b61a1b80192af312d70f6c607f2) by Timothée Mazzucotelli). - Fix crash when trying to get a decorator callable path (found thanks to pysource-codegen) ([e57f08e](https://github.com/mkdocstrings/griffe/commit/e57f08eb5770eb3a9ed12e97da3076b87f109224) by Timothée Mazzucotelli). - Fix crash when trying to get docstring after assignment (found thanks to pysource-codegen) ([fb0a0c1](https://github.com/mkdocstrings/griffe/commit/fb0a0c1a8558c9d04855b75e4a9f579b46e2edd8) by Timothée Mazzucotelli). - Fix type errors in expressions and value extractor, don't pass duplicate arguments (found thanks to pysource-codegen) ([7e53288](https://github.com/mkdocstrings/griffe/commit/7e53288586bd90198cfd6a898002850c67213209) by Timothée Mazzucotelli). ## [0.36.4](https://github.com/mkdocstrings/griffe/releases/tag/0.36.4) - 2023-09-28 <small>[Compare with 0.36.3](https://github.com/mkdocstrings/griffe/compare/0.36.3...0.36.4)</small> ### Bug Fixes - Fix visiting relative imports in non-init modules ([c1138c3](https://github.com/mkdocstrings/griffe/commit/c1138c34b89965fd780d669c7dd6b12f245d8cd9) by Timothée Mazzucotelli). ## [0.36.3](https://github.com/mkdocstrings/griffe/releases/tag/0.36.3) - 2023-09-28 <small>[Compare with 0.36.2](https://github.com/mkdocstrings/griffe/compare/0.36.2...0.36.3)</small> ### Bug Fixes - Fix parsing of choices in Numpy parameters ([5f2d997](https://github.com/mkdocstrings/griffe/commit/5f2d99776e326679d2c0d1d9cb6b06d6436971c6) by Timothée Mazzucotelli). [Issue #212](https://github.com/mkdocstrings/griffe/issues/212) ### Code Refactoring - Add `repr` methods to function parameters ([9442234](https://github.com/mkdocstrings/griffe/commit/94422349483a25db627921dfe13c7a89b81e700e) by Timothée Mazzucotelli). ## [0.36.2](https://github.com/mkdocstrings/griffe/releases/tag/0.36.2) - 2023-09-10 <small>[Compare with 0.36.1](https://github.com/mkdocstrings/griffe/compare/0.36.1...0.36.2)</small> ### Bug Fixes - Fix warnings for docstrings in builtin modules ([6ba3e04](https://github.com/mkdocstrings/griffe/commit/6ba3e0461647c2c76d0fd68889d37bbada686259) by Timothée Mazzucotelli). - Fix dumping `filepath` to a dict when it is a list ([066a4a7](https://github.com/mkdocstrings/griffe/commit/066a4a7f22827783c930feacd6a339ed3d00ec27) by davfsa). [PR #207](https://github.com/mkdocstrings/griffe/pull/207) ## [0.36.1](https://github.com/mkdocstrings/griffe/releases/tag/0.36.1) - 2023-09-04 <small>[Compare with 0.36.0](https://github.com/mkdocstrings/griffe/compare/0.36.0...0.36.1)</small> ### Bug Fixes - Fix iterating non-flat expressions (some nodes were skipped) ([3249155](https://github.com/mkdocstrings/griffe/commit/324915507c1100e04ffed6d926143f66f0016870) by Timothée Mazzucotelli). ## [0.36.0](https://github.com/mkdocstrings/griffe/releases/tag/0.36.0) - 2023-09-01 <small>[Compare with 0.35.2](https://github.com/mkdocstrings/griffe/compare/0.35.2...0.36.0)</small> ### Features - Add option to read return type of properties in their summary (Google-style) ([096970f](https://github.com/mkdocstrings/griffe/commit/096970ffa66f491ef34ae1121e8b907f2da4c742) by Timothée Mazzucotelli). [Issue #137](https://github.com/mkdocstrings/griffe/issues/137), [PR #206](https://github.com/mkdocstrings/griffe/pull/206) - Add option to make parentheses around the type of returned values optional (Google-style) ([b0620f8](https://github.com/mkdocstrings/griffe/commit/b0620f86e1767183d776771992ce12f961efe395) by Timothée Mazzucotelli). [Issue #137](https://github.com/mkdocstrings/griffe/issues/137) - Get class parameters from parent's `__init__` method ([e8a9fdc](https://github.com/mkdocstrings/griffe/commit/e8a9fdcce1cffdc7db5a216f833d10da6116db5a) by Timothée Mazzucotelli). [Issue #205](https://github.com/mkdocstrings/griffe/issues/205) ### Bug Fixes - Use all members (declared and inherited) when checking for breakages, avoid false-positives when a member of a class is moved into a parent class ([1c4340b](https://github.com/mkdocstrings/griffe/commit/1c4340b09b111313a5a242caa986a2fa3fdef852) by Timothée Mazzucotelli). [Issue #203](https://github.com/mkdocstrings/griffe/issues/203) - Skip early submodules with dots in their path ([5e81b8a](https://github.com/mkdocstrings/griffe/commit/5e81b8afef4e6ce8294cdbaf348f4f1a05add1d8) by Timothée Mazzucotelli). [Issue #185](https://github.com/mkdocstrings/griffe/issues/185) ### Code Refactoring - Allow iterating on expressions in both flat and nested ways ([3957fa7](https://github.com/mkdocstrings/griffe/commit/3957fa70abf3f2d8af1a4ab4b1041b873bc724e0) by Timothée Mazzucotelli). ## [0.35.2](https://github.com/mkdocstrings/griffe/releases/tag/0.35.2) - 2023-08-27 <small>[Compare with 0.35.1](https://github.com/mkdocstrings/griffe/compare/0.35.1...0.35.2)</small> ### Code Refactoring - Be more strict when parsing sections in Google docstrings ([6a8a228](https://github.com/mkdocstrings/griffe/commit/6a8a2280f8910d4268380400d7888cb8d72b4296) by Timothée Mazzucotelli). [Issue #204](https://github.com/mkdocstrings/griffe/issues/204) ## [0.35.1](https://github.com/mkdocstrings/griffe/releases/tag/0.35.1) - 2023-08-26 <small>[Compare with 0.35.0](https://github.com/mkdocstrings/griffe/compare/0.35.0...0.35.1)</small> ### Bug Fixes - Preserve inherited attribute on alias inherited members ([1e19e7b](https://github.com/mkdocstrings/griffe/commit/1e19e7b2c3f2bb10c822c7d8b63b04a76024b4f7) by Timothée Mazzucotelli). [Issue mkdocstrings/python#102](https://github.com/mkdocstrings/python/issues/102) ## [0.35.0](https://github.com/mkdocstrings/griffe/releases/tag/0.35.0) - 2023-08-24 <small>[Compare with 0.34.0](https://github.com/mkdocstrings/griffe/compare/0.34.0...0.35.0)</small> ### Features - Add an `is_public` helper method to guess if an object is public ([b823639](https://github.com/mkdocstrings/griffe/commit/b8236391f4ac8b16e9ee861c322e75ea10d6a39b) by Timothée Mazzucotelli). - Add option to Google parser allowing to parse Returns sections with or without multiple items ([65fee70](https://github.com/mkdocstrings/griffe/commit/65fee70cf87399b7da92f054180791de0eb4f22d) by Antoine Dechaume). [PR #196](https://github.com/mkdocstrings/griffe/pull/196) ### Bug Fixes - Allow passing `warn_unknown_params` option to Google and Numpy parsers ([5bf0746](https://github.com/mkdocstrings/griffe/commit/5bf07468d38a158f8e58e3e1c562e8d886d83321) by Timothée Mazzucotelli). ### Code Refactoring - Preserve alias members path by re-aliasing members instead of returning target's members ([d400cb1](https://github.com/mkdocstrings/griffe/commit/d400cb13c8b7c250ff1e6b6c8ec9be1c7b6ff989) by Timothée Mazzucotelli). ## [0.34.0](https://github.com/mkdocstrings/griffe/releases/tag/0.34.0) - 2023-08-20 <small>[Compare with 0.33.0](https://github.com/mkdocstrings/griffe/compare/0.33.0...0.34.0)</small> ### Features - Allow checking if docstring section is empty or not with `if section` ([f6cf559](https://github.com/mkdocstrings/griffe/commit/f6cf559db50718e86cde40eae9d14489cabd9ed8) by Timothée Mazzucotelli). - Implement Functions (or Methods), Classes and Modules docstring sections ([929e615](https://github.com/mkdocstrings/griffe/commit/929e6158c093b021ba80773e17613406b38fbf0c) by Timothée Mazzucotelli). - Allow passing a docstring parser name instead of its enumeration value ([ce59b7d](https://github.com/mkdocstrings/griffe/commit/ce59b7dca69e3a9946a0735405535e296e0ec9c9) by Timothée Mazzucotelli). ### Code Refactoring - Explicit checks for subprocess runs ([cc3ca2e](https://github.com/mkdocstrings/griffe/commit/cc3ca2e18877c17fe23e2ceeb1c13e10c9fe46d2) by Timothée Mazzucotelli). ## [0.33.0](https://github.com/mkdocstrings/griffe/releases/tag/0.33.0) - 2023-08-16 <small>[Compare with 0.32.3](https://github.com/mkdocstrings/griffe/compare/0.32.3...0.33.0)</small> ### Breaking Changes - Removed `griffe.expressions.Expression` in favor of [`griffe.Expr`][] and subclasses - Removed `griffe.expressions.Name` in favor of [`griffe.ExprName`][] ### Features - Add `-V`, `--version` CLI flag to show version ([a41515f](https://github.com/mkdocstrings/griffe/commit/a41515f39e6e5e2e28d68980c44cc07a7e0ebbe0) by jgart). [Issue #186](https://github.com/mkdocstrings/griffe/issues/186), [PR #187](https://github.com/mkdocstrings/griffe/pull/187), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Code Refactoring - Improve expressions ([66c8ad5](https://github.com/mkdocstrings/griffe/commit/66c8ad5074e1475aa88a51d8652b5e197760d774) and [0fe8f91](https://github.com/mkdocstrings/griffe/commit/0fe8f9155b571714b0fe2a1bd7aef0b9b0738b08) by Timothée Mazzucotelli). ## [0.32.3](https://github.com/mkdocstrings/griffe/releases/tag/0.32.3) - 2023-07-17 <small>[Compare with 0.32.2](https://github.com/mkdocstrings/griffe/compare/0.32.2...0.32.3)</small> ### Bug Fixes - Fix detecting whether an object should be an alias during inspection ([6a63b37](https://github.com/mkdocstrings/griffe/commit/6a63b375db7d639dd05589c56a2f89d1be9d66a8) by Timothée Mazzucotelli). [Issue #180](https://github.com/mkdocstrings/griffe/issues/180) ### Code Refactoring - Improve log message when trying to stubs-merge objects of different kinds ([d34a3ba](https://github.com/mkdocstrings/griffe/commit/d34a3ba4bbd15c3fafe9cc5e2e82a2281cf3e094) by Timothée Mazzucotelli). - De-duplicate stubs merging log message ([cedc062](https://github.com/mkdocstrings/griffe/commit/cedc062cd4035a4ad0f3a14b4ef31bea4e39374d) by Timothée Mazzucotelli). ## [0.32.2](https://github.com/mkdocstrings/griffe/releases/tag/0.32.2) - 2023-07-17 <small>[Compare with 0.32.1](https://github.com/mkdocstrings/griffe/compare/0.32.1...0.32.2)</small> ### Bug Fixes - Keep parentheses around tuples, except within subscripts ([df6e636](https://github.com/mkdocstrings/griffe/commit/df6e636c3ecfaa6befdfdaf26e898e1a71218675) by Timothée Mazzucotelli). [Issue mkdocstrings/python#88](https://github.com/mkdocstrings/python/issues/88) ## [0.32.1](https://github.com/mkdocstrings/griffe/releases/tag/0.32.1) - 2023-07-15 <small>[Compare with 0.32.0](https://github.com/mkdocstrings/griffe/compare/0.32.0...0.32.1)</small> ### Bug Fixes - Fix aliases for direct nested imports ([e9867f7](https://github.com/mkdocstrings/griffe/commit/e9867f78044a2a33b575e274224d3a4c16b62439) by Timothée Mazzucotelli). [Issue mkdocstrings/python#32](https://github.com/mkdocstrings/python/issues/32) ### Code Refactoring - Simplify AST imports, stop using deprecated code from `ast` ([21d5832](https://github.com/mkdocstrings/griffe/commit/21d5832ba6db051b9754f515f1d7125126dd801f) by Timothée Mazzucotelli). [Issue #179](https://github.com/mkdocstrings/griffe/issues/179) ## [0.32.0](https://github.com/mkdocstrings/griffe/releases/tag/0.32.0) - 2023-07-13 <small>[Compare with 0.31.0](https://github.com/mkdocstrings/griffe/compare/0.31.0...0.32.0)</small> ### Deprecations - Classes `InspectorExtension` and `VisitorExtension` are deprecated in favor of [`Extension`][griffe.Extension]. As a side-effect, the `hybrid` extension is also deprecated. See [how to use and write extensions](guide/users/extending.md). ### Breaking Changes - Module `griffe.agents.base` was removed - Module `griffe.docstrings.markdown` was removed - Class `ASTNode` was removed - Class `BaseInspector` was removed - Class `BaseVisitor` was removed - Function `get_parameter_default` was removed - Function `load_extension` was removed (made private) - Function `patch_ast` was removed - Function `tmp_worktree` was removed (made private) - Type [`Extension`][griffe.Extension] is now a class ### Features - Numpy parser: handle return section items with just type, or no name and no type ([bdec37d](https://github.com/mkdocstrings/griffe/commit/bdec37dd32a5d4e089ee5e14e5a66be645bb8360) by Michael Chow). [Issue #173](https://github.com/mkdocstrings/griffe/issues/173), [PR #174](https://github.com/mkdocstrings/griffe/pull/174), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Rework extension system ([dea4c83](https://github.com/mkdocstrings/griffe/commit/dea4c830e3bfa0bf7c9f307975cb53e1314c50eb) by Timothée Mazzucotelli). - Parse attribute values, parameter defaults and decorators as expressions ([7b653b3](https://github.com/mkdocstrings/griffe/commit/7b653b31bd9c38bf8d960baa5ab75dd56c62fbcb) by Timothée Mazzucotelli). - Add loader option to avoid storing source code, reducing memory footprint ([d592edf](https://github.com/mkdocstrings/griffe/commit/d592edf477d9e7a5f9723c96cc259db65b1cae71) by Timothée Mazzucotelli). - Add `extra` attribute to objects ([707a348](https://github.com/mkdocstrings/griffe/commit/707a34833f56cf4a1aa302cb1201ad96ff361252) by Timothée Mazzucotelli). ### Bug Fixes - Numpy-style: don't strip spaces from the left of indented lines ([f13fc0a](https://github.com/mkdocstrings/griffe/commit/f13fc0a7edc7c8ac14c8c482b58735a5f7301bd6) by Timothée Mazzucotelli). [Discussion #587](https://github.com/mkdocstrings/mkdocstrings/discussions/587) - Fix relative paths for old versions when checking API ([96fd45b](https://github.com/mkdocstrings/griffe/commit/96fd45b41186eb503d6a2ff4e587cae427aea013) by Timothée Mazzucotelli). ### Performance Improvements - Don't store source when dumping as JSON ([d7f314a](https://github.com/mkdocstrings/griffe/commit/d7f314a62dd40c38c8c76ec7102233a588c1e64a) by Timothée Mazzucotelli). - Stop caching properties on Object methods ([15bdd74](https://github.com/mkdocstrings/griffe/commit/15bdd744db1f089f4448b952f9acf184c43289ea) by Timothée Mazzucotelli). - Stop patching AST, use functions instead ([7302f17](https://github.com/mkdocstrings/griffe/commit/7302f178392c70890d083a1617f1cf4e72395be3) by Timothée Mazzucotelli). [Issue #171](https://github.com/mkdocstrings/griffe/issues/171) ### Code Refactoring - Privatize/remove objects ([fdeb16f](https://github.com/mkdocstrings/griffe/commit/fdeb16f61cb5ae7db2394ef2a8ec31843b7ae85b) by Timothée Mazzucotelli). - Document public objects with `__all__` ([db0e0e3](https://github.com/mkdocstrings/griffe/commit/db0e0e340efcd48904f448a6e4397a9df36ac50f) by Timothée Mazzucotelli). - Remove base visitor and inspector ([bc446e4](https://github.com/mkdocstrings/griffe/commit/bc446e4ac9445636be7fdadbfc0b056cbc1d73e3) by Timothée Mazzucotelli). - Auto-register module in collection within loading helpers ([591bacc](https://github.com/mkdocstrings/griffe/commit/591bacc6c46d91beb30f6e01e0ae96f8e3102cf8) by Timothée Mazzucotelli). [Issue #177](https://github.com/mkdocstrings/griffe/issues/177) ## [0.31.0](https://github.com/mkdocstrings/griffe/releases/tag/0.31.0) - 2023-07-04 <small>[Compare with 0.30.1](https://github.com/mkdocstrings/griffe/compare/0.30.1...0.31.0)</small> ### Breaking Changes - Drop support for Python 3.7 - API changes: - [`GriffeLoader.resolve_aliases(only_exported)`][griffe.GriffeLoader.resolve_aliases]: Deprecated parameter was removed and replaced by `implicit` (inverse semantics) - [`GriffeLoader.resolve_aliases(only_known_modules)`][griffe.GriffeLoader.resolve_aliases]: Deprecated parameter was removed and replaced by `external` (inverse semantics) - [`LinesCollection.tokens`][griffe.LinesCollection]: Public object was removed (Python 3.7) - `ASTNode.end_lineno`: Public object was removed (Python 3.7) - `griffe.agents.extensions`: Deprecated module was removed and replaced by `griffe.extensions` ### Features - Add `--color`, `--no-color` options to check subcommand ([eac783c](https://github.com/mkdocstrings/griffe/commit/eac783c2df5a0ba57612b71b0797a74cf7fc8e39) by Timothée Mazzucotelli). ### Bug Fixes - Report removed public modules ([68906cb](https://github.com/mkdocstrings/griffe/commit/68906cb6083e5f7cad3a1cb5a74878d6e74f9c69) by Timothée Mazzucotelli). ### Code Refactoring - Improve check output ([6b0a1f0](https://github.com/mkdocstrings/griffe/commit/6b0a1f0397d153a95d1b6c69d109ce141e39e1f1) by Timothée Mazzucotelli). - Remove deprecated `griffe.agents.extensions` module ([b555c78](https://github.com/mkdocstrings/griffe/commit/b555c788b624fa5aa0c871e2c199079868252f22) by Timothée Mazzucotelli). - Remove deprecated parameters from loader's `resolve_aliases` method ([dd98acd](https://github.com/mkdocstrings/griffe/commit/dd98acd5f0c85661c7a00002805c92caa4c11a21) by Timothée Mazzucotelli). - Drop Python 3.7 support ([e4be30a](https://github.com/mkdocstrings/griffe/commit/e4be30a4c1025fd2f99f088c76f8e263714d8e33) by Timothée Mazzucotelli). ## [0.30.1](https://github.com/mkdocstrings/griffe/releases/tag/0.30.1) - 2023-07-02 <small>[Compare with 0.30.0](https://github.com/mkdocstrings/griffe/compare/0.30.0...0.30.1)</small> ### Bug Fixes - Prevent duplicate yields of breaking changes ([9edef90](https://github.com/mkdocstrings/griffe/commit/9edef90d6c54b330046582e2a52ad88b5798d32c) by Timothée Mazzucotelli). [Issue #162](https://github.com/mkdocstrings/griffe/issues/162) - Prevent alias resolution errors when checking for API breaking changes ([93c964a](https://github.com/mkdocstrings/griffe/commit/93c964a4cc3f759d101db45af5816a4d3b07c85e) by Timothée Mazzucotelli). [Issue #145](https://github.com/mkdocstrings/griffe/issues/145) - Handle Git errors when checking for API breaking changes ([f9e8ba3](https://github.com/mkdocstrings/griffe/commit/f9e8ba381b75f650cfeb7bc96c976fec2251ac7a) by Timothée Mazzucotelli). [Issue #144](https://github.com/mkdocstrings/griffe/issues/144) ### Code Refactoring - Force remove worktree branch when done checking ([45332ba](https://github.com/mkdocstrings/griffe/commit/45332ba89e213b4f9490ea7d2507d972267bed73) by Timothée Mazzucotelli). - Change command to obtain latest tag ([f70f630](https://github.com/mkdocstrings/griffe/commit/f70f630ef7c67589d60c17ef4fb19c90127b2e06) by Timothée Mazzucotelli). ## [0.30.0](https://github.com/mkdocstrings/griffe/releases/tag/0.30.0) - 2023-06-30 <small>[Compare with 0.29.1](https://github.com/mkdocstrings/griffe/compare/0.29.1...0.30.0)</small> ### Features - Add `allow_section_blank_line` option to the Numpy parser ([245845e](https://github.com/mkdocstrings/griffe/commit/245845ecaabedf4abb0af80d783702e55ea83883) by Michael Chow). [Issue #167](https://github.com/mkdocstrings/griffe/issues/167), [PR #168](https://github.com/mkdocstrings/griffe/pull/168) - Support inheritance ([08bbe09](https://github.com/mkdocstrings/griffe/commit/08bbe09879dfa5440a359c8b2ad0b896c20c1dfc) by Timothée Mazzucotelli). [PR #170](https://github.com/mkdocstrings/griffe/pull/170) ### Bug Fixes - Handle semi-colons in pth files ([e2ec661](https://github.com/mkdocstrings/griffe/commit/e2ec661e614df6c5f4fda1444468363777985b7c) by Michael Chow). [Issue #172](https://github.com/mkdocstrings/griffe/issues/172), [PR #175](https://github.com/mkdocstrings/griffe/pull/175) ### Code Refactoring - Split members API in two parts: producer and consumer ([2269449](https://github.com/mkdocstrings/griffe/commit/226944983a9073d643ed09b47e7d3f99c76d3d5e) by Timothée Mazzucotelli). [PR #170](https://github.com/mkdocstrings/griffe/pull/170) ## [0.29.1](https://github.com/mkdocstrings/griffe/releases/tag/0.29.1) - 2023-06-19 <small>[Compare with 0.29.0](https://github.com/mkdocstrings/griffe/compare/0.29.0...0.29.1)</small> ### Bug Fixes - Fix detection of optional and default in Numpydoc-style parameters ([3509106](https://github.com/mkdocstrings/griffe/commit/3509106399c5475ef71bb074dfa8f885e6759058) by Timothée Mazzucotelli). [Issue #165](https://github.com/mkdocstrings/griffe/issues/165) - Fallback to string literal when parsing fails with syntax error ([53827c8](https://github.com/mkdocstrings/griffe/commit/53827c8c073e55a7f6d8ef61b36e9baf51f1c2bc) by Timothée Mazzucotelli). [Issue mkdocstrings/python#80](https://github.com/mkdocstrings/python/issues/80) - Don't mutate finder's import paths ([a9e025a](https://github.com/mkdocstrings/griffe/commit/a9e025a16571b83713ce44f2be2356e498a847a2) by Timothée Mazzucotelli). - Respect `external` when expanding wildcards ([8ef92c8](https://github.com/mkdocstrings/griffe/commit/8ef92c873db175dbd35e6d09277f6023a8fde32d) by Timothée Mazzucotelli). - Extract actual type for yielded/received values ([3ea37ba](https://github.com/mkdocstrings/griffe/commit/3ea37ba2bcafea47f4b28bab6ae916ecb921b5ce) by Timothée Mazzucotelli). [Issue mkdocstrings/python#75](https://github.com/mkdocstrings/python/issues/75) ### Code Refactoring - Improve error handling when importing a module ([a732e21](https://github.com/mkdocstrings/griffe/commit/a732e217622cc5ab2161479b9dde0ce59e2361af) by Timothée Mazzucotelli). - Improve tests helpers (accept all visit/inspection parameters) ([6da5869](https://github.com/mkdocstrings/griffe/commit/6da586963cddff4dceadcd4b485dbb805830b6ea) by Timothée Mazzucotelli). - Allow passing a modules collection to the inspector, for consistency with the visitor ([5f73a28](https://github.com/mkdocstrings/griffe/commit/5f73a28a09a4b445fa253356034c5ef40b9ecfec) by Timothée Mazzucotelli). - Always add import path of module to inspect when it has a file path ([4021e6f](https://github.com/mkdocstrings/griffe/commit/4021e6fe9f5e06543f9709e7ae42f6ad8cd0b093) by Timothée Mazzucotelli). ## [0.29.0](https://github.com/mkdocstrings/griffe/releases/tag/0.29.0) - 2023-05-26 <small>[Compare with 0.28.2](https://github.com/mkdocstrings/griffe/compare/0.28.2...0.29.0)</small> ### Features - Provide test helpers and pytest fixtures ([611ed58](https://github.com/mkdocstrings/griffe/commit/611ed5868e22ac3ada6467ba25c6dab606f5dee7) by Timothée Mazzucotelli). ## [0.28.2](https://github.com/mkdocstrings/griffe/releases/tag/0.28.2) - 2023-05-24 <small>[Compare with 0.28.1](https://github.com/mkdocstrings/griffe/compare/0.28.1...0.28.2)</small> ### Bug Fixes - Correctly resolve full expressions ([fa57f4f](https://github.com/mkdocstrings/griffe/commit/fa57f4ff6495679b4e7e70d72d5adb80bd8ebc56) by Timothée Mazzucotelli). [Issue mkdocstrings/autorefs#23](https://github.com/mkdocstrings/autorefs/issues/23) - Use `full` attribute instead of `canonical` for expressions ([4338ccc](https://github.com/mkdocstrings/griffe/commit/4338ccc9234f0c4df0ea302a81092a4f3d29f0bf) by Michael Chow). [Issue #163](https://github.com/mkdocstrings/griffe/issues/163), [PR #164](https://github.com/mkdocstrings/griffe/pull/164) ## [0.28.1](https://github.com/mkdocstrings/griffe/releases/tag/0.28.1) - 2023-05-22 <small>[Compare with 0.28.0](https://github.com/mkdocstrings/griffe/compare/0.28.0...0.28.1)</small> ### Bug Fixes - Return docstring warnings as warnings, not attributes ([7bd51ba](https://github.com/mkdocstrings/griffe/commit/7bd51ba7c9c268a1cc378d38fdff3a891adc520c) by Matthew Anderson). [PR #161](https://github.com/mkdocstrings/griffe/pull/161) ### Code Refactoring - Refactor AST nodes parsers ([7e53127](https://github.com/mkdocstrings/griffe/commit/7e5312744cd7f6ad3baba54fe8194d15896f5e6d) by Timothée Mazzucotelli). [Issue #160](https://github.com/mkdocstrings/griffe/issues/160) - Full expressions use canonical names ([65c7184](https://github.com/mkdocstrings/griffe/commit/65c7184b5462b70debce1195c69449935cb0a0b1) by Timothée Mazzucotelli). ## [0.28.0](https://github.com/mkdocstrings/griffe/releases/tag/0.28.0) - 2023-05-17 <small>[Compare with 0.27.5](https://github.com/mkdocstrings/griffe/compare/0.27.5...0.28.0)</small> ### Features - Support scikit-build-core editable modules (partially) ([eb64779](https://github.com/mkdocstrings/griffe/commit/eb64779cb5408553bd4923ab9cdfc72d0b5e6103) by Timothée Mazzucotelli). [Issue #154](https://github.com/mkdocstrings/griffe/issues/154) ### Bug Fixes - Parse complex, stringified annotations ([f743616](https://github.com/mkdocstrings/griffe/commit/f74361684a2cd5db153875b8880788c254828e95) by Timothée Mazzucotelli). [Issue #159](https://github.com/mkdocstrings/griffe/issues/159) ## [0.27.5](https://github.com/mkdocstrings/griffe/releases/tag/0.27.5) - 2023-05-12 <small>[Compare with 0.27.4](https://github.com/mkdocstrings/griffe/compare/0.27.4...0.27.5)</small> ### Code Refactoring - Represent function using their names when inspecting default values ([9116c1f](https://github.com/mkdocstrings/griffe/commit/9116c1fbb562c894547d72207921c02259147958) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#180](https://github.com/mkdocstrings/mkdocstrings/issues/180) ## [0.27.4](https://github.com/mkdocstrings/griffe/releases/tag/0.27.4) - 2023-05-10 <small>[Compare with 0.27.3](https://github.com/mkdocstrings/griffe/compare/0.27.3...0.27.4)</small> ### Bug Fixes - Don't recurse through targets, get directly to final target and handle alias-related errors ([c5bc197](https://github.com/mkdocstrings/griffe/commit/c5bc1973975951389501addf567622c0e3eb71c6) by Timothée Mazzucotelli). [Issue #155](https://github.com/mkdocstrings/griffe/issues/155) ### Code Refactoring - Follow `.pth` files to extend search paths with editable modules ([79bf724](https://github.com/mkdocstrings/griffe/commit/79bf72498150588d05ccdfc80a898c0330e08247) by Timothée Mazzucotelli). [Issue #154](https://github.com/mkdocstrings/griffe/issues/154) - Add default values to `_load_packages` helper ([f104c20](https://github.com/mkdocstrings/griffe/commit/f104c20304dcf24c5d2e39220302a941db4161eb) by Timothée Mazzucotelli). ## [0.27.3](https://github.com/mkdocstrings/griffe/releases/tag/0.27.3) - 2023-05-05 <small>[Compare with 0.27.2](https://github.com/mkdocstrings/griffe/compare/0.27.2...0.27.3)</small> ### Bug Fixes - Allow setting docstring through alias ([2e0f553](https://github.com/mkdocstrings/griffe/commit/2e0f553c833e9b27f5e97c05065c2127212b603c) by Timothée Mazzucotelli). - Prevent infinite recursion ([0e98546](https://github.com/mkdocstrings/griffe/commit/0e985460eb886ea832e7cbefca261620eedb0e56) by Timothée Mazzucotelli). [Issue #155](https://github.com/mkdocstrings/griffe/issues/155) ## [0.27.2](https://github.com/mkdocstrings/griffe/releases/tag/0.27.2) - 2023-05-03 <small>[Compare with 0.27.1](https://github.com/mkdocstrings/griffe/compare/0.27.1...0.27.2)</small> ### Dependencies - Remove async extra (aiofiles) ([70d9b93](https://github.com/mkdocstrings/griffe/commit/70d9b9305370f03c221876838aaad9b72dc388d3) by Timothée Mazzucotelli). ### Bug Fixes - Support walrus operator ([bf721f4](https://github.com/mkdocstrings/griffe/commit/bf721f4dd2bb7f1a6695b5c880df821920b994a6) by Timothée Mazzucotelli). [Issue #152](https://github.com/mkdocstrings/griffe/issues/152) - Respect `ClassVar` annotation ([60e01c1](https://github.com/mkdocstrings/griffe/commit/60e01c126df4e0529fe3806f9c2637a5a45dd138) by Victor Westerhuis). [PR #150](https://github.com/mkdocstrings/griffe/pull/150), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Add missing "other args" section aliases ([f5c0a0e](https://github.com/mkdocstrings/griffe/commit/f5c0a0ee70c34063ea38a8e76dcba4923f9673cb) by Timothée Mazzucotelli). ### Code Refactoring - Move utils from cli to respective modules ([c6ce49e](https://github.com/mkdocstrings/griffe/commit/c6ce49eb75c1799982b40a7862a1a7888f0fab93) by Timothée Mazzucotelli). ## [0.27.1](https://github.com/mkdocstrings/griffe/releases/tag/0.27.1) - 2023-04-16 <small>[Compare with 0.27.0](https://github.com/mkdocstrings/griffe/compare/0.27.0...0.27.1)</small> ### Bug Fixes - Actually parse warnings sections ([bc00da5](https://github.com/mkdocstrings/griffe/commit/bc00da5e9dfe4b2aee906000759e0c1e0a2f893b) by Timothée Mazzucotelli). - Allow Raises and Warns items to start with a newline ([f3b088c](https://github.com/mkdocstrings/griffe/commit/f3b088c02b3be86934125b142876b0dfb3702677) by Victor Westerhuis). [PR #149](https://github.com/mkdocstrings/griffe/pull/149), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ## [0.27.0](https://github.com/mkdocstrings/griffe/releases/tag/0.27.0) - 2023-04-10 <small>[Compare with 0.26.0](https://github.com/mkdocstrings/griffe/compare/0.26.0...0.27.0)</small> ### Features - Implement basic handling of Alias for breaking changes ([aa8ce00](https://github.com/mkdocstrings/griffe/commit/aa8ce009c8d69f7830bc46bc80dac34907b8ae83) by Yurii). [PR #140](https://github.com/mkdocstrings/griffe/pull/140), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Bug Fixes - Support `Literal` imported from `typing_extensions` ([3a16e58](https://github.com/mkdocstrings/griffe/commit/3a16e5858649f7d786ef8a60b9dfd588f406cd9d) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#545](https://github.com/mkdocstrings/mkdocstrings/issues/545) - Fix parameter default checking logic and diff tests ([1b940fd](https://github.com/mkdocstrings/griffe/commit/1b940fd270b3e51dc0f62edb500a6a3e85908953) by Timothée Mazzucotelli). ## [0.26.0](https://github.com/mkdocstrings/griffe/releases/tag/0.26.0) - 2023-04-03 <small>[Compare with 0.25.5](https://github.com/mkdocstrings/griffe/compare/0.25.5...0.26.0)</small> ### Breaking Changes - `AliasResolutionError` instances don't have a `target_path` attribute anymore. It is instead replaced by an `alias` attribute which is a reference to an `Alias` instance. - Lots of positional-or-keyword parameters were changed to keyword-only parameters. ### Deprecations - The `griffe.agents.extensions` module was moved to `griffe.extensions`. The old path is deprecated. ### Features - Support newer versions of `editables` ([ab7a3be](https://github.com/mkdocstrings/griffe/commit/ab7a3be3902af5f4af1d1e762b2b6e532826569f) by Timothée Mazzucotelli): the names of editable modules have changed from `__editables_*` to `_editable_impl_*`. - Provide a JSON schema ([7dfed39](https://github.com/mkdocstrings/griffe/commit/7dfed391c7714a9d1aea9223e1f8c9403d47e8bb) by Timothée Mazzucotelli). - Allow hybrid extension to filter objects and run multiple inspectors ([f8ff53a](https://github.com/mkdocstrings/griffe/commit/f8ff53a69a3a131998649d1a9ba272827b7f2adc) by Timothée Mazzucotelli). - Allow loading extension from file path ([131454e](https://github.com/mkdocstrings/griffe/commit/131454eece81da33cd7f1a8bf2ae030950df8441) by Timothée Mazzucotelli). - Add back `relative_filepath` which now really returns the filepath relative to the current working directory ([40fe0c5](https://github.com/mkdocstrings/griffe/commit/40fe0c53be8ff72f254bd88e9c9cf6df36d3bcb9) by Timothée Mazzucotelli). ### Bug Fixes - Fix JSON schema for ending line numbers (and add test) ([318c6b4](https://github.com/mkdocstrings/griffe/commit/318c6b41c0160070de1b10118d210cacd5f2e711) by Timothée Mazzucotelli). - Prevent cyclic aliases by not overwriting a module member with an indirect alias to itself ([c188a95](https://github.com/mkdocstrings/griffe/commit/c188a95b823e876f89ba9046df2cb06348f92459) by Timothée Mazzucotelli). [Issue #122](https://github.com/mkdocstrings/griffe/issues/122) - Prevent alias resolution errors when copying docstring or labels from previously existing attribute ([48747b6](https://github.com/mkdocstrings/griffe/commit/48747b6d14bdf1be03cfa5bbf849771e3e6801b0) by Timothée Mazzucotelli). - Fix Google admonition regular expression ([ef0be5f](https://github.com/mkdocstrings/griffe/commit/ef0be5f8f276a5ef2397ad89c0cfce0e1b41020e) by Timothée Mazzucotelli). - Add back `griffe.agents.extensions` module (deprecated) ([7129477](https://github.com/mkdocstrings/griffe/commit/7129477184f0b88d3bf165dfe8e1f6158c30914a) by Timothée Mazzucotelli). - Forward class attribute docstrings to instances ([7bf4952](https://github.com/mkdocstrings/griffe/commit/7bf49528541e211af37c2ac5c1a74a4523699c65) by Rodrigo Girão Serrão). [Issue #128](https://github.com/mkdocstrings/griffe/issues/128), [PR #135](https://github.com/mkdocstrings/griffe/pull/135) - Prevent errors related to getting attributes in the inspector ([5d15d27](https://github.com/mkdocstrings/griffe/commit/5d15d276259a4b9a70fbe490d86234e667711180) by Timothée Mazzucotelli). - Catch "member does not exist" errors while expanding wildcards ([a966022](https://github.com/mkdocstrings/griffe/commit/a9660220c0b5e9e786877efa228452a643e93c76) by Timothée Mazzucotelli). - Catch more inspection errors ([4f6eef9](https://github.com/mkdocstrings/griffe/commit/4f6eef9b0fbcdf56d61ac4bec9dc4ef3b90dd116) by Timothée Mazzucotelli). ### Code Refactoring - Log final path after resolving alias ([c7ec7f7](https://github.com/mkdocstrings/griffe/commit/c7ec7f7ca029492ced68737851d66256c5035f70) by Timothée Mazzucotelli). - Move extensions one level up ([67ebd71](https://github.com/mkdocstrings/griffe/commit/67ebd71f9b0933f08b263d0b21520dc0b1a5c4ff) by Timothée Mazzucotelli). - Set default `when` value on extension base classes ([e8ad889](https://github.com/mkdocstrings/griffe/commit/e8ad8893aaad2549bff134a7bf3dfe5a86bfc960) by Timothée Mazzucotelli). - Rename `relative_filepath` to `relative_package_filepath` to better express what it does ([6148f85](https://github.com/mkdocstrings/griffe/commit/6148f85c56848c6bb3e7df8986f1bb208e7083cf) by Timothée Mazzucotelli). - Show file name and line number in alias resolution error messages ([c48928d](https://github.com/mkdocstrings/griffe/commit/c48928df4a75be35771d39bf96699d801485b31d) by Timothée Mazzucotelli). ## [0.25.5](https://github.com/mkdocstrings/griffe/releases/tag/0.25.5) - 2023-02-16 <small>[Compare with 0.25.4](https://github.com/mkdocstrings/griffe/compare/0.25.4...0.25.5)</small> ### Bug Fixes - Fix parsing empty lines with indentation in Google docstrings ([705edff](https://github.com/mkdocstrings/griffe/commit/705edff6c208281bdab387a464799de613b087b5) by Timothée Mazzucotelli). [Issue #129](https://github.com/mkdocstrings/griffe/issues/129) ## [0.25.4](https://github.com/mkdocstrings/griffe/releases/tag/0.25.4) - 2023-01-19 <small>[Compare with 0.25.3](https://github.com/mkdocstrings/griffe/compare/0.25.3...0.25.4)</small> ### Bug Fixes - Fix creation of aliases to modules when inspecting ([54242cb](https://github.com/mkdocstrings/griffe/commit/54242cbdbbcb68785942fa327113cd6508815fa9) by Timothée Mazzucotelli). - Support (setuptools) editable packages with multiple roots ([bd37dfb](https://github.com/mkdocstrings/griffe/commit/bd37dfb16b43fac53207b426ee02218e57a5d5d1) by Gilad). [PR #126](https://github.com/mkdocstrings/griffe/pull/126) ## [0.25.3](https://github.com/mkdocstrings/griffe/releases/tag/0.25.3) - 2023-01-04 <small>[Compare with 0.25.2](https://github.com/mkdocstrings/griffe/compare/0.25.2...0.25.3)</small> ### Bug Fixes - Fix parsing of annotations in Numpy attributes sections ([18fa396](https://github.com/mkdocstrings/griffe/commit/18fa39612b828e2892665b7367f7cdf76908970c) by Timothée Mazzucotelli). [Issue #72](https://github.com/mkdocstrings/griffe/issues/72) ## [0.25.2](https://github.com/mkdocstrings/griffe/releases/tag/0.25.2) - 2022-12-24 <small>[Compare with 0.25.1](https://github.com/mkdocstrings/griffe/compare/0.25.1...0.25.2)</small> ### Bug Fixes - Make sure passage through aliases is reset ([79733f4](https://github.com/mkdocstrings/griffe/commit/79733f4d03f3f66b948dc17c57404349d9e72c9a) by Timothée Mazzucotelli). [Issue #123](https://github.com/mkdocstrings/griffe/issues/123) - Ignore cyclic alias errors when updating target aliases ([bb62b2f](https://github.com/mkdocstrings/griffe/commit/bb62b2f744d221efedeba1cb33151b3787d2ee57) by Timothée Mazzucotelli). [Issue #123](https://github.com/mkdocstrings/griffe/issues/123) ## [0.25.1](https://github.com/mkdocstrings/griffe/releases/tag/0.25.1) - 2022-12-20 <small>[Compare with 0.25.0](https://github.com/mkdocstrings/griffe/compare/0.25.0...0.25.1)</small> ### Bug Fixes - Pass through aliases earlier to prevent infinite recursion ([e533f29](https://github.com/mkdocstrings/griffe/commit/e533f29258838a1e171dea702fb033bfa68ed089) by Timothée Mazzucotelli). [Issue #83](https://github.com/mkdocstrings/griffe/issues/83), [#122](https://github.com/mkdocstrings/griffe/issues/122) ## [0.25.0](https://github.com/mkdocstrings/griffe/releases/tag/0.25.0) - 2022-12-11 <small>[Compare with 0.24.1](https://github.com/mkdocstrings/griffe/compare/0.24.1...0.25.0)</small> ### Breaking changes - Parameter `only_known_modules` was renamed `external` in the [`expand_wildcards()`][griffe.GriffeLoader.expand_wildcards] method of the loader. - Exception `UnhandledEditablesModuleError` was renamed `UnhandledEditableModuleError` since we now support editable installation from other packages than `editables`. ### Highlights - Properties are now fetched as attributes rather than functions, since that is how they are used. This was asked by users, and since Griffe generates signatures for Python APIs (emphasis on **APIs**), it makes sense to return data that matches the interface provided to users. Such property objects in Griffe's output will still have the associated `property` labels of course. - Lots of bug fixes. These bugs were discovered by running Griffe on *many* major packages as well as the standard library (again). Particularly, alias resolution should be more robust now, and should generate less issues like cyclic aliases, meaning indirect/wildcard imports should be better understood. We still highly discourage the use of wildcard imports :grinning: ### Features - Support `setuptools` editable modules ([abc18f7](https://github.com/mkdocstrings/griffe/commit/abc18f7b94cea7b7850bb9f14ebc4822beb1d27c) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#463](https://github.com/mkdocstrings/mkdocstrings/issues/463) - Support merging stubs on wildcard imported objects ([0ed9c36](https://github.com/mkdocstrings/griffe/commit/0ed9c363b6b064361d311acee1732e757899291b) by Timothée Mazzucotelli). [Issue #116](https://github.com/mkdocstrings/griffe/issues/116) ### Bug Fixes - Prevent cyclic alias creation when expanding wildcards ([a77e4e8](https://github.com/mkdocstrings/griffe/commit/a77e4e8bbba8a24d9f604eaff4cc57c6851c14c3) by Timothée Mazzucotelli). - Don't crash and show hint when wildcard expansion fails ([336faf6](https://github.com/mkdocstrings/griffe/commit/336faf6dff679c970e594151a7a5d2bd99f52af6) by Timothée Mazzucotelli). - Register top module after inspection ([86454ec](https://github.com/mkdocstrings/griffe/commit/86454ececfa8e88b0f1024bde49e6dd0cb8542d0) by Timothée Mazzucotelli). - Set alias attributes early ([2ac1a9b](https://github.com/mkdocstrings/griffe/commit/2ac1a9bafb632daa491b3d26f2c39d74c9b31e3d) by Timothée Mazzucotelli). - Allow writing attributes on aliases ([c8f736e](https://github.com/mkdocstrings/griffe/commit/c8f736efcee354d2c47675413955390e80e77425) by Timothée Mazzucotelli). - Don't crash on inspection of functions signatures ([051e337](https://github.com/mkdocstrings/griffe/commit/051e337306006a60b4ae0da030a6fb912db1f05c) by Timothée Mazzucotelli). - Don't crash on inspection of method descriptors' docstrings ([09571bb](https://github.com/mkdocstrings/griffe/commit/09571bb6ffebe041ac9fdd143fc4a1cb239dda63) by Timothée Mazzucotelli). - Fix stats computing (handle stubs and namespace packages) ([a81f8dc](https://github.com/mkdocstrings/griffe/commit/a81f8dcf9e8eedc3a42cfdaaaaa28ec9379e2c4b) by Timothée Mazzucotelli). - Support documenting multiple items for optional tuples ([727456d](https://github.com/mkdocstrings/griffe/commit/727456deba90ac01a04119371b72c011755360b6) by Timothée Mazzucotelli). [Issue #117](https://github.com/mkdocstrings/griffe/issues/117) - Fix comparing names with strings ([37ae0a2](https://github.com/mkdocstrings/griffe/commit/37ae0a2f37c7e446c890d9e1204edddfb3591dc7) by Timothée Mazzucotelli). [Issue #114](https://github.com/mkdocstrings/griffe/issues/114) - Fix deepcopy crashing because of `__getattr__` ([11b023b](https://github.com/mkdocstrings/griffe/commit/11b023b8bc0575313a9aea1f6ef99944c8b02537) by Timothée Mazzucotelli). [Issue #73](https://github.com/mkdocstrings/griffe/issues/73), [PR #119](https://github.com/mkdocstrings/griffe/pull/119) ### Code Refactoring - Prevent reloading of failed modules ([8ef14ab](https://github.com/mkdocstrings/griffe/commit/8ef14ab6389bb06e1903c7628dd1d811f2af101a) by Timothée Mazzucotelli). - Rename `only_known_modules` parameter to `external` ([5f816c6](https://github.com/mkdocstrings/griffe/commit/5f816c67222f9aa1bd008782430501a2de26d5a4) by Timothée Mazzucotelli). - Rework alias creation decision in the inspector ([f434943](https://github.com/mkdocstrings/griffe/commit/f434943579e02fb02c28f7e2be65293f6ab6b657) by Timothée Mazzucotelli). - Resolve alias chain recursively ([6cdd3b2](https://github.com/mkdocstrings/griffe/commit/6cdd3b2ed4170347282118c06407b587cd65fd36) by Timothée Mazzucotelli). - Don't try to stubs-merge identical modules ([7099971](https://github.com/mkdocstrings/griffe/commit/7099971e441d5dd804c0304f010343a558685f9a) by Timothée Mazzucotelli). - Load properties as attributes ([5c97a45](https://github.com/mkdocstrings/griffe/commit/5c97a45087e0ba8c39a9745d9c5248c4c35909a8) by Timothée Mazzucotelli). [Issue mkdocstrings/python#9](https://github.com/mkdocstrings/python/issues/9) - Use a cyclic relationship map for inspection ([9a2a711](https://github.com/mkdocstrings/griffe/commit/9a2a7117d2d9d7b8327e640e8760594349531627) by Timothée Mazzucotelli). [PR #115](https://github.com/mkdocstrings/griffe/pull/115) ## [0.24.1](https://github.com/mkdocstrings/griffe/releases/tag/0.24.1) - 2022-11-18 <small>[Compare with 0.24.0](https://github.com/mkdocstrings/griffe/compare/0.24.0...0.24.1)</small> ### Bug Fixes - Support nested namespace packages ([d571f8f](https://github.com/mkdocstrings/griffe/commit/d571f8f726d50b34c84fbdaa6db3b2059cfe9dec) by Timothée Mazzucotelli). ## [0.24.0](https://github.com/mkdocstrings/griffe/releases/tag/0.24.0) - 2022-11-13 <small>[Compare with 0.23.0](https://github.com/mkdocstrings/griffe/compare/0.23.0...0.24.0)</small> The "Breaking Changes" and "Deprecations" sections are proudly written with the help of our new API breakage detection feature :smile:! Many thanks to Talley Lambert ([@tlambert03](https://github.com/tlambert03)) for the initial code allowing to compare two Griffe trees. ### Breaking Changes - All parameters of the [`load_git`][griffe.load_git] function, except `module`, are now keyword-only. - Parameter `try_relative_path` of the [`load_git`][griffe.load_git] function was removed. - Parameter `commit` was renamed `ref` in the [`load_git`][griffe.load_git] function. - Parameter `commit` was renamed `ref` in the `tmp_worktree` helper, which will probably become private later. - Parameters `ref` and `repo` switched positions in the `tmp_worktree` helper. - All parameters of the [`resolve_aliases`][griffe.GriffeLoader.resolve_aliases] method are now keyword-only. - Parameters `only_exported` and `only_known_modules` of the [`resolve_module_aliases`][griffe.GriffeLoader.resolve_module_aliases] method were removed. This method is most probably not used by anyone, and will probably be made private in the future. ### Deprecations - Parameters `only_exported` and `only_known_modules` of the [`resolve_aliases`][griffe.GriffeLoader.resolve_aliases] method are deprecated in favor of their inverted counter-part `implicit` and `external` parameters. - Example before: `loader.resolve_aliases(only_exported=True, only_known_modules=True)` - Example after: `loader.resolve_aliases(implicit=False, external=False)` ### Features - Add CLI command to check for API breakages ([90bded4](https://github.com/mkdocstrings/griffe/commit/90bded46ccaab0417ed57ed11d3b67597f3845ba) by Timothée Mazzucotelli). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #105](https://github.com/mkdocstrings/griffe/pull/105) - Add function to find API breaking changes ([a4f1280](https://github.com/mkdocstrings/griffe/commit/a4f1280a2b65fabc4caa4448d556ac3e83b2f0d0) by Talley Lambert and Timothée Mazzucotelli). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #105](https://github.com/mkdocstrings/griffe/pull/105) ### Bug Fixes - Fix labels mismatch staticmethod-classmethod in inspector ([25060f6](https://github.com/mkdocstrings/griffe/commit/25060f6dad686c73bd32203dc1b3ac789fdc4aef) by Timothée Mazzucotelli). [Issue #111](https://github.com/mkdocstrings/griffe/issues/111) - Prevent infinite loop while looking for package's parent folder ([f297f1a](https://github.com/mkdocstrings/griffe/commit/f297f1a6550ecadf77c34effe45802327340b1c4) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#72](https://github.com/mkdocstrings/mkdocstrings/issues/72) - Fix comparing names and expressions ([07bffff](https://github.com/mkdocstrings/griffe/commit/07bffff71845d3c9e66007a6a7de269f17312d2b) by Timothée Mazzucotelli). ### Code Refactoring - Rename some parameters in Git module ([9ad7a2c](https://github.com/mkdocstrings/griffe/commit/9ad7a2c1abde97556d9b4657bef4231e1ef6fa19) by Timothée Mazzucotelli). - Set parameters as keyword-only ([44c01be](https://github.com/mkdocstrings/griffe/commit/44c01bec147add34ba3f5ac716ac6722540e3ba7) by Timothée Mazzucotelli). - Remove stars from parameters names ([91dce14](https://github.com/mkdocstrings/griffe/commit/91dce14d7fa3c8c2075a3319fdd7636443fe6cbc) by Timothée Mazzucotelli). - Refactor CLI to use subcommands ([760b091](https://github.com/mkdocstrings/griffe/commit/760b0918c60911386932cec720418af8d3360c1b) by Timothée Mazzucotelli). [PR #110](https://github.com/mkdocstrings/griffe/pull/110) - Rename parameters used when resolving aliases ([3d3a4eb](https://github.com/mkdocstrings/griffe/commit/3d3a4eb99e587bd9dd7bfadca4c45737fb886139) by Timothée Mazzucotelli). ## [0.23.0](https://github.com/mkdocstrings/griffe/releases/tag/0.23.0) - 2022-10-26 <small>[Compare with 0.22.2](https://github.com/mkdocstrings/griffe/compare/0.22.2...0.23.0)</small> ### Features - Support `typing_extensions.overload` ([c29fad5](https://github.com/mkdocstrings/griffe/commit/c29fad58c721399badfc93ff8e0f10a6f92c359e) by Nyuan Zhang). [PR #108](https://github.com/mkdocstrings/griffe/pull/108) ### Bug Fixes - Log debug instead of errors when failing to parse NumPy annotations for additional sections ([568ff60](https://github.com/mkdocstrings/griffe/commit/568ff60621c0b5cc35ac0e0d0209fa3bc1b2ba8a) by Sigurd Spieckermann). [Issue #93](https://github.com/mkdocstrings/griffe/issues/93), [PR #109](https://github.com/mkdocstrings/griffe/pull/109) - Don't strip too many parentheses around a call node ([bb5c5e7](https://github.com/mkdocstrings/griffe/commit/bb5c5e71f95c537ca2d19299b157a0bbf59e5279) by Timothée Mazzucotelli). [PR #107](https://github.com/mkdocstrings/griffe/pull/107) - Guard against more alias resolution errors ([2be135d](https://github.com/mkdocstrings/griffe/commit/2be135d8ab88d6f97175c958e31e76b0d7d8f934) by Timothée Mazzucotelli). [Issue #83](https://github.com/mkdocstrings/griffe/issues/83), [PR #103](https://github.com/mkdocstrings/griffe/pull/103) ## [0.22.2](https://github.com/mkdocstrings/griffe/releases/tag/0.22.2) - 2022-09-24 <small>[Compare with 0.22.1](https://github.com/mkdocstrings/griffe/compare/0.22.1...0.22.2)</small> ### Bug Fixes - Log debug instead of errors when failing to parse Numpy annotations ([75eeeda](https://github.com/mkdocstrings/griffe/commit/75eeeda2f1181ae680b3d47df3814bad200220d3) by Timothée Mazzucotelli). [Issue #93](https://github.com/mkdocstrings/griffe/issues/93) - Don't crash on unsupported module names (containing dots) ([6a57194](https://github.com/mkdocstrings/griffe/commit/6a571949000a3d2910990337f96751c0cac7e815) by Timothée Mazzucotelli). [Issue #94](https://github.com/mkdocstrings/griffe/issues/94) - Show correct docstring line numbers on Python 3.7 ([edd4b6d](https://github.com/mkdocstrings/griffe/commit/edd4b6d23f4399960db4e16a8c269318aef033d6) by Timothée Mazzucotelli). [Issue #98](https://github.com/mkdocstrings/griffe/issues/98) - Fix parsing of Numpy docstring with an Examples section at the end ([3114727](https://github.com/mkdocstrings/griffe/commit/3114727296891fdd5cacecf487652774ee6e4fc8) by Timothée Mazzucotelli). [Issue #97](https://github.com/mkdocstrings/griffe/issues/97) - Don't crash on unsupported item in `__all__` (log a warning instead) ([9e5df0a](https://github.com/mkdocstrings/griffe/commit/9e5df0aea8e615217554e5204221a35c9df25938) by Timothée Mazzucotelli). [Issue #92](https://github.com/mkdocstrings/griffe/issues/92) - Prevent infinite recursion while expanding exports ([68446f7](https://github.com/mkdocstrings/griffe/commit/68446f7ab94536596dccb690fb2cac613cd32460) by Timothée Mazzucotelli). - Add missing check while expanding wildcards ([7e816ed](https://github.com/mkdocstrings/griffe/commit/7e816ed141d6f13bf1ae7c758c32e68cc663fe0e) by Timothée Mazzucotelli). ## [0.22.1](https://github.com/mkdocstrings/griffe/releases/tag/0.22.1) - 2022-09-10 <small>[Compare with 0.22.0](https://github.com/mkdocstrings/griffe/compare/0.22.0...0.22.1)</small> ### Bug Fixes - Always use `encoding="utf8"` when reading text files ([3b279bf](https://github.com/mkdocstrings/griffe/commit/3b279bf61afabc7312e9e58745fd19a53d97ac74) by Rudolf Byker). [Issue #99](https://github.com/mkdocstrings/griffe/issues/99), [PR #100](https://github.com/mkdocstrings/griffe/pull/100) ## [0.22.0](https://github.com/mkdocstrings/griffe/releases/tag/0.22.0) - 2022-06-28 <small>[Compare with 0.21.0](https://github.com/mkdocstrings/griffe/compare/0.21.0...0.22.0)</small> ### Features - Support forward references ([245daea](https://github.com/mkdocstrings/griffe/commit/245daeabc8130bd7ecab86f55c4906d9161b9e73) by Timothée Mazzucotelli). [Issue #86](https://github.com/mkdocstrings/griffe/issues/86) ### Code Refactoring - Safely parse annotations and values ([b023e2b](https://github.com/mkdocstrings/griffe/commit/b023e2be509f3ac39dbe1ed9adf21247e4416e53) by Timothée Mazzucotelli). ## [0.21.0](https://github.com/mkdocstrings/griffe/releases/tag/0.21.0) - 2022-06-25 <small>[Compare with 0.20.0](https://github.com/mkdocstrings/griffe/compare/0.20.0...0.21.0)</small> ### Features - Add `load_git` function allowing to load data from a specific git ref ([b2c3946](https://github.com/mkdocstrings/griffe/commit/b2c39467630c33edc914dd7e6dc96fb611267905) by Talley Lambert). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #76](https://github.com/mkdocstrings/griffe/pull/76) ### Bug Fixes - Fix detecting and merging stubs for single-file packages ([6a82542](https://github.com/mkdocstrings/griffe/commit/6a825423a9dfd86343532c2872980240f2e98b74) by Talley Lambert). [Issue #77](https://github.com/mkdocstrings/griffe/issues/77), [PR #78](https://github.com/mkdocstrings/griffe/pull/78) - Fix parsing ExtSlice nodes when getting values ([b2fe968](https://github.com/mkdocstrings/griffe/commit/b2fe9684f274786decdf9fb395bebc5057235eda) by Timothée Mazzucotelli). [Issue #87](https://github.com/mkdocstrings/griffe/issues/87) - Don't trigger alias resolution when merging stubs ([2b88627](https://github.com/mkdocstrings/griffe/commit/2b88627862b8db50045cc97ae5644abd36f36b5a) by Timothée Mazzucotelli). [Issue #89](https://github.com/mkdocstrings/griffe/issues/89) - Fix handling of .pth files ([f212dd3](https://github.com/mkdocstrings/griffe/commit/f212dd3b92f51a64795fdbb30aefd0a730393523) by Gabriel Dugny). [Issue #84](https://github.com/mkdocstrings/griffe/issues/84), [PR #85](https://github.com/mkdocstrings/griffe/pull/85) ## [0.20.0](https://github.com/mkdocstrings/griffe/releases/tag/0.20.0) - 2022-06-03 <small>[Compare with 0.19.3](https://github.com/mkdocstrings/griffe/compare/0.19.3...0.20.0)</small> ### Features - Add `as_json` and `from_json` convenience methods on objects ([5c3d751](https://github.com/mkdocstrings/griffe/commit/5c3d7511d2465e16805fa564c3d60d44618410d8) by Talley Lambert). [PR #74](https://github.com/mkdocstrings/griffe/pull/74) ### Bug Fixes - Fix unparsing of f-strings ([9ca74bd](https://github.com/mkdocstrings/griffe/commit/9ca74bd144167de9506cf5b0725a784e52f5e67a) by Timothée Mazzucotelli). [Issue #80](https://github.com/mkdocstrings/griffe/issues/80) - Don't crash when overwriting a submodule with a wildcard imported attribute ([bfad1cc](https://github.com/mkdocstrings/griffe/commit/bfad1ccf079e69fa0161754d9f1f7edd5819f943) by Timothée Mazzucotelli). [Issue #72](https://github.com/mkdocstrings/griffe/issues/72), [#79](https://github.com/mkdocstrings/griffe/issues/79), [mkdocstrings/mkdocstrings#438](https://github.com/mkdocstrings/mkdocstrings/issues/438) ## [0.19.3](https://github.com/mkdocstrings/griffe/releases/tag/0.19.3) - 2022-05-26 <small>[Compare with 0.19.2](https://github.com/mkdocstrings/griffe/compare/0.19.2...0.19.3)</small> ### Bug Fixes - Support USub and UAdd nodes in annotations ([1169c51](https://github.com/mkdocstrings/griffe/commit/1169c51bd6ae04f491fa5e50cae93d99e8ce920d) by Timothée Mazzucotelli). [Issue #71](https://github.com/mkdocstrings/griffe/issues/71) ## [0.19.2](https://github.com/mkdocstrings/griffe/releases/tag/0.19.2) - 2022-05-18 <small>[Compare with 0.19.1](https://github.com/mkdocstrings/griffe/compare/0.19.1...0.19.2)</small> ### Bug Fixes - Don't crash on single line docstrings with trailing whitespace (Google) ([8d9ccd5](https://github.com/mkdocstrings/griffe/commit/8d9ccd531dd91c6fbfa0922a0133680f881733b0) by Timothée Mazzucotelli). ## [0.19.1](https://github.com/mkdocstrings/griffe/releases/tag/0.19.1) - 2022-05-07 <small>[Compare with 0.19.0](https://github.com/mkdocstrings/griffe/compare/0.19.0...0.19.1)</small> ### Bug Fixes - Don't crash on nested functions in `__init__` methods ([cd5af43](https://github.com/mkdocstrings/griffe/commit/cd5af43f3a98d54d822015818b7aa0ef15159286) by Timothée Mazzucotelli). [Issue #68](https://github.com/mkdocstrings/griffe/issues/68) ## [0.19.0](https://github.com/mkdocstrings/griffe/releases/tag/0.19.0) - 2022-05-06 <small>[Compare with 0.18.0](https://github.com/mkdocstrings/griffe/compare/0.18.0...0.19.0)</small> ### Features - Add `load` shortcut function for convenience ([f38a42d](https://github.com/mkdocstrings/griffe/commit/f38a42ddd7ac9d58f36627d9f2a69f4acd65df50) by Timothée Mazzucotelli). - Support loading (and merging) `*.pyi` files ([41518f4](https://github.com/mkdocstrings/griffe/commit/41518f4aa9e00756a910067cf6f01f07ca7327da) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#404](https://github.com/mkdocstrings/mkdocstrings/issues/404) - Improve support for call nodes in annotations ([45e5bf5](https://github.com/mkdocstrings/griffe/commit/45e5bf53d509344b3f28118836d356903c64bbf3) by Timothée Mazzucotelli). [Issue #66](https://github.com/mkdocstrings/griffe/issues/66) - Support `dataclass` decorators on classes ([f579431](https://github.com/mkdocstrings/griffe/commit/f579431474cc4db687e4264f5062074654dec2f3) by Timothée Mazzucotelli). ### Code Refactoring - Handle absence of values ([190585d](https://github.com/mkdocstrings/griffe/commit/190585d3482bfc3a72694910529b7a0aac35444c) by Timothée Mazzucotelli). - Simplify decorators to labels function ([04e768f](https://github.com/mkdocstrings/griffe/commit/04e768fb621898faf7a96cc7e7170f10da876664) by Timothée Mazzucotelli). - Always sort labels when serializing ([bd2504b](https://github.com/mkdocstrings/griffe/commit/bd2504bdb43df3e290c88bd8d25903823f5fc2d6) by Timothée Mazzucotelli). ## [0.18.0](https://github.com/mkdocstrings/griffe/releases/tag/0.18.0) - 2022-04-19 <small>[Compare with 0.17.0](https://github.com/mkdocstrings/griffe/compare/0.17.0...0.18.0)</small> ### Features - Add CLI option to disallow inspection ([8f71a07](https://github.com/mkdocstrings/griffe/commit/8f71a07c17de4cfb2b519dc2b4086f102de4d325) by Timothée Mazzucotelli). - Support complex `__all__` assignments ([9a2128b](https://github.com/mkdocstrings/griffe/commit/9a2128b8d4533119b705ec47fc1eca404b4282ef) by Timothée Mazzucotelli). [Issue #40](https://github.com/mkdocstrings/griffe/issues/40) - Inherit class parameters from `__init__` method ([e195593](https://github.com/mkdocstrings/griffe/commit/e195593b181690313c9e447c8bc2befa72fd6e09) by François Rozet). [Issue mkdocstrings/python#19](https://github.com/mkdocstrings/python/issues/19), [PR #65](https://github.com/mkdocstrings/python/pull/65). It allows to write "Parameters" sections in the docstring of the class itself. ### Performance Improvements - Avoid using `__len__` as boolean method ([d465493](https://github.com/mkdocstrings/griffe/commit/d4654930577186fb6d3e89ea1561a2daf15b3a65) by Timothée Mazzucotelli). ### Bug Fixes - Don't crash on unhandle `__all__` assignments ([cbc103c](https://github.com/mkdocstrings/griffe/commit/cbc103c91836db2e235a46a0f9048c1230de507d) by Timothée Mazzucotelli). - Handle empty packages names in CLI ([52b51c4](https://github.com/mkdocstrings/griffe/commit/52b51c49a14783c986beb851abd33cbcd0ab8729) by Timothée Mazzucotelli). - Don't crash on Google parameters sections found in non-function docstrings ([4a417bc](https://github.com/mkdocstrings/griffe/commit/4a417bc6c0e83b42fe1a74a4a8b0881d3955075f) by Timothée Mazzucotelli). [Issue mkdocstrings/python#19](https://github.com/mkdocstrings/python/issues/19) ### Code Refactoring - Improve "unknown parameter" messages ([7191799](https://github.com/mkdocstrings/griffe/commit/7191799c92d7544f949c5870cf2867e02d406c57) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#423](https://github.com/mkdocstrings/mkdocstrings/issues/423) - Set property label on `@cached_property`-decorated methods ([bc068f8](https://github.com/mkdocstrings/griffe/commit/bc068f8123c5bcbe4dce272dda52840019141b06) by Timothée Mazzucotelli). ## [0.17.0](https://github.com/mkdocstrings/griffe/releases/tag/0.17.0) - 2022-04-15 <small>[Compare with 0.16.0](https://github.com/mkdocstrings/griffe/compare/0.16.0...0.17.0)</small> ### Features - Handle properties setters and deleters ([50a4490](https://github.com/mkdocstrings/griffe/commit/50a449069de89bb83da854b1bbd1681ec68f0395) by Timothée Mazzucotelli). - Handle `typing.overload` decorator ([927bbd9](https://github.com/mkdocstrings/griffe/commit/927bbd9fe7712e8d0fc9763fb51d89bef3173350) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#308](https://github.com/mkdocstrings/mkdocstrings/issues/308) - Set labels on functions using decorators ([1c1feb2](https://github.com/mkdocstrings/griffe/commit/1c1feb264c748f4a78ffebf3b9ea1966f2533522) by Timothée Mazzucotelli). [Issue #47](https://github.com/mkdocstrings/griffe/issues/47) - Add `runtime` attribute to objects/aliases and handle type guarded objects ([2f2a04e](https://github.com/mkdocstrings/griffe/commit/2f2a04ea498aa50133b1404f3bc3498a25648545) by Timothée Mazzucotelli). [Issue #42](https://github.com/mkdocstrings/griffe/issues/42) - Support pkg-style namespace packages ([efba0c6](https://github.com/mkdocstrings/griffe/commit/efba0c6a5e1dc185e96e5a09c05e94c751abc4cb) by Timothée Mazzucotelli). [Issue #58](https://github.com/mkdocstrings/griffe/issues/58) ### Code Refactoring - Remove useless attribute ([c4a92b7](https://github.com/mkdocstrings/griffe/commit/c4a92b7e2cbe240a376d5d6944b7b0d23255648b) by Timothée Mazzucotelli). - Improve Google warnings ([641089a](https://github.com/mkdocstrings/griffe/commit/641089aed53423894df8733941e404f7e6505b94) by Timothée Mazzucotelli). - Remove useless import nodes generic visits ([f83fc8e](https://github.com/mkdocstrings/griffe/commit/f83fc8e629451abd4f4eadfe34b448fb3b77b9b6) by Timothée Mazzucotelli). ## [0.16.0](https://github.com/mkdocstrings/griffe/releases/tag/0.16.0) - 2022-04-09 <small>[Compare with 0.15.1](https://github.com/mkdocstrings/griffe/compare/0.15.1...0.16.0)</small> ### Features - Warn about unknown parameters in Numpy docstrings ([23f63f2](https://github.com/mkdocstrings/griffe/commit/23f63f255eef5aa2dbaa1765f93634ecaf94dbb3) by Timothée Mazzucotelli). - Warn about unknown parameters in Google docstrings ([72be993](https://github.com/mkdocstrings/griffe/commit/72be993c95460a6465a4e70a95b79ae4095db541) by Kevin Musgrave). [Issue mkdocstrings/mkdocstrings#408](https://github.com/mkdocstrings/mkdocstrings/issues/408), [PR #63](https://github.com/mkdocstrings/griffe/issues/63) ### Bug Fixes - Don't crash on unhandled AST nodes while parsing text annotations ([f3be3a6](https://github.com/mkdocstrings/griffe/commit/f3be3a68141e24a9c0c6b9a87e3f22e75a168d80) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#416](https://github.com/mkdocstrings/mkdocstrings/issues/416) ## [0.15.1](https://github.com/mkdocstrings/griffe/releases/tag/0.15.1) - 2022-04-08 <small>[Compare with 0.15.0](https://github.com/mkdocstrings/griffe/compare/0.15.0...0.15.1)</small> ### Bug Fixes - Don't overwrite existing (lower) members when expanding wildcards ([9ff86e3](https://github.com/mkdocstrings/griffe/commit/9ff86e369d8fb3a6eeb7d94cd60c87fa26bf74b4) by Timothée Mazzucotelli). - Don't insert admonition before current section (Google parser) ([8d8a46f](https://github.com/mkdocstrings/griffe/commit/8d8a46fca7df917c4bba979128d94d3b79252ff5) by Timothée Mazzucotelli). - Handle aliases chains in `has_docstrings` method ([77c6943](https://github.com/mkdocstrings/griffe/commit/77c69430ddc74fedaa33fa65afd59ac546900829) by Timothée Mazzucotelli). - Actually check for docstrings recursively ([15f4193](https://github.com/mkdocstrings/griffe/commit/15f4193b764f85dcab042ab193e984bebf151029) by Timothée Mazzucotelli). ## [0.15.0](https://github.com/mkdocstrings/griffe/releases/tag/0.15.0) - 2022-04-03 <small>[Compare with 0.14.1](https://github.com/mkdocstrings/griffe/compare/0.14.1...0.15.0)</small> ### Features - Support `ignore_init_summary` in Numpy parser ([f8cd147](https://github.com/mkdocstrings/griffe/commit/f8cd14734603d29e6e72c9a350f663dccdeb36b4) by Timothée Mazzucotelli). [Issue #44](https://github.com/mkdocstrings/griffe/issues/44) - Enable cross-references for Numpy docstrings annotations ([e32a73c](https://github.com/mkdocstrings/griffe/commit/e32a73c9e100cf0778768c4a1f76152d9aecc451) by Timothée Mazzucotelli). Issues [#11](https://github.com/mkdocstrings/griffe/issues/11), [#12](https://github.com/mkdocstrings/griffe/issues/12), [#13](https://github.com/mkdocstrings/griffe/issues/13), [#14](https://github.com/mkdocstrings/griffe/issues/14), [#15](https://github.com/mkdocstrings/griffe/issues/15), [#16](https://github.com/mkdocstrings/griffe/issues/16), [#17](https://github.com/mkdocstrings/griffe/issues/17), [#18](https://github.com/mkdocstrings/griffe/issues/18) - Retrieve annotations from parent in Numpy parser ([8d4eae3](https://github.com/mkdocstrings/griffe/commit/8d4eae353cbd42f47fe6f8101e6e1f8be4054c84) by Timothée Mazzucotelli). Issues [#29](https://github.com/mkdocstrings/griffe/issues/29), [#30](https://github.com/mkdocstrings/griffe/issues/30), [#31](https://github.com/mkdocstrings/griffe/issues/31), [#32](https://github.com/mkdocstrings/griffe/issues/32) - Parse annotations in Iterator/Generator for Google docstrings ([f0129ef](https://github.com/mkdocstrings/griffe/commit/f0129efa2046089355ee62c48f23eb0189b054ce) by Timothée Mazzucotelli). [Issue #28](https://github.com/mkdocstrings/griffe/issues/28) ### Bug Fixes - Fix missing "receives" entry in Google parser ([35d63fb](https://github.com/mkdocstrings/griffe/commit/35d63fbd566fa439a255c3f44ffeb4a9474db7f9) by Timothée Mazzucotelli). - Fix serialization of Windows paths ([b7e8da8](https://github.com/mkdocstrings/griffe/commit/b7e8da868cd6ec8230f2d58a8f3c38248f7c97b2) by Timothée Mazzucotelli). ### Code Refactoring - Be less strict on spacing around ":" in Numpy docstrings ([aa592b5](https://github.com/mkdocstrings/griffe/commit/aa592b5f38b71e6eadd883257d2239fceec43752) by Timothée Mazzucotelli). - Be less strict in Numpy regular expressions ([603dc0e](https://github.com/mkdocstrings/griffe/commit/603dc0e21aa12754ec4f76ffc40869bf8519935d) by Timothée Mazzucotelli). - Rename variables in Numpy module ([4407244](https://github.com/mkdocstrings/griffe/commit/4407244a2e4b59c988c61e4c7b9f07532cad5b3c) by Timothée Mazzucotelli). ## [0.14.1](https://github.com/mkdocstrings/griffe/releases/tag/0.14.1) - 2022-04-01 <small>[Compare with 0.14.0](https://github.com/mkdocstrings/griffe/compare/0.14.0...0.14.1)</small> ### Bug Fixes - Retrieve default value for non-string parameters ([15952ed](https://github.com/mkdocstrings/griffe/commit/15952ed72f6f5db3a4dec2fc60cb256c838be6a3) by ThomasPJ). [Issue #59](https://github.com/mkdocstrings/griffe/issues/59), [issue mkdocstrings/python#8](https://github.com/mkdocstrings/python/issues/8), [PR #60](https://github.com/mkdocstrings/griffe/pull/60) - Prevent infinite recursion while expanding wildcards ([428628f](https://github.com/mkdocstrings/griffe/commit/428628f423192611529b9b346cd295999d0dad25) by Timothée Mazzucotelli). [Issue #57](https://github.com/mkdocstrings/griffe/issues/57) ## [0.14.0](https://github.com/mkdocstrings/griffe/releases/tag/0.14.0) - 2022-03-06 <small>[Compare with 0.13.2](https://github.com/mkdocstrings/griffe/compare/0.13.2...0.14.0)</small> ### Features - Ignore `__doc__` from parent classes ([10aa59e](https://github.com/mkdocstrings/griffe/commit/10aa59ef2fbf1db2c8829e0905bea88406495c41) by Will Da Silva). [Issue #55](https://github.com/mkdocstrings/griffe/issues/55), [PR #56](https://github.com/mkdocstrings/griffe/pull/56) ## [0.13.2](https://github.com/mkdocstrings/griffe/releases/tag/0.13.2) - 2022-03-01 <small>[Compare with 0.13.1](https://github.com/mkdocstrings/griffe/compare/0.13.1...0.13.2)</small> ### Bug Fixes - Fix type regex in Numpy parser ([3a10fda](https://github.com/mkdocstrings/griffe/commit/3a10fda89c2e32e2d8acd89eb1ce8ab20a0fc251) by Timothée Mazzucotelli). - Current module must not be available in its members' scope ([54f9688](https://github.com/mkdocstrings/griffe/commit/54f9688c11a1f7d3893ca774a07afe876f0b809c) by Timothée Mazzucotelli). - Allow named sections after numpydoc examples ([a44d9c6](https://github.com/mkdocstrings/griffe/commit/a44d9c65cf24d2820e805d23365f38aab82c8c07) by Lucina). [PR #54](https://github.com/mkdocstrings/griffe/pull/54) ## [0.13.1](https://github.com/mkdocstrings/griffe/releases/tag/0.13.1) - 2022-02-24 <small>[Compare with 0.13.0](https://github.com/mkdocstrings/griffe/compare/0.13.0...0.13.1)</small> ### Bug Fixes - Don't cut through wildcard-expanded aliases chains ([65dafa4](https://github.com/mkdocstrings/griffe/commit/65dafa4660e8c95687cad4d5c5145a56f126ae61) by Timothée Mazzucotelli). - Fix docstrings warnings when there's no parent module ([e080549](https://github.com/mkdocstrings/griffe/commit/e080549e3eaf887a0f037a4457329eab35bd6409) by Timothée Mazzucotelli). [Issue #51](https://github.com/mkdocstrings/griffe/issues/51) ### Code Refactoring - Use proper classes for docstrings sections ([46eddac](https://github.com/mkdocstrings/griffe/commit/46eddac0b847eeb75e4964a3186069f7698235b0) by Timothée Mazzucotelli). [Issue mkdocstrings/python#3](https://github.com/mkdocstrings/python/issues/3), [PR #52](https://github.com/mkdocstrings/griffe/pull/52) ## [0.13.0](https://github.com/mkdocstrings/griffe/releases/tag/0.13.0) - 2022-02-23 <small>[Compare with 0.12.6](https://github.com/mkdocstrings/griffe/compare/0.12.6...0.13.0)</small> ### Features - Implement `trim_doctest_flags` for Google and Numpy ([8057153](https://github.com/mkdocstrings/griffe/commit/8057153823711d8f486b1c52469090ce404771cb) by Jeremy Goh). [Issue mkdocstrings/mkdocstrings#386](https://github.com/mkdocstrings/mkdocstrings/issues/386), [PR #48](https://github.com/mkdocstrings/griffe/pull/48) ### Bug Fixes - Rename keyword parameters to keyword arguments ([ce3eb6b](https://github.com/mkdocstrings/griffe/commit/ce3eb6b5d7caad6df41496dd300924535d92dc7f) by Jeremy Goh). ## [0.12.6](https://github.com/mkdocstrings/griffe/releases/tag/0.12.6) - 2022-02-18 <small>[Compare with 0.12.5](https://github.com/mkdocstrings/griffe/compare/0.12.5...0.12.6)</small> ### Bug Fixes - Support starred parameters in Numpy docstrings ([27f0fc2](https://github.com/mkdocstrings/griffe/commit/27f0fc21299a41a3afc07b46afbe8f37757c3918) by Timothée Mazzucotelli). [Issue #43](https://github.com/mkdocstrings/griffe/issues/43) ## [0.12.5](https://github.com/mkdocstrings/griffe/releases/tag/0.12.5) - 2022-02-17 <small>[Compare with 0.12.4](https://github.com/mkdocstrings/griffe/compare/0.12.4...0.12.5)</small> ### Bug Fixes - Fix getting line numbers on aliases ([351750e](https://github.com/mkdocstrings/griffe/commit/351750ea70d0ab3f10c2766846c10d00612cda1d) by Timothée Mazzucotelli). ## [0.12.4](https://github.com/mkdocstrings/griffe/releases/tag/0.12.4) - 2022-02-16 <small>[Compare with 0.12.3](https://github.com/mkdocstrings/griffe/compare/0.12.3...0.12.4)</small> ### Bug Fixes - Update target path when changing alias target ([5eda646](https://github.com/mkdocstrings/griffe/commit/5eda646f7bc2fdb112887fdeaa07f8a2f4635c12) by Timothée Mazzucotelli). - Fix relative imports to absolute with wildcards ([69500dd](https://github.com/mkdocstrings/griffe/commit/69500dd0ce06f4acc91eb60ff20ac8d79303a281) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) - Fix accessing members using tuples ([87ff1df](https://github.com/mkdocstrings/griffe/commit/87ff1dfae93d9eb6f735f9c1290092d61cac7591) by Timothée Mazzucotelli). - Fix recursive wildcard expansion ([60e6edf](https://github.com/mkdocstrings/griffe/commit/60e6edf9dcade104b069946380a0d1dcc22bce9a) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) - Only export submodules if they were imported ([98c72db](https://github.com/mkdocstrings/griffe/commit/98c72dbab114fd7782efd6f2f9bbf78e3f4ccb27) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) ## [0.12.3](https://github.com/mkdocstrings/griffe/releases/tag/0.12.3) - 2022-02-15 <small>[Compare with 0.12.2](https://github.com/mkdocstrings/griffe/compare/0.12.2...0.12.3)</small> ### Bug Fixes - Always decode source as UTF8 ([563469b](https://github.com/mkdocstrings/griffe/commit/563469b4cf320ea38096846312dc757a614d8094) by Timothée Mazzucotelli). - Fix JSON encoder and decoder ([3e768d6](https://github.com/mkdocstrings/griffe/commit/3e768d6574a45624237e0897c1d6a6c87e446016) by Timothée Mazzucotelli). ### Code Refactoring - Improve error handling ([7b15a51](https://github.com/mkdocstrings/griffe/commit/7b15a51fb9dd4722757f272f00402ce29ef2bd3f) by Timothée Mazzucotelli). ## [0.12.2](https://github.com/mkdocstrings/griffe/releases/tag/0.12.2) - 2022-02-13 <small>[Compare with 0.12.1](https://github.com/mkdocstrings/griffe/compare/0.12.1...0.12.2)</small> ### Bug Fixes - Fix JSON unable to serialize docstring kind values ([91e6719](https://github.com/mkdocstrings/griffe/commit/91e67190fc4f69911ad6ea3eb239a74fc1f15ba6) by Timothée Mazzucotelli). ### Code Refactoring - Make attribute labels more explicit ([19eac2e](https://github.com/mkdocstrings/griffe/commit/19eac2e5a13d77175849c199ba3337a66e3824a2) by Timothée Mazzucotelli). ## [0.12.1](https://github.com/mkdocstrings/griffe/releases/tag/0.12.1) - 2022-02-12 <small>[Compare with 0.11.7](https://github.com/mkdocstrings/griffe/compare/0.11.7...0.12.1)</small> ### Features - Add `ignore_init_summary` option to the Google parser ([81f0333](https://github.com/mkdocstrings/griffe/commit/81f0333b1691955f6020095051b2cf869f0c2c24) by Timothée Mazzucotelli). - Add `is_KIND` properties on objects ([17a08cd](https://github.com/mkdocstrings/griffe/commit/17a08cd7142bdee041577735d5e5ac246c181ec9) by Timothée Mazzucotelli). ## [0.11.7](https://github.com/mkdocstrings/griffe/releases/tag/0.11.7) - 2022-02-12 <small>[Compare with 0.11.6](https://github.com/mkdocstrings/griffe/compare/0.11.6...0.11.7)</small> ### Bug Fixes - Keep only first assignment in conditions ([0104440](https://github.com/mkdocstrings/griffe/commit/010444018ca6ba437e70166e0da3e2d2ca6bbbe8) by Timothée Mazzucotelli). - Support invert unary op in annotations ([734ef55](https://github.com/mkdocstrings/griffe/commit/734ef551f5c5b2b4b48de32033d4c2e7cff0a124) by Timothée Mazzucotelli). - Fix handling of missing modules during dynamic imports ([7a3b383](https://github.com/mkdocstrings/griffe/commit/7a3b38349712c5b66792da1a8a9efae1b6f663a7) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#380](https://github.com/mkdocstrings/mkdocstrings/issues/380) - Fix getting lines of compiled modules ([899461b](https://github.com/mkdocstrings/griffe/commit/899461b2f48622f334ceeaa6d73c935bacb540ea) by Timothée Mazzucotelli). ### Code Refactoring - Get annotation with the same property on functions ([ecc7bba](https://github.com/mkdocstrings/griffe/commit/ecc7bba8880f90417a21830e0e9cccf30f582399) by Timothée Mazzucotelli). ## [0.11.6](https://github.com/mkdocstrings/griffe/releases/tag/0.11.6) - 2022-02-10 <small>[Compare with 0.11.5](https://github.com/mkdocstrings/griffe/compare/0.11.5...0.11.6)</small> ### Bug Fixes - Fix infinite loop in Google parser ([8b7b97b](https://github.com/mkdocstrings/griffe/commit/8b7b97b6f507dc91b957592e1d247d79bd3e9a5b) by Timothée Mazzucotelli). [Issue #38](https://github.com/mkdocstrings/griffe/issues/38) ## [0.11.5](https://github.com/mkdocstrings/griffe/releases/tag/0.11.5) - 2022-02-08 <small>[Compare with 0.11.4](https://github.com/mkdocstrings/griffe/compare/0.11.4...0.11.5)</small> ### Bug Fixes - Fix building title and kind of Google admonitions ([87ab56c](https://github.com/mkdocstrings/griffe/commit/87ab56cfe5458b313527bc2eb47ea418fcb231ab) by Timothée Mazzucotelli). [Issue mkdocstrings#379](https://github.com/mkdocstrings/mkdocstrings/issues/379) ## [0.11.4](https://github.com/mkdocstrings/griffe/releases/tag/0.11.4) - 2022-02-07 <small>[Compare with 0.11.3](https://github.com/mkdocstrings/griffe/compare/0.11.3...0.11.4)</small> ### Bug Fixes - Don't trigger alias resolution while checking docstrings presence ([dda72ea](https://github.com/mkdocstrings/griffe/commit/dda72ea56b091d1c9bc1b7aa369548328894da29) by Timothée Mazzucotelli). [Issue #37](https://github.com/mkdocstrings/griffe/issues/37) ## [0.11.3](https://github.com/mkdocstrings/griffe/releases/tag/0.11.3) - 2022-02-05 <small>[Compare with 0.11.2](https://github.com/mkdocstrings/griffe/compare/0.11.2...0.11.3)</small> ### Bug Fixes - Fix getting params defaults on Python 3.7 ([0afd867](https://github.com/mkdocstrings/griffe/commit/0afd8675d2d24302d68619f31adbe5ac5d8ff5a7) by Timothée Mazzucotelli). ## [0.11.2](https://github.com/mkdocstrings/griffe/releases/tag/0.11.2) - 2022-02-03 <small>[Compare with 0.11.1](https://github.com/mkdocstrings/griffe/compare/0.11.1...0.11.2)</small> ### Code Refactoring - Factorize docstring annotation parser ([19609be](https://github.com/mkdocstrings/griffe/commit/19609bede6227998a1322dbed6fcc1ae2e924bc8) by Timothée Mazzucotelli). ## [0.11.1](https://github.com/mkdocstrings/griffe/releases/tag/0.11.1) - 2022-02-01 <small>[Compare with 0.11.0](https://github.com/mkdocstrings/griffe/compare/0.11.0...0.11.1)</small> ### Code Refactoring - Rename RST parser to Sphinx ([a612cb1](https://github.com/mkdocstrings/griffe/commit/a612cb1c8d52fabe5a1ebaf892e9b82c67d15a30) by Timothée Mazzucotelli). ## [0.11.0](https://github.com/mkdocstrings/griffe/releases/tag/0.11.0) - 2022-01-31 <small>[Compare with 0.10.0](https://github.com/mkdocstrings/griffe/compare/0.10.0...0.11.0)</small> ### Features - Support matrix multiplication operator in visitor ([6129e17](https://github.com/mkdocstrings/griffe/commit/6129e17c86ff49a8e539039dcd04a58b30e3648e) by Timothée Mazzucotelli). ### Bug Fixes - Fix name resolution for inspected data ([ed3e7e5](https://github.com/mkdocstrings/griffe/commit/ed3e7e5fa8a9d702c92f47e8244635cf11a923f2) by Timothée Mazzucotelli). - Make importer actually able to import any nested object ([d007219](https://github.com/mkdocstrings/griffe/commit/d00721971c7b820e16e463408f04cc3e81a14db6) by Timothée Mazzucotelli). ### Code Refactoring - Always use search paths to import modules ([a9a378f](https://github.com/mkdocstrings/griffe/commit/a9a378fc6e47678e08a22383879e4d01acd16b54) by Timothée Mazzucotelli). - Split out module finder ([7290642](https://github.com/mkdocstrings/griffe/commit/7290642e36341e64b8ed770e237e9f232e05eada) by Timothée Mazzucotelli). ## [0.10.0](https://github.com/mkdocstrings/griffe/releases/tag/0.10.0) - 2022-01-14 <small>[Compare with 0.9.0](https://github.com/mkdocstrings/griffe/compare/0.9.0...0.10.0)</small> ### Bug Fixes - Fix infinite recursion errors in alias resolver ([133b4e4](https://github.com/mkdocstrings/griffe/commit/133b4e4bf721fc7536a1ca957f13f7c9f83bf07a) by Timothée Mazzucotelli). - Fix inspection of nodes children (aliases or not) ([bb354f2](https://github.com/mkdocstrings/griffe/commit/bb354f21e7b079f4c1e8dd50297d53810c18450e) by Timothée Mazzucotelli). - Fix relative to absolute import conversion ([464c39e](https://github.com/mkdocstrings/griffe/commit/464c39eaa812a927190469b18bd910e95e3c1d3c) by Timothée Mazzucotelli). ### Code Refactoring - Rename some CLI options ([1323268](https://github.com/mkdocstrings/griffe/commit/13232685b0f2752d92428ab786d428d0af11743b) by Timothée Mazzucotelli). - Return the loader the to main function ([9c6317e](https://github.com/mkdocstrings/griffe/commit/9c6317e5afa25dd11d18906503b8010046878868) by Timothée Mazzucotelli). - Improve logging messages ([b8eb16e](https://github.com/mkdocstrings/griffe/commit/b8eb16e0fedfe50f2c3ad65e326f4dc6e6918ac0) by Timothée Mazzucotelli). - Skip inspection of some debug packages ([4ee8968](https://github.com/mkdocstrings/griffe/commit/4ee896864f1227e32d40571da03f7894c9404579) by Timothée Mazzucotelli). - Return ... instead of Ellipsis ([f9ae31d](https://github.com/mkdocstrings/griffe/commit/f9ae31d0f4c904a89c7f581aaa031692740edaef) by Timothée Mazzucotelli). - Catch attribute errors when cross-referencing docstring annotations ([288803a](https://github.com/mkdocstrings/griffe/commit/288803a3be93c4e077576ed36dded2a76ce33955) by Timothée Mazzucotelli). - Support dict methods in lines collection ([1b0cb94](https://github.com/mkdocstrings/griffe/commit/1b0cb945dba619df7ce1358f7961e4bd80f70218) by Timothée Mazzucotelli). ### Features - Compute and show some stats ([1b8d0a1](https://github.com/mkdocstrings/griffe/commit/1b8d0a1c91e03dfa5f92ad9c6dff02863a43fc01) by Timothée Mazzucotelli). - Add CLI options for alias resolution ([87a59cb](https://github.com/mkdocstrings/griffe/commit/87a59cb7af5f8e7df9ddba41fb4a4b65cb264481) by Timothée Mazzucotelli). - Support Google raises annotations cross-refs ([8006ae1](https://github.com/mkdocstrings/griffe/commit/8006ae13bc27d117ce6b8fdc8ac91dc8541a670f) by Timothée Mazzucotelli). ## [0.9.0](https://github.com/mkdocstrings/griffe/releases/tag/0.9.0) - 2022-01-04 <small>[Compare with 0.8.0](https://github.com/mkdocstrings/griffe/compare/0.8.0...0.9.0)</small> ### Features - Loader option to only follow aliases in known modules ([879d91b](https://github.com/mkdocstrings/griffe/commit/879d91b4c50832620ce6ee7bdcc85107a6df9a1f) by Timothée Mazzucotelli). - Use aliases when inspecting too ([60439ee](https://github.com/mkdocstrings/griffe/commit/60439eefb4635e58e4bd898e5565eab48a5c91d0) by Timothée Mazzucotelli). ### Bug Fixes - Handle more errors when loading modules ([1aa571a](https://github.com/mkdocstrings/griffe/commit/1aa571a112e3b2ca955c23f2eef97b36f34bcd8c) by Timothée Mazzucotelli). - Handle more errors when getting signature ([2db85e7](https://github.com/mkdocstrings/griffe/commit/2db85e7f655c1e383ba310f40195844c2867e1b9) by Timothée Mazzucotelli). - Fix checking parent truthfulness ([6129e50](https://github.com/mkdocstrings/griffe/commit/6129e50331f6e36bcbee2e07b871abee45f7e872) by Timothée Mazzucotelli). - Fix getting subscript value ([1699f12](https://github.com/mkdocstrings/griffe/commit/1699f121adc13fcc48f81f46dfca85946e2fb74f) by Timothée Mazzucotelli). - Support yield nodes ([7d536d5](https://github.com/mkdocstrings/griffe/commit/7d536d58ffc0faa4caf43f09194d88c35fc47704) by Timothée Mazzucotelli). - Exclude some special low-level members that cause cyclic issues ([b54ab34](https://github.com/mkdocstrings/griffe/commit/b54ab346308bb24cba66be9c8f1ee8599481381d) by Timothée Mazzucotelli). - Fix transforming elements of signatures to annotations ([e278c11](https://github.com/mkdocstrings/griffe/commit/e278c1102b2762b74bf6b83a2e97a5f87b566e2e) by Timothée Mazzucotelli). - Detect cyclic aliases and prevent resolution errors ([de5dd12](https://github.com/mkdocstrings/griffe/commit/de5dd12240acf8a203a86b04e458ce33b67ced9e) by Timothée Mazzucotelli). - Don't crash while trying to get the representation of an attribute value ([77ac55d](https://github.com/mkdocstrings/griffe/commit/77ac55d5033e83790c79f3303fdbd05ea66ab729) by Timothée Mazzucotelli). - Fix building value for joined strings ([6154b69](https://github.com/mkdocstrings/griffe/commit/6154b69b6da5d63c508ec5095aebe487e491b553) by Timothée Mazzucotelli). - Fix prevention of cycles while building objects nodes ([48062ac](https://github.com/mkdocstrings/griffe/commit/48062ac1f8356099b8e0e1069e4321a467073d33) by Timothée Mazzucotelli). - Better handle relative imports ([91b42de](https://github.com/mkdocstrings/griffe/commit/91b42dea73c035b2dc20db1e328a53960c51a645) by Timothée Mazzucotelli). - Fix Google parser missing lines ending with colon ([2f7969c](https://github.com/mkdocstrings/griffe/commit/2f7969ccbf91b63ae22deb742250068c114fe1a9) by Timothée Mazzucotelli). ### Code Refactoring - Improve alias resolution robustness ([e708139](https://github.com/mkdocstrings/griffe/commit/e708139c9bd19be320bdb279310560212872326f) by Timothée Mazzucotelli). - Remove async loader for now ([acc5ecf](https://github.com/mkdocstrings/griffe/commit/acc5ecf2bb45dcebdd56d763a657a1075c4a3002) by Timothée Mazzucotelli). - Improve handling of Google admonitions ([8aa5ed0](https://github.com/mkdocstrings/griffe/commit/8aa5ed0be4f1902dbdfbce9b4a9c7ac619418d43) by Timothée Mazzucotelli). - Better handling of import errors and system exits while inspecting modules ([7ba1589](https://github.com/mkdocstrings/griffe/commit/7ba1589552fb37fba3c2f3093058e135a6e48a27) by Timothée Mazzucotelli). - Empty generic visit/inspect methods in base classes ([338760e](https://github.com/mkdocstrings/griffe/commit/338760ea2189e74577250b8c3f4ffe91f81e6b6e) by Timothée Mazzucotelli). ## [0.8.0](https://github.com/mkdocstrings/griffe/releases/tag/0.8.0) - 2022-01-02 <small>[Compare with 0.7.1](https://github.com/mkdocstrings/griffe/compare/0.7.1...0.8.0)</small> ### Features - Support getting attribute annotation from parent in RST docstring parser ([25db61a](https://github.com/mkdocstrings/griffe/commit/25db61ab01042ad797ac5cdea0b2f7e2382191c1) by Timothée Mazzucotelli). - Handle relative imports ([62b0927](https://github.com/mkdocstrings/griffe/commit/62b0927516ca345de61aa3cc03e977d4d37220de) by Timothée Mazzucotelli). - Support wildcard imports ([77a3cb7](https://github.com/mkdocstrings/griffe/commit/77a3cb7e4198dc2e2cea953c5f621544b564552c) by Timothée Mazzucotelli). - Support configuring log level (CLI/env var) ([839d78e](https://github.com/mkdocstrings/griffe/commit/839d78ea302df004fba1b6fad9eb84d861f0f4aa) by Timothée Mazzucotelli). - Support loading `*.py[cod]` and `*.so` modules ([cd98a6f](https://github.com/mkdocstrings/griffe/commit/cd98a6f3afbbf8f6a176aa7780a8b916a9ee64f2) by Timothée Mazzucotelli). - Support inspecting builtin functions/methods ([aa1fce3](https://github.com/mkdocstrings/griffe/commit/aa1fce330ce3e2af4dd9a3c43827637d1e220dde) by Timothée Mazzucotelli). ### Code Refactoring - Handle extensions errors ([11278ca](https://github.com/mkdocstrings/griffe/commit/11278caea27e9f91a1dc9cc160414f01b24f5354) by Timothée Mazzucotelli). - Don't always try to find a module as a relative path ([e6df277](https://github.com/mkdocstrings/griffe/commit/e6df2774bfd631fd9a09913480b4d61d137bc0c6) by Timothée Mazzucotelli). - Improve loggers patching ([f4b262a](https://github.com/mkdocstrings/griffe/commit/f4b262ab5a3d874591324adc2b5ffff214c7e7da) by Timothée Mazzucotelli). - Improve dynamic imports ([2998195](https://github.com/mkdocstrings/griffe/commit/299819519b7eb9b07b938d22bfb3a27e3b05095d) by Timothée Mazzucotelli). ## [0.7.1](https://github.com/mkdocstrings/griffe/releases/tag/0.7.1) - 2021-12-28 <small>[Compare with 0.7.0](https://github.com/mkdocstrings/griffe/compare/0.7.0...0.7.1)</small> ### Code Refactoring - Only log warning if async mode is used ([356e848](https://github.com/mkdocstrings/griffe/commit/356e848c8e233334401461b02a0188731b71a8cf) by Timothée Mazzucotelli). ## [0.7.0](https://github.com/mkdocstrings/griffe/releases/tag/0.7.0) - 2021-12-28 <small>[Compare with 0.6.0](https://github.com/mkdocstrings/griffe/compare/0.6.0...0.7.0)</small> ### Features - Support more nodes on Python 3.7 ([7f2c4ec](https://github.com/mkdocstrings/griffe/commit/7f2c4ec3bf610ade7305e19ab220a4b447bed41d) by Timothée Mazzucotelli). ### Code Refactoring - Don't crash on syntax errors and log an error ([10bb6b1](https://github.com/mkdocstrings/griffe/commit/10bb6b15bb9b132626c525b81f3ee33c3bb5746f) by Timothée Mazzucotelli). ## [0.6.0](https://github.com/mkdocstrings/griffe/releases/tag/0.6.0) - 2021-12-27 <small>[Compare with 0.5.0](https://github.com/mkdocstrings/griffe/compare/0.5.0...0.6.0)</small> ### Features - Support more AST nodes ([cd1b305](https://github.com/mkdocstrings/griffe/commit/cd1b305932832ad5347ce829a48a311e3c44d542) by Timothée Mazzucotelli). ### Code Refactoring - Use annotation getter for base classes ([8b1a7ed](https://github.com/mkdocstrings/griffe/commit/8b1a7edc11a72f679689fa9ba9e632907f9304f8) by Timothée Mazzucotelli). ## [0.5.0](https://github.com/mkdocstrings/griffe/releases/tag/0.5.0) - 2021-12-20 <small>[Compare with 0.4.0](https://github.com/mkdocstrings/griffe/compare/0.4.0...0.5.0)</small> ### Features - Add support for Python 3.7 ([4535adc](https://github.com/mkdocstrings/griffe/commit/4535adce19edbe7e9cde90f3b1075a8245a6ebc8) by Timothée Mazzucotelli). ### Bug Fixes - Don't propagate aliases of an alias ([8af48f8](https://github.com/mkdocstrings/griffe/commit/8af48f87e2e6bb0f2cf1531fa10287a069f67289) by Timothée Mazzucotelli). - Don't reassign members defined in except clauses ([d918b4e](https://github.com/mkdocstrings/griffe/commit/d918b4efcedcedbec6db214ade8cde921d7e97b2) by Timothée Mazzucotelli). ## [0.4.0](https://github.com/mkdocstrings/griffe/releases/tag/0.4.0) - 2021-11-28 <small>[Compare with 0.3.0](https://github.com/mkdocstrings/griffe/compare/0.3.0...0.4.0)</small> ### Features - Add a prototype 'hybrid' extension ([8cb3c16](https://github.com/mkdocstrings/griffe/commit/8cb3c1661223378a2511fd42a0693d0fbfe924d8) by Timothée Mazzucotelli). - Allow passing extensions config as JSON on the CLI ([9a7fa8b](https://github.com/mkdocstrings/griffe/commit/9a7fa8bd88752ca1a074179db3a4c7fc41b68028) by Timothée Mazzucotelli). - Support names for returns, yields and receives sections items ([1c5a4c9](https://github.com/mkdocstrings/griffe/commit/1c5a4c95738615ea9bb6a816c61d078e6133100a) by Timothée Mazzucotelli). - Store aliases on each object ([91ba643](https://github.com/mkdocstrings/griffe/commit/91ba643b3e8e9a8f56f3280f699a18b1e654ccd7) by Timothée Mazzucotelli). - Support inspection/introspection ([3a0587d](https://github.com/mkdocstrings/griffe/commit/3a0587dbf26f288722c7d27e781d0887c5cdf641) by Timothée Mazzucotelli). - Support multiple return, yield and receive items ([0fc70cb](https://github.com/mkdocstrings/griffe/commit/0fc70cbcc07c63ecf1026e4bef30bd0ff3f73958) by Timothée Mazzucotelli). - Support namespace packages ([2414c8e](https://github.com/mkdocstrings/griffe/commit/2414c8e24b7ba7ee986d95b301662fd06ef350fe) by Timothée Mazzucotelli). ### Bug Fixes - Fix extensions loader ([78fb70b](https://github.com/mkdocstrings/griffe/commit/78fb70b77076b68fa30592caa5e92a91f0ce2caa) by Timothée Mazzucotelli). - Avoid visiting/inspecting multiple times ([75a8a8b](https://github.com/mkdocstrings/griffe/commit/75a8a8b7145e1872cbecf93f8e33749b51b5b77b) by Timothée Mazzucotelli). - Set modules collection attribute earlier ([592c0bd](https://github.com/mkdocstrings/griffe/commit/592c0bde6b6959615bc56030758098c8e45119a2) by Timothée Mazzucotelli). - Support inequality nodes ([b0ed247](https://github.com/mkdocstrings/griffe/commit/b0ed247c9fe42a324a4e8e4a972676afbaa26976) by Timothée Mazzucotelli). - Handle Div nodes for values ([272e4d6](https://github.com/mkdocstrings/griffe/commit/272e4d64b5ca557732af903d35aefbe405bd3ac0) by Timothée Mazzucotelli). ### Code Refactoring - Set log level to INFO ([718e73e](https://github.com/mkdocstrings/griffe/commit/718e73ebb6767c0b10c03482d6f92cf135778ec7) by Timothée Mazzucotelli). - Add target setter ([7f0064c](https://github.com/mkdocstrings/griffe/commit/7f0064c154459b4f4da7fc25bc49f8dd1e4fd2c0) by Timothée Mazzucotelli). - Reorganize conditions ([15ab876](https://github.com/mkdocstrings/griffe/commit/15ab8763acc92d9160b847dc878f8bdad7f0b705) by Timothée Mazzucotelli). - Avoid recursion loops ([ea6acec](https://github.com/mkdocstrings/griffe/commit/ea6acec10c0a805a9ae4e03ae0b92fb2a54cf79b) by Timothée Mazzucotelli). - Update aliases when replacing a member ([99a0f8b](https://github.com/mkdocstrings/griffe/commit/99a0f8b9a425251ddcde853f2ad9ee95504b2127) by Timothée Mazzucotelli). - Reorganize code ([31fcdb1](https://github.com/mkdocstrings/griffe/commit/31fcdb1cbe0eceedc59cc7c1c692dc4ef210ef53) by Timothée Mazzucotelli). - Replace DocstringException with DocstringRaise ([d5ed87a](https://github.com/mkdocstrings/griffe/commit/d5ed87a478411aeb8248e948dbb6c228b80f5fbe) by Timothée Mazzucotelli). - Refactor loaders ([d9b94bb](https://github.com/mkdocstrings/griffe/commit/d9b94bbcb55c29268ab1e077420e2b0d5297638c) by Timothée Mazzucotelli). - Improve typing ([e08bcfa](https://github.com/mkdocstrings/griffe/commit/e08bcfac68aa22dc4bc58914b3340c1743f87ee7) by Timothée Mazzucotelli). ## [0.3.0](https://github.com/mkdocstrings/griffe/releases/tag/0.3.0) - 2021-11-21 <small>[Compare with 0.2.0](https://github.com/mkdocstrings/griffe/compare/0.2.0...0.3.0)</small> ### Features - Handle aliases and their resolution ([67ae903](https://github.com/mkdocstrings/griffe/commit/67ae9034ac25061bc7d5c6def63715209643ca20) by Timothée Mazzucotelli). - Resolve annotations in docstrings ([847384a](https://github.com/mkdocstrings/griffe/commit/847384a322017ca94bd40d4342eb4b8b42858f91) by Timothée Mazzucotelli). - Resolve annotations ([6451eff](https://github.com/mkdocstrings/griffe/commit/6451effa01aa09cd3db1584fe111152de649e525) by Timothée Mazzucotelli). - Add lines property to objects ([7daf7db](https://github.com/mkdocstrings/griffe/commit/7daf7db9ae58fb13985d1adacbde5d0bec2a35e4) by Timothée Mazzucotelli). - Allow setting docstring parser and options on each object ([07a1d2e](https://github.com/mkdocstrings/griffe/commit/07a1d2e83c12bfa0f7b0dd35149b5cc0d0f600d6) by Timothée Mazzucotelli). - Get attributes annotations from parent ([003b990](https://github.com/mkdocstrings/griffe/commit/003b99020f45b350d29329690d18f6c6cb3821f9) by Timothée Mazzucotelli). - Draft extensions loader ([17ccd03](https://github.com/mkdocstrings/griffe/commit/17ccd03cadc5cbb230071e78beab96a0b97456a1) by Timothée Mazzucotelli). - Add properties to objects ([0ec301a](https://github.com/mkdocstrings/griffe/commit/0ec301a5e97bee6556b62cb6ee35af9976f8410b) by Timothée Mazzucotelli). - Handle .pth files when searching modules ([2a2e182](https://github.com/mkdocstrings/griffe/commit/2a2e1826fe0235c5bd47b5d6b1b64a30a81a3f4b) by Timothée Mazzucotelli). - Add `default` property to docstring parameters ([6298ba3](https://github.com/mkdocstrings/griffe/commit/6298ba34d4e769568e519e21549137df3649e01b) by Timothée Mazzucotelli). - Accept RST and Numpy parsers ([1cf147d](https://github.com/mkdocstrings/griffe/commit/1cf147d8df0491104efd084ce3308da77fc2c817) by Timothée Mazzucotelli). - Support data (attributes/variables) ([dce84d1](https://github.com/mkdocstrings/griffe/commit/dce84d106cf067f11305f804a24cfd7d5643d902) by Timothée Mazzucotelli). - Add Numpy-style parser ([ad5b72d](https://github.com/mkdocstrings/griffe/commit/ad5b72d174433764e85f937ea1096c0f458532f8) by Timothée Mazzucotelli). - Support more section kinds in Google-style ([9d3d047](https://github.com/mkdocstrings/griffe/commit/9d3d0472d0bb55352b371de3da0816419fcf59e0) by Timothée Mazzucotelli). - Add docstring section kinds ([b270483](https://github.com/mkdocstrings/griffe/commit/b2704833bc74131269306b9947ea2b46edafd349) by Timothée Mazzucotelli). - Accept initial arguments when creating container ([90c5956](https://github.com/mkdocstrings/griffe/commit/90c59568bb6cdbf18efe182bd821973f2a133663) by Timothée Mazzucotelli). - Add an RST-style docstring parser ([742e7b2](https://github.com/mkdocstrings/griffe/commit/742e7b2e2101d0679571645584c5a6d3077a9764) by Timothée Mazzucotelli). ### Performance Improvements - Improve JSON encoder perfs ([6a78eb0](https://github.com/mkdocstrings/griffe/commit/6a78eb0b707a148356fb5bc69d9d0c2115239074) by Timothée Mazzucotelli). ### Bug Fixes - Handle serialization of Posix paths ([3a66b95](https://github.com/mkdocstrings/griffe/commit/3a66b95a4c91e6160d161acc457c66196adaa4fe) by Timothée Mazzucotelli). - Fix list annotation getter ([5ae800a](https://github.com/mkdocstrings/griffe/commit/5ae800a8902a28b5241192c0905b1914e2bfe906) by Timothée Mazzucotelli). - Show accurate line number in Google warnings ([2953590](https://github.com/mkdocstrings/griffe/commit/29535902d53b553906f59295104690c9417eb79f) by Timothée Mazzucotelli). - Fix assignment names getters ([6990846](https://github.com/mkdocstrings/griffe/commit/69908460b4fe47d1dc3d8d9f6b43d49dee5823aa) by Timothée Mazzucotelli). - Fix async loader (passing parent) ([57e866e](https://github.com/mkdocstrings/griffe/commit/57e866e4c48f4646142a26c6d2537f4da10e3a2c) by Timothée Mazzucotelli). - Fix exception name ([4b8b85d](https://github.com/mkdocstrings/griffe/commit/4b8b85dde72a552091534b3293399b844523786f) by Timothée Mazzucotelli). - Fix Google sections titles logic ([87dd329](https://github.com/mkdocstrings/griffe/commit/87dd32988a9164c47dadf96c0c74a0da8af16bd8) by Timothée Mazzucotelli). - Prepend current module to base classes (still needs resolution) ([a4b1dee](https://github.com/mkdocstrings/griffe/commit/a4b1deef4beb0e9e79adc920d80232f04ddfdc31) by Timothée Mazzucotelli). - Fix Google admonition regex ([3902e74](https://github.com/mkdocstrings/griffe/commit/3902e7497ef8b388c3d232a8116cb3bd27fdaad2) by Timothée Mazzucotelli). - Fix docstring getter ([1442eba](https://github.com/mkdocstrings/griffe/commit/1442eba93479f24a4d90cd9b25f57d304a65cd6c) by Timothée Mazzucotelli). - Fix getting arguments defaults in the Google-style parser ([67adbaf](https://github.com/mkdocstrings/griffe/commit/67adbafe04de1c8effc124b26565bef59adfb393) by Timothée Mazzucotelli). - Fix getting arguments annotations in the Google-style parser ([8bcbfba](https://github.com/mkdocstrings/griffe/commit/8bcbfbae861be4c3f9c2b8841c8bc86f39611168) by Timothée Mazzucotelli). ### Code Refactoring - Export parsers and main function in docstrings module ([96469da](https://github.com/mkdocstrings/griffe/commit/96469dab63a28c061e1d064528f8e07f394c2d81) by Timothée Mazzucotelli). - Remove top exports ([cd76694](https://github.com/mkdocstrings/griffe/commit/cd7669481a272d7c939b61f6ff2df1cb55eab39e) by Timothée Mazzucotelli). - Reorganize exceptions ([7f9b805](https://github.com/mkdocstrings/griffe/commit/7f9b8055aa069816b3b55fd02730e97e37a6bea4) by Timothée Mazzucotelli). - Avoid circular import ([ef27dcd](https://github.com/mkdocstrings/griffe/commit/ef27dcd6cc85590d1982ee14b7f520d379d658b8) by Timothée Mazzucotelli). - Rename index to [new] offset ([c07cc7d](https://github.com/mkdocstrings/griffe/commit/c07cc7d916d613545073e1159d86c65d58d98b37) by Timothée Mazzucotelli). - Reorganize code ([5f4fff2](https://github.com/mkdocstrings/griffe/commit/5f4fff21d1da7e1b33554cfb8017b23955999ad5) by Timothée Mazzucotelli). - Use keyword only parameters ([d34edd6](https://github.com/mkdocstrings/griffe/commit/d34edd629589796d53dbc29d77c5f7041acea5ab) by Timothée Mazzucotelli). - Default to no parsing for serialization ([8fecd9e](https://github.com/mkdocstrings/griffe/commit/8fecd9ef63f773220bb85379537c4ad25ea0e4fd) by Timothée Mazzucotelli). - Always extend AST ([c227ae6](https://github.com/mkdocstrings/griffe/commit/c227ae62ee5a3cc764f2c6fc9185400f0c9c48e7) by Timothée Mazzucotelli). - Set default for kwargs parameters ([7a0b85e](https://github.com/mkdocstrings/griffe/commit/7a0b85e5fd255db743c122e1a13916cdc3eb46ff) by Timothée Mazzucotelli). - Rename visitor method ([3e0c43c](https://github.com/mkdocstrings/griffe/commit/3e0c43cbed6cec563367f80e86f245b3ba89694c) by Timothée Mazzucotelli). - Improve typing ([ac86f17](https://github.com/mkdocstrings/griffe/commit/ac86f17bfbfc98d3c41f1830e4356fecc2ed76fc) by Timothée Mazzucotelli). - Fix typo ([a9ed6e9](https://github.com/mkdocstrings/griffe/commit/a9ed6e95992381df41554a895ed6304ca61048f7) by Timothée Mazzucotelli). - Rewrite ParameterKind ([90249df](https://github.com/mkdocstrings/griffe/commit/90249df0b478f147fc50a18dfb56ad96ad09e78c) by Timothée Mazzucotelli). - Add bool methods to docstrings and objects ([548f72e](https://github.com/mkdocstrings/griffe/commit/548f72ed5289aa531c125e4da6ff72a1ff34124d) by Timothée Mazzucotelli). - Allow setting docstring parser and options on each docstring ([752e084](https://github.com/mkdocstrings/griffe/commit/752e0843bc7388c9a2c7ce9ae2dce03ffa9243e3) by Timothée Mazzucotelli). - Skip attribute assignments ([e9cc2cd](https://github.com/mkdocstrings/griffe/commit/e9cc2cdd8cae1d15b98ffaa60e777b679ac55e23) by Timothée Mazzucotelli). - Improve visitor getters ([2ea88c0](https://github.com/mkdocstrings/griffe/commit/2ea88c020481e78060c90d8307a4f6a68047eaa2) by Timothée Mazzucotelli). - Use relative filepath in docstring warnings ([e894df7](https://github.com/mkdocstrings/griffe/commit/e894df767262623720a45c0b5c16fed544fae106) by Timothée Mazzucotelli). - Set submodules parent earlier ([53767c0](https://github.com/mkdocstrings/griffe/commit/53767c0c4ef90bfe405dcffd6087e365b98efafc) by Timothée Mazzucotelli). - Rename Data to Attribute ([febc12e](https://github.com/mkdocstrings/griffe/commit/febc12e5e33bbbdd448298f2cc277a45fd986204) by Timothée Mazzucotelli). - Rename arguments to parameters ([957856c](https://github.com/mkdocstrings/griffe/commit/957856cf22772584bcced30141afb8ca6a2ac378) by Timothée Mazzucotelli). - Improve annotation support ([5b2262f](https://github.com/mkdocstrings/griffe/commit/5b2262f9cacce4044716661e6de49a1773ea3aa8) by Timothée Mazzucotelli). - Always set parent ([cae85de](https://github.com/mkdocstrings/griffe/commit/cae85def4af1f67b537daabdb1e8ae9830dcaec7) by Timothée Mazzucotelli). - Factorize function handling ([dfece1c](https://github.com/mkdocstrings/griffe/commit/dfece1c0c73076c7d87d4df551f0994b4c2e3b69) by Timothée Mazzucotelli). - Privatize stuff, fix loggers ([5513ed5](https://github.com/mkdocstrings/griffe/commit/5513ed5345db185e7c08890ca08de17932b34f51) by Timothée Mazzucotelli). - Use keyword only arguments ([e853fe9](https://github.com/mkdocstrings/griffe/commit/e853fe9188fd2cd2ccc90e5fa1f52443bb00bab7) by Timothée Mazzucotelli). - Set default values for Argument arguments ([d5cccaa](https://github.com/mkdocstrings/griffe/commit/d5cccaa6ee73e14ca4456b974fba6d01d40bf848) by Timothée Mazzucotelli). - Swallow extra parsing options ([3d9ebe7](https://github.com/mkdocstrings/griffe/commit/3d9ebe775e1b936e89115d166144610b3a90290c) by Timothée Mazzucotelli). - Rename `start_index` argument to `offset` ([dd88358](https://github.com/mkdocstrings/griffe/commit/dd88358d8db78636ba5f39fcad92ff5192791852) by Timothée Mazzucotelli). - Reuse parsers warn function ([03dfdd3](https://github.com/mkdocstrings/griffe/commit/03dfdd38c5977ee83383f95acda1280b3f9ac86b) by Timothée Mazzucotelli). ## [0.2.0](https://github.com/mkdocstrings/griffe/releases/tag/0.2.0) - 2021-09-25 <small>[Compare with 0.1.0](https://github.com/mkdocstrings/griffe/compare/0.1.0...0.2.0)</small> ### Features - Add Google-style docstring parser ([cdefccc](https://github.com/mkdocstrings/griffe/commit/cdefcccff2cb8236003736545cffaf0bd6f46539) by Timothée Mazzucotelli). - Support all kinds of functions arguments ([c177562](https://github.com/mkdocstrings/griffe/commit/c177562c358f89da8c541b51d86f9470dd849c8f) by Timothée Mazzucotelli). - Initial support for class decorators and bases ([8e229aa](https://github.com/mkdocstrings/griffe/commit/8e229aa5f04d21bde108dca517166d291fd2147a) by Timothée Mazzucotelli). - Add functions decorators support ([fee304d](https://github.com/mkdocstrings/griffe/commit/fee304d44ce33286dedd6bb13a9b7200ea3d4dfa) by Timothée Mazzucotelli). - Add async loader ([3218bd0](https://github.com/mkdocstrings/griffe/commit/3218bd03fd754a04a4280c29319e6b8d55aac015) by Timothée Mazzucotelli). - Add relative file path and package properties ([d26ee1f](https://github.com/mkdocstrings/griffe/commit/d26ee1f3f09337af925c8071b4f24b8ae69b01d3) by Timothée Mazzucotelli). - Add search and output option to the CLI ([3b37692](https://github.com/mkdocstrings/griffe/commit/3b3769234aed87e100ef917fa2db550e650bff0d) by Timothée Mazzucotelli). - Load docstrings and functions arguments ([cdf29a3](https://github.com/mkdocstrings/griffe/commit/cdf29a3b12b4c04235dfeba1c8ef7461cc05248f) by Timothée Mazzucotelli). - Support paths in loader ([8f4df75](https://github.com/mkdocstrings/griffe/commit/8f4df7518ee5164e695e27fc9dcedae7a8b05133) by Timothée Mazzucotelli). ### Performance Improvements - Avoid name lookups in visitor ([00de148](https://github.com/mkdocstrings/griffe/commit/00de1482891e0c0091e79c14fdc318c6a95e4f6f) by Timothée Mazzucotelli). - Factorize and improve main and extensions visitors ([9b27b56](https://github.com/mkdocstrings/griffe/commit/9b27b56c0fc17d94144fd0b7e3783d3f6f572d3d) by Timothée Mazzucotelli). - Delegate children computation at runtime ([8d54c87](https://github.com/mkdocstrings/griffe/commit/8d54c8792f2a98c744374ae290bcb31fa81141b4) by Timothée Mazzucotelli). - Cache dataclasses properties ([2d7447d](https://github.com/mkdocstrings/griffe/commit/2d7447db05c2a3227e6cb66be46d374dac5fdf19) by Timothée Mazzucotelli). - Optimize node linker ([03f955e](https://github.com/mkdocstrings/griffe/commit/03f955ee698adffb7217528c03691876f299f8ca) by Timothée Mazzucotelli). - Optimize docstring getter ([4a05516](https://github.com/mkdocstrings/griffe/commit/4a05516de320473b5defd70f208b4e90763f2208) by Timothée Mazzucotelli). ## [0.1.0](https://github.com/mkdocstrings/griffe/releases/tag/0.1.0) - 2021-09-09 <small>[Compare with first commit](https://github.com/mkdocstrings/griffe/compare/7ea73adcc6aebcbe0eb64982916220773731a6b3...0.1.0)</small> ### Features - Add initial code ([8cbdf7a](https://github.com/mkdocstrings/griffe/commit/8cbdf7a49202dcf3cd617ae905c0f04cdfe053dd) by Timothée Mazzucotelli). - Generate project from copier-pdm template ([7ea73ad](https://github.com/mkdocstrings/griffe/commit/7ea73adcc6aebcbe0eb64982916220773731a6b3) by Timothée Mazzucotelli). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/CODE_OF_CONDUCT.md��������������������������������������������������������������0000664�0001750�0001750�00000012536�14767006246�017005� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dev@pawamoy.fr. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/logo.svg������������������������������������������������������������������������0000664�0001750�0001750�00000014267�14767006246�015672� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8" standalone="no"?> <!-- Copyright François Rozet 2022 --> <svg id="svg" width="512" height="512" version="1.1" viewbox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <path id="path" fill="#000000" d="M 16.353555,0.02028172 C 6.0845792,0.53792196 -3.5255624,10.91814 1.2695074,20.365706 14.431688,46.305932 16.211565,67.483102 28.239923,79.141311 c 17.000421,16.474673 32.343015,12.942792 49.700142,39.295259 16.528952,25.09049 13.749525,47.45141 25.558475,72.47241 1.23303,2.61266 5.4807,9.67734 -10.834959,25.22786 -0.11279,0.10749 -0.181871,0.23544 -0.290586,0.34534 1.366563,-0.97244 2.865238,-2.16253 4.182783,-3.07171 15.455442,-10.66525 34.227902,-23.66232 41.716832,-28.88307 21.73425,-15.15067 51.49749,-31.65817 76.66698,-42.70499 -0.0198,0.006 -0.0346,-6.1e-4 -0.0543,0.006 -16.34949,5.52402 -16.58735,-2.05825 -14.36058,-13.16142 6.20633,-30.946611 18.12289,-43.704229 31.56677,-48.759275 10.22168,-3.840752 18.99735,-0.849552 25.29684,5.777574 8.19073,8.616619 6.9156,29.044281 -26.62556,49.138301 6.42826,-3.04004 13.75526,-6.83521 19.21133,-10.24973 7.39318,-4.62704 21.18623,-10.17731 30.98201,-12.46717 9.71163,-2.27015 28.76227,-9.04167 42.33418,-15.047915 19.96147,-8.833889 37.95922,-14.273638 62.42215,-17.421801 -0.0179,0 -0.0352,-0.0117 -0.0531,-0.0099 -55.87264,6.172011 -65.2272,-2.800912 -72.80731,-15.294565 -0.95381,-1.57208 -1.97867,-3.132391 -3.07712,-4.677918 -20.01357,-28.158447 -57.10949,-45.871256 -91.50019,-32.947374 -25.85067,9.717875 -51.82802,39.030059 -60.16253,92.696353 -1.45725,9.62571 -2.2737,22.02605 -4.80013,30.86948 -5.94816,20.82043 -15.02806,17.61739 -21.12011,7.00844 -7.12513,-12.41135 -18.44996,-54.97033 -23.2567,-70.78076 C 102.27144,64.584981 96.758639,48.981053 87.955063,33.648479 79.214334,18.426172 39.706987,5.1302774 20.755683,0.43406126 19.301889,0.07399851 17.820437,-0.05382529 16.353494,0.02015851 Z M 390.70187,105.7023 c -52.7833,15.12024 -97.37611,30.94691 -119.42441,43.17426 -7.87805,4.36905 -25.33321,11.5294 -38.78801,15.91416 -1.58517,0.51659 -3.47357,1.29271 -5.17875,1.90947 23.43739,-4.22361 15.43952,10.37406 4.36002,28.78919 -8.96131,14.89452 -18.32722,30.42718 -26.48691,45.94752 -6.23182,11.85349 -14.1416,20.30778 -51.42081,41.30212 -0.013,0.007 -0.0463,0.097 -0.0605,0.10707 3.10668,-1.46896 6.06545,-2.90403 9.51945,-4.45166 46.45678,-20.81649 81.78955,-40.67798 95.08255,-53.44564 11.2263,-10.78272 47.73629,-26.16088 89.84829,-37.84546 24.24587,-6.72747 57.37854,-17.24106 73.62727,-23.36415 43.62475,-16.43938 53.2912,-19.17912 76.3788,-21.48001 -21.09111,0.47804 -50.20989,4.17334 -69.99189,-3.1295 -20.1205,-7.83366 -42.9282,-7.9005 -64.18493,3.4579 -20.46788,10.93611 -34.51701,24.61924 -44.36113,40.18917 -2.21825,3.50852 -8.45125,13.15457 -16.84997,16.4629 -8.39866,3.30838 -13.50968,3.96629 -9.5694,-3.60956 4.5882,-8.82163 9.21165,-17.96759 13.41888,-27.28887 7.23069,-16.01987 11.64159,-36.60911 84.08121,-62.63897 z M 512,159.01919 c -22.27702,9.29035 -44.35643,19.12299 -58.8748,26.51164 -4.57758,2.32966 -10.05266,4.8111 -15.29747,7.25509 6.939,6.75095 11.99144,15.78288 12.91012,26.84368 0.20298,2.44597 0.27774,4.83759 0.23391,7.17927 -0.38893,20.7911 -29.4382,48.51316 -63.42417,71.90813 8.62399,-3.76844 17.3696,-7.90445 26.20719,-12.43347 19.84269,-10.16886 36.35498,-16.34617 60.06005,-22.47266 9.47056,-2.44757 20.92589,-5.17788 32.58928,-7.87345 -0.0849,0.013 -0.10644,-3.1e-4 -0.19293,0.0136 -26.49956,4.1426 -19.63773,-20.25758 -19.43684,-30.3293 0.0574,-2.88469 -0.0327,-5.83252 -0.28337,-8.85046 -0.6158,-7.41963 -2.18374,-14.61621 -4.56134,-21.46682 -3.4082,-9.81989 -5.56842,-16.84767 30.07037,-36.2849 z M 211.20296,173.2141 c -11.53781,5.09262 -24.17938,11.36024 -36.93121,18.53106 -18.56521,10.43979 -44.30476,23.74658 -57.19876,29.57011 -5.48465,2.47714 -10.82465,4.96702 -16.0631,7.41989 29.65489,-9.94198 34.31056,-5.83826 40.0348,-5.3577 8.88735,0.74606 19.12225,-1.79237 30.85175,-8.82637 3.16774,-2.42188 5.91963,-5.47222 8.32334,-8.98401 6.81942,-9.963 12.06238,-21.3161 30.98318,-32.35298 z m 136.58115,55.053 c -1.71654,0.56754 -3.5195,1.24774 -5.2161,1.79514 -12.22826,3.94529 -23.15175,9.34456 -29.20108,14.43429 -5.31593,4.47249 -23.17527,14.57531 -39.68755,22.45099 -0.11594,0.0555 -0.22601,0.1081 -0.34245,0.16367 24.03672,-8.64834 20.37158,21.25121 19.03774,34.34543 -0.19397,1.90436 -0.39375,3.80847 -0.6005,5.71258 -1.13135,10.41841 -11.18395,25.5283 -20.00234,33.96763 12.14306,-6.91487 22.31246,-12.97065 22.95648,-14.12272 0.5019,-0.89766 10.95886,-3.54204 23.23737,-5.8762 12.90475,-2.45323 26.55871,-6.2338 40.69438,-11.12566 -35.08608,6.36988 -29.62044,-9.7881 -27.009,-25.58761 3.26769,-19.77038 7.79576,-39.03457 16.13305,-56.15754 z m -79.43541,41.23713 c -15.58163,7.44292 -34.24495,16.38615 -43.41819,20.82556 -33.75724,16.33649 -58.68873,27.14546 -84.31994,36.59173 0.0216,0.006 0.0198,0.07 0.0445,0.0638 25.26277,-6.39692 37.93496,-3.25682 39.86603,16.91038 1.57942,16.49445 9.13441,33.66979 -17.9212,48.93256 -0.0346,0.0198 -0.0426,0.0733 -0.0748,0.0963 3.53358,-1.55766 7.14698,-3.1258 11.49207,-4.87283 15.69695,-6.31098 36.70835,-15.20013 46.6918,-19.75466 7.2594,-3.31165 19.60711,-9.8322 32.08527,-16.61685 -13.89028,4.34829 -14.84662,-6.5564 -15.94253,-15.72054 -0.0804,-0.67201 -0.15113,-1.3505 -0.21225,-2.03576 -0.80687,-9.0745 0.13057,-18.10151 2.27049,-27.05903 3.36572,-14.08884 9.35128,-24.82037 29.43857,-37.3606 z m 210.37356,15.05278 c -26.60773,15.7001 -52.05971,32.55239 -82.38227,42.30069 -11.35131,3.98928 -21.38526,7.94855 -31.44495,12.26025 17.65586,-3.2296 10.19403,7.91855 2.55619,24.91264 -7.88737,17.54926 -13.23299,37.88267 -13.87349,62.40796 -3.38406,129.40649 145.03678,86.88022 148.4832,35.42346 -47.51955,52.36372 -125.16428,47.81461 -123.19117,-27.61013 0.6847,-26.25636 8.21111,-47.84701 18.99915,-66.75379 12.52742,-21.95514 25.53508,-44.06495 80.85334,-82.94108 z m -129.1066,61.45862 c -9.38259,4.42831 -19.11953,9.26109 -30.52141,15.32469 -35.15355,18.69467 -38.28437,20.10234 -91.56773,41.16013 -19.28202,7.62033 -33.72464,13.45088 -47.83006,19.18318 28.28505,-9.83239 39.14354,-6.43838 48.67287,-1.74456 41.93093,20.65584 66.287,1.71019 80.51095,-28.95767 6.34814,-13.68695 16.37221,-29.63615 40.73538,-44.96577 z" /> </svg> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/duties.py�����������������������������������������������������������������������0000664�0001750�0001750�00000043104�14767006246�016050� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Development tasks.""" from __future__ import annotations import os import re import sys from contextlib import contextmanager from functools import partial, wraps from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Any, Callable from duty import duty, tools if TYPE_CHECKING: from collections.abc import Callable, Iterator from duty.context import Context PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" def _pyprefix(title: str) -> str: if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title def _not_from_insiders(func: Callable) -> Callable: @wraps(func) def wrapper(ctx: Context, *args: Any, **kwargs: Any) -> None: origin = ctx.run("git config --get remote.origin.url", silent=True) if "pawamoy-insiders/griffe" in origin: ctx.run( lambda: False, title="Not running this task from insiders repository (do that from public repo instead!)", ) return func(ctx, *args, **kwargs) return wrapper @contextmanager def _material_insiders() -> Iterator[bool]: if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: yield True finally: os.environ.pop("MATERIAL_INSIDERS") else: yield False def _get_changelog_version() -> str: changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") with Path(__file__).parent.joinpath("CHANGELOG.md").open("r", encoding="utf8") as file: return next(filter(bool, map(changelog_version_re.match, file))).group(1) # type: ignore[union-attr] @duty def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. ```bash make changelog [bump=VERSION] ``` Update the changelog in-place. The changelog task uses [git-changelog](https://pawamoy.github.io/git-changelog/) to read Git commits and parse their messages to infer the new version based on our [commit message convention][commit-message-convention]. The new version will be based on the types of the latest commits, unless a specific version is provided with the `bump` parameter. If the group of commits contains only bug fixes (`fix:`) and/or commits that are not interesting for users (`chore:`, `style:`, etc.), the changelog will gain a new **patch** entry. It means that the new suggested version will be a patch bump of the previous one: `0.1.1` becomes `0.1.2`. If the group of commits contains at least one feature (`feat:`), the changelog will gain a new **minor** entry. It means that the new suggested version will be a minor bump of the previous one: `0.1.1` becomes `0.2.0`. If there is, in the group of commits, a commit whose body contains something like `Breaking change`, the changelog will gain a new **major** entry, unless the version is still an "alpha" version (starting with 0), in which case it gains a **minor** entry. It means that the new suggested version will be a major bump of the previous one: `1.2.1` becomes `2.0.0`, but `0.2.1` is only bumped up to `0.3.0`. Moving from "alpha" status to "beta" or "stable" status is a choice left to the developers, when they consider the package is ready for it. The configuration for git-changelog is located at `config/git-changelog.toml`. Parameters: bump: Bump option passed to git-changelog. """ ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") ctx.run(tools.yore.check(bump=bump or _get_changelog_version()), title="Checking legacy code") @duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) def check(ctx: Context) -> None: """Check it all! ```bash make check ``` Composite command to run all the check commands: - [`check-quality`][], to check the code quality on all Python versions - [`check-types`][], to type-check the code on all Python versions - [`check-docs`][], to check the docs on all Python versions - [`check-api`][], to check for API breaking changes """ @duty def check_quality(ctx: Context) -> None: """Check the code quality. ```bash make check-quality ``` Check the code quality using [Ruff](https://astral.sh/ruff). The configuration for Ruff is located at `config/ruff.toml`. In this file, you can deactivate rules or activate others to customize your analysis. Rule identifiers always start with one or more capital letters, like `D`, `S` or `BLK`, then followed by a number. You can ignore a rule on a specific code line by appending a `noqa` comment ("no quality analysis/assurance"): ```python title="src/your_package/module.py" print("a code line that triggers a Ruff warning") # noqa: ID ``` ...where ID is the identifier of the rule you want to ignore for this line. Example: ```python title="src/your_package/module.py" import subprocess ``` ```console $ make check-quality ✗ Checking code quality (1) > ruff check --config=config/ruff.toml src/ tests/ scripts/ src/your_package/module.py:2:1: S404 Consider possible security implications associated with subprocess module. ``` Now add a comment to ignore this warning. ```python title="src/your_package/module.py" import subprocess # noqa: S404 ``` ```console $ make check-quality ✓ Checking code quality ``` You can disable multiple different warnings on a single line by separating them with commas, for example `# noqa: D300,D301`. You can disable a warning globally by adding its ID into the list in `config/ruff.toml`. You can also disable warnings per file, like so: ```toml title="config/ruff.toml" [per-file-ignores] "src/your_package/your_module.py" = [ "T201", # Print statement ] ``` """ ctx.run( tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=_pyprefix("Checking code quality"), ) @duty def check_docs(ctx: Context) -> None: """Check if the documentation builds correctly. ```bash make check-docs ``` Build the docs with [MkDocs](https://www.mkdocs.org/) in strict mode. The configuration for MkDocs is located at `mkdocs.yml`. This task builds the documentation with strict behavior: any warning will be considered an error and the command will fail. The warnings/errors can be about incorrect docstring format, or invalid cross-references. """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with _material_insiders(): ctx.run( tools.mkdocs.build(strict=True, verbose=True), title=_pyprefix("Building documentation"), ) @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed. ```bash make check-types ``` Run type-checking on the code with [Mypy](https://mypy.readthedocs.io/). The configuration for Mypy is located at `config/mypy.ini`. If you cannot or don't know how to fix a typing error in your code, as a last resort you can ignore this specific error with a comment: ```python title="src/your_package/module.py" print("a code line that triggers a Mypy warning") # type: ignore[ID] ``` ...where ID is the name of the warning. Example: ```python title="src/your_package/module.py" result = data_dict.get(key, None).value ``` ```console $ make check-types ✗ Checking types (1) > mypy --config-file=config/mypy.ini src/ tests/ scripts/ src/your_package/module.py:2:1: Item "None" of "Data | None" has no attribute "value" [union-attr] ``` Now add a comment to ignore this warning. ```python title="src/your_package/module.py" result = data_dict.get(key, None).value # type: ignore[union-attr] ``` ```console $ make check-types ✓ Checking types ``` """ os.environ["FORCE_COLOR"] = "1" ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=_pyprefix("Type-checking"), ) @duty def check_api(ctx: Context, *cli_args: str) -> None: """Check for API breaking changes. ```bash make check-api ``` Compare the current code to the latest version (Git tag) using [Griffe](https://mkdocstrings.github.io/griffe/), to search for API breaking changes since latest version. It is set to allow failures, and is more about providing information than preventing CI to pass. Parameters: *cli_args: Additional Griffe CLI arguments. """ ctx.run( tools.griffe.check("griffe", search=["src"], color=True).add_args(*cli_args), title="Checking for API breaking changes", nofail=True, ) @duty def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). ```bash make docs ``` This task uses [MkDocs](https://www.mkdocs.org/) to serve the documentation locally. Parameters: *cli_args: Additional MkDocs CLI arguments. host: The host to serve the docs from. port: The port to serve the docs on. """ with _material_insiders(): ctx.run( tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), title="Serving documentation", capture=False, ) @duty def docs_deploy(ctx: Context, *, force: bool = False) -> None: """Deploy the documentation to GitHub pages. ```bash make docs-deploy ``` Use [MkDocs](https://www.mkdocs.org/) to build and deploy the documentation to GitHub pages. Parameters: force: Whether to force deployment, even from non-Insiders version. """ os.environ["DEPLOY"] = "true" with _material_insiders() as insiders: if not insiders: ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") origin = ctx.run("git config --get remote.origin.url", silent=True, allow_overrides=False) if "pawamoy-insiders/griffe" in origin: ctx.run( "git remote add upstream git@github.com:mkdocstrings/griffe", silent=True, nofail=True, allow_overrides=False, ) ctx.run( tools.mkdocs.gh_deploy(remote_name="upstream", force=True), title="Deploying documentation", ) elif force: ctx.run( tools.mkdocs.gh_deploy(force=True), title="Deploying documentation", ) else: ctx.run( lambda: False, title="Not deploying docs from public repository (do that from insiders instead!)", nofail=True, ) @duty def format(ctx: Context) -> None: """Run formatting tools on the code. ```bash make format ``` Format the code with [Ruff](https://astral.sh/ruff). This command will also automatically fix some coding issues when possible. """ ctx.run( tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) ctx.run(tools.ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") @duty def build(ctx: Context) -> None: """Build source and wheel distributions. ```bash make build ``` Build distributions of your project for the current version. The build task uses the [`build` tool](https://build.pypa.io/en/stable/) to build `.tar.gz` (Gzipped sources archive) and `.whl` (wheel) distributions of your project in the `dist` directory. """ ctx.run( tools.build(), title="Building source and wheel distributions", pty=PTY, ) @duty @_not_from_insiders def publish(ctx: Context) -> None: """Publish source and wheel distributions to PyPI. ```bash make publish ``` Publish the source and wheel distributions of your project to PyPI using [Twine](https://twine.readthedocs.io/). """ if not Path("dist").exists(): ctx.run("false", title="No distribution files found") dists = [str(dist) for dist in Path("dist").iterdir()] ctx.run( tools.twine.upload(*dists, skip_existing=True), title="Publishing source and wheel distributions to PyPI", pty=PTY, ) @duty(post=["build", "publish", "docs-deploy"]) @_not_from_insiders def release(ctx: Context, version: str = "") -> None: """Release a new version of the project. ```bash make release [version=VERSION] ``` This task will: - Stage changes to `pyproject.toml` and `CHANGELOG.md` - Commit the changes with a message like `chore: Prepare release 1.0.0` - Tag the commit with the new version number - Push the commit and the tag to the remote repository - Build source and wheel distributions - Publish the distributions to PyPI - Deploy the documentation to GitHub pages Parameters: version: The new version number to use. If not provided, you will be prompted for it. """ if not (version := (version or input("> Version to release: ")).strip()): ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) @duty(silent=True, aliases=["cov"]) def coverage(ctx: Context) -> None: """Report coverage as text and HTML. ```bash make coverage ``` Combine coverage data from multiple test runs with [Coverage.py](https://coverage.readthedocs.io/), then generate an HTML report into the `htmlcov` directory, and print a text report in the console. """ ctx.run(tools.coverage.combine(), nofail=True) ctx.run(tools.coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) @duty def test(ctx: Context, *cli_args: str, match: str = "") -> None: """Run the test suite. ```bash make test [match=EXPR] ``` Run the test suite with [Pytest](https://docs.pytest.org/) and plugins. Code source coverage is computed thanks to [coveragepy](https://coverage.readthedocs.io/en/coverage-5.1/). Parameters: *cli_args: Additional Pytest CLI arguments. match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( tools.pytest( "tests", config_file="config/pytest.ini", select=match, color="yes", ).add_args("-n", "auto", *cli_args), title=_pyprefix("Running tests"), ) class _Seeds(list): def __init__(self, cli_value: str = "") -> None: if cli_value: self.extend(int(seed) for seed in cli_value.split(",")) @duty def fuzz( ctx: Context, *, size: int = 20, min_seed: int = 0, max_seed: int = 1_000_000, seeds: _Seeds = _Seeds(), # noqa: B008 ) -> None: """Fuzz Griffe against generated Python code. Parameters: ctx: The context instance (passed automatically). size: The size of the case set (number of cases to test). seeds: Seeds to test or exclude (comma-separated integers). min_seed: Minimum value for the seeds range. max_seed: Maximum value for the seeds range. """ import warnings from random import sample from tempfile import gettempdir from pysource_codegen import generate from pysource_minimize import minimize from griffe import visit warnings.simplefilter("ignore", SyntaxWarning) def fails(code: str, filepath: Path) -> bool: try: visit(filepath.stem, filepath=filepath, code=code) except Exception: # noqa: BLE001 return True return False def test_seed(seed: int, revisit: bool = False) -> bool: # noqa: FBT001,FBT002 filepath = Path(gettempdir(), f"fuzz_{seed}_{sys.version_info.minor}.py") if filepath.exists(): if revisit: code = filepath.read_text() else: return True else: code = generate(seed) filepath.write_text(code) if fails(code, filepath): new_code = minimize(code, partial(fails, filepath=filepath)) if code != new_code: filepath.write_text(new_code) return False return True revisit = bool(seeds) seeds = seeds or sample(range(min_seed, max_seed + 1), size) # type: ignore[assignment] for seed in seeds: ctx.run(test_seed, args=[seed, revisit], title=f"Visiting code generated with seed {seed}") ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�015537� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/���������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017722� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/config.yml�����������������������������������������������0000664�0001750�0001750�00000000330�14767006246�021706� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������blank_issues_enabled: false contact_links: - name: I have a question / I need help url: https://github.com/mkdocstrings/griffe/discussions/new?category=q-a about: Ask and answer questions in the Discussions tab. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/4-change.md����������������������������������������������0000664�0001750�0001750�00000001126�14767006246�021632� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Change request about: Suggest any other kind of change for this project. title: "change: " assignees: pawamoy --- ### Is your change request related to a problem? Please describe. <!-- A clear and concise description of what the problem is. --> ### Describe the solution you'd like <!-- A clear and concise description of what you want to happen. --> ### Describe alternatives you've considered <!-- A clear and concise description of any alternative solutions you've considered. --> ### Additional context <!-- Add any other context or screenshots about the change request here. --> ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/2-feature.md���������������������������������������������0000664�0001750�0001750�00000001213�14767006246�022033� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Feature request about: Suggest an idea for this project. title: "feature: " labels: feature assignees: pawamoy --- ### Is your feature request related to a problem? Please describe. <!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]. --> ### Describe the solution you'd like <!-- A clear and concise description of what you want to happen. --> ### Describe alternatives you've considered <!-- A clear and concise description of any alternative solutions or features you've considered. --> ### Additional context <!-- Add any other context or screenshots about the feature request here. --> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/3-docs.md������������������������������������������������0000664�0001750�0001750�00000001131�14767006246�021330� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Documentation update about: Point at unclear, missing or outdated documentation. title: "docs: " labels: docs assignees: pawamoy --- ### Is something unclear, missing or outdated in our documentation? <!-- A clear and concise description of what the documentation issue is. Ex. I can't find an explanation on feature [...]. --> ### Relevant code snippets <!-- If the documentation issue is related to code, please provide relevant code snippets. --> ### Link to the relevant documentation section <!-- Add a link to the relevant section of our documentation, or any addition context. --> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/ISSUE_TEMPLATE/1-bug.md�������������������������������������������������0000664�0001750�0001750�00000002677�14767006246�021173� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Bug report about: Create a bug report to help us improve. title: "bug: " labels: unconfirmed assignees: [pawamoy] --- ### Description of the bug <!-- Please provide a clear and concise description of what the bug is. --> ### To Reproduce <!-- Please provide a Minimal Reproducible Example (MRE) if possible. Try to boil down the problem to a few lines of code. Your code should run by simply copying and pasting it. Example: ``` git clone https://github.com/username/repro cd repro python -m venv .venv . .venv/bin/activate pip install -r requirements.txt ... # command or code showing the issue ``` --> ``` WRITE MRE / INSTRUCTIONS HERE ``` ### Full traceback <!-- Please provide the full error message / traceback if any, by pasting it in the code block below. No screenshots! --> <details><summary>Full traceback</summary> ```python PASTE TRACEBACK HERE ``` </details> ### Expected behavior <!-- Please provide a clear and concise description of what you expected to happen. --> ### Environment information <!-- Please run the following command in your repository and paste its output below it, redacting sensitive information. --> ```bash griffe --debug-info # | xclip -selection clipboard ``` PASTE MARKDOWN OUTPUT HERE ### Additional context <!-- Add any other relevant context about the problem here, like links to other issues or pull requests, screenshots, etc. --> �����������������������������������������������������������������python-griffe-1.6.2/.github/FUNDING.yml�������������������������������������������������������������0000664�0001750�0001750�00000000037�14767006246�017354� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������github: pawamoy polar: pawamoy �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/workflows/��������������������������������������������������������������0000775�0001750�0001750�00000000000�14767006246�017574� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/workflows/release.yml���������������������������������������������������0000664�0001750�0001750�00000002360�14767006246�021740� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: release on: push permissions: contents: write jobs: release: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Setup uv uses: astral-sh/setup-uv@v5 - name: Build dists if: github.repository_owner == 'pawamoy-insiders' run: uv tool run --from build pyproject-build - name: Upload dists artifact uses: actions/upload-artifact@v4 if: github.repository_owner == 'pawamoy-insiders' with: name: griffe-insiders path: ./dist/* - name: Prepare release notes if: github.repository_owner != 'pawamoy-insiders' run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release with assets uses: softprops/action-gh-release@v2 if: github.repository_owner == 'pawamoy-insiders' with: files: ./dist/* - name: Create release uses: softprops/action-gh-release@v2 if: github.repository_owner != 'pawamoy-insiders' with: body_path: release-notes.md ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.github/workflows/ci.yml��������������������������������������������������������0000664�0001750�0001750�00000006417�14767006246�020722� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: ci on: push: pull_request: branches: - main defaults: run: shell: bash env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 PYTHON_VERSIONS: "" jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Graphviz uses: ts-graphviz/setup-graphviz@v2 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Setup uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml - name: Install dependencies run: make setup - name: Check if the documentation builds correctly run: make check-docs - name: Check the code quality run: make check-quality - name: Check if the code is correctly typed run: make check-types - name: Check for breaking changes in the API run: make check-api - name: Store objects inventory for tests uses: actions/upload-artifact@v4 with: name: objects.inv path: site/objects.inv exclude-test-jobs: runs-on: ubuntu-latest outputs: jobs: ${{ steps.exclude-jobs.outputs.jobs }} steps: - id: exclude-jobs run: | if ${{ github.repository_owner == 'pawamoy-insiders' }}; then echo 'jobs=[ {"os": "macos-latest"}, {"os": "windows-latest"}, {"python-version": "3.10"}, {"python-version": "3.11"}, {"python-version": "3.12"}, {"python-version": "3.13"}, {"python-version": "3.14"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else echo 'jobs=[ {"os": "macos-latest", "resolution": "lowest-direct"}, {"os": "windows-latest", "resolution": "lowest-direct"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT fi tests: needs: - quality - exclude-test-jobs strategy: matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" resolution: - highest - lowest-direct exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.14' || matrix.python-version == '3.13' }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Setup uv uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml cache-suffix: ${{ matrix.resolution }} - name: Install dependencies env: UV_RESOLUTION: ${{ matrix.resolution }} run: make setup - name: Download objects inventory uses: actions/download-artifact@v4 with: name: objects.inv path: site/ - name: Run the test suite run: make test �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.gitignore����������������������������������������������������������������������0000664�0001750�0001750�00000000412�14767006246�016164� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# editors .idea/ .vscode/ # python *.egg-info/ *.py[cod] .venv/ .venvs/ /build/ /dist/ # tools .coverage* /.pdm-build/ /htmlcov/ /site/ uv.lock # cache .cache/ .pytest_cache/ .mypy_cache/ .ruff_cache/ __pycache__/ # tasks profile.pstats profile.svg .hypothesis/ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-1.6.2/.copier-answers.yml�������������������������������������������������������������0000664�0001750�0001750�00000001520�14767006246�017737� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changes here will be overwritten by Copier. _commit: 1.8.1 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothée Mazzucotelli author_username: pawamoy copyright_date: '2021' copyright_holder: Timothée Mazzucotelli copyright_holder_email: dev@pawamoy.fr copyright_license: ISC insiders: true insiders_email: insiders@pawamoy.fr insiders_repository_name: griffe project_description: Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API. project_name: Griffe public_release: true python_package_command_line_name: griffe python_package_distribution_name: griffe python_package_import_name: griffe repository_name: griffe repository_namespace: mkdocstrings repository_provider: github.com ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������