python-griffe-0.40.0/0000755000175000017500000000000014556223422014241 5ustar carstencarstenpython-griffe-0.40.0/logo.svg0000644000175000017500000001421714556223422015727 0ustar carstencarsten python-griffe-0.40.0/benchmark.sh0000644000175000017500000000460714556223422016536 0ustar carstencarsten#!/usr/bin/env bash repo=$(realpath $(dirname "$0")) tags=( # 0.25.5 # 0.26.0 # 0.27.5 # 0.28.2 # 0.29.1 # 0.30.1 # 0.31.0 HEAD ) echo "Testing tags: ${tags[@]}" mkdir /tmp/griffe-benchmark &>/dev/null cd /tmp/griffe-benchmark echo "Preparing environments" for tag in ${tags[@]}; do ( if ! [ -d "venv${tag}" ]; then python3.11 -m venv venv${tag} if [ "${tag}" = "HEAD" ]; then venv${tag}/bin/python -m pip install -e "${repo}" &>/dev/null else venv${tag}/bin/python -m pip install griffe==${tag} &>/dev/null fi venv${tag}/bin/python -m pip install pyinstrument scalene memray &>/dev/null fi ) & done wait cat <benchmark.py import sys import griffe from griffe.loader import GriffeLoader from griffe.extensions import load_extensions from griffe.extensions import Extension stdlib_packages = sorted([m for m in sys.stdlib_module_names if not m.startswith("_")]) # extensions = load_extensions([ # Extension, Extension, Extension, Extension, # Extension, Extension, Extension, Extension, # Extension, Extension, Extension, Extension, # Extension, Extension, Extension, Extension, # ]) extensions = None loader = GriffeLoader(allow_inspection=False, extensions=extensions) for package in stdlib_packages: try: loader.load(package) except: pass loader.resolve_aliases(implicit=False, external=False) EOF if [ "$1" = "hyperfine" ]; then commands=$( for tag in ${tags[@]}; do echo "'venv${tag}/bin/python benchmark.py'" done ) eval hyperfine --show-output --runs 2 ${commands} --export-json benchmark.json elif [ "$1" = "scalene" ]; then for tag in ${tags[@]}; do venv${tag}/bin/python -m scalene --cli --cpu --memory --profile-all benchmark.py done elif [ "$1" = "memray" ]; then for tag in ${tags[@]}; do venv${tag}/bin/python -m memray run -o report${tag}.bin benchmark.py # venv${tag}/bin/python -m memray flamegraph report${tag}.bin # venv${tag}/bin/python -m memray tree report${tag}.bin # venv${tag}/bin/python -m memray summary report${tag}.bin venv${tag}/bin/python -m memray stats report${tag}.bin done elif [ "$1" = "pyinstrument" ]; then for tag in ${tags[@]}; do venv${tag}/bin/pyinstrument benchmark.py done fi python-griffe-0.40.0/mkdocs.yml0000644000175000017500000001126414556223422016250 0ustar carstencarstensite_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/griffe] 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: - Overview: index.md - Changelog: changelog.md - Credits: credits.md - License: license.md - Usage: - CLI reference: cli_reference.md - Checking for API breakages: checking.md - Dumping data as JSON: dumping.md - Loading and navigating data: loading.md - Extensions: - Using and writing extensions: extensions.md - Docstrings: - Supported styles: docstrings.md - Parsing docstrings in Python: parsing_docstrings.md - Try it out!: - Run Griffe in your browser: try_it_out.md # defer to gen-files + literate-nav - API reference: - Griffe: reference/ - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md - Coverage report: coverage.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.footer - navigation.indexes - 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/mkdocstrings.css - css/insiders.css markdown_extensions: - attr_list - admonition - callouts: strip_period: no - footnotes - 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 - markdown-exec - gen-files: scripts: - scripts/gen_ref_nav.py - scripts/gen_griffe_json.py - scripts/redirects.py - literate-nav: nav_file: SUMMARY.md - coverage - mkdocstrings: handlers: python: import: - https://docs.python.org/3/objects.inv paths: [src] options: docstring_options: ignore_init_summary: true docstring_section_style: list extensions: - griffe_inherited_docstrings heading_level: 1 inherited_members: true merge_init_into_class: true separate_signature: 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 - minify: minify_html: !ENV [DEPLOY, false] - 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/ python-griffe-0.40.0/CODE_OF_CONDUCT.md0000644000175000017500000001254714556223422017051 0ustar carstencarsten# 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 pawamoy@pm.me. 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.40.0/config/0000755000175000017500000000000014556223422015506 5ustar carstencarstenpython-griffe-0.40.0/config/coverage.ini0000644000175000017500000000052614556223422020005 0ustar carstencarsten[coverage:run] branch = true parallel = true source = src/ tests/ [coverage:paths] equivalent = src/ __pypackages__/ [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.40.0/config/git-changelog.toml0000644000175000017500000000031214556223422021107 0ustar carstencarstenbump = "auto" convention = "angular" in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" python-griffe-0.40.0/config/vscode/0000755000175000017500000000000014556223422016771 5ustar carstencarstenpython-griffe-0.40.0/config/vscode/launch.json0000644000175000017500000000157114556223422021142 0ustar carstencarsten{ "version": "0.2.0", "configurations": [ { "name": "python (current file)", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": false }, { "name": "test", "type": "python", "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.40.0/config/vscode/tasks.json0000644000175000017500000000442614556223422021017 0ustar carstencarsten{ "version": "2.0.0", "tasks": [ { "label": "changelog", "type": "shell", "command": "pdm run duty changelog" }, { "label": "check", "type": "shell", "command": "pdm run duty check" }, { "label": "check-quality", "type": "shell", "command": "pdm run duty check-quality" }, { "label": "check-types", "type": "shell", "command": "pdm run duty check-types" }, { "label": "check-docs", "type": "shell", "command": "pdm run duty check-docs" }, { "label": "check-dependencies", "type": "shell", "command": "pdm run duty check-dependencies" }, { "label": "check-api", "type": "shell", "command": "pdm run duty check-api" }, { "label": "clean", "type": "shell", "command": "pdm run duty clean" }, { "label": "docs", "type": "shell", "command": "pdm run duty docs" }, { "label": "docs-deploy", "type": "shell", "command": "pdm run duty docs-deploy" }, { "label": "format", "type": "shell", "command": "pdm run duty format" }, { "label": "lock", "type": "shell", "command": "pdm lock -G:all" }, { "label": "release", "type": "shell", "command": "pdm run duty release ${input:version}" }, { "label": "setup", "type": "shell", "command": "bash scripts/setup.sh" }, { "label": "test", "type": "shell", "command": "pdm run duty test coverage", "group": "test" }, { "label": "vscode", "type": "shell", "command": "pdm run duty vscode" } ], "inputs": [ { "id": "version", "type": "promptString", "description": "Version" } ] }python-griffe-0.40.0/config/vscode/settings.json0000644000175000017500000000300614556223422021523 0ustar carstencarsten{ "files.watcherExclude": { "**/__pypackages__/**": true, "**/.venv*/**": true, "**/venv*/**": true }, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, "python.autoComplete.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "python.analysis.extraPaths": [ "__pypackages__/3.8/lib", "__pypackages__/3.9/lib", "__pypackages__/3.10/lib", "__pypackages__/3.11/lib", "__pypackages__/3.12/lib" ], "black-formatter.args": [ "--config=config/black.toml" ], "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.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.40.0/config/black.toml0000644000175000017500000000007214556223422017456 0ustar carstencarsten[tool.black] line-length = 120 exclude = "tests/fixtures" python-griffe-0.40.0/config/ruff.toml0000644000175000017500000000506214556223422017350 0ustar carstencarstentarget-version = "py38" line-length = 132 exclude = [ "fixtures", "site", ] 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 ] [per-file-ignores] "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 ] [flake8-quotes] docstring-quotes = "double" [flake8-tidy-imports] ban-relative-imports = "all" [isort] known-first-party = ["griffe"] [pydocstyle] convention = "google" python-griffe-0.40.0/config/pytest.ini0000644000175000017500000000064514556223422017544 0ustar carstencarsten[pytest] norecursedirs = .git .tox .env dist build python_files = test_*.py *_test.py tests.py addopts = --cov --cov-config config/coverage.ini testpaths = tests # action:message_regex:warning_class:module_regex:line filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist ignore:.*slated for removal in Python:DeprecationWarning:.* python-griffe-0.40.0/config/mypy.ini0000644000175000017500000000016214556223422017204 0ustar carstencarsten[mypy] ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true python-griffe-0.40.0/docs/0000755000175000017500000000000014556223422015171 5ustar carstencarstenpython-griffe-0.40.0/docs/logo.svg0000777000175000017500000000000014556223422020543 2../logo.svgustar carstencarstenpython-griffe-0.40.0/docs/changelog.md0000644000175000017500000000002614556223422017440 0ustar carstencarsten--8<-- "CHANGELOG.md" python-griffe-0.40.0/docs/loading.md0000644000175000017500000001300414556223422017126 0ustar carstencarsten# Loading data with Python Griffe provides a shortcut function for simple needs: ```python import griffe mkdocs = griffe.load("mkdocs") ``` The [`load`][griffe.loader.load] function accepts a number of parameters. For more complex needs, create and use a loader: ```python from griffe.loader import GriffeLoader loader = GriffeLoader() mkdocs = loader.load("mkdocs") ``` Similarly, the [`GriffeLoader`][griffe.loader.GriffeLoader] accepts a number of parameters to configure how the modules are found and loaded. If you don't want to recurse in the submodules: ```python mkdocs = loader.load("mkdocs", submodules=False) ``` ## Navigating into the loaded objects Both the `load` function and the `GriffeLoader.load` method return an [`Object`][griffe.dataclasses.Object] instance. There are several ways to access members of an object: - through its `members` attribute, which is a dictionary, with the usual `keys()`, `values()` and `items()` methods. - thanks to its `__getitem__` method. For example `griffe["dataclasses"]` returns the `Module` instance representing Griffe's `dataclasses` module. Since this module also has members, you can chain calls: `griffe["dataclasses"]["Module"]`. Conveniently, you can chain the names with dots in a single call: `griffe["dataclasses.Module"]`. You can even pass a tuple instead of a string: `griffe[("dataclasses", "Module")]`. - through the [`modules`][griffe.dataclasses.Object.modules], [`classes`][griffe.dataclasses.Object.classes], [`functions`][griffe.dataclasses.Object.functions] and [`attributes`][griffe.dataclasses.Object.attributes] properties, which take care of filtering members based on their kind, and return dictionaries. Most of the time, you will only use classes from the [`griffe.dataclasses`][griffe.dataclasses] and [`griffe.docstrings.dataclasses`][griffe.docstrings.dataclasses] modules. ## Class inheritance TIP: **New in version 0.30** WARNING: **Inheritance support is experimental** Inheritance support was recently added, and might need some corrections before being fully usable. Don't hesitate to report any issue that arises from using inheritance support in Griffe. Griffe supports class inheritance, both when visiting and inspecting modules. To access members of a class that are inherited from base classes, use [`Object.inherited_members`][griffe.dataclasses.Object.inherited_members]. 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 access inherited members in extensions, while visiting or inspecting a module. **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 from griffe.loader import GriffeLoader loader = 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 [`Object.all_members`][griffe.dataclasses.Object.all_members]. If you want to access only declared members, use [`Object.members`][griffe.dataclasses.Object]. Accessing [`Object.attributes`][griffe.dataclasses.Object.attributes], [`Object.functions`][griffe.dataclasses.Object.functions], [`Object.classes`][griffe.dataclasses.Object.classes] or [`Object.modules`][griffe.dataclasses.Object.modules] will trigger inheritance computation, so make sure to only call it once everything is loaded by Griffe. Don't access inherited members in extensions, while visiting or inspecting a module. ### 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. python-griffe-0.40.0/docs/parsing_docstrings.md0000644000175000017500000000250014556223422021412 0ustar carstencarsten# 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.dataclasses.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.40.0/docs/contributing.md0000644000175000017500000000003114556223422020214 0ustar carstencarsten--8<-- "CONTRIBUTING.md" python-griffe-0.40.0/docs/code_of_conduct.md0000644000175000017500000000003414556223422020625 0ustar carstencarsten--8<-- "CODE_OF_CONDUCT.md" python-griffe-0.40.0/docs/extensions.md0000644000175000017500000006450414556223422017723 0ustar carstencarsten# Extensions Extensions allow to enhance or customize the data that Griffe collects. ## 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.loader.GriffeLoader] or [`load` function][griffe.loader.load]. The parameter accepts an instance of the [`Extensions` class][griffe.extensions.Extensions]. Such an instance is created with the help of the [`load_extensions` function][griffe.extensions.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.extensions.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.agents.visitor] and for object trees, it uses its [Inspector agent][griffe.agents.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.dataclasses.Object] subclass, such as a [Module][griffe.dataclasses.Module], a [Class][griffe.dataclasses.Class], a [Function][griffe.dataclasses.Function], or an [Attribute][griffe.dataclasses.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.agents.nodes.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.dataclasses.Module], then walk through its members, continuing with the class node - it will instantiate a [Class][griffe.dataclasses.Class], then walk through its members, continuing with the function node - it will instantiate a [Function][griffe.dataclasses.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{{"on_node event
on_module_node event"}} create_mod{{create module instance}} event_mod_instance{{"on_instance event
on_module_instance event"}} visit_mod_members{{visit module members}} visit_cls{{enter class node}} event_cls_node{{"on_node event
on_class_node event"}} create_cls{{create class instance}} event_cls_instance{{"on_instance event
on_class_instance event"}} visit_cls_members{{visit class members}} visit_func{{enter func node}} event_func_node{{"on_node event
on_function_node event"}} create_func{{create function instance}} event_func_instance{{"on_instance event
on_function_instance event"}} event_cls_members{{"on_members event
on_class_members event"}} event_mod_members{{"on_members event
on_module_members 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.extensions.base.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.extensions.base.Extension.on_node] - [`on_instance`][griffe.extensions.base.Extension.on_instance] - [`on_members`][griffe.extensions.base.Extension.on_members] There are also specific **analysis events** for each object kind: - [`on_module_node`][griffe.extensions.base.Extension.on_module_node] - [`on_module_instance`][griffe.extensions.base.Extension.on_module_instance] - [`on_module_members`][griffe.extensions.base.Extension.on_module_members] - [`on_class_node`][griffe.extensions.base.Extension.on_class_node] - [`on_class_instance`][griffe.extensions.base.Extension.on_class_instance] - [`on_class_members`][griffe.extensions.base.Extension.on_class_members] - [`on_function_node`][griffe.extensions.base.Extension.on_function_node] - [`on_function_instance`][griffe.extensions.base.Extension.on_function_instance] - [`on_attribute_node`][griffe.extensions.base.Extension.on_attribute_node] - [`on_attribute_instance`][griffe.extensions.base.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.dataclasses.Module], [Class][griffe.dataclasses.Class], [Function][griffe.dataclasses.Function], or [Attribute][griffe.dataclasses.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.extensions.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.extensions.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.extensions.base.Extension.visit]: call `self.visit(node)` to start visiting an abstract syntax tree. - [`generic_visit`][griffe.extensions.base.Extension.generic_visit]: call `self.generic_visit(node)` to visit each subnode of a given node. - [`inspect`][griffe.extensions.base.Extension.inspect]: call `self.inspect(node)` to start visiting an object tree. Nodes contain references to the runtime objects, see [`ObjectNode`][griffe.agents.nodes.ObjectNode]. - [`generic_inspect`][griffe.extensions.base.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_` where `` 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_`, where `` is replaced with the string value of the node's kind. The different kinds are listed in the [`ObjectKind`][griffe.agents.nodes.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 >
> > - [`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] > > > > - [`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] > > > > - [`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] > > > > - [`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] > >
[^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. python-griffe-0.40.0/docs/css/0000755000175000017500000000000014556223422015761 5ustar carstencarstenpython-griffe-0.40.0/docs/css/insiders.css0000644000175000017500000000373114556223422020317 0ustar carstencarsten@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.40.0/docs/css/mkdocstrings.css0000644000175000017500000000211714556223422021203 0ustar carstencarsten/* 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,'); -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; vertical-align: middle; position: relative; height: 1em; width: 1em; background-color: var(--md-typeset-a-color); } a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); }python-griffe-0.40.0/docs/css/custom.css0000644000175000017500000000024714556223422020010 0ustar carstencarsten/* 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%); } python-griffe-0.40.0/docs/checking.md0000644000175000017500000006047214556223422017277 0ustar carstencarsten# Checking for API breakages 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. 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 ``` ## 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 By default, Griffe will print each detected breakage on a single line, on `stderr`: ```python exec="1" html="1" import os from rich.console import Console # TODO: instead of hardcoding output, actually run griffe check? report = """$ griffe check griffe -ssrc -b0.24.0 -a0.23.0 [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter default was changed[/]: True -> None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter default was changed[/]: True -> None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]max_iterations[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]repo[/]): [#afaf72]Positional parameter was moved[/]: position: from 2 to 1 (-1) [bold]src/griffe/git.py[/]:75: load_git([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]repo[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]submodules[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]try_relative_path[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]extensions[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]search_paths[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_parser[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_options[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]lines_collection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]modules_collection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]allow_inspection[/]): [#afaf72]Parameter kind was changed[/]: positional or keyword -> keyword-only """ with open(os.devnull, "w") as devnull: console = Console(record=True, width=150, file=devnull) console.print(report, markup=True, highlight=False) print(console.export_html(inline_styles=True, code_format="
{code}
")) ``` 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: ```python exec="1" html="1" import os from rich.console import Console # TODO: instead of hardcoding output, actually run griffe check? report = """$ griffe check griffe -ssrc -b0.24.0 -a0.23.0 --verbose [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter default was changed[/]: Old: True New: None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter default was changed[/]: Old: True New: None [bold]src/griffe/loader.py[/]:156: GriffeLoader.resolve_aliases([#7faeff]max_iterations[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_exported[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/loader.py[/]:308: GriffeLoader.resolve_module_aliases([#7faeff]only_known_modules[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:39: tmp_worktree([#7faeff]repo[/]): [#afaf72]Positional parameter was moved[/]: Details: position: from 1 to 0 (-1) [bold]src/griffe/git.py[/]:75: load_git([#7faeff]commit[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]repo[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]submodules[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]try_relative_path[/]): [#afaf72]Parameter was removed[/] [bold]src/griffe/git.py[/]:75: load_git([#7faeff]extensions[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]search_paths[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_parser[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]docstring_options[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]lines_collection[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]modules_collection[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only [bold]src/griffe/git.py[/]:75: load_git([#7faeff]allow_inspection[/]): [#afaf72]Parameter kind was changed[/]: Old: positional or keyword New: keyword-only """ with open(os.devnull, "w") as devnull: console = Console(record=True, width=80, file=devnull) console.print(report, markup=True, highlight=False) print(console.export_html(inline_styles=True, code_format="
{code}
")) ``` python-griffe-0.40.0/docs/.overrides/0000755000175000017500000000000014556223422017251 5ustar carstencarstenpython-griffe-0.40.0/docs/.overrides/main.html0000644000175000017500000000104714556223422021065 0ustar carstencarsten{% extends "base.html" %} {% block announce %} Sponsorship is now available! {% include ".icons/octicons/heart-fill-16.svg" %} — For updates follow @pawamoy on {% include ".icons/fontawesome/brands/mastodon.svg" %} Fosstodon {% endblock %} python-griffe-0.40.0/docs/docstrings.md0000644000175000017500000010774414556223422017707 0ustar carstencarsten# 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](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) - `numpy`, to parse Numpydoc docstrings, see [Numpydoc's documentation](https://numpydoc.readthedocs.io/en/latest/format.html) - `sphinx`, to parse Sphinx-style docstrings, see [Sphinx's documentation](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html) ## Syntax 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 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. """ ``` #### Parser 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](#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](#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. #### 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 - 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 - 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 - 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 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 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 - 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 - 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 - 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 - 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 - 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 - 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 "", line 1, in 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 - 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`](#parser-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`](#parser-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 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 """ ``` #### Parser 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. #### 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 - 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 - 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 - 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 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 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 - 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 - 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 - 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 - 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 - 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 - 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 "", line 1, in 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 - 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 python-griffe-0.40.0/docs/js/0000755000175000017500000000000014556223422015605 5ustar carstencarstenpython-griffe-0.40.0/docs/js/insiders.js0000644000175000017500000000506214556223422017766 0ustar carstencarstenfunction 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 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 += ` `; }); if (privateSponsors > 0) { sponsorsElem.innerHTML += ` +${privateSponsors} `; } }); }); getJSON(dataURL + '/sponsorsBronze.json', function (err, sponsors) { const bronzeSponsors = document.getElementById("bronze-sponsors"); if (sponsors) { let html = ''; html += 'Bronze sponsors

' sponsors.forEach(function (sponsor) { html += ` ${sponsor.name} ` }); html += '

' bronzeSponsors.innerHTML = html; } }); } python-griffe-0.40.0/docs/dumping.md0000644000175000017500000000250014556223422017153 0ustar carstencarsten# Dumping packages' signatures as JSON Griffe can be used to load packages' signatures and output them as JSON on the standard output or in writable files. Pass the names of packages to the `griffe dump` command: ```console $ griffe dump httpx fastapi { "httpx": { "name": "httpx", ... }, "fastapi": { "name": "fastapi", ... } } ``` It will output a JSON-serialized version of the packages' signatures. Try it out on Griffe itself: ```console $ griffe dump griffe { "griffe": { "name": "griffe", ... } } ``` To output in a file instead of standard output, use the `--output` or `-o` 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 ` 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`. For an example of what real data looks like, see [the full Griffe JSON dump](griffe.json). python-griffe-0.40.0/docs/license.md0000644000175000017500000000004414556223422017133 0ustar carstencarsten# License ``` --8<-- "LICENSE" ``` python-griffe-0.40.0/docs/credits.md0000644000175000017500000000020114556223422017141 0ustar carstencarsten--- hide: - toc --- ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` python-griffe-0.40.0/docs/cli_reference.md0000644000175000017500000000342614556223422020305 0ustar carstencarsten# CLI reference ```python exec="true" idprefix="" import argparse import sys from griffe.cli 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.40.0/docs/try_it_out.md0000644000175000017500000000146414556223422017721 0ustar carstencarsten# Try Griffe in your browser Try Griffe directly in your browser thanks to Pyodide! You can click the "Run" button in the top-right corner of each editor, or hit ++ctrl+enter++ to run the code. In the following example, we import `griffe` and use it to load itself. Then we output the signature of the `Function` class as JSON. ```pyodide install="griffe" import griffe griffe_pkg = griffe.load("griffe") griffe_pkg["dataclasses.Function"].as_json(indent=2) ``` Try it out with another package of your choice! Just replace `your-dist-name` with a package's distribution name, and `your_package_name` with the package's import name: ```pyodide import micropip await micropip.install("your-dist-name") data = griffe.load("your_package_name") data.as_json(indent=2)[:1000] # truncate to a thousand characters... ``` python-griffe-0.40.0/docs/insiders/0000755000175000017500000000000014556223422017011 5ustar carstencarstenpython-griffe-0.40.0/docs/insiders/installation.md0000644000175000017500000001536514556223422022046 0ustar carstencarsten--- 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). ### 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 pip (self-hosted) Self-hosting the Insiders package makes it possible to depend on *griffe* normally, while transparently downloading and installing the Insiders version locally. It means that you can specify your dependencies normally, and your contributors without access to Insiders will get the public version, while you get the Insiders version on your machine. WARNING: **Limitation** With this method, there is no way to force the installation of an Insiders version rather than a public version. If there is a public version that is more recent than your self-hosted Insiders version, the public version will take precedence. Remember to regularly update your self-hosted versions by uploading latest distributions. You can build the distributions for Insiders yourself, by cloning the repository and using [build] to build the distributions, or you can download them from our [GitHub Releases]. You can upload these distributions to a private PyPI-like registry ([Artifactory], [Google Cloud], [pypiserver], etc.) with [Twine]: [build]: https://pypi.org/project/build/ [Artifactory]: https://jfrog.com/help/r/jfrog-artifactory-documentation/pypi-repositories [Google Cloud]: https://cloud.google.com/artifact-registry/docs/python [pypiserver]: https://pypi.org/project/pypiserver/ [Github Releases]: https://github.com/pawamoy-insiders/griffe/releases [Twine]: https://pypi.org/project/twine/ ```bash # download distributions in ~/dists, then upload with: twine upload --repository-url https://your-private-index.com ~/dists/* ``` You might also need to provide a username and password/token to authenticate against the registry. Please check [Twine's documentation][twine docs]. [twine docs]: https://twine.readthedocs.io/en/stable/ You can then configure pip (or other tools) to look for packages into your package index. For example, with pip: ```bash pip config set global.extra-index-url https://your-private-index.com/simple ``` Note that the URL might differ depending on whether your are uploading a package (with Twine) or installing a package (with pip), and depending on the registry you are using (Artifactory, Google Cloud, etc.). Please check the documentation of your registry to learn how to configure your environment. **We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use](index.md#terms).** >? TIP: **Full example with `pypiserver`** > In this example we use [pypiserver] to serve a local PyPI index. > > ```bash > pip install --user pypiserver > # or pipx install pypiserver > > # create a packages directory > mkdir -p ~/.local/pypiserver/packages > > # run the pypi server without authentication > pypi-server run -p 8080 -a . -P . ~/.local/pypiserver/packages & > ``` > > We can configure the credentials to access the server in [`~/.pypirc`][pypirc]: > > [pypirc]: https://packaging.python.org/en/latest/specifications/pypirc/ > > ```ini title=".pypirc" > [distutils] > index-servers = > local > > [local] > repository: http://localhost:8080 > username: > password: > ``` > > We then clone the Insiders repository, build distributions and upload them to our local server: > > ```bash > # clone the repository > git clone git@github.com:pawamoy-insiders/griffe > cd griffe > > # install build > pip install --user build > # or pipx install build > > # checkout latest tag > git checkout $(git describe --tags --abbrev=0) > > # build the distributions > pyproject-build > > # upload them to our local server > twine upload -r local dist/* --skip-existing > ``` > > Finally, we configure pip, and for example [PDM][pdm], to use our local index to find packages: > > ```bash > pip config set global.extra-index-url http://localhost:8080/simple > pdm config pypi.extra.url http://localhost:8080/simple > ``` > > [pdm]: https://pdm.fming.dev/latest/ > > Now when running `pip install griffe`, > or resolving dependencies with PDM, > both tools will look into our local index and find the Insiders version. > **Remember to update your local index regularly!** ### with git Of course, you can use *griffe Insiders* directly from `git`: ``` git clone git@github.com:pawamoy-insiders/griffe ``` When cloning from `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.40.0/docs/insiders/changelog.md0000644000175000017500000000017214556223422021262 0ustar carstencarsten# Changelog ## griffe Insiders ### 1.0.0 April 22, 2023 { id="1.0.0" } - Release first Insiders version python-griffe-0.40.0/docs/insiders/goals.yml0000644000175000017500000000001114556223422020631 0ustar carstencarstengoals: {}python-griffe-0.40.0/docs/insiders/index.md0000644000175000017500000002172314556223422020447 0ustar carstencarsten# 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" --8<-- "scripts/insiders.py" print( f"""The moment you become a sponsor, you'll get **immediate 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) ``` ## 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. **Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email to pawamoy@pm.me 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 pawamoy@pm.me, 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 awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary }

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. ## Funding ### 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" 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 pawamoy@pm.me. ### 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 python-griffe-0.40.0/docs/schema-docstrings-options.json0000644000175000017500000000207014556223422023171 0ustar carstencarsten{ "$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.40.0/docs/index.md0000644000175000017500000000002314556223422016615 0ustar carstencarsten--8<-- "README.md" python-griffe-0.40.0/docs/schema.json0000644000175000017500000003247514556223422017337 0ustar carstencarsten{ "$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/griffe/dataclasses/#griffe.dataclasses.Alias.name", "type": "string" }, "kind": { "title": "The 'alias' kind.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Alias.kind", "const": "alias" }, "path": { "title": "The alias path.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Alias.path", "type": "string" }, "target_path": { "title": "For aliases, the Python path of their target.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Alias.lineno", "type": "integer" }, "endlineno": { "title": "For aliases, the import ending line number in their own module.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Object.name", "type": "string" }, "kind": { "title": "The kind of object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Object.path", "type": "string" }, "filepath": { "title": "The file path of the object's parent module.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Object.relative_package_filepath", "type": "string" }, "labels": { "title": "The labels of the object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Object.labels", "type": "array" }, "docstring": { "title": "The docstring of the object.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Docstring", "type": "object", "properties": { "value": { "title": "The actual string.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Docstring.value", "type": "string" }, "lineno": { "title": "The docstring starting line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Docstring.lineno", "type": "integer" }, "endlineno": { "title": "The docstring ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Docstring.endlineno", "type": [ "integer", "null" ] }, "parsed": { "title": "The parsed docstring (list of docstring sections).", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Docstring.lineno", "type": "integer" }, "endlineno": { "title": "The docstring ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Class.bases", "type": "array", "items": { "$ref": "#/$defs/annotation" } }, "decorators": { "title": "For classes, their decorators.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Class.decorators", "type": "array", "items": { "type": "object", "properties": { "value": { "title": "The decorator value (string, name or expression).", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Decorator.value", "$ref": "#/$defs/annotation" }, "lineno": { "title": "The decorator starting line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Decorator.lineno", "type": "integer" }, "endlineno": { "title": "The decorator ending line number.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Function.parameters", "type": "array", "items": { "type": "object", "properties": { "name": { "title": "The name of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Parameter.name", "type": "string" }, "annotation": { "title": "The annotation of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Parameter.annotation", "$ref": "#/$defs/annotation" }, "kind": { "title": "The kind of parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Parameter.kind", "type": "string" }, "default": { "title": "The default value of the parameter.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Parameter.default", "$ref": "#/$defs/annotation" } }, "required": [ "name", "kind" ] } }, "returns": { "title": "For functions, their return annotation.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.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/griffe/dataclasses/#griffe.dataclasses.Attribute.value", "$ref": "#/$defs/annotation" }, "annotation": { "title": "For attributes, their type annotation.", "markdownDescription": "https://mkdocstrings.github.io/griffe/reference/griffe/dataclasses/#griffe.dataclasses.Attribute.annotation", "$ref": "#/$defs/annotation" } } } } ] } ], "$defs": { "expression": { "type": "object", "additionalProperties": true }, "annotation": { "oneOf": [ { "type": "null" }, { "type": "string" }, { "$ref": "#/$defs/expression" } ] } } }python-griffe-0.40.0/README.md0000644000175000017500000001064214556223422015523 0ustar carstencarsten# 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%20material-blue.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-blue.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." ## 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 Dumping data section](https://mkdocstrings.github.io/griffe/dumping/) 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 ``` See [the API breakage section](https://mkdocstrings.github.io/griffe/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 data section](https://mkdocstrings.github.io/griffe/loading/) for more examples. ## Todo - Extensions - Post-processing extensions - Third-party libraries we could provide support for: - Django support - Marshmallow support - Pydantic support - Docstrings parsers - epydoc - New Markdown-based format? For graceful degradation - Serializer: - Flat JSON - API diff: - [ ] Mechanism to cache APIs? Should users version them, or store them somewhere (docs)? - [ ] Ability to return warnings (things that are not backward-compatibility-friendly) - List of things to consider for warnings - Multiple positional-or-keyword parameters - Public imports in public modules - Private things made public through imports/assignments - Too many public things? Generally annoying. Configuration? - [x] Ability to compare two APIs to return breaking changes - List of things to consider for breaking changes - [x] Changed position of positional only parameter - [x] Changed position of positional or keyword parameter - [ ] Changed type of parameter - [ ] Changed type of public module attribute - [ ] Changed return type of a public function/method - [x] Added parameter without a default value - [x] Removed keyword-only parameter without a default value, without **kwargs to swallow it - [x] Removed positional-only parameter without a default value, without *args to swallow it - [x] Removed positional-or_keyword argument without a default value, without *args and **kwargs to swallow it - [x] Removed public module/class/function/method/attribute - [ ] All of the previous even when parent is private (could be publicly imported or assigned somewhere), and later be smarter: public assign/import makes private things public! - [ ] Inheritance: removed, added or changed base that changes MRO python-griffe-0.40.0/.gitpod.dockerfile0000644000175000017500000000017414556223422017640 0ustar carstencarstenFROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ pipx install pdm; \ pipx ensurepath python-griffe-0.40.0/Makefile0000644000175000017500000000202414556223422015677 0ustar carstencarsten.DEFAULT_GOAL := help SHELL := bash DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 export PDM_MULTIRUN_USE_VENVS ?= $(if $(shell pdm config python.use_venv | grep True),1,0) args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files docs_args = host port release_args = version test_args = match fuzz_args = seeds min_seed max_seed size BASIC_DUTIES = \ changelog \ check-api \ check-dependencies \ clean \ coverage \ docs \ docs-deploy \ format \ release \ fuzz \ vscode QUALITY_DUTIES = \ check-quality \ check-docs \ check-types \ test .PHONY: help help: @$(DUTY) --list .PHONY: lock lock: @pdm lock -G:all .PHONY: setup setup: @bash scripts/setup.sh .PHONY: check check: @pdm multirun duty check-quality check-types check-docs @$(DUTY) check-dependencies check-api .PHONY: $(BASIC_DUTIES) $(BASIC_DUTIES): @$(DUTY) $@ $(call args,$@) .PHONY: $(QUALITY_DUTIES) $(QUALITY_DUTIES): @pdm multirun duty $@ $(call args,$@) python-griffe-0.40.0/.gitignore0000644000175000017500000000037514556223422016236 0ustar carstencarsten.idea/ __pycache__/ *.py[cod] dist/ *.egg-info/ build/ htmlcov/ .coverage* pip-wheel-metadata/ .pytest_cache/ .mypy_cache/ site/ pdm.lock pdm.toml .pdm-plugins/ .pdm-python __pypackages__/ .venv/ .cache/ profile.pstats profile.svg .hypothesis/ .vscode/ python-griffe-0.40.0/duties.py0000644000175000017500000002606614556223422016122 0ustar carstencarsten"""Development tasks.""" from __future__ import annotations import os import sys from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Iterator from duty import duty from duty.callables import black, coverage, lazy, mkdocs, mypy, pytest, ruff, safety 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("PDM_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) -> None: """Update the changelog in-place with latest commits. Parameters: ctx: The context instance (passed automatically). """ from git_changelog.cli import main as git_changelog ctx.run(git_changelog, args=[[]], 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! Parameters: ctx: The context instance (passed automatically). """ @duty def check_quality(ctx: Context) -> None: """Check the code quality. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), command=f"ruff check --config config/ruff.toml {PY_SRC}", ) @duty def check_dependencies(ctx: Context) -> None: """Check for vulnerabilities in dependencies. Parameters: ctx: The context instance (passed automatically). """ # retrieve the list of dependencies requirements = ctx.run( ["pdm", "export", "-f", "requirements", "--without-hashes"], title="Exporting dependencies as requirements", allow_overrides=False, ) ctx.run( safety.check(requirements), title="Checking dependencies", command="pdm export -f requirements --without-hashes | safety check --stdin", ) @duty def check_docs(ctx: Context) -> None: """Check if the documentation builds correctly. Parameters: ctx: The context instance (passed automatically). """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) with material_insiders(): ctx.run( mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), command="mkdocs build -vs", ) @duty def check_types(ctx: Context) -> None: """Check that the code is correctly typed. Parameters: ctx: The context instance (passed automatically). """ ctx.run( mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @duty def check_api(ctx: Context) -> None: """Check for API breaking changes. Parameters: ctx: The context instance (passed automatically). """ from griffe.cli import check as g_check griffe_check = lazy(g_check, name="griffe.check") ctx.run( griffe_check("griffe", search_paths=["src"], color=True), title="Checking for API breaking changes", command="griffe check -ssrc griffe", nofail=True, ) @duty(silent=True) def clean(ctx: Context) -> None: """Delete temporary files. Parameters: ctx: The context instance (passed automatically). """ ctx.run("rm -rf .coverage*") ctx.run("rm -rf .mypy_cache") ctx.run("rm -rf .pytest_cache") ctx.run("rm -rf tests/.pytest_cache") ctx.run("rm -rf build") ctx.run("rm -rf dist") ctx.run("rm -rf htmlcov") ctx.run("rm -rf pip-wheel-metadata") ctx.run("rm -rf site") ctx.run("find . -type d -name __pycache__ | xargs rm -rf") ctx.run("find . -name '*.rej' -delete") @duty def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( mkdocs.serve(dev_addr=f"{host}:{port}"), title="Serving documentation", capture=False, ) @duty def docs_deploy(ctx: Context) -> None: """Deploy the documentation on GitHub pages. Parameters: ctx: The context instance (passed automatically). """ 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( 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. Parameters: ctx: The context instance (passed automatically). """ ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) ctx.run(black.run(*PY_SRC_LIST, config="config/black.toml"), title="Formatting code") @duty(post=["docs-deploy"]) def release(ctx: Context, version: str) -> None: """Release a new Python package. Parameters: ctx: The context instance (passed automatically). version: The new version number to use. """ 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!)", ) 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) ctx.run("pdm build", title="Building dist/wheel", pty=PTY) ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) @duty(silent=True, aliases=["coverage"]) def cov(ctx: Context) -> None: """Report coverage as text and HTML. Parameters: ctx: The context instance (passed automatically). """ ctx.run(coverage.combine, nofail=True) ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) ctx.run(coverage.html(rcfile="config/coverage.ini")) @duty def test(ctx: Context, match: str = "") -> None: """Run the test suite. Parameters: ctx: The context instance (passed automatically). 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( pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes", verbosity=10), title=pyprefix("Running tests"), command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes 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, fails) 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}") @duty def vscode(ctx: Context) -> None: """Configure VSCode. This task will overwrite the following files, so make sure to back them up: - `.vscode/launch.json` - `.vscode/settings.json` - `.vscode/tasks.json` Parameters: ctx: The context instance (passed automatically). """ def update_config(filename: str) -> None: source_file = Path("config", "vscode", filename) target_file = Path(".vscode", filename) target_file.parent.mkdir(exist_ok=True) target_file.write_text(source_file.read_text()) for filename in ("launch.json", "settings.json", "tasks.json"): ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") python-griffe-0.40.0/tests/0000755000175000017500000000000014556223422015403 5ustar carstencarstenpython-griffe-0.40.0/tests/test_git.py0000644000175000017500000000671114556223422017604 0ustar carstencarsten"""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.cli import check from griffe.dataclasses import Module from griffe.git import 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.40.0/tests/fixtures/0000755000175000017500000000000014556223422017254 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/0000755000175000017500000000000014556223422020360 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/v0.2.0/0000755000175000017500000000000014556223422021203 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/v0.2.0/my_module/0000755000175000017500000000000014556223422023175 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/v0.2.0/my_module/__init__.py0000644000175000017500000000002614556223422025304 0ustar carstencarsten__version__ = "0.2.0" python-griffe-0.40.0/tests/fixtures/_repo/v0.1.0/0000755000175000017500000000000014556223422021202 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/v0.1.0/my_module/0000755000175000017500000000000014556223422023174 5ustar carstencarstenpython-griffe-0.40.0/tests/fixtures/_repo/v0.1.0/my_module/__init__.py0000644000175000017500000000002614556223422025303 0ustar carstencarsten__version__ = "0.1.0" python-griffe-0.40.0/tests/test_inheritance.py0000644000175000017500000001343014556223422021306 0ustar carstencarsten"""Tests for class inheritance.""" from __future__ import annotations from typing import TYPE_CHECKING, Callable import pytest from griffe.collections import ModulesCollection from griffe.tests import 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.40.0/tests/conftest.py0000644000175000017500000000005714556223422017604 0ustar carstencarsten"""Configuration for the pytest test suite.""" python-griffe-0.40.0/tests/test_docstrings/0000755000175000017500000000000014556223422020621 5ustar carstencarstenpython-griffe-0.40.0/tests/test_docstrings/conftest.py0000644000175000017500000000146014556223422023021 0ustar carstencarsten"""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.40.0/tests/test_docstrings/helpers.py0000644000175000017500000000632014556223422022636 0ustar carstencarsten"""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.dataclasses import Attribute, Class, Docstring, Function, Module from griffe.docstrings.dataclasses import DocstringAttribute, DocstringElement, DocstringParameter, DocstringSection from griffe.logger import LogLevel 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] sections = parser_module.parse(docstring_object, **parser_opts) return sections, warnings yield parse parser_module._warn = original_warn # type: ignore[attr-defined] def assert_parameter_equal(actual: DocstringParameter, expected: DocstringParameter) -> None: """Help assert docstring parameters are equal. Parameters: actual: The actual parameter. expected: The expected parameter. """ assert actual.name == expected.name assert_element_equal(actual, expected) assert actual.value == expected.value def assert_attribute_equal(actual: DocstringAttribute, expected: DocstringAttribute) -> None: """Help assert docstring attributes are equal. Parameters: actual: The actual attribute. expected: The expected attribute. """ assert actual.name == expected.name assert_element_equal(actual, expected) def assert_element_equal(actual: DocstringElement, expected: DocstringElement) -> None: """Help assert docstring elements are equal. Parameters: actual: The actual element. expected: The expected element. """ assert isinstance(actual, type(expected)) for k in actual.as_dict(): src = getattr(actual, k) dst = getattr(expected, k) assert src == dst, f"attribute {k!r}, {src!r} != {dst!r}" python-griffe-0.40.0/tests/test_docstrings/test_sphinx.py0000644000175000017500000007007414556223422023553 0ustar carstencarsten"""Tests for the [Sphinx-style parser][griffe.docstrings.sphinx].""" from __future__ import annotations import inspect from typing import TYPE_CHECKING import pytest from griffe.dataclasses import Attribute, Class, Function, Module, Parameter, Parameters from griffe.docstrings.dataclasses import ( DocstringAttribute, DocstringParameter, DocstringRaise, DocstringReturn, DocstringSectionKind, ) from tests.test_docstrings.helpers import assert_attribute_equal, assert_element_equal, assert_parameter_equal 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 assert_parameter_equal(sections[1].value[0], DocstringParameter(SOME_NAME, description=SOME_TEXT)) 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 assert_parameter_equal(sections[1].value[0], DocstringParameter(SOME_NAME, description=SOME_TEXT)) @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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, description=f"{SOME_TEXT} {SOME_EXTRA_TEXT}"), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT), ) @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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, annotation=union.replace(" or ", " | "), description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter(SOME_NAME, annotation="str", description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter("other", description=SOME_TEXT), ) 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 assert_parameter_equal( sections[1].value[0], DocstringParameter("foo", description=SOME_TEXT, value=repr("")), ) 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 assert_attribute_equal( sections[1].value[0], DocstringAttribute(SOME_NAME, description=f"{SOME_TEXT} {SOME_EXTRA_TEXT}"), ) 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 assert_attribute_equal( sections[1].value[0], DocstringAttribute(SOME_NAME, description=SOME_TEXT), ) 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 assert_attribute_equal( sections[1].value[0], DocstringAttribute(SOME_NAME, description=SOME_TEXT), ) 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 assert_attribute_equal( sections[1].value[0], DocstringAttribute(SOME_NAME, annotation="str", description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringReturn(name="", annotation=None, description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringReturn(name="", annotation="str", description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringReturn(name="", annotation="str", description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringReturn(name="", annotation="str", description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringReturn(name="", annotation="str", description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT), ) 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 assert_element_equal( sections[1].value[0], DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT), ) assert_element_equal( sections[1].value[1], DocstringRaise(annotation=SOME_OTHER_EXCEPTION_NAME, description=SOME_TEXT), ) @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 assert_element_equal( sections[1].value[0], DocstringRaise(annotation=SOME_EXCEPTION_NAME, description=SOME_TEXT), ) 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_kwargs = [ {"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 in enumerate(expected_kwargs): assert_attribute_equal(attr_section.value[index], DocstringAttribute(**expected)) # type: ignore[arg-type] assert not warnings python-griffe-0.40.0/tests/test_docstrings/test_warnings.py0000644000175000017500000000156514556223422024071 0ustar carstencarsten"""Tests for the docstrings utility functions.""" from __future__ import annotations from griffe.dataclasses import Docstring, Function, Parameter, ParameterKind, Parameters from griffe.docstrings.parsers import 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.40.0/tests/test_docstrings/__init__.py0000644000175000017500000000003414556223422022727 0ustar carstencarsten"""Tests for docstrings.""" python-griffe-0.40.0/tests/test_docstrings/test_google.py0000644000175000017500000012404214556223422023511 0ustar carstencarsten"""Tests for the [Google-style parser][griffe.docstrings.google].""" from __future__ import annotations import inspect from typing import TYPE_CHECKING import pytest from griffe.dataclasses import Attribute, Class, Docstring, Function, Module, Parameter, Parameters from griffe.docstrings.dataclasses import DocstringReturn, DocstringSectionKind from griffe.docstrings.utils import parse_annotation from griffe.expressions import ExprName 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." def test_dataclass_uses_attributes(parse_google: ParserType) -> None: """Assert we use the class' attributes as parameters when parsing dataclasses' parameters sections. Parameters: parse_google: Fixture parser. """ docstring = """ Parameters: auth: Auth method. base_url: Base URL. """ parent = Class("c") parent.labels.add("dataclass") parent["auth"] = Attribute("auth", annotation="Optional[str]", value="None") parent["base_url"] = Attribute("base_url", annotation="str", value="'https://api.example.com'") sections, warnings = parse_google(docstring, parent=parent) assert not warnings auth, base_url = sections[0].value assert auth.name == "auth" assert auth.annotation == "Optional[str]" assert auth.description == "Auth method." assert auth.default == "None" assert base_url.name == "base_url" assert base_url.annotation == "str" assert base_url.description == "Base URL." assert base_url.default == "'https://api.example.com'" # 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_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_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_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_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_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_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.40.0/tests/test_docstrings/test_numpy.py0000644000175000017500000010205114556223422023401 0ustar carstencarsten"""Tests for the [Numpy-style parser][griffe.docstrings.numpy].""" from __future__ import annotations import logging from typing import TYPE_CHECKING import pytest from griffe.dataclasses import Attribute, Class, Docstring, Function, Module, Parameter, Parameters from griffe.docstrings.dataclasses import ( DocstringSectionKind, ) from griffe.docstrings.utils import parse_annotation from griffe.expressions import ExprName 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." # ============================================================================================= # 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_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_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_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_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_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.40.0/tests/test_inspector.py0000644000175000017500000000771614556223422021035 0ustar carstencarsten"""Test inspection mechanisms.""" from __future__ import annotations import sys from pathlib import Path import pytest from griffe.agents.inspector import inspect from griffe.tests import temporary_inspected_module, temporary_pypackage 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]) sys.modules.pop("package", None) sys.modules.pop("package.module", None) 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=Path(tmp_package.path, "package.py"), import_paths=[tmp_package.tmpdir]) sys.modules.pop("package", None) sys.modules.pop("package.package", None) python-griffe-0.40.0/tests/test_dataclasses.py0000644000175000017500000000621314556223422021305 0ustar carstencarsten"""Tests for the `dataclasses` module.""" from __future__ import annotations from copy import deepcopy import griffe from griffe.dataclasses import Docstring, Module from griffe.loader import GriffeLoader from griffe.tests import module_vtree, temporary_pypackage def test_submodule_exports() -> None: """Check that a module is exported depending on whether it was also imported.""" root = Module("root") sub = Module("sub") root["sub"] = sub assert not root.member_is_exported(sub, explicitely=True) assert not root.member_is_exported(sub, explicitely=False) root.imports["sub"] = "root.sub" assert not root.member_is_exported(sub, explicitely=True) assert root.member_is_exported(sub, explicitely=False) root.exports = {"sub"} assert root.member_is_exported(sub, explicitely=True) assert root.member_is_exported(sub, explicitely=False) 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_alias_proxies() -> None: """Assert that the Alias class has all the necessary methods and properties. Parameters: cls: The class to check. """ api = griffe.load("griffe") alias_members = set(api["dataclasses.Alias"].all_members.keys()) for cls in ( api["dataclasses.Module"], api["dataclasses.Class"], api["dataclasses.Function"], api["dataclasses.Attribute"], ): for name in cls.all_members: if not name.startswith("_") or name.startswith("__"): assert name in alias_members python-griffe-0.40.0/tests/test_visitor.py0000644000175000017500000002724014556223422020520 0ustar carstencarsten"""Test visit mechanisms.""" from __future__ import annotations from textwrap import dedent import pytest from griffe.loader import GriffeLoader from griffe.tests import 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: set[str]) -> None: """Assert decorators are used to set labels on functions. Parameters: decorator: A parametrized decorator. labels: The parametrized set 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: set[str]) -> None: """Assert decorators are used to set labels on classes. Parameters: decorator: A parametrized decorator. labels: The parametrized set 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.40.0/tests/test_finder.py0000644000175000017500000002611314556223422020266 0ustar carstencarsten"""Tests for the `finder` module.""" from __future__ import annotations import os from pathlib import Path from textwrap import dedent import pytest from griffe.dataclasses import Module from griffe.finder import ModuleFinder, NamespacePackage, Package, _handle_editable_module, _handle_pth_file from griffe.tests import 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")] def test_setuptools_file_handling(tmp_path: Path) -> None: """Assert editable modules by `setuptools` are handled. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "__editable__whatever.py" pth_file.write_text("hello\nMAPPING = {'griffe': 'src/griffe'}") paths = [sp.path for sp in _handle_editable_module(pth_file)] assert paths == [Path("src")] def test_setuptools_file_handling_multiple_paths(tmp_path: Path) -> None: """Assert editable modules by `setuptools` are handled when multiple packages are installed in the same editable. Parameters: tmp_path: Pytest fixture. """ pth_file = tmp_path / "__editable__whatever.py" pth_file.write_text( "hello=1\nMAPPING = {\n'griffe':\n 'src1/griffe', 'briffe':'src2/briffe'}\ndef 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 == "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.40.0/tests/__init__.py0000644000175000017500000000030314556223422017510 0ustar carstencarsten"""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.40.0/tests/test_diff.py0000644000175000017500000001516114556223422017730 0ustar carstencarsten"""Tests for the `diff` module.""" from __future__ import annotations import pytest from griffe.diff import Breakage, BreakageKind, find_breaking_changes from griffe.tests import 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" 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.40.0/tests/test_encoders.py0000644000175000017500000000330214556223422020614 0ustar carstencarsten"""Tests for the `encoders` module.""" from __future__ import annotations import json import pytest from jsonschema import ValidationError, validate from griffe.dataclasses import Function, Module, Object from griffe.loader import GriffeLoader 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.40.0/tests/test_nodes.py0000644000175000017500000002072714556223422020134 0ustar carstencarsten"""Test nodes utilities.""" from __future__ import annotations import logging import sys from ast import PyCF_ONLY_AST import pytest from griffe.agents.nodes import get_value, relative_to_absolute from griffe.expressions import Expr, ExprName from griffe.tests import module_vtree, 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]", ] @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 @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 @pytest.mark.parametrize("expression", syntax_examples) def test_building_value_from_nodes(expression: str) -> None: """Test building value from AST nodes. Parameters: expression: An expression (parametrized). """ try: node = ( compile( # type: ignore[attr-defined] expression, mode="exec", filename="<>", flags=PyCF_ONLY_AST, ) .body[0] .value ) except SyntaxError: pytest.skip(reason=f"Unsupported expression '{expression}' on Python {sys.version}") value = get_value(node) # make space after comma non-significant value = value.replace(", ", ",") # type: ignore[union-attr] expression = expression.replace(", ", ",") assert value == expression # 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.40.0/tests/test_cli.py0000644000175000017500000000255214556223422017567 0ustar carstencarsten"""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.40.0/tests/test_extensions.py0000644000175000017500000001153414556223422021217 0ustar carstencarsten"""Tests for the `extensions` module.""" from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Any import pytest from griffe.extensions import Extension, load_extensions from griffe.tests import 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.40.0/tests/test_stdlib.py0000644000175000017500000000233714556223422020302 0ustar carstencarsten"""Fuzzing on the standard library.""" from __future__ import annotations import sys from contextlib import suppress from typing import TYPE_CHECKING import pytest from griffe.loader import GriffeLoader 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.mark.skipif(sys.version_info < (3, 10), reason="Python less than 3.10 does not have sys.stdlib_module_names") def test_fuzzing_on_stdlib() -> None: """Run Griffe on the standard library.""" stblib_packages = sorted([m for m in sys.stdlib_module_names if not m.startswith("_")]) # type: ignore[attr-defined,unused-ignore] loader = GriffeLoader() for package in stblib_packages: with suppress(ImportError): loader.load(package) loader.resolve_aliases(implicit=True, external=True) for module in loader.modules_collection.members.values(): _access_inherited_members(module) loader.stats() python-griffe-0.40.0/tests/test_merger.py0000644000175000017500000000406414556223422020301 0ustar carstencarsten"""Tests for the `merger` module.""" from __future__ import annotations from textwrap import dedent from griffe.loader import GriffeLoader from griffe.tests import 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.40.0/tests/test_loader.py0000644000175000017500000004022514556223422020265 0ustar carstencarsten"""Tests for the `loader` module.""" from __future__ import annotations import logging from textwrap import dedent from typing import TYPE_CHECKING import pytest from griffe.expressions import ExprName from griffe.loader import GriffeLoader from griffe.tests import temporary_pyfile, temporary_pypackage if TYPE_CHECKING: from pathlib import Path 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) 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 python-griffe-0.40.0/tests/test_expressions.py0000644000175000017500000000526414556223422021405 0ustar carstencarsten"""Test names and expressions methods.""" from __future__ import annotations import ast import pytest from griffe.dataclasses import Module from griffe.docstrings.parsers import Parser from griffe.expressions import get_expression from griffe.tests import 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="eval", flags=ast.PyCF_ONLY_AST, optimize=2) expression = get_expression(top_node.body, parent=Module("module")) # type: ignore[attr-defined] assert str(expression) == code python-griffe-0.40.0/tests/test_mixins.py0000644000175000017500000000065614556223422020332 0ustar carstencarsten"""Tests for the `mixins` module.""" from __future__ import annotations from griffe.tests 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.40.0/tests/test_functions.py0000644000175000017500000001252214556223422021026 0ustar carstencarsten"""Test functions loading.""" from __future__ import annotations import pytest from griffe.dataclasses import ParameterKind from griffe.tests import 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.40.0/src/0000755000175000017500000000000014556223422015030 5ustar carstencarstenpython-griffe-0.40.0/src/griffe/0000755000175000017500000000000014556223422016272 5ustar carstencarstenpython-griffe-0.40.0/src/griffe/tests.py0000644000175000017500000002703514556223422020015 0ustar carstencarsten"""Test helpers and pytest fixtures.""" 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.dataclasses import Module, Object from griffe.loader import GriffeLoader if TYPE_CHECKING: from griffe.collections import LinesCollection, ModulesCollection from griffe.docstrings.parsers import Parser from griffe.extensions import Extensions TMPDIR_PREFIX = "griffe_" @dataclass class TmpPackage: """A temporary package.""" tmpdir: Path """The temporary directory containing the package.""" name: str """The package name, as to dynamically import it.""" path: Path """The package path.""" @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. """ with temporary_pyfile(code, module_name=module_name) as (_, path): module = visit( module_name, filepath=path, code=dedent(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): 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: try: filepath = modules[-1].filepath.with_stem(parts[-1]) # type: ignore[union-attr] except AttributeError: # TODO: remove once Python 3.8 is dropped 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] __all__ = [ "htree", "module_vtree", "temporary_inspected_module", "temporary_pyfile", "temporary_pypackage", "temporary_visited_module", "temporary_visited_package", "TmpPackage", "vtree", ] python-griffe-0.40.0/src/griffe/logger.py0000644000175000017500000000456414556223422020134 0ustar carstencarsten"""This module contains logging utilities. We provide the [`patch_loggers`][griffe.logger.patch_loggers] function so dependant libraries can patch loggers as they see fit. For example, to fit in the MkDocs logging configuration and prefix each log message with the module name: ```python import logging from griffe.logger 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) ``` """ from __future__ import annotations import logging from enum import Enum from typing import Any, Callable, ClassVar class LogLevel(Enum): """Enumeration of available log levels.""" trace: str = "trace" debug: str = "debug" info: str = "info" success: str = "success" warning: str = "warning" error: str = "error" critical: str = "critical" class _Logger: _default_logger: Any = logging.getLogger _instances: ClassVar[dict[str, _Logger]] = {} def __init__(self, name: str) -> None: # Default logger that can be patched by third-party. self._logger = self.__class__._default_logger(name) # Register instance. self._instances[name] = self def __getattr__(self, name: str) -> Any: # Forward everything to the logger. return getattr(self._logger, name) @classmethod def _patch_loggers(cls, get_logger_func: Callable) -> None: # Patch current instances. for name, instance in cls._instances.items(): instance._logger = get_logger_func(name) # Future instances will be patched as well. cls._default_logger = get_logger_func def get_logger(name: str) -> _Logger: """Create and return a new logger instance. Parameters: name: The logger name. Returns: The logger. """ return _Logger(name) def patch_loggers(get_logger_func: Callable[[str], Any]) -> None: """Patch loggers. Parameters: get_logger_func: A function accepting a name as parameter and returning a logger. """ _Logger._patch_loggers(get_logger_func) __all__ = ["get_logger", "LogLevel", "patch_loggers"] python-griffe-0.40.0/src/griffe/merger.py0000644000175000017500000001052014556223422020123 0ustar carstencarsten"""This module contains utilities to merge data together.""" from __future__ import annotations from contextlib import suppress from typing import TYPE_CHECKING from griffe.exceptions import AliasResolutionError, CyclicAliasError from griffe.logger import get_logger if TYPE_CHECKING: from griffe.dataclasses import Attribute, Class, Function, Module, Object logger = get_logger(__name__) 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 __all__ = ["merge_stubs"] python-griffe-0.40.0/src/griffe/git.py0000644000175000017500000001526514556223422017440 0ustar carstencarsten"""This module contains the code allowing to load modules from specific git commits. ```python from griffe.git import load_git # where `repo` is the folder *containing* `.git` old_api = load_git("my_module", commit="v0.1.0", repo="path/to/repo") ``` """ 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 TYPE_CHECKING, Any, Iterator, Sequence from griffe import loader from griffe.exceptions import GitError if TYPE_CHECKING: from griffe.collections import LinesCollection, ModulesCollection from griffe.dataclasses import Object from griffe.docstrings.parsers import Parser from griffe.extensions import Extensions WORKTREE_PREFIX = "griffe-worktree-" def _assert_git_repo(repo: str | Path) -> None: if not shutil.which("git"): raise RuntimeError("Could not find git executable. Please install git.") try: subprocess.run( ["git", "-C", str(repo), "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: {repo}") from err def _get_latest_tag(path: str | Path) -> str: if isinstance(path, str): path = Path(path) if not path.is_dir(): path = path.parent process = subprocess.run( ["git", "tag", "-l", "--sort=-committerdate"], cwd=path, 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 {path}: {output or 'no tags'}") return output.split("\n", 1)[0] def _get_repo_root(path: str | Path) -> str: if isinstance(path, str): path = Path(path) if not path.is_dir(): path = path.parent output = subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], cwd=path, ) 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) 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, find_stubs_package: bool = False, # TODO: Remove at some point. module: str | Path | None = None, ) -> Object: """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.loader.load]. This function requires that the `git` executable is installed. 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. 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. 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 # TODO: Remove at some point. if isinstance(module, Path): module = worktree / module return loader.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, find_stubs_package=find_stubs_package, # TODO: Remove at some point. module=module, ) __all__ = ["load_git"] python-griffe-0.40.0/src/griffe/agents/0000755000175000017500000000000014556223422017553 5ustar carstencarstenpython-griffe-0.40.0/src/griffe/agents/__init__.py0000644000175000017500000000012014556223422021655 0ustar carstencarsten"""These modules contain the different agents that are able to extract data.""" python-griffe-0.40.0/src/griffe/agents/inspector.py0000644000175000017500000004427414556223422022146 0ustar carstencarsten"""This module defines introspection mechanisms. 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 node visitor is not powerful enough to infer all these dynamic modifications. In this case, we always try to visit the code first, and only then we load the object to update the data with introspection. This module exposes a public function, [`inspect()`][griffe.agents.inspector.inspect], which inspects the module using [`inspect.getmembers()`][inspect.getmembers], and returns a new [`Module`][griffe.dataclasses.Module] instance, populating its members recursively, by using a [`NodeVisitor`][ast.NodeVisitor]-like class. The inspection agent works similarly to the regular "node visitor" agent, in that it maintains a state with the current object being handled, and recursively handle its members. """ from __future__ import annotations import ast from inspect import Parameter as SignatureParameter from inspect import Signature, cleandoc from inspect import signature as getsignature from typing import TYPE_CHECKING, Any, Sequence from griffe.agents.nodes import ObjectKind, ObjectNode, safe_get_annotation from griffe.collections import LinesCollection, ModulesCollection from griffe.dataclasses import ( Alias, Attribute, Class, Docstring, Function, Module, Parameter, ParameterKind, Parameters, ) from griffe.extensions import Extensions from griffe.importer import dynamic_import if TYPE_CHECKING: from pathlib import Path from griffe.docstrings.parsers import Parser from griffe.expressions import Expr empty = Signature.empty def inspect( module_name: str, *, filepath: Path | None = None, import_paths: Sequence[str | Path] | None = None, extensions: Extensions | None = None, parent: Module | None = None, docstring_parser: Parser | None = None, docstring_options: dict[str, Any] | None = None, lines_collection: LinesCollection | None = None, modules_collection: ModulesCollection | None = None, ) -> Module: """Inspect a module. 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 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 self.filepath: Path | None = filepath self.extensions: Extensions = extensions.attach_inspector(self) self.parent: Module | None = parent self.current: Module | Class = None # type: ignore[assignment] self.docstring_parser: Parser | None = docstring_parser self.docstring_options: dict[str, Any] = docstring_options or {} self.lines_collection: LinesCollection = lines_collection or LinesCollection() self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() 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_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 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: target_path = child.alias_target_path if target_path: 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__}") class_ = Class( name=node.name, docstring=self._get_docstring(node), bases=bases, ) 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. """ node.obj = node.obj.func 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) ) 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), ) else: obj = Function( name=node.name, parameters=parameters, returns=returns, docstring=self._get_docstring(node), ) 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] __all__ = ["inspect", "Inspector"] python-griffe-0.40.0/src/griffe/agents/nodes/0000755000175000017500000000000014556223422020663 5ustar carstencarstenpython-griffe-0.40.0/src/griffe/agents/nodes/_parameters.py0000644000175000017500000000144714556223422023545 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations from typing import TYPE_CHECKING, Any from griffe.expressions import safe_get_expression from griffe.logger import get_logger if TYPE_CHECKING: import ast from griffe.dataclasses import Class, Module logger = get_logger(__name__) def get_call_keyword_arguments(node: ast.Call, parent: Module | Class) -> dict[str, Any]: """Get the list of keyword argument names and values from a Call node. Parameters: node: The node to extract the keyword arguments from. Returns: The keyword argument names and values. """ return {kw.arg: safe_get_expression(kw.value, parent) for kw in node.keywords if kw.arg} __all__ = ["get_call_keyword_arguments"] python-griffe-0.40.0/src/griffe/agents/nodes/_names.py0000644000175000017500000000343614556223422022505 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations import ast from typing import Any, Callable from griffe.logger import get_logger logger = get_logger(__name__) 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.")] __all__ = ["get_instance_names", "get_name", "get_names"] python-griffe-0.40.0/src/griffe/agents/nodes/_all.py0000644000175000017500000000566214556223422022155 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations import ast from contextlib import suppress from typing import TYPE_CHECKING, Any, Callable from griffe.agents.nodes._values import get_value from griffe.expressions import ExprName from griffe.logger import LogLevel, get_logger if TYPE_CHECKING: from griffe.dataclasses import Module logger = get_logger(__name__) def _extract_constant(node: ast.Constant, parent: Module) -> list[str | ExprName]: return [node.value] def _extract_name(node: ast.Name, parent: Module) -> list[str | ExprName]: return [ExprName(node.id, parent)] def _extract_starred(node: ast.Starred, parent: Module) -> list[str | ExprName]: return _extract(node.value, parent) def _extract_sequence(node: ast.List | ast.Set | ast.Tuple, parent: Module) -> list[str | ExprName]: sequence = [] for elt in node.elts: sequence.extend(_extract(elt, parent)) return sequence def _extract_binop(node: ast.BinOp, parent: Module) -> list[str | ExprName]: left = _extract(node.left, parent) right = _extract(node.right, parent) return left + right _node_map: dict[type, Callable[[Any, Module], list[str | ExprName]]] = { 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 | ExprName]: return _node_map[type(node)](node, parent) def get__all__(node: ast.Assign | ast.AugAssign, parent: Module) -> list[str | ExprName]: """Get the values declared in `__all__`. Parameters: node: The assignment node. parent: The parent module. Returns: A set of names. """ if node.value is None: return [] return _extract(node.value, parent) def safe_get__all__( node: ast.Assign | ast.AugAssign, parent: Module, log_level: LogLevel = LogLevel.debug, # TODO: set to error when we handle more things ) -> list[str | ExprName]: """Safely (no exception) extract values in `__all__`. Parameters: node: The `__all__` assignment node. parent: The parent used to resolve the names. log_level: Log level to use to log a message. Returns: A list of strings or 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 [] __all__ = ["get__all__", "safe_get__all__"] python-griffe-0.40.0/src/griffe/agents/nodes/_values.py0000644000175000017500000002712714556223422022704 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations import ast import sys from typing import TYPE_CHECKING, Any, Callable from griffe.logger import get_logger if TYPE_CHECKING: from pathlib import Path logger = get_logger(__name__) def _extract_add(node: ast.Add, **kwargs: Any) -> str: return "+" def _extract_and(node: ast.And, **kwargs: Any) -> str: return "and" def _extract_arguments(node: ast.arguments, **kwargs: Any) -> str: return ", ".join(arg.arg for arg in node.args) def _extract_attribute(node: ast.Attribute, **kwargs: Any) -> str: return f"{_extract(node.value, **kwargs)}.{node.attr}" def _extract_binop(node: ast.BinOp, **kwargs: Any) -> str: return f"{_extract(node.left, **kwargs)} {_extract(node.op, **kwargs)} {_extract(node.right, **kwargs)}" def _extract_bitor(node: ast.BitOr, **kwargs: Any) -> str: return "|" def _extract_bitand(node: ast.BitAnd, **kwargs: Any) -> str: return "&" def _extract_bitxor(node: ast.BitXor, **kwargs: Any) -> str: return "^" def _extract_boolop(node: ast.BoolOp, **kwargs: Any) -> str: return f" {_extract(node.op, **kwargs)} ".join(_extract(value, **kwargs) for value in node.values) def _extract_call(node: ast.Call, **kwargs: Any) -> str: positional_args = ", ".join(_extract(arg, **kwargs) for arg in node.args) keyword_args = ", ".join(_extract(kwarg, **kwargs) for kwarg in node.keywords) if positional_args and keyword_args: args = f"{positional_args}, {keyword_args}" elif positional_args: args = positional_args elif keyword_args: args = keyword_args else: args = "" return f"{_extract(node.func, **kwargs)}({args})" def _extract_compare(node: ast.Compare, **kwargs: Any) -> str: left = _extract(node.left, **kwargs) ops = [_extract(op, **kwargs) for op in node.ops] comparators = [_extract(comparator, **kwargs) for comparator in node.comparators] return f"{left} " + " ".join(f"{op} {comp}" for op, comp in zip(ops, comparators)) def _extract_comprehension(node: ast.comprehension, **kwargs: Any) -> str: target = _extract(node.target, **kwargs) iterable = _extract(node.iter, **kwargs) conditions = [_extract(condition, **kwargs) for condition in node.ifs] value = f"for {target} in {iterable}" if conditions: value = f"{value} if " + " if ".join(conditions) if node.is_async: return f"async {value}" return value def _extract_constant( node: ast.Constant, *, in_formatted_str: bool = False, in_joined_str: bool = False, **kwargs: Any, ) -> str: if in_joined_str and not in_formatted_str and isinstance(node.value, str): return node.value return {type(...): lambda _: "..."}.get(type(node.value), repr)(node.value) def _extract_dict(node: ast.Dict, **kwargs: Any) -> str: pairs = zip(node.keys, node.values) gen = (f"{'None' if key is None else _extract(key, **kwargs)}: {_extract(value, **kwargs)}" for key, value in pairs) return "{" + ", ".join(gen) + "}" def _extract_dictcomp(node: ast.DictComp, **kwargs: Any) -> str: key = _extract(node.key, **kwargs) value = _extract(node.value, **kwargs) generators = [_extract(gen, **kwargs) for gen in node.generators] return f"{{{key}: {value} " + " ".join(generators) + "}" def _extract_div(node: ast.Div, **kwargs: Any) -> str: return "/" def _extract_eq(node: ast.Eq, **kwargs: Any) -> str: return "==" def _extract_floordiv(node: ast.FloorDiv, **kwargs: Any) -> str: return "//" def _extract_formatted(node: ast.FormattedValue, *, in_formatted_str: bool = False, **kwargs: Any) -> str: return f"{{{_extract(node.value, in_formatted_str=True, **kwargs)}}}" def _extract_generatorexp(node: ast.GeneratorExp, **kwargs: Any) -> str: element = _extract(node.elt, **kwargs) generators = [_extract(gen, **kwargs) for gen in node.generators] return f"{element} " + " ".join(generators) def _extract_gte(node: ast.NotEq, **kwargs: Any) -> str: return ">=" def _extract_gt(node: ast.NotEq, **kwargs: Any) -> str: return ">" def _extract_ifexp(node: ast.IfExp, **kwargs: Any) -> str: return f"{_extract(node.body, **kwargs)} if {_extract(node.test, **kwargs)} else {_extract(node.orelse, **kwargs)}" def _extract_invert(node: ast.Invert, **kwargs: Any) -> str: return "~" def _extract_in(node: ast.In, **kwargs: Any) -> str: return "in" def _extract_is(node: ast.Is, **kwargs: Any) -> str: return "is" def _extract_isnot(node: ast.IsNot, **kwargs: Any) -> str: return "is not" def _extract_joinedstr(node: ast.JoinedStr, **kwargs: Any) -> str: return "f" + repr("".join(_extract(value, in_joined_str=True, **kwargs) for value in node.values)) def _extract_keyword(node: ast.keyword, **kwargs: Any) -> str: if node.arg is None: return f"**{_extract(node.value, **kwargs)}" return f"{node.arg}={_extract(node.value, **kwargs)}" def _extract_lambda(node: ast.Lambda, **kwargs: Any) -> str: return f"lambda {_extract(node.args, **kwargs)}: {_extract(node.body, **kwargs)}" def _extract_list(node: ast.List, **kwargs: Any) -> str: return "[" + ", ".join(_extract(el, **kwargs) for el in node.elts) + "]" def _extract_listcomp(node: ast.ListComp, **kwargs: Any) -> str: element = _extract(node.elt, **kwargs) generators = [_extract(gen, **kwargs) for gen in node.generators] return f"[{element} " + " ".join(generators) + "]" def _extract_lshift(node: ast.LShift, **kwargs: Any) -> str: return "<<" def _extract_lte(node: ast.NotEq, **kwargs: Any) -> str: return "<=" def _extract_lt(node: ast.NotEq, **kwargs: Any) -> str: return "<" def _extract_matmult(node: ast.MatMult, **kwargs: Any) -> str: return "@" def _extract_mod(node: ast.Mod, **kwargs: Any) -> str: return "%" def _extract_mult(node: ast.Mult, **kwargs: Any) -> str: return "*" def _extract_name(node: ast.Name, **kwargs: Any) -> str: return node.id def _extract_named_expr(node: ast.NamedExpr, **kwargs: Any) -> str: return f"({_extract(node.target, **kwargs)} := {_extract(node.value, **kwargs)})" def _extract_not(node: ast.Not, **kwargs: Any) -> str: return "not " def _extract_noteq(node: ast.NotEq, **kwargs: Any) -> str: return "!=" def _extract_notin(node: ast.NotIn, **kwargs: Any) -> str: return "not in" def _extract_or(node: ast.Or, **kwargs: Any) -> str: return "or" def _extract_pow(node: ast.Pow, **kwargs: Any) -> str: return "**" def _extract_rshift(node: ast.RShift, **kwargs: Any) -> str: return ">>" def _extract_set(node: ast.Set, **kwargs: Any) -> str: return "{" + ", ".join(_extract(el, **kwargs) for el in node.elts) + "}" def _extract_setcomp(node: ast.SetComp, **kwargs: Any) -> str: element = _extract(node.elt, **kwargs) generators = [_extract(gen, **kwargs) for gen in node.generators] return f"{{{element} " + " ".join(generators) + "}" def _extract_slice(node: ast.Slice, **kwargs: Any) -> str: lower = _extract(node.lower, **kwargs) if node.lower else "" upper = _extract(node.upper, **kwargs) if node.upper else "" value = f"{lower}:{upper}" if node.step: return f"{value}:{_extract(node.step, **kwargs)}" return value def _extract_starred(node: ast.Starred, **kwargs: Any) -> str: return f"*{_extract(node.value, **kwargs)}" def _extract_sub(node: ast.Sub, **kwargs: Any) -> str: return "-" def _extract_subscript(node: ast.Subscript, **kwargs: Any) -> str: subscript = _extract(node.slice, **kwargs) if isinstance(subscript, str) and subscript.startswith("(") and subscript.endswith(")"): subscript = subscript[1:-1] return f"{_extract(node.value, **kwargs)}[{subscript}]" def _extract_tuple(node: ast.Tuple, **kwargs: Any) -> str: return "(" + ", ".join(_extract(el, **kwargs) for el in node.elts) + ")" def _extract_uadd(node: ast.UAdd, **kwargs: Any) -> str: return "+" def _extract_unaryop(node: ast.UnaryOp, **kwargs: Any) -> str: return f"{_extract(node.op, **kwargs)}{_extract(node.operand, **kwargs)}" def _extract_usub(node: ast.USub, **kwargs: Any) -> str: return "-" def _extract_yield(node: ast.Yield, **kwargs: Any) -> str: if node.value is None: return repr(None) return _extract(node.value, **kwargs) _node_map: dict[type, Callable[[Any], str]] = { ast.Add: _extract_add, ast.And: _extract_and, ast.arguments: _extract_arguments, ast.Attribute: _extract_attribute, ast.BinOp: _extract_binop, ast.BitAnd: _extract_bitand, ast.BitOr: _extract_bitor, ast.BitXor: _extract_bitxor, ast.BoolOp: _extract_boolop, ast.Call: _extract_call, ast.Compare: _extract_compare, ast.comprehension: _extract_comprehension, ast.Constant: _extract_constant, ast.DictComp: _extract_dictcomp, ast.Dict: _extract_dict, ast.Div: _extract_div, ast.Eq: _extract_eq, ast.FloorDiv: _extract_floordiv, ast.FormattedValue: _extract_formatted, ast.GeneratorExp: _extract_generatorexp, ast.GtE: _extract_gte, ast.Gt: _extract_gt, ast.IfExp: _extract_ifexp, ast.In: _extract_in, ast.Invert: _extract_invert, ast.Is: _extract_is, ast.IsNot: _extract_isnot, ast.JoinedStr: _extract_joinedstr, ast.keyword: _extract_keyword, ast.Lambda: _extract_lambda, ast.ListComp: _extract_listcomp, ast.List: _extract_list, ast.LShift: _extract_lshift, ast.LtE: _extract_lte, ast.Lt: _extract_lt, ast.MatMult: _extract_matmult, ast.Mod: _extract_mod, ast.Mult: _extract_mult, ast.Name: _extract_name, ast.NamedExpr: _extract_named_expr, ast.NotEq: _extract_noteq, ast.Not: _extract_not, ast.NotIn: _extract_notin, ast.Or: _extract_or, ast.Pow: _extract_pow, ast.RShift: _extract_rshift, ast.SetComp: _extract_setcomp, ast.Set: _extract_set, ast.Slice: _extract_slice, ast.Starred: _extract_starred, ast.Sub: _extract_sub, ast.Subscript: _extract_subscript, ast.Tuple: _extract_tuple, ast.UAdd: _extract_uadd, ast.UnaryOp: _extract_unaryop, ast.USub: _extract_usub, ast.Yield: _extract_yield, } # TODO: remove once Python 3.8 support is if sys.version_info < (3, 9): def _extract_extslice(node: ast.ExtSlice, **kwargs: Any) -> str: return ",".join(_extract(dim, **kwargs) for dim in node.dims) def _extract_index(node: ast.Index, **kwargs: Any) -> str: return _extract(node.value, **kwargs) _node_map[ast.ExtSlice] = _extract_extslice _node_map[ast.Index] = _extract_index def _extract(node: ast.AST, **kwargs: Any) -> str: return _node_map[type(node)](node, **kwargs) 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 _extract(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 __all__ = ["get_value", "safe_get_value"] python-griffe-0.40.0/src/griffe/agents/nodes/__init__.py0000644000175000017500000000317714556223422023004 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations from griffe.agents.nodes._all import get__all__, safe_get__all__ from griffe.agents.nodes._ast import ( ast_children, ast_first_child, ast_kind, ast_last_child, ast_next, ast_next_siblings, ast_previous, ast_previous_siblings, ast_siblings, ) from griffe.agents.nodes._docstrings import get_docstring from griffe.agents.nodes._imports import relative_to_absolute from griffe.agents.nodes._names import get_instance_names, get_name, get_names from griffe.agents.nodes._parameters import get_call_keyword_arguments from griffe.agents.nodes._runtime import ObjectKind, ObjectNode from griffe.agents.nodes._values import get_value, safe_get_value from griffe.expressions import ( get_annotation, get_base_class, get_condition, get_expression, safe_get_annotation, safe_get_base_class, safe_get_condition, safe_get_expression, ) __all__ = [ "ast_children", "ast_first_child", "ast_kind", "ast_last_child", "ast_next", "ast_next_siblings", "ast_previous", "ast_previous_siblings", "ast_siblings", "get__all__", "get_annotation", "get_base_class", "get_call_keyword_arguments", "get_condition", "get_docstring", "get_expression", "get_instance_names", "get_name", "get_names", "get_value", "ObjectKind", "ObjectNode", "relative_to_absolute", "safe_get__all__", "safe_get_annotation", "safe_get_base_class", "safe_get_condition", "safe_get_expression", "safe_get_value", ] python-griffe-0.40.0/src/griffe/agents/nodes/_runtime.py0000644000175000017500000002250114556223422023057 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations import inspect from functools import cached_property from inspect import getmodule from typing import Any, ClassVar, Sequence from griffe.enumerations import ObjectKind from griffe.logger import get_logger logger = get_logger(__name__) _cyclic_relationships = { ("os", "nt"), ("os", "posix"), ("numpy.core._multiarray_umath", "numpy.core.multiarray"), ("pymmcore._pymmcore_swig", "pymmcore.pymmcore_swig"), } 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. """ # low level stuff known to cause issues when resolving aliases exclude_specials: ClassVar[set[str]] = {"__builtins__", "__loader__", "__spec__"} 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. """ 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}") 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.""" 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.""" 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 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_method: return ObjectKind.METHOD if self.is_builtin_method: return ObjectKind.BUILTIN_METHOD if self.is_coroutine: return ObjectKind.COROUTINE if self.is_function: return ObjectKind.FUNCTION if self.is_builtin_function: return ObjectKind.BUILTIN_FUNCTION if self.is_cached_property: return ObjectKind.CACHED_PROPERTY if self.is_property: return ObjectKind.PROPERTY if self.is_method_descriptor: return ObjectKind.METHOD_DESCRIPTOR 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.""" return inspect.isfunction(self.obj) @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 is_cached_property(self) -> bool: """Whether this node's object is a cached property.""" return isinstance(self.obj, 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.""" # the whole point of the following logic is to deal with these cases: # - parent object has a module member # - if this module is not a submodule of the parent, alias it # - but break special cycles coming from builtin modules # like ast -> _ast -> ast (here we inspect _ast) # or os -> posix/nt -> os (here we inspect posix/nt) if self.parent is None: return None obj = self.obj if isinstance(obj, cached_property): obj = obj.func try: child_module = getmodule(obj) except Exception: # noqa: BLE001 return None if not child_module: return None if self.parent.is_module: parent_module = self.parent.obj else: parent_module = getmodule(self.parent.obj) if not parent_module: return None parent_module_path = getattr(parent_module.__spec__, "name", parent_module.__name__) child_module_path = getattr(child_module.__spec__, "name", child_module.__name__) parent_base_name = parent_module_path.split(".")[-1] child_base_name = child_module_path.split(".")[-1] # special cases: inspect.getmodule does not return the real modules # for those, but rather the "user-facing" ones - we prevent that # and use the real parent module if ( parent_module_path, child_module_path, ) in _cyclic_relationships or parent_base_name == f"_{child_base_name}": child_module = parent_module child_module_path = getattr(child_module.__spec__, "name", child_module.__name__) # type: ignore[union-attr] if child_module_path == self.module.path or child_module_path.startswith(self.module.path + "."): return None child_module_path = child_module_path.lstrip("_") if self.kind is ObjectKind.MODULE: return child_module_path child_name = getattr(self.obj, "__name__", self.name) return f"{child_module_path}.{child_name}" __all__ = ["ObjectKind", "ObjectNode"] python-griffe-0.40.0/src/griffe/agents/nodes/_docstrings.py0000644000175000017500000000211114556223422023546 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations import ast from griffe.logger import get_logger logger = get_logger(__name__) 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 __all__ = ["get_docstring"] python-griffe-0.40.0/src/griffe/agents/nodes/_imports.py0000644000175000017500000000217114556223422023072 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations from typing import TYPE_CHECKING from griffe.logger import get_logger if TYPE_CHECKING: import ast from griffe.dataclasses import Module logger = get_logger(__name__) 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 __all__ = ["relative_to_absolute"] python-griffe-0.40.0/src/griffe/agents/nodes/_ast.py0000644000175000017500000001006314556223422022163 0ustar carstencarsten"""This module contains utilities for extracting information from nodes.""" from __future__ import annotations from ast import AST from typing import Iterator from griffe.exceptions import LastNodeError from griffe.logger import get_logger logger = get_logger(__name__) 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 __all__ = [ "ast_children", "ast_first_child", "ast_kind", "ast_last_child", "ast_next", "ast_next_siblings", "ast_previous", "ast_previous_siblings", "ast_siblings", ] python-griffe-0.40.0/src/griffe/agents/visitor.py0000644000175000017500000006261214556223422021633 0ustar carstencarsten"""Code parsing and data extraction utilies. This module exposes a public function, [`visit()`][griffe.agents.visitor.visit], which parses the module code using [`parse()`][ast.parse], and returns a new [`Module`][griffe.dataclasses.Module] instance, populating its members recursively, by using a [`NodeVisitor`][ast.NodeVisitor]-like class. """ from __future__ import annotations import ast from contextlib import suppress from itertools import zip_longest from typing import TYPE_CHECKING, Any, Iterable from griffe.agents.nodes import ( ast_children, ast_kind, ast_next, get_docstring, get_instance_names, get_names, relative_to_absolute, safe_get__all__, safe_get_annotation, safe_get_base_class, safe_get_condition, ) from griffe.collections import LinesCollection, ModulesCollection from griffe.dataclasses import ( Alias, Attribute, Class, Decorator, Docstring, Function, Kind, Module, Parameter, ParameterKind, Parameters, ) from griffe.exceptions import AliasResolutionError, CyclicAliasError, LastNodeError from griffe.expressions import Expr, safe_get_expression from griffe.extensions import Extensions if TYPE_CHECKING: from pathlib import Path from griffe.docstrings.parsers import Parser builtin_decorators = { "property": "property", "staticmethod": "staticmethod", "classmethod": "classmethod", } 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"}, } typing_overload = {"typing.overload", "typing_extensions.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. 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 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 self.filepath: Path = filepath self.code: str = code self.extensions: Extensions = extensions.attach_visitor(self) self.parent: Module | None = parent self.current: Module | Class = None # type: ignore[assignment] self.docstring_parser: Parser | None = docstring_parser self.docstring_options: dict[str, Any] = docstring_options or {} self.lines_collection: LinesCollection = lines_collection or LinesCollection() self.modules_collection: ModulesCollection = modules_collection or ModulesCollection() self.type_guarded: bool = False 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() annotation: str | Expr | None posonlyargs = node.args.posonlyargs # TODO: probably some optimizations to do here args_kinds_defaults: Iterable = reversed( ( *zip_longest( reversed( ( *zip_longest( posonlyargs, [], fillvalue=ParameterKind.positional_only, ), *zip_longest(node.args.args, [], fillvalue=ParameterKind.positional_or_keyword), ), ), reversed(node.args.defaults), fillvalue=None, ), ), ) arg: ast.arg kind: ParameterKind arg_default: ast.AST | None for (arg, kind), arg_default in args_kinds_defaults: annotation = safe_get_annotation(arg.annotation, parent=self.current) default = safe_get_expression(arg_default, parent=self.current, parse_strings=False) parameters.add(Parameter(arg.arg, annotation=annotation, kind=kind, default=default)) if node.args.vararg: annotation = safe_get_annotation(node.args.vararg.annotation, parent=self.current) parameters.add( Parameter( node.args.vararg.arg, annotation=annotation, kind=ParameterKind.var_positional, default="()", ), ) # TODO: probably some optimizations to do here kwargs_defaults: Iterable = reversed( ( *zip_longest( reversed(node.args.kwonlyargs), reversed(node.args.kw_defaults), fillvalue=None, ), ), ) kwarg: ast.arg kwarg_default: ast.AST | None for kwarg, kwarg_default in kwargs_defaults: annotation = safe_get_annotation(kwarg.annotation, parent=self.current) default = safe_get_expression(kwarg_default, parent=self.current, parse_strings=False) parameters.add( Parameter(kwarg.arg, annotation=annotation, kind=ParameterKind.keyword_only, default=default), ) if node.args.kwarg: annotation = safe_get_annotation(node.args.kwarg.annotation, parent=self.current) parameters.add( Parameter( node.args.kwarg.arg, annotation=annotation, kind=ParameterKind.var_keyword, default="{}", ), ) 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.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 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 = 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(safe_get__all__(node, self.current)) # type: ignore[arg-type,union-attr] 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 __all__ = ["visit", "Visitor"] python-griffe-0.40.0/src/griffe/docstrings/0000755000175000017500000000000014556223422020451 5ustar carstencarstenpython-griffe-0.40.0/src/griffe/docstrings/sphinx.py0000644000175000017500000003577514556223422022355 0ustar carstencarsten"""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.dataclasses import ( DocstringAttribute, DocstringParameter, DocstringRaise, DocstringReturn, DocstringSection, DocstringSectionAttributes, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReturns, DocstringSectionText, ) from griffe.docstrings.utils import warning if TYPE_CHECKING: from griffe.dataclasses import Docstring from griffe.expressions import Expr _warn = warning(__name__) # 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(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.returns # 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), ] __all__ = ["parse"] python-griffe-0.40.0/src/griffe/docstrings/__init__.py0000644000175000017500000000024114556223422022557 0ustar carstencarsten"""This module exposes objects related to docstrings.""" from griffe.docstrings.parsers import Parser, parse, parsers __all__ = ["Parser", "parse", "parsers"] python-griffe-0.40.0/src/griffe/docstrings/dataclasses.py0000644000175000017500000003322314556223422023315 0ustar carstencarsten"""This module contains the dataclasses related to docstrings.""" 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: return self.annotation class DocstringClass(DocstringNamedElement): """This class represents a documented class.""" @property def signature(self) -> str | Expr | None: 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: 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) __all__ = [ "DocstringAdmonition", "DocstringAttribute", "DocstringDeprecated", "DocstringElement", "DocstringNamedElement", "DocstringParameter", "DocstringRaise", "DocstringReceive", "DocstringReturn", "DocstringSection", "DocstringSectionAdmonition", "DocstringSectionAttributes", "DocstringSectionDeprecated", "DocstringSectionExamples", "DocstringSectionKind", "DocstringSectionOtherParameters", "DocstringSectionParameters", "DocstringSectionRaises", "DocstringSectionReceives", "DocstringSectionReturns", "DocstringSectionText", "DocstringSectionWarns", "DocstringSectionYields", "DocstringWarn", "DocstringYield", ] python-griffe-0.40.0/src/griffe/docstrings/google.py0000644000175000017500000007751114556223422022312 0ustar carstencarsten"""This module defines functions to parse Google-style docstrings into structured data.""" from __future__ import annotations import re from contextlib import suppress from typing import TYPE_CHECKING, List, Tuple from griffe.docstrings.dataclasses import ( DocstringAttribute, DocstringClass, DocstringFunction, DocstringModule, DocstringParameter, DocstringRaise, DocstringReceive, DocstringReturn, DocstringSection, DocstringSectionAdmonition, DocstringSectionAttributes, DocstringSectionClasses, DocstringSectionDeprecated, DocstringSectionExamples, DocstringSectionFunctions, DocstringSectionKind, DocstringSectionModules, DocstringSectionOtherParameters, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReceives, DocstringSectionReturns, DocstringSectionText, DocstringSectionWarns, DocstringSectionYields, DocstringWarn, DocstringYield, ) from griffe.docstrings.utils import parse_annotation, warning from griffe.expressions import ExprName from griffe.logger import LogLevel if TYPE_CHECKING: from typing import Any, Literal, Pattern from griffe.dataclasses import Docstring from griffe.expressions import Expr _warn = warning(__name__) _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_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_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_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_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_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_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( 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_annotation(annotation, docstring))], ), ) return sections __all__ = ["parse"] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/docstrings/parsers.py�����������������������������������������������0000644�0001750�0001750�00000002434�14556223422�022505� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module imports all the defined parsers.""" from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal from griffe.docstrings.dataclasses import DocstringSection, DocstringSectionText from griffe.docstrings.google import parse as parse_google from griffe.docstrings.numpy import parse as parse_numpy from griffe.docstrings.sphinx import parse as parse_sphinx from griffe.enumerations import Parser if TYPE_CHECKING: from griffe.dataclasses 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)] __all__ = ["parse", "Parser", "parsers"] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/docstrings/utils.py�������������������������������������������������0000644�0001750�0001750�00000005512�14556223422�022166� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains utilities for docstrings parsers.""" from __future__ import annotations from ast import PyCF_ONLY_AST from contextlib import suppress from typing import TYPE_CHECKING, Protocol from griffe.agents.nodes import safe_get_annotation from griffe.exceptions import BuiltinModuleError from griffe.logger import LogLevel, get_logger if TYPE_CHECKING: from griffe.dataclasses import Docstring from griffe.expressions import Expr class WarningCallable(Protocol): def __call__(self, docstring: Docstring, offset: int, message: str, log_level: LogLevel = ...) -> None: ... def warning(name: str) -> WarningCallable: """Create and return a warn function. Parameters: name: The logger name. Returns: A function used to log parsing warnings. This function logs a warning message by prefixing it with the filepath and line number. Other parameters: Parameters of the returned function: docstring (Docstring): The docstring object. offset (int): The offset in the docstring lines. message (str): The message to log. """ logger = get_logger(name) def warn(docstring: Docstring, offset: int, message: str, log_level: LogLevel = LogLevel.warning) -> None: try: prefix = docstring.parent.relative_filepath # type: ignore[union-attr] except (AttributeError, ValueError): prefix = "<module>" except BuiltinModuleError: prefix = f"<module: {docstring.parent.module.name}>" # type: ignore[union-attr] log = getattr(logger, log_level.value) log(f"{prefix}:{(docstring.lineno or 0)+offset}: {message}") return warn def parse_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 __all__ = ["parse_annotation", "warning"] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/docstrings/numpy.py�������������������������������������������������0000644�0001750�0001750�00000071057�14556223422�022205� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module defines functions to parse Numpy-style docstrings into structured data. Based on https://numpydoc.readthedocs.io/en/latest/format.html, it seems Numpydoc is a superset of RST. Since fully parsing RST is a non-goal of this project, some things are stripped from the Numpydoc specification. Rejected as non particularly Pythonic or useful as sections: - See also: this section feels too subjective (specially crafted as a standard for Numpy itself), and there are may ways to reference related items in a docstring, depending on the chosen markup. Rejected as naturally handled by the user-chosen markup: - Warnings: this is just markup. - Notes: again, just markup. - References: again, just markup. """ from __future__ import annotations import re from contextlib import suppress from textwrap import dedent from typing import TYPE_CHECKING from griffe.docstrings.dataclasses import ( DocstringAttribute, DocstringClass, DocstringFunction, DocstringModule, DocstringParameter, DocstringRaise, DocstringReceive, DocstringReturn, DocstringSection, DocstringSectionAdmonition, DocstringSectionAttributes, DocstringSectionClasses, DocstringSectionDeprecated, DocstringSectionExamples, DocstringSectionFunctions, DocstringSectionKind, DocstringSectionModules, DocstringSectionOtherParameters, DocstringSectionParameters, DocstringSectionRaises, DocstringSectionReceives, DocstringSectionReturns, DocstringSectionText, DocstringSectionWarns, DocstringSectionYields, DocstringWarn, DocstringYield, ) from griffe.docstrings.utils import parse_annotation, warning from griffe.expressions import ExprName from griffe.logger import LogLevel if TYPE_CHECKING: from typing import Any, Literal, Pattern from griffe.dataclasses import Docstring from griffe.expressions import Expr _warn = warning(__name__) _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<nt_name>{_RE_NAME})\s*:\s*(?P<nt_type>{_RE_TYPE}) # name and type | # or (?P<name>{_RE_NAME})\s*:\s* # just name | # or \s*:\s*$ # no name, no type | # or (?::\s*)?(?P<type>{_RE_TYPE})\s* # just type ) """, re.IGNORECASE | re.VERBOSE, ) _RE_YIELDS: Pattern = _RE_RETURNS _RE_RECEIVES: Pattern = _RE_RETURNS _RE_PARAMETER: Pattern = re.compile( rf""" (?P<names>{_RE_NAME}(?:,\s{_RE_NAME})*) (?: \s:\s (?: (?:{_RE_OB}(?P<choices>.+){_RE_CB})| (?P<type>{_RE_TYPE}) )? )? """, re.IGNORECASE | re.VERBOSE, ) _RE_DOCTEST_BLANKLINE: Pattern = re.compile(r"^\s*<BLANKLINE>\s*$") _RE_DOCTEST_FLAGS: Pattern = re.compile(r"(\s*#\s*doctest:.+)$") def _read_parameters( docstring: Docstring, *, offset: int, warn_unknown_params: bool = True, **options: Any, ) -> tuple[list[DocstringParameter], int]: parameters = [] 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<annotation>.+),\s+default(?: |: |=)(?P<default>.+)$", annotation) if match: default = match.group("default") annotation = match.group("annotation") if annotation and annotation.endswith(", optional"): annotation = annotation[:-10] description = "\n".join(item[1:]).rstrip() if len(item) > 1 else "" if annotation is None: # try to use the annotation from the signature for name in names: with suppress(AttributeError, KeyError): annotation = docstring.parent.parameters[name].annotation # type: ignore[union-attr] break else: _warn(docstring, new_offset, f"No types or annotations for parameters {names}") else: annotation = parse_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_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_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_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_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_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_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: sections.append( DocstringSectionAdmonition( kind=admonition_title.lower().replace(" ", "-"), 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( 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 __all__ = ["parse"] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/py.typed������������������������������������������������������������0000644�0001750�0001750�00000000000�14556223422�017757� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/mixins.py�����������������������������������������������������������0000644�0001750�0001750�00000032061�14556223422�020155� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains some mixins classes about accessing and setting members.""" from __future__ import annotations import json from contextlib import suppress from typing import TYPE_CHECKING, Any, Sequence, TypeVar from griffe.enumerations import Kind from griffe.exceptions import AliasResolutionError, CyclicAliasError from griffe.logger import get_logger from griffe.merger import merge_stubs if TYPE_CHECKING: from griffe.dataclasses import Alias, Attribute, Class, Function, Module, Object logger = get_logger(__name__) _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.""" 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] class DelMembersMixin: """Mixin class to share methods for deleting members.""" 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.""" 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: """A mixin that adds de/serialization conveniences.""" 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): """A mixin for methods that appear both in objects and aliases, unchanged.""" @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] def is_exported(self, *, explicitely: bool = True) -> bool: """Tell if this object/alias is implicitely exported by its parent. Parameters: explicitely: Whether to only return True when `__all__` is defined. Returns: True or False. """ return self.parent.member_is_exported(self, explicitely=explicitely) # type: ignore[attr-defined] @property def is_explicitely_exported(self) -> bool: """Whether this object/alias is explicitely exported by its parent.""" return self.is_exported(explicitely=True) @property def is_implicitely_exported(self) -> bool: """Whether this object/alias is implicitely exported by its parent.""" return self.parent.exports is None # type: ignore[attr-defined] def is_public( self, *, strict: bool = False, check_name: bool = True, ) -> 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. - In strict mode, the object is public only if it is explicitely exported (listed in `__all__`). Strict mode should only be used for module members. - Otherwise, if name checks are enabled, the object is private if its name starts with an underscore. - Otherwise, if the object is an alias, and is neither inherited from a base class, nor a member of a parent alias, it is not public. - Otherwise, the object is public. """ if self.public is not None: # type: ignore[attr-defined] return self.public # type: ignore[attr-defined] if self.is_explicitely_exported: return True if strict: return False if check_name and self.name.startswith("_"): # type: ignore[attr-defined] return False if self.is_alias and not (self.inherited or self.parent.is_alias): # type: ignore[attr-defined] return False return True __all__ = [ "DelMembersMixin", "GetMembersMixin", "ObjectAliasMixin", "SerializationMixin", "SetMembersMixin", ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/debug.py������������������������������������������������������������0000644�0001750�0001750�00000005201�14556223422�017730� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Debugging utilities.""" 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.""" 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, 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}") 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}") if __name__ == "__main__": print_debug_info() �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/finder.py�����������������������������������������������������������0000644�0001750�0001750�00000050341�14556223422�020116� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""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): # >>> 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 from griffe.logger import get_logger if TYPE_CHECKING: from typing import Pattern from griffe.dataclasses import Module NamePartsType = Tuple[str, ...] NamePartsAndPathType = Tuple[NamePartsType, Path] logger = get_logger(__name__) _editable_editables_patterns = [re.compile(pat) for pat in (r"^__editables_\w+\.py$", r"^_editable_impl_\w+\.py$")] _editable_setuptools_patterns = [re.compile(pat) for pat in (r"^__editable__\w+\.py$",)] _editable_scikit_build_core_patterns = [re.compile(pat) for pat in (r"^_\w+_editable.py$",)] _editable_meson_python_patterns = [re.compile(pat) for pat in (r"^_\w+_editable_loader.py$",)] 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.""" 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 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]: 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__": if path.parent.is_absolute(): return path.parent.name, path return path.parent.resolve().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 for search_path in self.search_paths: with suppress(ValueError): # FIXME: It does not work when `parent_path` is relative and `search_path` absolute. rel_path = parent_path.relative_to(search_path) top_path = search_path / rel_path.parts[0] return top_path.name # 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 = [] for line in path.read_text(encoding="utf8").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'}`. parsed_module = ast.parse(path.read_text()) for node in parsed_module.body: if ( isinstance(node, ast.Assign) and isinstance(node.targets[0], ast.Name) and node.targets[0].id == "MAPPING" ) and isinstance(node.value, ast.Dict): return [_SP(Path(cst.value).parent) for cst in node.value.values if isinstance(cst, ast.Constant)] 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") pkg_name = next(build_path.iterdir()).name return [_SP(build_path, always_scan_for=pkg_name)] raise UnhandledEditableModuleError(path) __all__ = ["ModuleFinder", "NamespacePackage", "Package"] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/exceptions.py�������������������������������������������������������0000644�0001750�0001750�00000005471�14556223422�021034� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains all the exceptions specific to Griffe.""" from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from griffe.dataclasses 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.""" __all__ = [ "AliasResolutionError", "BuiltinModuleError", "CyclicAliasError", "ExtensionError", "ExtensionNotLoadedError", "GitError", "GriffeError", "LastNodeError", "LoadingError", "NameResolutionError", "RootNodeError", "UnhandledEditableModuleError", "UnimportableModuleError", ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/loader.py�����������������������������������������������������������0000644�0001750�0001750�00000102347�14556223422�020121� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains the code allowing to load modules data. This is the entrypoint to use griffe programatically: ```python from griffe.loader import GriffeLoader griffe = GriffeLoader() fastapi = griffe.load("fastapi") ``` """ from __future__ import annotations import sys import warnings from contextlib import suppress from datetime import datetime, timezone 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.dataclasses import Alias, Kind, Module, Object from griffe.exceptions import AliasResolutionError, CyclicAliasError, LoadingError, UnimportableModuleError from griffe.expressions import ExprName from griffe.extensions import Extensions from griffe.finder import ModuleFinder, NamespacePackage, Package from griffe.logger import get_logger from griffe.merger import merge_stubs from griffe.stats import stats if TYPE_CHECKING: from pathlib import Path from griffe.docstrings.parsers import Parser logger = get_logger(__name__) _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"} 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, 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 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.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, } # TODO: Remove at some point. 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.loader.GriffeLoader.load]. """ warnings.warn( "The `load_module` method was renamed `load`, and is deprecated.", DeprecationWarning, stacklevel=2, ) return self.load( 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, # TODO: Remove at some point. module: str | Path | None = None, ) -> Object: """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.dataclasses.Module") Class("Module") >>> loader.load("src/griffe/dataclasses.py") Module("dataclasses") 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. """ # TODO: Remove at some point. 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 if objspec in _builtin_modules: logger.debug(f"{objspec} is a builtin module") if self.allow_inspection: logger.debug(f"Inspecting {objspec}") obj_path = objspec # type: ignore[assignment] top_module = self._inspect_module(objspec) # type: ignore[arg-type] 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 raise LoadingError("Cannot load builtin module without inspection") 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: logger.debug(f"Could not find {objspec}") if self.allow_inspection: logger.debug(f"Trying inspection on {objspec}") obj_path = objspec # type: ignore[assignment] top_module = self._inspect_module(objspec) # type: ignore[arg-type] self.modules_collection.set_member(top_module.path, top_module) else: raise else: 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 obj = self.modules_collection.get_member(obj_path) self.extensions.call("on_package_loaded", pkg=obj) return obj def resolve_aliases( self, *, implicit: bool = False, external: bool = False, 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 = False, 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 not external: continue try: self.load(package, try_relative_path=False) except ImportError 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 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 = False, 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_explicitely_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 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 as error: logger.debug(f"Could not follow alias {member.path}: {error}") load_failures.add(package) 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) -> dict: """Compute some statistics. Returns: Some statistics. """ return {**stats(self), **self._time_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 module_path.suffix in {".py", ".pyi"}: code = module_path.read_text(encoding="utf8") module = self._visit_module(code, 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: parent_module.set_member( submodule_name, self._load_module( submodule_name, subpath, submodules=False, parent=parent_module, ), ) except LoadingError as error: logger.debug(str(error)) 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, code: str, module_name: str, module_path: Path, parent: Module | None = None) -> Module: 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}'") 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, ) 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 explicitely = "__all__" in module.members return [ (imported_member, wildcard_obj.alias_lineno, wildcard_obj.alias_endlineno) for imported_member in module.members.values() if imported_member.is_exported(explicitely=explicitely) ] 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, store_source: bool = True, find_stubs_package: bool = False, # TODO: Remove at some point. module: str | Path | None = None, ) -> Object: """Load and return a module. Example: ```python import griffe module = griffe.load(...) ``` This is a shortcut for: ```python from griffe.loader import GriffeLoader loader = GriffeLoader(...) module = loader.load(...) ``` See the documentation for the loader: [`GriffeLoader`][griffe.loader.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. 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. Returns: A Griffe object. """ return 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, store_source=store_source, ).load( objspec, submodules=submodules, try_relative_path=try_relative_path, find_stubs_package=find_stubs_package, # TODO: Remove at some point. module=module, ) __all__ = ["GriffeLoader", "load"] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/enumerations.py�����������������������������������������������������0000644�0001750�0001750�00000011642�14556223422�021361� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains all the enumerations of the package.""" from __future__ import annotations import enum class DocstringSectionKind(enum.Enum): """Enumeration of the possible docstring section kinds.""" text = "text" """Text section.""" parameters = "parameters" """Parameters section.""" other_parameters = "other parameters" """Other parameters (keyword arguments) section.""" raises = "raises" """Raises (exceptions) section.""" warns = "warns" """Warnings section.""" returns = "returns" """Returned value(s) section.""" yields = "yields" """Yielded value(s) (generators) section.""" receives = "receives" """Received value(s) (generators) section.""" examples = "examples" """Examples section.""" attributes = "attributes" """Attributes section.""" functions = "functions" """Functions section.""" classes = "classes" """Classes section.""" modules = "modules" """Modules section.""" deprecated = "deprecated" """Deprecation section.""" admonition = "admonition" """Admonition block.""" class ParameterKind(enum.Enum): """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(enum.Enum): """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(enum.Enum): """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(enum.Enum): """Enumeration of the possible API breakages.""" PARAMETER_MOVED: str = "Positional parameter was moved" PARAMETER_REMOVED: str = "Parameter was removed" PARAMETER_CHANGED_KIND: str = "Parameter kind was changed" PARAMETER_CHANGED_DEFAULT: str = "Parameter default was changed" PARAMETER_CHANGED_REQUIRED: str = "Parameter is now required" PARAMETER_ADDED_REQUIRED: str = "Parameter was added as required" RETURN_CHANGED_TYPE: str = "Return types are incompatible" OBJECT_REMOVED: str = "Public object was removed" OBJECT_CHANGED_KIND: str = "Public object points to a different kind of object" ATTRIBUTE_CHANGED_TYPE: str = "Attribute types are incompatible" ATTRIBUTE_CHANGED_VALUE: str = "Attribute value was changed" CLASS_REMOVED_BASE: str = "Base class was removed" class Parser(enum.Enum): """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(enum.Enum): """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 class When(enum.Enum): """Enumeration of the different times at which an extension is used.""" 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.40.0/src/griffe/expressions.py������������������������������������������������������0000644�0001750�0001750�00000106334�14556223422�021235� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains the data classes that represent resolvable names and expressions.""" 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.enumerations import ParameterKind from griffe.exceptions import NameResolutionError from griffe.logger import LogLevel, get_logger if TYPE_CHECKING: from pathlib import Path from griffe.dataclasses import Class, Module logger = get_logger(__name__) 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 # TODO: Merge in decorators once Python 3.9 is dropped. 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]: 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.expressions.ExprName]. Yields: Strings and names when flat, strings and expressions otherwise. """ yield from () 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" @dataclass(eq=True, **dataclass_opts) class ExprAttribute(Expr): """Attributes like `a.b`.""" values: list[str | Expr] """The different parts of the dotted chain.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 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 @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]: # noqa: D102 yield from _yield(self.left, flat=flat) yield f" {self.operator} " yield from _yield(self.right, flat=flat) @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]: # noqa: D102 yield from _join(self.values, f" {self.operator} ", flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprCall(Expr): """Calls like `f()`.""" function: Expr """Function called.""" arguments: Sequence[str | Expr] """Passed arguments.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _yield(self.function, flat=flat) yield "(" yield from _join(self.arguments, ", ", flat=flat) yield ")" @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]: # noqa: D102 yield from _yield(self.left, flat=flat) yield " " yield from _join(zip_longest(self.operators, [], self.comparators, fillvalue=" "), " ", flat=flat) @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]: # noqa: D102 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) @dataclass(eq=True, **dataclass_opts) class ExprConstant(Expr): """Constants like `"a"` or `1`.""" value: str """Constant value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: ARG002,D102 yield self.value @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]: # noqa: D102 yield "{" yield from _join( (("None" if key is None else key, ": ", value) for key, value in zip(self.keys, self.values)), ", ", flat=flat, ) yield "}" @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]: # noqa: D102 yield "{" yield from _yield(self.key, flat=flat) yield ": " yield from _yield(self.value, flat=flat) yield from _join(self.generators, " ", flat=flat) yield "}" @dataclass(eq=True, **dataclass_opts) class ExprExtSlice(Expr): """Extended slice like `a[x:y, z]`.""" dims: Sequence[str | Expr] """Dims.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield from _join(self.dims, ", ", flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprFormatted(Expr): """Formatted string like `{1 + 1}`.""" value: str | Expr """Formatted value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "{" yield from _yield(self.value, flat=flat) yield "}" @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]: # noqa: D102 yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) @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]: # noqa: D102 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) @dataclass(eq=True, **dataclass_opts) class ExprJoinedStr(Expr): """Joined strings like `f"a {b} c"`.""" values: Sequence[str | Expr] """Joined values.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "f'" yield from _join(self.values, "", flat=flat) yield "'" @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]: # noqa: D102 yield self.name yield "=" yield from _yield(self.value, flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprVarPositional(Expr): """Variadic positional parameters like `*args`.""" value: Expr """Starred value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "*" yield from _yield(self.value, flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprVarKeyword(Expr): """Variadic keyword parameters like `**kwargs`.""" value: Expr """Double-starred value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "**" yield from _yield(self.value, flat=flat) @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]: # noqa: D102 yield "lambda " yield from _join(self.parameters, ", ", flat=flat) yield ": " yield from _yield(self.body, flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprList(Expr): """Lists like `[0, 1, 2]`.""" elements: Sequence[Expr] """List elements.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "[" yield from _join(self.elements, ", ", flat=flat) yield "]" @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]: # noqa: D102 yield "[" yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) yield "]" @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: if isinstance(other, ExprName): return self.name == other.name return NotImplemented def iterate(self, *, flat: bool = True) -> Iterator[ExprName]: # noqa: ARG002,D102 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 @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]: # noqa: D102 yield "(" yield from _yield(self.target, flat=flat) yield " := " yield from _yield(self.value, flat=flat) yield ")" @dataclass(eq=True, **dataclass_opts) class ExprParameter(Expr): """Parameters in function signatures like `a: int = 0`.""" kind: str """Parameter kind.""" name: str | None = None """Parameter name.""" annotation: Expr | None = None """Parameter type.""" default: Expr | None = None """Parameter default.""" @dataclass(eq=True, **dataclass_opts) class ExprSet(Expr): """Sets like `{0, 1, 2}`.""" elements: Sequence[str | Expr] """Set elements.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "{" yield from _join(self.elements, ", ", flat=flat) yield "}" @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]: # noqa: D102 yield "{" yield from _yield(self.element, flat=flat) yield " " yield from _join(self.generators, " ", flat=flat) yield "}" @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]: # noqa: D102 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) @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]: # noqa: D102 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 @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]: # noqa: D102 if not self.implicit: yield "(" yield from _join(self.elements, ", ", flat=flat) if not self.implicit: yield ")" @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]: # noqa: D102 yield self.operator yield from _yield(self.value, flat=flat) @dataclass(eq=True, **dataclass_opts) class ExprYield(Expr): """Yield statements like `yield a`.""" value: str | Expr | None = None """Yielded value.""" def iterate(self, *, flat: bool = True) -> Iterator[str | Expr]: # noqa: D102 yield "yield" if self.value is not None: yield " " 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="<string-annotation>", 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: # FIXME: This needs better handling (all parameter kinds). return ExprLambda( [ExprParameter(ParameterKind.positional_or_keyword.value, arg.arg) for arg in node.args.args], _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)) _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, } # TODO: remove once Python 3.8 support is dropped 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 = "<in-memory>" 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", ) __all__ = [ "Expr", "ExprAttribute", "ExprBinOp", "ExprBoolOp", "ExprCall", "ExprCompare", "ExprComprehension", "ExprConstant", "ExprDict", "ExprDictComp", "ExprExtSlice", "ExprFormatted", "ExprGeneratorExp", "ExprIfExp", "ExprJoinedStr", "ExprKeyword", "ExprVarPositional", "ExprVarKeyword", "ExprLambda", "ExprList", "ExprListComp", "ExprName", "ExprNamedExpr", "ExprParameter", "ExprSet", "ExprSetComp", "ExprSlice", "ExprSubscript", "ExprTuple", "ExprUnaryOp", "ExprYield", "get_annotation", "get_base_class", "get_condition", "get_expression", "safe_get_annotation", "safe_get_base_class", "safe_get_condition", "safe_get_expression", ] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/encoders.py���������������������������������������������������������0000644�0001750�0001750�00000022566�14556223422�020461� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains data encoders/serializers and decoders/deserializers. The available formats are: - `JSON`: see the [`JSONEncoder`][griffe.encoders.JSONEncoder] and [`json_decoder`][griffe.encoders.json_decoder]. """ 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.dataclasses import ( Alias, Attribute, Class, Decorator, Docstring, Function, Kind, Module, Object, Parameter, ParameterKind, Parameters, ) from griffe.docstrings.dataclasses import DocstringSectionKind if TYPE_CHECKING: from enum import Enum from griffe.docstrings.parsers import Parser def _enum_value(obj: Enum) -> str | int: return obj.value _json_encoder_map: dict[type, Callable[[Any], Any]] = { Path: str, PosixPath: str, WindowsPath: str, ParameterKind: _enum_value, Kind: _enum_value, DocstringSectionKind: _enum_value, 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.encoders import JSONEncoder >>> JSONEncoder(full=True).encode(..., **kwargs) >>> import json >>> from griffe.encoders import JSONEncoder >>> json.dumps(..., cls=JSONEncoder, full=True, **kwargs) """ def __init__( self, *args: Any, full: bool = False, docstring_parser: Parser | None = None, 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.encoders.json_decoder], you don't need the full data as it can be infered again using the base data. If you want to feed a non-Python tool instead, dump the full data. docstring_parser: Deprecated. The docstring parser to use. By default, no parsing is done. docstring_options: Deprecated. Additional docstring parsing options. **kwargs: See [`json.JSONEncoder`][]. """ super().__init__(*args, **kwargs) self.full: bool = full # TODO: Remove at some point. self.docstring_parser: Parser | None = docstring_parser self.docstring_options: dict[str, Any] = docstring_options or {} 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"], ) 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", None), 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", None), 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", None), docstring=_load_docstring(obj_dict), value=obj_dict.get("value", None), annotation=obj_dict.get("annotation", None), ) 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", None), ) _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.encoders 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 __all__ = ["JSONEncoder", "json_decoder"] ������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/stats.py������������������������������������������������������������0000644�0001750�0001750�00000014073�14556223422�020007� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains utilities to compute loading statistics.""" from __future__ import annotations from collections import defaultdict from typing import TYPE_CHECKING, Iterable, Union, cast from griffe.dataclasses import Class, Module from griffe.exceptions import BuiltinModuleError if TYPE_CHECKING: from griffe.dataclasses import Alias, Object from griffe.loader import GriffeLoader def _direct(objects: Iterable[Object | Alias]) -> list[Object | Alias]: return [obj for obj in objects if not obj.is_alias] def _n_modules(module: Module) -> int: submodules = _direct(module.modules.values()) return len(submodules) + sum(_n_modules(cast(Module, mod)) for mod in submodules) def _n_classes(module_or_class: Module | Class) -> int: submodules = _direct(module_or_class.modules.values()) subclasses = _direct(module_or_class.classes.values()) mods_or_classes = [mc for mc in (*submodules, *subclasses) if not mc.is_alias] return len(subclasses) + sum( _n_classes(cast(Union[Module, Class], mod_or_class)) for mod_or_class in mods_or_classes ) def _n_functions(module_or_class: Module | Class) -> int: submodules = _direct(module_or_class.modules.values()) subclasses = _direct(module_or_class.classes.values()) functions = _direct(module_or_class.functions.values()) mods_or_classes = [*submodules, *subclasses] return len(functions) + sum( _n_functions(cast(Union[Module, Class], mod_or_class)) for mod_or_class in mods_or_classes ) def _n_attributes(module_or_class: Module | Class) -> int: submodules = _direct(module_or_class.modules.values()) subclasses = _direct(module_or_class.classes.values()) attributes = _direct(module_or_class.attributes.values()) mods_or_classes = [*submodules, *subclasses] return len(attributes) + sum( _n_attributes(cast(Union[Module, Class], mod_or_class)) for mod_or_class in mods_or_classes ) def _merge_exts(exts1: dict[str, int], exts2: dict[str, int]) -> dict[str, int]: for ext, value in exts2.items(): exts1[ext] += value return exts1 def _sum_extensions(exts: dict[str, int], module: Module) -> None: current_exts = defaultdict(int) try: suffix = module.filepath.suffix # type: ignore[union-attr] except BuiltinModuleError: current_exts[""] = 1 except AttributeError: suffix = "" else: if suffix: current_exts[suffix] = 1 for submodule in _direct(module.modules.values()): _sum_extensions(current_exts, cast(Module, submodule)) _merge_exts(exts, current_exts) def stats(loader: GriffeLoader) -> dict: """Return some loading statistics. Parameters: loader: The loader to compute stats from. Returns: Some statistics. """ 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() for module in top_modules: _sum_extensions(modules_by_extension, module) n_lines = sum(len(lines) for lines in loader.lines_collection.values()) return { "packages": len(top_modules), "modules": len(top_modules) + sum(_n_modules(mod) for mod in top_modules), "classes": sum(_n_classes(mod) for mod in top_modules), "functions": sum(_n_functions(mod) for mod in top_modules), "attributes": sum(_n_attributes(mod) for mod in top_modules), "modules_by_extension": modules_by_extension, "lines": n_lines, } def _format_stats(stats: dict) -> str: lines = [] packages = stats["packages"] modules = stats["modules"] classes = stats["classes"] functions = stats["functions"] attributes = stats["attributes"] 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 = stats["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: {stats['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 = stats["time_spent_visiting"] / 1000 inspect_time = stats["time_spent_inspecting"] / 1000 total_time = visit_time + inspect_time visit_percent = visit_time / total_time * 100 inspect_percent = inspect_time / total_time * 100 try: visit_time_per_module = visit_time / regular except ZeroDivisionError: visit_time_per_module = 0 inspected_modules = builtin + compiled try: inspect_time_per_module = visit_time / inspected_modules except ZeroDivisionError: inspect_time_per_module = 0 lines.append("") lines.append( f"Time spent visiting modules ({regular}): " 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 = stats["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) __all__ = ["stats"] ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/__init__.py���������������������������������������������������������0000644�0001750�0001750�00000002015�14556223422�020401� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""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. """ from __future__ import annotations from griffe.agents.nodes import ObjectNode from griffe.dataclasses import Attribute, Class, Docstring, Function, Module, Object from griffe.diff import find_breaking_changes from griffe.docstrings.parsers import Parser, parse_google, parse_numpy, parse_sphinx from griffe.extensions import Extension, load_extensions from griffe.git import load_git from griffe.importer import dynamic_import from griffe.loader import load from griffe.logger import get_logger __all__: list[str] = [ "Attribute", "Class", "Docstring", "dynamic_import", "Extension", "Function", "find_breaking_changes", "get_logger", "load", "load_extensions", "load_git", "Module", "Object", "ObjectNode", "Parser", "parse_google", "parse_numpy", "parse_sphinx", ] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/dataclasses.py������������������������������������������������������0000644�0001750�0001750�00000166554�14556223422�021154� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains the data classes that represent Python objects. The different objects are modules, classes, functions, and attribute (variables like module/class/instance attributes). """ 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 Parser, parse from griffe.enumerations import Kind, ParameterKind from griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError from griffe.expressions import ExprCall, ExprName from griffe.logger import get_logger from griffe.mixins import ObjectAliasMixin if TYPE_CHECKING: from griffe.collections import LinesCollection, ModulesCollection from griffe.docstrings.dataclasses import DocstringSection from griffe.expressions import Expr from functools import cached_property logger = get_logger(__name__) 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. """ # TODO: Remove at some point. 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, ) -> 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. """ 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.""" 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})" @property def required(self) -> bool: """Whether this parameter is required.""" return self.default is None def as_dict(self, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002 """Return this parameter's data as a dictionary. Parameters: **kwargs: Additional serialization options. Returns: A dictionary. """ return { "name": self.name, "annotation": self.annotation, "kind": self.kind, "default": self.default, } 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: if isinstance(name_or_index, int): return self._parameters_list[name_or_index] return self._parameters_dict[name_or_index.lstrip("*")] def __len__(self): return len(self._parameters_list) def __iter__(self): return iter(self._parameters_list) def __contains__(self, param_name: str): 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 """Whether this object is an alias.""" is_collection: bool = False """Whether this object is a (modules) collection.""" inherited: bool = False """Whether this object (alias) is inherited. Objects can never be inherited, only aliases can. """ 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._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})" def __bool__(self) -> bool: # Prevent using `__len__`. return True def __len__(self) -> int: 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: """Whether a member of this object is "exported". By exported, we mean that the object is included in the `__all__` attribute of its parent module or class. When `__all__` is not defined, we consider the member to be *implicitely* exported, unless it's a module and it was not imported, and unless it's not defined at runtime. Parameters: member: The member to verify. explicitely: Whether to only return True when `__all__` is defined. Returns: True or False. """ if not member.runtime: return False if self.exports is None: return not explicitely and (member.is_alias or not member.is_module or member.name in self.imports) return member.name in self.exports 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 def has_labels(self, labels: set[str]) -> bool: """Tell if this object has all the given labels. Parameters: labels: A set of labels. Returns: True or False. """ return all(label in self.labels for label in 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. 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.""" 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.""" 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. """ 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.lineno is None or self.endlineno is None: return lines 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. """ # 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 = { "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: base["lineno"] = self.lineno if self.endlineno: 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.dataclasses.Kind] """ is_alias: bool = True is_collection: bool = False 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._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})" def __bool__(self) -> bool: # Prevent using `__len__`. return True def __len__(self) -> int: 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: """Whether a member of this object is "exported". By exported, we mean that the object is included in the `__all__` attribute of its parent module or class. When `__all__` is not defined, we consider the member to be *implicitely* exported, unless it's a module and it was not imported, and unless it's not defined at runtime. Parameters: member: The member to verify. explicitely: Whether to only return True when `__all__` is defined. Returns: True or False. """ return self.final_target.member_is_exported(member, explicitely=explicitely) 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: set[str]) -> bool: """Tell if this object has all the given labels. Parameters: labels: A set of labels. 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) def get_member(self, key: str | Sequence[str]) -> Object | Alias: # noqa: D102 return self.final_target.get_member(key) def set_member(self, key: str | Sequence[str], value: Object | Alias) -> None: # noqa: D102 return self.final_target.set_member(key, value) def del_member(self, key: str | Sequence[str]) -> None: # noqa: D102 return self.final_target.del_member(key) # 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 = { "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.dataclasses.Object`][]. filepath: The module file path (directory for namespace [sub]packages, none for builtin modules). **kwargs: See [`griffe.dataclasses.Object`][]. """ super().__init__(*args, **kwargs) self._filepath: Path | list[Path] | None = filepath self.overloads: dict[str, list[Function]] = defaultdict(list) """The overloaded signature declared in this module.""" 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.dataclasses.Object`][]. bases: The list of base classes, if any. decorators: The class decorators, if any. **kwargs: See [`griffe.dataclasses.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: if "dataclass" in self.labels: return Parameters( *[ Parameter(attr.name, annotation=attr.annotation, default=attr.value) for attr in self.attributes.values() ], ) 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.dataclasses.Object`][]. parameters: The function parameters. returns: The function return annotation. decorators: The function decorators, if any. **kwargs: See [`griffe.dataclasses.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.""" @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.dataclasses.Object`][]. value: The attribute value, if any. annotation: The attribute annotation, if any. **kwargs: See [`griffe.dataclasses.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 __all__ = [ "Alias", "Attribute", "Class", "Decorator", "Docstring", "Function", "Kind", "Module", "Object", "Parameter", "ParameterKind", "ParameterKind", "Parameters", ] ����������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/importer.py���������������������������������������������������������0000644�0001750�0001750�00000011306�14556223422�020506� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains utilities to dynamically import objects.""" 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 __all__ = ["dynamic_import", "sys_path"] ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/extensions/���������������������������������������������������������0000755�0001750�0001750�00000000000�14556223422�020471� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/extensions/hybrid.py������������������������������������������������0000644�0001750�0001750�00000007142�14556223422�022330� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Deprecated. This extension provides an hybrid behavior while loading data.""" from __future__ import annotations import re from typing import TYPE_CHECKING, Any, Pattern, Sequence from griffe.agents.nodes import ObjectNode from griffe.exceptions import ExtensionError from griffe.extensions.base import InspectorExtension, VisitorExtension, When, _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(__name__) 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 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 []] super().__init__() def attach(self, visitor: Visitor) -> None: # noqa: D102 super().attach(visitor) for extension in self._extensions: extension.attach(visitor) # type: ignore[arg-type] # tolerate hybrid behavior def visit(self, node: ast.AST) -> None: # noqa: D102 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) __all__ = ["HybridExtension"] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/extensions/__init__.py����������������������������������������������0000644�0001750�0001750�00000000617�14556223422�022606� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module is the public interface to import elements from the base.""" from griffe.extensions.base import ( Extension, Extensions, ExtensionType, InspectorExtension, VisitorExtension, When, load_extensions, ) __all__ = [ "Extension", "Extensions", "ExtensionType", "InspectorExtension", "load_extensions", "VisitorExtension", "When", ] �����������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/extensions/base.py��������������������������������������������������0000644�0001750�0001750�00000040050�14556223422�021754� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module contains the base classes for dealing with extensions.""" 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, Sequence, Union from griffe.agents.nodes import ast_children, ast_kind from griffe.enumerations import When from griffe.exceptions import ExtensionNotLoadedError from griffe.importer import dynamic_import if TYPE_CHECKING: import ast from types import ModuleType from griffe.agents.inspector import Inspector from griffe.agents.nodes import ObjectNode from griffe.agents.visitor import Visitor from griffe.dataclasses import Attribute, Class, Function, Module, Object class VisitorExtension: """Deprecated in favor of `Extension`. The node visitor extension base class, to inherit from.""" when: When = When.after_all 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] 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) class InspectorExtension: """Deprecated in favor of `Extension`. The object inspector extension base class, to inherit from.""" when: When = When.after_all 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] 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. """ ExtensionType = Union[VisitorExtension, InspectorExtension, Extension] class Extensions: """This class helps iterating on extensions that should run at different times.""" def __init__(self, *extensions: ExtensionType) -> None: """Initialize the extensions container. Parameters: *extensions: The extensions to add. """ self._visitors: dict[When, list[VisitorExtension]] = defaultdict(list) self._inspectors: dict[When, list[InspectorExtension]] = defaultdict(list) self._extensions: list[Extension] = [] self.add(*extensions) def add(self, *extensions: ExtensionType) -> None: """Add extensions to this container. Parameters: *extensions: The extensions to add. """ for extension in extensions: 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) 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 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 @property def before_visit(self) -> list[VisitorExtension]: """The visitors that run before the visit.""" return self._visitors[When.before_all] @property def before_children_visit(self) -> list[VisitorExtension]: """The visitors that run before the children visit.""" return self._visitors[When.before_children] @property def after_children_visit(self) -> list[VisitorExtension]: """The visitors that run after the children visit.""" return self._visitors[When.after_children] @property def after_visit(self) -> list[VisitorExtension]: """The visitors that run after the visit.""" return self._visitors[When.after_all] @property def before_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the inspection.""" return self._inspectors[When.before_all] @property def before_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run before the children inspection.""" return self._inspectors[When.before_children] @property def after_children_inspection(self) -> list[InspectorExtension]: """The inspectors that run after the children inspection.""" return self._inspectors[When.after_children] @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] = { "hybrid", } 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 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 ext_classes = (VisitorExtension, InspectorExtension, Extension) # If it's already an extension instance, return it. if isinstance(extension, ext_classes): return extension # If it's an extension class, instantiate it (without options) and return it. 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. # See https://github.com/mkdocs/mkdocs/issues/3414. # TODO: Update when this issue is resolved. 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(":") 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. 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(): 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(exts: Sequence[str | dict[str, Any] | ExtensionType | type[ExtensionType]]) -> Extensions: """Load configured extensions. Parameters: exts: A sequence of extension, with potential configuration options. Returns: An extensions container. """ extensions = Extensions() for extension in exts: ext = _load_extension(extension) if isinstance(ext, list): extensions.add(*ext) else: extensions.add(ext) return extensions __all__ = [ "builtin_extensions", "Extension", "Extensions", "ExtensionType", "InspectorExtension", "load_extensions", "VisitorExtension", "When", ] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/diff.py�������������������������������������������������������������0000644�0001750�0001750�00000051172�14556223422�017562� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module exports "breaking changes" related utilities.""" from __future__ import annotations import contextlib from pathlib import Path from typing import Any, Iterable, Iterator from colorama import Fore, Style from griffe.dataclasses import Alias, Attribute, Class, Function, Object, ParameterKind from griffe.enumerations import BreakageKind, ExplanationStyle from griffe.exceptions import AliasResolutionError from griffe.git import WORKTREE_PREFIX from griffe.logger import get_logger POSITIONAL = frozenset((ParameterKind.positional_only, ParameterKind.positional_or_keyword)) KEYWORD = frozenset((ParameterKind.keyword_only, ParameterKind.positional_or_keyword)) POSITIONAL_KEYWORD_ONLY = frozenset((ParameterKind.positional_only, ParameterKind.keyword_only)) VARIADIC = frozenset((ParameterKind.var_positional, ParameterKind.var_keyword)) logger = get_logger(__name__) class Breakage: """Breakages can explain what broke from a version to another.""" kind: BreakageKind 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 self.old_value = old_value self.new_value = new_value self.details = details 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 self.kind is BreakageKind.OBJECT_REMOVED: 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, *, ignore_private: bool = True, 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, ignore_private=ignore_private, 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, *, ignore_private: bool, seen_paths: set[str], ) -> Iterable[Breakage]: if not ignore_private: return 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, ignore_private=ignore_private, seen_paths=seen_paths) def _member_incompatibilities( old_obj: Object | Alias, new_obj: Object | Alias, *, ignore_private: bool = True, 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 ignore_private and name.startswith("_"): logger.debug(f"API check: {old_obj.path}.{name}: skip private object") continue logger.debug(f"API check: {old_obj.path}.{name}") try: new_member = new_obj.all_members[name] except KeyError: is_module = not old_member.is_alias and old_member.is_module if is_module or old_member.is_exported(explicitely=False): yield ObjectRemovedBreakage(old_member, old_member, None) # type: ignore[arg-type] continue yield from _type_based_yield(old_member, new_member, ignore_private=ignore_private, seen_paths=seen_paths) def _type_based_yield( old_member: Object | Alias, new_member: Object | Alias, *, ignore_private: bool, 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, ignore_private=ignore_private, 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, ignore_private=ignore_private, seen_paths=seen_paths, ) elif old_member.is_class: yield from _class_incompatibilities(old_member, new_member, ignore_private=ignore_private, seen_paths=seen_paths) # type: ignore[arg-type] 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 def find_breaking_changes( old_obj: Object | Alias, new_obj: Object | Alias, *, ignore_private: bool = True, ) -> Iterator[Breakage]: """Find breaking changes between two versions of the same API. The function will iterate recursively on all objects and yield breaking changes with detailed information. Parameters: old_obj: The old version of an object. new_obj: The new version of an object. Yields: Breaking changes. Examples: >>> import sys, griffe >>> new = griffe.load("pkg") >>> old = griffe.load_git("pkg", "1.2.3") >>> for breakage in griffe.find_breaking_changes(old, new) ... print(breakage.explain(style=style), file=sys.stderr) """ yield from _member_incompatibilities(old_obj, new_obj, ignore_private=ignore_private) __all__ = [ "AttributeChangedTypeBreakage", "AttributeChangedValueBreakage", "Breakage", "BreakageKind", "ClassRemovedBaseBreakage", "ExplanationStyle", "find_breaking_changes", "ObjectChangedKindBreakage", "ObjectRemovedBreakage", "ParameterAddedRequiredBreakage", "ParameterChangedDefaultBreakage", "ParameterChangedKindBreakage", "ParameterChangedRequiredBreakage", "ParameterMovedBreakage", "ParameterRemovedBreakage", "ReturnChangedTypeBreakage", ] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/c3linear.py���������������������������������������������������������0000644�0001750�0001750�00000006612�14556223422�020351� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Compute method resolution order. Implements `Class.mro` attribute.""" # 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") __all__ = ["c3linear_merge"] ����������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/cli.py��������������������������������������������������������������0000644�0001750�0001750�00000044235�14556223422�017423� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Module that contains the command line application.""" # Why does this file exist, and why not put this in `__main__`? # # You might be tempted to import things from `__main__` later, # but that will cause problems: the code will get executed twice: # # - When you 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 ExplanationStyle, find_breaking_changes from griffe.docstrings.parsers import Parser from griffe.encoders import JSONEncoder from griffe.exceptions import ExtensionError, GitError from griffe.extensions.base import load_extensions from griffe.git import _get_latest_tag, _get_repo_root, load_git from griffe.loader import GriffeLoader, load from griffe.logger import get_logger from griffe.stats import _format_stats if TYPE_CHECKING: from griffe.extensions import Extensions, ExtensionType DEFAULT_LOG_LEVEL = os.getenv("GRIFFE_LOG_LEVEL", "INFO").upper() logger = get_logger(__name__) 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 = False, allow_inspection: bool = True, 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, 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.", ) 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.", ) 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( "-y", "--sys-path", dest="append_sys_path", action="store_true", help="Whether to append `sys.path` to search paths specified with `-s`.", ) 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", action="store_true", help="Whether to resolve aliases pointing to external/unknown modules (not loaded directly).", ) 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 = False, search_paths: Sequence[str | Path] | None = None, find_stubs_package: bool = False, append_sys_path: bool = False, allow_inspection: bool = True, 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. 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. 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, 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: logger.info(_format_stats({"time_spent_serializing": elapsed.microseconds, **loader.stats()})) 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, find_stubs_package: bool = False, allow_inspection: bool = True, verbose: bool = False, color: bool | None = None, style: str | ExplanationStyle | None = None, ) -> int: """Load packages data and dump it as JSON. 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. allow_inspection: Whether to allow inspecting modules when visiting them is not possible. verbose: Use a verbose output. Returns: `0` for success, `1` for failure. """ # Prepare options. search_paths = list(search_paths) if search_paths else [] 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, ) if base_ref: new_package = load_git( package, ref=base_ref, repo=repository, extensions=loaded_extensions, search_paths=search_paths, allow_inspection=allow_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, find_stubs_package=find_stubs_package, ) # Find and display API breakages. breakages = list(find_breaking_changes(old_package, new_package)) colorama.deinit() colorama.init(strip=color if color is None else not color) if style is None: # noqa: SIM108 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) __all__ = ["check", "dump", "get_parser", "main"] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/collections.py������������������������������������������������������0000644�0001750�0001750�00000004122�14556223422�021161� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""This module stores collections of data, useful during parsing.""" 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.dataclasses 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]: return self._data[key] def __setitem__(self, key: Path, value: list[str]) -> None: self._data[key] = value def __bool__(self) -> bool: 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 def __init__(self) -> None: """Initialize the collection.""" self.members: dict[str, Module] = {} """Members (modules) of the collection.""" def __bool__(self) -> bool: return True def __contains__(self, item: Any) -> bool: 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 __all__ = ["LinesCollection", "ModulesCollection"] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/src/griffe/__main__.py���������������������������������������������������������0000644�0001750�0001750�00000000521�14556223422�020362� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Entry-point module, in case you use `python -m griffe`. Why does this file exist, and why `__main__`? For more info, read: - https://www.python.org/dev/peps/pep-0338/ - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ import sys from griffe.cli import main if __name__ == "__main__": sys.exit(main(sys.argv[1:])) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/scripts/�����������������������������������������������������������������������0000755�0001750�0001750�00000000000�14556223422�015730� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/scripts/gen_ref_nav.py���������������������������������������������������������0000644�0001750�0001750�00000002324�14556223422�020554� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Generate the code reference pages and navigation.""" from pathlib import Path import mkdocs_gen_files nav = mkdocs_gen_files.Nav() mod_symbol = '<code class="doc-symbol doc-symbol-nav doc-symbol-module"></code>' exclude = {"src/griffe/agents/extensions/base.py"} src = Path(__file__).parent.parent / "src" for path in sorted(src.rglob("*.py")): if str(path) in exclude: continue module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") elif parts[-1].startswith("_"): continue nav_parts = [f"{mod_symbol} {part}" for part in parts] nav[tuple(nav_parts)] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/scripts/insiders.py������������������������������������������������������������0000644�0001750�0001750�00000014604�14556223422�020127� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Functions related to Insiders funding goals.""" from __future__ import annotations import json import logging import os import posixpath from dataclasses import dataclass from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path from typing import 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") for feature in self.features: feature.render(rel_base) print("") 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.40.0/scripts/gen_credits.py���������������������������������������������������������0000644�0001750�0001750�00000011470�14556223422�020573� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Script to generate the project's credits.""" from __future__ import annotations import os import re import sys from importlib.metadata import PackageNotFoundError, metadata from itertools import chain from pathlib import Path from textwrap import dedent from typing import Mapping, cast from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment # TODO: Remove once support for Python 3.10 is dropped. 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"] pdm = pyproject["tool"]["pdm"] with project_dir.joinpath("pdm.lock").open("rb") as lock_file: lock_data = tomllib.load(lock_file) lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] regex = re.compile(r"(?P<dist>[\w.-]+)(?P<spec>.*)$") def _get_license(pkg_name: str) -> str: try: data = metadata(pkg_name) except PackageNotFoundError: return "?" license_name = cast(dict, data).get("License", "").strip() multiple_lines = bool(license_name.count("\n")) # TODO: Remove author logic once all my packages licenses are fixed. author = "" if multiple_lines or not license_name or license_name == "UNKNOWN": for header, value in cast(dict, data).items(): if header == "Classifier" and value.startswith("License ::"): license_name = value.rsplit("::", 1)[1].strip() elif header == "Author-email": author = value if license_name == "Other/Proprietary License" and "pawamoy" in author: license_name = "ISC" return license_name or "?" def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: deps = {} for dep in base_deps: parsed = regex.match(dep).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name not in lock_pkgs: continue deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True while again: again = False for pkg_name in lock_pkgs: if pkg_name in deps: for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]: deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True return deps def _render_credits() -> str: dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] prod_dependencies = _get_deps( chain( # type: ignore[arg-type] project.get("dependencies", []), chain(*project.get("optional-dependencies", {}).values()), ), ) template_data = { "project_name": project_name, "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), "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/) | [`pdm`](https://pdm.fming.dev/) | [`copier-pdm`](https://github.com/pawamoy/copier-pdm) {% macro dep_line(dep) -%} [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in prod_dependencies -%} {{ dep_line(dep) }} {% endfor %} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License ------- | ------- | ------------------ | ----------------------- | ------- {% for dep in dev_dependencies -%} {{ dep_line(dep) }} {% endfor %} {% 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.40.0/scripts/setup.sh���������������������������������������������������������������0000755�0001750�0001750�00000001217�14556223422�017430� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env bash set -e if ! command -v pdm &>/dev/null; then if ! command -v pipx &>/dev/null; then python3 -m pip install --user pipx fi pipx install pdm fi if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then pdm install --plugins fi if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then if [ "${PDM_MULTIRUN_USE_VENVS}" -eq "1" ]; then for version in ${PDM_MULTIRUN_VERSIONS}; do if ! pdm venv --path "${version}" &>/dev/null; then pdm venv create --name "${version}" "${version}" fi done fi pdm multirun -v pdm install -G:all else pdm install -G:all fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/scripts/redirects.py�����������������������������������������������������������0000644�0001750�0001750�00000000634�14556223422�020271� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Set redirection pages in docs.""" import mkdocs_gen_files redirect_map = { "usage.md": "../cli_reference", } redirect_template = """ <script type="text/javascript"> window.location.href = "{link}"; </script> <a href="{link}">Redirecting...</a> """ for page, link in redirect_map.items(): with mkdocs_gen_files.open(page, "w") as fd: print(redirect_template.format(link=link), file=fd) ����������������������������������������������������������������������������������������������������python-griffe-0.40.0/scripts/gen_griffe_json.py�����������������������������������������������������0000644�0001750�0001750�00000000464�14556223422�021432� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Generate the credits page.""" import mkdocs_gen_files import griffe data = griffe.load( "griffe", docstring_parser=griffe.Parser.google, docstring_options={"ignore_init_summary": True}, ) with mkdocs_gen_files.open("griffe.json", "w") as fd: fd.write(data.as_json(full=True, indent=0)) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.gitpod.yml��������������������������������������������������������������������0000644�0001750�0001750�00000000217�14556223422�016330� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������vscode: extensions: - ms-python.python image: file: .gitpod.dockerfile ports: - port: 8000 onOpen: notify tasks: - init: make setup ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/CONTRIBUTING.md����������������������������������������������������������������0000644�0001750�0001750�00000010330�14556223422�016467� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Environment setup Nothing easier! Fork and clone the repository, then: ```bash cd griffe make setup ``` > NOTE: > If it fails for some reason, > you'll need to install > [PDM](https://github.com/pdm-project/pdm) > manually. > > You can install it with: > > ```bash > python3 -m pip install --user pipx > pipx install pdm > ``` > > Now you can try running `make setup` again, > or simply `pdm install`. You now have the dependencies installed. You can run the application with `pdm run griffe [ARGS...]`. Run `make help` to see all the available actions! ## Tasks This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task on multiple Python versions, you run the task directly with `pdm run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-pdm/work/#vscode-setup) for the project. ## Development As usual: 1. create a new branch: `git switch -c feature-or-bugfix-name` 1. edit the code and/or the documentation **Before committing:** 1. run `make format` to auto-format the code 1. run `make check` to check everything (fix any warning) 1. run `make test` to run the tests (fix any issue) 1. if you updated the documentation or the project dependencies: 1. run `make docs` 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) 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. ## 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. 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: ``` Body. 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, but please avoid very long summaries, and very long lines in the body, unless they are part of code blocks that must not be wrapped. ## 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 ``` 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. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/�����������������������������������������������������������������������0000755�0001750�0001750�00000000000�14556223422�015601� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/ISSUE_TEMPLATE/��������������������������������������������������������0000755�0001750�0001750�00000000000�14556223422�017764� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/ISSUE_TEMPLATE/feature_request.md��������������������������������������0000644�0001750�0001750�00000001213�14556223422�023506� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Feature request about: Suggest an idea for this project. title: "feature: " labels: feature assignees: pawamoy --- ### Is your feature request related to a problem? Please describe. <!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]. --> ### Describe the solution you'd like <!-- A clear and concise description of what you want to happen. --> ### Describe alternatives you've considered <!-- A clear and concise description of any alternative solutions or features you've considered. --> ### Additional context <!-- Add any other context or screenshots about the feature request here. --> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/ISSUE_TEMPLATE/config.yml����������������������������������������������0000644�0001750�0001750�00000000330�14556223422�021750� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������blank_issues_enabled: false contact_links: - name: I have a question / I need help url: https://github.com/mkdocstrings/griffe/discussions/new?category=q-a about: Ask and answer questions in the Discussions tab. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/ISSUE_TEMPLATE/bug_report.md�������������������������������������������0000644�0001750�0001750�00000003177�14556223422�022466� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- name: Bug report about: Create a bug report to help us improve. title: "bug: " labels: unconfirmed assignees: [pawamoy] --- ### Description of the bug <!-- Please provide a clear and concise description of what the bug is. --> ### To Reproduce <!-- Please provide a Minimal Reproducible Example (MRE) if possible. Try to boil down the problem to a few lines of code. Your code should run by simply copying and pasting it. Example: ``` git clone https://github.com/username/repro cd repro python -m venv .venv . .venv/bin/activate pip install -r requirements.txt ... # command or code showing the issue ``` --> ``` WRITE MRE / INSTRUCTIONS HERE ``` ### Full traceback <!-- Please provide the full error message / traceback if any, by pasting it in the code block below. No screenshots! --> <details><summary>Full traceback</summary> ```python PASTE TRACEBACK HERE ``` </details> <!-- If using Griffe directly: --> ```console $ griffe dump ... -LDEBUG PASTE LOGS HERE ``` <!-- If using Griffe through MkDocs and mkdocstrings: --> ```console $ mkdocs build -v PASTE LOGS HERE ``` ### Expected behavior <!-- Please provide a clear and concise description of what you expected to happen. --> ### Environment information <!-- Please run the following command in your repository and paste its output below it, redacting sensitive information. --> ```bash griffe --debug-info # | xclip -selection clipboard ``` PASTE OUTPUT HERE ### Additional context <!-- Add any other relevant context about the problem here, like links to other issues or pull requests, screenshots, etc. --> �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/FUNDING.yml������������������������������������������������������������0000644�0001750�0001750�00000000107�14556223422�017414� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������github: pawamoy ko_fi: pawamoy custom: - https://www.paypal.me/pawamoy ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/workflows/�������������������������������������������������������������0000755�0001750�0001750�00000000000�14556223422�017636� 5����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/workflows/ci.yml�������������������������������������������������������0000644�0001750�0001750�00000005105�14556223422�020755� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: ci on: push: pull_request: branches: - main defaults: run: shell: bash env: LANG: en_US.utf-8 LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up PDM uses: pdm-project/setup-pdm@v3 with: python-version: "3.8" - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-quality - name: Install dependencies run: pdm install -G ci-quality - name: Check if the documentation builds correctly run: pdm run duty check-docs - name: Check the code quality run: pdm run duty check-quality - name: Check if the code is correctly typed run: pdm run duty check-types - name: Check for vulnerabilities in dependencies run: pdm run duty check-dependencies - name: Check for breaking changes in the API run: pdm run duty check-api 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"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else echo 'jobs=[]' >> $GITHUB_OUTPUT fi tests: needs: exclude-test-jobs strategy: max-parallel: 4 matrix: os: - ubuntu-latest - macos-latest - windows-latest python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.python-version == '3.12' }} steps: - name: Checkout uses: actions/checkout@v3 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up PDM uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} allow-python-prereleases: true - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-tests - name: Install dependencies run: pdm install --no-editable -G ci-tests - name: Run the test suite run: pdm run duty test �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.github/workflows/release.yml��������������������������������������������������0000644�0001750�0001750�00000002546�14556223422�022010� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������name: release on: push permissions: contents: write jobs: release: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout uses: actions/checkout@v3 - 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@v3 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.40.0/CHANGELOG.md�������������������������������������������������������������������0000644�0001750�0001750�00000336330�14556223422�016062� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). <!-- insertion marker --> ## [0.40.0](https://github.com/mkdocstrings/griffe/releases/tag/0.40.0) - 2024-01-30 <small>[Compare with 0.39.1](https://github.com/mkdocstrings/griffe/compare/0.39.1...0.40.0)</small> ### Features - Store reference to function call in keyword expressions ([d72f9d3](https://github.com/mkdocstrings/griffe/commit/d72f9d3a425fee11f23f9f7b44814b6fda458e6e) by Timothée Mazzucotelli). [PR #231](https://github.com/mkdocstrings/griffe/pull/231) ## [0.39.1](https://github.com/mkdocstrings/griffe/releases/tag/0.39.1) - 2024-01-18 <small>[Compare with 0.39.0](https://github.com/mkdocstrings/griffe/compare/0.39.0...0.39.1)</small> ### Bug Fixes - De-duplicate search paths in finder as they could lead to the same modules being yielded twice or more when scanning namespace packages ([80a158a](https://github.com/mkdocstrings/griffe/commit/80a158a2de8d53a054405c3e14113b09d73335a3) by Timothée Mazzucotelli). - Fix logic for skipping already encountered modules when scanning namespace packages ([21a48d0](https://github.com/mkdocstrings/griffe/commit/21a48d0b9248467fe3c36440bee649ce8879f295) by Timothée Mazzucotelli). [Issue mkdocstrings#646](https://github.com/mkdocstrings/mkdocstrings/issues/646) ## [0.39.0](https://github.com/mkdocstrings/griffe/releases/tag/0.39.0) - 2024-01-16 <small>[Compare with 0.38.1](https://github.com/mkdocstrings/griffe/compare/0.38.1...0.39.0)</small> ### Features - Support editable installs dynamically exposing modules from other directories ([2c4ba75](https://github.com/mkdocstrings/griffe/commit/2c4ba751d7d47eb48b47179d316722315e5d4647) by Timothée Mazzucotelli). [Issue #229](https://github.com/mkdocstrings/griffe/issues/229) - Support meson-python editable modules ([9123897](https://github.com/mkdocstrings/griffe/commit/9123897ad8d85e48bd3c435ffabcf9a36a0ed355) by Timothée Mazzucotelli). - Support admonitions in Numpydoc docstrings ([1e311a4](https://github.com/mkdocstrings/griffe/commit/1e311a4eb935c58d488c928a86493ab3f3368f06) by Michael Chow). [Issue #214](https://github.com/mkdocstrings/griffe/issues/214), [PR #219](https://github.com/mkdocstrings/griffe/pull/219), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Expose module properties on all objects ([123f8c5](https://github.com/mkdocstrings/griffe/commit/123f8c5ba1826435e90dafffbfe304bd6ab8e187) by Timothée Mazzucotelli). [Issue #226](https://github.com/mkdocstrings/griffe/issues/226) ### Bug Fixes - Consider space-only lines to be empty, never break Numpydoc sections on blank lines ([8c57354](https://github.com/mkdocstrings/griffe/commit/8c5735497578417e1dd723625590539016e7b7a5) by Timothée Mazzucotelli). [PR #220](https://github.com/mkdocstrings/griffe/pull/220), [Related to PR #219](https://github.com/mkdocstrings/griffe/pull/219), [Numpydoc discussion](https://github.com/numpy/numpydoc/issues/463) - Allow merging stubs into alias targets ([3cf7958](https://github.com/mkdocstrings/griffe/commit/3cf795871a0549b901d9374705d6a1eb84700128) by Timothée Mazzucotelli). - Insert the right directory in front of import paths before inspecting a module (dynamically imported) ([7d75c71](https://github.com/mkdocstrings/griffe/commit/7d75c71477ccb208e071bfe3c3204a0490274b44) by Timothée Mazzucotelli). ### Code Refactoring - Set lineno to 0 for removed objects when checking API ([b660c34](https://github.com/mkdocstrings/griffe/commit/b660c346feb3a95fbe54a6dad460e988a9a41774) by Timothée Mazzucotelli). - Prepare support for new output formats (styles) of the check command ([f2ece1e](https://github.com/mkdocstrings/griffe/commit/f2ece1e602b0fb3d888a60d892089a55fdcf60f0) by Timothée Mazzucotelli). - Transform finder's package and namespace package classes into dataclasses ([16be6a4](https://github.com/mkdocstrings/griffe/commit/16be6a4a7660d8ed13ccdcf9c571eda647e078f0) by Timothée Mazzucotelli). ## [0.38.1](https://github.com/mkdocstrings/griffe/releases/tag/0.38.1) - 2023-12-06 <small>[Compare with 0.38.0](https://github.com/mkdocstrings/griffe/compare/0.38.0...0.38.1)</small> ### Bug Fixes - Support absolute Windows paths for extensions ([4e67d8f](https://github.com/mkdocstrings/griffe/commit/4e67d8fa5f0e9f23c1df2e1d772fc0f1e4e6c2e0) by Timothée Mazzucotelli). [Issue mkdocstrings-python#116](https://github.com/mkdocstrings/python/issues/116) ## [0.38.0](https://github.com/mkdocstrings/griffe/releases/tag/0.38.0) - 2023-11-13 <small>[Compare with 0.37.0](https://github.com/mkdocstrings/griffe/compare/0.37.0...0.38.0)</small> ### Features - Allow passing load parameters to the temporary package visit helper ([3a7854f](https://github.com/mkdocstrings/griffe/commit/3a7854fb180e34392fd520d9d25a6298d4b80830) by Timothée Mazzucotelli). ## [0.37.0](https://github.com/mkdocstrings/griffe/releases/tag/0.37.0) - 2023-11-12 <small>[Compare with 0.36.9](https://github.com/mkdocstrings/griffe/compare/0.36.9...0.37.0)</small> ### Deprecations - The loader `load_module` method was renamed `load`, Its `module` parameter was renamed `objspec` and is now positional-only. This method always returned the specified object, not just modules, so it made more sense to rename it `load` and to rename the parameter specifying the object. Old usages (`load_module` and `module=...`) will continue to work for some time (a few months, a year, more), and will emit deprecation warnings. ### Features - Add option to warn about unknown parameters in Sphinx docstrings ([8b11d77](https://github.com/mkdocstrings/griffe/commit/8b11d77315ca7a5e15da519db1663d05805dd075) by Ashwin Vinod). [Issue #64](https://github.com/mkdocstrings/griffe/issues/64), [PR #210](https://github.com/mkdocstrings/griffe/pull/210), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Add `on_package_loaded` event ([a5cf654](https://github.com/mkdocstrings/griffe/commit/a5cf6543b43db06c4d0f24d2631ddc86b1fee41e) by Timothée Mazzucotelli). - Add option to find, load and merge stubs-only packages ([6e55f3b](https://github.com/mkdocstrings/griffe/commit/6e55f3bd0838e3f229fcd37d3aeced0146d33ff1) by Romain). [PR #221](https://github.com/mkdocstrings/griffe/pull/221), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Bug Fixes - Report attributes who lost their value as "unset" ([dfffa4b](https://github.com/mkdocstrings/griffe/commit/dfffa4b96a8a70f93b899bd41aefeaa9939819e9) by Geethakrishna-Puligundla). [Issue #218](https://github.com/mkdocstrings/griffe/issues/218), [PR #225](https://github.com/mkdocstrings/griffe/pull/225) - Don't crash when computing MRO for a class that is named after its parent ([a2dd8a6](https://github.com/mkdocstrings/griffe/commit/a2dd8a6bc3f95679e1c2e79ce05d175fb8f89ccc) by Timothée Mazzucotelli). ### Code Refactoring - Rename loader `load_module` method to `load` ([2bfe206](https://github.com/mkdocstrings/griffe/commit/2bfe206b57f607b56f7bcb5a85a7e2a25fe3bf47) by Timothée Mazzucotelli). ## [0.36.9](https://github.com/mkdocstrings/griffe/releases/tag/0.36.9) - 2023-10-27 <small>[Compare with 0.36.8](https://github.com/mkdocstrings/griffe/compare/0.36.8...0.36.9)</small> ### Bug Fixes - Fix accessing alias members with `__getitem__` ([8929409](https://github.com/mkdocstrings/griffe/commit/8929409d4703c6b684084e88aae0d99423e05dbf) by Timothée Mazzucotelli). [Issue mkdocstrings-python#111](https://github.com/mkdocstrings/python/issues/111) ### Code Refactoring - Expose parser 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 <small>[Compare with 0.36.7](https://github.com/mkdocstrings/griffe/compare/0.36.7...0.36.8)</small> ### Bug Fixes - Use already parsed docstring sections when dumping full data ([311807b](https://github.com/mkdocstrings/griffe/commit/311807b8fa1716dabe5ba18d3e12c947286afd8e) by Timothée Mazzucotelli). [Discussion griffe-typingdoc#6](https://github.com/mkdocstrings/griffe-typingdoc/discussions/6) ## [0.36.7](https://github.com/mkdocstrings/griffe/releases/tag/0.36.7) - 2023-10-17 <small>[Compare with 0.36.6](https://github.com/mkdocstrings/griffe/compare/0.36.6...0.36.7)</small> ### Bug Fixes - Add missing proxies (methods/properties) to aliases ([7320640](https://github.com/mkdocstrings/griffe/commit/7320640d42ebb4546f787fe458d5032a67ea20b7) by Timothée Mazzucotelli). ### Code Refactoring - Use final target in alias proxies ([731d662](https://github.com/mkdocstrings/griffe/commit/731d66237252e754b7a935ca4d0344f554edb5ff) by Timothée Mazzucotelli). ## [0.36.6](https://github.com/mkdocstrings/griffe/releases/tag/0.36.6) - 2023-10-16 <small>[Compare with 0.36.5](https://github.com/mkdocstrings/griffe/compare/0.36.5...0.36.6)</small> ### Code Refactoring - Only consider presence/absence for docstrings truthiness, not emptiness of their value ([4c49611](https://github.com/mkdocstrings/griffe/commit/4c496117880d2166bfc2bc8c40a235c23cef8527) by Timothée Mazzucotelli). ## [0.36.5](https://github.com/mkdocstrings/griffe/releases/tag/0.36.5) - 2023-10-09 <small>[Compare with 0.36.4](https://github.com/mkdocstrings/griffe/compare/0.36.4...0.36.5)</small> ### Bug Fixes - Force extension import path to be a string (coming from MkDocs' `!relative` tag) ([34e21a9](https://github.com/mkdocstrings/griffe/commit/34e21a9545a38b61a1b80192af312d70f6c607f2) by Timothée Mazzucotelli). - Fix crash when trying to get a decorator callable path (found thanks to pysource-codegen) ([e57f08e](https://github.com/mkdocstrings/griffe/commit/e57f08eb5770eb3a9ed12e97da3076b87f109224) by Timothée Mazzucotelli). - Fix crash when trying to get docstring after assignment (found thanks to pysource-codegen) ([fb0a0c1](https://github.com/mkdocstrings/griffe/commit/fb0a0c1a8558c9d04855b75e4a9f579b46e2edd8) by Timothée Mazzucotelli). - Fix type errors in expressions and value extractor, don't pass duplicate arguments (found thanks to pysource-codegen) ([7e53288](https://github.com/mkdocstrings/griffe/commit/7e53288586bd90198cfd6a898002850c67213209) by Timothée Mazzucotelli). ## [0.36.4](https://github.com/mkdocstrings/griffe/releases/tag/0.36.4) - 2023-09-28 <small>[Compare with 0.36.3](https://github.com/mkdocstrings/griffe/compare/0.36.3...0.36.4)</small> ### Bug Fixes - Fix visiting relative imports in non-init modules ([c1138c3](https://github.com/mkdocstrings/griffe/commit/c1138c34b89965fd780d669c7dd6b12f245d8cd9) by Timothée Mazzucotelli). ## [0.36.3](https://github.com/mkdocstrings/griffe/releases/tag/0.36.3) - 2023-09-28 <small>[Compare with 0.36.2](https://github.com/mkdocstrings/griffe/compare/0.36.2...0.36.3)</small> ### Bug Fixes - Fix parsing of choices in Numpy parameters ([5f2d997](https://github.com/mkdocstrings/griffe/commit/5f2d99776e326679d2c0d1d9cb6b06d6436971c6) by Timothée Mazzucotelli). [Issue #212](https://github.com/mkdocstrings/griffe/issues/212) ### Code Refactoring - Add `repr` methods to function parameters ([9442234](https://github.com/mkdocstrings/griffe/commit/94422349483a25db627921dfe13c7a89b81e700e) by Timothée Mazzucotelli). ## [0.36.2](https://github.com/mkdocstrings/griffe/releases/tag/0.36.2) - 2023-09-10 <small>[Compare with 0.36.1](https://github.com/mkdocstrings/griffe/compare/0.36.1...0.36.2)</small> ### Bug Fixes - Fix warnings for docstrings in builtin modules ([6ba3e04](https://github.com/mkdocstrings/griffe/commit/6ba3e0461647c2c76d0fd68889d37bbada686259) by Timothée Mazzucotelli). - Fix dumping `filepath` to a dict when it is a list ([066a4a7](https://github.com/mkdocstrings/griffe/commit/066a4a7f22827783c930feacd6a339ed3d00ec27) by davfsa). [PR #207](https://github.com/mkdocstrings/griffe/pull/207) ## [0.36.1](https://github.com/mkdocstrings/griffe/releases/tag/0.36.1) - 2023-09-04 <small>[Compare with 0.36.0](https://github.com/mkdocstrings/griffe/compare/0.36.0...0.36.1)</small> ### Bug Fixes - Fix iterating non-flat expressions (some nodes were skipped) ([3249155](https://github.com/mkdocstrings/griffe/commit/324915507c1100e04ffed6d926143f66f0016870) by Timothée Mazzucotelli). ## [0.36.0](https://github.com/mkdocstrings/griffe/releases/tag/0.36.0) - 2023-09-01 <small>[Compare with 0.35.2](https://github.com/mkdocstrings/griffe/compare/0.35.2...0.36.0)</small> ### Features - Add option to read return type of properties in their summary (Google-style) ([096970f](https://github.com/mkdocstrings/griffe/commit/096970ffa66f491ef34ae1121e8b907f2da4c742) by Timothée Mazzucotelli). [Issue #137](https://github.com/mkdocstrings/griffe/issues/137), [PR #206](https://github.com/mkdocstrings/griffe/pull/206) - Add option to make parentheses around the type of returned values optional (Google-style) ([b0620f8](https://github.com/mkdocstrings/griffe/commit/b0620f86e1767183d776771992ce12f961efe395) by Timothée Mazzucotelli). [Issue #137](https://github.com/mkdocstrings/griffe/issues/137) - Get class parameters from parent's `__init__` method ([e8a9fdc](https://github.com/mkdocstrings/griffe/commit/e8a9fdcce1cffdc7db5a216f833d10da6116db5a) by Timothée Mazzucotelli). [Issue #205](https://github.com/mkdocstrings/griffe/issues/205) ### Bug Fixes - Use all members (declared and inherited) when checking for breakages, avoid false-positives when a member of a class is moved into a parent class ([1c4340b](https://github.com/mkdocstrings/griffe/commit/1c4340b09b111313a5a242caa986a2fa3fdef852) by Timothée Mazzucotelli). [Issue #203](https://github.com/mkdocstrings/griffe/issues/203) - Skip early submodules with dots in their path ([5e81b8a](https://github.com/mkdocstrings/griffe/commit/5e81b8afef4e6ce8294cdbaf348f4f1a05add1d8) by Timothée Mazzucotelli). [Issue #185](https://github.com/mkdocstrings/griffe/issues/185) ### Code Refactoring - Allow iterating on expressions in both flat and nested ways ([3957fa7](https://github.com/mkdocstrings/griffe/commit/3957fa70abf3f2d8af1a4ab4b1041b873bc724e0) by Timothée Mazzucotelli). ## [0.35.2](https://github.com/mkdocstrings/griffe/releases/tag/0.35.2) - 2023-08-27 <small>[Compare with 0.35.1](https://github.com/mkdocstrings/griffe/compare/0.35.1...0.35.2)</small> ### Code Refactoring - Be more strict when parsing sections in Google docstrings ([6a8a228](https://github.com/mkdocstrings/griffe/commit/6a8a2280f8910d4268380400d7888cb8d72b4296) by Timothée Mazzucotelli). [Issue #204](https://github.com/mkdocstrings/griffe/issues/204) ## [0.35.1](https://github.com/mkdocstrings/griffe/releases/tag/0.35.1) - 2023-08-26 <small>[Compare with 0.35.0](https://github.com/mkdocstrings/griffe/compare/0.35.0...0.35.1)</small> ### Bug Fixes - Preserve inherited attribute on alias inherited members ([1e19e7b](https://github.com/mkdocstrings/griffe/commit/1e19e7b2c3f2bb10c822c7d8b63b04a76024b4f7) by Timothée Mazzucotelli). [Issue mkdocstrings/python#102](https://github.com/mkdocstrings/python/issues/102) ## [0.35.0](https://github.com/mkdocstrings/griffe/releases/tag/0.35.0) - 2023-08-24 <small>[Compare with 0.34.0](https://github.com/mkdocstrings/griffe/compare/0.34.0...0.35.0)</small> ### Features - Add an `is_public` helper method to guess if an object is public ([b823639](https://github.com/mkdocstrings/griffe/commit/b8236391f4ac8b16e9ee861c322e75ea10d6a39b) by Timothée Mazzucotelli). - Add option to Google parser allowing to parse Returns sections with or without multiple items ([65fee70](https://github.com/mkdocstrings/griffe/commit/65fee70cf87399b7da92f054180791de0eb4f22d) by Antoine Dechaume). [PR #196](https://github.com/mkdocstrings/griffe/pull/196) ### Bug Fixes - Allow passing `warn_unknown_params` option to Google and Numpy parsers ([5bf0746](https://github.com/mkdocstrings/griffe/commit/5bf07468d38a158f8e58e3e1c562e8d886d83321) by Timothée Mazzucotelli). ### Code Refactoring - Preserve alias members path by re-aliasing members instead of returning target's members ([d400cb1](https://github.com/mkdocstrings/griffe/commit/d400cb13c8b7c250ff1e6b6c8ec9be1c7b6ff989) by Timothée Mazzucotelli). ## [0.34.0](https://github.com/mkdocstrings/griffe/releases/tag/0.34.0) - 2023-08-20 <small>[Compare with 0.33.0](https://github.com/mkdocstrings/griffe/compare/0.33.0...0.34.0)</small> ### Features - Allow checking if docstring section is empty or not with `if section` ([f6cf559](https://github.com/mkdocstrings/griffe/commit/f6cf559db50718e86cde40eae9d14489cabd9ed8) by Timothée Mazzucotelli). - Implement Functions (or Methods), Classes and Modules docstring sections ([929e615](https://github.com/mkdocstrings/griffe/commit/929e6158c093b021ba80773e17613406b38fbf0c) by Timothée Mazzucotelli). - Allow passing a docstring parser name instead of its enumeration value ([ce59b7d](https://github.com/mkdocstrings/griffe/commit/ce59b7dca69e3a9946a0735405535e296e0ec9c9) by Timothée Mazzucotelli). ### Code Refactoring - Explicit checks for subprocess runs ([cc3ca2e](https://github.com/mkdocstrings/griffe/commit/cc3ca2e18877c17fe23e2ceeb1c13e10c9fe46d2) by Timothée Mazzucotelli). ## [0.33.0](https://github.com/mkdocstrings/griffe/releases/tag/0.33.0) - 2023-08-16 <small>[Compare with 0.32.3](https://github.com/mkdocstrings/griffe/compare/0.32.3...0.33.0)</small> ### Breaking Changes - Removed `griffe.expressions.Expression` in favor of [`griffe.expressions.Expr`][] and subclasses - Removed `griffe.expressions.Name` in favor of [`griffe.expressions.ExprName`][] ### Features - Add `-V`, `--version` CLI flag to show version ([a41515f](https://github.com/mkdocstrings/griffe/commit/a41515f39e6e5e2e28d68980c44cc07a7e0ebbe0) by jgart). [Issue #186](https://github.com/mkdocstrings/griffe/issues/186), [PR #187](https://github.com/mkdocstrings/griffe/pull/187), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Code Refactoring - Improve expressions ([66c8ad5](https://github.com/mkdocstrings/griffe/commit/66c8ad5074e1475aa88a51d8652b5e197760d774) and [0fe8f91](https://github.com/mkdocstrings/griffe/commit/0fe8f9155b571714b0fe2a1bd7aef0b9b0738b08) by Timothée Mazzucotelli). ## [0.32.3](https://github.com/mkdocstrings/griffe/releases/tag/0.32.3) - 2023-07-17 <small>[Compare with 0.32.2](https://github.com/mkdocstrings/griffe/compare/0.32.2...0.32.3)</small> ### Bug Fixes - Fix detecting whether an object should be an alias during inspection ([6a63b37](https://github.com/mkdocstrings/griffe/commit/6a63b375db7d639dd05589c56a2f89d1be9d66a8) by Timothée Mazzucotelli). [Issue #180](https://github.com/mkdocstrings/griffe/issues/180) ### Code Refactoring - Improve log message when trying to stubs-merge objects of different kinds ([d34a3ba](https://github.com/mkdocstrings/griffe/commit/d34a3ba4bbd15c3fafe9cc5e2e82a2281cf3e094) by Timothée Mazzucotelli). - De-duplicate stubs merging log message ([cedc062](https://github.com/mkdocstrings/griffe/commit/cedc062cd4035a4ad0f3a14b4ef31bea4e39374d) by Timothée Mazzucotelli). ## [0.32.2](https://github.com/mkdocstrings/griffe/releases/tag/0.32.2) - 2023-07-17 <small>[Compare with 0.32.1](https://github.com/mkdocstrings/griffe/compare/0.32.1...0.32.2)</small> ### Bug Fixes - Keep parentheses around tuples, except within subscripts ([df6e636](https://github.com/mkdocstrings/griffe/commit/df6e636c3ecfaa6befdfdaf26e898e1a71218675) by Timothée Mazzucotelli). [Issue mkdocstrings/python#88](https://github.com/mkdocstrings/python/issues/88) ## [0.32.1](https://github.com/mkdocstrings/griffe/releases/tag/0.32.1) - 2023-07-15 <small>[Compare with 0.32.0](https://github.com/mkdocstrings/griffe/compare/0.32.0...0.32.1)</small> ### Bug Fixes - Fix aliases for direct nested imports ([e9867f7](https://github.com/mkdocstrings/griffe/commit/e9867f78044a2a33b575e274224d3a4c16b62439) by Timothée Mazzucotelli). [Issue mkdocstrings/python#32](https://github.com/mkdocstrings/python/issues/32) ### Code Refactoring - Simplify AST imports, stop using deprecated code from `ast` ([21d5832](https://github.com/mkdocstrings/griffe/commit/21d5832ba6db051b9754f515f1d7125126dd801f) by Timothée Mazzucotelli). [Issue #179](https://github.com/mkdocstrings/griffe/issues/179) ## [0.32.0](https://github.com/mkdocstrings/griffe/releases/tag/0.32.0) - 2023-07-13 <small>[Compare with 0.31.0](https://github.com/mkdocstrings/griffe/compare/0.31.0...0.32.0)</small> ### Deprecations - Classes [`InspectorExtension`][griffe.extensions.base.InspectorExtension] and [`VisitorExtension`][griffe.extensions.base.VisitorExtension] are deprecated in favor of [`Extension`][griffe.extensions.base.Extension]. As a side-effect, the [`hybrid`][griffe.extensions.hybrid.HybridExtension] extension is also deprecated. See [how to use and write extensions](extensions.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.extensions.base.Extension] is now a class ### Features - Numpy parser: handle return section items with just type, or no name and no type ([bdec37d](https://github.com/mkdocstrings/griffe/commit/bdec37dd32a5d4e089ee5e14e5a66be645bb8360) by Michael Chow). [Issue #173](https://github.com/mkdocstrings/griffe/issues/173), [PR #174](https://github.com/mkdocstrings/griffe/pull/174), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Rework extension system ([dea4c83](https://github.com/mkdocstrings/griffe/commit/dea4c830e3bfa0bf7c9f307975cb53e1314c50eb) by Timothée Mazzucotelli). - Parse attribute values, parameter defaults and decorators as expressions ([7b653b3](https://github.com/mkdocstrings/griffe/commit/7b653b31bd9c38bf8d960baa5ab75dd56c62fbcb) by Timothée Mazzucotelli). - Add loader option to avoid storing source code, reducing memory footprint ([d592edf](https://github.com/mkdocstrings/griffe/commit/d592edf477d9e7a5f9723c96cc259db65b1cae71) by Timothée Mazzucotelli). - Add `extra` attribute to objects ([707a348](https://github.com/mkdocstrings/griffe/commit/707a34833f56cf4a1aa302cb1201ad96ff361252) by Timothée Mazzucotelli). ### Bug Fixes - Numpy-style: don't strip spaces from the left of indented lines ([f13fc0a](https://github.com/mkdocstrings/griffe/commit/f13fc0a7edc7c8ac14c8c482b58735a5f7301bd6) by Timothée Mazzucotelli). [Discussion #587](https://github.com/mkdocstrings/mkdocstrings/discussions/587) - Fix relative paths for old versions when checking API ([96fd45b](https://github.com/mkdocstrings/griffe/commit/96fd45b41186eb503d6a2ff4e587cae427aea013) by Timothée Mazzucotelli). ### Performance Improvements - Don't store source when dumping as JSON ([d7f314a](https://github.com/mkdocstrings/griffe/commit/d7f314a62dd40c38c8c76ec7102233a588c1e64a) by Timothée Mazzucotelli). - Stop caching properties on Object methods ([15bdd74](https://github.com/mkdocstrings/griffe/commit/15bdd744db1f089f4448b952f9acf184c43289ea) by Timothée Mazzucotelli). - Stop patching AST, use functions instead ([7302f17](https://github.com/mkdocstrings/griffe/commit/7302f178392c70890d083a1617f1cf4e72395be3) by Timothée Mazzucotelli). [Issue #171](https://github.com/mkdocstrings/griffe/issues/171) ### Code Refactoring - Privatize/remove objects ([fdeb16f](https://github.com/mkdocstrings/griffe/commit/fdeb16f61cb5ae7db2394ef2a8ec31843b7ae85b) by Timothée Mazzucotelli). - Document public objects with `__all__` ([db0e0e3](https://github.com/mkdocstrings/griffe/commit/db0e0e340efcd48904f448a6e4397a9df36ac50f) by Timothée Mazzucotelli). - Remove base visitor and inspector ([bc446e4](https://github.com/mkdocstrings/griffe/commit/bc446e4ac9445636be7fdadbfc0b056cbc1d73e3) by Timothée Mazzucotelli). - Auto-register module in collection within loading helpers ([591bacc](https://github.com/mkdocstrings/griffe/commit/591bacc6c46d91beb30f6e01e0ae96f8e3102cf8) by Timothée Mazzucotelli). [Issue #177](https://github.com/mkdocstrings/griffe/issues/177) ## [0.31.0](https://github.com/mkdocstrings/griffe/releases/tag/0.31.0) - 2023-07-04 <small>[Compare with 0.30.1](https://github.com/mkdocstrings/griffe/compare/0.30.1...0.31.0)</small> ### Breaking Changes - Drop support for Python 3.7 - API changes: - [`GriffeLoader.resolve_aliases(only_exported)`][griffe.loader.GriffeLoader.resolve_aliases]: Deprecated parameter was removed and replaced by `implicit` (inverse semantics) - [`GriffeLoader.resolve_aliases(only_known_modules)`][griffe.loader.GriffeLoader.resolve_aliases]: Deprecated parameter was removed and replaced by `external` (inverse semantics) - [`LinesCollection.tokens`][griffe.collections.LinesCollection]: Public object was removed (Python 3.7) - `ASTNode.end_lineno`: Public object was removed (Python 3.7) - [`griffe.agents.extensions`][griffe.agents] Deprecated module was removed and replaced by [`griffe.extensions`][] ### Features - Add `--color`, `--no-color` options to check subcommand ([eac783c](https://github.com/mkdocstrings/griffe/commit/eac783c2df5a0ba57612b71b0797a74cf7fc8e39) by Timothée Mazzucotelli). ### Bug Fixes - Report removed public modules ([68906cb](https://github.com/mkdocstrings/griffe/commit/68906cb6083e5f7cad3a1cb5a74878d6e74f9c69) by Timothée Mazzucotelli). ### Code Refactoring - Improve check output ([6b0a1f0](https://github.com/mkdocstrings/griffe/commit/6b0a1f0397d153a95d1b6c69d109ce141e39e1f1) by Timothée Mazzucotelli). - Remove deprecated `griffe.agents.extensions` module ([b555c78](https://github.com/mkdocstrings/griffe/commit/b555c788b624fa5aa0c871e2c199079868252f22) by Timothée Mazzucotelli). - Remove deprecated parameters from loader's `resolve_aliases` method ([dd98acd](https://github.com/mkdocstrings/griffe/commit/dd98acd5f0c85661c7a00002805c92caa4c11a21) by Timothée Mazzucotelli). - Drop Python 3.7 support ([e4be30a](https://github.com/mkdocstrings/griffe/commit/e4be30a4c1025fd2f99f088c76f8e263714d8e33) by Timothée Mazzucotelli). ## [0.30.1](https://github.com/mkdocstrings/griffe/releases/tag/0.30.1) - 2023-07-02 <small>[Compare with 0.30.0](https://github.com/mkdocstrings/griffe/compare/0.30.0...0.30.1)</small> ### Bug Fixes - Prevent duplicate yields of breaking changes ([9edef90](https://github.com/mkdocstrings/griffe/commit/9edef90d6c54b330046582e2a52ad88b5798d32c) by Timothée Mazzucotelli). [Issue #162](https://github.com/mkdocstrings/griffe/issues/162) - Prevent alias resolution errors when checking for API breaking changes ([93c964a](https://github.com/mkdocstrings/griffe/commit/93c964a4cc3f759d101db45af5816a4d3b07c85e) by Timothée Mazzucotelli). [Issue #145](https://github.com/mkdocstrings/griffe/issues/145) - Handle Git errors when checking for API breaking changes ([f9e8ba3](https://github.com/mkdocstrings/griffe/commit/f9e8ba381b75f650cfeb7bc96c976fec2251ac7a) by Timothée Mazzucotelli). [Issue #144](https://github.com/mkdocstrings/griffe/issues/144) ### Code Refactoring - Force remove worktree branch when done checking ([45332ba](https://github.com/mkdocstrings/griffe/commit/45332ba89e213b4f9490ea7d2507d972267bed73) by Timothée Mazzucotelli). - Change command to obtain latest tag ([f70f630](https://github.com/mkdocstrings/griffe/commit/f70f630ef7c67589d60c17ef4fb19c90127b2e06) by Timothée Mazzucotelli). ## [0.30.0](https://github.com/mkdocstrings/griffe/releases/tag/0.30.0) - 2023-06-30 <small>[Compare with 0.29.1](https://github.com/mkdocstrings/griffe/compare/0.29.1...0.30.0)</small> ### Features - Add `allow_section_blank_line` option to the Numpy parser ([245845e](https://github.com/mkdocstrings/griffe/commit/245845ecaabedf4abb0af80d783702e55ea83883) by Michael Chow). [Issue #167](https://github.com/mkdocstrings/griffe/issues/167), [PR #168](https://github.com/mkdocstrings/griffe/pull/168) - Support inheritance ([08bbe09](https://github.com/mkdocstrings/griffe/commit/08bbe09879dfa5440a359c8b2ad0b896c20c1dfc) by Timothée Mazzucotelli). [PR #170](https://github.com/mkdocstrings/griffe/pull/170) ### Bug Fixes - Handle semi-colons in pth files ([e2ec661](https://github.com/mkdocstrings/griffe/commit/e2ec661e614df6c5f4fda1444468363777985b7c) by Michael Chow). [Issue #172](https://github.com/mkdocstrings/griffe/issues/172), [PR #175](https://github.com/mkdocstrings/griffe/pull/175) ### Code Refactoring - Split members API in two parts: producer and consumer ([2269449](https://github.com/mkdocstrings/griffe/commit/226944983a9073d643ed09b47e7d3f99c76d3d5e) by Timothée Mazzucotelli). [PR #170](https://github.com/mkdocstrings/griffe/pull/170) ## [0.29.1](https://github.com/mkdocstrings/griffe/releases/tag/0.29.1) - 2023-06-19 <small>[Compare with 0.29.0](https://github.com/mkdocstrings/griffe/compare/0.29.0...0.29.1)</small> ### Bug Fixes - Fix detection of optional and default in Numpydoc-style parameters ([3509106](https://github.com/mkdocstrings/griffe/commit/3509106399c5475ef71bb074dfa8f885e6759058) by Timothée Mazzucotelli). [Issue #165](https://github.com/mkdocstrings/griffe/issues/165) - Fallback to string literal when parsing fails with syntax error ([53827c8](https://github.com/mkdocstrings/griffe/commit/53827c8c073e55a7f6d8ef61b36e9baf51f1c2bc) by Timothée Mazzucotelli). [Issue mkdocstrings/python#80](https://github.com/mkdocstrings/python/issues/80) - Don't mutate finder's import paths ([a9e025a](https://github.com/mkdocstrings/griffe/commit/a9e025a16571b83713ce44f2be2356e498a847a2) by Timothée Mazzucotelli). - Respect `external` when expanding wildcards ([8ef92c8](https://github.com/mkdocstrings/griffe/commit/8ef92c873db175dbd35e6d09277f6023a8fde32d) by Timothée Mazzucotelli). - Extract actual type for yielded/received values ([3ea37ba](https://github.com/mkdocstrings/griffe/commit/3ea37ba2bcafea47f4b28bab6ae916ecb921b5ce) by Timothée Mazzucotelli). [Issue mkdocstrings/python#75](https://github.com/mkdocstrings/python/issues/75) ### Code Refactoring - Improve error handling when importing a module ([a732e21](https://github.com/mkdocstrings/griffe/commit/a732e217622cc5ab2161479b9dde0ce59e2361af) by Timothée Mazzucotelli). - Improve tests helpers (accept all visit/inspection parameters) ([6da5869](https://github.com/mkdocstrings/griffe/commit/6da586963cddff4dceadcd4b485dbb805830b6ea) by Timothée Mazzucotelli). - Allow passing a modules collection to the inspector, for consistency with the visitor ([5f73a28](https://github.com/mkdocstrings/griffe/commit/5f73a28a09a4b445fa253356034c5ef40b9ecfec) by Timothée Mazzucotelli). - Always add import path of module to inspect when it has a file path ([4021e6f](https://github.com/mkdocstrings/griffe/commit/4021e6fe9f5e06543f9709e7ae42f6ad8cd0b093) by Timothée Mazzucotelli). ## [0.29.0](https://github.com/mkdocstrings/griffe/releases/tag/0.29.0) - 2023-05-26 <small>[Compare with 0.28.2](https://github.com/mkdocstrings/griffe/compare/0.28.2...0.29.0)</small> ### Features - Provide test helpers and pytest fixtures ([611ed58](https://github.com/mkdocstrings/griffe/commit/611ed5868e22ac3ada6467ba25c6dab606f5dee7) by Timothée Mazzucotelli). ## [0.28.2](https://github.com/mkdocstrings/griffe/releases/tag/0.28.2) - 2023-05-24 <small>[Compare with 0.28.1](https://github.com/mkdocstrings/griffe/compare/0.28.1...0.28.2)</small> ### Bug Fixes - Correctly resolve full expressions ([fa57f4f](https://github.com/mkdocstrings/griffe/commit/fa57f4ff6495679b4e7e70d72d5adb80bd8ebc56) by Timothée Mazzucotelli). [Issue mkdocstrings/autorefs#23](https://github.com/mkdocstrings/autorefs/issues/23) - Use `full` attribute instead of `canonical` for expressions ([4338ccc](https://github.com/mkdocstrings/griffe/commit/4338ccc9234f0c4df0ea302a81092a4f3d29f0bf) by Michael Chow). [Issue #163](https://github.com/mkdocstrings/griffe/issues/163), [PR #164](https://github.com/mkdocstrings/griffe/pull/164) ## [0.28.1](https://github.com/mkdocstrings/griffe/releases/tag/0.28.1) - 2023-05-22 <small>[Compare with 0.28.0](https://github.com/mkdocstrings/griffe/compare/0.28.0...0.28.1)</small> ### Bug Fixes - Return docstring warnings as warnings, not attributes ([7bd51ba](https://github.com/mkdocstrings/griffe/commit/7bd51ba7c9c268a1cc378d38fdff3a891adc520c) by Matthew Anderson). [PR #161](https://github.com/mkdocstrings/griffe/pull/161) ### Code Refactoring - Refactor AST nodes parsers ([7e53127](https://github.com/mkdocstrings/griffe/commit/7e5312744cd7f6ad3baba54fe8194d15896f5e6d) by Timothée Mazzucotelli). [Issue #160](https://github.com/mkdocstrings/griffe/issues/160) - Full expressions use canonical names ([65c7184](https://github.com/mkdocstrings/griffe/commit/65c7184b5462b70debce1195c69449935cb0a0b1) by Timothée Mazzucotelli). ## [0.28.0](https://github.com/mkdocstrings/griffe/releases/tag/0.28.0) - 2023-05-17 <small>[Compare with 0.27.5](https://github.com/mkdocstrings/griffe/compare/0.27.5...0.28.0)</small> ### Features - Support scikit-build-core editable modules (partially) ([eb64779](https://github.com/mkdocstrings/griffe/commit/eb64779cb5408553bd4923ab9cdfc72d0b5e6103) by Timothée Mazzucotelli). [Issue #154](https://github.com/mkdocstrings/griffe/issues/154) ### Bug Fixes - Parse complex, stringified annotations ([f743616](https://github.com/mkdocstrings/griffe/commit/f74361684a2cd5db153875b8880788c254828e95) by Timothée Mazzucotelli). [Issue #159](https://github.com/mkdocstrings/griffe/issues/159) ## [0.27.5](https://github.com/mkdocstrings/griffe/releases/tag/0.27.5) - 2023-05-12 <small>[Compare with 0.27.4](https://github.com/mkdocstrings/griffe/compare/0.27.4...0.27.5)</small> ### Code Refactoring - Represent function using their names when inspecting default values ([9116c1f](https://github.com/mkdocstrings/griffe/commit/9116c1fbb562c894547d72207921c02259147958) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#180](https://github.com/mkdocstrings/mkdocstrings/issues/180) ## [0.27.4](https://github.com/mkdocstrings/griffe/releases/tag/0.27.4) - 2023-05-10 <small>[Compare with 0.27.3](https://github.com/mkdocstrings/griffe/compare/0.27.3...0.27.4)</small> ### Bug Fixes - Don't recurse through targets, get directly to final target and handle alias-related errors ([c5bc197](https://github.com/mkdocstrings/griffe/commit/c5bc1973975951389501addf567622c0e3eb71c6) by Timothée Mazzucotelli). [Issue #155](https://github.com/mkdocstrings/griffe/issues/155) ### Code Refactoring - Follow `.pth` files to extend search paths with editable modules ([79bf724](https://github.com/mkdocstrings/griffe/commit/79bf72498150588d05ccdfc80a898c0330e08247) by Timothée Mazzucotelli). [Issue #154](https://github.com/mkdocstrings/griffe/issues/154) - Add default values to `_load_packages` helper ([f104c20](https://github.com/mkdocstrings/griffe/commit/f104c20304dcf24c5d2e39220302a941db4161eb) by Timothée Mazzucotelli). ## [0.27.3](https://github.com/mkdocstrings/griffe/releases/tag/0.27.3) - 2023-05-05 <small>[Compare with 0.27.2](https://github.com/mkdocstrings/griffe/compare/0.27.2...0.27.3)</small> ### Bug Fixes - Allow setting 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 <small>[Compare with 0.27.1](https://github.com/mkdocstrings/griffe/compare/0.27.1...0.27.2)</small> ### Dependencies - Remove async extra (aiofiles) ([70d9b93](https://github.com/mkdocstrings/griffe/commit/70d9b9305370f03c221876838aaad9b72dc388d3) by Timothée Mazzucotelli). ### Bug Fixes - Support walrus operator ([bf721f4](https://github.com/mkdocstrings/griffe/commit/bf721f4dd2bb7f1a6695b5c880df821920b994a6) by Timothée Mazzucotelli). [Issue #152](https://github.com/mkdocstrings/griffe/issues/152) - Respect `ClassVar` annotation ([60e01c1](https://github.com/mkdocstrings/griffe/commit/60e01c126df4e0529fe3806f9c2637a5a45dd138) by Victor Westerhuis). [PR #150](https://github.com/mkdocstrings/griffe/pull/150), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> - Add missing "other args" section aliases ([f5c0a0e](https://github.com/mkdocstrings/griffe/commit/f5c0a0ee70c34063ea38a8e76dcba4923f9673cb) by Timothée Mazzucotelli). ### Code Refactoring - Move utils from cli to respective modules ([c6ce49e](https://github.com/mkdocstrings/griffe/commit/c6ce49eb75c1799982b40a7862a1a7888f0fab93) by Timothée Mazzucotelli). ## [0.27.1](https://github.com/mkdocstrings/griffe/releases/tag/0.27.1) - 2023-04-16 <small>[Compare with 0.27.0](https://github.com/mkdocstrings/griffe/compare/0.27.0...0.27.1)</small> ### Bug Fixes - Actually parse warnings sections ([bc00da5](https://github.com/mkdocstrings/griffe/commit/bc00da5e9dfe4b2aee906000759e0c1e0a2f893b) by Timothée Mazzucotelli). - Allow Raises and Warns items to start with a newline ([f3b088c](https://github.com/mkdocstrings/griffe/commit/f3b088c02b3be86934125b142876b0dfb3702677) by Victor Westerhuis). [PR #149](https://github.com/mkdocstrings/griffe/pull/149), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ## [0.27.0](https://github.com/mkdocstrings/griffe/releases/tag/0.27.0) - 2023-04-10 <small>[Compare with 0.26.0](https://github.com/mkdocstrings/griffe/compare/0.26.0...0.27.0)</small> ### Features - Implement basic handling of Alias for breaking changes ([aa8ce00](https://github.com/mkdocstrings/griffe/commit/aa8ce009c8d69f7830bc46bc80dac34907b8ae83) by Yurii). [PR #140](https://github.com/mkdocstrings/griffe/pull/140), Co-authored-by: Timothée Mazzucotelli <pawamoy@pm.me> ### Bug Fixes - Support `Literal` imported from `typing_extensions` ([3a16e58](https://github.com/mkdocstrings/griffe/commit/3a16e5858649f7d786ef8a60b9dfd588f406cd9d) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#545](https://github.com/mkdocstrings/mkdocstrings/issues/545) - Fix parameter default checking logic and diff tests ([1b940fd](https://github.com/mkdocstrings/griffe/commit/1b940fd270b3e51dc0f62edb500a6a3e85908953) by Timothée Mazzucotelli). ## [0.26.0](https://github.com/mkdocstrings/griffe/releases/tag/0.26.0) - 2023-04-03 <small>[Compare with 0.25.5](https://github.com/mkdocstrings/griffe/compare/0.25.5...0.26.0)</small> ### Breaking changes - `AliasResolutionError` instances don't have a `target_path` attribute anymore. It is instead replaced by an `alias` attribute which is a reference to an `Alias` instance. - Lots of positional-or-keyword parameters were changed to keyword-only parameters. ### Deprecations - The `griffe.agents.extensions` module was moved to `griffe.extensions`. The old path is deprecated. ### Features - Support newer versions of `editables` ([ab7a3be](https://github.com/mkdocstrings/griffe/commit/ab7a3be3902af5f4af1d1e762b2b6e532826569f) by Timothée Mazzucotelli): the names of editable modules have changed from `__editables_*` to `_editable_impl_*`. - Provide a JSON schema ([7dfed39](https://github.com/mkdocstrings/griffe/commit/7dfed391c7714a9d1aea9223e1f8c9403d47e8bb) by Timothée Mazzucotelli). - Allow hybrid extension to filter objects and run multiple inspectors ([f8ff53a](https://github.com/mkdocstrings/griffe/commit/f8ff53a69a3a131998649d1a9ba272827b7f2adc) by Timothée Mazzucotelli). - Allow loading extension from file path ([131454e](https://github.com/mkdocstrings/griffe/commit/131454eece81da33cd7f1a8bf2ae030950df8441) by Timothée Mazzucotelli). - Add back `relative_filepath` which now really returns the filepath relative to the current working directory ([40fe0c5](https://github.com/mkdocstrings/griffe/commit/40fe0c53be8ff72f254bd88e9c9cf6df36d3bcb9) by Timothée Mazzucotelli). ### Bug Fixes - Fix JSON schema for ending line numbers (and add test) ([318c6b4](https://github.com/mkdocstrings/griffe/commit/318c6b41c0160070de1b10118d210cacd5f2e711) by Timothée Mazzucotelli). - Prevent cyclic aliases by not overwriting a module member with an indirect alias to itself ([c188a95](https://github.com/mkdocstrings/griffe/commit/c188a95b823e876f89ba9046df2cb06348f92459) by Timothée Mazzucotelli). [Issue #122](https://github.com/mkdocstrings/griffe/issues/122) - Prevent alias resolution errors when copying docstring or labels from previously existing attribute ([48747b6](https://github.com/mkdocstrings/griffe/commit/48747b6d14bdf1be03cfa5bbf849771e3e6801b0) by Timothée Mazzucotelli). - Fix Google admonition regular expression ([ef0be5f](https://github.com/mkdocstrings/griffe/commit/ef0be5f8f276a5ef2397ad89c0cfce0e1b41020e) by Timothée Mazzucotelli). - Add back `griffe.agents.extensions` module (deprecated) ([7129477](https://github.com/mkdocstrings/griffe/commit/7129477184f0b88d3bf165dfe8e1f6158c30914a) by Timothée Mazzucotelli). - Forward class attribute docstrings to instances ([7bf4952](https://github.com/mkdocstrings/griffe/commit/7bf49528541e211af37c2ac5c1a74a4523699c65) by Rodrigo Girão Serrão). [Issue #128](https://github.com/mkdocstrings/griffe/issues/128), [PR #135](https://github.com/mkdocstrings/griffe/pull/135) - Prevent errors related to getting attributes in the inspector ([5d15d27](https://github.com/mkdocstrings/griffe/commit/5d15d276259a4b9a70fbe490d86234e667711180) by Timothée Mazzucotelli). - Catch "member does not exist" errors while expanding wildcards ([a966022](https://github.com/mkdocstrings/griffe/commit/a9660220c0b5e9e786877efa228452a643e93c76) by Timothée Mazzucotelli). - Catch more inspection errors ([4f6eef9](https://github.com/mkdocstrings/griffe/commit/4f6eef9b0fbcdf56d61ac4bec9dc4ef3b90dd116) by Timothée Mazzucotelli). ### Code Refactoring - Log final path after resolving alias ([c7ec7f7](https://github.com/mkdocstrings/griffe/commit/c7ec7f7ca029492ced68737851d66256c5035f70) by Timothée Mazzucotelli). - Move extensions one level up ([67ebd71](https://github.com/mkdocstrings/griffe/commit/67ebd71f9b0933f08b263d0b21520dc0b1a5c4ff) by Timothée Mazzucotelli). - Set default `when` value on extension base classes ([e8ad889](https://github.com/mkdocstrings/griffe/commit/e8ad8893aaad2549bff134a7bf3dfe5a86bfc960) by Timothée Mazzucotelli). - Rename `relative_filepath` to `relative_package_filepath` to better express what it does ([6148f85](https://github.com/mkdocstrings/griffe/commit/6148f85c56848c6bb3e7df8986f1bb208e7083cf) by Timothée Mazzucotelli). - Show file name and line number in alias resolution error messages ([c48928d](https://github.com/mkdocstrings/griffe/commit/c48928df4a75be35771d39bf96699d801485b31d) by Timothée Mazzucotelli). ## [0.25.5](https://github.com/mkdocstrings/griffe/releases/tag/0.25.5) - 2023-02-16 <small>[Compare with 0.25.4](https://github.com/mkdocstrings/griffe/compare/0.25.4...0.25.5)</small> ### Bug Fixes - Fix parsing empty lines with indentation in Google docstrings ([705edff](https://github.com/mkdocstrings/griffe/commit/705edff6c208281bdab387a464799de613b087b5) by Timothée Mazzucotelli). [Issue #129](https://github.com/mkdocstrings/griffe/issues/129) ## [0.25.4](https://github.com/mkdocstrings/griffe/releases/tag/0.25.4) - 2023-01-19 <small>[Compare with 0.25.3](https://github.com/mkdocstrings/griffe/compare/0.25.3...0.25.4)</small> ### Bug Fixes - Fix creation of aliases to modules when inspecting ([54242cb](https://github.com/mkdocstrings/griffe/commit/54242cbdbbcb68785942fa327113cd6508815fa9) by Timothée Mazzucotelli). - Support (setuptools) editable packages with multiple roots ([bd37dfb](https://github.com/mkdocstrings/griffe/commit/bd37dfb16b43fac53207b426ee02218e57a5d5d1) by Gilad). [PR #126](https://github.com/mkdocstrings/griffe/pull/126) ## [0.25.3](https://github.com/mkdocstrings/griffe/releases/tag/0.25.3) - 2023-01-04 <small>[Compare with 0.25.2](https://github.com/mkdocstrings/griffe/compare/0.25.2...0.25.3)</small> ### Bug Fixes - Fix parsing of annotations in Numpy attributes sections ([18fa396](https://github.com/mkdocstrings/griffe/commit/18fa39612b828e2892665b7367f7cdf76908970c) by Timothée Mazzucotelli). [Issue #72](https://github.com/mkdocstrings/griffe/issues/72) ## [0.25.2](https://github.com/mkdocstrings/griffe/releases/tag/0.25.2) - 2022-12-24 <small>[Compare with 0.25.1](https://github.com/mkdocstrings/griffe/compare/0.25.1...0.25.2)</small> ### Bug Fixes - Make sure passage through aliases is reset ([79733f4](https://github.com/mkdocstrings/griffe/commit/79733f4d03f3f66b948dc17c57404349d9e72c9a) by Timothée Mazzucotelli). [Issue #123](https://github.com/mkdocstrings/griffe/issues/123) - Ignore cyclic alias errors when updating target aliases ([bb62b2f](https://github.com/mkdocstrings/griffe/commit/bb62b2f744d221efedeba1cb33151b3787d2ee57) by Timothée Mazzucotelli). [Issue #123](https://github.com/mkdocstrings/griffe/issues/123) ## [0.25.1](https://github.com/mkdocstrings/griffe/releases/tag/0.25.1) - 2022-12-20 <small>[Compare with 0.25.0](https://github.com/mkdocstrings/griffe/compare/0.25.0...0.25.1)</small> ### Bug Fixes - Pass through aliases earlier to prevent infinite recursion ([e533f29](https://github.com/mkdocstrings/griffe/commit/e533f29258838a1e171dea702fb033bfa68ed089) by Timothée Mazzucotelli). [Issue #83](https://github.com/mkdocstrings/griffe/issues/83), [#122](https://github.com/mkdocstrings/griffe/issues/122) ## [0.25.0](https://github.com/mkdocstrings/griffe/releases/tag/0.25.0) - 2022-12-11 <small>[Compare with 0.24.1](https://github.com/mkdocstrings/griffe/compare/0.24.1...0.25.0)</small> ### Breaking changes - Parameter `only_known_modules` was renamed `external` in the [`expand_wildcards()`][griffe.loader.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 <small>[Compare with 0.24.0](https://github.com/mkdocstrings/griffe/compare/0.24.0...0.24.1)</small> ### Bug Fixes - Support nested namespace packages ([d571f8f](https://github.com/mkdocstrings/griffe/commit/d571f8f726d50b34c84fbdaa6db3b2059cfe9dec) by Timothée Mazzucotelli). ## [0.24.0](https://github.com/mkdocstrings/griffe/releases/tag/0.24.0) - 2022-11-13 <small>[Compare with 0.23.0](https://github.com/mkdocstrings/griffe/compare/0.23.0...0.24.0)</small> The "Breaking Changes" and "Deprecations" sections are proudly written with the help of our new API breakage detection feature :smile:! Many thanks to Talley Lambert ([@tlambert03](https://github.com/tlambert03)) for the initial code allowing to compare two Griffe trees. ### Breaking changes - All parameters of the [`load_git`][griffe.git.load_git] function, except `module`, are now keyword-only. - Parameter `try_relative_path` of the [`load_git`][griffe.git.load_git] function was removed. - Parameter `commit` was renamed `ref` in the [`load_git`][griffe.git.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.loader.GriffeLoader.resolve_aliases] method are now keyword-only. - Parameters `only_exported` and `only_known_modules` of the [`resolve_module_aliases`][griffe.loader.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.loader.GriffeLoader.resolve_aliases] method are deprecated in favor of their inverted counter-part `implicit` and `external` parameters. - Example before: `loader.resolve_aliases(only_exported=True, only_known_modules=True)` - Example after: `loader.resolve_aliases(implicit=False, external=False)` ### Features - Add CLI command to check for API breakages ([90bded4](https://github.com/mkdocstrings/griffe/commit/90bded46ccaab0417ed57ed11d3b67597f3845ba) by Timothée Mazzucotelli). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #105](https://github.com/mkdocstrings/griffe/pull/105) - Add function to find API breaking changes ([a4f1280](https://github.com/mkdocstrings/griffe/commit/a4f1280a2b65fabc4caa4448d556ac3e83b2f0d0) by Talley Lambert and Timothée Mazzucotelli). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #105](https://github.com/mkdocstrings/griffe/pull/105) ### Bug Fixes - Fix labels mismatch staticmethod-classmethod in inspector ([25060f6](https://github.com/mkdocstrings/griffe/commit/25060f6dad686c73bd32203dc1b3ac789fdc4aef) by Timothée Mazzucotelli). [Issue #111](https://github.com/mkdocstrings/griffe/issues/111) - Prevent infinite loop while looking for package's parent folder ([f297f1a](https://github.com/mkdocstrings/griffe/commit/f297f1a6550ecadf77c34effe45802327340b1c4) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#72](https://github.com/mkdocstrings/mkdocstrings/issues/72) - Fix comparing names and expressions ([07bffff](https://github.com/mkdocstrings/griffe/commit/07bffff71845d3c9e66007a6a7de269f17312d2b) by Timothée Mazzucotelli). ### Code Refactoring - Rename some parameters in Git module ([9ad7a2c](https://github.com/mkdocstrings/griffe/commit/9ad7a2c1abde97556d9b4657bef4231e1ef6fa19) by Timothée Mazzucotelli). - Set parameters as keyword-only ([44c01be](https://github.com/mkdocstrings/griffe/commit/44c01bec147add34ba3f5ac716ac6722540e3ba7) by Timothée Mazzucotelli). - Remove stars from parameters names ([91dce14](https://github.com/mkdocstrings/griffe/commit/91dce14d7fa3c8c2075a3319fdd7636443fe6cbc) by Timothée Mazzucotelli). - Refactor CLI to use subcommands ([760b091](https://github.com/mkdocstrings/griffe/commit/760b0918c60911386932cec720418af8d3360c1b) by Timothée Mazzucotelli). [PR #110](https://github.com/mkdocstrings/griffe/pull/110) - Rename parameters used when resolving aliases ([3d3a4eb](https://github.com/mkdocstrings/griffe/commit/3d3a4eb99e587bd9dd7bfadca4c45737fb886139) by Timothée Mazzucotelli). ## [0.23.0](https://github.com/mkdocstrings/griffe/releases/tag/0.23.0) - 2022-10-26 <small>[Compare with 0.22.2](https://github.com/mkdocstrings/griffe/compare/0.22.2...0.23.0)</small> ### Features - Support `typing_extensions.overload` ([c29fad5](https://github.com/mkdocstrings/griffe/commit/c29fad58c721399badfc93ff8e0f10a6f92c359e) by Nyuan Zhang). [PR #108](https://github.com/mkdocstrings/griffe/pull/108) ### Bug Fixes - Log debug instead of errors when failing to parse NumPy annotations for additional sections ([568ff60](https://github.com/mkdocstrings/griffe/commit/568ff60621c0b5cc35ac0e0d0209fa3bc1b2ba8a) by Sigurd Spieckermann). [Issue #93](https://github.com/mkdocstrings/griffe/issues/93), [PR #109](https://github.com/mkdocstrings/griffe/pull/109) - Don't strip too many parentheses around a call node ([bb5c5e7](https://github.com/mkdocstrings/griffe/commit/bb5c5e71f95c537ca2d19299b157a0bbf59e5279) by Timothée Mazzucotelli). [PR #107](https://github.com/mkdocstrings/griffe/pull/107) - Guard against more alias resolution errors ([2be135d](https://github.com/mkdocstrings/griffe/commit/2be135d8ab88d6f97175c958e31e76b0d7d8f934) by Timothée Mazzucotelli). [Issue #83](https://github.com/mkdocstrings/griffe/issues/83), [PR #103](https://github.com/mkdocstrings/griffe/pull/103) ## [0.22.2](https://github.com/mkdocstrings/griffe/releases/tag/0.22.2) - 2022-09-24 <small>[Compare with 0.22.1](https://github.com/mkdocstrings/griffe/compare/0.22.1...0.22.2)</small> ### Bug Fixes - Log debug instead of errors when failing to parse Numpy annotations ([75eeeda](https://github.com/mkdocstrings/griffe/commit/75eeeda2f1181ae680b3d47df3814bad200220d3) by Timothée Mazzucotelli). [Issue #93](https://github.com/mkdocstrings/griffe/issues/93) - Don't crash on unsupported module names (containing dots) ([6a57194](https://github.com/mkdocstrings/griffe/commit/6a571949000a3d2910990337f96751c0cac7e815) by Timothée Mazzucotelli). [Issue #94](https://github.com/mkdocstrings/griffe/issues/94) - Show correct docstring line numbers on Python 3.7 ([edd4b6d](https://github.com/mkdocstrings/griffe/commit/edd4b6d23f4399960db4e16a8c269318aef033d6) by Timothée Mazzucotelli). [Issue #98](https://github.com/mkdocstrings/griffe/issues/98) - Fix parsing of Numpy docstring with an Examples section at the end ([3114727](https://github.com/mkdocstrings/griffe/commit/3114727296891fdd5cacecf487652774ee6e4fc8) by Timothée Mazzucotelli). [Issue #97](https://github.com/mkdocstrings/griffe/issues/97) - Don't crash on unsupported item in `__all__` (log a warning instead) ([9e5df0a](https://github.com/mkdocstrings/griffe/commit/9e5df0aea8e615217554e5204221a35c9df25938) by Timothée Mazzucotelli). [Issue #92](https://github.com/mkdocstrings/griffe/issues/92) - Prevent infinite recursion while expanding exports ([68446f7](https://github.com/mkdocstrings/griffe/commit/68446f7ab94536596dccb690fb2cac613cd32460) by Timothée Mazzucotelli). - Add missing check while expanding wildcards ([7e816ed](https://github.com/mkdocstrings/griffe/commit/7e816ed141d6f13bf1ae7c758c32e68cc663fe0e) by Timothée Mazzucotelli). ## [0.22.1](https://github.com/mkdocstrings/griffe/releases/tag/0.22.1) - 2022-09-10 <small>[Compare with 0.22.0](https://github.com/mkdocstrings/griffe/compare/0.22.0...0.22.1)</small> ### Bug Fixes - Always use `encoding="utf8"` when reading text files ([3b279bf](https://github.com/mkdocstrings/griffe/commit/3b279bf61afabc7312e9e58745fd19a53d97ac74) by Rudolf Byker). [Issue #99](https://github.com/mkdocstrings/griffe/issues/99), [PR #100](https://github.com/mkdocstrings/griffe/pull/100) ## [0.22.0](https://github.com/mkdocstrings/griffe/releases/tag/0.22.0) - 2022-06-28 <small>[Compare with 0.21.0](https://github.com/mkdocstrings/griffe/compare/0.21.0...0.22.0)</small> ### Features - Support forward references ([245daea](https://github.com/mkdocstrings/griffe/commit/245daeabc8130bd7ecab86f55c4906d9161b9e73) by Timothée Mazzucotelli). [Issue #86](https://github.com/mkdocstrings/griffe/issues/86) ### Code Refactoring - Safely parse annotations and values ([b023e2b](https://github.com/mkdocstrings/griffe/commit/b023e2be509f3ac39dbe1ed9adf21247e4416e53) by Timothée Mazzucotelli). ## [0.21.0](https://github.com/mkdocstrings/griffe/releases/tag/0.21.0) - 2022-06-25 <small>[Compare with 0.20.0](https://github.com/mkdocstrings/griffe/compare/0.20.0...0.21.0)</small> ### Features - Add `load_git` function allowing to load data from a specific git ref ([b2c3946](https://github.com/mkdocstrings/griffe/commit/b2c39467630c33edc914dd7e6dc96fb611267905) by Talley Lambert). [Issue #75](https://github.com/mkdocstrings/griffe/issues/75), [PR #76](https://github.com/mkdocstrings/griffe/pull/76) ### Bug Fixes - Fix detecting and merging stubs for single-file packages ([6a82542](https://github.com/mkdocstrings/griffe/commit/6a825423a9dfd86343532c2872980240f2e98b74) by Talley Lambert). [Issue #77](https://github.com/mkdocstrings/griffe/issues/77), [PR #78](https://github.com/mkdocstrings/griffe/pull/78) - Fix parsing ExtSlice nodes when getting values ([b2fe968](https://github.com/mkdocstrings/griffe/commit/b2fe9684f274786decdf9fb395bebc5057235eda) by Timothée Mazzucotelli). [Issue #87](https://github.com/mkdocstrings/griffe/issues/87) - Don't trigger alias resolution when merging stubs ([2b88627](https://github.com/mkdocstrings/griffe/commit/2b88627862b8db50045cc97ae5644abd36f36b5a) by Timothée Mazzucotelli). [Issue #89](https://github.com/mkdocstrings/griffe/issues/89) - Fix handling of .pth files ([f212dd3](https://github.com/mkdocstrings/griffe/commit/f212dd3b92f51a64795fdbb30aefd0a730393523) by Gabriel Dugny). [Issue #84](https://github.com/mkdocstrings/griffe/issues/84), [PR #85](https://github.com/mkdocstrings/griffe/pull/85) ## [0.20.0](https://github.com/mkdocstrings/griffe/releases/tag/0.20.0) - 2022-06-03 <small>[Compare with 0.19.3](https://github.com/mkdocstrings/griffe/compare/0.19.3...0.20.0)</small> ### Features - Add `as_json` and `from_json` convenience methods on objects ([5c3d751](https://github.com/mkdocstrings/griffe/commit/5c3d7511d2465e16805fa564c3d60d44618410d8) by Talley Lambert). [PR #74](https://github.com/mkdocstrings/griffe/pull/74) ### Bug Fixes - Fix unparsing of f-strings ([9ca74bd](https://github.com/mkdocstrings/griffe/commit/9ca74bd144167de9506cf5b0725a784e52f5e67a) by Timothée Mazzucotelli). [Issue #80](https://github.com/mkdocstrings/griffe/issues/80) - Don't crash when overwriting a submodule with a wildcard imported attribute ([bfad1cc](https://github.com/mkdocstrings/griffe/commit/bfad1ccf079e69fa0161754d9f1f7edd5819f943) by Timothée Mazzucotelli). [Issue #72](https://github.com/mkdocstrings/griffe/issues/72), [#79](https://github.com/mkdocstrings/griffe/issues/79), [mkdocstrings/mkdocstrings#438](https://github.com/mkdocstrings/mkdocstrings/issues/438) ## [0.19.3](https://github.com/mkdocstrings/griffe/releases/tag/0.19.3) - 2022-05-26 <small>[Compare with 0.19.2](https://github.com/mkdocstrings/griffe/compare/0.19.2...0.19.3)</small> ### Bug Fixes - Support USub and UAdd nodes in annotations ([1169c51](https://github.com/mkdocstrings/griffe/commit/1169c51bd6ae04f491fa5e50cae93d99e8ce920d) by Timothée Mazzucotelli). [Issue #71](https://github.com/mkdocstrings/griffe/issues/71) ## [0.19.2](https://github.com/mkdocstrings/griffe/releases/tag/0.19.2) - 2022-05-18 <small>[Compare with 0.19.1](https://github.com/mkdocstrings/griffe/compare/0.19.1...0.19.2)</small> ### Bug Fixes - Don't crash on single line docstrings with trailing whitespace (Google) ([8d9ccd5](https://github.com/mkdocstrings/griffe/commit/8d9ccd531dd91c6fbfa0922a0133680f881733b0) by Timothée Mazzucotelli). ## [0.19.1](https://github.com/mkdocstrings/griffe/releases/tag/0.19.1) - 2022-05-07 <small>[Compare with 0.19.0](https://github.com/mkdocstrings/griffe/compare/0.19.0...0.19.1)</small> ### Bug Fixes - Don't crash on nested functions in `__init__` methods ([cd5af43](https://github.com/mkdocstrings/griffe/commit/cd5af43f3a98d54d822015818b7aa0ef15159286) by Timothée Mazzucotelli). [Issue #68](https://github.com/mkdocstrings/griffe/issues/68) ## [0.19.0](https://github.com/mkdocstrings/griffe/releases/tag/0.19.0) - 2022-05-06 <small>[Compare with 0.18.0](https://github.com/mkdocstrings/griffe/compare/0.18.0...0.19.0)</small> ### Features - Add `load` shortcut function for convenience ([f38a42d](https://github.com/mkdocstrings/griffe/commit/f38a42ddd7ac9d58f36627d9f2a69f4acd65df50) by Timothée Mazzucotelli). - Support loading (and merging) `*.pyi` files ([41518f4](https://github.com/mkdocstrings/griffe/commit/41518f4aa9e00756a910067cf6f01f07ca7327da) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#404](https://github.com/mkdocstrings/mkdocstrings/issues/404) - Improve support for call nodes in annotations ([45e5bf5](https://github.com/mkdocstrings/griffe/commit/45e5bf53d509344b3f28118836d356903c64bbf3) by Timothée Mazzucotelli). [Issue #66](https://github.com/mkdocstrings/griffe/issues/66) - Support `dataclass` decorators on classes ([f579431](https://github.com/mkdocstrings/griffe/commit/f579431474cc4db687e4264f5062074654dec2f3) by Timothée Mazzucotelli). ### Code Refactoring - Handle absence of values ([190585d](https://github.com/mkdocstrings/griffe/commit/190585d3482bfc3a72694910529b7a0aac35444c) by Timothée Mazzucotelli). - Simplify decorators to labels function ([04e768f](https://github.com/mkdocstrings/griffe/commit/04e768fb621898faf7a96cc7e7170f10da876664) by Timothée Mazzucotelli). - Always sort labels when serializing ([bd2504b](https://github.com/mkdocstrings/griffe/commit/bd2504bdb43df3e290c88bd8d25903823f5fc2d6) by Timothée Mazzucotelli). ## [0.18.0](https://github.com/mkdocstrings/griffe/releases/tag/0.18.0) - 2022-04-19 <small>[Compare with 0.17.0](https://github.com/mkdocstrings/griffe/compare/0.17.0...0.18.0)</small> ### Features - Add CLI option to disallow inspection ([8f71a07](https://github.com/mkdocstrings/griffe/commit/8f71a07c17de4cfb2b519dc2b4086f102de4d325) by Timothée Mazzucotelli). - Support complex `__all__` assignments ([9a2128b](https://github.com/mkdocstrings/griffe/commit/9a2128b8d4533119b705ec47fc1eca404b4282ef) by Timothée Mazzucotelli). [Issue #40](https://github.com/mkdocstrings/griffe/issues/40) - Inherit class parameters from `__init__` method ([e195593](https://github.com/mkdocstrings/griffe/commit/e195593b181690313c9e447c8bc2befa72fd6e09) by François Rozet). [Issue mkdocstrings/python#19](https://github.com/mkdocstrings/python/issues/19), [PR #65](https://github.com/mkdocstrings/python/pull/65). It allows to write "Parameters" sections in the docstring of the class itself. ### Performance Improvements - Avoid using `__len__` as boolean method ([d465493](https://github.com/mkdocstrings/griffe/commit/d4654930577186fb6d3e89ea1561a2daf15b3a65) by Timothée Mazzucotelli). ### Bug Fixes - Don't crash on unhandle `__all__` assignments ([cbc103c](https://github.com/mkdocstrings/griffe/commit/cbc103c91836db2e235a46a0f9048c1230de507d) by Timothée Mazzucotelli). - Handle empty packages names in CLI ([52b51c4](https://github.com/mkdocstrings/griffe/commit/52b51c49a14783c986beb851abd33cbcd0ab8729) by Timothée Mazzucotelli). - Don't crash on Google parameters sections found in non-function docstrings ([4a417bc](https://github.com/mkdocstrings/griffe/commit/4a417bc6c0e83b42fe1a74a4a8b0881d3955075f) by Timothée Mazzucotelli). [Issue mkdocstrings/python#19](https://github.com/mkdocstrings/python/issues/19) ### Code Refactoring - Improve "unknown parameter" messages ([7191799](https://github.com/mkdocstrings/griffe/commit/7191799c92d7544f949c5870cf2867e02d406c57) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#423](https://github.com/mkdocstrings/mkdocstrings/issues/423) - Set property label on `@cached_property`-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 <small>[Compare with 0.16.0](https://github.com/mkdocstrings/griffe/compare/0.16.0...0.17.0)</small> ### Features - Handle properties setters and deleters ([50a4490](https://github.com/mkdocstrings/griffe/commit/50a449069de89bb83da854b1bbd1681ec68f0395) by Timothée Mazzucotelli). - Handle `typing.overload` decorator ([927bbd9](https://github.com/mkdocstrings/griffe/commit/927bbd9fe7712e8d0fc9763fb51d89bef3173350) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#308](https://github.com/mkdocstrings/mkdocstrings/issues/308) - Set labels on functions using decorators ([1c1feb2](https://github.com/mkdocstrings/griffe/commit/1c1feb264c748f4a78ffebf3b9ea1966f2533522) by Timothée Mazzucotelli). [Issue #47](https://github.com/mkdocstrings/griffe/issues/47) - Add `runtime` attribute to objects/aliases and handle type guarded objects ([2f2a04e](https://github.com/mkdocstrings/griffe/commit/2f2a04ea498aa50133b1404f3bc3498a25648545) by Timothée Mazzucotelli). [Issue #42](https://github.com/mkdocstrings/griffe/issues/42) - Support pkg-style namespace packages ([efba0c6](https://github.com/mkdocstrings/griffe/commit/efba0c6a5e1dc185e96e5a09c05e94c751abc4cb) by Timothée Mazzucotelli). [Issue #58](https://github.com/mkdocstrings/griffe/issues/58) ### Code Refactoring - Remove useless attribute ([c4a92b7](https://github.com/mkdocstrings/griffe/commit/c4a92b7e2cbe240a376d5d6944b7b0d23255648b) by Timothée Mazzucotelli). - Improve Google warnings ([641089a](https://github.com/mkdocstrings/griffe/commit/641089aed53423894df8733941e404f7e6505b94) by Timothée Mazzucotelli). - Remove useless import nodes generic visits ([f83fc8e](https://github.com/mkdocstrings/griffe/commit/f83fc8e629451abd4f4eadfe34b448fb3b77b9b6) by Timothée Mazzucotelli). ## [0.16.0](https://github.com/mkdocstrings/griffe/releases/tag/0.16.0) - 2022-04-09 <small>[Compare with 0.15.1](https://github.com/mkdocstrings/griffe/compare/0.15.1...0.16.0)</small> ### Features - Warn about unknown parameters in Numpy docstrings ([23f63f2](https://github.com/mkdocstrings/griffe/commit/23f63f255eef5aa2dbaa1765f93634ecaf94dbb3) by Timothée Mazzucotelli). - Warn about unknown parameters in Google docstrings ([72be993](https://github.com/mkdocstrings/griffe/commit/72be993c95460a6465a4e70a95b79ae4095db541) by Kevin Musgrave). [Issue mkdocstrings/mkdocstrings#408](https://github.com/mkdocstrings/mkdocstrings/issues/408), [PR #63](https://github.com/mkdocstrings/griffe/issues/63) ### Bug Fixes - Don't crash on unhandled AST nodes while parsing text annotations ([f3be3a6](https://github.com/mkdocstrings/griffe/commit/f3be3a68141e24a9c0c6b9a87e3f22e75a168d80) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#416](https://github.com/mkdocstrings/mkdocstrings/issues/416) ## [0.15.1](https://github.com/mkdocstrings/griffe/releases/tag/0.15.1) - 2022-04-08 <small>[Compare with 0.15.0](https://github.com/mkdocstrings/griffe/compare/0.15.0...0.15.1)</small> ### Bug Fixes - Don't overwrite existing (lower) members when expanding wildcards ([9ff86e3](https://github.com/mkdocstrings/griffe/commit/9ff86e369d8fb3a6eeb7d94cd60c87fa26bf74b4) by Timothée Mazzucotelli). - Don't insert admonition before current section (Google parser) ([8d8a46f](https://github.com/mkdocstrings/griffe/commit/8d8a46fca7df917c4bba979128d94d3b79252ff5) by Timothée Mazzucotelli). - Handle aliases chains in `has_docstrings` method ([77c6943](https://github.com/mkdocstrings/griffe/commit/77c69430ddc74fedaa33fa65afd59ac546900829) by Timothée Mazzucotelli). - Actually check for docstrings recursively ([15f4193](https://github.com/mkdocstrings/griffe/commit/15f4193b764f85dcab042ab193e984bebf151029) by Timothée Mazzucotelli). ## [0.15.0](https://github.com/mkdocstrings/griffe/releases/tag/0.15.0) - 2022-04-03 <small>[Compare with 0.14.1](https://github.com/mkdocstrings/griffe/compare/0.14.1...0.15.0)</small> ### Features - Support `ignore_init_summary` in Numpy parser ([f8cd147](https://github.com/mkdocstrings/griffe/commit/f8cd14734603d29e6e72c9a350f663dccdeb36b4) by Timothée Mazzucotelli). [Issue #44](https://github.com/mkdocstrings/griffe/issues/44) - Enable cross-references for Numpy docstrings annotations ([e32a73c](https://github.com/mkdocstrings/griffe/commit/e32a73c9e100cf0778768c4a1f76152d9aecc451) by Timothée Mazzucotelli). Issues [#11](https://github.com/mkdocstrings/griffe/issues/11), [#12](https://github.com/mkdocstrings/griffe/issues/12), [#13](https://github.com/mkdocstrings/griffe/issues/13), [#14](https://github.com/mkdocstrings/griffe/issues/14), [#15](https://github.com/mkdocstrings/griffe/issues/15), [#16](https://github.com/mkdocstrings/griffe/issues/16), [#17](https://github.com/mkdocstrings/griffe/issues/17), [#18](https://github.com/mkdocstrings/griffe/issues/18) - Retrieve annotations from parent in Numpy parser ([8d4eae3](https://github.com/mkdocstrings/griffe/commit/8d4eae353cbd42f47fe6f8101e6e1f8be4054c84) by Timothée Mazzucotelli). Issues [#29](https://github.com/mkdocstrings/griffe/issues/29), [#30](https://github.com/mkdocstrings/griffe/issues/30), [#31](https://github.com/mkdocstrings/griffe/issues/31), [#32](https://github.com/mkdocstrings/griffe/issues/32) - Parse annotations in Iterator/Generator for Google docstrings ([f0129ef](https://github.com/mkdocstrings/griffe/commit/f0129efa2046089355ee62c48f23eb0189b054ce) by Timothée Mazzucotelli). [Issue #28](https://github.com/mkdocstrings/griffe/issues/28) ### Bug Fixes - Fix missing "receives" entry in Google parser ([35d63fb](https://github.com/mkdocstrings/griffe/commit/35d63fbd566fa439a255c3f44ffeb4a9474db7f9) by Timothée Mazzucotelli). - Fix serialization of Windows paths ([b7e8da8](https://github.com/mkdocstrings/griffe/commit/b7e8da868cd6ec8230f2d58a8f3c38248f7c97b2) by Timothée Mazzucotelli). ### Code Refactoring - Be less strict on spacing around ":" in Numpy docstrings ([aa592b5](https://github.com/mkdocstrings/griffe/commit/aa592b5f38b71e6eadd883257d2239fceec43752) by Timothée Mazzucotelli). - Be less strict in Numpy regular expressions ([603dc0e](https://github.com/mkdocstrings/griffe/commit/603dc0e21aa12754ec4f76ffc40869bf8519935d) by Timothée Mazzucotelli). - Rename variables in Numpy module ([4407244](https://github.com/mkdocstrings/griffe/commit/4407244a2e4b59c988c61e4c7b9f07532cad5b3c) by Timothée Mazzucotelli). ## [0.14.1](https://github.com/mkdocstrings/griffe/releases/tag/0.14.1) - 2022-04-01 <small>[Compare with 0.14.0](https://github.com/mkdocstrings/griffe/compare/0.14.0...0.14.1)</small> ### Bug Fixes - Retrieve default value for non-string parameters ([15952ed](https://github.com/mkdocstrings/griffe/commit/15952ed72f6f5db3a4dec2fc60cb256c838be6a3) by ThomasPJ). [Issue #59](https://github.com/mkdocstrings/griffe/issues/59), [issue mkdocstrings/python#8](https://github.com/mkdocstrings/python/issues/8), [PR #60](https://github.com/mkdocstrings/griffe/pull/60) - Prevent infinite recursion while expanding wildcards ([428628f](https://github.com/mkdocstrings/griffe/commit/428628f423192611529b9b346cd295999d0dad25) by Timothée Mazzucotelli). [Issue #57](https://github.com/mkdocstrings/griffe/issues/57) ## [0.14.0](https://github.com/mkdocstrings/griffe/releases/tag/0.14.0) - 2022-03-06 <small>[Compare with 0.13.2](https://github.com/mkdocstrings/griffe/compare/0.13.2...0.14.0)</small> ### Features - Ignore `__doc__` from parent classes ([10aa59e](https://github.com/mkdocstrings/griffe/commit/10aa59ef2fbf1db2c8829e0905bea88406495c41) by Will Da Silva). [Issue #55](https://github.com/mkdocstrings/griffe/issues/55), [PR #56](https://github.com/mkdocstrings/griffe/pull/56) ## [0.13.2](https://github.com/mkdocstrings/griffe/releases/tag/0.13.2) - 2022-03-01 <small>[Compare with 0.13.1](https://github.com/mkdocstrings/griffe/compare/0.13.1...0.13.2)</small> ### Bug Fixes - Fix type regex in Numpy parser ([3a10fda](https://github.com/mkdocstrings/griffe/commit/3a10fda89c2e32e2d8acd89eb1ce8ab20a0fc251) by Timothée Mazzucotelli). - Current module must not be available in its members' scope ([54f9688](https://github.com/mkdocstrings/griffe/commit/54f9688c11a1f7d3893ca774a07afe876f0b809c) by Timothée Mazzucotelli). - Allow named sections after numpydoc examples ([a44d9c6](https://github.com/mkdocstrings/griffe/commit/a44d9c65cf24d2820e805d23365f38aab82c8c07) by Lucina). [PR #54](https://github.com/mkdocstrings/griffe/pull/54) ## [0.13.1](https://github.com/mkdocstrings/griffe/releases/tag/0.13.1) - 2022-02-24 <small>[Compare with 0.13.0](https://github.com/mkdocstrings/griffe/compare/0.13.0...0.13.1)</small> ### Bug Fixes - Don't cut through wildcard-expanded aliases chains ([65dafa4](https://github.com/mkdocstrings/griffe/commit/65dafa4660e8c95687cad4d5c5145a56f126ae61) by Timothée Mazzucotelli). - Fix docstrings warnings when there's no parent module ([e080549](https://github.com/mkdocstrings/griffe/commit/e080549e3eaf887a0f037a4457329eab35bd6409) by Timothée Mazzucotelli). [Issue #51](https://github.com/mkdocstrings/griffe/issues/51) ### Code Refactoring - Use proper classes for docstrings sections ([46eddac](https://github.com/mkdocstrings/griffe/commit/46eddac0b847eeb75e4964a3186069f7698235b0) by Timothée Mazzucotelli). [Issue mkdocstrings/python#3](https://github.com/mkdocstrings/python/issues/3), [PR #52](https://github.com/mkdocstrings/griffe/pull/52) ## [0.13.0](https://github.com/mkdocstrings/griffe/releases/tag/0.13.0) - 2022-02-23 <small>[Compare with 0.12.6](https://github.com/mkdocstrings/griffe/compare/0.12.6...0.13.0)</small> ### Features - Implement `trim_doctest_flags` for Google and Numpy ([8057153](https://github.com/mkdocstrings/griffe/commit/8057153823711d8f486b1c52469090ce404771cb) by Jeremy Goh). [Issue mkdocstrings/mkdocstrings#386](https://github.com/mkdocstrings/mkdocstrings/issues/386), [PR #48](https://github.com/mkdocstrings/griffe/pull/48) ### Bug Fixes - Rename keyword parameters to keyword arguments ([ce3eb6b](https://github.com/mkdocstrings/griffe/commit/ce3eb6b5d7caad6df41496dd300924535d92dc7f) by Jeremy Goh). ## [0.12.6](https://github.com/mkdocstrings/griffe/releases/tag/0.12.6) - 2022-02-18 <small>[Compare with 0.12.5](https://github.com/mkdocstrings/griffe/compare/0.12.5...0.12.6)</small> ### Bug Fixes - Support starred parameters in Numpy docstrings ([27f0fc2](https://github.com/mkdocstrings/griffe/commit/27f0fc21299a41a3afc07b46afbe8f37757c3918) by Timothée Mazzucotelli). [Issue #43](https://github.com/mkdocstrings/griffe/issues/43) ## [0.12.5](https://github.com/mkdocstrings/griffe/releases/tag/0.12.5) - 2022-02-17 <small>[Compare with 0.12.4](https://github.com/mkdocstrings/griffe/compare/0.12.4...0.12.5)</small> ### Bug Fixes - Fix getting line numbers on aliases ([351750e](https://github.com/mkdocstrings/griffe/commit/351750ea70d0ab3f10c2766846c10d00612cda1d) by Timothée Mazzucotelli). ## [0.12.4](https://github.com/mkdocstrings/griffe/releases/tag/0.12.4) - 2022-02-16 <small>[Compare with 0.12.3](https://github.com/mkdocstrings/griffe/compare/0.12.3...0.12.4)</small> ### Bug Fixes - Update target path when changing alias target ([5eda646](https://github.com/mkdocstrings/griffe/commit/5eda646f7bc2fdb112887fdeaa07f8a2f4635c12) by Timothée Mazzucotelli). - Fix relative imports to absolute with wildcards ([69500dd](https://github.com/mkdocstrings/griffe/commit/69500dd0ce06f4acc91eb60ff20ac8d79303a281) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) - Fix accessing members using tuples ([87ff1df](https://github.com/mkdocstrings/griffe/commit/87ff1dfae93d9eb6f735f9c1290092d61cac7591) by Timothée Mazzucotelli). - Fix recursive wildcard expansion ([60e6edf](https://github.com/mkdocstrings/griffe/commit/60e6edf9dcade104b069946380a0d1dcc22bce9a) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) - Only export submodules if they were imported ([98c72db](https://github.com/mkdocstrings/griffe/commit/98c72dbab114fd7782efd6f2f9bbf78e3f4ccb27) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#382](https://github.com/mkdocstrings/mkdocstrings/issues/382) ## [0.12.3](https://github.com/mkdocstrings/griffe/releases/tag/0.12.3) - 2022-02-15 <small>[Compare with 0.12.2](https://github.com/mkdocstrings/griffe/compare/0.12.2...0.12.3)</small> ### Bug Fixes - Always decode source as UTF8 ([563469b](https://github.com/mkdocstrings/griffe/commit/563469b4cf320ea38096846312dc757a614d8094) by Timothée Mazzucotelli). - Fix JSON encoder and decoder ([3e768d6](https://github.com/mkdocstrings/griffe/commit/3e768d6574a45624237e0897c1d6a6c87e446016) by Timothée Mazzucotelli). ### Code Refactoring - Improve error handling ([7b15a51](https://github.com/mkdocstrings/griffe/commit/7b15a51fb9dd4722757f272f00402ce29ef2bd3f) by Timothée Mazzucotelli). ## [0.12.2](https://github.com/mkdocstrings/griffe/releases/tag/0.12.2) - 2022-02-13 <small>[Compare with 0.12.1](https://github.com/mkdocstrings/griffe/compare/0.12.1...0.12.2)</small> ### Bug Fixes - Fix JSON unable to serialize docstring kind values ([91e6719](https://github.com/mkdocstrings/griffe/commit/91e67190fc4f69911ad6ea3eb239a74fc1f15ba6) by Timothée Mazzucotelli). ### Code Refactoring - Make attribute labels more explicit ([19eac2e](https://github.com/mkdocstrings/griffe/commit/19eac2e5a13d77175849c199ba3337a66e3824a2) by Timothée Mazzucotelli). ## [0.12.1](https://github.com/mkdocstrings/griffe/releases/tag/0.12.1) - 2022-02-12 <small>[Compare with 0.11.7](https://github.com/mkdocstrings/griffe/compare/0.11.7...0.12.1)</small> ### Features - Add `ignore_init_summary` option to the Google parser ([81f0333](https://github.com/mkdocstrings/griffe/commit/81f0333b1691955f6020095051b2cf869f0c2c24) by Timothée Mazzucotelli). - Add `is_KIND` properties on objects ([17a08cd](https://github.com/mkdocstrings/griffe/commit/17a08cd7142bdee041577735d5e5ac246c181ec9) by Timothée Mazzucotelli). ## [0.11.7](https://github.com/mkdocstrings/griffe/releases/tag/0.11.7) - 2022-02-12 <small>[Compare with 0.11.6](https://github.com/mkdocstrings/griffe/compare/0.11.6...0.11.7)</small> ### Bug Fixes - Keep only first assignment in conditions ([0104440](https://github.com/mkdocstrings/griffe/commit/010444018ca6ba437e70166e0da3e2d2ca6bbbe8) by Timothée Mazzucotelli). - Support invert unary op in annotations ([734ef55](https://github.com/mkdocstrings/griffe/commit/734ef551f5c5b2b4b48de32033d4c2e7cff0a124) by Timothée Mazzucotelli). - Fix handling of missing modules during dynamic imports ([7a3b383](https://github.com/mkdocstrings/griffe/commit/7a3b38349712c5b66792da1a8a9efae1b6f663a7) by Timothée Mazzucotelli). [Issue mkdocstrings/mkdocstrings#380](https://github.com/mkdocstrings/mkdocstrings/issues/380) - Fix getting lines of compiled modules ([899461b](https://github.com/mkdocstrings/griffe/commit/899461b2f48622f334ceeaa6d73c935bacb540ea) by Timothée Mazzucotelli). ### Code Refactoring - Get annotation with the same property on functions ([ecc7bba](https://github.com/mkdocstrings/griffe/commit/ecc7bba8880f90417a21830e0e9cccf30f582399) by Timothée Mazzucotelli). ## [0.11.6](https://github.com/mkdocstrings/griffe/releases/tag/0.11.6) - 2022-02-10 <small>[Compare with 0.11.5](https://github.com/mkdocstrings/griffe/compare/0.11.5...0.11.6)</small> ### Bug Fixes - Fix infinite loop in Google parser ([8b7b97b](https://github.com/mkdocstrings/griffe/commit/8b7b97b6f507dc91b957592e1d247d79bd3e9a5b) by Timothée Mazzucotelli). [Issue #38](https://github.com/mkdocstrings/griffe/issues/38) ## [0.11.5](https://github.com/mkdocstrings/griffe/releases/tag/0.11.5) - 2022-02-08 <small>[Compare with 0.11.4](https://github.com/mkdocstrings/griffe/compare/0.11.4...0.11.5)</small> ### Bug Fixes - Fix building title and kind of Google admonitions ([87ab56c](https://github.com/mkdocstrings/griffe/commit/87ab56cfe5458b313527bc2eb47ea418fcb231ab) by Timothée Mazzucotelli). [Issue mkdocstrings#379](https://github.com/mkdocstrings/mkdocstrings/issues/379) ## [0.11.4](https://github.com/mkdocstrings/griffe/releases/tag/0.11.4) - 2022-02-07 <small>[Compare with 0.11.3](https://github.com/mkdocstrings/griffe/compare/0.11.3...0.11.4)</small> ### Bug Fixes - Don't trigger alias resolution while checking docstrings presence ([dda72ea](https://github.com/mkdocstrings/griffe/commit/dda72ea56b091d1c9bc1b7aa369548328894da29) by Timothée Mazzucotelli). [Issue #37](https://github.com/mkdocstrings/griffe/issues/37) ## [0.11.3](https://github.com/mkdocstrings/griffe/releases/tag/0.11.3) - 2022-02-05 <small>[Compare with 0.11.2](https://github.com/mkdocstrings/griffe/compare/0.11.2...0.11.3)</small> ### Bug Fixes - Fix getting params defaults on Python 3.7 ([0afd867](https://github.com/mkdocstrings/griffe/commit/0afd8675d2d24302d68619f31adbe5ac5d8ff5a7) by Timothée Mazzucotelli). ## [0.11.2](https://github.com/mkdocstrings/griffe/releases/tag/0.11.2) - 2022-02-03 <small>[Compare with 0.11.1](https://github.com/mkdocstrings/griffe/compare/0.11.1...0.11.2)</small> ### Code Refactoring - Factorize docstring annotation parser ([19609be](https://github.com/mkdocstrings/griffe/commit/19609bede6227998a1322dbed6fcc1ae2e924bc8) by Timothée Mazzucotelli). ## [0.11.1](https://github.com/mkdocstrings/griffe/releases/tag/0.11.1) - 2022-02-01 <small>[Compare with 0.11.0](https://github.com/mkdocstrings/griffe/compare/0.11.0...0.11.1)</small> ### Code Refactoring - Rename RST parser to Sphinx ([a612cb1](https://github.com/mkdocstrings/griffe/commit/a612cb1c8d52fabe5a1ebaf892e9b82c67d15a30) by Timothée Mazzucotelli). ## [0.11.0](https://github.com/mkdocstrings/griffe/releases/tag/0.11.0) - 2022-01-31 <small>[Compare with 0.10.0](https://github.com/mkdocstrings/griffe/compare/0.10.0...0.11.0)</small> ### Features - Support matrix multiplication operator in visitor ([6129e17](https://github.com/mkdocstrings/griffe/commit/6129e17c86ff49a8e539039dcd04a58b30e3648e) by Timothée Mazzucotelli). ### Bug Fixes - Fix name resolution for inspected data ([ed3e7e5](https://github.com/mkdocstrings/griffe/commit/ed3e7e5fa8a9d702c92f47e8244635cf11a923f2) by Timothée Mazzucotelli). - Make importer actually able to import any nested object ([d007219](https://github.com/mkdocstrings/griffe/commit/d00721971c7b820e16e463408f04cc3e81a14db6) by Timothée Mazzucotelli). ### Code Refactoring - Always use search paths to import modules ([a9a378f](https://github.com/mkdocstrings/griffe/commit/a9a378fc6e47678e08a22383879e4d01acd16b54) by Timothée Mazzucotelli). - Split out module finder ([7290642](https://github.com/mkdocstrings/griffe/commit/7290642e36341e64b8ed770e237e9f232e05eada) by Timothée Mazzucotelli). ## [0.10.0](https://github.com/mkdocstrings/griffe/releases/tag/0.10.0) - 2022-01-14 <small>[Compare with 0.9.0](https://github.com/mkdocstrings/griffe/compare/0.9.0...0.10.0)</small> ### Bug Fixes - Fix infinite recursion errors in alias resolver ([133b4e4](https://github.com/mkdocstrings/griffe/commit/133b4e4bf721fc7536a1ca957f13f7c9f83bf07a) by Timothée Mazzucotelli). - Fix inspection of nodes children (aliases or not) ([bb354f2](https://github.com/mkdocstrings/griffe/commit/bb354f21e7b079f4c1e8dd50297d53810c18450e) by Timothée Mazzucotelli). - Fix relative to absolute import conversion ([464c39e](https://github.com/mkdocstrings/griffe/commit/464c39eaa812a927190469b18bd910e95e3c1d3c) by Timothée Mazzucotelli). ### Code Refactoring - Rename some CLI options ([1323268](https://github.com/mkdocstrings/griffe/commit/13232685b0f2752d92428ab786d428d0af11743b) by Timothée Mazzucotelli). - Return the loader the to main function ([9c6317e](https://github.com/mkdocstrings/griffe/commit/9c6317e5afa25dd11d18906503b8010046878868) by Timothée Mazzucotelli). - Improve logging messages ([b8eb16e](https://github.com/mkdocstrings/griffe/commit/b8eb16e0fedfe50f2c3ad65e326f4dc6e6918ac0) by Timothée Mazzucotelli). - Skip inspection of some debug packages ([4ee8968](https://github.com/mkdocstrings/griffe/commit/4ee896864f1227e32d40571da03f7894c9404579) by Timothée Mazzucotelli). - Return ... instead of Ellipsis ([f9ae31d](https://github.com/mkdocstrings/griffe/commit/f9ae31d0f4c904a89c7f581aaa031692740edaef) by Timothée Mazzucotelli). - Catch attribute errors when cross-referencing docstring annotations ([288803a](https://github.com/mkdocstrings/griffe/commit/288803a3be93c4e077576ed36dded2a76ce33955) by Timothée Mazzucotelli). - Support dict methods in lines collection ([1b0cb94](https://github.com/mkdocstrings/griffe/commit/1b0cb945dba619df7ce1358f7961e4bd80f70218) by Timothée Mazzucotelli). ### Features - Compute and show some stats ([1b8d0a1](https://github.com/mkdocstrings/griffe/commit/1b8d0a1c91e03dfa5f92ad9c6dff02863a43fc01) by Timothée Mazzucotelli). - Add CLI options for alias resolution ([87a59cb](https://github.com/mkdocstrings/griffe/commit/87a59cb7af5f8e7df9ddba41fb4a4b65cb264481) by Timothée Mazzucotelli). - Support Google raises annotations cross-refs ([8006ae1](https://github.com/mkdocstrings/griffe/commit/8006ae13bc27d117ce6b8fdc8ac91dc8541a670f) by Timothée Mazzucotelli). ## [0.9.0](https://github.com/mkdocstrings/griffe/releases/tag/0.9.0) - 2022-01-04 <small>[Compare with 0.8.0](https://github.com/mkdocstrings/griffe/compare/0.8.0...0.9.0)</small> ### Features - Loader option to only follow aliases in known modules ([879d91b](https://github.com/mkdocstrings/griffe/commit/879d91b4c50832620ce6ee7bdcc85107a6df9a1f) by Timothée Mazzucotelli). - Use aliases when inspecting too ([60439ee](https://github.com/mkdocstrings/griffe/commit/60439eefb4635e58e4bd898e5565eab48a5c91d0) by Timothée Mazzucotelli). ### Bug Fixes - Handle more errors when loading modules ([1aa571a](https://github.com/mkdocstrings/griffe/commit/1aa571a112e3b2ca955c23f2eef97b36f34bcd8c) by Timothée Mazzucotelli). - Handle more errors when getting signature ([2db85e7](https://github.com/mkdocstrings/griffe/commit/2db85e7f655c1e383ba310f40195844c2867e1b9) by Timothée Mazzucotelli). - Fix checking parent truthfulness ([6129e50](https://github.com/mkdocstrings/griffe/commit/6129e50331f6e36bcbee2e07b871abee45f7e872) by Timothée Mazzucotelli). - Fix getting subscript value ([1699f12](https://github.com/mkdocstrings/griffe/commit/1699f121adc13fcc48f81f46dfca85946e2fb74f) by Timothée Mazzucotelli). - Support yield nodes ([7d536d5](https://github.com/mkdocstrings/griffe/commit/7d536d58ffc0faa4caf43f09194d88c35fc47704) by Timothée Mazzucotelli). - Exclude some special low-level members that cause cyclic issues ([b54ab34](https://github.com/mkdocstrings/griffe/commit/b54ab346308bb24cba66be9c8f1ee8599481381d) by Timothée Mazzucotelli). - Fix transforming elements of signatures to annotations ([e278c11](https://github.com/mkdocstrings/griffe/commit/e278c1102b2762b74bf6b83a2e97a5f87b566e2e) by Timothée Mazzucotelli). - Detect cyclic aliases and prevent resolution errors ([de5dd12](https://github.com/mkdocstrings/griffe/commit/de5dd12240acf8a203a86b04e458ce33b67ced9e) by Timothée Mazzucotelli). - Don't crash while trying to get the representation of an attribute value ([77ac55d](https://github.com/mkdocstrings/griffe/commit/77ac55d5033e83790c79f3303fdbd05ea66ab729) by Timothée Mazzucotelli). - Fix building value for joined strings ([6154b69](https://github.com/mkdocstrings/griffe/commit/6154b69b6da5d63c508ec5095aebe487e491b553) by Timothée Mazzucotelli). - Fix prevention of cycles while building objects nodes ([48062ac](https://github.com/mkdocstrings/griffe/commit/48062ac1f8356099b8e0e1069e4321a467073d33) by Timothée Mazzucotelli). - Better handle relative imports ([91b42de](https://github.com/mkdocstrings/griffe/commit/91b42dea73c035b2dc20db1e328a53960c51a645) by Timothée Mazzucotelli). - Fix Google parser missing lines ending with colon ([2f7969c](https://github.com/mkdocstrings/griffe/commit/2f7969ccbf91b63ae22deb742250068c114fe1a9) by Timothée Mazzucotelli). ### Code Refactoring - Improve alias resolution robustness ([e708139](https://github.com/mkdocstrings/griffe/commit/e708139c9bd19be320bdb279310560212872326f) by Timothée Mazzucotelli). - Remove async loader for now ([acc5ecf](https://github.com/mkdocstrings/griffe/commit/acc5ecf2bb45dcebdd56d763a657a1075c4a3002) by Timothée Mazzucotelli). - Improve handling of Google admonitions ([8aa5ed0](https://github.com/mkdocstrings/griffe/commit/8aa5ed0be4f1902dbdfbce9b4a9c7ac619418d43) by Timothée Mazzucotelli). - Better handling of import errors and system exits while inspecting modules ([7ba1589](https://github.com/mkdocstrings/griffe/commit/7ba1589552fb37fba3c2f3093058e135a6e48a27) by Timothée Mazzucotelli). - Empty generic visit/inspect methods in base classes ([338760e](https://github.com/mkdocstrings/griffe/commit/338760ea2189e74577250b8c3f4ffe91f81e6b6e) by Timothée Mazzucotelli). ## [0.8.0](https://github.com/mkdocstrings/griffe/releases/tag/0.8.0) - 2022-01-02 <small>[Compare with 0.7.1](https://github.com/mkdocstrings/griffe/compare/0.7.1...0.8.0)</small> ### Features - Support getting attribute annotation from parent in RST docstring parser ([25db61a](https://github.com/mkdocstrings/griffe/commit/25db61ab01042ad797ac5cdea0b2f7e2382191c1) by Timothée Mazzucotelli). - Handle relative imports ([62b0927](https://github.com/mkdocstrings/griffe/commit/62b0927516ca345de61aa3cc03e977d4d37220de) by Timothée Mazzucotelli). - Support wildcard imports ([77a3cb7](https://github.com/mkdocstrings/griffe/commit/77a3cb7e4198dc2e2cea953c5f621544b564552c) by Timothée Mazzucotelli). - Support configuring log level (CLI/env var) ([839d78e](https://github.com/mkdocstrings/griffe/commit/839d78ea302df004fba1b6fad9eb84d861f0f4aa) by Timothée Mazzucotelli). - Support loading `*.py[cod]` and `*.so` modules ([cd98a6f](https://github.com/mkdocstrings/griffe/commit/cd98a6f3afbbf8f6a176aa7780a8b916a9ee64f2) by Timothée Mazzucotelli). - Support inspecting builtin functions/methods ([aa1fce3](https://github.com/mkdocstrings/griffe/commit/aa1fce330ce3e2af4dd9a3c43827637d1e220dde) by Timothée Mazzucotelli). ### Code Refactoring - Handle extensions errors ([11278ca](https://github.com/mkdocstrings/griffe/commit/11278caea27e9f91a1dc9cc160414f01b24f5354) by Timothée Mazzucotelli). - Don't always try to find a module as a relative path ([e6df277](https://github.com/mkdocstrings/griffe/commit/e6df2774bfd631fd9a09913480b4d61d137bc0c6) by Timothée Mazzucotelli). - Improve loggers patching ([f4b262a](https://github.com/mkdocstrings/griffe/commit/f4b262ab5a3d874591324adc2b5ffff214c7e7da) by Timothée Mazzucotelli). - Improve dynamic imports ([2998195](https://github.com/mkdocstrings/griffe/commit/299819519b7eb9b07b938d22bfb3a27e3b05095d) by Timothée Mazzucotelli). ## [0.7.1](https://github.com/mkdocstrings/griffe/releases/tag/0.7.1) - 2021-12-28 <small>[Compare with 0.7.0](https://github.com/mkdocstrings/griffe/compare/0.7.0...0.7.1)</small> ### Code Refactoring - Only log warning if async mode is used ([356e848](https://github.com/mkdocstrings/griffe/commit/356e848c8e233334401461b02a0188731b71a8cf) by Timothée Mazzucotelli). ## [0.7.0](https://github.com/mkdocstrings/griffe/releases/tag/0.7.0) - 2021-12-28 <small>[Compare with 0.6.0](https://github.com/mkdocstrings/griffe/compare/0.6.0...0.7.0)</small> ### Features - Support more nodes on Python 3.7 ([7f2c4ec](https://github.com/mkdocstrings/griffe/commit/7f2c4ec3bf610ade7305e19ab220a4b447bed41d) by Timothée Mazzucotelli). ### Code Refactoring - Don't crash on syntax errors and log an error ([10bb6b1](https://github.com/mkdocstrings/griffe/commit/10bb6b15bb9b132626c525b81f3ee33c3bb5746f) by Timothée Mazzucotelli). ## [0.6.0](https://github.com/mkdocstrings/griffe/releases/tag/0.6.0) - 2021-12-27 <small>[Compare with 0.5.0](https://github.com/mkdocstrings/griffe/compare/0.5.0...0.6.0)</small> ### Features - Support more AST nodes ([cd1b305](https://github.com/mkdocstrings/griffe/commit/cd1b305932832ad5347ce829a48a311e3c44d542) by Timothée Mazzucotelli). ### Code Refactoring - Use annotation getter for base classes ([8b1a7ed](https://github.com/mkdocstrings/griffe/commit/8b1a7edc11a72f679689fa9ba9e632907f9304f8) by Timothée Mazzucotelli). ## [0.5.0](https://github.com/mkdocstrings/griffe/releases/tag/0.5.0) - 2021-12-20 <small>[Compare with 0.4.0](https://github.com/mkdocstrings/griffe/compare/0.4.0...0.5.0)</small> ### Features - Add support for Python 3.7 ([4535adc](https://github.com/mkdocstrings/griffe/commit/4535adce19edbe7e9cde90f3b1075a8245a6ebc8) by Timothée Mazzucotelli). ### Bug Fixes - Don't propagate aliases of an alias ([8af48f8](https://github.com/mkdocstrings/griffe/commit/8af48f87e2e6bb0f2cf1531fa10287a069f67289) by Timothée Mazzucotelli). - Don't reassign members defined in except clauses ([d918b4e](https://github.com/mkdocstrings/griffe/commit/d918b4efcedcedbec6db214ade8cde921d7e97b2) by Timothée Mazzucotelli). ## [0.4.0](https://github.com/mkdocstrings/griffe/releases/tag/0.4.0) - 2021-11-28 <small>[Compare with 0.3.0](https://github.com/mkdocstrings/griffe/compare/0.3.0...0.4.0)</small> ### Features - Add a prototype 'hybrid' extension ([8cb3c16](https://github.com/mkdocstrings/griffe/commit/8cb3c1661223378a2511fd42a0693d0fbfe924d8) by Timothée Mazzucotelli). - Allow passing extensions config as JSON on the CLI ([9a7fa8b](https://github.com/mkdocstrings/griffe/commit/9a7fa8bd88752ca1a074179db3a4c7fc41b68028) by Timothée Mazzucotelli). - Support names for returns, yields and receives sections items ([1c5a4c9](https://github.com/mkdocstrings/griffe/commit/1c5a4c95738615ea9bb6a816c61d078e6133100a) by Timothée Mazzucotelli). - Store aliases on each object ([91ba643](https://github.com/mkdocstrings/griffe/commit/91ba643b3e8e9a8f56f3280f699a18b1e654ccd7) by Timothée Mazzucotelli). - Support 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 <small>[Compare with 0.2.0](https://github.com/mkdocstrings/griffe/compare/0.2.0...0.3.0)</small> ### Features - Handle aliases and their resolution ([67ae903](https://github.com/mkdocstrings/griffe/commit/67ae9034ac25061bc7d5c6def63715209643ca20) by Timothée Mazzucotelli). - Resolve annotations in docstrings ([847384a](https://github.com/mkdocstrings/griffe/commit/847384a322017ca94bd40d4342eb4b8b42858f91) by Timothée Mazzucotelli). - Resolve annotations ([6451eff](https://github.com/mkdocstrings/griffe/commit/6451effa01aa09cd3db1584fe111152de649e525) by Timothée Mazzucotelli). - Add lines property to objects ([7daf7db](https://github.com/mkdocstrings/griffe/commit/7daf7db9ae58fb13985d1adacbde5d0bec2a35e4) by Timothée Mazzucotelli). - Allow setting docstring parser and options on each object ([07a1d2e](https://github.com/mkdocstrings/griffe/commit/07a1d2e83c12bfa0f7b0dd35149b5cc0d0f600d6) by Timothée Mazzucotelli). - Get attributes annotations from parent ([003b990](https://github.com/mkdocstrings/griffe/commit/003b99020f45b350d29329690d18f6c6cb3821f9) by Timothée Mazzucotelli). - Draft extensions loader ([17ccd03](https://github.com/mkdocstrings/griffe/commit/17ccd03cadc5cbb230071e78beab96a0b97456a1) by Timothée Mazzucotelli). - Add properties to objects ([0ec301a](https://github.com/mkdocstrings/griffe/commit/0ec301a5e97bee6556b62cb6ee35af9976f8410b) by Timothée Mazzucotelli). - Handle .pth files when searching modules ([2a2e182](https://github.com/mkdocstrings/griffe/commit/2a2e1826fe0235c5bd47b5d6b1b64a30a81a3f4b) by Timothée Mazzucotelli). - Add `default` property to docstring parameters ([6298ba3](https://github.com/mkdocstrings/griffe/commit/6298ba34d4e769568e519e21549137df3649e01b) by Timothée Mazzucotelli). - Accept RST and Numpy parsers ([1cf147d](https://github.com/mkdocstrings/griffe/commit/1cf147d8df0491104efd084ce3308da77fc2c817) by Timothée Mazzucotelli). - Support data (attributes/variables) ([dce84d1](https://github.com/mkdocstrings/griffe/commit/dce84d106cf067f11305f804a24cfd7d5643d902) by Timothée Mazzucotelli). - Add Numpy-style parser ([ad5b72d](https://github.com/mkdocstrings/griffe/commit/ad5b72d174433764e85f937ea1096c0f458532f8) by Timothée Mazzucotelli). - Support more section kinds in Google-style ([9d3d047](https://github.com/mkdocstrings/griffe/commit/9d3d0472d0bb55352b371de3da0816419fcf59e0) by Timothée Mazzucotelli). - Add docstring section kinds ([b270483](https://github.com/mkdocstrings/griffe/commit/b2704833bc74131269306b9947ea2b46edafd349) by Timothée Mazzucotelli). - Accept initial arguments when creating container ([90c5956](https://github.com/mkdocstrings/griffe/commit/90c59568bb6cdbf18efe182bd821973f2a133663) by Timothée Mazzucotelli). - Add an RST-style docstring parser ([742e7b2](https://github.com/mkdocstrings/griffe/commit/742e7b2e2101d0679571645584c5a6d3077a9764) by Timothée Mazzucotelli). ### Performance Improvements - Improve JSON encoder perfs ([6a78eb0](https://github.com/mkdocstrings/griffe/commit/6a78eb0b707a148356fb5bc69d9d0c2115239074) by Timothée Mazzucotelli). ### Bug Fixes - Handle serialization of Posix paths ([3a66b95](https://github.com/mkdocstrings/griffe/commit/3a66b95a4c91e6160d161acc457c66196adaa4fe) by Timothée Mazzucotelli). - Fix list annotation getter ([5ae800a](https://github.com/mkdocstrings/griffe/commit/5ae800a8902a28b5241192c0905b1914e2bfe906) by Timothée Mazzucotelli). - Show accurate line number in Google warnings ([2953590](https://github.com/mkdocstrings/griffe/commit/29535902d53b553906f59295104690c9417eb79f) by Timothée Mazzucotelli). - Fix assignment names getters ([6990846](https://github.com/mkdocstrings/griffe/commit/69908460b4fe47d1dc3d8d9f6b43d49dee5823aa) by Timothée Mazzucotelli). - Fix async loader (passing parent) ([57e866e](https://github.com/mkdocstrings/griffe/commit/57e866e4c48f4646142a26c6d2537f4da10e3a2c) by Timothée Mazzucotelli). - Fix exception name ([4b8b85d](https://github.com/mkdocstrings/griffe/commit/4b8b85dde72a552091534b3293399b844523786f) by Timothée Mazzucotelli). - Fix Google sections titles logic ([87dd329](https://github.com/mkdocstrings/griffe/commit/87dd32988a9164c47dadf96c0c74a0da8af16bd8) by Timothée Mazzucotelli). - Prepend current module to base classes (still needs resolution) ([a4b1dee](https://github.com/mkdocstrings/griffe/commit/a4b1deef4beb0e9e79adc920d80232f04ddfdc31) by Timothée Mazzucotelli). - Fix Google admonition regex ([3902e74](https://github.com/mkdocstrings/griffe/commit/3902e7497ef8b388c3d232a8116cb3bd27fdaad2) by Timothée Mazzucotelli). - Fix docstring getter ([1442eba](https://github.com/mkdocstrings/griffe/commit/1442eba93479f24a4d90cd9b25f57d304a65cd6c) by Timothée Mazzucotelli). - Fix getting arguments defaults in the Google-style parser ([67adbaf](https://github.com/mkdocstrings/griffe/commit/67adbafe04de1c8effc124b26565bef59adfb393) by Timothée Mazzucotelli). - Fix getting arguments annotations in the Google-style parser ([8bcbfba](https://github.com/mkdocstrings/griffe/commit/8bcbfbae861be4c3f9c2b8841c8bc86f39611168) by Timothée Mazzucotelli). ### Code Refactoring - Export parsers and main function in docstrings module ([96469da](https://github.com/mkdocstrings/griffe/commit/96469dab63a28c061e1d064528f8e07f394c2d81) by Timothée Mazzucotelli). - Remove top exports ([cd76694](https://github.com/mkdocstrings/griffe/commit/cd7669481a272d7c939b61f6ff2df1cb55eab39e) by Timothée Mazzucotelli). - Reorganize exceptions ([7f9b805](https://github.com/mkdocstrings/griffe/commit/7f9b8055aa069816b3b55fd02730e97e37a6bea4) by Timothée Mazzucotelli). - Avoid circular import ([ef27dcd](https://github.com/mkdocstrings/griffe/commit/ef27dcd6cc85590d1982ee14b7f520d379d658b8) by Timothée Mazzucotelli). - Rename index to [new] offset ([c07cc7d](https://github.com/mkdocstrings/griffe/commit/c07cc7d916d613545073e1159d86c65d58d98b37) by Timothée Mazzucotelli). - Reorganize code ([5f4fff2](https://github.com/mkdocstrings/griffe/commit/5f4fff21d1da7e1b33554cfb8017b23955999ad5) by Timothée Mazzucotelli). - Use keyword only parameters ([d34edd6](https://github.com/mkdocstrings/griffe/commit/d34edd629589796d53dbc29d77c5f7041acea5ab) by Timothée Mazzucotelli). - Default to no parsing for serialization ([8fecd9e](https://github.com/mkdocstrings/griffe/commit/8fecd9ef63f773220bb85379537c4ad25ea0e4fd) by Timothée Mazzucotelli). - Always extend AST ([c227ae6](https://github.com/mkdocstrings/griffe/commit/c227ae62ee5a3cc764f2c6fc9185400f0c9c48e7) by Timothée Mazzucotelli). - Set default for kwargs parameters ([7a0b85e](https://github.com/mkdocstrings/griffe/commit/7a0b85e5fd255db743c122e1a13916cdc3eb46ff) by Timothée Mazzucotelli). - Rename visitor method ([3e0c43c](https://github.com/mkdocstrings/griffe/commit/3e0c43cbed6cec563367f80e86f245b3ba89694c) by Timothée Mazzucotelli). - Improve typing ([ac86f17](https://github.com/mkdocstrings/griffe/commit/ac86f17bfbfc98d3c41f1830e4356fecc2ed76fc) by Timothée Mazzucotelli). - Fix typo ([a9ed6e9](https://github.com/mkdocstrings/griffe/commit/a9ed6e95992381df41554a895ed6304ca61048f7) by Timothée Mazzucotelli). - Rewrite ParameterKind ([90249df](https://github.com/mkdocstrings/griffe/commit/90249df0b478f147fc50a18dfb56ad96ad09e78c) by Timothée Mazzucotelli). - Add bool methods to docstrings and objects ([548f72e](https://github.com/mkdocstrings/griffe/commit/548f72ed5289aa531c125e4da6ff72a1ff34124d) by Timothée Mazzucotelli). - Allow setting docstring parser and options on each docstring ([752e084](https://github.com/mkdocstrings/griffe/commit/752e0843bc7388c9a2c7ce9ae2dce03ffa9243e3) by Timothée Mazzucotelli). - Skip attribute assignments ([e9cc2cd](https://github.com/mkdocstrings/griffe/commit/e9cc2cdd8cae1d15b98ffaa60e777b679ac55e23) by Timothée Mazzucotelli). - Improve visitor getters ([2ea88c0](https://github.com/mkdocstrings/griffe/commit/2ea88c020481e78060c90d8307a4f6a68047eaa2) by Timothée Mazzucotelli). - Use relative filepath in docstring warnings ([e894df7](https://github.com/mkdocstrings/griffe/commit/e894df767262623720a45c0b5c16fed544fae106) by Timothée Mazzucotelli). - Set submodules parent earlier ([53767c0](https://github.com/mkdocstrings/griffe/commit/53767c0c4ef90bfe405dcffd6087e365b98efafc) by Timothée Mazzucotelli). - Rename Data to Attribute ([febc12e](https://github.com/mkdocstrings/griffe/commit/febc12e5e33bbbdd448298f2cc277a45fd986204) by Timothée Mazzucotelli). - Rename arguments to parameters ([957856c](https://github.com/mkdocstrings/griffe/commit/957856cf22772584bcced30141afb8ca6a2ac378) by Timothée Mazzucotelli). - Improve annotation support ([5b2262f](https://github.com/mkdocstrings/griffe/commit/5b2262f9cacce4044716661e6de49a1773ea3aa8) by Timothée Mazzucotelli). - Always set parent ([cae85de](https://github.com/mkdocstrings/griffe/commit/cae85def4af1f67b537daabdb1e8ae9830dcaec7) by Timothée Mazzucotelli). - Factorize function handling ([dfece1c](https://github.com/mkdocstrings/griffe/commit/dfece1c0c73076c7d87d4df551f0994b4c2e3b69) by Timothée Mazzucotelli). - Privatize stuff, fix loggers ([5513ed5](https://github.com/mkdocstrings/griffe/commit/5513ed5345db185e7c08890ca08de17932b34f51) by Timothée Mazzucotelli). - Use keyword only arguments ([e853fe9](https://github.com/mkdocstrings/griffe/commit/e853fe9188fd2cd2ccc90e5fa1f52443bb00bab7) by Timothée Mazzucotelli). - Set default values for Argument arguments ([d5cccaa](https://github.com/mkdocstrings/griffe/commit/d5cccaa6ee73e14ca4456b974fba6d01d40bf848) by Timothée Mazzucotelli). - Swallow extra parsing options ([3d9ebe7](https://github.com/mkdocstrings/griffe/commit/3d9ebe775e1b936e89115d166144610b3a90290c) by Timothée Mazzucotelli). - Rename `start_index` argument to `offset` ([dd88358](https://github.com/mkdocstrings/griffe/commit/dd88358d8db78636ba5f39fcad92ff5192791852) by Timothée Mazzucotelli). - Reuse parsers warn function ([03dfdd3](https://github.com/mkdocstrings/griffe/commit/03dfdd38c5977ee83383f95acda1280b3f9ac86b) by Timothée Mazzucotelli). ## [0.2.0](https://github.com/mkdocstrings/griffe/releases/tag/0.2.0) - 2021-09-25 <small>[Compare with 0.1.0](https://github.com/mkdocstrings/griffe/compare/0.1.0...0.2.0)</small> ### Features - Add Google-style docstring parser ([cdefccc](https://github.com/mkdocstrings/griffe/commit/cdefcccff2cb8236003736545cffaf0bd6f46539) by Timothée Mazzucotelli). - Support all kinds of functions arguments ([c177562](https://github.com/mkdocstrings/griffe/commit/c177562c358f89da8c541b51d86f9470dd849c8f) by Timothée Mazzucotelli). - Initial support for class decorators and bases ([8e229aa](https://github.com/mkdocstrings/griffe/commit/8e229aa5f04d21bde108dca517166d291fd2147a) by Timothée Mazzucotelli). - Add functions decorators support ([fee304d](https://github.com/mkdocstrings/griffe/commit/fee304d44ce33286dedd6bb13a9b7200ea3d4dfa) by Timothée Mazzucotelli). - Add async loader ([3218bd0](https://github.com/mkdocstrings/griffe/commit/3218bd03fd754a04a4280c29319e6b8d55aac015) by Timothée Mazzucotelli). - Add relative file path and package properties ([d26ee1f](https://github.com/mkdocstrings/griffe/commit/d26ee1f3f09337af925c8071b4f24b8ae69b01d3) by Timothée Mazzucotelli). - Add search and output option to the CLI ([3b37692](https://github.com/mkdocstrings/griffe/commit/3b3769234aed87e100ef917fa2db550e650bff0d) by Timothée Mazzucotelli). - Load docstrings and functions arguments ([cdf29a3](https://github.com/mkdocstrings/griffe/commit/cdf29a3b12b4c04235dfeba1c8ef7461cc05248f) by Timothée Mazzucotelli). - Support paths in loader ([8f4df75](https://github.com/mkdocstrings/griffe/commit/8f4df7518ee5164e695e27fc9dcedae7a8b05133) by Timothée Mazzucotelli). ### Performance Improvements - Avoid name lookups in visitor ([00de148](https://github.com/mkdocstrings/griffe/commit/00de1482891e0c0091e79c14fdc318c6a95e4f6f) by Timothée Mazzucotelli). - Factorize and improve main and extensions visitors ([9b27b56](https://github.com/mkdocstrings/griffe/commit/9b27b56c0fc17d94144fd0b7e3783d3f6f572d3d) by Timothée Mazzucotelli). - Delegate children computation at runtime ([8d54c87](https://github.com/mkdocstrings/griffe/commit/8d54c8792f2a98c744374ae290bcb31fa81141b4) by Timothée Mazzucotelli). - Cache dataclasses properties ([2d7447d](https://github.com/mkdocstrings/griffe/commit/2d7447db05c2a3227e6cb66be46d374dac5fdf19) by Timothée Mazzucotelli). - Optimize node linker ([03f955e](https://github.com/mkdocstrings/griffe/commit/03f955ee698adffb7217528c03691876f299f8ca) by Timothée Mazzucotelli). - Optimize docstring getter ([4a05516](https://github.com/mkdocstrings/griffe/commit/4a05516de320473b5defd70f208b4e90763f2208) by Timothée Mazzucotelli). ## [0.1.0](https://github.com/mkdocstrings/griffe/releases/tag/0.1.0) - 2021-09-09 <small>[Compare with first commit](https://github.com/mkdocstrings/griffe/compare/7ea73adcc6aebcbe0eb64982916220773731a6b3...0.1.0)</small> ### Features - Add initial code ([8cbdf7a](https://github.com/mkdocstrings/griffe/commit/8cbdf7a49202dcf3cd617ae905c0f04cdfe053dd) by Timothée Mazzucotelli). - Generate project from copier-pdm template ([7ea73ad](https://github.com/mkdocstrings/griffe/commit/7ea73adcc6aebcbe0eb64982916220773731a6b3) by Timothée Mazzucotelli). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-griffe-0.40.0/.copier-answers.yml������������������������������������������������������������0000644�0001750�0001750�00000001462�14556223422�020006� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Changes here will be overwritten by Copier _commit: 1.2.0 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli author_username: pawamoy copyright_date: '2021' copyright_holder: Timothée Mazzucotelli copyright_holder_email: pawamoy@pm.me copyright_license: ISC License insiders: true 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.40.0/LICENSE������������������������������������������������������������������������0000644�0001750�0001750�00000001362�14556223422�015250� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������ISC 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.40.0/pyproject.toml�����������������������������������������������������������������0000644�0001750�0001750�00000005460�14556223422�017162� 0����������������������������������������������������������������������������������������������������ustar �carsten�������������������������carsten����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������[build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [project] name = "griffe" description = """Signatures for entire Python programs. \ Extract the structure, the frame, the skeleton of your project, \ to generate API documentation or find breaking changes in your API.""" authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] 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", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", "Topic :: Utilities", "Typing :: Typed", ] dependencies = [ "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.cli:main" [tool.pdm] version = {source = "scm"} plugins = [ "pdm-multirun", ] [tool.pdm.build] package-dir = "src" editable-backend = "editables" [tool.pdm.dev-dependencies] duty = ["duty>=0.10"] ci-quality = ["griffe[duty,docs,quality,typing,security]"] ci-tests = ["griffe[duty,tests]"] docs = [ "black>=23.9", "griffe-inherited-docstrings>=1.0", "markdown-callouts>=0.3", "markdown-exec>=1.7", "mkdocs>=1.5", "mkdocs-coverage>=1.0", "mkdocs-gen-files>=0.5", "mkdocs-git-committers-plugin-2>=1.2", "mkdocs-literate-nav>=0.6", "mkdocs-material>=9.4", "mkdocs-minify-plugin>=0.7", "mkdocstrings[python]>=0.23", "rich>=12.6", "tomli>=2.0; python_version < '3.11'", ] maintain = [ "black>=23.9", "blacken-docs>=1.16", "git-changelog>=2.3", ] quality = [ "ruff>=0.0", ] tests = [ "jsonschema>=4.17", "pysource-codegen>=0.4", "pysource-minimize>=0.5", "pytest>=7.4", "pytest-cov>=4.1", "pytest-randomly>=3.15", "pytest-xdist>=3.3", ] typing = [ "mypy>=1.5", "types-markdown>=3.5", "types-pyyaml>=6.0", ] security = [ "safety>=2.3", ] ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������