python-griffe-0.48.0/0000775000175000017500000000000014645165123014205 5ustar katharakatharapython-griffe-0.48.0/.envrc0000664000175000017500000000002114645165123015314 0ustar katharakatharaPATH_add scripts python-griffe-0.48.0/tests/0000775000175000017500000000000014645165123015347 5ustar katharakatharapython-griffe-0.48.0/tests/test_loader.py0000664000175000017500000004741414645165123020240 0ustar katharakathara"""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_pyfile, temporary_pypackage, temporary_visited_package from tests.helpers import clear_sys_modules if TYPE_CHECKING: from pathlib import Path from griffe.dataclasses 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', 'WatchfilesRustInternalError' 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("_json") assert "_ast" in loader.modules_collection assert "_collections" in loader.modules_collection assert "_json" 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.""" with temporary_pypackage("pkg", {"__init__.py": "a = 0", "mod.py": "b = 1"}) as pkg: static_loader = GriffeLoader(force_inspection=False, search_paths=[pkg.tmpdir]) dynamic_loader = GriffeLoader(force_inspection=True, search_paths=[pkg.tmpdir]) static_package = static_loader.load("pkg") dynamic_package = dynamic_loader.load("pkg") 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 clear_sys_modules("pkg") 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") assert alias.is_alias assert not alias.resolved python-griffe-0.48.0/tests/test_encoders.py0000664000175000017500000000323514645165123020565 0ustar katharakathara"""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: schema = json.load(f) validate(data, schema) python-griffe-0.48.0/tests/test_functions.py0000664000175000017500000001245614645165123021000 0ustar katharakathara"""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-0.48.0/tests/test_docstrings/0000775000175000017500000000000014645165123020565 5ustar katharakatharapython-griffe-0.48.0/tests/test_docstrings/__init__.py0000664000175000017500000000003414645165123022673 0ustar katharakathara"""Tests for docstrings.""" python-griffe-0.48.0/tests/test_docstrings/test_google.py0000664000175000017500000012213614645165123023457 0ustar katharakathara"""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, DocstringReturn, 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_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" # ============================================================================================= # 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 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 "" 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 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 "" 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 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_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-0.48.0/tests/test_docstrings/test_sphinx.py0000664000175000017500000007447014645165123023523 0ustar katharakathara"""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-0.48.0/tests/test_docstrings/test_warnings.py0000664000175000017500000000150414645165123024026 0ustar katharakathara"""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), # I only changed this line 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-0.48.0/tests/test_docstrings/test_numpy.py0000664000175000017500000010444614645165123023357 0ustar katharakathara"""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") ``` """ 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" # ============================================================================================= # 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 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 "" 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 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 "" 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-0.48.0/tests/test_docstrings/helpers.py0000664000175000017500000000410314645165123022577 0ustar katharakathara"""This module contains helpers for testing docstring parsing.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Iterator, List, Protocol, Tuple, Union from griffe import ( Attribute, Class, Docstring, DocstringSection, Function, LogLevel, Module, ) if TYPE_CHECKING: 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._warn def parse(docstring: str, parent: ParentType | None = None, **parser_opts: Any) -> ParseResultType: """Parse a doctring. 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._warn = lambda _docstring, _offset, message, log_level=LogLevel.warning: warnings.append(message) # type: ignore[attr-defined] 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._warn = original_warn # type: ignore[attr-defined] python-griffe-0.48.0/tests/test_docstrings/conftest.py0000664000175000017500000000146114645165123022766 0ustar katharakathara"""Pytest fixture for docstrings tests.""" from __future__ import annotations from typing import Iterator import pytest from _griffe.docstrings import google, numpy, sphinx from tests.test_docstrings.helpers import ParserType, parser @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-0.48.0/tests/test_public_api.py0000664000175000017500000000144014645165123021066 0ustar katharakathara"""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-0.48.0/tests/fixtures/0000775000175000017500000000000014645165123017220 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/0000775000175000017500000000000014645165123020324 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/v0.1.0/0000775000175000017500000000000014645165123021146 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/v0.1.0/my_module/0000775000175000017500000000000014645165123023140 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/v0.1.0/my_module/__init__.py0000664000175000017500000000002614645165123025247 0ustar katharakathara__version__ = "0.1.0" python-griffe-0.48.0/tests/fixtures/_repo/v0.2.0/0000775000175000017500000000000014645165123021147 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/v0.2.0/my_module/0000775000175000017500000000000014645165123023141 5ustar katharakatharapython-griffe-0.48.0/tests/fixtures/_repo/v0.2.0/my_module/__init__.py0000664000175000017500000000002614645165123025250 0ustar katharakathara__version__ = "0.2.0" python-griffe-0.48.0/tests/__init__.py0000664000175000017500000000030314645165123017454 0ustar katharakathara"""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-0.48.0/tests/test_visitor.py0000664000175000017500000002724214645165123020466 0ustar katharakathara"""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__ = moda_all + modb_all + modc_all + ["CONST_INIT"]""", """__all__ = ["CONST_INIT", *moda_all, *modb_all, *modc_all]""", """ __all__ = ["CONST_INIT"] __all__ += moda_all + modb_all + modc_all """, """ __all__ = moda_all + modb_all + modc_all __all__ += ["CONST_INIT"] """, """ __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" python-griffe-0.48.0/tests/test_stdlib.py0000664000175000017500000000310314645165123020236 0ustar katharakathara"""Fuzzing on the standard library.""" from __future__ import annotations import sys from contextlib import suppress from typing import TYPE_CHECKING, Iterator import pytest from griffe import GriffeLoader, LoadingError if TYPE_CHECKING: from griffe.dataclasses 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-0.48.0/tests/test_mixins.py0000664000175000017500000000065014645165123020270 0ustar katharakathara"""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-0.48.0/tests/test_merger.py0000664000175000017500000000402514645165123020242 0ustar katharakathara"""Tests for the `merger` module.""" from __future__ import annotations from textwrap import dedent from griffe import GriffeLoader, temporary_pypackage def test_dont_trigger_alias_resolution_when_merging_stubs() -> None: """Assert that we don't trigger alias resolution when merging stubs.""" with temporary_pypackage("package", ["mod.py", "mod.pyi"]) as tmp_package: tmp_package.path.joinpath("mod.py").write_text( dedent( """ import pathlib def f() -> pathlib.Path: return pathlib.Path() """, ), ) tmp_package.path.joinpath("mod.pyi").write_text( dedent( """ import pathlib def f() -> pathlib.Path: ... """, ), ) loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) loader.load(tmp_package.name) def test_merge_stubs_on_wildcard_imported_objects() -> None: """Assert that stubs can be merged on wildcard imported objects.""" with temporary_pypackage("package", ["mod.py", "__init__.pyi"]) as tmp_package: tmp_package.path.joinpath("mod.py").write_text( dedent( """ class A: def hello(value: int | str) -> int | str: return value """, ), ) tmp_package.path.joinpath("__init__.py").write_text("from .mod import *") tmp_package.path.joinpath("__init__.pyi").write_text( dedent( """ from typing import overload class A: @overload def hello(value: int) -> int: ... @overload def hello(value: str) -> str: ... """, ), ) loader = GriffeLoader(search_paths=[tmp_package.tmpdir]) module = loader.load(tmp_package.name) assert module["A.hello"].overloads python-griffe-0.48.0/tests/test_nodes.py0000664000175000017500000002031214645165123020066 0ustar katharakathara"""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-0.48.0/tests/test_internals.py0000664000175000017500000001725014645165123020764 0ustar katharakathara"""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 Iterator import pytest from mkdocstrings.inventory import Inventory import griffe @pytest.fixture(name="loader", scope="module") def _fixture_loader() -> griffe.GriffeLoader: # noqa: PT005 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: # noqa: PT005 return loader.modules_collection["_griffe"] @pytest.fixture(name="public_api", scope="module") def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: # noqa: PT005 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]: # noqa: PT005 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]: # noqa: PT005 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]: # noqa: PT005 return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) @pytest.fixture(name="inventory", scope="module") def _fixture_inventory() -> Inventory: # noqa: PT005 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 = [] for obj in modulelevel_internal_objects: if obj.name not in griffe.__all__ or not hasattr(griffe, obj.name): not_exposed.append(obj.path) 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.""" not_in_inventory = [] ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"} ignore_paths = {"griffe.DataclassesExtension.*"} 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 ): not_in_inventory.append(obj.path) 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) # YORE: Bump 1: Remove line. and not (obj.is_module and obj.package.name == "griffe") ): 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-0.48.0/tests/test_models.py0000664000175000017500000003165014645165123020250 0ustar katharakathara"""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, module_vtree, temporary_inspected_module, temporary_pypackage, 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.""" 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"] python-griffe-0.48.0/tests/helpers.py0000664000175000017500000000066514645165123017372 0ustar katharakathara"""Helpers for tests.""" import sys def clear_sys_modules(name: str) -> None: """Clear `sys.modules` of a module and its submodules. Use this function after having used `temporary_pypackage` and `inspect` together. Parameters: name: A top-level module name. """ for module in list(sys.modules.keys()): if module == name or module.startswith(f"{name}."): sys.modules.pop(module, None) python-griffe-0.48.0/tests/test_cli.py0000664000175000017500000000255314645165123017534 0ustar katharakathara"""Tests for the `cli` module.""" 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-0.48.0/tests/test_inheritance.py0000664000175000017500000001336414645165123021260 0ustar katharakathara"""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.dataclasses 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-0.48.0/tests/test_expressions.py0000664000175000017500000000512614645165123021346 0ustar katharakathara"""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 python-griffe-0.48.0/tests/conftest.py0000664000175000017500000000005714645165123017550 0ustar katharakathara"""Configuration for the pytest test suite.""" python-griffe-0.48.0/tests/test_extensions.py0000664000175000017500000001146514645165123021166 0ustar katharakathara"""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.agents.nodes import ObjectNode from griffe.dataclasses import Attribute, Class, Function, Module, Object 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) -> None: # noqa: D102,ARG002 self.records.append("on_attribute_instance") def on_attribute_node(self, *, node: ast.AST | ObjectNode) -> None: # noqa: D102,ARG002 self.records.append("on_attribute_node") def on_class_instance(self, *, node: ast.AST | ObjectNode, cls: Class) -> None: # noqa: D102,ARG002 self.records.append("on_class_instance") def on_class_members(self, *, node: ast.AST | ObjectNode, cls: Class) -> None: # noqa: D102,ARG002 self.records.append("on_class_members") def on_class_node(self, *, node: ast.AST | ObjectNode) -> None: # noqa: D102,ARG002 self.records.append("on_class_node") def on_function_instance(self, *, node: ast.AST | ObjectNode, func: Function) -> None: # noqa: D102,ARG002 self.records.append("on_function_instance") def on_function_node(self, *, node: ast.AST | ObjectNode) -> None: # noqa: D102,ARG002 self.records.append("on_function_node") def on_instance(self, *, node: ast.AST | ObjectNode, obj: Object) -> None: # noqa: D102,ARG002 self.records.append("on_instance") def on_members(self, *, node: ast.AST | ObjectNode, obj: Object) -> None: # noqa: D102,ARG002 self.records.append("on_members") def on_module_instance(self, *, node: ast.AST | ObjectNode, mod: Module) -> None: # noqa: D102,ARG002 self.records.append("on_module_instance") def on_module_members(self, *, node: ast.AST | ObjectNode, mod: Module) -> None: # noqa: D102,ARG002 self.records.append("on_module_members") def on_module_node(self, *, node: ast.AST | ObjectNode) -> None: # noqa: D102,ARG002 self.records.append("on_module_node") def on_node(self, *, node: ast.AST | ObjectNode) -> 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-0.48.0/tests/test_inspector.py0000664000175000017500000001345714645165123021000 0ustar katharakathara"""Test inspection mechanisms.""" from __future__ import annotations from pathlib import Path import pytest from griffe import inspect, temporary_inspected_module, 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 temporary_pypackage("package", ["module.py"]) as tmp_package: filepath = Path(tmp_package.path, "module.py") filepath.write_text("import missing") with pytest.raises(ImportError, match="ModuleNotFoundError: No module named 'missing'"): inspect("package.module", filepath=filepath, import_paths=[tmp_package.tmpdir]) clear_sys_modules("package") 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_pypackage("package", {"package.py": "a = 0"}) as tmp_package: inspect("package.package", filepath=tmp_package.path / "package.py", import_paths=[tmp_package.tmpdir]) clear_sys_modules("package") 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" python-griffe-0.48.0/tests/test_finder.py0000664000175000017500000002661714645165123020243 0ustar katharakathara"""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 = os.getcwd() 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 = os.getcwd() 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-0.48.0/tests/test_git.py0000664000175000017500000000662114645165123017550 0ustar katharakathara"""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-0.48.0/tests/test_diff.py0000664000175000017500000001514514645165123017676 0ustar katharakathara"""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-0.48.0/.copier-answers.yml0000664000175000017500000000152714645165123017754 0ustar katharakathara# Changes here will be overwritten by Copier _commit: 1.4.0 _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 License 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 python-griffe-0.48.0/pyproject.toml0000664000175000017500000000431614645165123017125 0ustar katharakathara[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 = {text = "ISC"} readme = "README.md" requires-python = ">=3.8" keywords = ["api", "signature", "breaking-changes", "static-analysis", "dynamic-analysis"] dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ "astunparse>=1.6; python_version < '3.9'", "backports-strenum>=1.3; python_version < '3.11'", "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 = "scm"} [tool.pdm.build] package-dir = "src" editable-backend = "editables" excludes = ["**/.pytest_cache"] source-includes = [ "config", "docs", "scripts", "share", "tests", "devdeps.txt", "duties.py", "mkdocs.yml", "*.md", "LICENSE", ] [tool.pdm.build.wheel-data] data = [ {path = "share/**/*", relative-to = "."}, ] python-griffe-0.48.0/devdeps.txt0000664000175000017500000000136514645165123016405 0ustar katharakathara# dev editables>=0.5 # maintenance build>=1.2 git-changelog>=2.5 twine>=5.0; python_version < '3.13' # ci duty>=1.4 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 black>=24.4 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-committers-plugin-2>=2.3 mkdocs-git-revision-date-localized-plugin>=1.2 mkdocs-literate-nav>=0.6 mkdocs-material>=9.5 mkdocs-minify-plugin>=0.8 mkdocs-section-index>=0.3 mkdocs-redirects>=1.2 mkdocstrings[python]>=0.25 pydeps>=1.12 tomli>=2.0; python_version < '3.11' python-griffe-0.48.0/.gitignore0000664000175000017500000000040214645165123016171 0ustar katharakathara# editors .idea/ .vscode/ # python *.egg-info/ *.py[cod] .venv/ .venvs/ /build/ /dist/ # tools .coverage* /.pdm-build/ /htmlcov/ /site/ # cache .cache/ .pytest_cache/ .mypy_cache/ .ruff_cache/ __pycache__/ # tasks profile.pstats profile.svg .hypothesis/ python-griffe-0.48.0/.gitpod.yml0000664000175000017500000000021714645165123016274 0ustar katharakatharavscode: extensions: - ms-python.python image: file: .gitpod.dockerfile ports: - port: 8000 onOpen: notify tasks: - init: make setup python-griffe-0.48.0/README.md0000664000175000017500000000630514645165123015470 0ustar katharakathara# 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/) [![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/griffe) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings_griffe:gitter.im) Griffe logo, created by François Rozet <francois.rozet@outlook.com> 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 With `pip`: ```bash pip install griffe ``` With [`pipx`](https://github.com/pipxproject/pipx): ```bash python3.8 -m pip install --user pipx pipx install griffe ``` ## Usage **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. Or 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. **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-0.48.0/LICENSE0000664000175000017500000000136214645165123015214 0ustar katharakatharaISC 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-0.48.0/.github/0000775000175000017500000000000014645165123015545 5ustar katharakatharapython-griffe-0.48.0/.github/ISSUE_TEMPLATE/0000775000175000017500000000000014645165123017730 5ustar katharakatharapython-griffe-0.48.0/.github/ISSUE_TEMPLATE/4-change.md0000664000175000017500000000112614645165123021640 0ustar katharakathara--- 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. ### Describe the solution you'd like ### Describe alternatives you've considered ### Additional context python-griffe-0.48.0/.github/ISSUE_TEMPLATE/1-bug.md0000664000175000017500000000267714645165123021201 0ustar katharakathara--- name: Bug report about: Create a bug report to help us improve. title: "bug: " labels: unconfirmed assignees: [pawamoy] --- ### Description of the bug ### To Reproduce ``` WRITE MRE / INSTRUCTIONS HERE ``` ### Full traceback
Full traceback ```python PASTE TRACEBACK HERE ```
### Expected behavior ### Environment information ```bash griffe --debug-info # | xclip -selection clipboard ``` PASTE MARKDOWN OUTPUT HERE ### Additional context python-griffe-0.48.0/.github/ISSUE_TEMPLATE/2-feature.md0000664000175000017500000000121314645165123022041 0ustar katharakathara--- 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. ### Describe the solution you'd like ### Describe alternatives you've considered ### Additional context python-griffe-0.48.0/.github/ISSUE_TEMPLATE/3-docs.md0000664000175000017500000000113114645165123021336 0ustar katharakathara--- 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? ### Relevant code snippets ### Link to the relevant documentation section python-griffe-0.48.0/.github/ISSUE_TEMPLATE/config.yml0000664000175000017500000000033014645165123021714 0ustar katharakatharablank_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-0.48.0/.github/FUNDING.yml0000664000175000017500000000012614645165123017361 0ustar katharakatharagithub: pawamoy ko_fi: pawamoy polar: pawamoy custom: - https://www.paypal.me/pawamoy python-griffe-0.48.0/.github/workflows/0000775000175000017500000000000014645165123017602 5ustar katharakatharapython-griffe-0.48.0/.github/workflows/release.yml0000664000175000017500000000254614645165123021754 0ustar katharakatharaname: 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 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Setup Python uses: actions/setup-python@v4 - name: Install build if: github.repository_owner == 'pawamoy-insiders' run: python -m pip install build - name: Build dists if: github.repository_owner == 'pawamoy-insiders' run: python -m build - name: Upload dists artifact uses: actions/upload-artifact@v4 if: github.repository_owner == 'pawamoy-insiders' with: name: griffe-insiders path: ./dist/* - name: Install git-changelog if: github.repository_owner != 'pawamoy-insiders' run: pip install git-changelog - name: Prepare release notes if: github.repository_owner != 'pawamoy-insiders' run: git-changelog --release-notes > release-notes.md - name: Create release with assets uses: softprops/action-gh-release@v1 if: github.repository_owner == 'pawamoy-insiders' with: files: ./dist/* - name: Create release uses: softprops/action-gh-release@v1 if: github.repository_owner != 'pawamoy-insiders' with: body_path: release-notes.md python-griffe-0.48.0/.github/workflows/ci.yml0000664000175000017500000000602414645165123020722 0ustar katharakatharaname: 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 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up Graphviz uses: ts-graphviz/setup-graphviz@v2 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install uv run: pip install uv - 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.9"}, {"python-version": "3.10"}, {"python-version": "3.11"}, {"python-version": "3.12"}, {"python-version": "3.13"} ]' | 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.8" - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" resolution: - highest - lowest-direct exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.13' }} steps: - name: Checkout uses: actions/checkout@v4 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install uv run: pip install uv - 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-0.48.0/CHANGELOG.md0000664000175000017500000041664714645165123016040 0ustar katharakathara# 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). ## [0.48.0](https://github.com/mkdocstrings/griffe/releases/tag/0.48.0) - 2024-07-15 [Compare with 0.47.0](https://github.com/mkdocstrings/griffe/compare/0.47.0...0.48.0) 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 [Compare with 0.46.1](https://github.com/mkdocstrings/griffe/compare/0.46.1...0.47.0) 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 [Compare with 0.46.0](https://github.com/mkdocstrings/griffe/compare/0.46.0...0.46.1) 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 [Compare with 0.45.3](https://github.com/mkdocstrings/griffe/compare/0.45.3...0.46.0) 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()`][griffe.Object.member_is_exported], [`is_explicitely_exported`][griffe.ObjectAliasMixin.is_explicitely_exported], [`is_implicitely_exported`][griffe.ObjectAliasMixin.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 - 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 [Compare with 0.45.2](https://github.com/mkdocstrings/griffe/compare/0.45.2...0.45.3) ### 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 [Compare with 0.45.1](https://github.com/mkdocstrings/griffe/compare/0.45.1...0.45.2) ### 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 [Compare with 0.45.0](https://github.com/mkdocstrings/griffe/compare/0.45.0...0.45.1) ### 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 [Compare with 0.44.0](https://github.com/mkdocstrings/griffe/compare/0.44.0...0.45.0) ### 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 [Compare with 0.43.0](https://github.com/mkdocstrings/griffe/compare/0.43.0...0.44.0) ### 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 [Compare with 0.42.2](https://github.com/mkdocstrings/griffe/compare/0.42.2...0.43.0) ### 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 [Compare with 0.42.1](https://github.com/mkdocstrings/griffe/compare/0.42.1...0.42.2) ### 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 [Compare with 0.42.0](https://github.com/mkdocstrings/griffe/compare/0.42.0...0.42.1) ### 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 [Compare with 0.41.3](https://github.com/mkdocstrings/griffe/compare/0.41.3...0.42.0) ### 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 [Compare with 0.41.2](https://github.com/mkdocstrings/griffe/compare/0.41.2...0.41.3) ### 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 [Compare with 0.41.1](https://github.com/mkdocstrings/griffe/compare/0.41.1...0.41.2) ### 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 [Compare with 0.41.0](https://github.com/mkdocstrings/griffe/compare/0.41.0...0.41.1) ### 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 [Compare with 0.40.1](https://github.com/mkdocstrings/griffe/compare/0.40.1...0.41.0) ### 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 depdendency 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 [Compare with 0.40.0](https://github.com/mkdocstrings/griffe/compare/0.40.0...0.40.1) ### 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 [Compare with 0.39.1](https://github.com/mkdocstrings/griffe/compare/0.39.1...0.40.0) ### 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 [Compare with 0.39.0](https://github.com/mkdocstrings/griffe/compare/0.39.0...0.39.1) ### 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 [Compare with 0.38.1](https://github.com/mkdocstrings/griffe/compare/0.38.1...0.39.0) ### 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 - 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 [Compare with 0.38.0](https://github.com/mkdocstrings/griffe/compare/0.38.0...0.38.1) ### 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 [Compare with 0.37.0](https://github.com/mkdocstrings/griffe/compare/0.37.0...0.38.0) ### 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 [Compare with 0.36.9](https://github.com/mkdocstrings/griffe/compare/0.36.9...0.37.0) ### 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 - 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 ### 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 [Compare with 0.36.8](https://github.com/mkdocstrings/griffe/compare/0.36.8...0.36.9) ### 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 enuemration 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 [Compare with 0.36.7](https://github.com/mkdocstrings/griffe/compare/0.36.7...0.36.8) ### 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 [Compare with 0.36.6](https://github.com/mkdocstrings/griffe/compare/0.36.6...0.36.7) ### 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 [Compare with 0.36.5](https://github.com/mkdocstrings/griffe/compare/0.36.5...0.36.6) ### 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 [Compare with 0.36.4](https://github.com/mkdocstrings/griffe/compare/0.36.4...0.36.5) ### 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 [Compare with 0.36.3](https://github.com/mkdocstrings/griffe/compare/0.36.3...0.36.4) ### 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 [Compare with 0.36.2](https://github.com/mkdocstrings/griffe/compare/0.36.2...0.36.3) ### 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 [Compare with 0.36.1](https://github.com/mkdocstrings/griffe/compare/0.36.1...0.36.2) ### 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 [Compare with 0.36.0](https://github.com/mkdocstrings/griffe/compare/0.36.0...0.36.1) ### 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 [Compare with 0.35.2](https://github.com/mkdocstrings/griffe/compare/0.35.2...0.36.0) ### 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 [Compare with 0.35.1](https://github.com/mkdocstrings/griffe/compare/0.35.1...0.35.2) ### 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 [Compare with 0.35.0](https://github.com/mkdocstrings/griffe/compare/0.35.0...0.35.1) ### 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 [Compare with 0.34.0](https://github.com/mkdocstrings/griffe/compare/0.34.0...0.35.0) ### 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 [Compare with 0.33.0](https://github.com/mkdocstrings/griffe/compare/0.33.0...0.34.0) ### 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 [Compare with 0.32.3](https://github.com/mkdocstrings/griffe/compare/0.32.3...0.33.0) ### 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 ### 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 [Compare with 0.32.2](https://github.com/mkdocstrings/griffe/compare/0.32.2...0.32.3) ### 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 [Compare with 0.32.1](https://github.com/mkdocstrings/griffe/compare/0.32.1...0.32.2) ### 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 [Compare with 0.32.0](https://github.com/mkdocstrings/griffe/compare/0.32.0...0.32.1) ### 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 [Compare with 0.31.0](https://github.com/mkdocstrings/griffe/compare/0.31.0...0.32.0) ### Deprecations - Classes [`InspectorExtension`][griffe.InspectorExtension] and [`VisitorExtension`][griffe.VisitorExtension] are deprecated in favor of [`Extension`][griffe.Extension]. As a side-effect, the [`hybrid`][griffe.HybridExtension] 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 - Fucntion `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 - 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 [Compare with 0.30.1](https://github.com/mkdocstrings/griffe/compare/0.30.1...0.31.0) ### 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 [Compare with 0.30.0](https://github.com/mkdocstrings/griffe/compare/0.30.0...0.30.1) ### 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 [Compare with 0.29.1](https://github.com/mkdocstrings/griffe/compare/0.29.1...0.30.0) ### 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 [Compare with 0.29.0](https://github.com/mkdocstrings/griffe/compare/0.29.0...0.29.1) ### 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 [Compare with 0.28.2](https://github.com/mkdocstrings/griffe/compare/0.28.2...0.29.0) ### 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 [Compare with 0.28.1](https://github.com/mkdocstrings/griffe/compare/0.28.1...0.28.2) ### 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 [Compare with 0.28.0](https://github.com/mkdocstrings/griffe/compare/0.28.0...0.28.1) ### 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 [Compare with 0.27.5](https://github.com/mkdocstrings/griffe/compare/0.27.5...0.28.0) ### 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 [Compare with 0.27.4](https://github.com/mkdocstrings/griffe/compare/0.27.4...0.27.5) ### 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 [Compare with 0.27.3](https://github.com/mkdocstrings/griffe/compare/0.27.3...0.27.4) ### 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 [Compare with 0.27.2](https://github.com/mkdocstrings/griffe/compare/0.27.2...0.27.3) ### Bug Fixes - Allow setting doctring 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 [Compare with 0.27.1](https://github.com/mkdocstrings/griffe/compare/0.27.1...0.27.2) ### 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 - 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 [Compare with 0.27.0](https://github.com/mkdocstrings/griffe/compare/0.27.0...0.27.1) ### 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 ## [0.27.0](https://github.com/mkdocstrings/griffe/releases/tag/0.27.0) - 2023-04-10 [Compare with 0.26.0](https://github.com/mkdocstrings/griffe/compare/0.26.0...0.27.0) ### 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 ### 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 [Compare with 0.25.5](https://github.com/mkdocstrings/griffe/compare/0.25.5...0.26.0) ### 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 [Compare with 0.25.4](https://github.com/mkdocstrings/griffe/compare/0.25.4...0.25.5) ### 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 [Compare with 0.25.3](https://github.com/mkdocstrings/griffe/compare/0.25.3...0.25.4) ### 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 [Compare with 0.25.2](https://github.com/mkdocstrings/griffe/compare/0.25.2...0.25.3) ### 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 [Compare with 0.25.1](https://github.com/mkdocstrings/griffe/compare/0.25.1...0.25.2) ### 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 [Compare with 0.25.0](https://github.com/mkdocstrings/griffe/compare/0.25.0...0.25.1) ### 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 [Compare with 0.24.1](https://github.com/mkdocstrings/griffe/compare/0.24.1...0.25.0) ### 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 wilcard 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 [Compare with 0.24.0](https://github.com/mkdocstrings/griffe/compare/0.24.0...0.24.1) ### 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 [Compare with 0.23.0](https://github.com/mkdocstrings/griffe/compare/0.23.0...0.24.0) 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 [Compare with 0.22.2](https://github.com/mkdocstrings/griffe/compare/0.22.2...0.23.0) ### 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 [Compare with 0.22.1](https://github.com/mkdocstrings/griffe/compare/0.22.1...0.22.2) ### 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 [Compare with 0.22.0](https://github.com/mkdocstrings/griffe/compare/0.22.0...0.22.1) ### 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 [Compare with 0.21.0](https://github.com/mkdocstrings/griffe/compare/0.21.0...0.22.0) ### 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 [Compare with 0.20.0](https://github.com/mkdocstrings/griffe/compare/0.20.0...0.21.0) ### 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 [Compare with 0.19.3](https://github.com/mkdocstrings/griffe/compare/0.19.3...0.20.0) ### 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 [Compare with 0.19.2](https://github.com/mkdocstrings/griffe/compare/0.19.2...0.19.3) ### 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 [Compare with 0.19.1](https://github.com/mkdocstrings/griffe/compare/0.19.1...0.19.2) ### 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 [Compare with 0.19.0](https://github.com/mkdocstrings/griffe/compare/0.19.0...0.19.1) ### 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 [Compare with 0.18.0](https://github.com/mkdocstrings/griffe/compare/0.18.0...0.19.0) ### 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 [Compare with 0.17.0](https://github.com/mkdocstrings/griffe/compare/0.17.0...0.18.0) ### 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`-decoratored 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 [Compare with 0.16.0](https://github.com/mkdocstrings/griffe/compare/0.16.0...0.17.0) ### 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 [Compare with 0.15.1](https://github.com/mkdocstrings/griffe/compare/0.15.1...0.16.0) ### 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 [Compare with 0.15.0](https://github.com/mkdocstrings/griffe/compare/0.15.0...0.15.1) ### 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 [Compare with 0.14.1](https://github.com/mkdocstrings/griffe/compare/0.14.1...0.15.0) ### 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 [Compare with 0.14.0](https://github.com/mkdocstrings/griffe/compare/0.14.0...0.14.1) ### 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 [Compare with 0.13.2](https://github.com/mkdocstrings/griffe/compare/0.13.2...0.14.0) ### 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 [Compare with 0.13.1](https://github.com/mkdocstrings/griffe/compare/0.13.1...0.13.2) ### 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 [Compare with 0.13.0](https://github.com/mkdocstrings/griffe/compare/0.13.0...0.13.1) ### 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 [Compare with 0.12.6](https://github.com/mkdocstrings/griffe/compare/0.12.6...0.13.0) ### 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 [Compare with 0.12.5](https://github.com/mkdocstrings/griffe/compare/0.12.5...0.12.6) ### 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 [Compare with 0.12.4](https://github.com/mkdocstrings/griffe/compare/0.12.4...0.12.5) ### 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 [Compare with 0.12.3](https://github.com/mkdocstrings/griffe/compare/0.12.3...0.12.4) ### 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 [Compare with 0.12.2](https://github.com/mkdocstrings/griffe/compare/0.12.2...0.12.3) ### 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 [Compare with 0.12.1](https://github.com/mkdocstrings/griffe/compare/0.12.1...0.12.2) ### 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 [Compare with 0.11.7](https://github.com/mkdocstrings/griffe/compare/0.11.7...0.12.1) ### 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 [Compare with 0.11.6](https://github.com/mkdocstrings/griffe/compare/0.11.6...0.11.7) ### 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 [Compare with 0.11.5](https://github.com/mkdocstrings/griffe/compare/0.11.5...0.11.6) ### 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 [Compare with 0.11.4](https://github.com/mkdocstrings/griffe/compare/0.11.4...0.11.5) ### 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 [Compare with 0.11.3](https://github.com/mkdocstrings/griffe/compare/0.11.3...0.11.4) ### 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 [Compare with 0.11.2](https://github.com/mkdocstrings/griffe/compare/0.11.2...0.11.3) ### 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 [Compare with 0.11.1](https://github.com/mkdocstrings/griffe/compare/0.11.1...0.11.2) ### 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 [Compare with 0.11.0](https://github.com/mkdocstrings/griffe/compare/0.11.0...0.11.1) ### 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 [Compare with 0.10.0](https://github.com/mkdocstrings/griffe/compare/0.10.0...0.11.0) ### 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 [Compare with 0.9.0](https://github.com/mkdocstrings/griffe/compare/0.9.0...0.10.0) ### 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 [Compare with 0.8.0](https://github.com/mkdocstrings/griffe/compare/0.8.0...0.9.0) ### 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 [Compare with 0.7.1](https://github.com/mkdocstrings/griffe/compare/0.7.1...0.8.0) ### 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 [Compare with 0.7.0](https://github.com/mkdocstrings/griffe/compare/0.7.0...0.7.1) ### 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 [Compare with 0.6.0](https://github.com/mkdocstrings/griffe/compare/0.6.0...0.7.0) ### 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 [Compare with 0.5.0](https://github.com/mkdocstrings/griffe/compare/0.5.0...0.6.0) ### 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 [Compare with 0.4.0](https://github.com/mkdocstrings/griffe/compare/0.4.0...0.5.0) ### 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 [Compare with 0.3.0](https://github.com/mkdocstrings/griffe/compare/0.3.0...0.4.0) ### 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 in[tro]spection ([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 [Compare with 0.2.0](https://github.com/mkdocstrings/griffe/compare/0.2.0...0.3.0) ### 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 [Compare with 0.1.0](https://github.com/mkdocstrings/griffe/compare/0.1.0...0.2.0) ### 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 [Compare with first commit](https://github.com/mkdocstrings/griffe/compare/7ea73adcc6aebcbe0eb64982916220773731a6b3...0.1.0) ### 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-0.48.0/scripts/0000775000175000017500000000000014645165123015674 5ustar katharakatharapython-griffe-0.48.0/scripts/gen_credits.py0000664000175000017500000001504414645165123020540 0ustar katharakathara"""Script to generate the project's credits.""" from __future__ import annotations import os import sys from collections import defaultdict from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent from typing import Dict, Iterable, 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"] with project_dir.joinpath("devdeps.txt").open() as devdeps_file: devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().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 = [] for classifier in metadata["classifier"]: if classifier.startswith("License ::"): license_names.append(classifier.rsplit("::", 1)[1].strip()) 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-0.48.0/scripts/gen_griffe_json.py0000664000175000017500000000046114645165123021373 0ustar katharakathara"""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-0.48.0/scripts/make0000775000175000017500000002645114645165123016547 0ustar katharakathara#!/usr/bin/env python3 """Management commands.""" from __future__ import annotations import os import shutil import subprocess import sys from contextlib import contextmanager from pathlib import Path from typing import Any, Callable, Iterator PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() _exe = "" _prefix = "" _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() -> None: uv_opts = "" if "UV_RESOLUTION" in os.environ: uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" requirements = _shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) _shell("uv pip install -r -", input=requirements, text=True) if "CI" not in os.environ: _shell("uv pip install --no-deps -e .") else: _shell("uv pip install --no-deps .") def _activate(path: str) -> None: global _exe, _prefix # noqa: PLW0603 if (bin := Path(path, "bin")).exists(): activate_script = bin / "activate_this.py" elif (scripts := Path(path, "Scripts")).exists(): activate_script = scripts / "activate_this.py" _exe = ".exe" _prefix = f"{path}/Scripts/" else: raise ValueError(f"make: activate: Cannot find activation script in {path}") if not activate_script.exists(): raise ValueError(f"make: activate: Cannot find activation script in {path}") exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 def _run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: kwargs = {"check": True, **kwargs} if version == "default": _activate(".venv") subprocess.run([f"{_prefix}{cmd}{_exe}", *args], **kwargs) # noqa: S603, PLW1510 else: _activate(f".venvs/{version}") os.environ["MULTIRUN"] = "1" subprocess.run([f"{_prefix}{cmd}{_exe}", *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") for cmd in _commands: print(f" {cmd.__cmdname__:21} {cmd.__doc__.splitlines()[0]}") # type: ignore[attr-defined,union-attr] try: _run("default", "python", "-V", capture_output=True) except (subprocess.CalledProcessError, ValueError): pass else: 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" $ 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() 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() @_command("run") def run(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in the default virtual environment. ```bash make run [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 [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 [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 [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 any(dirpath.match(pattern) for pattern in cache_dirs) and not ( dirpath.match(".venv") or dirpath.match(".venvs") ): shutil.rmtree(path, 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 everytime 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-0.48.0/scripts/insiders.py0000664000175000017500000001532014645165123020067 0ustar katharakathara"""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 Iterable, cast from urllib.error import HTTPError from urllib.parse import urljoin from urllib.request import urlopen import yaml logger = logging.getLogger(f"mkdocs.logs.{__name__}") def human_readable_amount(amount: int) -> str: # noqa: D103 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: """Class representing an Insiders project.""" name: str url: str @dataclass class Feature: """Class representing an Insiders 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: """Class representing an Insiders 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]: """Load goals from JSON data. Parameters: data: The JSON data. funding: The current total funding, per month. origin: The origin of the data (URL). Returns: A dictionaries of goals, keys being their target monthly amount. """ 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]: """Load funding goals from a given data source. Parameters: source: The data source (local file path or URL). funding: The current total funding, per month. Returns: A dictionaries of goals, keys being their target monthly amount. """ 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]: """Extract feature list from funding goals. Parameters: goals: A list of funding goals. Returns: A list of features. """ return list(chain.from_iterable(goal.features for goal in goals)) def load_json(url: str) -> str | list | dict: # noqa: D103 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-0.48.0/scripts/gen_structure_docs.py0000664000175000017500000000623614645165123022156 0ustar katharakathara"""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'
{svg}
') 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-0.48.0/scripts/make.py0000775000175000017500000002654514645165123017202 0ustar katharakathara#!/usr/bin/env python3 """Management commands.""" from __future__ import annotations import os import shutil import subprocess import sys from contextlib import contextmanager from pathlib import Path from typing import Any, Callable, Iterator PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() _exe = "" _prefix = "" _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() -> None: uv_opts = "" if "UV_RESOLUTION" in os.environ: uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" requirements = _shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) _shell("uv pip install -r -", input=requirements, text=True) if "CI" not in os.environ: _shell("uv pip install --no-deps -e .") else: _shell("uv pip install --no-deps .") def _activate(path: str) -> None: global _exe, _prefix # noqa: PLW0603 if (bin := Path(path, "bin")).exists(): activate_script = bin / "activate_this.py" elif (scripts := Path(path, "Scripts")).exists(): activate_script = scripts / "activate_this.py" _exe = ".exe" _prefix = f"{path}/Scripts/" else: raise ValueError(f"make: activate: Cannot find activation script in {path}") if not activate_script.exists(): raise ValueError(f"make: activate: Cannot find activation script in {path}") exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 def _run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: kwargs = {"check": True, **kwargs} if version == "default": _activate(".venv") subprocess.run([f"{_prefix}{cmd}{_exe}", *args], **kwargs) # noqa: S603, PLW1510 else: _activate(f".venvs/{version}") os.environ["MULTIRUN"] = "1" subprocess.run([f"{_prefix}{cmd}{_exe}", *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") for cmd in _commands: print(f" {cmd.__cmdname__:21} {cmd.__doc__.splitlines()[0]}") # type: ignore[attr-defined,union-attr] try: _run("default", "python", "-V", capture_output=True) except (subprocess.CalledProcessError, ValueError): pass else: 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() 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() @_command("run") def run(cmd: str, *args: str, **kwargs: Any) -> None: """Run a command in the default virtual environment. ```bash make run [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 [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 [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 [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 any(dirpath.match(pattern) for pattern in cache_dirs) and not ( dirpath.match(".venv") or dirpath.match(".venvs") ): shutil.rmtree(path, 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 everytime 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-0.48.0/.gitpod.dockerfile0000664000175000017500000000017314645165123017603 0ustar katharakatharaFROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ pipx install uv; \ pipx ensurepath python-griffe-0.48.0/config/0000775000175000017500000000000014645165123015452 5ustar katharakatharapython-griffe-0.48.0/config/ruff.toml0000664000175000017500000000522414645165123017314 0ustar katharakatharatarget-version = "py38" line-length = 120 [lint] exclude = [ "tests/fixtures/*.py", ] select = [ "A", "ANN", "ARG", "B", "BLE", "C", "C4", "COM", "D", "DTZ", "E", "ERA", "EXE", "F", "FBT", "G", "I", "ICN", "INP", "ISC", "N", "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", "Q", "RUF", "RSE", "RET", "S", "SIM", "SLF", "T", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", ] ignore = [ "A001", # Variable is shadowing a Python builtin "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 "ERA001", # Commented out code "G004", # Logging statement uses f-string "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "SLF001", # Private member accessed "TRY003", # Avoid specifying long messages outside the exception class ] [lint.per-file-ignores] "src/*/__main__.py" = [ "D100", # Missing module docstring ] "src/*/cli.py" = [ "T201", # Print statement ] "src/*/git.py" = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path ] "tests/test_git.py" = [ "S603", # `subprocess` call: check for execution of untrusted input "S607", # Starting a process with a partial executable path ] "src/*/*/nodes/*.py" = [ "ARG001", # Unused function argument "N812", # Lowercase `keyword` imported as non-lowercase `NodeKeyword` ] "src/*/debug.py" = [ "T201", # Print statement ] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement ] "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-0.48.0/config/pytest.ini0000664000175000017500000000077014645165123017507 0ustar katharakathara[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:.* # YORE: Bump 1: Remove line. ignore:.*`get_logger`:DeprecationWarning:_griffe # YORE: Bump 1: Remove line. ignore:.*`name`:DeprecationWarning:_griffe python-griffe-0.48.0/config/coverage.ini0000664000175000017500000000061014645165123017743 0ustar katharakathara[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-0.48.0/config/git-changelog.toml0000664000175000017500000000034014645165123021054 0ustar katharakatharabump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" versioning = "pep440" python-griffe-0.48.0/config/vscode/0000775000175000017500000000000014645165123016735 5ustar katharakatharapython-griffe-0.48.0/config/vscode/settings.json0000664000175000017500000000170414645165123021472 0ustar katharakathara{ "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-0.48.0/config/vscode/tasks.json0000664000175000017500000000460514645165123020762 0ustar katharakathara{ "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-0.48.0/config/vscode/launch.json0000664000175000017500000000217714645165123021111 0ustar katharakathara{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false }, { "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-0.48.0/config/mypy.ini0000664000175000017500000000016214645165123017150 0ustar katharakathara[mypy] ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true python-griffe-0.48.0/src/0000775000175000017500000000000014645165123014774 5ustar katharakatharapython-griffe-0.48.0/src/griffe/0000775000175000017500000000000014645165123016236 5ustar katharakatharapython-griffe-0.48.0/src/griffe/tests.py0000664000175000017500000000057414645165123017760 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.tests` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/__main__.py0000664000175000017500000000052514645165123020332 0ustar katharakathara# 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-0.48.0/src/griffe/finder.py0000664000175000017500000000057514645165123020066 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.finder` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/c3linear.py0000664000175000017500000000057714645165123020321 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.c3linear` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/dataclasses.py0000664000175000017500000000060214645165123021075 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.dataclasses` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/logger.py0000664000175000017500000000057514645165123020076 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.logger` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/__init__.py0000664000175000017500000002733414645165123020360 0ustar katharakathara# 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")`. If you need to alter our logger, do so temporarily and restore it to its original state, for example using [context managers][contextlib.contextmanager]. 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). """ 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 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 parse, parsers from _griffe.docstrings.sphinx import parse_sphinx from _griffe.docstrings.utils import DocstringWarningCallable, docstring_warning, parse_docstring_annotation from _griffe.encoders import JSONEncoder, json_decoder from _griffe.enumerations import ( BreakageKind, DocstringSectionKind, ExplanationStyle, Kind, LogLevel, ObjectKind, ParameterKind, Parser, # YORE: Bump 1: Remove line. When, ) 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, # YORE: Bump 1: Remove line. ExtensionType, # YORE: Bump 1: Remove line. InspectorExtension, LoadableExtensionType, # YORE: Bump 1: Remove line. VisitorExtension, builtin_extensions, load_extensions, ) from _griffe.extensions.dataclasses import DataclassesExtension # YORE: Bump 1: Remove line. from _griffe.extensions.hybrid import HybridExtension 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 # YORE: Bump 1: Replace `get_logger` with `logger` within line. # YORE: Bump 1: Replace `, patch_loggers` with `` within line. from _griffe.logger import Logger, get_logger, patch_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_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__ = [ "Alias", "AliasResolutionError", "Attribute", "AttributeChangedTypeBreakage", "AttributeChangedValueBreakage", "Breakage", "BreakageKind", "BuiltinModuleError", "Class", "ClassRemovedBaseBreakage", "CyclicAliasError", "DEFAULT_LOG_LEVEL", "DataclassesExtension", "Decorator", "DelMembersMixin", "Docstring", "DocstringAdmonition", "DocstringAttribute", "DocstringClass", "DocstringDeprecated", "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", "DocstringWarn", # YORE: Bump 1: Remove line. "DocstringWarningCallable", "DocstringYield", "ExplanationStyle", "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", # YORE: Bump 1: Remove line. "ExtensionType", "Extensions", "Function", "GetMembersMixin", "GitError", "GriffeError", "GriffeLoader", # YORE: Bump 1: Remove line. "HybridExtension", "Inspector", # YORE: Bump 1: Remove line. "InspectorExtension", "JSONEncoder", "Kind", "LastNodeError", "LinesCollection", "LoadableExtensionType", "LoadingError", "Logger", "LogLevel", "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", # YORE: Bump 1: Remove line. "VisitorExtension", # YORE: Bump 1: Remove line. "When", "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", "dump", "docstring_warning", "dynamic_import", "find_breaking_changes", "get__all__", "get_annotation", "get_base_class", "get_condition", "get_docstring", "get_expression", "get_instance_names", "get_latest_tag", # YORE: Bump 1: Remove line. "get_logger", "get_name", "get_names", "get_parameters", "get_parser", "get_repo_root", "get_value", "htree", "inspect", "json_decoder", "load", "load_extensions", "load_git", # YORE: Bump 1: Uncomment line. # "logger", "main", "merge_stubs", "module_vtree", "parse", "parse_docstring_annotation", "parse_google", "parse_numpy", "parse_sphinx", "parsers", "patch_logger", # YORE: Bump 1: Remove line. "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_pyfile", "temporary_pypackage", "temporary_visited_module", "temporary_visited_package", "tmp_worktree", "typing_overload", "visit", "vtree", ] python-griffe-0.48.0/src/griffe/collections.py0000664000175000017500000000060214645165123021124 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.collections` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/merger.py0000664000175000017500000000057514645165123020100 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.merger` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/importer.py0000664000175000017500000000057714645165123020462 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.importer` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/loader.py0000664000175000017500000000057514645165123020065 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.loader` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/expressions.py0000664000175000017500000000060214645165123021170 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.expressions` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/cli.py0000664000175000017500000000057214645165123017363 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.cli` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/agents/0000775000175000017500000000000014645165123017517 5ustar katharakatharapython-griffe-0.48.0/src/griffe/agents/__init__.py0000664000175000017500000000057514645165123021637 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.agents` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/agents/visitor.py0000664000175000017500000000060514645165123021571 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.agents.visitor` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/agents/inspector.py0000664000175000017500000000060714645165123022102 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.agents.inspector` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/agents/nodes.py0000664000175000017500000000112414645165123021177 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.agents.nodes` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "Name": warnings.warn( "The `Name` class was renamed `ExportedName`.", DeprecationWarning, stacklevel=2, ) return griffe.ExportedName return getattr(griffe, name) python-griffe-0.48.0/src/griffe/stats.py0000664000175000017500000000113214645165123017743 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.stats` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "stats": warnings.warn( "The 'stats' function was made into a class and renamed 'Stats'.", DeprecationWarning, stacklevel=2, ) return griffe.Stats return getattr(griffe, name) python-griffe-0.48.0/src/griffe/mixins.py0000664000175000017500000000057514645165123020126 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.mixins` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/exceptions.py0000664000175000017500000000060114645165123020766 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.exceptions` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/py.typed0000664000175000017500000000000014645165123017723 0ustar katharakatharapython-griffe-0.48.0/src/griffe/encoders.py0000664000175000017500000000057714645165123020423 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.encoders` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/git.py0000664000175000017500000000057214645165123017377 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.git` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/extensions/0000775000175000017500000000000014645165123020435 5ustar katharakatharapython-griffe-0.48.0/src/griffe/extensions/dataclasses.py0000664000175000017500000000061514645165123023300 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.extensions.dataclasses` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/extensions/base.py0000664000175000017500000000060614645165123021723 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.extensions.base` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/extensions/__init__.py0000664000175000017500000000060114645165123022543 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.extensions` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/extensions/hybrid.py0000664000175000017500000000061014645165123022265 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.extensions.hybrid` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/enumerations.py0000664000175000017500000000060314645165123021320 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.enumerations` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/0000775000175000017500000000000014645165123020415 5ustar katharakatharapython-griffe-0.48.0/src/griffe/docstrings/dataclasses.py0000664000175000017500000000061514645165123023260 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.dataclasses` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/parsers.py0000664000175000017500000000061114645165123022444 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.parsers` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/__init__.py0000664000175000017500000000060114645165123022523 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/sphinx.py0000664000175000017500000000113614645165123022301 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.sphinx` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "parse": warnings.warn( "The 'parse' function was renamed 'parse_sphinx'.", DeprecationWarning, stacklevel=2, ) return griffe.parse_sphinx return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/utils.py0000664000175000017500000000216214645165123022130 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.utils` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "WarningCallable": warnings.warn( "The 'WarningCallable' class was renamed 'DocstringWarningCallable'.", DeprecationWarning, stacklevel=2, ) return griffe.DocstringWarningCallable if name == "warning": warnings.warn( "The 'warning' function was renamed 'docstring_warning'.", DeprecationWarning, stacklevel=2, ) return griffe.docstring_warning if name == "parse_annotation": warnings.warn( "The 'parse_annotation' function was renamed 'parse_docstring_annotation'.", DeprecationWarning, stacklevel=2, ) return griffe.parse_docstring_annotation return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/numpy.py0000664000175000017500000000113314645165123022135 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.numpy` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "parse": warnings.warn( "The 'parse' function was renamed 'parse_numpy'.", DeprecationWarning, stacklevel=2, ) return griffe.parse_numpy return getattr(griffe, name) python-griffe-0.48.0/src/griffe/docstrings/google.py0000664000175000017500000000113614645165123022244 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.docstrings.google` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) if name == "parse": warnings.warn( "The 'parse' function was renamed 'parse_google'.", DeprecationWarning, stacklevel=2, ) return griffe.parse_google return getattr(griffe, name) python-griffe-0.48.0/src/griffe/diff.py0000664000175000017500000000057314645165123017525 0ustar katharakathara"""Deprecated. Import from `griffe` directly.""" from __future__ import annotations import warnings from typing import Any import griffe def __getattr__(name: str) -> Any: warnings.warn( "Importing from `griffe.diff` is deprecated. Import from `griffe` directly instead.", DeprecationWarning, stacklevel=2, ) return getattr(griffe, name) python-griffe-0.48.0/src/_griffe/0000775000175000017500000000000014645165123016375 5ustar katharakatharapython-griffe-0.48.0/src/_griffe/tests.py0000664000175000017500000003001014645165123020103 0ustar katharakathara# 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, Iterator, Mapping, Sequence from _griffe.agents.inspector import inspect from _griffe.agents.visitor import visit from _griffe.collections import LinesCollection from _griffe.loader import GriffeLoader from _griffe.models import Module, Object if TYPE_CHECKING: from _griffe.collections import ModulesCollection from _griffe.enumerations import Parser from _griffe.extensions.base import Extensions _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 = {mod: "" for mod in 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, extensions: Extensions | None = None, docstring_parser: 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, ) -> 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 leaf package. 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. Yields: A module. """ with temporary_pypackage(package, modules, init=init) as tmp_package: loader = GriffeLoader( 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, ) yield loader.load(tmp_package.name) # type: ignore[misc] @contextmanager def temporary_visited_module( code: str, *, module_name: str = "module", extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: 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: 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: # YORE: EOL 3.8: Replace block with line 2. try: filepath = modules[-1].filepath.with_stem(parts[-1]) # type: ignore[union-attr] except AttributeError: filepath = modules[-1].filepath.with_name(f"{parts[-1]}.py") # type: ignore[union-attr] modules[-1]._filepath = filepath return vtree(*modules, return_leaf=return_leaf) # type: ignore[return-value] python-griffe-0.48.0/src/_griffe/finder.py0000664000175000017500000005255114645165123020226 0ustar katharakathara# 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, Iterator, Sequence, Tuple from _griffe.exceptions import UnhandledEditableModuleError # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from typing import Pattern from _griffe.models import Module # YORE: Bump 1: Remove line. _logger = get_logger("griffe.finder") _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 if real_module_name.endswith("-stubs"): real_module_name = real_module_name[:-6] 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(f"Skip {subpath}, another module took precedence") 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): # 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: 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): 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-0.48.0/src/_griffe/c3linear.py0000664000175000017500000000665214645165123020460 0ustar katharakathara# 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 itertools import islice from typing import Deque, 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] @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-0.48.0/src/_griffe/logger.py0000664000175000017500000001101514645165123020224 0ustar katharakathara# 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`. # YORE: Bump 1: Replace `patch_loggers` with `patch_logger` within file. from __future__ import annotations import logging import warnings from contextlib import contextmanager from typing import Any, Callable, ClassVar, Iterator class Logger: _default_logger: Any = logging.getLogger # YORE: Bump 1: Replace line with `_instance: _Logger | None = None`. _instances: ClassVar[dict[str, Logger]] = {} def __init__(self, name: str) -> None: # YORE: Bump 1: Uncomment block. # if self._instance: # raise ValueError("Logger is a singleton.") # 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: # YORE: Bump 1: Replace line with `if not cls._instance:`. if name not in cls._instances: # YORE: Bump 1: Replace line with `cls._instance = cls(name)`.` cls._instances[name] = cls(name) # YORE: Bump 1: Replace line with `return cls._instance`.` return cls._instances[name] @classmethod def _patch_logger(cls, get_logger_func: Callable) -> None: # YORE: Bump 1: Uncomment block. # if not cls._instance: # raise ValueError("Logger is not initialized.") # Patch current instance. # YORE: Bump 1: Replace block with `cls._instance._logger = get_logger_func(cls._instance._logger.name)`. 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 # YORE: Bump 1: Uncomment block. # 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 dependant libraries can patch Griffe's logger 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) # ``` # """ # YORE: Bump 1: Remove block. def get_logger(name: str) -> Logger: # YORE: Bump 1: Replace `Deprecated.` with `Deprecated, use [logger][griffe.logger] directly.`. """Deprecated. Create and return a new logger instance. Parameters: name: The logger name (unused). Returns: The logger. """ # YORE: Bump 1: Replace `deprecated.` with `deprecated. Use [logger][griffe.logger] directly.`. warnings.warn("The `get_logger` function is deprecated.", DeprecationWarning, stacklevel=1) return Logger._get(name) def patch_logger(get_logger_func: Callable[[str], Any]) -> None: """Patch Griffe's logger. Parameters: get_logger_func: A function accepting a name as parameter and returning a logger. """ Logger._patch_logger(get_logger_func) # YORE: Bump 1: Remove block. def patch_loggers(get_logger_func: Callable[[str], Any]) -> None: """Deprecated, use `patch_logger` instead.""" warnings.warn( "The `patch_loggers` function is deprecated. Use `patch_logger` instead.", DeprecationWarning, stacklevel=1, ) return patch_logger(get_logger_func) python-griffe-0.48.0/src/_griffe/debug.py0000664000175000017500000000570014645165123020037 0ustar katharakathara# 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-0.48.0/src/_griffe/models.py0000664000175000017500000017250614645165123020245 0ustar katharakathara# This module contains our models definitions, # to represent Python objects (and other aspects of Python APIs)... in Python. from __future__ import annotations import inspect import warnings from collections import defaultdict from contextlib import suppress from pathlib import Path from textwrap import dedent from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, Union, cast from _griffe.c3linear import c3linear_merge from _griffe.docstrings.parsers import parse from _griffe.enumerations import Kind, ParameterKind, Parser from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError from _griffe.expressions import ExprCall, ExprName # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger from _griffe.mixins import ObjectAliasMixin if TYPE_CHECKING: from _griffe.collections import LinesCollection, ModulesCollection from _griffe.docstrings.models import DocstringSection from _griffe.expressions import Expr from functools import cached_property # YORE: Bump 1: Remove line. _logger = get_logger("griffe.dataclasses") 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: Literal["google", "numpy", "sphinx"] | 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`.""" self.lineno: int | None = lineno """The starting line number of the docstring.""" self.endlineno: int | None = endlineno """The ending line number of the docstring.""" self.parent: Object | None = parent """The object this docstring is attached to.""" self.parser: Literal["google", "numpy", "sphinx"] | Parser | None = parser """The selected docstring parser.""" self.parser_options: dict[str, Any] = parser_options or {} """The configured parsing options.""" @property def lines(self) -> list[str]: """The lines of the docstring.""" return self.value.split("\n") @cached_property def parsed(self) -> list[DocstringSection]: """The docstring sections, parsed into structured data.""" return self.parse() def parse( self, parser: Literal["google", "numpy", "sphinx"] | Parser | None = None, **options: Any, ) -> list[DocstringSection]: """Parse the docstring into structured data. 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, docstring_parser: Parser | None = None, **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. docstring_parser: Deprecated. The docstring parser to parse the docstring with. By default, no parsing is done. **kwargs: Additional serialization options. Returns: A dictionary. """ # YORE: Bump 1: Remove block. if docstring_parser is not None: warnings.warn("Parameter `docstring_parser` is deprecated and has no effect.", stacklevel=1) 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.""" 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. 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 ``` """ def __init__(self, *parameters: Parameter) -> None: """Initialize the parameters container. Parameters: *parameters: The initial parameters to add to the container. """ self._parameters_list: list[Parameter] = [] self._parameters_dict: dict[str, Parameter] = {} for parameter in parameters: self.add(parameter) def __repr__(self) -> str: return f"Parameters({', '.join(repr(param) for param in self._parameters_list)})" def __getitem__(self, name_or_index: int | str) -> Parameter: """Get a parameter by index or name.""" if isinstance(name_or_index, int): return self._parameters_list[name_or_index] return self._parameters_dict[name_or_index.lstrip("*")] def __len__(self): """The number of parameters.""" return len(self._parameters_list) def __iter__(self): """Iterate over the parameters, in order.""" return iter(self._parameters_list) def __contains__(self, param_name: str): """Whether a parameter with the given name is present.""" return param_name.lstrip("*") in self._parameters_dict def add(self, parameter: Parameter) -> None: """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 not in self._parameters_dict: self._parameters_dict[parameter.name] = parameter self._parameters_list.append(parameter) else: raise ValueError(f"parameter {parameter.name} already present") 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.""" self.endlineno: int | None = endlineno """The ending line number of the object.""" self.docstring: Docstring | None = docstring """The object docstring.""" 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).""" self.labels: set[str] = set() """The object labels (`property`, `dataclass`, etc.).""" 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: set[str] | 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. """ 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: 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).""" return bool(self.docstring) @property def has_docstrings(self) -> bool: """Whether this object or any of its members has a docstring (empty or not).""" if self.has_docstring: return True return any(member.has_docstrings for member in self.members.values()) def member_is_exported(self, member: Object | Alias, *, explicitely: bool = True) -> bool: # noqa: ARG002 """Deprecated. Use [`member.is_exported`][griffe.Object.is_exported] instead.""" warnings.warn( "Method `member_is_exported` is deprecated. Use `member.is_exported` instead.", DeprecationWarning, stacklevel=2, ) return member.is_exported def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: """Tell if this object is of the given kind. 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 @cached_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! """ 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.""" return self.kind is Kind.MODULE @property def is_class(self) -> bool: """Whether this object is a class.""" return self.kind is Kind.CLASS @property def is_function(self) -> bool: """Whether this object is a function.""" return self.kind is Kind.FUNCTION @property def is_attribute(self) -> bool: """Whether this object is an attribute.""" return self.kind is Kind.ATTRIBUTE @property def is_init_module(self) -> bool: """Whether this object is an `__init__.py` module.""" return False @property def is_package(self) -> bool: """Whether this object is a package (top module).""" return False @property def is_subpackage(self) -> bool: """Whether this object is a subpackage.""" return False @property def is_namespace_package(self) -> bool: """Whether this object is a namespace package (top folder, no `__init__.py`).""" return False @property def is_namespace_subpackage(self) -> bool: """Whether this object is a namespace subpackage.""" return False # YORE: Bump 1: Replace ` | set[str]` with `` within line. def has_labels(self, *labels: str | set[str]) -> bool: """Tell if this object has all the given labels. Parameters: *labels: Labels that must be present. Returns: True or False. """ # YORE: Bump 1: Remove block. all_labels = set() for label in labels: if isinstance(label, str): all_labels.add(label) else: warnings.warn( "Passing a set of labels to `has_labels` is deprecated. Pass multiple strings instead.", DeprecationWarning, stacklevel=2, ) all_labels.update(label) # YORE: Bump 1: Replace `all_labels` with `set(labels)` within line. return all_labels.issubset(self.labels) def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]: """Filter and return members based on predicates. 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] = {} for name, member in self.members.items(): if all(predicate(member) for predicate in predicates): members[name] = member return members @property def module(self) -> Module: """The parent module of this object. Examples: >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown["core.Markdown.references"].module Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py')) >>> # The `module` of a module is itself. >>> markdown["core"].module Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py')) Raises: ValueError: When the object is not a module and does not have a parent. """ 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. Examples: >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown["core.Markdown.references"].package Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py')) """ module = self.module while module.parent: module = module.parent # type: ignore[assignment] # always a module return module @property def filepath(self) -> Path | list[Path]: """The file path (or directory list for namespace packages) where this object was defined. Examples: >>> import griffe >>> markdown = griffe.load("markdown") >>> markdown.filepath PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py') """ return self.module.filepath @property def relative_package_filepath(self) -> Path: """The file path where this object was defined, relative to the top module path. 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. 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. 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). """ if self.parent is None: return self.name return ".".join((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. 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.""" 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.""" 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 was imported. if name in self.imports: return self.imports[name] # Name unknown and no more parent scope. if self.parent is None: # could be a built-in raise NameResolutionError(f"{name} could not be resolved in the scope of {self.path}") # Name is parent, non-module object. # NOTE: possibly useless branch. 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. 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 # doing this last for a prettier JSON dump base["labels"] = self.labels base["members"] = [member.as_dict(full=full, **kwargs) for member in self.members.values()] 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.""" 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.""" # 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.""" 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.""" 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.""" return ".".join((self.parent.path, self.name)) # type: ignore[union-attr] # we assume there's always a parent @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] # we assume there's always a parent @cached_property def members(self) -> dict[str, Object | Alias]: """The target's members (modules, classes, functions, attributes).""" 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() } @cached_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! """ 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. 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, trigerring 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.""" return self.final_target.lineno @property def endlineno(self) -> int | None: """The ending line number of the target object.""" return self.final_target.endlineno @property def docstring(self) -> Docstring | None: """The target docstring.""" 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.).""" 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 ...`). """ return self.final_target.imports @property def exports(self) -> set[str] | 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. """ return self.final_target.exports @property def aliases(self) -> dict[str, Alias]: """The aliases pointing to this object.""" return self.final_target.aliases def member_is_exported(self, member: Object | Alias, *, explicitely: bool = True) -> bool: # noqa: ARG002 """Deprecated. Use [`member.is_exported`][griffe.Alias.is_exported] instead.""" warnings.warn( "Method `member_is_exported` is deprecated. Use `member.is_exported` instead.", DeprecationWarning, stacklevel=2, ) return member.is_exported def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool: """Tell if this object is of the given kind. 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.""" return self.final_target.is_module @property def is_class(self) -> bool: """Whether this object is a class.""" return self.final_target.is_class @property def is_function(self) -> bool: """Whether this object is a function.""" return self.final_target.is_function @property def is_attribute(self) -> bool: """Whether this object is an attribute.""" return self.final_target.is_attribute def has_labels(self, *labels: str | set[str]) -> bool: """Tell if this object has all the given 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. 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. 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.""" 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.""" 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. 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. 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). """ return self.final_target.canonical_path @property def lines_collection(self) -> LinesCollection: """The lines collection attached to this object or its parents. 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.""" return self.final_target.lines @property def source(self) -> str: """The source code of this object.""" 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, trigerring 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.""" return cast(Class, self.final_target).bases @property def decorators(self) -> list[Decorator]: """The class/function decorators.""" 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.""" return cast(Module, self.final_target).is_init_module @property def is_package(self) -> bool: """Whether this module is a package (top module).""" return cast(Module, self.final_target).is_package @property def is_subpackage(self) -> bool: """Whether this module is a subpackage.""" 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`).""" return cast(Module, self.final_target).is_namespace_package @property def is_namespace_subpackage(self) -> bool: """Whether this module is a namespace subpackage.""" 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(Function, self.final_target).setter @property def deleter(self) -> Function | None: """The deleter linked to this function (property).""" return cast(Function, 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. """ if not self.resolved: self.resolve_target() return self._target # type: ignore[return-value] # cannot return None, exception is raised @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. """ # 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. 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] # we just set the target 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).""" 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. 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.""" 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).""" return not bool(self.parent) and self.is_init_module @property def is_subpackage(self) -> bool: """Whether this module is a subpackage.""" 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`).""" 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.""" 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. 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.""" 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() @cached_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! """ resolved_bases = [] for base in self.bases: base_path = base if isinstance(base, str) else base.canonical_path try: resolved_base = self.modules_collection[base_path] if resolved_base.is_alias: resolved_base = resolved_base.final_target except (AliasResolutionError, CyclicAliasError, KeyError): _logger.debug(f"Base class {base_path} is not loaded, or not static, it cannot be resolved") 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.""" return self._mro()[1:] # remove self def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this class' data as a dictionary. 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.setter: Function | None = None """The setter linked to this function (property).""" self.deleter: Function | None = None """The deleter linked to this function (property).""" 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 as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this function's data as a dictionary. 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.""" def as_dict(self, **kwargs: Any) -> dict[str, Any]: """Return this function's data as a dictionary. 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-0.48.0/src/_griffe/__init__.py0000664000175000017500000000023214645165123020503 0ustar katharakathara# 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-0.48.0/src/_griffe/collections.py0000664000175000017500000000475714645165123021302 0ustar katharakathara# This module contains collection-related classes, # which are used throughout the API. from __future__ import annotations from typing import TYPE_CHECKING, Any, ItemsView, KeysView, ValuesView from _griffe.mixins import DelMembersMixin, GetMembersMixin, SetMembersMixin if TYPE_CHECKING: 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-0.48.0/src/_griffe/merger.py0000664000175000017500000001074214645165123020234 0ustar katharakathara# 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 # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from _griffe.models import Attribute, Class, Function, Module, Object # YORE: Bump 1: Remove line. _logger = get_logger("griffe.merger") 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: 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 and not obj_member.is_alias: _logger.debug( f"Cannot merge stubs for {obj_member.path}: kind {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(f"Trying to merge {mod1.filepath} and {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-0.48.0/src/_griffe/importer.py0000664000175000017500000001147014645165123020613 0ustar katharakathara# 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, Iterator, Sequence if TYPE_CHECKING: 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 unncessary 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,TRY200 return value python-griffe-0.48.0/src/_griffe/loader.py0000664000175000017500000012355114645165123020224 0ustar katharakathara# This module contains all the logic for loading API data from sources or compiled modules. from __future__ import annotations import sys import warnings from contextlib import suppress from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Sequence, cast from _griffe.agents.inspector import inspect from _griffe.agents.visitor import visit from _griffe.collections import LinesCollection, ModulesCollection from _griffe.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 # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger from _griffe.merger import merge_stubs from _griffe.models import Alias, Module, Object from _griffe.stats import Stats if TYPE_CHECKING: from _griffe.enumerations import Parser # YORE: Bump 1: Remove line. _logger = get_logger("griffe.loader") _builtin_modules: set[str] = set(sys.builtin_module_names) 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: 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: 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, } # YORE: Bump 1: Remove block. def load_module( self, module: str | Path, *, submodules: bool = True, try_relative_path: bool = True, find_stubs_package: bool = False, ) -> Object: """Renamed `load`. Load an object as a Griffe object, given its dotted path. This method was renamed [`load`][griffe.GriffeLoader.load]. """ warnings.warn( "The `load_module` method was renamed `load`, and is deprecated.", DeprecationWarning, stacklevel=2, ) return self.load( # type: ignore[return-value] module, submodules=submodules, try_relative_path=try_relative_path, find_stubs_package=find_stubs_package, ) def load( self, objspec: str | Path | None = None, /, *, submodules: bool = True, try_relative_path: bool = True, find_stubs_package: bool = False, # YORE: Bump 1: Remove line. module: str | Path | None = None, ) -> 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. module: Deprecated. Use `objspec` positional-only parameter instead. 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. """ # YORE: Bump 1: Remove block. if objspec is None and module is None: raise TypeError("load() missing 1 required positional argument: 'objspec'") if objspec is None: objspec = module warnings.warn( "Parameter 'module' was renamed 'objspec' and made positional-only.", DeprecationWarning, stacklevel=2, ) obj_path: str package = None top_module = None # We always start by searching paths on the disk, # even if inspection is forced. _logger.debug(f"Searching path(s) for {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(f"Could not find path for {objspec} on disk") if not (self.allow_inspection or self.force_inspection): raise # Otherwise we try to dynamically import the top-level module. obj_path = str(objspec) top_module_name = obj_path.split(".", 1)[0] _logger.debug(f"Trying to dynamically import {top_module_name}") 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(f"Module {top_module_name} has no paths set (built-in module?). Inspecting it as-is.") top_module = self._inspect_module(top_module_name) self.modules_collection.set_member(top_module.path, top_module) obj = self.modules_collection.get_member(obj_path) self.extensions.call("on_package_loaded", pkg=obj) return obj # We found paths, and use them to build our intermediate Package or NamespacePackage struct. _logger.debug(f"Module {top_module_name} has paths set: {top_module_path}") 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(f"Found {objspec}: loading") try: top_module = self._load_package(package, submodules=submodules) except LoadingError as error: _logger.exception(str(error)) # noqa: TRY401 raise # 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=top_module) 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 explicitely 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 # We must first expand exports (`__all__` values), # then expand wildcard imports (`from ... import *`), # and then only we can start resolving aliases. for exports_module in list(collection.values()): self.expand_exports(exports_module) 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( f"Iteration {iteration} finished, {len(resolved)} aliases resolved, still {len(unresolved)} to go", ) 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). 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 = set() 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(f"Cannot expand '{export.canonical_path}', try pre-loading corresponding package") continue if next_module.path not in seen: self.expand_exports(next_module, seen) try: expanded |= next_module.exports except TypeError: _logger.warning(f"Unsupported item in {module.path}.__all__: {export} (use strings only)") # It's a string, simply add it to the current exports. else: expanded.add(export) module.exports = expanded def expand_wildcards( self, obj: Object, *, external: bool | None = None, seen: set | None = None, ) -> None: """Expand wildcards: try to recursively expand all found wildcards. 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] # we know it's an alias 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(f"Could not expand wildcard import {member.name} in {obj.path}: {error}") continue # Try getting the module from which every public object is imported. try: target = self.modules_collection.get_member(member.target_path) # type: ignore[union-attr] except KeyError: _logger.debug( f"Could not expand wildcard import {member.name} in {obj.path}: " f"{cast(Alias, member).target_path} not found in modules collection", ) 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(f"Could not expand wildcard import {member.name} in {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) 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 explicitely 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(f"Failed to resolve alias {member.path} -> {target}") try: self.load(package, try_relative_path=False) except (ImportError, LoadingError) as error: _logger.debug(f"Could not follow alias {member.path}: {error}") load_failures.add(package) # TODO: Immediately try again? except CyclicAliasError as error: _logger.debug(str(error)) else: _logger.debug(f"Alias {member.path} was resolved to {member.final_target.path}") # type: ignore[union-attr] resolved.add(member.path) # Recurse into unseen modules and classes. 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) self.modules_collection.set_member(top_module.path, top_module) 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(f"Loading path {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 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(f"Skip {subpath}, dots in filenames are not supported") 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 suuport this, and to warn users. _logger.debug(f"{error}. Missing __init__ module?") 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( f"Submodule '{submodule.path}' is shadowing the member at the same path. " "We recommend renaming the member or the submodule (for example prefixing it with `_`), " "see https://mkdocstrings.github.io/griffe/best_practices/#avoid-member-submodule-name-shadowing.", ) 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] # we know it's a wildcard 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: 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, # YORE: Bump 1: Remove line. module: str | Path | None = None, 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. module: Deprecated. Use `objspec` positional-only parameter instead. 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 explicitely 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, # YORE: Bump 1: Remove line. module=module, ) 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: 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, # YORE: Bump 1: Remove line. module: str | Path | None = None, 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. module: Deprecated. Use `objspec` positional-only parameter instead. 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 explicitely 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 # YORE: Bump 1: Remove block. if isinstance(module, Path): module = worktree / module 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, # YORE: Bump 1: Remove line. module=module, resolve_aliases=resolve_aliases, resolve_external=resolve_external, resolve_implicit=resolve_implicit, ) python-griffe-0.48.0/src/_griffe/expressions.py0000664000175000017500000012132414645165123021334 0ustar katharakathara# 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 helpersto 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, Iterable, Iterator, Sequence from _griffe.agents.nodes.parameters import get_parameters from _griffe.enumerations import LogLevel, ParameterKind from _griffe.exceptions import NameResolutionError # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from pathlib import Path from _griffe.models import Class, Module # YORE: Bump 1: Remove line. _logger = get_logger("griffe.expressions") 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.""" return self.canonical_path.rsplit(".", 1)[-1] @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) # 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 desinged 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 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( f"Tried and failed to parse {node.value!r} as Python code, " "falling back to using it as a string literal " "(postponed annotations might help: https://peps.python.org/pep-0563/)", ) 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, } # YORE: EOL 3.8: Remove block. if sys.version_info < (3, 9): def _build_extslice(node: ast.ExtSlice, parent: Module | Class, **kwargs: Any) -> Expr: return ExprExtSlice([_build(dim, parent, **kwargs) for dim in node.dims]) def _build_index(node: ast.Index, parent: Module | Class, **kwargs: Any) -> Expr: return _build(node.value, parent, **kwargs) _node_map[ast.ExtSlice] = _build_extslice _node_map[ast.Index] = _build_index 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-0.48.0/src/_griffe/cli.py0000664000175000017500000005011414645165123017517 0ustar katharakathara# 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, Sequence 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 # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from _griffe.extensions.base import Extensions, ExtensionType DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper() """The default log level for the CLI. This can be overriden by the `GRIFFE_LOG_LEVEL` environment variable. """ # YORE: Bump 1: Remove line. _logger = get_logger("griffe.cli") 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: 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(f"Loading package {package}") try: loader.load(package, try_relative_path=True, find_stubs_package=find_stubs_package) except ModuleNotFoundError as error: _logger.error(f"Could not find package {package}: {error}") # noqa: TRY400 except ImportError as error: _logger.exception(f"Tried but could not import package {package}: {error}") # noqa: TRY401 _logger.info("Finished loading packages") # Resolve aliases. if resolve_aliases: _logger.info("Starting alias resolution") unresolved, iterations = loader.resolve_aliases(implicit=resolve_implicit, external=resolve_external) if unresolved: _logger.info(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") else: _logger.info(f"All aliases were resolved after {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 implicitely exported aliases as well. " "Aliases are explicitely 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 = ("oneline", "verbose") 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] | ExtensionType | type[ExtensionType]] | 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 as error: _logger.exception(str(error)) # noqa: TRY401 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) _print_data(serialized, output.format(package=package_name)) # type: ignore[union-attr] else: serialized = json.dumps(data_packages, cls=JSONEncoder, indent=2, full=full) _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] | ExtensionType | type[ExtensionType]] | 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 as error: _logger.exception(str(error)) # noqa: TRY401 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, ) 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, ) 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, ) # 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-0.48.0/src/_griffe/agents/0000775000175000017500000000000014645165123017656 5ustar katharakatharapython-griffe-0.48.0/src/_griffe/agents/nodes/0000775000175000017500000000000014645165123020766 5ustar katharakatharapython-griffe-0.48.0/src/_griffe/agents/nodes/__init__.py0000664000175000017500000000011414645165123023073 0ustar katharakathara# These submodules contain utilities for working with AST and object nodes. python-griffe-0.48.0/src/_griffe/agents/nodes/imports.py0000664000175000017500000000201614645165123023034 0ustar katharakathara# 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-0.48.0/src/_griffe/agents/nodes/exports.py0000664000175000017500000000650714645165123023054 0ustar katharakathara# 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 # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from _griffe.models import Module # YORE: Bump 1: Remove line. _logger = get_logger("griffe.agents.nodes._all") @dataclass class ExportedName: """An intermediate class to store names.""" name: str """The exported name.""" parent: Module """The parent module.""" def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExportedName]: return [node.value] def _extract_name(node: ast.Name, parent: Module) -> list[str | ExportedName]: return [ExportedName(node.id, parent)] def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExportedName]: return _extract(node.value, parent) def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExportedName]: sequence = [] for elt in node.elts: sequence.extend(_extract(elt, parent)) return sequence def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExportedName]: left = _extract(node.left, parent) right = _extract(node.right, parent) return left + right _node_map: dict[type, Callable[[Any, Module], list[str | ExportedName]]] = { ast.Constant: _extract_constant, ast.Name: _extract_name, ast.Starred: _extract_starred, ast.List: _extract_sequence, ast.Set: _extract_sequence, ast.Tuple: _extract_sequence, ast.BinOp: _extract_binop, } def _extract(node: ast.AST, parent: Module) -> list[str | ExportedName]: return _node_map[type(node)](node, parent) def get__all__(node: ast.Assign | ast.AnnAssign | ast.AugAssign, parent: Module) -> list[str | ExportedName]: """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 | ExportedName]: """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 resovable 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-0.48.0/src/_griffe/agents/nodes/values.py0000664000175000017500000000301414645165123022635 0ustar katharakathara# This module contains utilities for extracting attribute values. from __future__ import annotations import ast import sys from typing import TYPE_CHECKING # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger # YORE: EOL 3.8: Replace block with line 4. if sys.version_info < (3, 9): from astunparse import unparse else: from ast import unparse if TYPE_CHECKING: from pathlib import Path # YORE: Bump 1: Remove line. _logger = get_logger("griffe.agents.nodes._values") 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-0.48.0/src/_griffe/agents/nodes/assignments.py0000664000175000017500000000324414645165123023676 0ustar katharakathara# 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-0.48.0/src/_griffe/agents/nodes/docstrings.py0000664000175000017500000000233314645165123023520 0ustar katharakathara# This module contains utilities for extracting docstrings from nodes. from __future__ import annotations import ast # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger # YORE: Bump 1: Remove line. _logger = get_logger("griffe.agents.nodes._docstrings") 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-0.48.0/src/_griffe/agents/nodes/runtime.py0000664000175000017500000002423114645165123023025 0ustar katharakathara# This module contains utilities for extracting information from runtime objects. from __future__ import annotations import inspect import sys from functools import cached_property from typing import Any, ClassVar, Sequence from _griffe.enumerations import ObjectKind # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger # YORE: Bump 1: Remove line. _logger = get_logger("griffe.agents.nodes._runtime") _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: # YORE: EOL 3.8: Replace `lstrip` with `removeprefix` within line. 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(f"Could not unwrap {name}: {error!r}") # Unwrap cached properties (`inpsect.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_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_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 _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.""" if self.parent is None: return None # Get the path of the module the child was declared in. child_module_path = self.module_path if not child_module_path: return None # Get 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("_") if self.is_module: return child_module_path child_name = getattr(self.obj, "__qualname__", self.path[len(self.module.path) + 1 :]) return f"{child_module_path}.{child_name}" python-griffe-0.48.0/src/_griffe/agents/nodes/ast.py0000664000175000017500000000742014645165123022132 0ustar katharakathara# This module contains utilities for navigating AST nodes. from __future__ import annotations from ast import AST from typing import Iterator from _griffe.exceptions import LastNodeError 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-0.48.0/src/_griffe/agents/nodes/parameters.py0000664000175000017500000000445414645165123023512 0ustar katharakathara# This module contains utilities for extracting information from parameter nodes. from __future__ import annotations import ast from itertools import zip_longest from typing import Iterable, List, Optional, Tuple, Union from _griffe.enumerations import ParameterKind 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-0.48.0/src/_griffe/agents/__init__.py0000664000175000017500000000011414645165123021763 0ustar katharakathara# These modules contain the different agents that are able to extract data. python-griffe-0.48.0/src/_griffe/agents/visitor.py0000664000175000017500000006200714645165123021734 0ustar katharakathara# 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.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: 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: 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.attach_visitor(self) """The extensions to use when visiting the AST.""" self.parent: Module | None = parent """An optional parent for the final module object.""" self.current: Module | Class = None # type: ignore[assignment] """The current object being visited.""" self.docstring_parser: Parser | None = docstring_parser """The docstring parser to use.""" self.docstring_options: dict[str, Any] = docstring_options or {} """The docstring parsing options.""" self.lines_collection: LinesCollection = lines_collection or LinesCollection() """A collection of source code lines.""" self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() """A collection of modules.""" self.type_guarded: bool = False """Whether the current code branch is type-guarded.""" def _get_docstring(self, node: ast.AST, *, strict: bool = False) -> Docstring | None: value, lineno, endlineno = get_docstring(node, strict=strict) 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. """ for before_visitor in self.extensions.before_visit: before_visitor.visit(node) getattr(self, f"visit_{ast_kind(node)}", self.generic_visit)(node) for after_visitor in self.extensions.after_visit: after_visitor.visit(node) def generic_visit(self, node: ast.AST) -> None: """Extend the base generic visit with extensions. Parameters: node: The node to visit. """ for before_visitor in self.extensions.before_children_visit: before_visitor.visit(node) for child in ast_children(node): self.visit(child) for after_visitor in self.extensions.after_children_visit: after_visitor.visit(node) 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) self.extensions.call("on_module_node", node=node) 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) self.extensions.call("on_module_instance", node=node, mod=module) self.generic_visit(node) self.extensions.call("on_members", node=node, obj=module) self.extensions.call("on_module_members", node=node, mod=module) 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) self.extensions.call("on_class_node", node=node) # handle decorators decorators = [] if node.decorator_list: lineno = node.decorator_list[0].lineno for decorator_node in node.decorator_list: decorators.append( 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, ), ) else: lineno = node.lineno # handle base classes bases = [] if node.bases: for base in node.bases: bases.append(safe_get_base_class(base, parent=self.current)) 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_) self.extensions.call("on_class_instance", node=node, cls=class_) self.generic_visit(node) self.extensions.call("on_members", node=node, obj=class_) self.extensions.call("on_class_members", node=node, cls=class_) 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) self.extensions.call("on_function_node", node=node) 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) self.extensions.call("on_attribute_instance", node=node, attr=attribute) 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: Function = 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) self.extensions.call("on_function_instance", node=node, func=function) if self.current.kind is Kind.CLASS and function.name == "__init__": self.current = function # type: ignore[assignment] # temporary assign a function 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 self.current.set_member( alias_name, Alias( alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno, runtime=not self.type_guarded, ), ) 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}": self.current.set_member( alias_name, Alias( alias_name, alias_path, lineno=node.lineno, endlineno=node.end_lineno, runtime=not self.type_guarded, ), ) 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) self.extensions.call("on_attribute_node", node=node) 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 member 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) self.extensions.call("on_attribute_instance", node=node, attr=attribute) 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-0.48.0/src/_griffe/agents/inspector.py0000664000175000017500000005313614645165123022246 0ustar katharakathara# 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, Sequence from _griffe.agents.nodes.runtime import ObjectNode from _griffe.collections import LinesCollection, ModulesCollection from _griffe.enumerations import ObjectKind, ParameterKind from _griffe.expressions import safe_get_annotation from _griffe.extensions.base import Extensions, load_extensions from _griffe.importer import dynamic_import # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger from _griffe.models import Alias, Attribute, Class, Docstring, Function, Module, Parameter, Parameters if TYPE_CHECKING: from pathlib import Path from _griffe.enumerations import Parser from _griffe.expressions import Expr # YORE: Bump 1: Remove line. _logger = get_logger("griffe.agents.inspector") _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: 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: 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.attach_inspector(self) """The extensions to use when inspecting.""" self.parent: Module | None = parent """An optional parent for the final module object.""" self.current: Module | Class = None # type: ignore[assignment] """The current object being inspected.""" self.docstring_parser: Parser | None = docstring_parser """The docstring parser to use.""" self.docstring_options: dict[str, Any] = docstring_options or {} """The docstring parsing options.""" self.lines_collection: LinesCollection = lines_collection or LinesCollection() """A collection of source code lines.""" self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() """A collection of modules.""" def _get_docstring(self, node: ObjectNode) -> Docstring | None: try: # Access `__doc__` directly to avoid taking the `__doc__` attribute from a parent class. value = getattr(node.obj, "__doc__", None) except Exception: # noqa: BLE001 # getattr can trigger exceptions 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. """ for before_inspector in self.extensions.before_inspection: before_inspector.inspect(node) getattr(self, f"inspect_{node.kind}", self.generic_inspect)(node) for after_inspector in self.extensions.after_inspection: after_inspector.inspect(node) def generic_inspect(self, node: ObjectNode) -> None: """Extend the base generic inspection with extensions. Parameters: node: The node to inspect. """ for before_inspector in self.extensions.before_children_inspection: before_inspector.inspect(node) 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(f"Module {target_path} is not discoverable on disk, inspecting right now") 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, ) try: inspector.inspect_module(child) finally: self.extensions.attach_inspector(self) self.current.set_member(child.name, inspector.current.module) # Otherwise, alias the object. else: self.current.set_member(child.name, Alias(child.name, target_path)) else: self.inspect(child) for after_inspector in self.extensions.after_children_inspection: after_inspector.inspect(node) def inspect_module(self, node: ObjectNode) -> None: """Inspect a module. Parameters: node: The node to inspect. """ self.extensions.call("on_node", node=node) self.extensions.call("on_module_node", node=node) 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) self.extensions.call("on_module_instance", node=node, mod=module) self.generic_inspect(node) self.extensions.call("on_members", node=node, obj=module) self.extensions.call("on_module_members", node=node, mod=module) def inspect_class(self, node: ObjectNode) -> None: """Inspect a class. Parameters: node: The node to inspect. """ self.extensions.call("on_node", node=node) self.extensions.call("on_class_node", node=node) 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_) self.extensions.call("on_class_instance", node=node, cls=class_) self.generic_inspect(node) self.extensions.call("on_members", node=node, obj=class_) self.extensions.call("on_class_members", node=node, cls=class_) 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 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) self.extensions.call("on_function_node", node=node) 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) if obj.is_attribute: self.extensions.call("on_attribute_instance", node=node, attr=obj) else: self.extensions.call("on_function_instance", node=node, func=obj) 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 potentiel annotation. """ self.extensions.call("on_node", node=node) self.extensions.call("on_attribute_node", node=node) # TODO: to improve parent = self.current labels: set[str] = set() if parent.kind is ObjectKind.MODULE: labels.add("module") elif parent.kind is ObjectKind.CLASS: labels.add("class") elif parent.kind is ObjectKind.FUNCTION: if parent.name != "__init__": return parent = parent.parent 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 = set(node.obj) self.extensions.call("on_instance", node=node, obj=attribute) self.extensions.call("on_attribute_instance", node=node, attr=attribute) _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-0.48.0/src/_griffe/stats.py0000664000175000017500000001231314645165123020105 0ustar katharakathara# 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-0.48.0/src/_griffe/mixins.py0000664000175000017500000005117314645165123020265 0ustar katharakathara# This module contains some mixins classes that hold shared methods # of the different kinds of objects, and aliases. from __future__ import annotations import json import warnings from contextlib import suppress from typing import TYPE_CHECKING, Any, Sequence, TypeVar from _griffe.enumerations import Kind from _griffe.exceptions import AliasResolutionError, CyclicAliasError # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger from _griffe.merger import merge_stubs if TYPE_CHECKING: from _griffe.models import Alias, Attribute, Class, Function, Module, Object # YORE: Bump 1: Remove line. _logger = get_logger("griffe.mixins") _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): with suppress(AliasResolutionError, CyclicAliasError): 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! """ return {**self.inherited_members, **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 has_private_name(self) -> bool: """Deprecated. Use [`is_private`][griffe.Object.is_private] instead.""" warnings.warn( "The `has_private_name` property is deprecated. Use `is_private` instead.", DeprecationWarning, stacklevel=2, ) return self.name.startswith("_") # type: ignore[attr-defined] @property def has_special_name(self) -> bool: """Deprecated. Use [`is_special`][griffe.Object.is_special] instead.""" warnings.warn( "The `has_special_name` property is deprecated. Use `is_special` instead.", DeprecationWarning, stacklevel=2, ) return self.name.startswith("__") and self.name.endswith("__") # type: ignore[attr-defined] @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__`).""" result = self.parent.is_module and bool(self.parent.exports and self.name in self.parent.exports) # type: ignore[attr-defined] return _True if result else _False # type: ignore[return-value] @property def is_explicitely_exported(self) -> bool: """Deprecated. Use the [`is_exported`][griffe.ObjectAliasMixin.is_exported] property instead.""" warnings.warn( "The `is_explicitely_exported` property is deprecated. Use `is_exported` instead.", DeprecationWarning, stacklevel=2, ) return self.is_exported @property def is_implicitely_exported(self) -> bool: """Deprecated. Use the [`is_exported`][griffe.ObjectAliasMixin.is_exported] property instead.""" warnings.warn( "The `is_implicitely_exported` property is deprecated. Use `is_exported` instead.", DeprecationWarning, stacklevel=2, ) return self.is_exported @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] # YORE: Bump 1: Replace line with `return self.public`. return _True if self.public else _False # type: ignore[return-value,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] # YORE: Bump 1: Replace line with `return True`. return _True # type: ignore[return-value] # 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] # YORE: Bump 1: Replace line with `return self.name in self.parent.exports`. return _True if self.name in self.parent.exports else _False # type: ignore[attr-defined,return-value] # 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: # YORE: Bump 1: Replace line with `return False`. return _False # type: ignore[return-value] # 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: # YORE: Bump 1: Replace line with `return False`. return _False # type: ignore[return-value] # If we reached this point, the object is public. # YORE: Bump 1: Replace line with `return True`. return _True # type: ignore[return-value] @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] # This is used to allow the `is_public` property to be "callable", # for backward compatibility with the previous implementation. class _Bool: def __init__(self, value: bool) -> None: # noqa: FBT001 self.value = value def __bool__(self) -> bool: return self.value def __repr__(self) -> str: return repr(self.value) def __call__(self, *args: Any, **kwargs: Any) -> bool: # noqa: ARG002 warnings.warn( "This method is now a property and should be accessed as such (without parentheses).", DeprecationWarning, stacklevel=2, ) return self.value _True = _Bool(True) # noqa: FBT003 _False = _Bool(False) # noqa: FBT003 python-griffe-0.48.0/src/_griffe/exceptions.py0000664000175000017500000000473714645165123021143 0ustar katharakathara# 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-0.48.0/src/_griffe/py.typed0000664000175000017500000000000014645165123020062 0ustar katharakatharapython-griffe-0.48.0/src/_griffe/encoders.py0000664000175000017500000002214314645165123020553 0ustar katharakathara# 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 import warnings from pathlib import Path, PosixPath, WindowsPath from typing import TYPE_CHECKING, 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, ) if TYPE_CHECKING: from _griffe.enumerations import Parser _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, # YORE: Bump 1: Remove line. docstring_parser: Parser | None = None, # YORE: Bump 1: Remove line. docstring_options: dict[str, Any] | None = None, **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 infered 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.""" # YORE: Bump 1: Remove block. self.docstring_parser: Parser | None = docstring_parser """Deprecated. The docstring parser to use. By default, no parsing is done.""" self.docstring_options: dict[str, Any] = docstring_options or {} """Deprecated. Additional docstring parsing options.""" if docstring_parser is not None: warnings.warn("Parameter `docstring_parser` is deprecated and has no effect.", stacklevel=1) if docstring_options is not None: warnings.warn("Parameter `docstring_options` is deprecated and has no effect.", stacklevel=1) 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)) for module_member in obj_dict.get("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"], ) for class_member in obj_dict.get("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-0.48.0/src/_griffe/git.py0000664000175000017500000000752514645165123017543 0ustar katharakathara# 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 shutil import subprocess from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory from typing import Iterator from _griffe.exceptions import GitError _WORKTREE_PREFIX = "griffe-worktree-" 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=-committerdate"], 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 with TemporaryDirectory(prefix=f"{_WORKTREE_PREFIX}{repo_name}-{ref}-") as tmp_dir: branch = f"griffe_{ref}" location = os.path.join(tmp_dir, branch) process = subprocess.run( ["git", "-C", repo, "worktree", "add", "-b", 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", branch], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "worktree", "prune"], stdout=subprocess.DEVNULL, check=False) subprocess.run(["git", "-C", repo, "branch", "-D", branch], stdout=subprocess.DEVNULL, check=False) python-griffe-0.48.0/src/_griffe/extensions/0000775000175000017500000000000014645165123020574 5ustar katharakatharapython-griffe-0.48.0/src/_griffe/extensions/dataclasses.py0000664000175000017500000002011714645165123023436 0ustar katharakathara# 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 lru_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.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 {} @lru_cache(maxsize=None) 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 # 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) -> None: """Hook for loaded packages. Parameters: pkg: The loaded package. """ _apply_recursively(pkg, set()) python-griffe-0.48.0/src/_griffe/extensions/base.py0000664000175000017500000004502714645165123022070 0ustar katharakathara# This module contains the base class for extensions # and the functions to load them. from __future__ import annotations import os import sys import warnings from collections import defaultdict 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, Dict, Sequence, Type, Union from _griffe.agents.nodes.ast import ast_children, ast_kind from _griffe.enumerations import When from _griffe.exceptions import ExtensionNotLoadedError from _griffe.importer import dynamic_import if TYPE_CHECKING: import ast from types import ModuleType from _griffe.agents.inspector import Inspector from _griffe.agents.nodes.runtime import ObjectNode from _griffe.agents.visitor import Visitor from _griffe.models import Attribute, Class, Function, Module, Object # YORE: Bump 1: Remove block. class VisitorExtension: """Deprecated in favor of `Extension`. The node visitor extension base class, to inherit from.""" when: When = When.after_all """When the visitor extension should run.""" def __init__(self) -> None: """Initialize the visitor extension.""" warnings.warn( "Visitor extensions are deprecated in favor of the new, more developer-friendly Extension. " "See https://mkdocstrings.github.io/griffe/extensions/", DeprecationWarning, stacklevel=1, ) self.visitor: Visitor = None # type: ignore[assignment] """The parent visitor.""" def attach(self, visitor: Visitor) -> None: """Attach the parent visitor to this extension. Parameters: visitor: The parent visitor. """ self.visitor = visitor 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) # YORE: Bump 1: Remove block. class InspectorExtension: """Deprecated in favor of `Extension`. The object inspector extension base class, to inherit from.""" when: When = When.after_all """When the inspector extension should run.""" def __init__(self) -> None: """Initialize the inspector extension.""" warnings.warn( "Inspector extensions are deprecated in favor of the new, more developer-friendly Extension. " "See https://mkdocstrings.github.io/griffe/extensions/", DeprecationWarning, stacklevel=1, ) self.inspector: Inspector = None # type: ignore[assignment] """The parent inspector.""" def attach(self, inspector: Inspector) -> None: """Attach the parent inspector to this extension. Parameters: inspector: The parent inspector. """ self.inspector = inspector def inspect(self, node: ObjectNode) -> None: """Inspect a node. Parameters: node: The node to inspect. """ getattr(self, f"inspect_{node.kind}", lambda _: None)(node) 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) -> 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) -> None: """Run when an Object has been created. Parameters: node: The currently visited node. obj: The object instance. """ def on_members(self, *, node: ast.AST | ObjectNode, obj: Object) -> None: """Run when members of an Object have been loaded. Parameters: node: The currently visited node. obj: The object instance. """ def on_module_node(self, *, node: ast.AST | ObjectNode) -> None: """Run when visiting a new module node during static/dynamic analysis. Parameters: node: The currently visited node. """ def on_module_instance(self, *, node: ast.AST | ObjectNode, mod: Module) -> None: """Run when a Module has been created. Parameters: node: The currently visited node. mod: The module instance. """ def on_module_members(self, *, node: ast.AST | ObjectNode, mod: Module) -> None: """Run when members of a Module have been loaded. Parameters: node: The currently visited node. mod: The module instance. """ def on_class_node(self, *, node: ast.AST | ObjectNode) -> None: """Run when visiting a new class node during static/dynamic analysis. Parameters: node: The currently visited node. """ def on_class_instance(self, *, node: ast.AST | ObjectNode, cls: Class) -> None: """Run when a Class has been created. Parameters: node: The currently visited node. cls: The class instance. """ def on_class_members(self, *, node: ast.AST | ObjectNode, cls: Class) -> None: """Run when members of a Class have been loaded. Parameters: node: The currently visited node. cls: The class instance. """ def on_function_node(self, *, node: ast.AST | ObjectNode) -> None: """Run when visiting a new function node during static/dynamic analysis. Parameters: node: The currently visited node. """ def on_function_instance(self, *, node: ast.AST | ObjectNode, func: Function) -> None: """Run when a Function has been created. Parameters: node: The currently visited node. func: The function instance. """ def on_attribute_node(self, *, node: ast.AST | ObjectNode) -> None: """Run when visiting a new attribute node during static/dynamic analysis. Parameters: node: The currently visited node. """ def on_attribute_instance(self, *, node: ast.AST | ObjectNode, attr: Attribute) -> None: """Run when an Attribute has been created. Parameters: node: The currently visited node. attr: The attribute instance. """ def on_package_loaded(self, *, pkg: Module) -> None: """Run when a package has been completely loaded. Parameters: pkg: The package (Module) instance. """ # YORE: Bump 1: Remove block. ExtensionType = Union[VisitorExtension, InspectorExtension, Extension] """All the types that can be passed to `Extensions.add`. Deprecated. Use `Extension` instead.""" # YORE: Bump 1: Regex-replace `\bExtensionType\b` with `Extension` within line. LoadableExtensionType = Union[str, Dict[str, Any], ExtensionType, Type[ExtensionType]] """All the types that can be passed to `load_extensions`.""" class Extensions: """This class helps iterating on extensions that should run at different times.""" # YORE: Bump 1: Replace `ExtensionType` with `Extension` within line. def __init__(self, *extensions: ExtensionType) -> None: """Initialize the extensions container. Parameters: *extensions: The extensions to add. """ # YORE: Bump 1: Remove block. self._visitors: dict[When, list[VisitorExtension]] = defaultdict(list) self._inspectors: dict[When, list[InspectorExtension]] = defaultdict(list) self._extensions: list[Extension] = [] self.add(*extensions) # YORE: Bump 1: Replace `ExtensionType` with `Extension` within line. def add(self, *extensions: ExtensionType) -> None: """Add extensions to this container. Parameters: *extensions: The extensions to add. """ for extension in extensions: # YORE: Bump 1: Replace block with line 6 if isinstance(extension, VisitorExtension): self._visitors[extension.when].append(extension) elif isinstance(extension, InspectorExtension): self._inspectors[extension.when].append(extension) else: self._extensions.append(extension) # YORE: Bump 1: Remove block. def attach_visitor(self, parent_visitor: Visitor) -> Extensions: """Attach a parent visitor to the visitor extensions. Parameters: parent_visitor: The parent visitor, leading the visit. Returns: Self, conveniently. """ for when in self._visitors: for visitor in self._visitors[when]: visitor.attach(parent_visitor) return self # YORE: Bump 1: Remove block. def attach_inspector(self, parent_inspector: Inspector) -> Extensions: """Attach a parent inspector to the inspector extensions. Parameters: parent_inspector: The parent inspector, leading the inspection. Returns: Self, conveniently. """ for when in self._inspectors: for inspector in self._inspectors[when]: inspector.attach(parent_inspector) return self # YORE: Bump 1: Remove block. @property def before_visit(self) -> list[VisitorExtension]: """The visitors that run before the visit.""" return self._visitors[When.before_all] # YORE: Bump 1: Remove block. @property def before_children_visit(self) -> list[VisitorExtension]: """The visitors that run before the children visit.""" return self._visitors[When.before_children] # YORE: Bump 1: Remove block. @property def after_children_visit(self) -> list[VisitorExtension]: """The visitors that run after the children visit.""" return self._visitors[When.after_children] # YORE: Bump 1: Remove block. @property def after_visit(self) -> list[VisitorExtension]: """The visitors that run after the visit.""" return self._visitors[When.after_all] # YORE: Bump 1: Remove block. @property def before_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the inspection.""" return self._inspectors[When.before_all] # YORE: Bump 1: Remove block. @property def before_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the children inspection.""" return self._inspectors[When.before_children] # YORE: Bump 1: Remove block. @property def after_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run after the children inspection.""" return self._inspectors[When.after_children] # YORE: Bump 1: Remove block. @property def after_inspection(self) -> list[InspectorExtension]: """The inspectors that run after the inspection.""" return self._inspectors[When.after_all] def call(self, event: str, **kwargs: Any) -> None: """Call the extension hook for the given event. Parameters: event: The trigerred event. **kwargs: Arguments passed to the hook. """ for extension in self._extensions: getattr(extension, event)(**kwargs) builtin_extensions: set[str] = { # YORE: Bump 1: Remove line. "hybrid", "dataclasses", } """The names of built-in Griffe extensions.""" def _load_extension_path(path: str) -> ModuleType: module_name = os.path.basename(path).rsplit(".", 1)[0] 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 # YORE: Bump 1: Replace `ExtensionType` with `Extension` within block. def _load_extension( extension: str | dict[str, Any] | ExtensionType | type[ExtensionType], ) -> ExtensionType | list[ExtensionType]: """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 # YORE: Bump 1: Remove line. ext_classes = (VisitorExtension, InspectorExtension, Extension) # If it's already an extension instance, return it. # YORE: Bump 1: Replace `ext_classes` with `Extension` within line. if isinstance(extension, ext_classes): return extension # If it's an extension class, instantiate it (without options) and return it. # YORE: Bump 1: Replace `ext_classes` with `Extension` within line. if isclass(extension) and issubclass(extension, ext_classes): 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): 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. # YORE: Bump 1: Replace `ext_classes` with `Extension` within line. if isclass(ext_object) and issubclass(ext_object, ext_classes): return ext_object(**options) # type: ignore[misc] # 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 = [] for obj in vars(ext_object).values(): # YORE: Bump 1: Replace `ext_classes` with `Extension` within line. # YORE: Bump 1: Replace `not in` with `is not` within line. if isclass(obj) and issubclass(obj, ext_classes) and obj not in ext_classes: extensions.append(obj) return [ext(**options) for ext in extensions] def load_extensions( # YORE: Bump 1: Replace ` | Sequence[LoadableExtension],` with `` within line. *exts: LoadableExtensionType | Sequence[LoadableExtensionType], ) -> Extensions: """Load configured extensions. Parameters: exts: Extensions with potential configuration options. Returns: An extensions container. """ extensions = Extensions() # YORE: Bump 1: Remove block. all_exts: list[LoadableExtensionType] = [] for ext in exts: if isinstance(ext, (list, tuple)): warnings.warn( "Passing multiple extensions as a single list or tuple is deprecated. " "Please pass them as separate arguments instead.", DeprecationWarning, stacklevel=2, ) all_exts.extend(ext) else: all_exts.append(ext) # type: ignore[arg-type] # YORE: Bump 1: Replace `all_exts` with `exts` within line. for extension in all_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-0.48.0/src/_griffe/extensions/__init__.py0000664000175000017500000000012314645165123022701 0ustar katharakathara# These submodules contain our extension system, # as well as built-in extensions. python-griffe-0.48.0/src/_griffe/extensions/hybrid.py0000664000175000017500000000741514645165123022436 0ustar katharakathara# Deprecated. This extension provides an hybrid behavior while loading data. # YORE: Bump 1: Remove module. from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Pattern, Sequence from _griffe.agents.nodes.runtime import ObjectNode from _griffe.enumerations import When from _griffe.exceptions import ExtensionError from _griffe.extensions.base import InspectorExtension, VisitorExtension, _load_extension from _griffe.importer import dynamic_import from _griffe.logger import get_logger if TYPE_CHECKING: import ast from _griffe.agents.visitor import Visitor _logger = get_logger("griffe.extensions.hybrid") class HybridExtension(VisitorExtension): """Inspect during a visit. This extension accepts the name of another extension (an inspector) and runs it appropriately. It allows to inspect objects after having visited them, so as to extract more data. Indeed, during the visit, an object might be seen as a simple attribute (assignment), when in fact it's a function or a class dynamically constructed. In this case, inspecting it will provide the desired data. """ when = When.after_all """The moment when the extension should be executed.""" def __init__( self, extensions: Sequence[str | dict[str, Any] | InspectorExtension | type[InspectorExtension]], object_paths: Sequence[str | Pattern] | None = None, ) -> None: """Initialize the extension. Parameters: extensions: The names or configurations of other inspector extensions. object_paths: Optional list of regular expressions to match against objects paths, to select which objects to inspect. Raises: ExtensionError: When the passed extension is not an inspector extension. """ self._extensions: list[InspectorExtension] = [_load_extension(ext) for ext in extensions] # type: ignore[misc] for extension in self._extensions: if not isinstance(extension, InspectorExtension): raise ExtensionError( f"Extension '{extension}' is not an inspector extension. " "The 'hybrid' extension only accepts inspector extensions. " "If you want to use a visitor extension, just add it normally " "to your extensions configuration, without using 'hybrid'.", ) self.object_paths = [re.compile(op) if isinstance(op, str) else op for op in object_paths or []] """The list of regular expressions to match against objects paths.""" super().__init__() def attach(self, visitor: Visitor) -> None: super().attach(visitor) for extension in self._extensions: extension.attach(visitor) # type: ignore[arg-type] # tolerate hybrid behavior def visit(self, node: ast.AST) -> None: try: just_visited = self.visitor.current.get_member(node.name) # type: ignore[attr-defined] except (KeyError, AttributeError, TypeError): return if self.object_paths and not any(op.search(just_visited.path) for op in self.object_paths): return if just_visited.is_alias: return try: value = dynamic_import(just_visited.path) except AttributeError: # can happen when an object is defined conditionally, # for example based on the Python version return parent = None for part in just_visited.path.split(".")[:-1]: parent = ObjectNode(None, name=part, parent=parent) object_node = ObjectNode(value, name=node.name, parent=parent) # type: ignore[attr-defined] for extension in self._extensions: extension.inspect(object_node) python-griffe-0.48.0/src/_griffe/enumerations.py0000664000175000017500000001434714645165123021471 0ustar katharakathara# This module contains all the enumerations of the package. from __future__ import annotations import sys from enum import IntEnum # YORE: Bump 1: Replace block with line 2. if sys.version_info >= (3, 11): from enum import StrEnum else: from backports.strenum import StrEnum class LogLevel(StrEnum): """Enumeration of available log levels.""" trace: str = "trace" """The TRACE log level.""" debug: str = "debug" """The DEBUG log level.""" info: str = "info" """The INFO log level.""" success: str = "success" """The SUCCESS log level.""" warning: str = "warning" """The WARNING log level.""" error: str = "error" """The ERROR log level.""" critical: str = "critical" """The CRITICAL log level.""" class DocstringSectionKind(StrEnum): """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(StrEnum): """Enumeration of the different parameter kinds.""" positional_only: str = "positional-only" """Positional-only parameter.""" positional_or_keyword: str = "positional or keyword" """Positional or keyword parameter.""" var_positional: str = "variadic positional" """Variadic positional parameter.""" keyword_only: str = "keyword-only" """Keyword-only parameter.""" var_keyword: str = "variadic keyword" """Variadic keyword parameter.""" class Kind(StrEnum): """Enumeration of the different object kinds.""" MODULE: str = "module" """Modules.""" CLASS: str = "class" """Classes.""" FUNCTION: str = "function" """Functions and methods.""" ATTRIBUTE: str = "attribute" """Attributes and properties.""" ALIAS: str = "alias" """Aliases (imported objects).""" class ExplanationStyle(StrEnum): """Enumeration of the possible styles for explanations.""" ONE_LINE: str = "oneline" """Explanations on one-line.""" VERBOSE: str = "verbose" """Explanations on multiple lines.""" MARKDOWN: str = "markdown" """Explanations in Markdown, adapted to changelogs.""" GITHUB: str = "github" """Explanation as GitHub workflow commands warnings, adapted to CI.""" class BreakageKind(StrEnum): """Enumeration of the possible API breakages.""" PARAMETER_MOVED: str = "Positional parameter was moved" """Positional parameter was moved""" PARAMETER_REMOVED: str = "Parameter was removed" """Parameter was removed""" PARAMETER_CHANGED_KIND: str = "Parameter kind was changed" """Parameter kind was changed""" PARAMETER_CHANGED_DEFAULT: str = "Parameter default was changed" """Parameter default was changed""" PARAMETER_CHANGED_REQUIRED: str = "Parameter is now required" """Parameter is now required""" PARAMETER_ADDED_REQUIRED: str = "Parameter was added as required" """Parameter was added as required""" RETURN_CHANGED_TYPE: str = "Return types are incompatible" """Return types are incompatible""" OBJECT_REMOVED: str = "Public object was removed" """Public object was removed""" OBJECT_CHANGED_KIND: str = "Public object points to a different kind of object" """Public object points to a different kind of object""" ATTRIBUTE_CHANGED_TYPE: str = "Attribute types are incompatible" """Attribute types are incompatible""" ATTRIBUTE_CHANGED_VALUE: str = "Attribute value was changed" """Attribute value was changed""" CLASS_REMOVED_BASE: str = "Base class was removed" """Base class was removed""" class Parser(StrEnum): """Enumeration of the different docstring parsers.""" google = "google" """Google-style docstrings parser.""" sphinx = "sphinx" """Sphinx-style docstrings parser.""" numpy = "numpy" """Numpydoc-style docstrings parser.""" class ObjectKind(StrEnum): """Enumeration of the different runtime object kinds.""" MODULE: str = "module" """Modules.""" CLASS: str = "class" """Classes.""" STATICMETHOD: str = "staticmethod" """Static methods.""" CLASSMETHOD: str = "classmethod" """Class methods.""" METHOD_DESCRIPTOR: str = "method_descriptor" """Method descriptors.""" METHOD: str = "method" """Methods.""" BUILTIN_METHOD: str = "builtin_method" """Built-in ethods.""" COROUTINE: str = "coroutine" """Coroutines""" FUNCTION: str = "function" """Functions.""" BUILTIN_FUNCTION: str = "builtin_function" """Built-in functions.""" CACHED_PROPERTY: str = "cached_property" """Cached properties.""" PROPERTY: str = "property" """Properties.""" ATTRIBUTE: str = "attribute" """Attributes.""" def __str__(self) -> str: return self.value # YORE: Bump 1: Remove block. class When(IntEnum): """Enumeration of the different times at which an extension is used. Deprecated. This enumeration is used with the `VisitorExtension` and `InspectorExtension` classes, which are deprecated. Use the `Extension` class instead, which does not need `When`. """ before_all: int = 1 """For each node, before the visit/inspection.""" before_children: int = 2 """For each node, after the visit has started, and before the children visit/inspection.""" after_children: int = 3 """For each node, after the children have been visited/inspected, and before finishing the visit/inspection.""" after_all: int = 4 """For each node, after the visit/inspection.""" python-griffe-0.48.0/src/_griffe/docstrings/0000775000175000017500000000000014645165123020554 5ustar katharakatharapython-griffe-0.48.0/src/_griffe/docstrings/parsers.py0000664000175000017500000000240414645165123022605 0ustar katharakathara# 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, 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 parsers = { Parser.google: parse_google, Parser.sphinx: parse_sphinx, Parser.numpy: parse_numpy, } def parse( docstring: Docstring, parser: Literal["google", "numpy", "sphinx"] | 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 isinstance(parser, str): parser = Parser(parser) return parsers[parser](docstring, **options) # type: ignore[operator] return [DocstringSectionText(docstring.value)] python-griffe-0.48.0/src/_griffe/docstrings/models.py0000664000175000017500000003211114645165123022407 0ustar katharakathara# 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-0.48.0/src/_griffe/docstrings/__init__.py0000664000175000017500000000007514645165123022667 0ustar katharakathara# These submodules define models and parsers for docstrings. python-griffe-0.48.0/src/_griffe/docstrings/sphinx.py0000664000175000017500000003624714645165123022453 0ustar katharakathara# 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 # YORE: Bump 1: Regex-replace `\b_warn\b` with `docstring_warning` within file. # YORE: Bump 1: Remove line. _warn = docstring_warning("griffe.docstrings.sphinx") # TODO: Examples: from the documentation, we're not sure there is a standard format for examples _PARAM_NAMES = frozenset(("param", "parameter", "arg", "argument", "key", "keyword")) _PARAM_TYPE_NAMES = frozenset(("type",)) _ATTRIBUTE_NAMES = frozenset(("var", "ivar", "cvar")) _ATTRIBUTE_TYPE_NAMES = frozenset(("vartype",)) _RETURN_NAMES = frozenset(("returns", "return")) _RETURN_TYPE_NAMES = frozenset(("rtype",)) _EXCEPTION_NAMES = frozenset(("raises", "raise", "except", "exception")) @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: _warn(docstring, 0, f"Failed to parse field directive from '{parsed_directive.line}'") return parsed_directive.next_index if name in parsed_values.parameters: _warn(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 _warn(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: _warn(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): _warn(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: _warn(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: _warn(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: _warn(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): annotation = docstring.parent.attributes[name].annotation # type: ignore[union-attr] if name in parsed_values.attributes: _warn(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: _warn(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: _warn(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: _warn(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: _warn(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: _warn(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-0.48.0/src/_griffe/docstrings/utils.py0000664000175000017500000001116414645165123022271 0ustar katharakathara# This module contains utilities for docstrings parsers. from __future__ import annotations import warnings from ast import PyCF_ONLY_AST from contextlib import suppress from typing import TYPE_CHECKING, Protocol, overload from _griffe.enumerations import LogLevel from _griffe.exceptions import BuiltinModuleError from _griffe.expressions import safe_get_annotation # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: from _griffe.expressions import Expr from _griffe.models import Docstring # YORE: Bump 1: Remove block. class DocstringWarningCallable(Protocol): """A callable that logs a warning message.""" def __call__(self, docstring: Docstring, offset: int, message: str, log_level: LogLevel = ...) -> None: """Log a warning message. Parameters: docstring: The docstring in which the warning occurred. offset: The offset in the docstring lines. message: The message to log. log_level: The log level to use. """ # YORE: Bump 1: Remove line. _sentinel = object() # YORE: Bump 1: Remove block. @overload def docstring_warning(name: str) -> DocstringWarningCallable: ... # YORE: Bump 1: Remove block. @overload def docstring_warning( docstring: Docstring, offset: int, message: str, log_level: LogLevel = LogLevel.warning, ) -> None: ... def docstring_warning( # type: ignore[misc] # YORE: Bump 1: Remove line. name: str | None = None, # YORE: Bump 1: Replace line with `docstring: Docstring,`. docstring: Docstring = _sentinel, # type: ignore[assignment] # YORE: Bump 1: Replace line with `offset: int,`. offset: int = _sentinel, # type: ignore[assignment] # YORE: Bump 1: Replace line with `message: str,`. message: str = _sentinel, # type: ignore[assignment] log_level: LogLevel = LogLevel.warning, # YORE: Bump 1: Replace line with `) -> None:`. ) -> DocstringWarningCallable | None: """Log a warning when parsing a docstring. This function logs a warning message by prefixing it with the filepath and line number. Parameters: name: Deprecated. If passed, the function returns a callable, and other arguments are ignored. 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. """ # YORE: Bump 1: Remove block. if name is not None: warnings.warn("The `name` parameter is deprecated.", DeprecationWarning, stacklevel=1) logger = get_logger(name) else: if docstring is _sentinel or offset is _sentinel or message is _sentinel: raise ValueError("Missing required arguments docstring/offset/message.") logger = get_logger("griffe") 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 = "" except BuiltinModuleError: prefix = f"" # type: ignore[union-attr] log = getattr(logger, log_level.value) log(f"{prefix}:{(docstring.lineno or 0)+offset}: {message}") if name is not None: return warn warn(docstring, offset, message, log_level) return None 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, log_level=log_level, ) return name_or_expr or annotation return annotation python-griffe-0.48.0/src/_griffe/docstrings/numpy.py0000664000175000017500000007204114645165123022302 0ustar katharakathara# 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 typing import Any, Literal, Pattern from _griffe.expressions import Expr from _griffe.models import Docstring # YORE: Bump 1: Regex-replace `\b_warn\b` with `docstring_warning` within file. # YORE: Bump 1: Remove line. _warn = docstring_warning("griffe.docstrings.numpy") _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:]) _warn( 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{_RE_NAME})\s*:\s*(?P{_RE_TYPE}) # name and type | # or (?P{_RE_NAME})\s*:\s* # just name | # or \s*:\s*$ # no name, no type | # or (?::\s*)?(?P{_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{_RE_NAME}(?:,\s{_RE_NAME})*) (?: \s:\s (?: (?:{_RE_OB}(?P.+){_RE_CB})| (?P{_RE_TYPE}) )? )? """, re.IGNORECASE | re.VERBOSE, ) _RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*\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 = [] 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: _warn(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.+),\s+default(?: |: |=)(?P.+)$", 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: _warn(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 _warn(docstring, new_offset, message) for name in names: parameters.append(DocstringParameter(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) if parameters: return DocstringSectionParameters(parameters), new_offset _warn(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 _warn(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: _warn(docstring, new_offset, f"Empty deprecated section at line {offset}") return None, new_offset if len(items) > 1: _warn(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: _warn(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: _warn(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: _warn(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: _warn(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): annotation = docstring.parent.returns # 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: _warn(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: _warn(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: _warn(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: _warn(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: _warn(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): annotation = docstring.parent.members[name].annotation # type: ignore[union-attr] 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: _warn(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: _warn(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: _warn(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 _warn(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-0.48.0/src/_griffe/docstrings/google.py0000664000175000017500000010003614645165123022402 0ustar katharakathara# 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, List, Tuple 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 typing import Any, Literal, Pattern from _griffe.expressions import Expr from _griffe.models import Docstring # YORE: Bump 1: Regex-replace `\b_warn\b` with `docstring_warning` within file. # YORE: Bump 1: Remove line. _warn = docstring_warning("griffe.docstrings.google") _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.I) _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:]) _warn( 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: _warn(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("()") if annotation.endswith(", optional"): annotation = annotation[:-10] # 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: _warn(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 _warn(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: _warn(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("()") if annotation.endswith(", optional"): annotation = annotation[:-10] # 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): annotation = docstring.parent.members[name].annotation # type: ignore[union-attr] 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: _warn(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: _warn(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: _warn(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: _warn(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: _warn(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_returns_section( docstring: Docstring, *, offset: int, returns_multiple_items: bool = True, returns_named_value: bool = True, **options: Any, ) -> tuple[DocstringSectionReturns | None, int]: returns = [] if returns_multiple_items: block, new_offset = _read_block_items(docstring, offset=offset, **options) else: one_block, new_offset = _read_block(docstring, offset=offset, **options) block = [(new_offset, one_block.splitlines())] for index, (line_number, return_lines) in enumerate(block): if returns_named_value: match = _RE_NAME_ANNOTATION_DESCRIPTION.match(return_lines[0]) if not match: _warn(docstring, line_number, f"Failed to get name, annotation or description from '{return_lines[0]}'") continue name, annotation, description = match.groups() else: name = None if ":" in return_lines[0]: annotation, description = return_lines[0].split(":", 1) annotation = annotation.lstrip("(").rstrip(")") else: annotation = None description = return_lines[0] description = "\n".join([description.lstrip(), *return_lines[1:]]).rstrip("\n") 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 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(block) > 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 if annotation is None: returned_value = repr(name) if name else index + 1 _warn(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, **options: Any, ) -> tuple[DocstringSectionYields | None, int]: yields = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) for index, (line_number, yield_lines) in enumerate(block): match = _RE_NAME_ANNOTATION_DESCRIPTION.match(yield_lines[0]) if not match: _warn(docstring, line_number, f"Failed to get name, annotation or description from '{yield_lines[0]}'") continue name, annotation, description = match.groups() description = "\n".join([description.lstrip(), *yield_lines[1:]]).rstrip("\n") 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 with suppress(AttributeError, KeyError, ValueError): annotation = docstring.parent.returns # 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 if annotation is None: yielded_value = repr(name) if name else index + 1 _warn(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, **options: Any, ) -> tuple[DocstringSectionReceives | None, int]: receives = [] block, new_offset = _read_block_items(docstring, offset=offset, **options) for index, (line_number, receive_lines) in enumerate(block): match = _RE_NAME_ANNOTATION_DESCRIPTION.match(receive_lines[0]) if not match: _warn(docstring, line_number, f"Failed to get name, annotation or description from '{receive_lines[0]}'") continue name, annotation, description = match.groups() description = "\n".join([description.lstrip(), *receive_lines[1:]]).rstrip("\n") 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 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 if annotation is None: received_value = repr(name) if name else index + 1 _warn(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 _read_deprecated_section( docstring: Docstring, *, offset: int, **options: Any, ) -> tuple[DocstringSectionDeprecated | None, int]: text, new_offset = _read_block(docstring, offset=offset, **options) # check the presence of a name and description, separated by a semi-colon try: version, text = text.split(":", 1) except ValueError: _warn(docstring, new_offset, f"Could not parse version, text at line {offset}") return None, new_offset version = version.lstrip() description = text.lstrip() return ( DocstringSectionDeprecated(version=version, text=description), 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, DocstringSectionKind.deprecated: _read_deprecated_section, } _sentinel = object() def parse_google( docstring: Docstring, *, ignore_init_summary: bool = False, trim_doctest_flags: bool = True, returns_multiple_items: bool = True, warn_unknown_params: bool = True, returns_named_value: bool = True, returns_type_in_property_summary: bool = False, **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 the `Returns` section has multiple items. warn_unknown_params: Warn about documented parameters not appearing in the signature. returns_named_value: Whether to parse `thing: Description` in returns sections 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`. returns_type_in_property_summary: Whether to parse the return type of properties at the beginning of their summary: `str: Summary of the property`. **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, "warn_unknown_params": warn_unknown_params, "returns_named_value": returns_named_value, "returns_type_in_property_summary": returns_type_in_property_summary, **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) _warn( 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-0.48.0/src/_griffe/diff.py������������������������������������������������������������0000664�0001750�0001750�00000052023�14645165123�017661� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 import warnings from pathlib import Path from typing import TYPE_CHECKING, Any, Iterable, Iterator from colorama import Fore, Style from _griffe.enumerations import BreakageKind, ExplanationStyle, ParameterKind from _griffe.exceptions import AliasResolutionError from _griffe.git import _WORKTREE_PREFIX # YORE: Bump 1: Replace `_logger` with `logger` within file. # YORE: Bump 1: Replace `get_logger` with `logger` within line. from _griffe.logger import get_logger if TYPE_CHECKING: 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)) # YORE: Bump 1: Remove line. _logger = get_logger("griffe.diff") class Breakage: """Breakages can explain what broke from a version to another.""" kind: BreakageKind """The kind of breakage.""" def __init__(self, obj: Object, old_value: Any, new_value: Any, details: str = "") -> None: """Initialize the breakage. 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 "<module>" @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: return self._explain_oneline() def _explain_github(self) -> str: return self._explain_oneline() class ParameterMovedBreakage(Breakage): """Specific breakage class for moved parameters.""" kind: BreakageKind = BreakageKind.PARAMETER_MOVED def _format_title(self) -> str: return f"{self._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 def _format_title(self) -> str: return f"{self._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 def _format_title(self) -> str: return f"{self._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 def _format_title(self) -> str: return f"{self._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 def _format_title(self) -> str: return f"{self._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 def _format_title(self) -> str: return f"{self._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: Use beartype.peps.resolve_pep563 and beartype.door.is_subhint? # if old_attribute.annotation is not None and new_attribute.annotation is not None: # if not is_subhint(new_attribute.annotation, old_attribute.annotation): 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(f"API check: {old_obj.path} | {new_obj.path}: skip alias with unknown target") return yield from _type_based_yield(old_member, new_member, seen_paths=seen_paths) 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(f"API check: {old_obj.path}.{name}: skip non-public object") continue _logger.debug(f"API check: {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: Use beartype.peps.resolve_pep563 and beartype.door.is_subhint? return True _sentinel = object() def find_breaking_changes( old_obj: Object | Alias, new_obj: Object | Alias, *, ignore_private: bool = _sentinel, # type: ignore[assignment] ) -> 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) """ if ignore_private is not _sentinel: warnings.warn( "The `ignore_private` parameter is deprecated and will be removed in a future version.", DeprecationWarning, stacklevel=2, ) yield from _member_incompatibilities(old_obj, new_obj) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/CODE_OF_CONDUCT.md�������������������������������������������������������������0000664�0001750�0001750�00000012550�14645165123�017007� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/duties.py����������������������������������������������������������������������0000664�0001750�0001750�00000040732�14645165123�016062� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Development tasks.""" from __future__ import annotations import os import sys from contextlib import contextmanager from functools import partial from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Iterator from duty import duty, tools if TYPE_CHECKING: 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: # noqa: D103 if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title @contextmanager def material_insiders() -> Iterator[bool]: # noqa: D103 if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: yield True finally: os.environ.pop("MATERIAL_INSIDERS") else: yield False @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") @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) def check(ctx: Context) -> None: # noqa: ARG001 """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 ``` """ 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) -> 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. """ 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) if "pawamoy-insiders/griffe" in origin: ctx.run("git remote add upstream git@github.com:mkdocstrings/griffe", silent=True, nofail=True) ctx.run( tools.mkdocs.gh_deploy(remote_name="upstream", 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 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"]) 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. """ origin = ctx.run("git config --get remote.origin.url", silent=True) if "pawamoy-insiders/griffe" in origin: ctx.run( lambda: False, title="Not releasing from insiders repository (do that from public repo instead!)", ) 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): # noqa: D101 def __init__(self, cli_value: str = "") -> None: # noqa: D107 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. 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.agents.visitor 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-0.48.0/logo.svg�����������������������������������������������������������������������0000664�0001750�0001750�00000014267�14645165123�015700� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?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-0.48.0/Makefile�����������������������������������������������������������������������0000664�0001750�0001750�00000000760�14645165123�015650� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/CONTRIBUTING.md����������������������������������������������������������������0000664�0001750�0001750�00000001566�14645165123�016446� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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) However 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-0.48.0/mkdocs.yml���������������������������������������������������������������������0000664�0001750�0001750�00000020120�14645165123�016203� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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 - 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 - 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 # TODO: Add an Extensions tab, with Built-in, Official and Third-Party sections. - 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 - Deprecated: reference/api/deprecated.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 features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.expand - navigation.footer - navigation.indexes - 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 - literate-nav: nav_file: SUMMARY.md - section-index - coverage: page_path: guide/contributors/coverage - mkdocstrings: enabled: !ENV [MKDOCSTRINGS_ENABLED, true] handlers: python: import: # YORE: SOL 3.13: Replace `3.13` with `3` within line. - url: https://docs.python.org/3.13/objects.inv domains: [std, py] paths: [src, scripts, .] options: docstring_options: ignore_init_summary: true 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: false 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 - git-committers: enabled: !ENV [DEPLOY, false] repository: mkdocstrings/griffe - 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 extensions.md: guide/users/extending.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-0.48.0/docs/��������������������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�015135� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/changelog.md��������������������������������������������������������������0000664�0001750�0001750�00000000062�14645165123�017404� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- hide: - navigation --- --8<-- "CHANGELOG.md" ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/.overrides/���������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�017215� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/.overrides/partials/������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�021034� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/.overrides/partials/comments.html�����������������������������������������0000664�0001750�0001750�00000004137�14645165123�023554� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- 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-0.48.0/docs/.overrides/main.html������������������������������������������������������0000664�0001750�0001750�00000001104�14645165123�021023� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{% 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-0.48.0/docs/insiders/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�016755� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/insiders/changelog.md�����������������������������������������������������0000664�0001750�0001750�00000000721�14645165123�021226� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog ## Griffe Insiders ### 1.2.0 <small>March 11, 2024</small> { id="1.2.0" } - [Expressions modernization](../guide/users/navigating.md#modernization) ### 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) ### 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-0.48.0/docs/insiders/goals.yml��������������������������������������������������������0000664�0001750�0001750�00000001277�14645165123�020614� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������goals: 500: name: PlasmaVac User Guide features: [] 1000: name: GraviFridge Fluid Renewal features: - name: "Markdown output format for the `griffe check` command" ref: /checking/#markdown since: 2024/01/16 - name: "GitHub output format for the `griffe check` command" ref: /checking/#github since: 2024/01/16 1500: name: HyperLamp Navigation Tips features: - name: "Check API of Python packages from PyPI" ref: /checking/#using-pypi since: 2024/03/02 - name: "Expressions modernization" ref: /expressions/#modernization since: 2024/03/11 2000: name: FusionDrive Ejection Configuration features: [] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/insiders/installation.md��������������������������������������������������0000664�0001750�0001750�00000005554�14645165123�022011� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- 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. [become an eligible sponsor]: index.md#how-to-become-a-sponsor ## Installation ### with PyPI Insiders [PyPI Insiders](https://pawamoy.github.io/pypi-insiders/) 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.). See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). **We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use](index.md#terms).** ### with pip (ssh/https) *Griffe Insiders* can be installed with `pip` [using SSH][using ssh]: ```bash pip install git+ssh://git@github.com/pawamoy-insiders/griffe.git ``` [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh 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] 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] > 3. Enter a name and select the [`repo`][scopes] scope > 4. Generate the token and store it in a safe place > > [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token > [Generate a new token]: https://github.com/settings/tokens/new > [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes > > 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. [changelog]: ./changelog.md ����������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/insiders/index.md���������������������������������������������������������0000664�0001750�0001750�00000024060�14645165123�020410� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Insiders *Griffe* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][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 made collaborators of this 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" ``` ```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).**" ) ``` ## 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**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email to insiders@pawamoy.fr with the name of your organization and the GitHub account of the individual that should be added as a collaborator.[^4] You can cancel your sponsorship anytime.[^5] [^4]: It's currently not possible to grant access to each member of an organization, as GitHub only allows for adding users. Thus, after sponsoring, please send an email to insiders@pawamoy.fr, stating which account should become a collaborator of the Insiders repository. We're working on a solution which will make access to organizations much simpler. To ensure that access is not tied to a particular individual GitHub account, 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 added to the list of collaborators, the bot account can create a private fork of the private Insiders GitHub repository, and grant access to all members of the organizations. [^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](https://github.com/sponsors/pawamoy){ .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, you're automatically removed as a collaborator and 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]. [insiders]: #what-is-insiders [sponsorship]: #what-sponsorships-achieve [sponsors]: #how-to-become-a-sponsor [features]: #whats-in-it-for-me [funding]: #funding [goals completed]: #goals-completed [github sponsor profile]: https://github.com/sponsors/pawamoy [billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle [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 <script src="../js/insiders.js"></script> <script>updateInsidersPage('pawamoy');</script> ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/installation.md�����������������������������������������������������������0000664�0001750�0001750�00000004151�14645165123�020161� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-ruff: uv" ```bash uv pip install griffe ``` <div class="result" markdown> [uv](https://github.com/astral-sh/uv) is an ultra fast dependency resolver and package installer, 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> �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/css/����������������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�015725� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/css/material.css����������������������������������������������������������0000664�0001750�0001750�00000015767�14645165123�020255� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* 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-0.48.0/docs/css/custom.css������������������������������������������������������������0000664�0001750�0001750�00000000237�14645165123�017753� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* Prevent selection of prompts in pycon code blocks. */ .language-pycon .gp, .language-pycon .go { /* Generic.Prompt, Generic.Output */ user-select: none; }�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/css/insiders.css����������������������������������������������������������0000664�0001750�0001750�00000003731�14645165123�020263� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@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-0.48.0/docs/css/mkdocstrings.css������������������������������������������������������0000664�0001750�0001750�00000002706�14645165123�021153� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* 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; } ����������������������������������������������������������python-griffe-0.48.0/docs/playground.md�������������������������������������������������������������0000664�0001750�0001750�00000001001�14645165123�017633� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- 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-0.48.0/docs/js/�����������������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�015551� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/js/insiders.js������������������������������������������������������������0000664�0001750�0001750�00000005473�14645165123�017740� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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-0.48.0/docs/js/feedback.js������������������������������������������������������������0000664�0001750�0001750�00000000775�14645165123�017644� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������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-0.48.0/docs/schema-docstrings-options.json��������������������������������������������0000664�0001750�0001750�00000002070�14645165123�023135� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "$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-0.48.0/docs/img/����������������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�015711� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/img/gha_annotations_2.png�������������������������������������������������0000664�0001750�0001750�00000041627�14645165123�022026� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR��‘���¶���—':���sBITÛáOà�� �IDATxœíÝgXÇ�àovO½wAQ@@Œ`C±·ØkÌ5ͨѨijLŒ¹ILÔ“ØcÔØ» "‚ ©J‘Ò9½ìÎý(*E¼Nœ÷ñàž™ÙÙÝogf÷Ì ‘‘4âÜÉ%ëÞ] ‚h—¨×]�‚ ˆ6 1‹ mòÒcmã?qöÜÉÓfOž6{ò´™ƒÝD�€Òwî2qöÔ³Æ ñ±¾ìR<7®ë’ÛëW…ryCfM™Þ‹[÷W^à¶ G¾uá÷ù)gÕ{Ýh��@sEÚg1-|ãïóL)�à:}xeSLþÆ y#SVþ¼¢» ·­p[–øÉ;ôB÷ª=xæ: =FìÎùx¤9Çcå7Q»zê½Îb¯ç¥çÀãSù1Ïg¨ý xöëmZxnOx×¾ïˆ~>ŽÆäª›Mâ5 ©DbÌ2r‰˜‘Õÿ••J”’9#UÈÔ ©×m*+±LÊ*%bVV«ÄõÛV·rC<Ë·ëúþÎYË‹òço)c_Ïδ+m¨C,‘Ë4r‰ËÄr¥D¡zÅ&^»—³¸\«R>q‘ò,m ËRÎ?PbPæÝNí:ÌÉœÎ-d^vYžVHjå¸cV.3²ú8„eµrI­³ ©T.‘ÖÿU%V¨Är#—J™¸!f5$¤ÌOÞ¿¿hgßÂ-e*gß…?‡úZph¶2zõögªX�àù-ŸúÞ(—Çÿqé…ÔšFi Dz…Ÿ;_ødÞÕB 2 ýÅWoYó)¨LßùážÓ™�àuö[þÛÐnBYALt¢Ýh‡=KWœÑP®³âÊe”’Û¿ïùiw¢™}åúO9ð¡tOšesÉé쾦ÀÐD^œ^oïü˜w û÷^°6Ù¡è‡u«WÁ3çÕ¦:ĹTªªY™X!y²b‰7ÍËî".‹L<Ç͘4uÚð�/s>��` Õõw°J­áêëóÛgï+¤µr‰c©\Z+“5Ä^i­\R£À ÔÊ%â†H&‘ÕÔÈU˜‘Š%ÉÓm)DóhVͰH§ÿò ]b¶LîùÅ´/Ëú¯ìÅ�d9eú'^é_õ[6¢û‡¸ƒW,rjÔ¤,FO]î—üÍÂøB �¯óŒµÞyŸ}=ÞwÅŠXû÷{ê�� \>Òæä¦·û|ûÕyÛ½)†@zVÏè—s`Žï—ÇFr>š>ÙnvgY–êán~z×'£×,ÜÉ™ô—5“fyž¼Ûïo=­¬<¾àëûÞ¡…mÊ«-uˆ% iL «•Ëj夕úf{éí,VRRP,¾{ûL™Æ¸³È€žÕGcrU%ù•¾îîV¹·ËhKΦ´tv¿Flõñ™ë‘’eqÔÂiÐÐ^d²×¯û+±ZúçÐí¬´~[õåCs®±*`®~úí5µæñ„(ÝŽ=fL6LX{WeQï}~)U�êĬ\3>€F¯ÇÇ{¶fË� "üƒ•1Xõ°·,ôYù1Þ6étrݵ­Jÿ¹ß7´B �™× ÀÇЀmÓµkõ•µÅ*Àª‹—/æùX�Ï©OŸêó#ïV³�%·N_óÎ�ã©ÍtN10÷ïDÞc€²´Bé(CC j›Ê« ›[˜#–ˆ*äù÷ª”R±L¤«#hK^mªCyâ7Co«Õ˜=¼eÒ´Çö8ñê¼ì˜…¥÷¯\º_÷sYÆí{®vÆTnimrÜUC¿þo»àšü”üµ¹JÝN[üX#¯?Ñ(dþʪ”J��¬’6†cÔr�€Q(]W”ñ¸C¿ŒÆXUQ¿eÛO'Å(³~~äe/d4HÏŽW�”Ž¡#®®ïø° …äa Û‰«l¡&®¼¶áòGzîÓÆÌm«‡¬o*,ÏE�€ººòûuA­­(ÇV� ÐÑYO>øÍh�€âsk÷êQÐ쀖«��3˜­k 7™�V©Ô�3j �Æâ·-¯6Ô!0J)� VË›Ixc¼ì˜Eé˜ÙŠ”¥u—E!ÌÖ_TY—ÎdÆ��eÚs¸¢¢ê_;°Z?ßèÔé:o}OÙÂæDˆ±™ßºË��¬¬ºŠ60æ#Ð`�ÊÀØN$Ï/³�ÀVž˜»éδ…K>MúÏWw¥�œÁŸ¿'Ü5ü¿gò5ü9Gæ��VÊ==€› ��¹¤º:ï¯Ñ?ý“ûœ÷„¦ój’âÿÍ‹ žÁK×Ahß;ÀÏË„‡€kÜÉÃòïW°€tÜOÚÅ€Ú°“· “y·òͤ@<=Aíý ¼Ž£½;r¸|>,¹y>×e|_'!�×(èÇÅkf˜×ñ±²ŠÒª¸U‡Ò†Lšë'D�H_G§¦$»D<³À‘N<>‹�˜âŒ “ž<@Fý{÷³¥��ÔÙq—L‚¦9é!�¡Õ°5ÓFº¶­Þt^Mj)/^¿Ÿ×ÙÐ]çùªŒ yÙí,¶âvdbßþƒ&½ÅeäU¹×£¯1� »{ã¶m¿á3ßeUÎÕ˜;oPÈ\sçàN¿O~Öï~yÚßçw]™1sÛàÌñgÓþÞ½Î~êêK„4S¶ò—ÜÆ7¸2é·•Þ~?2nèþkW.î+œ½ú\§¢¼‚sžŒýyäÊÏr?Xvîû°>¿.Ø?±:+"6ö¶Æ �°,få.§'íHà«Ì=uêÇ{mR7•×ü³MîX yažHdÄå’W˜‰ÿ"ß7ü7¢LÞ>ºØñç/¿Ö´¾1Ahrãû÷@fßý{K p<ú8§g'lÄ¿ÏËžxUpyXÄ™!S¿6 «Ä7×î /&cáÄ¿é¡MHß mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›¼ŠwJùÝúøyZéÒʲ”+Ñ Er�@:¶>~½ÝL ¬Èˆ¾Z û¼ÿˆŒÆýö÷«÷ö•5ì e9õ¯cÝvô_ù|óV ]˃k:``+%Òð¨{‹/ÔV7_U´¹ý‰ù˜]eʆlü½b ºì*W6û!‚Ð&/¿Åsôñw®½|`ßî®Ö:zw6¥�@àäÓß¾âÒ¾}ï»XbÕ»o'ÁK/Ç+ÀéàâÜÖ5*Z§©ZôåE³±ž–躭ìÒÒ¼ <Kçv:{"A¼/=fñì:[UÜJ*V`,/Œ;púV9 À±r²ªNK-Qb¬,OM­¶è`Å{Ùy÷EÇÿÿ“Ÿß±ÿdØ®û!��dèûÞæSÑQb.œúu†‡.��pýV†ÿ9ÕÑwé騸«—Î,ó­^˜ºNýåÈ¹Øø˜£_±zXáÜ>__¾³ŽÝ³¶2¯èÔ§“€24Y=ß7yU¯ÔUomî¥S7aµ‘[â K/—”µ}2¿õ^dS?;ËÓ{wö[‰ßöIÿ´ÓH£ö9‡5A<«—>矾±‘R¡ï5dB¾òAJBìÍÒѱ55õÝ%UX­o¨‡ ²ýuYì{ÛÞþÞOµzþßûbÖùëRÁuÚWÓ›‡ :-v˜³ûàâ‘á³ö–²êØ•!Ÿ^:ûQß�NÔìiCòƒ>ùÑä‘?%×͵ ¬-+­”´eÞÄ£‘ZƒPÿ`—á¥é½6T1ŽNQï9'¦WBÕµT¾ç5—b÷íú†�H×YŸÞt«ûjÔ¬Ÿõ+>}¢M™DûòÒdzx|¾­Yƹã‡kŽý‡÷÷­8“Ïáp†¡œ†L@Çí½¡a¹´¿˜�℈Ø NˆK±œài„RËRÈÕÈ0@Á­¤ª±V¦”6?˜&ñØþ)yâõ“næ í*õG¶¡È¢£ýGnÊS›dÀÑú"V �…µÉ``­‡ ùåh49¥Ûó lB–ÜÌO‰Y„{é1‹ae^ʽJ% ÊûÉÙÝlŒ¨<…ZC i\›Ÿ‘ŠjÚŒÒ(4í2``Iuý4ìŒL¦ÖÕÓ¥ \à2bɉÞfH£XšÑG[îni$âºyÏFƒ í]3ŽÑºÕþ?"VRY{à`ÚÏù,�˜8Z¯5÷‚syÊÄP+˜ºh¶aµ#‚ÐV/} IM-eÁ§�X�@�³€¥µ5”­!¯<9¾@èþ·ºTÒNcÒÓ¯[ùø<‰XÌÒ¦~½´cØÄ‰Û3åŸ/¾~É%ÐT-Z™´KÜè/´Á'ÓlõÃ]U¨¸¦Û¾±{É% ˆväeÁcY^V¥•—‡9Ñ"·tqA5 LQv¡«»µ�!¥§›AAfQ»\E�@·gð@3(ó] “j0êÉsïÈ6÷ÖLjÏoX›+•j3{;�GÇ@ÔÊSnOOEÿ1Éú9Ž�¢ š»E* §·Ì}¹”°ái¥Ri#‡c$ /ßÿB/ý´Æ’´¸Ë¥S¦LçÅM‹¹ž¯�Uε˜û†~“&OŸÔÇðþ¥øìvûö.½QäýÝþ°¨ÃóEû¿Ý•®MêÁí‰Ý¿>~pçŸX‡¯?¦ºjUˆ�šw—‡î¸št3úøÚá¶­¼tÀÓ361Ôyž745[#”£æ¼uv¾ûû¨hMwîL§�>�@MjÑ_”]ì~9«¼¿u'Ó¯ÿBdο–p\?>´Q÷ë57Ûm3 Þ0äNL„6!1‹ mBú†AhÒÎ"B›˜E„6!1‹ mÒÄ{ðÎ\^}9‚ žE1k~ËþxŸé+, AD+Hß mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mÒÄ<¥ÿ?kê£P�¬9¤,®Â/# ‚ ÞL/¥5Ïņr±¡>ÇéñÆ¢ùB½Æ¿›˜ô´Îøkx²N[SìïNOÀûá¨2þ.ó¶/³˜Í-{þ¦mòî8Û¼Û÷emOƒ¶yw¬ÕýÛyòWÕÔ£í‚fŒ¶)º“'}uKÊÔoît׊ĬZ“^3'ºV'eW¿šÌ_ÇÎ6‡2}´ïHÇ)pÜÔñ~^‚›BŸS'û÷2«½}·LÝr*ÈÐ}È”éÃtáÉDG8–ÞÊ?þц.e~«Hñbv†hÙ‹ï~<ŒWRÅž¹Á�Àœ@v~(/&EþŒ…1îÒ¿/Gs‡•V$F½ßú§Ú¦ìÎ¥Kš*öµdÎVß¹È+ež;¤cë;Ä¿G'3J™|øÏ°{ ¾mï¡¡~ŽHV˜y2"³öѾ½ìEF½'M2Žûãtnë»Ôhß)ÏžÒÈëÓ•4p; {{ûÆ«Uˆj6•†ŒÂßö…G~Û–£¾i^-ªx=’hÙ ŽYCߢ-¨å»ëï8ëO©¾›&ú]ÂZ„ô=ÇN¤“röŸmYj¾‘…%¯Rñúïàm£(ÉÊ~m™k*³²žÿÓ´Y¯·‡:Þ;û÷©¼5 „€¶óÑCýÇÆ4¥©ßäÑÁž%oI“—»³”‘«›næù‚g‹ÁöèU—Ë1‹5”‘¾òÁµ*3Ðl:3¢:ëëVÝ-V�(˳2Ÿ¹¨´í<Õj/8fÍ ä%f31)ÌÇÃx�°þ”*1›™=ˆwæFkM-Ú¡gÛ¼ðÍÑi2��ein-�� €A·ãàY£]-t¡&+öXصb5�×¼{àPÿNÆeEê¥SgS+4�ˆgá=0¸_gK]\9ô÷G{iÕsâxÇ{ûÇ—j��(óþsÇêœû3<‡Q·)(o>tG<—Qó»ü¹?ÍÜoØ 7+C!­(I<sòÂ=)Öë6iŠAb¯÷OÃÌ£#õÇO3ÏLzõt0â©KoE={OŒiûw‚äGvDWÚ†Ìî-O¨rèén¥ µÙqGÏ$«¨“ÿˆ¡^Ö|eEF|:ÏÇ8qÛ©ÌFWmä4x€‡¥R”¥] Oz Žëð÷ÜJ/3{»˜™ª´ˆ“a·+›¸)S¿wÆéžÛrö>Ø7;í|B†öp4¤åEw"_ºÛ¨ÕÄuêÑ]uuW\n]LÂ�(‹ÎŽª;§Òk4Jâ‹Þïæ¬sûvCWðáΖQ-—é9„y[ér°¢<-bD<^™' L;p %,P¦]ºî…2MWZ¶‘ßìALt†yPÿ.œ¤Ý›“fÓ=·åë;6¤‹ž!L_Ð-ïvîØÃBŸ;f¾Óƒø#û.W›>}Î@CF5Žƒ&÷wâëÚ½7¯OúÙm·íf¦NnŽ.Òé6©‰ À·ôì×Ñ�$E·ÒTLCÐzº†9®Ãßí˜uNêÒ˺ôÌÏnèu›4Ã2'Y·[[-ɉ;{:ží;oubû¹< �pœ‡¼¬8´ùb1iè5áEƬÙ\K#êÛƒr�èd]?º¿=Bµñ]áì@îöˆ–F(³v¼œK÷dOÿmÞÙìÆ®íá•”µÿÛvIÝ{Giç7¦÷ò®ß’jEÝ'Læ[²ûr%íà?v€NÂÞ?þ.}N- 6uI¸ëZzj}À�¶"·�ÚS9eüŽúUbÊÙ–{'“µ´³ç_–`7÷Ú‘k¹åj‘÷¨Y»ÝÉŠ+@F^Ü®žÞ²±DÆhÈÄÃCxpß/Çzn#æôLÉŽx¬#ËqêÞ!l×ös•”µÿÛ“º¦ìM’ê¸éT~lë¾,¥¾ëàÉcùy‰U‚…ßÈ Óäý”0†ž£Þ;¨zkx$tó²<¸ÿ÷cbžSàô1½SOßmeh¦©ÜùÇy*Nÿ½þžÂ¬ß„·‡ºîOn@”™£µ¼²¢×”™.f̓Ԩ“—2ki#cQMj}ïO]Y!161¦@ÚD¼l©„”E·�÷Ús›e+i‘¹+Ð}¬2‘ÃÀ'�eîîÂI +a�t›©4Ê¢× É…c[ÃÊUÖÐ ��TyW„|è_¾kÇõj T2ž¬±)"Ž}ÀÓç û0£šÒ¢}|ãE]Ò~;’¬ÊÚîѾ5u mý‡ù nþýÓÍ Ú²÷¤IºP� h¢†•€tÝúõ¸vn÷ºIC #·.èŸ]?©<GÍ6 xÇÍdõx›È¼\¸önNÊÔ#¥$`5í…=7Ô¢ }9‰ÙÌÍìÇêúf6›˜ÍLèËÑ¢>Ž:|…´ÉÁr,ɸ}§BXU’‘+ˆmíÞ¥]K®Ô€¦*ùv‘YGDÛ¸wd“®Ü,Q0Œ¢ªRÂ��`àZ÷ŸàGÇÌnÜÒcŠsŠôí¬…ˆçàl^ŸXaÛÁ†F†vÖ(?¿‚¦"/»LÁ`¦&;·BGO¯®äTuRäµüZ•ZÃ��0…I ùR˜ÚÌ{Å:†Oì–f$>,¶X¤/BÀuèh_‘Ÿ%e±¦:ýFêã%”‰K¬ø[%J šªä¸Æ­‹- �À–§^¿[«,ËÍ,`E-Ve³¹wpëT{çê=1ƒÕ¥·Ò*œìݱ(‘¾¾Y£¢°=¿­ßuöÓð¡nº—ËeÕê†Bª4 —ÛÜ=®…²Òꮇ‡­>­—VÔGÉG• M8ÊÜÍÒSÊXh¾Ò]’páV±L¥Ö<ÃÕÝä9óXFÍzú@S&NNüÌ«Iej`%7óêÂx³5,¿{1§ZQâÔ§y½H΂¦"%! wèhRy'MêâêÀàÚºv”§¥< !«/¬5¡/G$¤¾=XßPºWô¨Ê7œRí˜/œÐ—ÓBS +*¾ ©kËÆ2̲ˆB€tôt ;ŽûØ�P(/ $ÔHËžx†E÷ð3¤Ø{2Õg€*?ÿAˆ­ ÆÙ®<û|¶Ž§—“¥n…ƒ~Qb \S>ý»;ò0Pº&TNýgؚʚFÉc™¤þa&˲€‚dž5°ìÉb_WHK¥õBX.•âÇžÙ"]¡\,©/)+‘Hù¦B ��Ke ;†ŒPëwš¦r×ÓáYõ™ñ‰/Ô×Z!ŸððÎpIâÕ´ 5€:ïFjåL[stO­FnCf\­RkžÊ©>“J(N>¾[Ó;`Ðìþs9"âjWf޶qí¬Ê8XÆB •†åUUÊV+¢A“ç ÈeÔ¬§4èdµ GJ-•©@s5 ÀVWW?‘VÈëÇ|Y©XÎ7°•©É•SÜ;ðòØÎNâô½ådˆ¬9/&fY¡ }9a74ß ]Jõðï±a74Olð¶<¯˜îâ$ȼûÔ㧆8±B&}påðŽèÆç-’*tE:jÇé°=÷:O6¨KÑ©tI£4¤ybo{…meNŒ´F'› éÔY×¢ì~¾Š²è;rˆyÒ®}GÊÔ”IÏé3 ›/HK•òôȬJ¦`uutÔ`�Äã?¢±L*×1Ô¥X� ôDz ‰ìyï¶Mä.•©ò.nÙ(i¢ØlmµX(Ò¡ †@ÍaX4UåbC# JY�®±±^M^ås•GU–~ñ@ú·ÉÃ<Ørº´q±âÉDZíÚQ–v¨.¯*­-—u“ç Ç¡QF-~ú‰_•r¥@O—ª;R4ÏÁ�ÍÔpÓWÒÑ«ßcJd £*�צ&•õtsvV;Ô¤\}¾z~3¼˜¾áœ@žHHm‹x§vÎì˜/xøë¶•HHÍ ä5›„:ûZ|µKÈØ¾.æº<Wh`fm"hf[¦àÎ]®goO >@ Œ,M�LaF6dz§‡)!Ž®©a]ïD)‘H².†eX…¸é5len>·cyV޳åÙyBo;q~¾ @(PU?¨RpÍ\ÍZí‡=3U^V¾‰‡ƒJ·ƒ—›ÙãµÏ–§%×8õö¶ à¹÷q£Ó3žñ±Ù3åž“–eìÝßÕ‹�8:¦–¼F¸ÒätuW¿·,ùÇÀ¥·›NnN Ö¦eR]{¸pÀÒ§§ÝƒôœçxMtL­Mø€ªæA¹Œzª‘øÔãÚºu’¤¥Ô7MZ©´gÕÔ9Ãy,£6`+²³4|ºšp�ñͽ»ÛsëŠÚR ϶ǰ¡]MêŠÏ±òð±RÀ1ñð킳ï•1�ÒôÔ"»ž~N5i©5¤•Õ¼Ðβ2B!oqĪŸhC5¾Ö‹«ðXõ?î¶U3M-¦ìÊ?™Àþ3{ )FZy7òèñЦ;“š¼˜CÑCÆ¿¤Ïaå‘'NW(TYÑGâ‚‚§}¬‹d÷cöï½U¿5VæDËx'4Ä«àЭ‡7s¦ä~±^㔬 Àåäó»;Þͯb)¸y)oXè¼™J…¼èvV–Âàÿ­¡‡Ä©gO™5w!GU••œ_ÌÐý7.¿z2\8d䇽õheyڕÑyšØ}—ß;{@<ñ£‘Bš‘–ÞŠ<|¾æá]†)‰?z> ôíyºluö'2•�Ptåd|HèÜFr¥w.œ¸ù<×%°ôðvGŠQ)¤EžÉր𱠞8p‡Ì«3âfÕJ¥=«§Ï™sF®eÔ–ÄòcN_<}±?HŠnÆ'—¸@Ó5ü°ûÊ5´êà,½ƒ �ä¹¹Œïô…cõiqNì鋹j��yfJÁ Q¢Ûéµ$dµ�‰Œ,ÿîÜÉe~ËýxŸi«I|1žßÏû½\ÒâûæzBtx©ðR óÍÁg…ø£LýÞ«{vëÙgxe’¨÷ï¨4½n“?´¿ùÉô'v »ŽŸÝ5sç?7¥¯§`ÚáÜÄ-X†ßîËmuK± [½¸®–öA"+{~mA¹”¡EÎÝÝ Jbhóµ÷j¼1•Æ·õíi“{‚¬–½€˜µá”ê»iü٭Ǭ’*vC£±ù7â[õ6~¢!FR”t2<í¿Õô&{*v z¤¯"éä±§ŸA{}C‚ ˆW†ÌùG„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›¼”õ ÿ4M …:|CsÿžucXÃh” •\.c˜ç9‚hRûŠYB¡P__¿ª²¬º¼H©T`Üô—®B|¾@Gd`jjV[[+—ÿû¾ÌADÓÚQÌ …B¡ 07S¡h%aŒ ¹B!—‰k̭퀄-‚xC´—˜EÓ´¾¾þËÁÁaæÌ™tpp@åææ^¼xqÇŽÙÙõkT)òEy6U*é$Ä› ½|GZOO¤RJ+ËJê~år¹«V­Z´h÷äÔ¦jµú×_]¾|¹JU?E„±™%¯+‘ˆ_MQ ‚xÚËsC¾€'×ÔÿÌç?~|Ù²eO,�àr¹ .  ë§»”‰kø‚ægmn ï­Å§®'Ŭnñ¢+™MÚu'33éסÍÍý¢PÖc6Ä%%ïÙúl@íí<ïXÒ®©–€Î€Õg×÷|¾£Jü«µ—˜Å¡9JeýÌA¿üòKppð7Ö­[W^^Þx³ªªªõë×_¹r% à÷߯û£R©àÐ-ur‘žKÈüv‰½žšvçÖÕ¨Ó{×-hE�Åé zú:u§Ì{Oý`–ÃÚŒÀñýòBFfjÄR¯†p@»|p4-3íÚ÷þ/òrÒ üéZZffÆÝøï鶺5m?p·“|Lªrt ô:ú¢¿ëu¸�� �IDAT&W-z¡(ø—Ëç—z½üP¢ºåäÉk��Qè/û¸·—Q âuk/1 �Õ=%ôññ™;w.�œ>}zñâÅ;wöððÐÓÓÓÕÕuwwïܹóÂ… ?�Ó¦Mó÷÷�ŒqóoE�2ðåÁÃ?ÏÝ«“1./(¬dôßò±`Å,�(.¯äÛ½÷»û Y� Ç-ûüƒYþ¶Ï7ãx“š{öÙ¸„úý†ú€\*£ÃüD­lÎï>ã«O>˜èS¿°y»gûv÷ þ6áeO§ˆŒŽêWvòDÊËŸ·Qsÿ̯›" Y�õ ði}AGâÑ~bV½ùóçS���ªªªRRRd2™\.OKK«kvÕýWÝÆ­$‡ô|úí¤N¦8â›ñ~½ûìëÓ½ïØïb$��¼Á?߸yëÖŽ©–´A÷9¿n~×K[Oß–™‘²uŒa«Šþ¨?R22S·Ž7F�€LßÞ™š‘™òÛȇm%¬Fv£¾=};%1öðšñ›ê'"ƒÃúàÒ“ÛO–bƒþÃú=ZÝ•kðÁOû#¯ÞIKNŠûûó@+K¿Eþ0цæº|&#ãÞÍïòh—ݺuóâªÞ<��¤ë:úËm'¯Ü¾“šxñøKBë2¥]>8š–ùep¿6¿’t'%áôŸ üêV±A†ÝgýwoäõÛ©)7â£þöi m'e>d”ïýS'ï>±È¡°óدÿ‰¾ž’z=rç’!¡ŸŸ?ñ‘+�ßvûìâ® $nßo.Ýø!€�HÔmÆwŒJ¸s'9þÔó{›<YÕ¼ u×ãVõ曆üxæç‘?8ž’™™´yLŸÏ"ÿšbÕP:ÊfÆßÉ‘K»k{¯˜hƒv³‚ƒƒ@­Voܸ±¹m6oÞ¬P(� ((ˆ¦[léúlN±gVú÷­Êº‹ +ËÔ<¹>é²25,»}èà¡Ã—r”/`õáÀõdóïWS^c¾Þø‘7ÿÉ-q@¨Ÿ®¾rþ¯ˆ¸*¬ß/t@}°Ôí¹tû¦ú›)2o%ç«Í­ ºZÇH¨Q��[•röСƒGŠžX‹Úfôº]kfôwDI·‹èŽç¬Ûµ&äázg´ÍÄu›æº) ¾ÿò@�×wÁºec¼ ‹.8“.³ò4‡§ê€²:Ò#õø™'–Ðí³dóªžyföþá>4ã« 6­žTX^{cÏÊw†ùõ ý4ÆüUs½šîúáò°%#VÆÈîméÞ±£ç{GN†w hY—eÔµä\øf×ú%þ}ÚWÌ233311€{÷î6·YYYYrr2�èééÙØØ´ míä @ ¾s9^Œyþß]»›‘™™‘™™‘º)ôñ«Ø’Èß6_|À[}eÛ—Ë?ÿjWbÃK´ãÜi™uL=³Ð­ 7uåG˜0mô˜•‘5˜ë:Ì󉋙 í£â+Ñת¯GÅÕbÝ>Ú �Ð0}œ#ÍÛ3oØèICû÷öÓ yöÉv^“``Š"Öþùg«§?Öè¡;™ÑßIb¾5|ʤáãVÅH)‹ÁïŽsjìˆSzè½Ð·§O|{E„Sú^Ž Œ (ÀÕw#¶}óÉŒa.zú,í:Â%ñxøARÐ{Ì0ãK›¾;š\\^œvvýư*ÜzGNs?úÐÙÙjªsãŽ_È5sr¶ú™º¦:“ëT7i0س8üL Yo’ö³øüúf‡ÓÊ+—[9ö›V7š„0ư´ðnzú½bi×àÄÊŠÜ´´ôº9mi~iÒ“d�lÙ•Øthc;»Ç/NÊbðpÈ®EÇK±,þB¼„½‡ 2£€cçÜA€°4ñÒu ��«·ž3×¹³#4YW¯•±�lY•L âttíÔPXv#î† �‹srÊ@\.òÊÁC™rÊaÔÚӗ¶}6ÂEçé”9]F„Ú^9Qöx(s~^jFÊ|ªÌ»9­×mê;såÖ£g/^ºp1rëÌŽ4MqžqÄJs/ìt¦{È` i@Wѹð4Më#þ=ÚWÌ*++Óh4�àäädjÚì«aúúú;w�ŒqIII ²ŠJ ´‹{¨¯ÿ:eĨ©Ûx[f‹,3|äðá#‡óÁžì§.„��Ÿzt‡(D£úšHš² ú!ÝÀãïe¦ßøeˆ!¾ÏÐÁ–TÃc…6uPQ3WþÃD°\*«ÿ‰}ºqMìš±Ãç}ðæ®ƒÿì5û¶ÍîôÄ=ƒë9*ÔèⱋO¯§‹�0û诬†m®Ä 7"Ê~ÊO¾k·jJPÿ�ÿAswfµå&Âäœ9îhË1èY~6„¬7KûŠYJ¥211�8Îwß}×Üfß|óM]ó*55µ¶¶¶…±øÊ…krL[^ôŸn­í,£Ö� ¾ÿ¬O©Ô* Æ@™[[Ð�ÈÀ«[Ç'×8î=»‹Жýü»p€©ÈÏoü%#Ê6$Ô›¬¸8ó^Ì"1 üî¡C¬qQ^#Ý·úûŠ� {Gs.�h4�$l²”ª{iÙàtìåkFPf¾½;r@“•žÕâu„zBÅýè-Ë'Žüéš„n½½ Kï;bïÂñاºŒlY^¾Ê¶Kdž§´]»újÆjµ¸Ü†åß)S{;= �€çîÛMuiïž[å* @›Z<5ÿ†e¦;¶ ìÔ—ÀA]úx„Ÿ}ò‰�ño×îÞzÙ½{·ÏúõëǵuëÖˆˆˆ�€™™Y``à¬Y³nÜJrlÉñ6ŽôZÒ£Çü}±“óó«X}+[@SM-¦$¯@‰»øæ€]SqzÁÂC­¤®JKÊPöê8ý‡õf·±ÇàO…EÊlôºÓïJ,];êƒ:ëÌÉÛ.1Ú!$¤+4·~›>qK�@9¼³/l©—Wh°ÝîÇϕ i7yó©n)ØÂÅéîê¾ÿ9!-¸Ÿ¯Á¦nøÇ£„Êûk檬F»yxkÄ”CúyìĈlpô꬇ËÏí8’Í4ÿ2€ÎࢗXÜŒO-vïÄELaföc˯ëú¤>»(¾‰/uÊ/—þúþ²Q÷7Ä”ëzLøp¸Uÿn°<#å¾å°Ð^&]ªäؽ?µ;Š�@SZð@×׳“NLŠÚ²ïGó‚ Ñæ ‡å%EµV}w·ÌÏV3Õb [|þdâüQóG:åŸû™„¬7NûigáºÖÖ­[³³³CCC—,YÒ½{÷]»véèÔ¯Ð4½{÷  à·ß~ƒú®Y³](eêÖ¹üvêf¾L×ÚÙÉF¤*N‰9¾ãLÚSa W…­_s<µ‚5íìéÈ—ÔÈ[í–19»>_u8±”vòÖß&wËÂï£û–^úñ“­©Âúš’›?ÿpã­F¯6ÑÎ!Cݸ ¹v¶ áY[q.U\÷‡Ús_Íýlw\–ÔÀÅÛË‘“sþB†€-8òýº¨ÌjÚʽ«-ˆkß ¶äô§Óo‰ÊÔØzu³‡Ü˜]K¦/=^ÒrçKsÿæµr“î!ã&ïoQ¿Ňo4JøPv<±Éײ$1kßûöŽË'GÆœÜ<roBCѤïüê‡ôîߟ¿r%êàCr·î¬,×ÜÚºrkÕˆ±Wã/l™¦Ø²öDeKåS_Û¶ö$»%*öèWú�pYÄ©›®>ÅgÏf‘/™¾qÚË÷ MLMJ î×}AÚÇÇ'**ê?þذaƒ±±ñ­[·nVUUe``�� …bÈ!—.]�@haëXQ^ñjŠJ´�™LØ=ãÖ„‘_êÈ8í<ïð±Àð‘oÿžM‚Ö›¦½´³” •ŽÈ îçk×®…„„L™2%!!áŸþi¼YÝ[ååååǯ X� #2P*^þ«ÙD»AÛØ!õtX. Xo ö³är™‘±™@Pÿ&@ll¬‡‡ÇÞ½{¾ýP§´´tÓ¦M‘‘‘u„FÆfr¹ìU—˜xxº†Vݧ}6Ë*z׉<²ÞDíe ža˜ÚÚZskûEyu=ÄòòòE‹-Z´¨ñf®®®„æÖöµµµdò¬7×kÁ±íSË®ÿýÙòðŠð=Bû´—ñ¬:çV–‰kžene#c2·2A¼YÚK;«Ž\.W©TB¡®ÈÀèYÖ°(//'-,‚x£´¯˜� ÃH$b‰äu—ƒ ˆv©½ŒÁA< ³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡Mþ1ˤ§àÂZݨ©ôS+ ¶o4µp™nÜá«×]‚h¯ÚÓw¤9[çÓN��˜¹ŒÍ¼§Ù®N¨yÝ{y8T`?šsO^@æ‚"ˆgÒÛYâ 6­ˆ•ñ(oÞÚ©Üí±Œ/†^îÂÁ¼`›ÇçÜÁÐ̼aA´«vVƒ¤(ùÒ@›r]Àó°¢} Õ9U¨»oªålJP¸8_³÷´úTÆ�@!ï>¼Y¾´«1Ð*|/Uýí´qr\jÊ,á¼§˜š‘›qææô¶¡¬Dˆ‘²×®©ÖG1X�„º à}Ò—cC³É7Ôá\Þ2_8µMößL�@–¹ïrzZ!ž‚MNÕlS§=>Ï eÁÝ6Ÿg¢\~Ÿþh ÇÃ&_W­ gŠ4€øÔˆ`Þ°N”­â«Ù»šßN©“¤`ãÉ_1‚6@à3Z'v4Ä(¦œ­UBÞw“8=M@\Êì;¡ÜŸÛ¶•Y â_¬ý¶ah.â"�� €±FŸræâäTMtX8q—Låzr��: üÊén …ùl®’²¢¡¢ñÂS /øO\–¬\z†©Á Q‚£’•0‘·4…\jÀ þ²ˆ0ôà­Ìé ‹++°¾'ïÓèaÕ¹?Nã4Å7Ôg Á݇÷ÓxŽySÓêºñHÉJ1‡z«¯`e_D`5è[Szbæêmõ-)r÷æ­ÆÑCH$…!€²ûš“×ÕQù ;SÔøá\G›+C&6œ÷ßæy¶Ç; A¼íñjð(ÜÒ¬Ì)C$w™¸Z�€¤3òѧE@ñ8_,å1 »ÂíjzT/J�8ò€|EÆP���€aW°`¹’ßW-?¨©[ä­Ñ,]«a1P8TP;‡PnŽçÓË›6B¸ V1ó«ÒŸ/„ˆêŠƒúôá8ÒrAõëMÓ,5›?Â…ÓWOsô©U•Åþ³C¾¥,{ öŒ ;{r.©sXvÏï²Ýž#ìžËµ´CŽNŽWtá¼e÷UßÇc �4�@8'ZñŸVcÌÝ´ˆç©Ouчە¯¦î ¢½k1KdBuÁ ³ ©š?Ï©K0�áOõ¢Eõ/`0—J—rÐ`Ø„,Œ�ƒ¢ñ¥¶ÜÏ=(Ëî:¦ÎhXK̰÷£!œÞ¶”> �piˆ²3„QJ«��9{½Bº���…œÌ!è$8Ô2‹Œõ�ž^ ^Á&—�”Ýg 0ídDYR§OÏÆ u¦Ì@!��†ƒ¸-ì? ·2± �jq®<…ˆG?oUÄ¿N{ŒYq¥Ko<6‚ãÐWð¹EU1¿ïVÝ’ÐS¦óêkè 59h­cBQJŒyÔØ NøM)àÓNáëâ;qªmI Ç¿Öÿ±ÞñÃt4f™G�ãëQÊÃEòËmªáƒ®O5 ¡àQüÙ]På]ÕÊ‹L‰ˆóÕnko2`‰7¿[ñFk¿ãYÙY .Âù·TRÙôJÌãÕÿ•°ÅJ�šêÕ©~øÉÌ >¥ÉWÏ[§<^ÝyK})€QBÀ sèœúZ[SßÒÀ¸°a„];P|�àP],‹s�F`Ƈ„MLŠ&&Es3—)TpP×®´¯9z8´…øo�� 'Ž-¶’-ÊÉ!ŒccÔ‘Yl¶  „aàs›]«ƒ ˆ'´ÇvÖÓŠ*XÓ¶žÜwT¬‰;· n¼ZÜId÷¥Æ û`….ê UOÿ]­��¦†É«a6czÌâøå¿/ÿ§)ÁMHåZ«éP_„4ñIšÚ.‡~‚m¶Lµ>íf„ ¾©‡/]Òdºr;öl5Ó$‹ÁØœöT¨'ïP£îü £iN…êýŸÕ©u¥Ð„YB¯R°´¡ø€oßbò4¸ °ê3€7ÝõèÉ1‚GMÈÂ2Ìr$üµ K©?:KÚUÑ íhgå\Vÿ™ÊÊ 9ûsÌrTË£MÃ%†+WÄ0÷d`g‡œ8!…}âTi–ꇫ˜áQsÇó:k˜?©ïÔ‚kîXG|ð/eTÃúÓ·T+"™|²±¦Ô©êÅ£2OõÉvÕ©,VäÈêÍq×a/§32 Ò26WŽËŠpyÓJ¦R³ó ™QzjöZŒrÕe–ÅøÌiÕ¹b¬ïÄêƒòÎ+~Íz˜2â”{2Y9ºX#• ³@D+Úך¬¯jÔþáÑŸ.ŒÔc·o”o+~¦O×½ŸÕQª^ð_Õ MëÛñ|´£oø ˜ø¶ô…»Å¸BŽEv(ò5J_w±‚x‰YõeL†šãéFésB†oßPí Wg“ÞA´3$fՓ樗ÿªn}»f°¥êYŸ?ÿÇ ‚xFÚ1OQ‡Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³ˆ7Ò÷=wbOÓWq%¼Ê¼þ½Ú]íqÜžJOÙ=ݦQÉѸͿO2{bbÚñÝ¿~ƒg‚L&lKúg†]Cª¼Þ+.~×éY‡hS^ÿ®ïÒ3—ã®ßI:úž³V.]!úkâÖñƯ|¶ha›©ÛæÏáÚ”˜[S¿ý¸—Áó•ù©sƒ²œú÷Õ5q¶üßy½v¯îBh^{‹Y—  {L{ °xT4N秪á:wrlqZ Ôè¼À5ßNùâLésN.Ój^/Œ:áû¡}üß?PJ¦Y~UØŠk|ôŸŸ¯Ö<_•·éÜxþ¼I·ÁþNz¯ù‚}uBóÚYÌâ¸ÙÜØ»?Çcp€y]Ù¸~+Ãÿœêè»ôtlüÕKg–ùr�ŒøùÜO¡fƒ¿»5.îŸw»p��„Áëâë9ú»ã± ·’¯¬À�ÚqÚÎØ¸¸ˆ£û¿jñØþZ}±óô…Øøs¿Îê*x¼ÀüËÍ­ãQsyQæ}l9yñâù¨óþ;±‹°¹]¢¬¦üu}ÏŒ†v#Ï狨è5þB�Ê"`É_g£.DG_8òÝX§æï]ïÛÈlÒΆŸ¹vÁŸï<¶gÅP‡æ–ãv[~îð.m”íÌ}Ñ_öâ¯ÃðoöŸ¾u)òï/[ÕmAYø/Ùq"â|XÄùc~ԻœÀyÔŠ=ç/^¾µõØNW¡í‡³ÿltü•“?w®Û±&ò¢¬gì9÷ßwßýaÛ΃áç¯ µ£�ž×Ü?Ãâ.†ß¶dÊÒ}‘_úp�Ç}{0*úÂ…óg¶-èoQWXŽ]è×Gc.FœØûó wÇöºÏ×—ïìŸc÷'8ÏïëØƒ_Íûbý–]ÃOý6ÛC§¾bž>^Íœ€Y¡ëÔ_Žœ‹9úõ«r•i¸N9ubÓ§ãº[ð[Þ”ãúñ±ˆÏºs�h—×ýÌóûúÒ_ŸLÿü×{Fžßþ^w½ºå4 z¼ûûñ³çÂ#Âv~Ôp<}z7·³¯ZûŠY—Ávw"vˆ(ð X´Ô±+C>;'¹þ}¨_Ï^ý†®MP�®9±hì÷ ’sËzöêÛwâŸé��u~^¹Ç¾òYH¯n]û,Q�s÷L¿¾ý>>ñÄê^œN~vq‹G œ|Àà½åcm›«Š&óBFÁ_¬ ¼¿ft@PÐÔœykÞiî�²%gOÜìX—>ï­@ˆ8vE¼žï¯ Ê^5<`@ð‚«îKç´±[CÙ½½z¥GÂÂAƒ‚ç±ÿôë ÖMï&?+ßÂÁ†$001PÀqè`Ÿ}ŸÕúäs¸ ]ôÙ;=¸�ÀqŸöIŸÄ%CƒB‡vÉ¢o÷æ_8]fÿ¸ÐäÈì >}çEØ/];£cÝùÎõìcvâ½&x,˜Õ“€šÌ‹e°…¿wå†y3ÇYu§×ûo»q�hû‰ËÞ¡w¾=(xìŠ{>!]Ã��Çí˜3$ `ħ·{»,ÈèöŸÿy·ø!#fýªtóz,j+kËJ+%Ï5Í5ÃPî>ú§—Í1aê6˜»|œM¯fÎC@:=zP[¦ éò]î &»½€‹×\ûý½Q#©ñY²ïì?ÿ7ØE¿ƒ,C{÷³<»|öä±3÷ðæ¼;È�P¶c>S¼vtàààw"¿žvt3§ws;ûªµ«˜Åéd—t6ª0#*ªØkð�Ó¶vúYq­L_±a_Š˜`Ù{‚¸2öäÅrT9á‘Ù]º»·©“ÎëгääÞ„J˜Òó‡bŒúõ³næ ‘'œ‚ƒì(�AÏaaÇU�ª+«‡ŒZsU  H»‘–Fm:Ƞ瀮)‡ö¥H1¨rIrÐS¿ÉúÂ5¹¹rk{Sž÷Çû#.ó:Ú£û9•Lí™…ƒ&ýž®,Nº™e`iÁ�,—ª,{ ô²ÒU§îþâLj²æ*‘¶÷°ºzäd¾ @qwǼAÓwd×­ ©¹{êŸk•,HoßÈš[è ÀMæ�êgÂòÕ�Š{iùÆ–f�Ͻ[ÇŒÈ …М9x©�ж}û[\9p8G O=x"·§¿78NÝ=äW"îÈ�”9aç’_Bê?Ž |oñó 0Åq‘Ér�\y5.«s77^[—&ñØþ)ÆÕ‰×sL¬Ì_ÔeÆÖfFþ±tRðÄŸRÝÿ³fH³­û&aÐä]8}KŒ-͸+157¢�@)“‰¼ƒƒ}ìE8ÿø7+ŽÝgÚtz¿z¯»oÚ§SPÝý‘¥¬¦ì|ÔƒéAþ¦û–µ©ã1#˼›Ï´¾%�SU^Á�`q­˜ÖÕã#P>sF|=—wþŠ˜Ì�P|AõA# òšÌWGŸ¸¼tNýÎ½Ž¡þU§f§ª�u›ôéüa]ô±šÕ·Ó©Hn[€F"¾ï’#Ñ��ÍÇ÷oRPÝD ØûYVÎ] ÜSßpêÝÅI×&?+W´EŸYËþ3°ƒ@£AFŽÜ{Ç�˜Ìí‹>Çó¦wxIù•=ÿ]±)öAÓÕ‰ $U 3¹øaÈÀR‰ �05Pˆh:/�ÐÈdj��̲ €tôt¸VŒ�ØÊ²J¬�ÈÀØ@/èës¿��Ž€N4"J¤§+)³��¬D,yqC€lmMMݹ!“ËëÏ Ã¶/D¬�À £Aϱ|%e?uûþܹ�ê´ß&ÏÙ‘óðÐF®ƒ§¼3g¼Çƒ32Ú:3.VÈ帮\˜E�ûàðgŸߟùÅÞÏmä·®[ùCXžª-§÷+׎b§SP 3Ïqñé„E�@ó0ØßôС¶-�†Ñ<Û'(=== €¤'ÒÓHÅJ c éº›"/háTSTUÖ¤lž<u{þ3ÜȱøÒ‰¸/?ì–ß±_áÉÉY �p½ÿóý½M&)Tóƒ~º4§¥ÏcLÑu·9ÄÔ ×TV+âöŽ˜wTÜZölUnÝÿ­AÎ1ŸÅô[íï«áå]zÀ ý?BºlÒ˜è ÖtÂŽˆõ›+ó¢~ÿ,êwJ×1hÙæï>I Z­h²TÕ•5"gÃú&×ÈÞŽ_‘S"m²ÍåÕDª ¹ôDzä‘©1R��[UQYöMÐòKªFÛr¤R¹H_D0@™š¼¸NÕ,Ò×i¤b%nËñzØÂC †Ÿå �¬–VÕÅ uÏQ³Þhz÷äÎÕc—Ý.o>baŒÑÃ3FØÊè—¦ôêÎWw®X÷ý`ÃOŸ Oxï°¸-§÷«Ö~ú†ç  Ûk«ƒ½½{x{÷ð~kêÖï!ýŒ�`¥Rmfo'�àèˆúpJ…š²¶·æ≠ôšnmå7ÄKÇ.0À)åz² €­(«±vràp¬‚½µ†ŸÊKuÙzĤnú@è<~Å·“:·þ¥WN^0ðÁŸÌau­@$2Ы;û@ <ûa¡Ýø<¯QˆdÌáqëÿÀŠË+°C[€ç80À…F�‹¯FÞî:fBg!�%òœ½zÅ0›æ¦æ~N¡ãÐAê«—K®Å«†:eÝgO_$(˼WÅ€Ðuøà.4ŸÏG€ }±yq}¬´0å^ƒ›½0y—.–ô;ÜŽ pžºîàš`³fJÐt^MS¤§äuØß’®MÈ¿ºqb¶0æbiŸñÃm¹�¿¿_Ôß&ëvŠnß!Þ"„Dž£wjÜ{áöøôTô“šãk eé7´»×>0À)ýFŠª…ãõÿž‡McÕåeåeeååÕr�€î0îÓ¹òvügè¨뎶°�å��7IDAT˜ÊòjGH×=ÈÏ–j¡¥'è1ËWÁæ4`EIZz±œeÙNï—³³mÓnb§cPååCgJê»:ùðáïàþF@sãàîòÐW“nF_;ܶîÔÄâ˜=‡èÙÇnÞºzaÿ2ÿæFɨÑñWã.­aæ5ÿÐ司‹k�4ͤ\¯õëѳ{'ÖlüþH  NÜû{jßý³kÓ“ÔË•—ÓL^¸öüwË#œV¿pþ™uCb²Z”'œŒ²Ð#ãÄÙúT%ìÝZ¼ñØÞ¿6Î@ÿlŒ0ž²nqŸ†ax&çF‚pÆžËWãb~ªd—vl­ûû‘}Û7Ì`®_g9ÀújU²ßú°¨È¨Ã_w/:w¥¨¹{"ûà~‰ìÊ• ¦2þºÂÖ$?»„ŵѻö i»ïÙñßáÅÛ~KèøÁ¦w<9µñ'®Ú->pþ|XDئ)«7Ä5ÙÈ�Фn]òså¸íçc.…ÿ>({õ²½÷›é<4WÓg<“¹÷Ç:‹:¹÷3û[Qyu;ÅÜݹx}Õøç.Dýçc»äˆÄ* ‰Z¿6É÷—óq;¦U]ŒS êQØâé›ê<ç Œ&3Q<lãѳç÷N”l^{¤mþx=ãyøcrþ^ðÎêݱùòÖ»¸,lë~Ýwÿ9ü×¶ïC*¯$anóõ ¸s&Œ?swXĹðsO¬ÞðÓé*Üìéýªv¶EdMV¢ýâx,>º/ùSÊ«|BÅë½âüââYþÌnã7ÄãÚM;‹ êÐ.svŸXhDÐe`_Ãô;¹d]n¢‘v4O��Læ‰?/þ÷«“Q_2ªâ k–DI^w‰ˆv…ô ‚Ð&¤oH„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mBbAÚ„Ä,‚ ´ ‰YAh³‚Ð&$f¡MHÌ"B›˜E„6!1‹ mò?jÊôoú`8Œ����IEND®B`‚���������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/img/gha_annotations_1.png�������������������������������������������������0000664�0001750�0001750�00000152746�14645165123�022032� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR��ß��>���r’—µ���sBITÛáOà�� �IDATxœìÝg|W¾?þį̈ږ,¹÷Þ+˜ÞmãFojB =$[îîÞ»wïÝýíÝÝì{Ú’„С÷b0c÷Þ{“dKV×Ìü¸Ðl°0|ß/ifÎ9šÑgŽÎ4B"w†‡ñ ÊÏÍ~èd!„òIW�!„Ð0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹0Bˆ‹xcY˜§·¯ÔVÊãñDzP„ž ‹Å¬îQ7Ö×>銠§Ï¥³µµ¿?˰ƒ¶hǦP„ž8ŠGI$’ð¨èºêj­¶÷IW=MÆhdÃÇßßb¶èt:ÚBM‰qm¡u:Ålññ÷ÒuAO™±HgOo_–aFã”…F–a=½}ŸtEÐÓd,ÒYj+5š0šÑsÍh2Jm¥Oºèi2éÌãñq@=çh ÇÃѨàu!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄE˜Î!ÄEcz÷ý‘ãÙñlâd¢�‘ÀC�¦&£¡ÊЛÚmQZžtÕBh,p±ï,[áàö;oi¬m_4€ÀC(µuû·l…ÃW&!1!=㺻‡�¼ñæ»öìã „žOœë;»üÊCà.î]i¬­(@Ôö禱¬Ò êš;;»¾¿Å"Ñ™sg’LxsT.ñôö™8i²ÔVÞ«Vgd\miݦâáá9~Òd¹Ùd.-)ÌËÉé{ÇçÇÆÅ‹ÄV'Ž~ µFhÜJgÙ ‡Ds‡P¶Â¡ûPרTéNçÏž;ö\ßß“&Oæññ†\Cxxx^¿–ÖÙÑáåí;7!e×7_²,;òùm¤ÒìímíVVV‹–­hommmi±‘H“ç©TݯÞÝC#<;ž4Öv$SJcmyvÚ¯ØÚÚþñƒ?]ºœzä葤äämÛ·½öúë�àíížq=!1¡2™mzÆõeË—�I’o½ýöž}{/]N=øý¡åË—ß¿ØÿøÅ/Ž; �qqq¿ûýï� õJjzÆõV¾žq}Úôi}“qäè‘?ýùƒQ4=lFúÕö¶6†aêëjx|ž••�Ì[¸8,"ò…×lÜ´%vn‚³‹ëòU«×¿²9!yIRwÎ_VRÜÚÒÂ0to¯F¡è²±‘�€Åbι™UQ^rç”áQ/®]¿îåMKW¬’Jñ¶úèÑãPßÙ&Nö€w»;ÌM&—_z Nü€îóOþ³©S§þûÓ«TÊÕ/­ ÊÉÉ}pé Ãôú_ïlmk‰‹‹ûéÏVXTTYQ1äÄù‡¿?üâêÕ[^}Õl¡›׬Y“’2/ãz�DEG9:9ýýoHƒÑãdïà¨×ët:]߃‚>DÄ’+år»Ó'Ž™ÍæEK–ùøùÕTUÞ3/I’^^vvvךš�À 74Ô×¹yx N Š&Lž|`÷.ƒÑhgg§éÕŒY»ÐóƒCé,  ÷–bw‡6Sc=Ur{â€a'–J¥sçÎÝùÍÎï�€ò²ò}ö¤;vìèû£®¶nåªUAAAÃ¥³B©èêR�@UUM߸ó‰ã'ÖmXoee¥Óé»UÝéׯ¤Pô8PoƬٙééƒÃ•å}OëèhS)•z�Z[[îïöΊ  ¶X,™éWuº¡Ÿo2ÍfKPhXIq¡R©xœMAÏ/lÜ9â¬+Кšû¶ F³ýZ§Û{ ;<íææFDqIQßõýCK‹Å[ßÛzà»ï.¦^:zì(�ˆDÃî�îwüøqGQd\|ܹóçh žü÷d171©¹©©®¶zðEý@'šeY£¡Óbh† ïý \½œúõÛŽ=6d,Ã?rH&“¿øÒúèñ�ˆÇÐô¼ãPßùNÊÝ�à´ÕMs¹çþh~0‚ �€en ¢ÍC?ÕP$þ½õ½÷“ÿëWÿ•w+Ç£Î_¼0ª wvvf\ÏHINîìè”Ëå§NžÕìèQ!H"6>±··7;+óÎ×GshX–íVªjª«]ÝÜËJK†œ¦W£I»|ÑÚÆ&eÁ¢n•²¾®öÇT¡ûq¨ï<ØY�§wÝ� íƒ¦á¢ùΉïÑÒÚ�ÁAÁ}ÿutr²‘Øôý­ÓëÀƦÿ¿~~·Ÿ`™››uã†Édtswhmiš�Šºý;vdÜø˜ùóçUUU 7$‚3bæì8†¡oÞÈäñù<>Ÿ¼¯kü`>¾~¡��¤R©_@@G{û“ñù;{�ÐëtÚÞ^ŠÇÑ^zªqh«2T7B§wÝ:>jGYÙk6T†[NOwÏÕ«W_Z»FÕ­R*”/¼¸Ò20ÈÐÕÙÙ¥èZ¶lYiI™•µøÍ·Þœ«¹¹%0ÐßÃÃÏçÿô§?eæÁµmkk€ 7äçWUUuvvffd(º:?þèãѶ=2;YPp�öï›s²o ž³<Þ¾¾3gÇR<¾Ñ ¯ª¬())r2’$g͉µ‘Hišn¬¯«­®úñ•GèJçÞÔî;Ϩx=þìû€‰°¨?ýáÿñË_¼õöÛ:­ö£? í{eÙßýæ·ïýä½O?û´¥¥eïî=›·lî{ëã>üïßüf×î]] Å—Ÿqg§xH×®];sæôòåË—,Yò“÷ÒÙÙIÓÌ•+W–¯XqîìÙ‘¶=RÝJÕ—Û>½ÿõÓ'Ž þ}ùŽ1«¬Ì{Ü^¹tq¸…·45 ^Ûb4Ž~ðGÕ¡‡!$rç‡N䔟›ýƒËˆŽ™ØÓ=¢3ùe+FrʳúrϨ®F¹|åòž={·oÛ6òY~�Š¢vîú¶¡¡á?ù«ÇZzJÙÊd?æ{„ž7ê;@÷¡.Q èÁ— ššOäBÁ°µµ NJLôôôúÃïÿ¤«ƒzp+ íƒ¦ô GÛkž^^ýÛ_»º:ÿûÿWRRú¤«ƒzp. ûPWoj·Mœl°mj6*øDcçÄ>â*Þ­¨°pάٵ„Ðó†‹é �¥…ƒ}d„3:ß!„Ð Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„⢱Hg‹ÅLñ¨‡O‡Ð³‹âQ‹ùI×=MÆ"Õ=j¡à!OÚFèÙ&Õ=ê'] ô4‹tn¬¯%HB(Ä€FÏ)¡PHDc=>?Â;×UW÷=¾‡8Ðs…âQVVV<>¯®ºúáS#t‡1ºÏ†VÛ[\ïéí+µ•òxü±)¡'Îb1«{ÔØkF?À˜Þ ·Q„!<£!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸Ó!„¸hDélÐë¬mlwUB Q:«Õj{ÇÇ]„BƒF”εZ,;99?îÚ „êC Å#²ÐéuŽŽNÖ66‹Ål2=îj!„ÐsŽÈGÑ#–H¥R©T$¶z|B!£Mg„BcϨC!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.ÂtF!.âjj™\n#‘…¢ÇT„B}FšÎ|ÀÙÅÅl2uuvôúÇZ'„B#Mgg—^µ¦»[õXkƒB¨ÏˆÆer¹ÙdÂhF¡13¢t¶‘HzzzwUB Q: …"kF¡±„gÔ!„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ñ¯ \p—†zìÆ2h(ïä”P>0 Ãòƒ’’ýÇ.ž)ß-ß—–¤þW ¤sXÏýÿ®W”|±ÒŽ\†Kp¨ƒ•{D =¦3Bè±à¡|“Cù¬.ãLš†á&'ŒU<S~ó燎8˜�$3ÄËïþlLWÿ¸nÓ»ï¬ßüY±å‘Ö!„†0véLù$%‡ðYsÞ…ONÞгüÀäÄ@Þ@-\Ö~[Z^vþçSBý÷Ž“7 Kr/ìû`m¤´¯ëÊŸú»´ŠÊ¼Ï–L{ío/g—”ÞÊ<ùïŸÏu¿¹b¿”Ÿþó`jfQI~îµ£_þvíD �€t˜úÖ'Ûß åå¹ioiUyUñ‡ Ä� òMzóŸï?s%£ ¤0?ãÔ®^ždO€(`Éïwþß[³þ”UVUU|êýPð§ülÇŽ>ÞùϵÞý{Ê~âÚßu<ýVAqAÆÅýßšâ'î{‡¼õxqUéÞW#¢×þñÛ Ùù%…ç¿ùÍ’�Qßû„,zÝï¿:•žS\Zp+ã‘/~»išöÈBwà=|’GƒòIJâ³–œ´«Õé–[Æ„™IÉŸ••Þîˆò¼W~ø­Ä¢³|©çÄþ{ÑœüËËš¾· áŒ_};C&# F–9Ç¿þ/»žÅk>¯¡ç½æã=¿#'YÆl2[9ÏY÷›©ÓýÞXùWu>Sg{‹�X“²¡¦]ÏššÕ �Ð’q‹WÄúКö¦ƒ“¯ßÔ~én^´q¯9jF”œ$�¦·µª©›¦: ì½í!¤Ó~µû‹õB‚¥M&Fæ5aÁÖñÓ'ü¿5›wÕ˜ûÛùÚ—_ÊíD&=C‰ì|g¬ýàSCý‚¿äYl“»íÊ CgmU›ÈÝ+lvJý¡ÜWBèy6V6Ê7)9˜–Ê´´K×Õ‹ùf–矜x×Þ´•´|½nZLtÌüß]Õ°@ÚOŸv»{LŠeôÕÿž7!züô—÷ÔZ€…ÍšfO�ˆglyw–œ¤;Îþç¼ñã&-û{¦„¾/þbcÏœýÏuïï©¥˜ö#¿Z¶hÉ¢¾j�sÑÎÿùÉæùS&Æ&§$.ýs¶‰%¬&$̰g[¿ÿÅÊ®Y�ÀrëÃuK-ZþöîZúžöž/¼ÿ¢¿ŒEÛ×NÿÎÁz !Ÿöþ; Ò¡j‚o'(ùÛÒiQQS–T`b ¾çŒY~P®Á˜®Sÿ³bñ‚Ù§Ì]ýÚ_/ô`:#„î0FéLù%%óÁÒx5­–¦=-µÄÌòü’“‚ïŠgKîÁ]7U4˜S¯”›H‰TBÜ~›é¼°ëHe{n^Ìêf�‰DB/`ò9 Lçù]Gë ,£)Ú³ÿº%øþÓ&;ß@¦íæé+õ&±­ƒÌÔÜÐÍAØHmFö‰ØŒ›& Àrë»]Ù*L­¿=YK)<#j°I¬.}ßîb úò‹é ô@{,M…EJHÇeÿ<³ÿƒ·R¼µæÑ~¤¡gÛØŒlP~IÉ|�ð{ýpÙë·_÷MJ ù¤¸h`pƒ5¶µ(��Öl2³�@w„3Ðí-mýXËÀû�@Hm%$�£RªúûŸF…R˜”ØJIh²JBï”÷õÎÒé"j°Œ{{ÈÃ"%¶R,­Rv3��À(J€È¤ƒ;EK›±¯e‹‰í¯.@oêÿ½þ¿ðû÷‡;Ç,ûéø%›KüúÍߟnÆ£¡AcÒw¦ü“’‚†:g‚ç“”rÇ‚¦œÌï³ên5@Êíåý9+²·—�Lª?;𣏕߆¿ü}s|°\•¾ý·¾¶åµ.)†žr˜Š¨»ÕJnßjiç`G�°jeÏí:]_�VS²÷—/Ä%oúßYíB¶ê·ïÍ¢|„Ð3o,Ò™òOJ âÓsò'3&ÆÄôý›ñö÷] 𼓒Gw®Û,åé4Ž ë–x R¾úÅi"‚5×ÜÈéd;⤭——- ”µÔ†Oùûó  «ýåûN]J/Ó‹¬î쥛Í&€töñl¥"âž2µ9×òŒ,ðÆ¿°n¢œ¾küúù¾<`4Ù™EëváS£Ý¬]ÃõÝÿïÍ?^Ô±@Ú8:Zß[Bèy6#T@rR�X]ö•kuÿàƒ&ýÒÞe ¤^‰óBÿUÜñ£J0Ýøâ¯çÿ’âœü§3·~k"„B>¦úÃßUn�º¥¨DÉD9KçþéJÞo*þKÒËWÊ«Ml´ pÝ_þíSIøOŸä ÌíÃr–ŠÂRã¢I¢ ×ößXo¡ôG_Ÿõ묻ŠdZýíË;ߊŒØ²;cƒ‘á Á¨3?ý÷…ö!Ÿ*å’ô¿{ÞòÖ6×6(,r¿+‚5”¤çtãaA„Ðm¿ïL&&ñ5å§Ý¸ó¼mÖÕl= <Ϥyá?¶÷Ì´ûÙÚ7ÿq$«Ne¡(³¢&óà_^]û»Kʾò éÿøåÇ—ªš"M%E :Kí®ßüïœfuàÌØ‰òêí›Wÿáš~°vLãþßüÏ¡Üf™ XuÝ­ò®û‡=ôy½²ö7_]*iÓ±$«m)<ÿùÏ×½¾£ÂôðÚvå¿RÔŽþáArcÝÍÃyóí¯ªG<èz¹óC'ò ª®¬ƒÚ „ꃨ!„a:#„a:#„a:#„a:#„a:#„a:#„a:#„=«éL:L}eS¢ÏSödY„0&w%lcÖmMìࣾ¹sûÙºÇ|Ý2Ó]‘vEÐŽWG#„žRc“ÎVÖÂÖ´m_ÝP°�À²ÌÜïÇ¢¬®~ü¥ „Ðc2Fél%Öµô2Ìý±LHcÖ¼áyë_GKŒ ]¼õ›´öfu³”wÂ)úï>O×úÍZ87ÌU&¦ m·NO­Ô²6ã^Zk{+]0-9JVuøÓŠð×ÂÚ¯ÓÁÓ‚ìÅ´ªôÂñÓùJšt˜¹ùësŸŸmÏÜ”$Ìktœå!¥t ¹§¾¿V§ Ä^3ç/˜æieé©Ë,4ÆxÖ}~¸Èdã7/q¼«55t•^Ø¡¼w,>„ºÏؤ³@ òŽ}ígñ|е•Ý8® }àNnloccÏ$Oª¤žô pîU‚8+O/÷r#š®(�UýÍïoÖw™%ã—¾?®°:½€Gdžežüü£6ÍFˆÃ¢]¾ÛÿÙÀ/aÃòiþ%'+îÒ \LJUïþòÓV‹|Ò‹kãcJ¾NWòüf/Ž1¥nÿ¸X#ô»rµL]@:‹ WŸûä`‘’8Ù2Ú±øpBh(crTÐRvæ“·}ô×ýsûÙzû9+â<nïe}#xxÉHÊÅÏ»ûÖVÇ�/!!v÷’6×¶Ò�´¢¡¦Ó@³tOM½ÂÊÆ¦ïõdwÁÅ›j“ÙB�ÓU’]¡¶�««¯jb$¶â{îco©Í¹Õj`Á¢ª¬VXË¥P®!¾–⛥Ý µu7 ›i��FÛÝÃwŒôRM»B‹7\F=1có\AÖ¬Ó�0ê†ë×k&Æ{ÉɦÎþ[&Ó­u-ÒñîVÕ¶>ÚúC¥Z¯é>"³—sg}£ €ï9}vŒ·LÀimOÖö/éQÞq¯hV«HR–f âž]kÔjûûÒ Ã�b+‘¶S×7«Óõÿ¥):ú­eZÜÜM³¡öú… ™MС'dÌϨ#(ÌfË©gjllwö ôbjj»uµÝþQž®šÆ-Γ—$;5žÚ»ãóo¿>x«óŽ@†Ñäæ}Ó²Ql3ÐÇ …½mSgÙ•_úuL\ë76û.„ºßX¤3!óô²Sð¤>³¦{+Ëjî|H h›{Ýbµ• †ÕÔÖý'òT "±ÈÔÝ¡2ðCýÙ“÷è–Š:QÄ„ )„ÐmB¸{ßÙ~VnöBÀÔÓÑ¥#ïíƒ#„ÐØ“Þ!)J\šbg̓²6÷ô÷™w?ÿšQÕ7òg†ÔW·3� ª©gãÆ·ÔµÓ�Д{µaá‚7^6ô-ùÕÕÛGU%SÕ•c æ½ñÎ<³º1§¾Ù"�Rä2yÕ‹¶$m2t”^<Uó°ç·"„Ðã‚O®�›è5o{åþíx¦1Bˆ+žÛïVNž®>@Š='G¹´7·áu…!y^{<ûÈs^pñ€Ñu”?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ÐØªbŸHBˆË(¡Øæ¡ÙÙÛ«”ŠGXêï^¹“öR"Ì“:ky„KF#By'¾óÞÊ„0¢"·A‹{Ç1A:L\»e:U^Úf��Â&`ÎÊuK&N‹–+ +ÔŽ“¬^³()v‚·¥¦¸I7¢uBˆ<g,~é¥ùÉ 3ü-6I/Na+JÛŒ#®/xÅÖu~­Ù•=¸ pÒè;ϧÆûQøÎ��¿^)šN¥Ó£_ŒÍ¸5?Iö£XF‘½ë³K f��B>iÃ;ñn­©Ûvd)™G\o ¼æ½¶nŠ”`Y†6öv5VÞ¼˜–ßfzÒ{îN36mr½ùñÁüÞ‘ÎÂhÛÊ -íz�€ò˜¾`‚¨èп³TL’ÈøDÅ©oöU[(KÏ·\B2iÕëÑu;vÜè¶Iгo<ôÕ®F ˜-NP¢í_:Q±[R ßíÈT9ÍÜ4Ïôý×YJ(¯ø7×Û¥ýí`apBÂ~æ†×\¨;gfÔ7wn?[G.}{Õ8Ñÿ*Ýpzû·7ÔœmòöÒù½…‚6s*‡€W˜­ iÅú¼4‚´—0¥à›k]? áŸN,­Q´kh±ÜÁ9pÂB¡þ““†‡Ï†Ê9*TR“^¡ÍLú¦›—šúÿæÛÊ­5µ¥Ý�(/;9´åwõš¶Â6,Òµ-ÿ´ŠJ&·55]®Pji�€Úô+£oÈØ99 »²{89‰ºr‡ÏT¶;÷èüÁÿ󼦯žK6wÐ@XY[S­iûNõý¶`Íšgû‡ßX§óü ”‹œüÏoûÓä_'LZ/š?ê ëQc4jµÄuf\dÁÁ¼ûV¸Ðm\ÜœH_W;©5eQwTe]>ŸÙ¤c ÇÙ7ÇŠrž3DÇMð“ñ ŠÊô³©-žñó&8 ÍʆœÓ§Òjt��”4`Vììq¾NžQÕ\qãÊÅìV —¨ø¤É¡25жª©çóv³7nŽ“•;Üè73Äž×ÛœwþÌ¥’n�¬½¦Îîãh'“¦žæ’¬ çóû~ƒŠ\£çÌ ñ°³âYz;n?žÕb!¬='%Κä"н­59/ݨÓt©ØžÂÓßœk¡E‹ÞZ-ñ ò¤**éa �|‡ðé±3B}œlø´^ÑîеÆ;?&Â&dÉšeÑ2Cå¹ûó¢a«*p?oÑô'¾­ìj¦v²iŽåÇÿz Ä rŽM˜ê)2ºÎÚ¢«g3*ºï±¢‚–ýripÃÅýEö3g‡zØ’½-eWO\Èk7¿¦€òN|kc´6íûLá¤Øq¶„®µäÚÉ«½a)ñ1þr¡«òÚÙ“7Z,�ðìÃgÎêíhM”E™.–tšÁ:tÞÆ¥ÁlቯOVÝѱ¤d!3“ã#½íE¤©·£êÆ©#·ÚÀ#ñíãá3ÚÉ)³Cäç¾ØsS' œ’íçlEè{ê¯9˜ÕÅ��å.¬:S­g€ç8.a~\¨›5­ªÍ¿\ ˆŸ¡?´ýZ;qïÒöÕF¼üšwþÇûóíf®Y6ÉÏ|í?¦Óu©ú€A¶jÙ¯¹Dysç'—š(§q‰ñÓ#Ü¥„¶µôÆù³·Zö¾¤CX„Ssvi¯ËŒ—Ïö É—~E·\ÙþeMø–u7·íÉÑZǬz3 òPƒGüô�G+FÓT’zìR‰’� ä! IñãÜm mS}aã�� �IDATaF9Üñ­ Q(é0íåµ6Wö4-ŽtÖeìØyµÊ#ñí$æB&oêœhw+FÝR”zîBB³òÍ9Ýß}t¾®oÍó|æo]f›º}ï--�åè*W¶vÑ@¹¸Ø©ÚøºÓºîNÝàÆiïmÌ?\¦ ­$6EKsGÇs2:Öéüj‚àV VL¿·P��ÿ:aºUCoš+8•óƒºÏLg^Zm̼(Ÿ9ñþåG«îéAšÍ"w‘¶©¦Ñ ö õ‰LZÝÛ–š��HiÌò¥–îö¥ÉÝÉ9,ù¥@3«ëlSh]]ýg.ž^÷Ñ…šï9wå Óì-í·Jtò ˆqóWZ›¿ú®@<uiÒgFYURe°vòv‘SÆþè$A Vx«Ú:» ž.Þ“—/êíØ•ÑÅ‚‘±ñpäõ4—×Ó¶>Þ“VX”ÛÎ4ÒòqË7$ù‰¢µÙdí`Ç7kh §®^ïªêâ…Ø':4~-ûùžÌλZGP|I�0´å5Z‡¥¬]j&UK‹^dçhÍôÞaxÌ^¶8ZF·f<”§ a¸ªZøÞñ«"ä„EÝ¡&½–ÙðIè‹;Â6rñúä@¡¦¡ðV;á9m…õí¶ËM÷}‰(ߨ}ŒÊ¦öN¾»³gä¼¥ÝÍŸgt2ÃV��H—YËT­j+O¹ÇøäM,­nëPÝœC“;ª¿¹ÖÉŠƒ“Ö¬ˆèZK²Ëiçàˆ) ^·«;8Ù àb/&îHgQpÂÒéýŸTjyRG{S7@¶á‹SzJo|ÿ•ª·§—rŸ¹je„:íÜîÃ= µkúÇø~á¡då±j�ð½ç¬\èÝrzÿ±J­ÈsBÒ‚([Í5t×ÒXñÀfYwõ›m=/½?©å«¯®´2�p¹1ñ­µâó>VFÖ!KWÌÜ<²íhá4añ’•󴟩б�@¹D…ÚÖdTh}úþm] ß_h>ôϳµ4�étççÌš“`J=óÍånÂ!fñ’É­õûŠ´,å:{éÒðžÔ½_©xNãâ­¾ªZ‡,¢P�Øø%.s¬½yjW»VÓ5ÐC äS"®ûî³Z»dñ2‹êë e%uI³C½Rëj,�@y¯TБ/þdAIÁlùe$E0¯ý2¡éâ§rüõ ÆMñUälo²��aemÍ÷Ÿ¿å'K`ìi.ÉJ½\Òù,êi:oJà»ÈÉ?|§€@·þ“ùv\0}ôšxSÇ…‡þ¬»Á³Ô¥¦×%ùFΙžS—Ú{W÷™é¼ñÍß2Y’ï©ß´nšÔÝÞ*m훕ÔÜv¾ŽqOxkÍT{ž¹ôÈG*LvS^~+ÖMâèlM4˜ü'ÅØSLKúÉÔB Äš•Ñ~QÖE ++‚�“²:÷RV›–åñ Dÿb7÷m¿ÚÊH'®ßœìçf#­‹±4]ü÷§XB¼àÝÃ¥nR²Y0>ÆG ¦šK;våv3@ñù`f)Ÿq“Üù¬"ûì©lCT˜dkf¹D„Ùg]Qöa9o£/Hìíl@+ÊJšèa[Ö6%Ȇ`Ú®ìýær»>Ÿ23Ð7¨Ç2„í¸…óg»RªâÃû®5ö…áÐUmìñ •‘Ð[rôßÇËô<Ä—7̰ï«ãø‰VloþåS—[h(VK×Îõ w¿ÚTo‰ ÙÆS;÷ÞT®³6o™îàèê‡Nãpkª¥&P\ÛùmZ;´ôõ•ã¬(õͽÛ.5ô­8;;'t‰Â§†J sÕ•³—+ÀïnJ  ÷9]]–yt™m©¹ë8A0£A¯£õõ=탯óE-é_¥VX�à&“צîM¯Ö²�]] ý#ÌŇ,�À÷‰ ³®ºt.·EÇ_Iõ Zã;äÒ€ÃH²Ð)Áš¬ÏrºY€úëéW†ù *Š�”[x¸¸úlÕCG—Y¦!ól~£�47j''¹:EZpˆ’·f|ŸÕÐÃÔ¦_¹±!æ…–¤UOöžs·4w•I=yWÒ«º�uÖ…Œ×fFy¤žª.®ž›æq¾¦Î<A]Z•ΠÝÿBžßÂwÆÕö}¡%`ÉÖÈêO��(Û·Cè?-ZRy©@É�ÐÍé¾/­Ú�VNþSç­µ±|~¸âÙÝ»t¶«fðnÕй5wõÈ­anÕЫfð¤[zG}PƒÏ§zr/fŒÛë411&ÿ{Ëy@ÚNKŒ‹òq–)‚��`(Þ`ƒYKS}“€èVt3`mõ [©´°n<Šâiåà Ò}Ϋo̘±²¶b ²Û"âÜS6¼3­±(ýÚ•ìÆƒC¬¡µ¹ƒ€ÞÆëç(³“ÐEهƥL ÷²·ôW…¦( (;LWuš� Íf�Bêä`Mé0ñ¥­kkmcE@:SWÆ¢W·T§_Ȭ7ßXÒÎÑŽ"Øž†ê. ��k6ßîÐÖa‰)2B_tþBÙÀ¸Ð0U%lìd"èÖºz�XZëZ Óí…��<';HIôâ7£ë 6ÖÜQÔÕj�P*• 8<Šz蚦·©®‹`” bKsCóíG|H{GŠ x ^ œ¶² î®/é¾·úòËçü—/ÙüÖ”ÒœÌìܲNCëÙ–Ú†þ¿ {;ªú¾mVþº’­4�ÖövUuÇÀì´¢CɦóK1ÒÞÉžï<{Ë»ÓûþOðD¼6kF–çJU¯yxw‘Uw)ÎÝ Mf I’�À—ÙÙèÛ[º0Œª£“f\(�0µu½÷5‚Ut©ú¿Ç¬®­µWèl+‚ºêšäù!^üºÆ58PP—ZÓ¤ƒ›³¶í† HgWGu뵑ux Yø”Psá®Á½­iªÕ��€¢«£¶{{E„¯°¢è™=è2vé¼jO"&ÿð]ÿˆReËíŒþð„é«­âU3x?¤û @·g¦æF¯šè95>4çöR%á)/ÌäëjÒŽ_¯ê±¿pñ„»öÔ&ƒ©ïØË��Xúc‹½½!Àë®Ímô5({X‹1mïö†¨i³&EûyŽŸ÷¢—í¾ÏÏ7÷M@AÜžX� í'/›?Åè.M;x£Që4yåüQß40D„õ½D·dOkü<ÌÝ;Fy}÷¶s-wõKÐXú«qR*“͇̞ì^uµÙü ªÞ‹a—I� À(òΧ– öcXuóC‹&cß7óŽù¡k ƒ«Š�‹e`Mݱ›'�FSvábá`'™Õ·{fš¥#çØöÇÀ˜ Sço˜2!u×ÞÜN��–6þxï[ý÷Îiî£*½ÜÎô7¦ïìzܱ´‘#€°Ô_øâlåíïmèeþQ–’##ùŽÐÌp»w|ð,;¸‡+Ô€ø¼ï«æí¿ ¢ïC0V—T¼0o^½)0PT¹ROyÅ¿¾~¼”$)‚}õ×Ó�HŠ`·üzº!ïðGÇkØžûä n­9'î ��‹R©±�ÓùÇq•«fðNçX¯=ù׉ÛûÏŠætŽåž FÃXíbyØ ¡³bôW?R2G>Áê*3®–ÖÑü€‰‚û‚ð!%1ª®.ë(zëÊûr’Z ,Fà …Œº.÷l]~fÌÒÍ‹d¾ž2²/ §—+¯º‘‘zùÈ`z=,!wr VQ|5»¼•±–ñHÓªn 2‡�Û ¥ŠÊZ.¡U½] ëem#6×W–÷ ÇóÅbV@ YÏ5–éQª°–øø;Pí4";¥RõmÏŒºìè—×­—®Kò™²hnÃWgëÃV•íUõY;{z + ¤ÌÅq µé®Žn6ÌÁÚšm)¯ìë‚SVbJÏ!tðsÐ7U6iÂbˆÏ|kê¡«JÙÙI³>b ©¨*ë;{‡[Qz �_æè̶V׫îý’3úÎòô3…m/½=)Ê%ïbÛ=o÷*•¹«“ˆè¼ò„mh´{GÁyE_겕Ò,sv@§�€”ÙKI¸gA£Ã*»”Äx}nýÝûGêK¿mùáGÄÌ=*ØÉÉš¨W³�@ØÈåÑö B‡¿”˜°w’SÐF�aãênehï6°�憢rfI¨·»ÎWXs½Ú�tÃ¥OÿpÅ+åÕÙÊ}»³´^ó^©Ø·'«ÿ—Ú°4�XM',?VÜ=̺ç;8HÍ=ªQ6ó”£t~5A “_^¸½—ûz«ˆxåÃþW¾¼`š7ÁêÕÁÿ}7ò³éï -½z­Î?É×Ê úC3=ªnš•YùOO˜æÁ󎉴º«¿6†Ê7;g:N^³Á¡¼¥—´rðò0¦}±'Ï)ñínõ-JÏÅE¬¾³KÀ��H»‰«ßòì°HÝù`j,,Q2,©T1¬‹<lÎLS›MÈ$?>��À´µMšéæ3wÓ[azYðÍŽKõ¹ a‰ÞaK¶ØTÖ÷0"¹»¿`ÇÎkÃÖtØÆ²Ýeù 3=|ç¬y3¨]CÚ:Iê¾ÿû麾ÙôJ…¦«ìDZЖŸÉ)‰Õ;OT÷ SU JËÕáã¥K·ÈšÔB'{’èoEG B^ÙäSÝiÊ\¼dõ‡>¹Ðì>kõKléš“ÿø.oøÕúÖ«.º^<Ù+Ê-=jÚ ¤ÄÙ˵ëü‡‡*eS—¾4ך.oÛqãöгÈ="D hèPjY‰‡³4•C ^šk³ 5ëgÍŸ¢½R¦b¬í©ÎJmh¤së­Cƒ—oXj Ê «§ÏT\®ìºEÍ'‡~z(��£,¹Y1yÁ”®“ÕJ3ßÖÅÇ^_œßH†Døô”^iÿ§ñÓ­E=fÆMhK-UQöa³c\€m{P¡·W!Y²1Ù:sçž<��ØFÏ™^{áV;m™0Ý­§à\ßo%Kca¹yYÌ4£¸6­f`ŒHìàHt•iYÂÚÉ :Kîþ¬ ¾ÄÑѱ¿$Ö¤VôX�Â1f’¿¶àÛÊÛU #f…°µíJµEì:+9@“½·öY>}c,ÒÙUNÌ›À;pÍ|O¿øÎR«Š=pͼj&ÿË ¦Ô}fT·ÎgušÕ¿XV]xþŒûâ¸`ïÉSìšJ.琢x1Înt µ4¥Ø«›53Æß3"‚bŒ=-ÕE­&`5MµÝÞAAQ~$cP·\¾t¶j`#bT×K¬Âb|…fUÝ­³§³,@çÍÓW]M š4Í¡)ïä÷Õ³–õ ðÑm™ßí‡ÄØh?WW™QU›W¯aîÊÚ»Ï7sB¨GH´'˜z;jòšxìcøÆ²Ý9Ç÷Qqq“Ü]ÜmLêæÂ:åÝKb·N] |u¾OÔ¢„šÏO WU0Ö^üî²hÑÔ@g‡Þ¢ôL]Â,X€Uý†UÆM ó ˆtK¯²¡°¾‡FÝÞ¦1 ´mŠŽ3>Š5úÊó»÷kbg†ùú…:SŒ^Õ\^­`€Õw¶udlk×ÝÃÇ<û¨¹‰óeb£ïj,<zöV7{GÎÔ¶ÿ04gÁ+‰V„AQ~îH·_¨¼.«âŽ#d¦šËÏ$ÌK\»u)ÓS_x=§Á7l”U¿Û[tô�;uņdkÒ¬éªËºTHH#¢Ü; Îwý¨k¬,-W$Ůݒ�Ú–Âk—2d‰âá ½gn’ ‰ï,k®È(¶šõÂ6 i)<zøÊÀøÝ\ZF¯›f[v¨zà;AÚ;Ù©Ú»h œí•wº^q¯nЍaÍñÏöähAè3e’]ÓÕíwLË|¯™ó§Él„„©§½úæá}mÏôU„DîüЉüƒª++~pÿ½R8+œZñgýƒúÙˆ‰C¿_-¦`÷ùI#goÜ碌¹ÿã“÷¬ðl¸=K¹Î}ù•YöÚg¸±?ˆu̪w&7ïØžÞù ^ª:€òH|w•ô܇‡J†ì¸òƒ—¿5Ÿ=õÉáÊgùl·±0}g9¡Ñ±/Îà?tJŽu‘u¨ÿ)ò”WxTÐâ­säímJÁÂs·'heqáÐÇþžW<{';Жì6O7‚fKzùš+Öa4ÿhc‘Ξ0ýi½pSÂÃÓ¹MÅ|x×*7±=-:wŸ�W+>AzZK¯^MKox–‡ýF‚tŠ˜H×–7+u¬Ï”„ñ¢†sµÏC:ßä …ïØYÁ½»jÈÙWènc‘Î-ÌŠ?ÿ¸C%O¶3íë?¥=éZ<>L{öÁÝÙOº\Úh«€éËgɬ`ÖtÕ¤9{ë¾/ÏVQKÞZàixøÆ³=<VÆbÜ!„Ðhá³QBˆ‹0Bˆ‹0Bˆ‹09‘KÔŒqÞ6crÚäX–…¸èéNgÒ%þÕÿúï—&ÙÞ±âq«_ˆ¹÷a‰„ÝŒ—–„Žô ë˜U¿xe¢l`©”oÒ»›§Úô³]Y?å÷úÏÞùÙ¯¶iÖˆkÇ)¼Ð~µjœÕ˜—+[ù«åÂÑÏhh¯nO\4Ëgd·ýQƲ¬Çbì¾Ϩ§ú£#ƒCd,) ’dß8‰´wt ï{D-ÏÁIN*Gº`}ÙÅo ÷ß6qdFWÖA×§nû[š×¼Í cQ�V[Ÿq¨žóeY»…¸k«Æ'yrߨ}žQOs:“NÁ¶M99VãBým²oiX�Ê7åí%ãå”å­wæ0¬®àøçhB±|Ó¼0^ð?Y�Œ2{ï×™ ðÿU·¿ÔcÞÜ@;+¶lÿ§Ç+-¤ý„Õ§9 ¢ö´}ûþX„$8euD ƒ5Ý|ýЉ­fà…¾ðóȪäé�¨ eÿ1®öÃyú¡Ë"$>³ÅG9ó³®!ãì™œŽ¡OÖ'¤Öo +ÿj×�(Ϥ· Nq¢Ò" š³8)DN ¯¿úý¹ü®a.!$7¼âžùéÑr Ö1ë^õÌúôh¹(YHbJ\¨Œ¤-êªô“gK•Cž‘J¹%¼™h<°ójGË ÙÄõåiŸo”…¥,™á+¡HV]vþÄ…R5@Hüâ–Æ†ÈH`-Š¢ÔWꇿß!*nÞl{!eVU];~!¿³¯ „<rÞÆI>v"cÝ¥£Çr4�e_Y„tòÆÕΕ„¯·­ŒjJ;t¼´›ºM[>² ÑÛY™ßá>™JÛvº‘žÓøÄù³|lƬ(¹pôZµ† eá‰+’ü„zukq‹à®VûÌ{…Óõ/wf wC´aQþÉoÏ0dtغÚÊlôùG§7š�ˆû×å°y¿\øå™UöãÆ»¨.ï=R n£ÂPv—$$v—ffe6itÍé»a…ð¶³M4޳7¬_Üv¶ †nˆ¼¦.šéÈ`5UO](ëÛ îkÂ0_:4Oq:“N¡A²–¼¬\+Ÿ—ƒ%y¹jèÚ3ŸœÿÇøúO÷ç>,™5}ÿÀj³wög‡Koo§´J¥uŸšlÉ>ùéé6 �Œ"gÏßs(ÿ”wæÜ]˜£üêž/Ní¦¯ÜYþMî}wt¾,Bš² Xyâ˽õ›ÐÅ›æOkÚ™6äÍÆXuYasìø@YV¶ŠÊ+$*Õ˜ò™¹ HqtÇîÖ-aýêXÿ҃壹¦’Å$ÏsmØ÷é¾V‹|âêÕóbš÷Üê¢ ¦»K%ñ”QÐIˆ¬(‹ÎÀÚÙÛvw*XQðÜ—š}Ÿ]îàÎÛ²`JyÅùzštëÛxbû·žó¤y!Ö åÃüâ '-žk•óõçù*pœñÂú¥“š¿Ìè�ž›¯MƾON[¼7¯˜ä¦†ª,†eXI€§î«½û»)ßoÅǸ”_jaí&ÄM¥²¿úWžZ±ä墔�ÒeÊâ8ëÜo>ÏU®±«W&u|þ}¹^à7;ŽþÀY‡i/¼Â3åß®ÅØÛ«3ý ð`YÒÓWzyïž3&ÙÔ—^˜»¿XOzß¿¾h–á¹ñ>:ª]¿1Ò²c÷ùYoFy kýG¸mŒŠ¾ñú¾¯rbfÌÙ0›hÈÍʼYÙ9ª Ù.EÍTŸûú»Z3%Ÿ;ÝKVQ¤d‡Ü¼‡þÒ¡Ñx*G+�€t ‘µ–Võ´WVªÝB­G»�Öh4‰Œ—oµØ»OWWT©eÀ¢(©T8»»<èÆ´÷¡Ü|4E7t,0šŠü*±_€íp¼¶¼¸Þ!8XN�ð¼#,%EÍ4�]wöã¯Îך�Ì µ±Õ±"BääÚ–w«Õ@« óZ½DC.5ª”f™ÜŠç1óå­¼ùb{(”:ÆPrè³o¯vX� ÍÍ]"‰„�¬ÉDK|Cƒ<¤¦ýæñ+ÃE3�! °­-,RY�,‡>Ùy³ÿÉtGQN£ŽSsS_"�°C–�tcqY7 `îjWYI$$�ÏÅ᣼ª›¦»,¯ªï‚TBæï'©Í+PX�Ìm¹ÅJw H{w7s]y‹À¢(®¸ëb6ºéÒ¶mßåü küX •Õ%MF�VÓÞi´‘Xî/VÙÙiÔkµFE§Æ¢Õ›„"oÛÆhkf謼~d÷g_]iw‰Ý¸8äáwRxh»Àb6 =ÂB¼ì„¬ªøÜÉ"%3ºÍÆSÛw&‚Bä­·Ê5,Ó[^Þ;)Äß&7t#Å,Ëšº:Fv»RF§íûÍÎR(äŒü÷'O$:LÝøÆ�€ xú[Cg#�€¾º¨&~jˆ<+[¨/ÞÕN�!vŸ7;ÒIÄÒ¬HÆïíýÝ…"å•ðò»±��@ðXE“˜€¡nÈ(»º%vNb—¶¼&{?Gmw—ŠBâ;)aV =ŸfÀÊŽ×Y�Àve= Ó&-Þ¸ÀZ[w3õtZmïÐÝ>Rl%2ênï͘ƒ·!dͦ¾Ÿ04ÝÿÀ!Ë�ÆÜß½e†�� øaÔ÷-‹ÑõêY!�"+‘0$åÍŸô0É#›Å| DBQÛ7ËšŒr@Ölîÿà ÜÂo˜õÅÒf`à * K�AfÛaóÒ¦™.�Ýqí«ýYŠÛãqb—à‰Ó§Œsë-½Ü1ê‹«ïoÛ›ì8ö¤Ä—d¦–ÂKç.–¨èQmÞhžÖt&Cíyò¸7ß‹�’âA¿u~~ïÃæ»ÃŒð7$!ûA(2&£�X¢¯“Àã=èƒ4ë´úöô¯öÜÉž€5ÖÖ&Ï rQ9øw—|ÛÅ�å1mÑtáµ/¿-è¦y!‹¶NHeÉþ®Ë`µ :½¥öÜŽù½7+«Sv“îŽÚªc5þ ý½hJUÝËòýã—†›ŽîÚQ©cl¢×¼ëß?¹¥»òêéÊ«„À>(aõü¸æíÇ+‡Ük1zAè îÿÒRb¹œ§Uh†œ®¬!*k1›A(�˜+k1a�V¯ÕŠ/üûØ]wf'M&³PÔ¿­¬G÷ãc”F±¾,£Ù6†Ávþ¬œ�`Lýw`âÛú„Ož91غ³(óÜŽ£­Úá³™ Èþ§­ñùIFSŸu²>ëßÖoúòås#êæAÐPžÖ_ Á!²†Ó_üåƒþåƒþåÏ{2Õî!}gfY,´\Î báÀ¶F[hÒÖNJP"‘pTã��@Úú†¸ �(yp€}kc+ Àêz R;�) t¿ýIÞWÓRU#˜à."�øöãæÏ‹q~P L5%UÖþ3'xv–õ?YS$(yx„;ÇçÝ.,Ã’ÔÀYC¯–•ÛÛ’�”}@ 3ÙÿlÄòV×qÑN|�Bè6-%9Âv¸tbŠûÐ`º¡NÝXo ³Ww)à‰„üÞ®N=|—ˆ`g’Ç£�QPÊŠ8?�kênëÔ<`OǪªj4~‘r €ï0iñË‹ƒ‡=‘wȲ†fîhív ò—’@Ù†Dù÷=K‹í©ªÑøG‡Ë(�ÒÚÆ¢x?+˜Î–6¡Oˆ‡¡[Tãˤ<ç¾ù檉ÒG–Ø\_wå¶1ÌB,ú^­¶W«íÕ÷=È´ŒŸáØqpÛö£—óÍ�ŒN«ÙÉE€Ð%(`Ø �€ç9{Mbˆ„�Ö¬nëP›–}@~ì—î¹÷´öƒ¥uÅ…t[~žòåP?ëü"-Ó”—Õ»tÝ»X“¦.ýÀ[ €5TßÌŸ¸äåŸÏ¶zªÎ{ºD3Ô~ž°Š\¹1Á‹ž@Ìg_ùÙT «O}tºŠ$˜–&Cô²WÛÙ°-Wö°�tsÎÕöåK7lìV+«šët^dßö7DY†²s§%n~O�¬©«äÚéÎþÊ47UHÖŽë<y¤¿t]nfÏ¢¯yªº;Š3®U,™½tnëÎó}·Ð¥• ü%kߟh6×\üüpiMfÖø¥+6ö¨;*ëY�Xõ­3gìæ-;†bCkÑÅ“=ÃõsXªÛÚ–Ê©Ó2ºúsŠw·BÍ]}ófôü ƒT=-9Y×êæÍzqjÇÎÌúÂúˆëß²�twÙ…“ÕÃ÷0m™G/Í¿öµY°(ªÎÍS2Ãt C•õmñ•í̾œç—òê{UÍE*÷¾²:²¦Æ/X÷Ú,’¥µÙg²ÊS¢ñ�� �IDATu,€±úʹ€å/¼þ¾¹§.³²ÖìJÞÎ!J`e%æ?²®Êëkwù-å¶12Œ"÷ðî‘NÜ[|ãVdÒÆ-áªîöêÚVà ÿ9XZKJ"lÜ,°½u—Ïë�`˜&ŒðK‡†…÷¨CÏÒmîË áäüqÁÓ:²��é4yí)ÁVðüÅ-*Œfô¬xZG6�`:‹3*&¿ñFK«+.ÕYàqŽl „áÈBq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3Bq¦3BqÑHï¾ïàèôXëBèN#Mç®ÎŽÇZ„Bw‘ „â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„â"Lg„⢑ÞAôÑ r#ß] �€?4¶ªØ'R„â2J(¶yèDvöö*¥â–ú»—„A”ó¤NçZá’шPމW2!Œ¨ÈmÐ>M{GÂqöËï½’4Ѻéfeg+N:L\»e:U^Úf��Â&`ÎÊuK&N‹–+ +ÔŽ“¬^³()v‚·¥¦¸I7¢Vðb–¬|qyâܸpA';yÝ\iMQ“vä5â¯ØºÎ¯5›ÃºÏè;ϧÆûQøÎ��¿^)šN¥Ó£_ŒÍ¸5?Iö£XF‘½ë³K f��B>iÃ;ñn­©Ûvd)™G\o ¼æ½¶nŠ”`Y†6öv5VÞ¼˜–ßfzÒ{lŸ‰ÑNªÂ›Uê'+¤ÓŒM›\o~|0¿w¤³0Ú¶òBK»ž� <¦/˜ *:ôï,“$2>ÑCqê›}ÕÊÒóÀ”LZõztÝŽ7LQ± ¾ªs_¬Ô³“4¼Rôr7f)Ø-)†ïvdªœfnšgúþë,% ”Wü›ëíÒþv°À00a5nÍ› ïÊ ÖRzèƒ ¢‡zýOÇÊ~@P<Åž@:¿·PЦbNåÐ�ðj³u ­Xÿƒ—Fvã¦|s­ë¹Yq,­Q´kh±ÜÁ9pÂB¡þ““†‡Ïö4"¢æ¦L·ÊnÌ©Rß±~Ù1L&Ê9*TR“^1ŠŽ*€¾é楦þ¿ù¶rkMmic·†�ÊËNmyÅ]½æ‡-„° ‹tmË?­b(7;Ñš^ÚÞk�Ðæ^h}CÆŽÈÉIØ•ÝÀÀÉIÔ•;ì~•Õ—ø¦YH üŸtœ²t¡Cs;­×ùú3Øßz°±Nçù(9ùŸßö§É¿N˜þ´^4ÕÖ£ÆhÔ:k‰ë̸Ȃƒy÷mB·qqs"}]í¤Ö”EÝQ•uù|f“Ž%goÜ+Ê9xÎ7ÁOÆ7(*ÓϦ¶xÆÏ›à$4+rNŸJ«Ñ�PÒ€Y±³Çù:IxFUsÅ+³[ ,\¢â“&‡zÈD`Ô(Úªn¤žÏSØÍÞ¸9NV~ìp£_ÜÌ{^osÞù3—Jºi�°öš:wj¸£TLšzšK².œÏo3üÿì½wxW–ð}ªªs«¥ŽR·rV+"‘„„H‘ < 8‡™ÝÙÙÙwgÿø¾ç}çÛgÞñŒ=c0d°É9Id”sΡ¥Žêœªî÷‡$¢’íÁ®ßèºêžÎ=uî©�8ŠYó$)ýÅ<†Û¬î¹sâÄÝ7ÆH-ÈN‰”{²Hó`Gù¥Ëwº,ãʉFjÏì9?@r—½³r– 42€hi%'),�0¥±™¹YÑÁÞLÒ¦í©>äzïÃÕ„y(—oX9Kho=ÿÅ*-gÒ¬²³‹–e*½Y6USémKòÊ Yó‰ÿ{°ÁÀQÌÊÍO‰²)«º³®ôÜ­ã+"råoW(u7•0Ó fðÝÚÖ;gNÜë³�C—›ªyy°)ËpwÕ¥‹×ÛL¬€´e+æÈ K_ÿ¿ÒÁÝzú/{ëFsã’”UyJ Çmè¸{ùôµN3~tѦQ¨öäç§Últ*ç.š$áàNópÛÓßVªÀ¿àÝ|Ç7g-i…9JÑðù{ïYéù¹³B}x˜m¤»ôÛÃw5�¾ñ±ì¶³í6@x)Οíç‰YúkoܵÌ^È/ùøT7I<žÚþθÍoUÿí@µx©~LfÄ¿É$»n]±…gEz±ˆ•¿ûírݽ/>ºÜGx'ÌÏŒóóÄ,ƒw.œ«ÑâÒ˜8ïþ²f"á¥ÍùÑB&±ô7ÿÏbí}ŸÜò~ù½„îO÷\W#~ÒÚ·Ã[ôøÏÏ —ñ(S_Õã—t$��!Ræ/œŸèç…Yújo5ÃÆGÍ[<7!Ћa×¶ß¹rîf.ÍØüŠÇµ½ý‘Źñ>Ö[»¾(UQ@ø¼»ºx›1gÞ,?e¨»rþb)tåK‰‹i²#��\‘ýúÖÚ¿yS‹�™B¤Ô@Èåb½J;yÏFöÍ¿‚šÁl9S¯G&¾>iB?Qž·uޚϪì KêÉ_.eÀ_N:+;È- X§Ë¿“ûL©«J:“Š‚çÍk>Öö˜érqä~K_G¯¿p%vkt�àžI«V¸ CÃ:§Ÿ·OÌ¢õ.dU«´…\6·8³ë¯{HfÀ‚5/eHÜC-• VQd\ââ5|×îC5Ü9+&ûPº¶†6;ß;H."c¦cE.Y¤W©5ö�yPÚªeæá¯ni8(c¤¿¹›ô ŽJY¸Ú­Û~¶—%®Ú¸0”CYµƒýN¾TÌt™H dsÖ­™ïúöúr-7xVôü ^èÓ½·Õ”#˜  H÷S ëàǾ²:ÚœúG,ãSæË?geñ,!9xëð‘*- “eÕÍ š¿6?N„¹ÃF<(¥‡Ñþ‚yÅ¿º(‚mê©­Âübã3VK‰/·_í{â‹¦Ø24¨6rý¼csW»tÛµÛé`Iü<ªö¦N¦wdTdö¦é£Ãm<r‘� §º£¡×BéÜcýóLÌË1«†- ?i伥Ρß6:¹Ro/6 ä.öuæDå¯PÚOø¨ÕÂð”ùKœ �À¼b‹ GïÝ­7˜ ¿¹k×ÄKÎýÍxJ¸¦±˜346o=ÞîÀ½³V,·–Ø]§gøÌÎ[<׫¼ß¤†¸ãØUºgûÈú_¥ìÞ}m€«½ï¼Â½ð‡ãM$�ÆW®Xú÷íöcÌ;¹xùš"˧߶X�ò„h¯Ž[-&½íðçÚ…Û^–þé`ƒ �ó|ä�ffä¼|畳{®0iRñò%‹»÷×Y¡ÈY±"väʾÏêô ïÄùųÙ0š[vÐüWúÔŸÚóÍ€K³híŠbûçûË��¡+e÷N5d1iÆL”Rwóø¡O†Iatîòâ•nýçWëÚÈUQ¡ì¦;�ನp¡ª¾IÏŽùí%‘8†õúo†õÆoóû.}|°âiý�0¯„Ôhgí×'»þsà¹Zç-ùL¹ÿý!�DøŽMæÛuÑù×7¸[ò™».>s¬÷ÃÝuåFgäÂøy™å]W ÆQê;{þû6B€áÌ�Û–_dxúùKˆÆÑQ!†›jo¿ÐEù忳aŽ„ájüvç·-Nqúæwr}2>Öã KM’ÔÀSWjM€÷bÖÌ Mç×iy< §®½âò]•1˜6–¬öÞþ¥ƒ”gʫۅúÄÄHî”h(wߥ¿|!À0VÔ’÷_Žõô÷õÄûY³“‚¹àì¸¼ë« “ .D'¦ú1‘¶ìÜé2-…µ8…²åq1’»×t£"¼â‹6…€@"ö`©mjè#'-l“9&=Ò£T×öí¹:äŒÉ$\��€(Ì+qé⡯ÿfÿõ^��LœÕÞ‘ ˜h!æ†c?ÑdcølÞ˜%ÍlvJ8™«¯ž¾:@B½Ñó•Aѱ~¥}Ý»M¸[O}q¸ÖÊ )xýÕ$¡26øT{“ ™*|X`8!ÑoÝœ§PÈñÊæ›7ü£"å,[ÇÝ3gzÈÑ��0ÌXsø“ N^üËo+9 ?Þ¨ÒÜ>¶Wëƒ:ùä€a8”Ûa·YI[÷ÈÐýëLÎÀÝWZí� EWöÝh· �fü&vX|¸«þ›7�!OH”¨n»Ýe@�¦’KåQ›SaÂÔ�çÂTÀ„ÑéQ¦»Ÿ”÷@÷Í-)kbBX-õ�Â76–Û~îáAÀÄ ªçö¹ê^�˜nÝéL[¨âuPÄ%ˆo½Û3‚�:o\«ŒÛ˜��ܨä¨Ý{­CGXªnT'mLTT˜0œ7R¶÷|¥é™6RuíF›†0Þ½xKùÆÜÿ+ç›+b"8 µvÀ%QJ‘ªºEOÙuþ§–ºô½ÄîOŽÖºÃ—ßþñ7uv��Âë©Å )é¾wÎ ’S»þ³àùYg.¶6‹QÙAVt<?ªè *;ȵYŒƒ7Üægêâã0™ÄHÅ¥[‰[r½S ’ªºjDÜ+"£ /!ØGÀ&0 �€"÷ ŒÜ}Ý}.�Ì 5P Uw¯t:7òepžTÊÂ0ÜoÞÖ·æ?Fñø<ª¶¦L—çQ¸ñ½ŒÞºׯ•õŽ1BöÁþa�̽=z*Š=1Ð’è¼Â9±ÖXVH‚ €K…Pšö.#�@ºF}#)\š²þƒ”û¹å{ð0³Î„@ªð Ü6ã@ký‹·»]“ËĆFzÚ5n��är=ph1~LA¡Ð³Õ]¸Ø4š$«˜‡XÈÁ€ìê¶€{°kÀž)a�0¤Þb pÁ¬â·gÝÏ/xðïÇ ‚ÒöõÛ�ÀÙ×7DÎ1=… ôÜÀŒùy)¡ !‡À1 �EOiuäêíêq�€]£1#�—¡»Áðø½¶æ«çÃV-ßöNzcùí²Š&µ}¬ h ³gìÿ˜‡DÌÒ·=®~¼°¸kÃ$�0ÅbM58n¹¨š|èö‡R›2¸Ä[ÂôÉyýýÌÑ¿1‡¡âs0p FpL4Ñz¢ãÙß{‘Q£uŒýŸtºÜ8Ž�S(ö° Ž{+”~x,·¸È[Âoø—„Ñ^ˆ,b„ÏÇÀ �”º³ëɯH«ÑuYdU šÙ>^²®¾Áþrl¯®Þ.‰ˆ«êÇfƒàR_‹êŽp…Ì8x}j_¬yÑ© ìæSµG''»þóàùYçµY ÿý!ë蟭lô‡'»?à®Íb|÷€º}¥bÖÚ”€9ó£Ë¤*ˆ-|)+‚ií(9q³mÄcöÒâäG^ßN»� D�¸ÇÌÖC_œ0 @Ž®ëÇ*T÷ƒ¾vÝr;JöíèIÈÈN0»èå@¯ýŸ^è½ {ð4 �À%i+§ûa†Æ’Ãwz-Þik+9£÷À&lô9Pv¢¤ç~}¸ Ãã/Jwóëíçq&žRX Ʋñ¸§ÐÓárW™“æ×VÚïzZV‡¢î§‰ ´U®4ÝŸž‡Œýz;8öD‰Y‘y« byŽþ{ǯ7hð¨Å«2Š|€Ë1^1Ïþ@è.?¾£A‘”<gñÆôä+_í«P� Òuð>ÚÒ?題 Ö7^ÿ5Þœcèa/ã¡Ô¦˜»ûâÎs­Ôž´›�+,!ÂÝðmïTºIMæV>T7Ýo.£n}¨öÁ ÊaÍ;r»'šÞú°–b�ÈÚ&óFe·q@!¬mADàü7_í‰ã†¶þ.�'0ôúï2íUßüõDçÓŠ€K3Â,_·Û§výçÂsZ+¨ak³gÊÝ÷מüå¤ó/'ÇÞ«-Ô™r÷Ú,†B4‘¿õlÝ×/5[‘$/!”I™²¶Þ*mìêSÛ Öi?£cSzƉ0¦€eîjnjmnjmnééSÀ`³)cWŹ/wn?ÑnB “‹± �Ü30Xˆ5¢A˜È[J`H__ZÖÜ=¨w2˜cH½Á„�—†{á��_$ä ‹FkE€yp]Ý­crÛû5O ÌOZXjD§§�‡I ��Œ#Ýwj)cÓ±¿¼Ôå$äé˱1€I³ŠÌúÂ'ÀŸ �¸P.·Ú¤fØ€�ãóÑ@óX†Ûzúµf[­ô<ðƒqIP�X C®½ R CTgùÕª®¾A<øXI` 擎ôDmÇÇD‰&ð8(›ºùÆÙ/>½4œ Bé)³Nç)¼9«æ=Ëo¸¦Q;j¹\½™'óæÝ‚ñDBæwÓÖeÐit˜Dêa3Mãÿ¬. €ak¬øî+\#z+Wæ=ÞÔ˜‡HDŒŽ¨F†u”Dâå4=jv>5l€I¼EÄx: ?ž]o°# U #þQÞááâþº6#²çòÇ¿ÿóÞ{#Ýgwüáÿy_ÙH×Ùøßúó‰Î§‡%X!)É’¾»åÃäÔ®ÿlxN¾óÖ|–€‹vñÁ;ðó8àµÇ®|vÑY”ÌÛšÏú?‡“¤ñT,¥×»Â†ðx0öqšÑH$ä…eægø3‚’âyÓžˆeo½sO1W–¶a£´yÀŒó¤þŽ’{«¼ Þ]â«îÐÙr9M­1Qà ��¸8eÝ;ÃnO_œ½µ : á:=…䢘ys*ej(€� †jêT©s}ƒly'Fmc‰½ñš=».wWÜê‰)ŠYþºGk÷Åù3kv}q]=iN'-,24U÷Ìõñ™·áíÈ!îå-è:ú§3]£ÙtZ“¦édIäëùÁi…í_œl™$«@ö46cg{Æ­x]Øgd{ûKpl¬ÃwZR–(Ãó_ÛÜ®v³…ò@a÷‘.öûe¯[ŸìEvœúŸCU£ÍбÂoz#ÑÌôññÄ)c}]— ^ïD Npr~ŽÐåŸ$ÂÆÛ‰2êFÜÈ›—P¸Yd@–Æ£Gª'o-L:gÅú>Ðwuû®;BÏ¿8%KÛ3¬³ ¿'˜Z'X~ãê,«5½š½8Ýr­IOñ%2BÝj‰Ž÷¬<2¾|ƒ¬­ÑmÌÊ›=x¥Q‡KbsR}14üLz”®á^KÚ’¥…šS·Úu.¦—<Xb«¯îÅ•qÁ#×¾Ï 2r°®fdãܼdÕ•F=!‰ÉI’R�XšÊjsV¬Hw]i¶ï�9tV¶<‹q•Ë7-âßþbo•��¼fÍËì¼X9DŠãó3}GjÎ÷~èh¨3$'dñ¼êšÇ>W*Ã4M„ñ½½AÝðh]cLL&ëàÈiÔŽØ`‚ØŒh¬þXÃcыɮÿŒx¾³B„%3^w=¶hûaÏcP^w%g÷™ÒW^({8ˆŒµÎ6 Y¹Aié‰Þº«{/wM;lâî»rpßùš.' ..NéÇ5´·:™ú: LßÈ„”D¥Ô=Psõȹ¶ñw ¥¯¹Y¦eKÅl—¾ëö‘“eZ”úÞ™ÒË+25#NÐ}êèuÕ˜7@ªn:p½¡ßŒ‰þ GOK· ©¹»oÿé;:†\9+.&ÄËÑÙÒ÷Ô%}“ÊOì?[Û£#=ä~ !¥jèÒ=š¥­<}©Û –åÇð5“e—]m¶B)Þwãv/ 0:zFÆúc{NÞlT“ÒðøDe¨”hì¡€2©L.›Z¥>"²ûÎåN·P"À-ªêKGÎw:�Í¥gÊúÍ„bÖܤ�[ÙÁ“÷‡ ¶†Ò 5CVäáí'f8ìOm@dS« v‡~Póhø˜!IX°î­·ýÛ·^Íõl;v®Ò0AM:{J|ÓHÌ^òÚûolÛ«1eñÑ¢®º–_ÈÈÁ’c'›y¿x㗿ܰ@Þ[ÑôüˆG²l®;vðL;/eõÆw~õúæ—2‚ù@až1 ~êšÍ÷šÞë¸ö͉&nú+¯¿ÿÞ˹²öË·ÆÓ³wÿêDŠ^òÚ¶÷?Ø´6?Ò ž„c"PÈÕr«ž—ýÒ[¿Úº6Ùxì›kcsq¶¾Q¦h~PK¸Ä[¬Ò@H}$º¡Ç¼^<0oë–7ßý·iI,�Åìô`CåÝîÇÔ“]ÿ9 D>ϼ),"²½µå;Ëø5ììXbõlOÿèçÁÅŽü–[ZO~G÷ùÇ“ålÚ6ßÛzïÀßN=1Yá§Áƒà,¡X°ùµl‰eê…%"WþvE,Ñsö/Ê~ Þ#jÕ{‹ìG?:ÝóÓlëQÿ‚÷×zžÿðHÃDQ¿à½5ÒÒû*¦µV‡fŠ<Ȇ\„™¬èå,æ3ï4Y‘ü;úÎ3†<û“CD0O4¤Ò™ìn†4<V‚‘ºúÚ‰¿ýýôÁ…2nix¡6)ù®`O~É��†oD8§ûz3mšÿA<ëüáIç¾ÊÞ’ÿlë¬ÒSžüdf4hd ×ê®à11Ò>2ØXZZr£çç³…æ3'9ÐÜ­1»9Š”üT©ön³þ{…^X6‡Í“§æÇãuÇhãüãyD6hh~ ð#rŠÄúJ=ØiÕôÔ\½x­Ñð:þï¯õºð×ÃõÇü™Ê•﬊}ÛíßÜéû™Nw{ÐÖ™†††f&BŸBCCC3¡­3 ÍL„¶Î44443Ú:ÓÐLŽ<!+1Èã¹L›|ž²hf"/¶uÆåó·þû¬OõzHƒ1n⺗’?,g­_=Õùƒü¤µÿúZŠp<U"dáûÛæH¦ZWÓ“õ} ‚òÞüõ{¿þݯ·dO9w3 FôKÿ¶6‘÷Üå²bÖüÛª8öô´µ÷qS–eOmÐ'xB70Aʦ–GM¤-ßWÖÎóë?Q^èªÃeQJ!Â…Q‘‚²{ãëÏp‰LŠ?qD-Cê-ÂuSMØÖtéË^ûw=¹mz²¾d÷•íÿ]X´-ÿyH£�dé¾u¤û;?>-Ýø²ø¾J…£³]ëø1WË<¿ŽðåÇ9“û‡÷™½p4V¹#55*'�!…ï®J ƒg'§g¤ÎòªjA'nõ¶¥±Þ³R礥+ñÎê> fLñ/°{ˆäµ¯,Î_.,oÑQ¸$yýÛ«d&§øYkªÇNÂEa©a,§OZÑâÜœT?ÔÛÞo¦€ýÒ¿ÎgUÔ«\£Ë”p*êUî‰ea‚ày/­*ÎOKM‰‘“ªÎAËÄ«0Ïäoçskû��DÀÂ÷_‰ÖU·è 2wÍúÂy©i³¼ÝCÖû à^I¡Î†ª‚Q_ì­T{y³–ŒŸôêÛiŽòf-„P¹hÅšesÓÓ•[_‡fâUõ„oþ»«üºjzÆ—`”oÆŽ”u˜Ä1‹×¯X”“6'=ÒËØÝ©q �L:ÿå•Eó’SÓByú®î‘É·Â`JòW¯Y8?{Nr´ØÙ×5d¥�—ÅdÊ‘5pÞÊ‚ù9 {Oë  ’'daži›7%ñ™ÊyYÙs“}­­;`ûf¬}yuAr\¸'!ÏZi¨h3"`xÏ^´fmÁÜ9ɳ#x†Î^½�paìÂW~±0#1ÊŸ…8Ü¡Ûcû@ÁEÿ¼5 k¦?w—[ôÁªPÂ'>33=;3Œhë7Q�Øí5¡ÛwV¢ÄME-Z²h~zœÈØÞ¦ý62`#ç/^š" MÑù´53¸Oî¦MQšŠv#L–³i³R[ÑnÄý¿ÌŸR$åfÏÉÎŽb ¶öI�àÎY¹®(7#955RdêWƒ'Ô{’NG3^`ß÷ŽŽTÝ­àoŽŒTUg?:ÍýÍìîTÜß¿ÙëŽîañ¶•}òMヵm¤^oñ›³È]vêã3*;`�J[¾÷OåDXá{ó& •îÝyÚ!Î\³qQ|óžŠ'¶yŸ\Æ.\¥;ùÙ¾n»Gtñ–Å}_”L¸26ÕöçÎŽÞ-Ó# •‘Ðz¼ÃDðÜ%‘Úc»¾î@¾ù¯®Ë k<Ü<5•˜0iQ‘¢gÿÇûÝ¢”u늒ú÷Þ›h· Ê Ñ „¨1p[íH,ñ2¨µˆµ _Þ±ÿ“«ÃŒˆ¢×—¤7·\è&qEZnHïÉ_»>©EJ~Oó$#Ü'µx¯üóO«õ ËzéÕ©ýŸÝÒ��Ã7ÄãÖþθƒ ¶­N ª>ÛAM$‹B„Xwï;` B–¼3?IÞ|y�‰“óæe»ÿReôŒ[¾9 kD�€ËÓ‹óø{>­ÐbŠÜukz´ÙÆ Í)ôë>¸ób/’f¼ôÃùÐ~wn‡Ùl}ª › a¾Üó_8ëâÏZõú¢„æÏË xÐí5±n��3 Û¿gÇ7„råÛs“+Z¯ ~ޛûwWÈ“²æmÌÁz*î޾תžÖé�á!žW÷í=ëÎY¿5+¢â@½ „ âç??Ôé"D± 2…-u:4¡zOÜéh¦Ã ­��\©6¶ µ¶}•üé&€'ÇÑrµReG�èÛ‹Z»êZ-¸µ ­Z?ùÓNîxÂ/<ØTo¾—Ü�� �IDATw¯ÇŠ€2µT·qCý&«xKs}·4*J„0‚âÂÝ uý$�Ùuîo»/t:\ýÃàéÁ›Ö·"Œ©PUU:�H}mÕ OD gÂC¯s E<†ÿÜÍlÌbr%BÐꬔ½áÈ'_–»ìýýŽ@À��ät’‚èHO5tïĵÉL3�&Š÷ꬭӻÜê[G>úâÞØ¾Éäp]y¯³¿o˜)ðd  e�Ù[ßd \š!=O Àréps›ÊÐTÕ6º·& tVÕhÝ�.UE½.8Ì�\âçëêjp¸µõ-ª‡ù‘}—·o?Tþ7g¢FºZ\�`íìÒzûʉé¶Õ_]5è�°õ÷jxžž?TŸDvuëÍo¿þd÷µ!yî¦bå³wRxäi uí }�dR;<< �Ü.'Û?F(f#}ýùSu:jzêM3^Xß÷ŽTŠ+›Mˆ277›S•aÕÓ‹#„œšá©ôKY-£ã2äpØq6› 0õýH.[:gÓ[É��#¶Ê‰m#�€­½®cþ¥èn™(&ÂVÿÕ �×/%/'Þ›ƒHÄ2ͪ)Ë…Íáù›ßÏ��Œ´}\ & nP:A {s媪>I¨LÊò2hô`‚Ôüì “¤€'f¨k�æÖ±S‘Z¼i ßÒuïÊ™’NóÄnÎåqÖñ¸årܽ#—sôŒŠ$ÇNá˜P�P®1÷–¢( ��c²X˜Ã6še5Û�0ÃV¾ýO£gàý\&`6ËaÃ"§ã È"»}´&‘ËåÕ rZíE9ä#åš&˜8iý–¹r€¾¾ûÀ]íƒSk¸ò¨”ÌôD_sãÕéïaïrϨñ­ ‘¹úø fNjÁæ|¡s öòùK zrZêM3 ^TëŒË”†(ïí_æ�N0 2Œ_]m~ÖsAQSCblÖ葛ͦœ� 6ê$0O«H—Õbº±{塚 ££¶sQv¤\/ 34|©A�@øg,Ëd_ÿìËÉP.û ó™ÅÇ\—ûÙ²[mîÎó»V?3¤‰¬:î!³´ï[Húv3b†Í_ë<öÕ®V+å1kÃûac·» ­¥gZK1–$2Ýâ¼þ'Z'|kQ6«-åŽuZ‚+1,ZÓÄÁ™ÉdMY·Ël À€ñø\Ì �Èf±Úë/þýxçÃ#jÜét±9cÈãOoðñT06{Ôa6‡r:\Óm¯ï 2Ô|óI3�@9­£:Æô ŽM››ÅW×Ý>¿ëØ erÛŒ�0|ì´5&ó2uß=Õ}÷4Ó+4sÕªq݇«íÓQošið¢Ž@¤QJaÏ™ü¯?ÿñ¿þüÇ?ì½môS†ÎÌr»I‘ˆ €³¸ìq]#Ý$î%ö$�‡=­¸��à^!J?�!Š — ö’�Èj¶{JÅ �ÜSá÷ &ŸE ´uxÆ%ûq0�¦$qqQ’ÏÓràìhhã‡ÍMÐÔ6íÆá°íZµ‰BçÇ`0D!üþ!©Èn¶ ‘Ä  $á>øØÙˆÍƒŠÄYÞL�Œí›Q¸(Îk2ëDiµ#’è(²§ËØÛí‘5Z 6Ó¬QÛ(`Êã¢|pƒ�À8‘…«óB9�ÈiP©MOyÓ!}[‡)4>ND�0¥©Å›‹£&È;¡¬‰q ¼#Ã<q ¼” a£gi¡‘¶SجX!€óò–Íåa@©Tì`¥?0¶oB¤ìá4‰€o¿½6Åó»YlÌ+$&€=¦ª¾Aò)íõ}õpb(·Íl±˜-³môtC\??Kf¸uxûŽcW«Ÿfš(«ÅÆ‹8�[>©b��0r6( —Q5ltQ=E½ÿ1…ýñ¢ÎÙH\8ßUvªzxÔQC#+2'Ì^S?ìB#,{I~îܤX¹½½AeC�à6Ûyñù…óR“b¼ômj'`\¿Y³y=·Û¼ò1^üÚm¯ÍI—ó„q©)é}Y›Nž*é$-\87ÕWýäõN3È<âòÍ\˜êcV½…ƪúAçD²nM¯Úcvþâüô´ÔpÞPÕÝ:ÍÓFÖ”Ù)ÎÈÒÜ8U©r� “ Ï\楾Û@ÆÍI¨ë:F7HC$K–”??{NJŠÜTÓ4¤3b¹yé±áa"í€=?|¯QM9†z ²ÔÅK²ÓÓg…à½÷î¶&‹Ì¸¸A¹‰ìª+÷zÍÏØ¹á†ò«-:—ÉÊŽ™—Ÿ¤Œ âv•·r²f1újº­ü„ÂEy™I©©J~×Õ‹åêÉʅ̽¶€ì¥ù9s“¢yý—OÜ᣻S¯rà’è,¹áN½Ê>‘¬Zµ a¶gß½f-K‘äÕ¯YKYÕnBÞâÜ„Hokû�Ï:ÊÛ”e¨×³4?{Nr‚Ÿ³åNu·‘Ò0lñÉXº0'%‚ßÙn èî4z׸X™=K8PU=ý9¸(,Eaêç&æ/œ›æk¼y²´ÝDMÞ^Oê!ËwÖåšn6¯mÛ`SmÇ€Þ>•„œ 7q^~jdx0wX…ü}e­# K uÔVöÙà°”0g]eŸ´Zˆˆ¼â¼¬´¤ÔD±¦ôò­^‚ÉÔ{‚NG3è=êh~"ྠ6/…S;/}Ÿóø¦ ²ðƽ»no‹JCó/jdƒ†��÷N{å­Â(LYxwxàgº>ÍO‘õ« ��¥®¿ÕºtÑ[o-D¤±åÒ±iͧ¡™ÑБ š™Ù ¡¡¡™‰ÐÖ™†††f&B[gš™mihhhf"´u¦¡¡¡™‰ÐÖ™†††f&B[gš™mihhhf"´u¦¡¡¡™‰ÐÖ™†††f&B[gš™mihhhf"´u¦¡¡¡™‰ÐÖ™†††f&B[gš™ÈTwߗʼÿ¡ù ¡¡¡¡y˜©Zgzøšš‡¡#44443Ú:ÓÐÐÐÌDhëLCCC3¡­3 ÍL„¶Î44443Ú:ÓÐÐÐÌDhëLCCC3¡­3 ÍL„¶Î44443Ú:ÓÐÐÐÌDhëLCCC3¡­3 ÍL„¶Î44443Ú:ÓÐÐÐÌD¦ºƒèK¤/þþ�ü‡ƒzô£ä†††f&C°¹ϼI,‘èuÚPêÿ»žé‡K<±˜�âL…ûLyú`ü¤l]™é­.kPS?jV~>0×n]¬©lA�„8¦pýK«–Λ˨êrd®zuå²EYÉrSCðsJ bóÖübÅÒÂy™¾&­ß²uqÆêf9å áÒŒ-¿šKT×8¾k™hh~`~ß9'–˜Jüþ�~·†“K”ÔO½ÝÇ#qÃ?- %�¢(—M?ÜY}ãÒ.m`Ÿ3[¹ú<ã‘ç¦ÜŒ¤®³¡ž¡��˜á¹ù±î;_ý¥ÞŠ#3å¹8CÜylÇž!„9MOW1½»Œufljvyæ’dNí‘¿ßÑ’”ƒÔ1õ?îKÿi`œ„u[C*vkÆbÖn ­üôd«0^↷3Õû¶?ZÜÄ5¿\z¿›’ 'þïÁ�pýÓ sS#}<Y¤i°½üÂ¥ÛÝVzúÓâG°Î¿\ÊRé©Óå$�lͧ>XÂ*©·}×Ä]?¨·3R©_ÜüÕbjÏç74´}~®pÃãÃÌõ{TÓyÃRºú¥£ÿÅ=Äb†¦¾yÐhF�@øŠ½ì=:Ó³ÓcÇ*±–c.` E<Sgc¯ÁL�4ݸ:ýr<?p©Ä0¤vî#—†ÕO))ÆðȶË_œïr��²]��ì¨ÂÙªov°sýç®X;_÷áÉfÚñÿIñ¼­óâdB.Âÿ×—öÑ?ÿrÒùŸ¯r'£ÆzúPý¥_®±â¬õoæûùD{ÞÔxs̉ –‰=¹¸s¤¿áîÅ Õ*��î˜2?3)R!äâΑ¡Ö’Ó§ªÑh†"}ý¦y„úÞ¾ýç;\Ò¸ìÜÔP…Ì˃MY†»«.]¼Þf¢�€µ¬ -\D˜ûk.×ù…IDÍÞÿ9×IžáÙ¹9‰!Þ†CßßrçÚ¥²Aû#^ &ËÙ´-—S~øœ-.79\Ìq:ï]>uµÓŒ�pψœyéÑ~2!Ÿ v]OÃõ³¥ Z7‘+»"ªçâzïy¹Ñ >9ÒSwùT–Z7Ëß·ªê¯Ÿ<]«q�p³róS¢„lʪî¬+=w«ÅàÆD /½–h);´«¤ÇõPv8ŠÔ¢éQ>žLʪ¨¹pòJ«…—´öí°º]U²¢ÅIxÝþ/t1|ósçDû‰Øn³ºþÌ—Úì�€ bâ‚´—‡)�àø§-I±šö{—:eËcúv(3"þã©]a¯x¯ÐþÍß®± 6$* ÿ7·ÐÙPr‡ŸÌfãÿ%‰¼üñ—7µà–Q˜Ÿ"㸵ݕ—.”¶Ç5…6+ÜUdÈ?{óª4?&#âßd’'ÿû¨!ïõ5œKÿs¤ÅMø¼[@^¸'g% ™vmû­ §nöÙ��7 µpqj„ŒãÒt”ÝÑ?P@ŒãŸžŸŸ.çQ#½ ¥§¯ÖiÜ�Œ¨Õï¤ôì»ÎÉ)Ê öh?ùááf'�?iíÛ‘mßôæÏ 3šÖòó'o÷òÒ_{+ºõÓÏK†Ðh‚±kÞZè<ùÑ·mN�àɼ™š¶™Œ©éxÚ�ç{p­êÁ¡ÇL8ÎIXªêêN­µ½¬q(=FÈÃÀA{Ï?%ž·uޚϪì KêÉ_.eÀ_N:+;È- X§Ë¿³û �€3˜`�ˆ"ÀAyøË#ýÍݤWpDPÊÂÕnÝö³½nÂ'sÃKó|”IÝ߇<}„„ÃBݯ¸WLñºœ@–©éÛ£;l�˜ƒ%ñótªÚ›:™Þ‘Q‘Ùk˜¦Uš¸1‹—çFóÀaT™¹ÑKñXXFSa,XóR†Ä=ÔRÙ`EÆ%.^Ãwí>TõDÄ=“V¯"C*“o€$"gi¾jÇ·@N\ä+¦tÝÝ O-^mWï¼¥��"$o] mxPcâ(Ä¡)+ÞLDî‘Áá^€ØvÁÂþ®}å&ðŠ/~uQÛÔS[9„ùÅÆg¬–_n¿:(ùx09\o²Î¸<£hž°ùÐ'G†IŽÈ×—£ þ™«¼«O¸b²h nùšQ󹃗ûl‘ˆ}Ûæ“ ª9«£�x‘…/çH›Î~±¿×é’µx¾’«êóHj„Œ^5·œÝ©1nÝ^³óË;F�%š•ÿºÀºoǹ.�ò¬—WGô<ôq§‘³zõ ×®¯n Q��¼ðøpKý•M5ðù'#ë•:°{÷µA €~¸ž1qjQÂíSÇ?;æà‡e¯\V”ÙýÙ¥> <¢—¬›ëU{j÷Þ~§ xî²… b°��¥,_“b¹|à³–f@Ö²åësŒŸ\îq�.K_–?Üpuß³á~@œž“k½trç%3Ë7­xñêæû›jû3£eׇ†)�`‡(CÝmG»Ý~9onNá8£€À …ü{¼µâèG§'<¸çyð<B–½›Àb"›®§áÆ…Û-z7PÆ®mFRjXsI›™œ'î¯k¡MóOŒçj·ä3å"ü÷‡l�á;6™o×Eç_ßànÉgîºèzêÓ‚ûeoÜ”Æñ’ЏrtÔ¶›€»ïÒß?¾ˆ`+jÉû/Çzúûzâ½Æ Ä,Ç>9Þ` ˜ ä«D‘,¿¼õEÑž®ÞKG׌†D‘©òȇ†ýÖÍy E€œ¨t…ÆEò0Jgïçº]œ¨%o¾7öm•–š$!¨§®Ôš�ïÅ6¬™šί®4?Þw0ÜX³ÿïº\ܸµo.æ(üDx£ŠBöæow4#FðÖþSa˜¿/ÔÎÑg çìW{ïy³×¼WÊ ´W?ýꆚˆ\ñæšD®·\‚ƒY2;%œ‡ÌÕWO_ ¡ÞèùÊ‚ èX¿Ò¾žÒý{$¶Þ6ë£ù p £œ6›Ía³5ë\÷p6|y枆�\’ži/ß}¹¦Ÿ�íÐxõK£c½û+G�?2!ŽœoP¹�FjΕE¼ÄŸ0µ)«#(-ѳåÌ¥µÀR{£"e[bŒìÖИGtl®aÔgf¬¹VÚ<LŒÔÜ­ÍøE€œ }~d|ÕxøR‹Ú`ª¿t=,z��÷IL÷í/ý¤zÀ�-%÷ºRòâ®õt��ƃ®#Go÷>Ò&»ïœ¯í·@óåóÊ_ÄEyÔ7Õ ä¦Fy— «(`‡F¹Úuº(WÉöß—Š2Ö¯á]ÞyiH4÷«Ùçw^¢FÓž�WÛч½ÜF³‹ðGçä®~…½wû¥n©*=ü-kÕKï¿i±2‰kû÷WjéˆÞOçg=¸ØÚ,FeYÑñˆUtP•äÚ,ÆÁn³mº¯Œ#Rø Êeîj)+¹Z1‚�It^áœØ@‰‹À0��’ ÀxR1r°«kÔ<‘.7�`c)ùf/‰”êÖ™›Cc¯ Œ˜1?/%T!ä8†�¢p¡HÄÀs »Ï�öîn��¸P*eaî7oë[óÆóHñø< ž°ÎÈÕÓÕë�‡VkF "�lŸÙù¹éÑ~b>Ë=N÷ÓÒvw™€]«· ð4ööèH�J§5QÀÅ™8�Cê-Æ�Ì*~{Ö}QàÁÇ�95mšÇkRݾT°x㯢Û+ËïÞ®ï1ŽÙ¤ïéÒ5&–©á¡ÇÂO¸O|´°ãv³�p/™S·[.·Zc Xç‡S›*˜‡LÆåÊ‹?ˆ{ÁÂ]j.�˜gL‚ïp͹)$‰4Ããw!§Ó A`€{I¼@Ýz?·NµÖ@ñ��XR™¶øõ.ý…`3ñV>€�ÀÞ×5ðÄ×FʤÕ;Òäð…‰„˜±©±wAJ´Ï Õ #$:˜l9Þ=¦ULE€×p­––Ü×c¨æFÕ­ïk}cj´* îý~n|ÐÕî6’2;3šÑy½´ &¥ÎÉO×-ív÷¡™Ñ<?ë¼6‹!àâ¿?4渵<ÐÊO:wÀ]›Å˜¾ûL¶ÿãáûCWpIÚÊÅé~˜¡±äð^‹wÚšÅJ�<°ÃhÆX"1ætSž4/®îH°"óVÄòý÷Ž_oÐàQ‹We(yh¬ùPÈÃ�£ëú± Õx‘]7á Óå/íƒþ9Ë‹R…ÔPíÙokþó7Í yä!‡Ãñp·ÛFx �@i«.\i²Œ_EÆþIãúÈÜyy÷Žò è䌴—ßM®9´ÿܨwMºï?ƒ†=ù$áÇk?ßþQx¨œˆz¤Ð¥6e0�JWöí¾[LrÚH�\ï3Pqx*ƒy„ȉî°G4¢îgkýñ/¯ô>PP·utŒ…H×D…À°Ç �0·ÖuåÎõ)ÑyD…ºZŽô»1~â†7 C ÇQÔJÀpGQ¿Œ&®ü}ÿóK6ƒÞÆôäàÀ Ê[o>³çX£ ¦¶:nå¶¥ wžlýÃOšËs²Î ¶6‹q¦Ü}íÉ_N>˜ÈÚ2@)w?vÃwyK iëKËš)¾ÁûYô#\Eh(¿®ÞŒ€!óí:ãè¯äÀµ¯õÎzõ•Ĩ¢ÂÙ}‡+tH •p0Du–_­êrŠ(ö˜¢F ÉXŠ ³­ÏÅ˽Ǘ\RzƉd,ËÜÕ<ÜÅÙ<–Û€áéÀÒ´ui|†|¢¨Kâ#À꫸QÙf±/óñ;&zµ<©6 )Ÿš[�€àq Œ- •ÚúZûžœá鮹ÜÝØ^øÚºÔÐkíuýLéu<VáM4=4Ý‹Åh;Õî«íÄˤ˜Ý��˜——ç÷\‰ŠL­ËÓ[H™º ÛãÞñÑ^£>ûwƒÑŽ@œLF€Å �€‹D^£¹ué4#ì` ÓR?ÕÉ?˜@*eà �€¡Pxc†VÈÚZÛUáÛà v·ëqrU}ý?U³Ö¿.¿ýá¹N^↭޷þz¾s´F'xû=O,嚆 $æ!•r õýcßjHcO·Ž+åc­Ú{þ ñœVroÍg ¸øgXäÏ?àìþ€sÿÏÏ.:\|k>ë{‹B#:=…0Q̼¹™¹‹_^zßÀ¹»êju$𣖽õÚ¦×6¾ýϯ/‹áßw¨j©ãÚ™rÅ ^°<YF ³^ïDœœŸ“Qðò²Ѹ‹dë¨oµ#\œþÊÆ_¼²þõuI^÷»–½õÎ=µ§mظ~eѲի_ûå[+b9€ñã—¯[½fÍÚ_&9 :+¸|vNöÜœ•/gúâÓínÔpÅ3pÂó_Û²|iñ’Õ·~°-K޽n}ñêM…ñD81Iä,e Ô‹ËâxúøÉØ6£Ùõ„LJÛPÞÉK^</ÆO${ńʬЄª¡þþFKKm;›—&áq<d¹ó¢¸S±8OÃÝy·R0·8/R!Ä>!‰IJ)„".–×QÓþ}æ÷Z[ê:±óóÂ¥ž@•77Œ3š[r°òN¿(sIÁ,?±§@蛣x´ÍpyÆkÿ²­ dܵ!‚2 â^|rA~,ÑV×lB�`okhç„f¦»›šûÆã!„Ä[¬Uk( d>Íðcas‚'”yËÆþIø B‘\â#zŠý£óVe+*ª(420`V$Í‹öñ`2˜|Ŭ¹ÉŠ‘îžģyÑx¾³B„%3^w=æ?Üwõèàu×Ú¹ÌÏ.:¿ŸûL©ï)•/›™š!í«:u´={eæèO®žË{O 3⃥~þ”MÝYÕoCWrt^:Wº&50»8»gOié™2¯‚YŠYs½†o<é·rY��²ÔŸ>.f¤† åsEIuHA²|Ì©u÷]9¸Ïš=7), .Ž #íuƒN@ ÔÙý¹ªaóS=2²§ôÜmiArPtf†¶ýÎño{®Hš^ cý±=H——ïn³®§¶{„Ê8¤2¹X•ö±åwü€¬%¹ÞršTÍ·¿¹Ü5ÁèT=ÌZ”—÷‹DOeê+;ÑÇŽ´7|õPÖÜxú€gÑâ¢msXN]wùíÚ!…|zYràæä‚ܼ ™åÐ÷7—ö�#(6šÕ~ºí{MîE¦†Ó…E…[ç°šŽ»J›W)GÑÝ;¶æåæ¬z½˜ƒ9GÛî{ìa ÃÇ>D��PC•wõaK__$e;Ô­7Ÿ¬3“Ž®ºÖ…ëÉ»%ý㵄ñ¼ÅHÝdE˜—Bê®}tª%x%.Û–8žìHùW«próWfù,°úšnî»X¡¥� ïÊþó°pÎúóÒ¢î©ûöhÉäÁ+šL òyæMa‘í­-ßYƬagÇ«ÿ`{úG?.vä·ÜÒzòÿšù“ê±a fø¢w^IäÝØ¹ãú3gü|À¥›ß¬úë Ó•$ü¤µï¥÷¾}lšßã¿&¿ü^–zïG—{iËI3mž‡ï,a&+z9ë‰ê˜¬H.ú¾Ãá<öºiì¡!ƒÅîfû*#=ÀÕ_ÛL/R|¦L*¤¬û³ï|ñ™De1~„R1Òpmê+ÜihâyXçO:ÿóUö–üg[g•žúðäÔ¶½ù1A.mßk¶T�‡�—MßW}óæ•Û?û-”ß”<¹±©k@g'Da9 "¨æ=3w·‹$“Ãb £ægùÞ9óø<Dš©ñ<¬sË�µúßk)àLÃÙSztWé‹™rQ^Ñ ÖÍóâ10‡q ùê¡s­?ϸ¸$å•׳¤¶¡š“'*t?Ë* ùxqgšéBŸBCCC3¡­3 ÍL„¶Î44443Ú:ÓÐLŽ<!+1Èã¹Ìú|ž²hf"/¶uÆåó·þû¬OõzHƒ1n⺗’?,g­_=Õ*ü¤µÿúZŠp<U"dáûÛæH¦ZWÓ“õ} ‚òÞüõ{¿þݯ·dO9w3 FôKÿ¶6q½3ÿ¡°bÖüÛª8öô´µ÷qS–esøLý˜²þ!<¿Žðå…®:\¥"\)(»g›¸„KdRü‰#jRo®›j¶¦K_öÚŸØósŠLOÖ÷ì¾²ý¿K‹¶å?i4�€,Ý·ŽtÏxY|_¥ÂÑÙ®ýQKy~á'Ê‹lqïˆ(¯¾òr^bt˜GY¥ !…ï.Ÿ-"Üï¼7BÖš;Ï÷'nÕ–¢FÔ[ÿ´(]Ù¾ÏoSÀŒ)~/¡ë@£Ñ‚15øøD«—$¯Û”áÃbq†JþþyÙƒ-¿0ATẸ)Ÿì¿yääA0¢_ú—ø¶VYˆÈ•¿Iìüð`•mbY˜ 8gÙü&å²öÜ:w¶|xâ­1ÏäW·Ä4ïþêÎ�"`á»KYgvžlu "ç/TŠ lÝ¥GÏWk&Yæ R6¾æwûãcÍnÀøI¿Øp÷ãcÍÞFš�� �IDATn „ʂ¼h!Nºm7NkœøÀjÂ7ÿíÇÁ/J‡ÇJŽ S^Ý$*ùÛ…^aLáò¬#cÓ…“�&Í[‘«â€ÜÚº+'¯u[&5LiB^QN˜„M¸ôm×O\¬VÅmJ s]—¯Ð’�„ä Y˜gÚ¦u>­5XHØKHô•9Ñh �ؾ«§É1³ºµzØ/(Ù~¦—†÷ì‚ÅÙÁåÒ6\<v½Ý„�palÁê…¡l›q°~à‘í¶ˆà¢_­ö¾ùÙ·¦½Ç¶èÝ,û­aq„ÂKèa«þö›½N�ìÉö"‚ò·e3;AÎl¾Ý&Iœ-×_Ý÷m ¦¨Ó‚"Ä)Ëó ·ïÞ­í3=mMî“»q5ûâös}$`²œk¸—¶Ÿëƒ‰ËœÀ9ËŠâeL�dj»túbÓ¨<Q„I:Ítx­3î)¨º[Á Þ!¨ª0" ;Ï~tšû›Ùݨ¸¿½$²×ÝÃâm *û䛯zJêõ¿9‹Üe§>>£²mÒKiË÷þ©œ+|oÞ£ÂdÁ¢Ò½;O;Ä™k6.ŠoÞS1ñ~¼ʸхK¢t'?Û×m÷ˆ.Þ²8£ï‹’ ÷e@ƦÚþÜÙ»ezD 2Zw¸€ž»$R{l××È7ÿÕu¹a‡›§³¦&-*Rôìÿxÿ [”²n]QRÿÞû£‡¡ ½ @H€ãð·ÕŽÄ/ƒZ‹8Q òåû?¹:̈(z}IzsË…nW¤å†ôžÜñå°›á“Z¤ôç÷4O2âÀ}R‹ðÊ?ÿ´Z²¬—^]‘ÚÿÙ- �0|C<níÿèŒ;¨`ÛêÔ ê³ÔD²(D!Ax€u÷¾"dÉ;ó“äÍ—89oQ¶û/UFϸ囓°F�¸<½8_±çÓ -¦È]·fáð§G›m¬ÐœB¿îƒ;/ö"iÆK¯1œÕrçv˜ÍVçw2á!žW÷í=ëÎY¿5+¢â@½ z²½HD1|™==fyuS¼{×ײßN`Õv†MQ7¦…­÷æþݲð¤¬ys°žŠ»·ïµªíÓyõLX.&,ˆ7žÿüP§‹Å.È ¶ÔéЄê=q§£™/d´��pi¤R8ØØ62ÔÚjôUFðŸýÈ£ ‡ÃÉq´\­TÙÑ£û×O„µ«®ÕB[ÛЪõñ“?mÐ' üƒMu÷z¬(SKu74Ük²Š·4×wK£¢D�#(.ÜÝP×O]çþ¶ûB§À5Ü; ž¼i}+Â8Á‘ UUå €Ô×V úDr&L9ô:—PÄcøÏÝüÁÆü &W"­ÎJÙŽ|òeé°ÀÞ߯á �@N')‰Žô÷dQC÷N\›Ì4`¢ˆp¯ÎÚ:½À­¾uä£/îí¨Oו÷Z8ûû†™O�šP�½õMÀ¥Òó€!÷—7·(  MUm£ R1aX¨ ³ªFëp©*êuÁa~à?_WWó€ À­­oyäq²ïòöí‡Ê'z]=¤®½¡Ï€LCj‡‡€‡MÚ^H§V;l‹C«6¹-6'›ÃbLC7¦›3»ºõæ·_²ûÚ<wS±òÙ;)<³\àv9Ùþ1Ê@1éëÏŸªÓQÓSošéðÂúθw¤R4XÙlB”¹¹Ùœª 󨨞^¤!äÔ Om»RÊj³#‡Ã޳ÙL€©?.[:gÓ[É£4 [åĶ�ÀÖ^×1ŽRt·La«ÿjˆ�Œë—’—ïÍA$â™fÕ”eÂæpˆÀüÍïç��Æ@Ú>.-³¦tƒ@*öæÊUU}’P™”åeÐè)À!©ùÙ&IOÌP×� Í­c§ #µxÓ¾¥ëÞ•3%“lŠsy‡u|K$Ê帿 !r9`ôˆ 0€‰e�åso)ŠÂ��0&‹…9lcÛÿ[Í6Ä�ŒÃã°•…oÿÓ¨Æx?— ‡ÍrXFã°Èéø!².ר8†߸p’ö?[…$)� 1ݘLœ´~Ë\9@_ß}à®öA<Ž+JÉLOô57^žövO– ™«Ÿ`æ¤lÎ:j/Ÿ¿Ô '§¥Þ4ÓàEµÎ¸L!aˆòÞþe.�à"ÃøÕÕÓ=,ƒ¢¦zþ›5z4 Æf³)§Ã�†: ÆÓ*ÒeµØ†nìÞ{g*oäè¨í\”)×Kà _j�þË2Ù×?û²Æ@2”Ë>È|Ffñ1×å~¶ìV›»óü®ƒÕÏÜ›Yu<Ü/Bfi;Þ¶4,$ôífÄ ›¿"Öyì«]­VÊcÖ†÷ÃÆnwZKÏ´–b,IdþºÅyý;NL|xe³ÚÙÒñíø ®HİhMg&“5AfÝ.°9,��Æãs17� ›Åj¯¿ø÷ã¨q§ÓÅæŒ5"?½ÁÇ4™F{¹§£“€ 5ß|ÒŒ�Pα# ˜^Á±isS¢øêºÛçw´Ln›�†VÆd>Ã"P¦î»§ºïžfz…f®Zµ ®ûpµý(ÍD¼¨#i”RØsfçÿëÏü¯?ÿñ{oý”á£3³ÜnÒC$bà,.{\×H7‰{‰= �‚ÃaO+.��¸WˆÒ@ˆ¢Â%ƒ½ƒ$�²šížR1�÷TFø=¨É'dQmžqÉ~ €)I\\”äó´8;Úøaÿ?{÷Å•çþWU•ºÕÊ9g ¡ˆ²„’ÈÑà�gfvvv÷ÝÝ÷Þû>÷wwï<³³ã'Œ ÆÆDÛäœ$@åœsèV«“:§ªsÿD  û|ÿÕÕçT8õ­S§+d&úÊ›ÚU“GkRŒki DÑ1Þ,›uO¸ Ý}/,2éôH$v"(qH¨;9õ¢Ã‰gü76�ÁõJ+^ãô¨tbŠ qd8=د°…D‰5r,—­“`{Ä„»“,@ðÂŠ×æñ�E-×>æH‡TݽڠØÀvI^±uEø#/ä±®™Yeµ[X°# ”SD\ðä»vÐDw¯6xA´ í‚3–/ÀŒJ¹>\ ¸^qa®÷–Iùæ¿ûî†$Ç-±»½î3ǶñˆBlF^¯ÓëuF+�@Šcg¸ª+Ž|¾ãØÕ†ÇE3�cÐyÎ"Àõ ydÃ��`ùf¿\á@�²j¤2•Aè1‹ð¬;Ý/Þ‹Úw wì¯h™(¤¥ õÊ­‘Av Ízf¸þ¶nÕ«ï'"‹¶ÿÆ¡Cu �™zª’Vnýûl›i¢û¾3­3¾å‡Ä®ßRàG‹Ãg£×·èžÓÿ}¦›$˜ÑaÓ‚ÕÛW8Û£Ñò£M€©)[³jóµFÙ=Òoð#'Ûß u™ÚÏŸv]^øÆ‡@yëõ3ã=Ë´5w:¼?~ꇩ¤ûk+'–¯}ËW¥–µT\ï\™½*_òÕ…~ ��­d¯|å7IVkï¥/¾oë­¼½pÕÚm¡Y×Ð�r!�iêΞu.Yóë 1&Ió¥S|_*ÒªÔvNTM¿ž1 Z‹ýÕ º§ªjAéæ-aª‰ÑšÛ×ûK²^Z$ûªr i fék¿â"�ZÝ~ñTÏ#‡{iå±Ëù¥¯¼•Å›¢ûü±z%óˆÎi¦ºöµÌ8³ãÕW냊·˜¤élîTyOÖ%«>veñÒWßÊ"­ª>[m@�æžkçCÖ¬{û7Ö‰þÊ®>«'y7‡(Ž@Àgÿh]•·×73.ÁÛÆì0ŠÚï¿™íĺ–[u±E[ÞŒV©Çzú$Àzôz°IZ[c–nys1 @ºþ«ç[ �ðˆE˜åN‡=~Fö3Azåo]§v^zö+0l>xQG60 �€tKyåâpl×`¾lT…£û¹xQG60 �€o©èZ¶äwŠ­é¼tlNWcؼ†G60 Ãæ#<²a6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðù§3†aØ|4ۧﻸºýMçÃ0 »×lÓY>.û›Î†av/<²a6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðùh¶Oýq…y‘ï/å�Àÿ9b–¨ÐO2†aóÅåÛ?q"g±X¥Tüˆµþ¯MÜ0oRìHDùRgjm?bÉsGØ%lþ`ûêt·ñêÖqæ'•¹¡ü ßûp}AÑY;¨Ÿ¿G8vȲíäu=€rŽ*Þ´nͲœEÑÜÑú~‹oúš×V/_’‘è¡mm•YfU aš³þÕUËŠsÒ½´ ïåc4 JzÖ3d—°áïV‰ºçõJÃ0øIúÎÙÑÔ ê‡M�ð/ëyÙÑTYËì÷­;ìã_þí’ �!†±U²¾†—nôk_¤€–{dj8Ù[Ù"™]Œý æÀoɯ—sÎì8Ñiõwhe_k =Á��°Cr ¢m·¾þK‹D:Æ-½4Í¹ïØŽ½cˆ°h›•¤[ƶmžU=Ò`ôI_šÈk:úé-͘)?‡f¶ê§=¾?Á‹Û¸=°vDZ"jÃö º/NvـĿünúø·ŸŸ} áSŽ)ùiñÁžBií:÷é&= „ùKòz )‹¢§îÂé›}_YØÏÄOÎ.ãHUÌé�¶0,唵Ÿ¶0dRIT&¶ƒ‹‹wÌâµÎÌÞ=7ä?Ï|&¸!™…¹QcÆÚûÒ!ôÜöTv@tÑy¬wöÑ �Œ²åFùä?I{gg–¼¥C¢Ñ!� ¼œLƒm}JíΔ{\¤CïN=�O(hûÚ†Ô:� ýÆÕ¹/ÈóCº¸‹Õcã6 Ý=ÄjÙøc—Ô.´dk‘sûµÓ{OÊ´’@F�¤kúŠÑš+öté J×®2|ùuüxÞé\šHyˆÈÿ±Ï4ù¿9iù·×x¥‰ÔdXÏ3RþÕ‘F圱éío÷°�Ç›rµÀoQþ¢è�WgG>i™i½}ñBƒÔ �@Úû%-NOóòIËÄXWÙéS æ{Ëcy¦nÚ’ãGW}{à|¯Õ%&+79ÈÓÕÉžËèeõ—.^ïÖ2� ðÍX^˜"¢t#—[¨‚âªqÿŸÏõÑ�”cHVnv| ›ˬé¼uíRµÄtß¾D¸foy#OØqüû¡ ¼Ì1K7RáìåV5 ÀõŠÏˉ ôtv´£lY÷í«*‡ „0ªdù’hA”þý?–2ªÊÝ_^™,Œæød¯Y b™Ç;oŸ;Y5lB·îõ?}õá]eƒ÷f)Ï3¹$?5ÜݑͣN^é2‡­ýUÒà·×yÙ%éö='?:ÒÁˆ#rŠÒbœùÈ è¸vè‡Ö �Üà!Ö–£C6��žORÉÒÔPW®EÞSu©ÏueÔðŽƒÕd—°áÝàæ]õ®%¥ ¾dó®pW½Wlúþ¯×¸…/Æ{²X>oÿK‘¥µì/6%€Ë%7ÿ}-¹üɾ› °N+.ˆtåÙu—.”wj¦åÍí>ÛCûgm]“âÍf…¾õûtºï䟾Sç½¹žwéÏG;m”Oá¯ é •dbF\€mRôT\8us؈�€í–_’áeϨû®ݳ1žOjAAZˆ‡€™j-?}µYn`…?´N,�v Þ ëþ~Я 3È™m‘wÕœ?Y9$H}ýÈ®/ö”¡É£×¿Sd9ùñÝ�¸º±åÝx®®lyïãú¼¤gjV@ÿÙzîk-¤ktœËÈõï«´”U§o…½ëÖP6öóì„`÷xÞé¼½€S×K—µÐ.ã�À_NZêzémùœÓ5OÝ}� Yl �ÄÐ�`fì}\Y#´S@¨RÑZ›òó³C6Ê=ýåu9^,F;>2ŒÝ…”YÏÜYH§¨³ý8Úö¾»Øk ̱·£EÚÓÞÇv ËZÏÖ~|¸NË*]™)�³FªãG.["à Ÿ,…훿~]šØ6ÖY×j…ÅÄ—®·³î>\ÿÐ~Ip–®õWIÇå&_ÿ”5Ëu²¯+äÈjåyxóôýC&¾od@lÑjPï8ÖÉãQV+Àh‡ZúýˆŽG��Ò#q‰óÄè˜Òèãæ“»J'ýôì8¸ºÛ³y|7îIgÒ#­$GØqø³£2š'òòâ) (�Ò5uy¬õê·7&tj /°ô•Rÿá«ÇwèXŽb–jjÖ!±!ú–½R�aÅ/e»´ŸýêÀÅ10£tq_:<] 哾ÆIÒpúà­^Î@àä_ugwÊ5Û·†4îÜwKƒ� L¾úò ßî8×O�Ë#㥵¡Ã'Òg´ Í^»v•u××7Æ�`EG’]Ç{,[ùžÏ&6ý&yt÷îk€ ¸o•:'—ÄUž:þå1³]pÖêå%é_^f8ÁyJ}Nø¾[ÏóI,^áOj%“_%­\Ÿ¤¿|ðËÎ ¶oÆò•›²5Ÿ]´ÂƒëdºvHv®áÒÉ—t¯”¥kWévhoIt½>&c�€dëþnÀæýöÖdIR$òýç�’"Pà?Çj¿ûøôLïè$Å¡¡üáûÜ­ïÄzÛÑꡆ‹g¯¶kh®«»ÓÄÈðÔ 9ÒŽhr½<¹0öL; ö"x®é¼­€í!"ÿpØ�¡^Sóíºhùï·øÛ Ø».ÎéŒyéµyK ÏÉEħ¹·©G‡�l×>ýä"B@œð¥ï¿íèãåHiüã“<Ù o;öÙñV=�Åf!ëÔ*@ ÍñÎÛTéhºôÝñÆÉqR¤­;úQ-B@”Xµ}kž§§¯Ug Š ŒêÖþ=¬¼ð¥o¿3õÛ*/89AL1£7N]iÒ9D¼¼~AP\ˆ]Cî¡x&Uv”KǤ×ÞXä%¾U&gÆoíýS%B@l_ã¶WÓ½}ÄTÛhíÙÆà8!¡h:w¦V�@9� ¿±÷ë2)-L{ùÝ"/wbH=T~`ÿ¨Ø8Ôm¸¿JŠ$Æb4ÍFc‡êΟÐô»ÊÉN±]|b »íè‰Ú>+�(Ƨ§±ŒöW¶^–1�`­GÏ·J­�çÊüC×ÙÝ­ÅÞÒºïLÕÔÓ,[Ë?%Þ±óÌ¥Öq€¾éFmÒñQ®cc pƒcC¬-ß>qt™ 4×Ê;d ÀDãí¦´W}=ø0lŒ‹äw_¼Ø 5"д_/ ŽÜè��¤{|ª×Hùg £F�è,«êOÊ‹ñ½6Øûà:¹‹¸u¾iÄ�—ÏûE¼nßÒÞ<š›îV&“2À õ·öë³2Ö²ÏÿP.JÛ´^pyç¥1Qæ«k¹çw^c&Ë~éìâÌ Zè{éÄ×¥´8®pÃêeÚßV!ŒãtëAF½ììù$qçùçîù¥³=ŸØÁªë¥k{ïkVµ½L]/½!ƒuè†MgœëhÁyº#ƪ“õwV—]­@�”82¯xQ´ŸØžC��MQg>´¤¿2³h« �ˆ©’¼²–F:SŒ´âÌͱ©Ã!ðK[œ—ä)äQ$A��b( H¡HÄ"et`Ø �¦)��¤ÐÅ…C¤wÎöwr¦ç‘Ø x(‘I2"£@74¨BA®BgG”N¡i…yqîÜ©yg(Öã6£ê·€V®4Å¢�Æ"ïn“?4­´òRoéæßDöÔÕÜ®lÔLei¸t꟤“«ˆï{ •Ǩ8/Yã9%3Ã4¶q¹š¾›ÎH5دœctö®®|¾ÇЧ¾ÈâÖq>�‚à˜PCëW’Y }!¹lºbd±X€¢(‚°9³T=2óÔú·ÉeJ4™ÎW!;¸ôÍ¿+žüˆâ²É.;�Ý¿Nîb´ åtGš–ÉQ°HHhÚÛ†ò“"ÝoH%¬ÀÈ�ºóøÀTb{ú:Éš p<¼ìÇ[).—²v]9_7l€±úµÉ[ƒª»'羕…b¦"°Ÿ™ç—Î2X|ò‡§zs]£w›êG'-»?àoÈ`ͽûL÷ÿã‘FÓ=!Å)«KS½ u[Ù‘[Cz·”õ¥<�¸›Ã3ýŒFpD΄Šl„œ˜æ£À Ë[[-0T¿Þ*'ÃKפyÞ÷¥©%`è»ÁA�2÷_?V+^@dRNÌtÔ!aj'#&“�¢‹×e„² ½e'nvOØ/\¶"Ñé kÀb¶LÎ<ùȆt}—wï¨ñLLKyé׉‡œë¶� Úzg b¦=ŸGźÖ¹w9îýçýuÓ¶¹ÿˆ@�0Êê¾­¸aÈb¤ì#¢TmWg5ÌŠýðJ˜<ÆÝ3Ñ=Ó`h9¾ïÊÐÝm†ÉÓ©{×ɽ…T€�@×ÕÜŸ›í^¦´²v±vñ/¿UD‘$‰Â?ˆ�‚$Iþa$=zåÓ·Ô3Í;mµ2VƒaºRdÔë€Ëã‚Ao�¾@@ÀÄÔ¸¶LzÜqþ%xNéì)"6d°ÎÔØîÜ{ò—“w/<èeÎÔØ˜ài"7Š@Š–òê c'd±§>@zÕ„ß3(È®¹E‡€åàlgRj&?¥G¯}sxhÁk¯Ä‡—/>R«D.b˜¾š«õýÊ3œ;[Ì„ZM#Wާ¿'»{ØÊòðp›¾å’QÉåäÊqàèú;&G|I®€c3#`9z…øräÝýò©Ÿ! ޝŸ'«gˆqô ÀL(&Ha€ ›@†®Šò¶~š’Ĺ›”!@�,‹x +5ÓJຄ¹‡»†¾Â:1Ðxy ­§øõÉA×zÚïÿ”V+ÔëîÎÝ=Ön±‘N}•ºé ˜€XW t6��ÂÉÉño:EZ¹Âêè&d´ý÷ÐN‘ ¼eÛí||É:µÚ&ôpáÀ¸ �€r;0 �`UÊ'¸b¶¾e¶×ù..\�Xžžn„ºKÍ�2t5õæ†z lÝÇ­€¬õßü¹Þ~Á¦7=*?:×'ˆy»[ÅŸï›Ü3v|­r™R "å“ç'bg¤iÑ1(ý¼ Ñ�aïíå4!‘šf*û™yNé¼½€ãÀ'¿¼x·Qíù€‡�^ÿhê/_^´”$ ¶pþ÷aó#ʘ%4¡T1ÈC•“i‘ÚG$±§{N¶þæ&eTŠ8|ù;¯')lWCù®¯*'CiÆ•ÚÞkgj6&ä¯LúªJ£RY'/ ± [hõ‰MM÷›Œ½-]¦(çÔW6{¼½œîìo¦®[Uã¡™®)/ovéÕ‘?sÙÎýµ»rci0¥ºñÍç&’Ò9iã¯|e6G/W6X†šZ•6«JM#¡ 8½ ͇åŸ+¸ÛѧU 5±Wök›B ¤ôÊ·WÝA¥|³6nJt¢{OýùpýÝ¡gBçj‘ŒkÌl7oW®Q©³>”óú¶º¶œå¥£ÖZÂÁÍÙ6ØCÅD z/õ¦'Öw6õä/ÉËë=~cÄl盚Î'Æ,hnl}·ë”[3Wä.×It„ÀÅÏ›;\׉"cÝ%uGg<÷˜%k_C‡ù¥ô‚8åµn=×{Av¬1ù‹-©»5²pñÒ‰‹U}†'ôvPÞn½w …ôHÛòjôðÑ=úl��”ZaŒüZŸŽç›^MuŸïÐ"�0u·ög¤§ðlí§‡§ÇC(±›³b\Î�åê.–Oþhx%ººM÷ÏiƒJ!¯«ìO,.Y<~®j¹'å-`wî4cjªMÉÈO’•wéíà R½eu祸ëüKð<ÒÙSD”$²]·>Ð/¾· !Q¡C×­2Ù_^´<[÷™¯:Sî±|QHXršËpý©ïz²V§O~d¼¼ÿ]”àâíÃÇûêGŒèÞU€Ì}—ÎÕ­OöËZ‘5¸·¼üLµSáÏ™Ncm‡Nz¯^ �€ô-§;³ “ƒ„b]mYC`a¢ÇTŒÚ†¯úÖ•™ìC1æ‰Ñžf‰($J“_*ÓMïXŒªñf« *!kUõ×;S­@š.œõ^‘ê<ÜzuwÌKyÎS“KoŸ¹åµ4ÁOäéAŽô™°áQ͘Tkå襊î[±óÍXšëæÀA­´£òûËýôÃ-ÀÐuö› ‹—¤®}»ˆ‡ cu祶 HNÏéî{›º¶ÓKJKÞXı(j*›Æ<=æ¸DÞ<xÎÏÍ{9ÝØU#僤k\¤¨ÿvç³Ý|aî¹|ä|AqÁË¿^AOô7\¹Ø$žjHYuì�ääf¯ys°LHº«Î=ðe‚ §~s��`Æên«‚—½¹Ä…kïºyädËÔ¬™û›»Š6ÆÓ·ËF¦Ã™¸9£ñv"œ<]¬²¦û¯ª§øåoÄO;QóõGNd/.,Ú”ìHêF;Ê\j7��RלúÁ~IÁK[ ¹6uÓñ#U/Ô=­ØS#DîOœ(84¬§«ó©ëø×õܬhjíõÒ‰Ó�� �IDATÿ£Ÿ=Ÿ8úüòú™»ÏÏÁ=£ ì%¿z%ÞnìÆÎ×e³Úm×ì-o,v3Tüë©§»Î{^!]Ò¶¾åWÿßkµ?õ¬ü-Ù%lx/udÏç7f·K|齌ñý_úlQl^x}g¡5 —2Ý×›¦5 Ñüÿ9š°[¸qs wlL­7Ù¸^aö`iê˜ëMŠóAg‰íê"d ú_ÄXè#6aá9ÑzíÁ[³1ìé=tþè¤åß^ãn+xr:KUÌG'ª‡H̲*†Ç¬ }Â}yXªá†›7¯Tþ’N7)¯¤<M{ÿ¨ÒD‰‚³óC™ŽO¾ùg‰bó8aøâ É­3c8œ±ÏóHçÎQfíü¬îl² –·«ü©¿ŽÆËöü[Ù8?ϲ2N‘ùsœ,¬í¸zø\ל¯UÿY ÅI¯¼™ábk<y¢Vù‹\ØßÊówÆ0 Ãæ ¿Ã0l>ÂéŒa6átÆ0 ›p:cØìñ<â2âýíŸËÅϳ.l>z±Ó™ôX¼ýŸÿuS²Ó=-˜àÇo\—ðàË çŒM+#g{…Š]†x=I8]*Xôþ‹Ä³]Ws«ëYPþyoÿî½ßýËï¶eÍzîæVäºÚ?Ó5ÿ¶8QëÿiM wî_4õ ó“–gðŸ®â‡Úá´åƒ•á3µ–g­ë'÷üv„Ÿ©zÕ‘®áBD Ãê«4SW3‘bWò¡WÔ²\ÜD¤r¶Û/í2=ôÌÏYš[]Ï‚¸òùŸÊüJÞ(xµa�€ôGžúësjÏP—W„§¹¯Gaþ)¯ñ{~;ÂÏÔOóNîé¾°h´ÕÛBý,R �PÅ¿^“è, X˜˜š–¼À~¬¾g¼˜µo,‹¶w ]¼(%5‚ìkÖ#`G­ø0Ÿ;H%nx¥´ ?U,©éT2¤8qÓ»kóÓ“¼ £“·¿‘¢àä`ŽÅ=¥¤47;Ù õŒè`E®û‡ÅœÚ©€ [ýù¼Ú©m溇€œukV¤$'EyÐÒ>‰~æ[WÇÄÍïðÚšFÌ��”oÑû¯D*:•È!,wý¦âœ´ä”næ¾1ÃH§Ð„ Kký Ád_ìdSM‡‚Â.áµwSÌ5 (aÄ’Uë—g¦¦ÄGˆÃ½ò™/O¦¼ ~½Æ»¿qP?=G¤ÍoGOT÷j£J7­Z’²(5ÌI3Ð'7#�Â!hñK«Kr“S₪þ‰G?–íW°v}Ñâ¬E‰‘Ζáþ1¤kTº2øå¬.\œçiì’�%~¨.Â1eë–;vDNFZVf¢—¡¯KnB�\¯´ /­-LŒ q¤<2V…©k»5Xn —¬ßP˜¹(qa¨@Ý7¤²��)Œ.zåÕ¢´øpâùñÇ*Ûd“wP%·=Ú‡ç|¿#¼äƒ5A”{lzzjVz05Ú=¢e�ˆ¶×Œí¸^ âÅ6&|ÉÒ%‹ScDšžnÅð –0lq鲬@Z+—k,»QŠtÏݲ%\^Û£A@¸foÙ¡¨íÑÁKÞ_îÃx&äf-ÊÊ çHº†44�ðü­ÞX’›–˜œ&ÒN7ƒ‡š÷#v:l.^à¾3é&­¿]+ØêP_«A@÷ýø4ÿ÷ >9X«›ž™š¿Û˼á_ýÙ÷mwoh£U*½÷¢%¶êSŸœ‘š¦žÜË(jöÿg \ü^Îý•¹ˆÊ÷ï<mvN_¿yIlÇÞÚÒ;s]?²xi¸òä—ߘì#Wl+Mþjæ÷Â!M{ÓHîÂPáíjÊ/" ºŽ÷Z È\¦8¶ë›^äUðÚÆÜà¶#s¹§’&,)ñ<ðɉM”´qcIÂÈþ;g÷bÔr•ƒ¯‚q‚' lr;©Çˆž_àÑ{೫2VhÉ›KS;:/ ФgJnàÐÉûd6–{rI„Ý`Ç#Î8H÷äù‚š=_4¨À5cÝk«’G¾¬�Ë+оâÀÇglþ…o¬Möo8ÛËÌTƒäâkØýíA5¸ôW‹<:."çļETõî¿ÔkcVnM Ú�©+òìj÷~Q« <s7®/’}ñ]‡‘”]ì=phçÅ!ä’¶îu–¥áîÜÙÌ:á±öH "¼|ùç÷<kµ[°æÍ%q{jÔ¤ÿ Ûkæ¶�l_?âÀÞßS«ßÍL¬íº"yæÛNC7ì®u IÈÈÙœM ÖÞ®¬ê7Í%"}¯~»ÿ¬E¸hÓöŒÐÚƒ-FÆåÇjÎï9Üg¥DÑùé~ÂÎf%š±yϼÓasñBŽV��é!”´uOŒuui¼"Bížü•û!³ÙÂ3w^­“šÐäã“ÇÐßÜ¥gÀ¦híR¸{{Ps©‰ò Ð6W 0ÚΆn~PˆÓ£V¼¾£eÀ%<\D�°ücBl­Í#4�Ý/ôY�¬²!8Ú æô[Á ó”Ö×IÌ�´ª©^âêÇ›±dV)­B‘€å“¹õƒÍþl¾X ¥1µýl_¹Ì`‘óX�€,Ú!02ÌǑÌU¸ö¨h D¡!N}MÍ*€m¼âèÇ_UM=²™–5× XF†elG�š±.� ‡ZÚÕ4€U>¦88�,YG·šFÝ^ß=yC*! rè«oTØ�¬ÒÚe@°7¤ØÛËÚß1j°)Z:¥÷ÞsM_þüóÃ53®f™èïµ€¡¯_áæåAÍu{1# õ3€qdH.p|ÖGeßLã]7øæ³Ýׯ<r·¬ˆxò“îû6ÐÊžÖa3�ÒŽ›í�ج®OT„Ÿ3©ZΟjV2skÞØ\¼°}gÒ-,B$©ëÐ"F×Ñ¡K޶¯m˜ÛH1BÈ"—Íîq¥ŒA?y^†ÌfÉå²fÿÏuY´åÄÉ·[P,cÝÌÙ�`ìiî]¼(Bt»ZjlùzŒ�‚ï”—ëÆC4â Ù:é¬ëžÄåñ(¿‚­ïç��ÁBŠa>3 n0J¹ÚÁÅÙï!­¹ºpœÔr„C`rAV¨˜M3 pf7� yűS–¼bËR;}Õ•3e}º™»}$_À3¦Ç «ùÎÙ;²Z&_îÂÐ4@�Ì\�0Ö©î-Ã0��Áæp³q²,Æ 3".�<Qüîo'˜d‘#|6<.ǬŸ‡Eó8 ‹L¦É5‰¬VËdÛ ç´½³™¾o¹æˆpNØ´-Óƒ e×w¼­¸³lß#<)=5ÞK×vU6çg€X­SçgÌô‘®áø vvráÖ¡e´éòùK­*zNÍ›ƒ5I׈P1K”÷�@R, ¶khÐ=é{`˜Ù¾ƒË™|5 Áår‹Ù �ALvX}÷ŸÕ 7ŽÝؽÿÖlŽÈÜÛÔ·$+ÌCå¬nÝ'G�@ù¤-Oç^ÿr_£šfE,ÿ ý 3KNu]îÌ–É`´õßu¨á‰CšÈ T“!Þ¡®úîã½ÁË‚ýhJÕ£CìàÅ«¢-ǾÞÕe`ì¼ü~ðÔä6uWù™®r‚#+ØXš7²ãD׌G-Æh0q]øS;-ʼnXz…væÁ™GÕ5ÃÌÚ¬Vàò8�V�B`Ç'l0ùbTSËÅO÷Ý{FMZ,V.oj# ìævòñX—;™FËc,fë\·×³BêÆï?ë �ËÔ+ØNÑ)™IávãÍ•çw“èÍ€ §Þ¡Æf?!íÀíS·O³‚Ò׬É8Ò`šKóÆæàE=q žÙùÇÿ¯?þûýñ?öWj¼#B&¯Ì²Ùh{‘ˆ @røÜé¶FÛhÒÉÙ‘ x<îœÆ%��€t Œðæ�P¢ð±dHB ƒÎäèâÌ #B½ï®É‡êbF»{c½y�[_Z’àþ¸9°ô¶vÛg&úÊ›ÚU“GkRŒki DÑ1Þ,›uO¸ ‘ÔtȤÓ#‘؉ Ä!¡îäÔ‹;$žñ ÜØ�×+­xIŒÓ£Ò‰Q(&Ä‘áô`¿fhÀ%ÖÈ °x\¶N>nd€íîN²X�Á +^›Ä@µt\û˜#Ru÷jƒbcD�Û%yÅÖá¼wƺff•IÔnaÁŽ$PNqÁ“ïDݽÚàÑB €´ ÎX¾8H@�3>*åDøpàzÅ…¹Þ[&å›ÿî»’Ÿ.± §À(_îTÛKèÇl¯gm‡3clF^¯ÓëuF+�@Šcg¸ª+Ž|¾ãØÕ†ÇE3�cÐyÎ"Àõ ydÃ��`ùf¿\á@�²j¤2•Aè1Íûo³°¿ /ê5ñE‹­Õ§dS¯Òk8aÙÁ¦Æ™é¬à¬¥¹™ ѦžV©€MgÄæ$'D9©ÚºÇ-@ð½, Vvß=䂨 o¼R²(5ÖC ô‹INJK«ª»•¢dñÄCBQQf²—êúÉë}:nÂê•^”䮓jÜ„šú‰e¦ºÌ6ùиý‚҂ԔäÁXýífùãάÅ9-7\~ãTÔ�€´Z"$=?=24Øiüv+³(Áa¼¹WM� šãšP°8kQR’‡¶±}L©!BsóR£C‚EŠQ“Ÿ¬ªmœ1 ©]“K—f¥¦.$‡ªnw«52cåûçÆsë¯T éÌŽÑ™!êš«J«ÖÀÊ)Hˆóç÷×tñã2°†û%»¸â%yé ÉÉvýW/ÖŒ?j¹ntÈ蛵¬ ;3!R0rùDåàÔ5îÊ[-R+�)ŽÌðPßj‘šfª«iÜ!n¡ãpU‡‚‚ã—à4RÕ¡` ãj~\^in\˜›¡gTà ½5=F?6dòÍ^Vµ(1ÎÛÒy«a@C­–éÝÓ–e'…Úõõhü”·Ú¦^+N:Gd-ŽÖ7Ìýš Rœä©áÇe¦xinž,ïÑ2Þ^·CŽ×‚–k®³ñàÚ6JÚ›zGU¦ÙdQëùñ9Éa!|™y³†«»&@œdnª6" …ÁIÁ–æºa#mÐS¡y+ò2R’ãåå—+†ŒÕ¼gØé°¹ÀϨÃ~&H¯ü­ËàÔÎK³z}÷… ,úU¾fÿ®ÊgNT {À‹:²a��¤[Ê+ï‡ `»†óe£*’ØÏÅ‹ú« †�0ã-]Ë–¼óN¢5—ŽÍé*p ›×ðȆaØ|„G60 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6átÆ0 ›fûô}W·¿é|`†a÷šm:ËÇeÓùÀ0 Ãî…G60 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6átÆ0 ›p:c†ÍG81 Ãæ#œÎ†aóNg ðù§3†aØ|„ÓÃ0l>ÂéŒa6Íö ¢?®0/òý¥�ø?GÌúIæÃ0l>£¸|û'Nä,«”бÖÿµ‰æMЉ(_êL­íG,yÍl_î6^Ý:Îü¤³2”á{®/ˆ":kõ/ÔátIzåÍtª£Mj x¾+6m*)ÌYè©ïh“ÙÅ.[»qma^F8{¨©bv ÆvKX¹þ¥5…ùyÑœq”òj¾coó°~Ö3Dù~øF‚öEÚúØ/ÃOÐwÎŽ¦Q8l€YÏËŽ¦ÊZè¹cÿòo—Q�1ŒÕ¨’õ5ܸt£_ûïb„(<9ÆAR[3ô‚.á¼áíý»vÝRÎzµ3ziG“m̈�€tYX”':ºç›DZ´œðåK¢,å{v´I¤}l› ¸kßÊÓÝq~”—[¨:ÿå‘.#²Y£›(…nÞ®>Â9ãå—ì¯í87b—²a“°ì‹ó€ò[üîkÎe:Òhz`rŽû‚ÌœÔp?W{¨níÝ}iˆ–SpjfFB°§Mk¤—/TILóv‰±§÷¤ó‡Ë8Rsº†€íÌK9e-Ƨ- ™T•‰íàââ³x­3³wÏ ù‹šÏ,ŸäâœDÓöÚÙ§3ú)÷JÂ)*ÖSÚpF5§5n®º<<ùORèìd¾Ú©ÔÑ�@¸ˆ)Yc›Tûätå‡ÄëZöJi œœ…„äFۘΠ� ¯½(yŠEy^(Ww¡¢WNåê!Rô=þ ÆrO_÷òB]Å…ï. O¬4�ðS²Ã,§žPXí•–®)P}q²Ëòœ–�{~žw:—&R"òì›ê#üå¤åß^ã•&R“a=wÌHùWGM”sƦ· ¼ÝÃoÊÕ¿Eù‹¢\ù¤eb¤õöÅ R3��iï—´8=!ÌSÈ'-c]e§O5˜ï-噺iKŽ5^õíó½V—˜¬Üä OW'{.£— Ô_ºx½[Ë�€À7cyaJˆˆÒ4^n¡ ЍÆý>×GPŽ!Y¹Ùñn,³j¤óÖµKÕvm8q‹‹R"}„<0kÒî[W.ö8d®.N’‘õöÿ•…L͇þxF¿xûÖ ¡¦âÛOÏ Ó@ù•¼õjª½²|ߎKÒÉrÃõÍ^»&Õ_š¾[O]ëס ¿P?áSøÊº$þÀéýGî/ ì³ '¹Ù“6bàö¥®Ù\Ò¶¾bmÿHØŠÜXwCÅ®¯ÊÇx¾©¹y)!žŽ,‹FRóýá²!�.Q1n#Õm�”SD~ñâ…ÞŽ„~¤éÆmýÂ"»²ON  ‡J»aK}ý-ÿ†¿‰^»:ÇŸÏ#7ýÏ8ZR[6ê²ÀÍ*üàÿ.07|÷çï»m·øÂÅé1ÞŽ„^Òv빺ѩfC8DÅø+Ú.ˬ[S)dSË~ÿÿ”ª+¾ý¬Âí¥÷â¾Ø{}Ù%lx7¤ëè ÏâôW£n½rür«’�Ò!0sY^bˆmo»Ñd»glåžSšççÄ2)zn]9wsPÀ.aûÁÍ»ê]KJ|Éæ]è§(ŸÂ_1+Y‹rx Íhó•óµA«ßZF]üähûäF'=³ÞÜØôé¾› ¤ÈÝÕ «°�)ts5ÉnY×¶aY‹ˆŠ='+÷e¸©ëÂ«ê¯×ÆG&øŠÈ®±µS‚=ÒóNç휺^º¬…þp�þrÒR×KoË眮yêî3��ÉbS@� †�3cïãÊšé Bý“ŠÖÚ”ŸŸ²Qîé/¯Ëñb1Úñ‘aäè.¤ÌzæÎ*@@:E­Ø˜íÇѶÿðÝÅ^#�a戽-Òžö>¶[XxXÖz¶öãÃuZ~TéÊÜH˜5R?rÙ‡€©N¶oþúuibÛXg]«A_ºÞκûp½ön&’n‹V%º3ÊîÖn“›¿‡ˆ2#®‡±�2ŒutŒ™,#èɇtKÈ·—Kå:{çМ•KÕ»5ò.œ!8"wg—rs·#aâÎq ÌX‘®ؿ³I v.^î„Ú @�€}Páj×¾ªÓ_éµr$N]ùRÔœ:rZFÛ¹Ø[Æ'ÃŒòˆ‹tê­èÔ�é–±je¬¡ìàîfË}a^i¦Q7]Í}¥1 œü+#©8´C¹ì7ˬGÿë\ �DçÄ–m ï«Ó!�Â.bùÚlNÕŸ“n‰+V®/ÑñC§NQqžcg•¶ Å‘=Š¢7^–ÿç¡V+�áxßˉÙa9–+g÷^U. +V.]"8Ь'DIkW%Áí>«CΑùÅù"è™ü×ñ+Eî-§ö~?jE-Ù°j…iÏ �P>ékœ$ §^Ñê¤â˜›Ç&£…‘¹+W¬¶©ö\mî¦×„qÛ[M�@º†‡¥-íšÀe¿[# H’@oüv$I ·~›6~kïŽëã3m\N@ˆŸj¨+zå[)Γ¬­òÌ™ZɃC|{£Ðãhþ9z®é¼­€í!"ÿpØ�¡^Sóíºhùï·øÛ Ø».>¶'13Ò;kó–ž“‹ˆO!soS؆/}úÉE„€ 8áKß)ÚÑÇË‘ÒøÇ'y²Aßvì³ã­z�ŠÍBÖ©U€šã·©$ÒÑ:té»ã ��ÒÖý¨! HJ¬Ú¾5ÏÓÓ׃ª³Å„ Fukÿž V^øÒ·_Š™úm•œœ ¦˜Ñ§®4i"^^¿ (.Ä®¡îîÙ:Á,ÊžÚË·¥zÄb6ši½TáìÌ›è¼vâæ8�¤×\ýòÀm5åU°yk¦KPl°]³òႦãL¾œñîÑ{OQ’"ÙÌ&£ÁjìOþ€ ÕûÏ×i�P^)žò»®¶ª€\>6õeÊ+:šßs®Ûˆ�(¸x±ôæ±Ê~5Ж]ª ßš|·–{J›õ5œ„025\{û³šA5¸y£3i}T §³Å ¤Kd´ÛHmÛ“4DÌ`幆!#�h+nõ¥yºÍFQDŒ¶nWe¯‚Ðל«‰ˆÈ›œžžMû¯õ*i�}ý†„Íq!µµ4�aoiÝw¦êa3‚˜¨¿v£[Î�hn_¬ˆx+3Îçʹ¶˪¨P^k“ Hqx„HÚЩ²ªOþéO§½rÞ\¢?¸§Zë»ø­|Í·{ªÕ�€ši8¹:s=¼£ûÏùë ×'sýªu…ª'úî9×#¢Åq:δêf·R±ËóKg{>±!ƒU×K×öÞ×Âk{™º^zCëÐ ›Î8×aT‚'òtGŒU'ëï¬.»Z;�(qd^ñ¢h?±=‡"��š¢( .Î|hI¿��h« &ûŠ��^YK#)FZqææØÔa‚ø¥-ÎK òò(’ ��1¤P$bÈ2:0l�ÓÀ€”‰ ��RèâÂ!Ò;gû;9ÓóÈìÜMgz´±Z“çZ¼ù½´¡æׯU=Ýî…Ô’a °õ2\NBG¦yÆÂiÍh{˃_·õUœk[UüÎÛñíõ·+êÚFõSÙÍŒ÷õOÏ/ÛYì`”ŽjØ2¬€¨HªëD¯erg£T2= 3!§ïNoi³FŠÝÄl÷ì7ßOŸü‚ÅcIíx˜ ÷ØHaoeÇ,VÒÈÓaF[¬6’$‘HÈÈ$²©eEzù¸��H‘›˜# xùïã&[(Iq¨ ;;4�H5Ø?Ã(1RȧGÝ‘A*ÑqÝxtsK«é¥è`As‹Iê,mž:vÞ”ô¦vn0Úùà:½ð¸\×»Ú£d�Lýׯ÷Ä/ ó¢úú¦°<ÿ¬uE¢æ£:Ÿé¼›·ž_:oÈ`9ðÉ?žLFè½ÛÔ?:iÙýCkîÝgºçøïÿ¥›§¬.Mõ&ÔmeGn éÝRÖ—Fð�ànÏôKÁ9+°=rbš6j�',oma´À<Ruüz«œ /]“æyß—¦–€¡ïvH ‚�@æþëÇj¥Ó ˆLÊû»y¶Ñ²ow Æ¥e%/ò]Xò’ŸÓ/. ?rWžo6›E<ðIÄtÅ-|æ«iu멽=•±))¹[“nÙuÄ�Èf»ÿ Í'8.ÔÖúÃÐôö"F÷ÙC¥Í „màâÎs]w›mÒ! ¼cc=ç{fu §™Î îk sçPBÌhå7‡›î&?c60À�Ú6sQpw›�èѦvÝæˆ`~ÛhD¨XÒÔ>.Y¯mËu£Š@>¿O‚ ôÒïS˜îcŸi4ÏT0²ÙlŒÁxç Ú 7sì¸Óµñ²7® –œ8|¹ß4Ó×±Ÿçt¯ §ˆØÁ:Sc»sïÉ_NZþrrêwæÎQæLmCËSô`üÌ!rs¡¤j)¯î¨,,öÔH¯š0# <ƒ‚ì��–ƒ³Óô‡@^ûæÓƒõ*$/)^èL.b˜¾š«õýÃ#LïÌ„ZM#‚ãéïÉ�–‡‡ÛôjdTr¹lŽ®¿£½«£½«£spXª6#`9zED¸p—ËhúkÏíÛùù‰#PÂ@_! À0 �Áfß9b26€à;:²€rñöâ=°z'? ÀöõåM¨'`æÂïÖþ dVôUŸ9øå½¢”~Ÿc[ÕJ=ßÃÓᾪùÁ±¡Æ¶ÆQÛô4*ÀÕM0}¤ˆ„ìgÜ’H)Wb{£V£þÏ`e€åÎênê™1ÑfW²Z­¦\Ü\¦·G(š–b&dJF,v²hïVª³<þÊ>±›hjöžÞ“JmB@KÛZ'|ÂCÝBBœGš»5ÉË÷ýØ[6*»±óÏÿþ‡¯®Ke7¾øó¿ÿ¿~D4�£”ŽÛ<ý|§7Wä,Ði4 �vA¹›Ö ?t®sþ^<ˆ=³çÔwÞ^Àqà“_^¼{˜ßó¼þÑÔ_¾¼h)Il/àüïÃO¿ß��šPªä!ŠÊÉ´Hí#’ƒØÓ\[s“2*E¾üד6«‹¡|×W•“}3¤Wj{¯© ؘ¿2qè«*JeAž¼€Ä‚l¡Õ'6ADLí ÆÞ–.SH”sê+›=‡ Þ^Nw¢ÈÔm òí��VIDATu«j<4Ó5ååÍ.£:Ràâçc.Û¹¿bWn, ¦T7¾ùü2·ð×K½ÆF•&–‡q\®e€Q+Õ r'®Ýæ©!UU_ë3"WAxΆվjÇÀ(wòN}AyælÛ¡f‰=Idîlì6¾K.ÙÝ­ýÂÝ¡gÊ#|“v`T©µñ<½„,mß û:-i¨_˜±$KyµiÌÂ{ðUí2ÿ˜€‰¶kw. %MÊÍy %WÚ”¤8:;Ù‹@²gÚŒŒ²µª3eé²bù©Š¥•íä 6¶4ŒùÆ…2­ÇŸâ'Š;%+Ú›FR3 S‡N5ÊÇàœ$ú��@ß^Ý”½¶pUªõJ»ÌD9¸ùz@_]—æî— ~ÄÊ-Kì*¿Ú_¯��§9é}ëÆhçØ‚t¯‰Æó#4�0ã­Íêĸ ÛhsÇô�%vsR¶+`»º;(š a³\]]§?²h]Õuº+Œ—º4‚àœl_eC™”»à¼ëÃuåg›õž^�€ƒrLcÆ9ýsó<ÒÙSD”$²]·>pÓö½½+‰ ºnÝÉþò¢åÙîífƫΔ{,_–œæ2\껞¬ÕSc—ÖÁËûOÐEi±.Þ>Œq¼¯~Äxß•ÈÜwé\MÐúd¿¬Yƒ{ËËÏT;.ð\é4ÖVqè¤÷êå¡��Hßrú¸3»09Hè!ÖÕ–5&zL,Û†¯úÖ•™ìC1æ‰Ñžf‰($J“_*Ó1È2ܧö ‹ "“f´ñêåsÝf�P7]ºä·,3ÐÙÓƒ“šh°t^;VÆ/JöóŽ·l;ýd®Œ¿wQ•Uç*ȸì8ʤì¼uñT£¼ G¬»µß»®HÇÐÅù.62«‡:.|[Ê<|6EËnþp„ÌË+Ý”aOYTƒ×J\â¼Ç/Üó -);v’W”ûê[E„AÒ\QÛú Û��éš"ór­Ý¼ÄŽ´jåý·/7ñƒcÃL­_>Óí¥Œ²úèq»åy«•Î2ÈÛËËÊVøL~dê?ÿõ SaúÒ×3íÙŒA>Üp±çÁ¯“yg Y;+ZYëÞñ±íhÓ±ï¯M!!EK›*7׳ÿüwÓáL:»:kär^nb•Lö@ŸœôËÛ¾mê×I°õžølÍèµýǙҬ5oçqmêþšGnÈ`ù&Ä{ðØDá†Ð;«ÊÐxàOgzžîšTlþ"DîOœ(84¬§«ó©ëø×õܬhjíÿ£Ÿ=Ÿ8úüòú™»ÏÏqw˜•²äW¯ÄÛÝØ¹ãº _Ù�À _óÞÓwŸü9'åSøþÇómé`Aù¾·Þ¥|Ç·µ³¿©Ãîõ<úÎ"Bk@/e°Ÿ8¥Ö€<~„¡ç¿5ÂnáÆÍ)ܱ1µÞdãzE„Ùƒu¤©ã…½IñÇF ]]I}ë r;ú³!ˆ™Û+Ë+4„7p½G3öÔžG:tÒòo¯q·<9¥*棓óÿ–TdU Yú„ûò(°Uà 7o^©ü%?D‡pŠZÇíël<Ϥ‚dÅ펹Ýàý³Aqy\GrA,Ù| ‡3ö žG:wŽ2kÿãguI¦e°ü»]å?õ\Ì#Èfeûd—¦,·ç´A>ØðÝÙÊ_è(;´ôí51 ê®<vi`þ÷4°yìyŒ;c†as…ß‚a6átÆ0 ›p:c†ÍG81löxqñþö?ÝUŸ„Rjœ× ·äãº~v^ìt&=oÿçÝ”ìtÏÞBðã7®Kxðe‰„sƦ•‘³½BÅ.aÃ?¼ž$œ.• ,zÿEâÙ®«¹Õõ,(ÿ¼·÷ÞïþåwÛ²f=wó +rÝ?mˆ<÷z9QëÿiMÌSDi¬g˜Ÿ´<+€ÿ¤)gn‡Oæ´åƒ•á³n-H?Ü%wÉ)^äùä TŸÕCu‘âÔmÿó7âí��H÷œw¶f»ÿHíîy.× ã§y'÷„t "RæP]¥™¾aVìêB>ôŠZ–‹›ˆTζ`cû¥}C¦§}ÀÌÜêzôÀ•ÏÿTæWòFÁó¨ �¤¨8:0‹ gn‡?z¢çò·Ý_>v^žæ¾ÅìŸÈñp]ÈJø$DŠªUO3s«ëïENgÒ-4Üi¸¦Fl_]§E�T`ñ¯W.Q¶_½—à Cã‰çi‚³f[I”=+üß.FYýížJì¨ïÅõló)Éu öƒŸœè²‘âÄ[ÒÜ9ÞXÙ§ÓG� ‹7Æ„ºØÑ#7ž¼%±+rÝßÇvt¨Þ�@…­þ}|ßG‡ê3×E8d/_çÎf¬†ÁŠsgkd3?ƇpL|m[TÇî¯oM � |‹~½ŒsfçÉ.›CXΊ¢E€q ü»ó òG<j‚pHÚüºwå'Ç:l@Ø%¼ºÝ÷ö'Ç:l@ # ‹ó"…$mÓtß8u®M9ãMÖ”WÁ»…æC_•˦–œ&½¶ETö× C¨â•‰4íN^lÓ0�„CPÞªÜ! Ȧh¾ròÚÀ£od»Äå•d‹¹”UÕ}ýÄņ©w¬¢Ø’-ÉÎ<sÿåcÇk4�%~¨.Â1eËF÷®F"ÐßÙIH —=Ѧf�¸^ikJS<ÝxWƒÌ;…*ûüÌ ,·……¥YöcU´^<v½G‹�HatáÚ¢ ®Q#iåÜ·Ô%¿Yëvó˯*Ôs?"³=’‹Kyr@?tãø…F¹ííp¦MÓ �ˆí™´6;Ö[ÈÒ4_<z¶K‹�(QLqIf`‘Ôž>Q#yöÇ0”sÒÊ‚Bu[åíÛMÃÚ§zX 3ÑÛÏŠ‰ó¨½6v÷¤C`ÖŠ¼1 À"©:ºrÔØ^³l‡À Τ[d˜p´þv­ `kX¨C}­ÝwöãÓüß/øä`ígô"Sów{9‚7ü«?û¾ín“¤U*½÷¢%¶êSŸœ‘š€ �0ŠšýÿYC¿—se®¢òý;O›Ó×o^Û±·V=ãLÍXÁ,^®<ùå·&ûÈÛJÓ†¿*›ñ=pHÓÞ4’»0Tx»Z…€ò‹ƒ®ã½V 2—†)Žíú¦y¼¶17¸íHÇ\ît „ KJ<|r@b%mÜX’0²ÿÎÙÆ½µ\åà+¤`œà (›Á„œÅNêqâ…çxôøìªŒZòæÒÔŽÎ 4é™’8trÇ>™åž\ác7Øñˆ3Ò=yE¾ fÏ *pÍX÷Úªä‘/+ä�Àò ´¯8ðñ›ák“ýÎö23ÕÅ 9„øv{PM.ýÕâŽË£È91oU½û/õǘ•[ˆ6�¤GêŠ<»Ú½_Ô*ÏÜë‹d_|×aäe{Úyq¹¤­{ei¸;w6³Ng°<Õí3¼°ÌL¢ò³­FÂ.<+/HÜ"£gl‡Äñ÷cöíùBÆ ^öfQfGß™^´h±sûîkµˆëŸ™ìÉ‘ô?ó­-Æ¡›v׺†$dälÎ&koWVuÏñmÞ9ÑZÃÍMð»qfú3BU²4dì»ûG-‚Ðeo,ÍÙ}ahæí5Ûvˆ¼ÈãΤKX„PÒÖ=1ÖÕ¥ñе›kÈl¶ðÌWë¤&€žðvkCs—ž›¢µKáîí1Ó»†‰ò Ð6W 0ÚΆn~PˆÓ£V¼¾£eÀ%<\D�°ücBl­Í#4�Ý/ôY�¬²!8Ú æô»Á ó”Ö×IÌ�´ª©^âê÷à³¢'!³JiŠ,ŸÌ­l.ðgóÅBP( Œ©õègûÊe6�ÓȈœçàÀ�d±Ð‘a>Žf¬êĵGE3�! qêkjVÙ�lãG?þªjê š´¬¹fÈ€À22,c;8r�ÐŒu�=ÔÒ®¦¬ò1•ÀÁøÿÛ»Óç&Î;à¿Ý•´»ò!˲ƒ€åC€9LÂ3áh¹ %ÐfJH“RH'G§ý#ÒÉ›ÌôE›4…™d24™ÄiÁÄ@.;Áá´ñ¶åŸ²,Ù–´ºvŸ¾Mh‘‰DJg|?ï<ÖìóHûìwŸýÍÎórçd ·8½iÞuÎX^pöü´Îºkî(QdðJÓè|{ž@¼-ov¤«¥?Bu7µÞ>eSo~úúëÿ¸|O1ÁÂ>§¨d~–Yð·|ù¯ CÉϵÁæ¦á‘Ò}½‹Ÿ='ƒ'¢PXÍZ°¤pVªê®>Uóý£yª»AWÛ×½ó×c_ å>~ ÜqUÞ‰ækC –Þª¼ yóÇš¯„1G£Ó0??“'Šs¾‡@4ƒçÎ|N‘Ã:pµe‚i¾–ßJ‡=õJ}r•bÆXxd8±åJµ€?öÌÎB¡ /ŠF¢Ä—6H²˜µêÀá±ý‹ƒruú1©´7vlXå°^¸d]T¨4½=¤'ç•®_·$Gb*“2Œ¾Á„ÛŽ%I˜·éà g`î›2Gñ– ÔFG¼iY™9rî`ÝM[~v–ÉâñhÄ¥-X¹©¬ÐfT52g\ DDlä|ÅIZ½²üÀö×ÅÏ*¿ìôÅŸ€ò²Y ¦–÷Ö"¡[Ïé,3¢Ø3±Fâ¶EDZdrz«iGDÄM&.¤ÄŽ¥| ‰ˆ“Ì’èØrä÷±œä |Ÿl$NM!¬äÊ¡ÿÙjÈ¡¶ª÷Ìk×–=¹ÑFÃ7jN«Lvñi-à}TÂ&ÑÄ©½Õ'Î<ºvMù¡=¦±Ö U§.ö&»Ÿ»î¹}¥VŽHíùäØûu©prnqéšG—Ïö]ÿü¿×1MSœu­^œ6@¤‘I–…``r0± e‰#%ÎùJbÍÜtæ³…6ƒuý‘—'"^0P‘=¥¾>Ùíù4-Á§YN4ŶFáDQÔ¡1F› wû!#¿2ôÕ±w¿IäNÀB ›ËŠr=YvoóñFDœÕ;׈5?~Í«;_\óå'gæ·º (ÑΪ£'꿳xÉ£^¾ ¯0Ûïü¸Ã¾Ã>O<í>f´oؽ8\ñöѶ€–ºì—/Ø'?õ¶UW¶Us&[Ѧ'·­ï{ãŸmq³ISA1Kž¼' ²Õjð»'âO§k+Ng£‘‰’‰(BÄ™Sd.JDLñ‚Mgÿòqçí•U>ŽˆÒäI4§$÷ðqWJ_ýÙõg…”9«vìÝþpϱoâl@8éÎSCD¼Éd Š‘(CÞ0#"v5VÔXÍË9KwìÙQÖ÷FUrIª¹jß>z96‰ Æ¢Ùh™¿ø‘ÇJ‹S\µUG+ü÷VóeÑž+­›Êf0rQXQT)Eæ(Àˆ8É,§]*8ñq4s+YÅŽŒžÊ7_}åµW_yíÕ?½[;žç(ˆ½™ª©V«‘ˆ7ÉâÔ% FUÞ’™. ’$&U— ""Þ²À‘g"¬Å¶Þ•ˆ|Áô¬LŸî(Ìûö—¼£-­ßÙ‘^²"O∌¶åÛ¶><ën=w4;Sì­˜;Òpcr7IƒnׄJ‚uqIžÁð; 2ñÂÔYÐçgV›…'l…³øÉ[X¾,ÇHĉ³WoÙ\b™.4·{̶°XíéïíŽ,²¸52H¢Ñ7âR42æ–Ïâ ˆ“ж<±>_"baï kâ.w:æqvLä/)± DƬ•åË‹§}i8n[ñE†¼9Eötž‹c©=¶$svLØ—-ΈøûÚòÍi®þAq¾cŽHœ8{iQöíÇæn<räç¥é÷Øü¬Ò½»—e Dªß5è iljáï;ÆaÜSCDÄå-*Έ¤¹Åj}=^8Ëòí{×Î2iŠghDÑ´ï(¼Å£†~¿Ïï÷ùƒQ""Þ¶dÃÚlïù÷_£âóú{æØ¡‡" ¬±?úÚºÒ.Ï5ñiKí¡Ž¶inOÉŒC˜¹sçÂâô®óMS…Bu°¾nôàÂü”úF¿v³î‚o÷S/¬`ቮ¯Nœ¸êÖˆX°ýb}鮃\ Ž9ϯlŽ»2g^²ïÀ¦yL²‘=ó‡U¤¶Ÿús¥“ç´þ›Áe?{¶<3•õWÐ0ƈԾËÕC{v?}À;>êìë Ìãc|œ¶‚7ªNeïüÉs/™ˆ…Gšk*]w½2"½­iû—»N~4ùÕ®+µc;Ÿ84×ãn:_ÓºkÝîo‰#ÕÑžã®ý/—F"çþöáõŽÚ í~â×…cãÃm½Ý,‹'"6~õôéÌ­{~÷°À´à@㹓cÓ]ílÂãM±—»üZ »'²åA¯{œ‘Ú~ñâ²mO(òŒõ_¾PÓµµì«†ßªínè.Ùþ«çEF¤zoœ=Ù>íC½6X[ñéÆmû•™(êvVUÔjÓL‚ñÚ:~ÇŽâDDÌuéóºü-ϾTêékmlõäÅÚ¾TñÙ†íO*ã™êï½túR€…Ú¿¨*س÷·/GƺjÛ:#ð߃`2›eã½LU4Wkcpë¾çá‰H¼tªn4öËÆ‡‘x§†ã9¥{ mÓÁgr,¢¿ñÌÝQ"kkÞµõÐCF\x¤é“¾ÿŠšûʇï|ï£LËÓp¥ÝÎ4""¸^Y™S¾õ7/HSz¿>YÓ§RüÐMbÖ¨ƒ ~öƃ;èä›çâ¾ 0ãÌÔÊ�ŸóÈþÃ[ŠÍ³ ìòpÿtÉø!š©• �""ÍÕt¾mÇæÃ‡ÊÔñÖsI½ k¨l��è*��z„t�Ð#¤3�€!�ôé � GHg��=B:�èÒ�@Î��z„t�Ð#¤3�€!�ôé � GHg��=B:�èQBé %Y¾ß]�€[JgßÄ„Åb¹ß]�€[Jg¯Çc4™22¬÷»7��#ˆrj"ŸS%Óf“eYUÕh4z¿»�ð#—о‚·dX­©ii¢(Ý¿��%›Î��ðÿ7ê��ôé � GHg��=B:�èÒ�@Î��z„t�Ð#¤3�€!�ôé � GHg��=B:�èÒ�@Î��z„t�Ð#¤3�€!�ôé � GHg��=B:�èÑ¿Vvàªu(����IEND®B`‚��������������������������python-griffe-0.48.0/docs/schema.json���������������������������������������������������������������0000664�0001750�0001750�00000031613�14645165123�017274� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������{ "$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": "array", "items": { "$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-0.48.0/docs/reference/����������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�017073� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/cli.md����������������������������������������������������������0000664�0001750�0001750�00000003426�14645165123�020171� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/reference/docstrings.md���������������������������������������������������0000664�0001750�0001750�00000115276�14645165123�021610� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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] 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) 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) 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`. """ ``` #### Deprecated {#google-section-deprecated} Deprecated sections allow to document a deprecation that happened at a particular version. They can be used in every docstring. ```python """My module. Deprecated: 1.2: The `foo` attribute is deprecated. """ foo: int = 0 ``` #### 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. ... """ ... ``` 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 {#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. ... """ ... ``` 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 {#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 identifer 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`. """ ``` #### Deprecated {#numpydoc-section-deprecated} Deprecated sections allow to document a deprecation that happened at a particular version. They can be used in every docstring. ```python """My module. Deprecated ---------- 1.2 The `foo` attribute is deprecated. """ foo: int = 0 ``` #### 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. ## 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: - Deprecated (single item, version and text) - 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 | ✅ | ✅ | ⌠Deprecated | ✅ | ✅[^1]| [âŒ][issue-section-sphinx-deprecated] 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 | ✅ | ✅ | ✅ [^1]: Support for a regular section instead of the RST directive specified in the [Numpydoc styleguide](https://numpydoc.readthedocs.io/en/latest/format.html#deprecation-warning). [issue-section-sphinx-deprecated]: https://github.com/mkdocstrings/griffe/issues/6 [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 ### Getting annotations/defaults from parent Section | Google | Numpy | Sphinx ---------------- | ------ | ----- | ------ Attributes | ✅ | ✅ | [âŒ][issue-parent-sphinx-attributes] Functions | / | / | / Methods | / | / | / Classes | / | / | / Modules | / | / | / Deprecated | / | / | / Examples | / | / | / Parameters | ✅ | ✅ | ✅ Other Parameters | ✅ | ✅ | [âŒ][issue-parent-sphinx-other-parameters] Raises | / | / | / Warns | / | / | / Yields | ✅ | ✅ | [âŒ][issue-parent-sphinx-yields] Receives | ✅ | ✅ | [âŒ][issue-parent-sphinx-receives] Returns | ✅ | ✅ | ✅ [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 ### 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 | / | / | / Deprecated | / | / | / 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] [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 [issue-xref-numpy-func-cls]: https://github.com/mkdocstrings/griffe/issues/200 [issue-xref-google-func-cls]: https://github.com/mkdocstrings/griffe/issues/199 [merge_init]: https://mkdocstrings.github.io/python/usage/configuration/docstrings/#merge_init_into_class [doctest flags]: https://docs.python.org/3/library/doctest.html#option-flags [doctest]: https://docs.python.org/3/library/doctest.html#module-doctest [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-0.48.0/docs/reference/api/������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�017644� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/git.md������������������������������������������������������0000664�0001750�0001750�00000000172�14645165123�020751� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Git utilities ::: griffe.assert_git_repo ::: griffe.get_latest_tag ::: griffe.get_repo_root ::: griffe.tmp_worktree ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/checks.md���������������������������������������������������0000664�0001750�0001750�00000001125�14645165123�021425� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/reference/api/exceptions.md�����������������������������������������������0000664�0001750�0001750�00000000620�14645165123�022345� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/reference/api/loaders.md��������������������������������������������������0000664�0001750�0001750�00000000352�14645165123�021617� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Loaders ## **Main API** ::: griffe.load ::: griffe.load_git ## **Advanced API** ::: griffe.GriffeLoader ::: griffe.ModulesCollection ::: griffe.LinesCollection ## **Additional API** ::: griffe.Stats ::: griffe.merge_stubs ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/deprecated.md�����������������������������������������������0000664�0001750�0001750�00000006630�14645165123�022273� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Deprecated API <!-- YORE: Bump 1: Remove file. --> Previously, Griffe exposed its [module layout][module-layout]. Before v1, it started hiding the module layout to expose the whole public API from the top-level [`griffe`][griffe] module. All the following submodules are deprecated, and all the objects they used to expose can now be imported or accessed from `griffe` directly. ::: griffe.agents options: members: false show_root_full_path: true ::: griffe.agents.inspector options: members: false show_root_full_path: true ::: griffe.agents.nodes options: members: false show_root_full_path: true ::: griffe.agents.visitor options: members: false show_root_full_path: true ::: griffe.c3linear options: members: false show_root_full_path: true ::: griffe.cli options: members: false show_root_full_path: true ::: griffe.collections options: members: false show_root_full_path: true ::: griffe.dataclasses options: members: false show_root_full_path: true ::: griffe.diff options: members: false show_root_full_path: true ::: griffe.docstrings options: members: false show_root_full_path: true ::: griffe.docstrings.dataclasses options: members: false show_root_full_path: true ::: griffe.docstrings.google options: members: false show_root_full_path: true ::: griffe.docstrings.numpy options: members: false show_root_full_path: true ::: griffe.docstrings.parsers options: members: false show_root_full_path: true ::: griffe.docstrings.sphinx options: members: false show_root_full_path: true ::: griffe.docstrings.utils options: members: false show_root_full_path: true ::: griffe.encoders options: members: false show_root_full_path: true ::: griffe.enumerations options: members: false show_root_full_path: true ::: griffe.exceptions options: members: false show_root_full_path: true ::: griffe.expressions options: members: false show_root_full_path: true ::: griffe.extensions options: members: false show_root_full_path: true ::: griffe.extensions.base options: members: false show_root_full_path: true ::: griffe.extensions.dataclasses options: members: false show_root_full_path: true ::: griffe.extensions.hybrid options: members: false show_root_full_path: true ::: griffe.finder options: members: false show_root_full_path: true ::: griffe.git options: members: false show_root_full_path: true ::: griffe.importer options: members: false show_root_full_path: true ::: griffe.loader options: members: false show_root_full_path: true ::: griffe.logger options: members: false show_root_full_path: true ::: griffe.merger options: members: false show_root_full_path: true ::: griffe.mixins options: members: false show_root_full_path: true ::: griffe.stats options: members: false show_root_full_path: true ::: griffe.tests options: members: false show_root_full_path: true ��������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/cli.md������������������������������������������������������0000664�0001750�0001750�00000000203�14645165123�020730� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# CLI entrypoints ## **Main API** ::: griffe.main ::: griffe.check ::: griffe.dump ## **Advanced API** ::: griffe.get_parser ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/�����������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�021127� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/attribute.md�����������������������������������������0000664�0001750�0001750�00000000042�14645165123�023450� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Attribute ::: griffe.Attribute ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/alias.md���������������������������������������������0000664�0001750�0001750�00000000032�14645165123�022535� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Alias ::: griffe.Alias ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/function.md������������������������������������������0000664�0001750�0001750�00000000230�14645165123�023271� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Function ::: griffe.Function ::: griffe.Parameters ::: griffe.Parameter ::: griffe.ParameterKind ::: griffe.ParametersType ::: griffe.Decorator ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/class.md���������������������������������������������0000664�0001750�0001750�00000000107�14645165123�022554� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Class ::: griffe.Class ## **Utilities** ::: griffe.c3linear_merge ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models/module.md��������������������������������������������0000664�0001750�0001750�00000000034�14645165123�022733� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Module ::: griffe.Module ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/loggers.md��������������������������������������������������0000664�0001750�0001750�00000000506�14645165123�021631� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Loggers ## **Main API** <!-- YORE: Bump 1: Uncomment line. --> <!-- ::: griffe.logger --> ::: griffe.Logger ::: griffe.LogLevel ::: griffe.DEFAULT_LOG_LEVEL options: annotations_path: full ## **Advanced API** ::: griffe.patch_logger ## **Deprecated API** ::: griffe.get_logger ::: griffe.patch_loggers ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/extensions.md�����������������������������������������������0000664�0001750�0001750�00000000701�14645165123�022363� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extensions ## **Main API** ::: griffe.load_extensions ::: griffe.Extension ## **Advanced API** ::: griffe.Extensions ## **Types** ::: griffe.ExtensionType ::: griffe.LoadableExtensionType ## **Builtin extensions** ::: griffe.builtin_extensions ::: griffe.DataclassesExtension inherited_members: false ## **Deprecated API** ::: griffe.When ::: griffe.VisitorExtension ::: griffe.InspectorExtension ::: griffe.HybridExtension���������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/docstrings.md�����������������������������������������������0000664�0001750�0001750�00000000214�14645165123�022342� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstrings Docstrings are [parsed](docstrings/parsers.md) and the extracted information is structured in [models](docstrings/models.md). ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/models.md���������������������������������������������������0000664�0001750�0001750�00000002334�14645165123�021453� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 members: false ::: griffe.SetMembersMixin members: false ::: griffe.DelMembersMixin members: false ::: griffe.SerializationMixin members: false ::: griffe.ObjectAliasMixin members: false inherited_members: false ::: griffe.Object ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/serializers.md����������������������������������������������0000664�0001750�0001750�00000000460�14645165123�022522� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Serializers ## **Main API** ::: griffe.Object.as_json options: show_root_full_path: true heading_level: 2 ::: griffe.Object.from_json options: show_root_full_path: true heading_level: 2 ## **Advanced API** ::: griffe.JSONEncoder ::: griffe.json_decoder ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/helpers.md��������������������������������������������������0000664�0001750�0001750�00000000415�14645165123�021630� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Helpers ::: griffe.TmpPackage ::: griffe.temporary_pyfile ::: griffe.temporary_pypackage ::: griffe.temporary_visited_package ::: griffe.temporary_visited_module ::: griffe.temporary_inspected_module ::: griffe.vtree ::: griffe.htree ::: griffe.module_vtree ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/agents.md���������������������������������������������������0000664�0001750�0001750�00000001701�14645165123�021446� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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.ExportedName ::: griffe.get__all__ ::: griffe.safe_get__all__ ::: griffe.relative_to_absolute ::: griffe.get_parameters ::: griffe.get_value ::: griffe.safe_get_value ���������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/docstrings/�������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�022023� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/docstrings/parsers.md���������������������������������������0000664�0001750�0001750�00000000447�14645165123�024031� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstring parsers ## **Main API** ::: griffe.parse ::: griffe.parse_google ::: griffe.parse_numpy ::: griffe.parse_sphinx ## **Advanced API** ::: griffe.Parser ::: griffe.parsers ::: griffe.parse_docstring_annotation ::: griffe.docstring_warning ::: griffe.DocstringWarningCallable �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/docstrings/models.md����������������������������������������0000664�0001750�0001750�00000002237�14645165123�023634� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/reference/api/finder.md���������������������������������������������������0000664�0001750�0001750�00000000261�14645165123�021434� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Finder ## **Advanced API** ::: griffe.ModuleFinder ::: griffe.Package ::: griffe.NamespacePackage ## **Types** ::: griffe.NamePartsType ::: griffe.NamePartsAndPathType �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference/api/expressions.md����������������������������������������������0000664�0001750�0001750�00000002320�14645165123�022545� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/reference/api.md����������������������������������������������������������0000664�0001750�0001750�00000000136�14645165123�020166� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ::: griffe options: summary: functions: true members: false ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/credits.md����������������������������������������������������������������0000664�0001750�0001750�00000000116�14645165123�017112� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/introduction.md�����������������������������������������������������������0000664�0001750�0001750�00000004472�14645165123�020207� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/code-of-conduct.md��������������������������������������������������������0000664�0001750�0001750�00000000034�14645165123�020425� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--8<-- "CODE_OF_CONDUCT.md" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/reference.md��������������������������������������������������������������0000664�0001750�0001750�00000000546�14645165123�017422� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/license.md����������������������������������������������������������������0000664�0001750�0001750�00000000076�14645165123�017104� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- hide: - feedback --- # License ``` --8<-- "LICENSE" ``` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/index.md������������������������������������������������������������������0000664�0001750�0001750�00000004610�14645165123�016567� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- 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-0.48.0/docs/community.md��������������������������������������������������������������0000664�0001750�0001750�00000003575�14645165123�017515� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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-0.48.0/docs/guide/��������������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�016232� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/contributors.md�����������������������������������������������������0000664�0001750�0001750�00000002276�14645165123�021320� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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://github.com/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-0.48.0/docs/guide/users.md������������������������������������������������������������0000664�0001750�0001750�00000004527�14645165123�017725� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/��������������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�017373� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/checking.md���������������������������������������������������0000664�0001750�0001750�00000061557�14645165123�021506� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 relatively 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()) ``` ## 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 pass 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. ### 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 ``` ### 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 ``` ### Markdown [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [: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 ``` ### GitHub [:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [: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-0.48.0/docs/guide/users/how-to/�������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�020610� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/how-to/parse-docstrings.md������������������������������������0000664�0001750�0001750�00000002464�14645165123�024427� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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.dataclasses 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.dataclasses 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-0.48.0/docs/guide/users/loading.md����������������������������������������������������0000664�0001750�0001750�00000040522�14645165123�021335� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 specifed 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 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, 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 hierarcy, 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 accesing 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 reason it always load 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 by loading both packages. ```pycon ```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("package1", resolve_aliases=True, resolve_external=True) print(package2["X"].target.name) # X ``` Here Griffe automatically loaded `package2` 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-0.48.0/docs/guide/users/navigating.md�������������������������������������������������0000664�0001750�0001750�00000066346�14645165123�022063� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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.dataclasses.Module'> >>> type(griffe.load("markdown.core.Markdown")) <class '_griffe.dataclasses.Class'> >>> type(griffe.load("markdown.Markdown")) <class '_griffe.dataclasses.Alias'> >>> type(griffe.load("markdown.core.markdown")) <class '_griffe.dataclasses.Function'> >>> type(griffe.load("markdown.markdown")) <class '_griffe.dataclasses.Alias'> >>> type(griffe.load("markdown.Markdown.references")) <class '_griffe.dataclasses.Attribute'> ``` However deep is the object, 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 both regular and inherited members for classes. - Safer method for extensions: `markdown.set_member("thing", ...)`, also supporting dotted-paths and string tuples. This will not (re)assign inherited members for classes. - 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 both regular and inherited members for classes. - Safer method for extensions: `markdown.del_member("thing")`, also supporting dotted-paths and string tuples. This will not delete inherited members for classes. - 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. If this is the first time you access inherited members, the base classes of the given class will be resolved and cached, then the MRO (Method Resolution Order) will be computed for these bases classes, and a dictionary of inherited members will be built and cached. Next times you access it, you'll get the cached dictionary. 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 class 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, or attribute and 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 visibility of 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 check 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 name 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 new lines), 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 depend 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 fiels. ### 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. - [`deleter`][griffe.Function.deleter]: The property deleter, if the function is a property. - [`setter`][griffe.Function.setter]: The property setter, if the function is a property. - [`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]. ### 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][rich]'s pretty printer to see how expressions look like. [rich]: https://rich.readthedocs.io/en/stable/ ```pyodide install="griffe,rich" from griffe.tests 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). [mkdocstrings-python]: https://mkdocstrings.github.io/python During static analysis, these expressions also allow to analyze decorators, dataclass fields, and many more things in great details, 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][griffe.expressions]. ### 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). ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/recommendations/����������������������������������������������0000775�0001750�0001750�00000000000�14645165123�022562� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/recommendations/public-apis.md��������������������������������0000664�0001750�0001750�00000105313�14645165123�025317� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 module, 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`. Often times, 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 one 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 message for more robust consumption. > GRIFFE: **Our recommandation — 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 module, 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 object 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 and `__all__`. 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 recommandation — 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). 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 both you and your users if 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 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 yours 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 to use them, not to prevent the developers of the current package themselves to use 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 succint 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 controled 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 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 if [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 hint 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 periods, 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 avoiding 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 tool 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-0.48.0/docs/guide/users/recommendations/python-code.md��������������������������������0000664�0001750�0001750�00000033113�14645165123�025336� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 import 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 to import 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 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 recommandations 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 seem 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. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/users/recommendations/docstrings.md���������������������������������0000664�0001750�0001750�00000035336�14645165123�025275� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Docstrings Here are explanations on what docstrings are, and a few recommandations 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 opininons), 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 to 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 exception 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-0.48.0/docs/guide/users/extending.md��������������������������������������������������0000664�0001750�0001750�00000063653�14645165123�021717� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 finish 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 gave you a pretty good idea of what happens when Griffe collects data from a Python module. The next setion 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 is only one **load event**: - [`on_package_loaded`][griffe.Extension.on_package_loaded] This 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. #### Analysis events There are 3 generic **analysis events**: - [`on_node`][griffe.Extension.on_node] - [`on_instance`][griffe.Extension.on_instance] - [`on_members`][griffe.Extension.on_members] 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] The "on node" events are triggered when the agent (visitor or inspector) starts handling a node in the tree (AST or object tree). 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 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" event for these two kinds. **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 from griffe import Extension, Object, ObjectNode class MyExtension(Extension): def on_instance(self, node: ast.AST | ObjectNode, obj: Object) -> 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. 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 | ObjectNode, obj: Object) -> 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]. To support static analysis, dynamic analysis, or both, you can therefore check the type of the received node: ```python import ast from griffe import Extension, Object, ObjectNode class MyExtension(Extension): def on_instance(self, node: ast.AST | ObjectNode, obj: Object) -> None: """Do something with `node` and/or `obj`.""" if isinstance(node, ast.AST): ... # apply logic for static analysis else: ... # apply logic for dynamic analysis ``` 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. If you do need to, read the next section explaining how to visit trees. ### 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) ``` ### 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 ast from griffe import Extension, Object, ObjectNode self_namespace = "my_extension" class MyExtension(Extension): def on_instance(self, node: ast.AST | ObjectNode, obj: Object) -> 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 ast from griffe import Extension, ObjectNode, Class self_namespace = "my_extension" mkdocstrings_namespace = "mkdocstrings" class MyExtension(Extension): def on_class_instance(self, node: ast.AST | ObjectNode, cls: Class) -> 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 ast from griffe import Attribute, Extension, ObjectNode class MyExtension(Extension): def __init__(self, option1: str, option2: bool = False) -> None: super().__init__() self.option1 = option1 self.option2 = option2 def on_attribute_instance(self, node: ast.AST | ObjectNode, attr: Attribute) -> 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 ast from griffe import Extension, ObjectNode, Module, get_logger logger = get_logger(__name__) class MyExtension(Extension): def on_module_members(self, node: ast.AST | ObjectNode, mod: Module) -> None: logger.info(f"Doing some work on module {mod.path} and its members") ``` ### Full example The following example shows how one could write a "dynamic docstrings" extension that dynamically import 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 from griffe import Docstring, Extension, Object, ObjectNode, get_logger, dynamic_import logger = get_logger(__name__) class DynamicDocstrings(Extension): def __init__(self, object_paths: list[str] | None = None) -> None: self.object_paths = object_paths def on_instance(self, node: ast.AST | ObjectNode, obj: Object) -> None: if isinstance(node, 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 = 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 = Docstring(docstring, parent=obj) ``` 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-0.48.0/docs/guide/users/serializing.md������������������������������������������������0000664�0001750�0001750�00000010451�14645165123�022236� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 packages 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 to load 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 serialize 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 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/contributors/�������������������������������������������������������0000775�0001750�0001750�00000000000�14645165123�020767� 5����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide/contributors/commands.md��������������������������������������������0000664�0001750�0001750�00000014737�14645165123�023126� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� # 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-0.48.0/docs/guide/contributors/architecture.md����������������������������������������0000664�0001750�0001750�00000026272�14645165123�024004� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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/) called [copier-uv](https://pawamoy.github.io/copier-uv/). When generating the project, Copier asks a series of questions (configuref 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`. Right now, the `griffe` package has a lot of public submodules: this is only for backward-compatibility reasons. These submodules will be removed in version 1. Importing or accessing objects from these submodules will emit deprecation warnings, recommending to import things from `griffe` directly. 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-0.48.0/docs/guide/contributors/setup.md�����������������������������������������������0000664�0001750�0001750�00000005705�14645165123�022460� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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://github.com/astral-sh/uv) installed and available on your command line path. === ":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://github.com/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-0.48.0/docs/guide/contributors/workflow.md��������������������������������������������0000664�0001750�0001750�00000015631�14645165123�023171� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Development workflow This document describes our workflow when developping 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` 1. edit the code and the documentation 1. write new tests **For a bug fix:** 1. create a new branch: `git switch -c fix-summary` 1. write tests that fail but are expected to pass once the bug is fixed 1. run [`make test`][task-test] to make sure the new tests fail 1. fix the code **For a docs update:** <div class="annotate" markdown> 1. create a new branch: `git switch -c docs-summary` 1. start the live reloading server: `make docs` (1) 1. update the documentation 1. 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 1. run [`make check`][task-check] to check everything (fix any warning) 1. run [`make test`][task-test] to run the tests (fix any issue) 1. if you updated the documentation or the project dependencies: 1. run [`make docs`][task-docs] 1. 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-0.48.0/docs/getting-started.md��������������������������������������������������������0000664�0001750�0001750�00000001420�14645165123�020561� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Getting started To get started with Griffe, see [how to install it](installation.md) and [how to use it](introduction.md) with a short introduction. We also provide a [playground](playground.md) if you want to try it directly in your browser without actually installing it. If you need help, if you have questions, or if you would like to contribute to the project, 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-0.48.0/docs/logo.svg������������������������������������������������������������������0000777�0001750�0001750�00000000000�14645165123�020505� 2../logo.svg�����������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/contributing.md�����������������������������������������������������������0000664�0001750�0001750�00000004516�14645165123�020174� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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 in 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 setup a development environment to run tests or serve the documentation locally. [:fontawesome-solid-helmet-safety: Contributor guide](guide/contributors.md){ .md-button }����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.48.0/docs/guide.md������������������������������������������������������������������0000664�0001750�0001750�00000001447�14645165123�016562� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Guide Welcome to the Griffe guide! This guide is composed of a series of topics on how to use Griffe, as well as an explanation of the project's inner workings. Although the project's explanation is more targetted at contributors, users are welcome to read it too, as it helps understanding further how Griffe works and how to best use it. <div class="grid cards" markdown> - :fontawesome-solid-user:{ .lg .middle } **User guide** --- A series 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-0.48.0/docs/getting-help.md�����������������������������������������������������������0000664�0001750�0001750�00000002137�14645165123�020051� 0����������������������������������������������������������������������������������������������������ustar �kathara�������������������������kathara����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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, on 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. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������