diff --git a/.copier-answers.yml b/.copier-answers.yml index a7f91ade..b2cee68e 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ -# Changes here will be overwritten by Copier -_commit: 1.2.3 +# Changes here will be overwritten by Copier. +_commit: 1.4.0 _src_path: gh:mkdocstrings/handler-template author_email: dev@pawamoy.fr author_fullname: Timothée Mazzucotelli @@ -7,7 +7,7 @@ author_username: pawamoy copyright_date: '2021' copyright_holder: Timothée Mazzucotelli copyright_holder_email: dev@pawamoy.fr -copyright_license: ISC License +copyright_license: ISC insiders: true insiders_email: insiders@pawamoy.fr insiders_repository_name: mkdocstrings-python diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a502284a..812789e6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,2 @@ github: pawamoy -ko_fi: pawamoy polar: pawamoy -custom: -- https://www.paypal.me/pawamoy diff --git a/.github/ISSUE_TEMPLATE/1-bug.md b/.github/ISSUE_TEMPLATE/1-bug.md index 0df6e967..a0e35e01 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -50,7 +50,7 @@ PASTE TRACEBACK HERE redacting sensitive information. --> ```bash -python -m mkdocstrings_handlers.python.debug # | xclip -selection clipboard +python -m mkdocstrings_handlers.python._internal.debug # | xclip -selection clipboard ``` PASTE MARKDOWN OUTPUT HERE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 594d1c42..32767fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml @@ -55,6 +55,12 @@ jobs: - name: Check for breaking changes in the API run: make check-api + - name: Store objects inventory for tests + uses: actions/upload-artifact@v4 + with: + name: objects.inv + path: site/objects.inv + exclude-test-jobs: runs-on: ubuntu-latest outputs: @@ -81,7 +87,9 @@ jobs: tests: - needs: exclude-test-jobs + needs: + - quality + - exclude-test-jobs strategy: max-parallel: 4 matrix: @@ -117,16 +125,22 @@ jobs: allow-prereleases: true - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml - cache-suffix: py${{ matrix.python-version }} + cache-suffix: ${{ matrix.resolution }} - name: Install dependencies env: UV_RESOLUTION: ${{ matrix.resolution }} run: make setup + - name: Download objects inventory + uses: actions/download-artifact@v4 + with: + name: objects.inv + path: site/ + - name: Run the test suite run: make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9388125b..3ef68f27 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: with: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 - name: Build dists if: github.repository_owner == 'pawamoy-insiders' run: uv tool run --from build pyproject-build diff --git a/CHANGELOG.md b/CHANGELOG.md index d31fb5ab..8798a05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,168 @@ 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). +## [1.16.10](https://github.com/mkdocstrings/python/releases/tag/1.16.10) - 2025-04-03 + +[Compare with 1.16.9](https://github.com/mkdocstrings/python/compare/1.16.9...1.16.10) + +### Bug Fixes + +- Fix inventory `base_url` being ignored ([8870eb9](https://github.com/mkdocstrings/python/commit/8870eb9af837666f59f96149c67c849e02f7ee25) by Stefan Mejlgaard). [Issue-268](https://github.com/mkdocstrings/python/issues/268), [PR-269](https://github.com/mkdocstrings/python/pull/269) + +## [1.16.9](https://github.com/mkdocstrings/python/releases/tag/1.16.9) - 2025-04-03 + +[Compare with 1.16.8](https://github.com/mkdocstrings/python/compare/1.16.8...1.16.9) + +### Bug Fixes + +- Use `toc_label` option in a few missing places ([337b46b](https://github.com/mkdocstrings/python/commit/337b46be912ff69e70b398bb252c8217c917db0a) by Timothée Mazzucotelli). [Issue-267](https://github.com/mkdocstrings/python/discussions/267) + +## [1.16.8](https://github.com/mkdocstrings/python/releases/tag/1.16.8) - 2025-03-24 + +[Compare with 1.16.7](https://github.com/mkdocstrings/python/compare/1.16.7...1.16.8) + +### Bug Fixes + +- Prevent infinite recursion by detecting parent-member cycles ([f3917e9](https://github.com/mkdocstrings/python/commit/f3917e9dd50ca7f94d0dd22b6e4e11885b4617e7) by Timothée Mazzucotelli). [Issue-griffe-368](https://github.com/mkdocstrings/griffe/issues/368) + +### Code Refactoring + +- Prepare feature for ordering by `__all__` value ([bfb5b30](https://github.com/mkdocstrings/python/commit/bfb5b303f4ea2187c15bccc688f7eba25e7edfcc) by Timothée Mazzucotelli). [Issue-219](https://github.com/mkdocstrings/python/issues/219) +- Sort objects without line numbers last instead of first ([681afb1](https://github.com/mkdocstrings/python/commit/681afb146225d98350a8eb2178aab07aec95fe6b) by Timothée Mazzucotelli). + +## [1.16.7](https://github.com/mkdocstrings/python/releases/tag/1.16.7) - 2025-03-20 + +[Compare with 1.16.6](https://github.com/mkdocstrings/python/compare/1.16.6...1.16.7) + +### Code Refactoring + +- Prepare `public` filtering method feature ([fde2019](https://github.com/mkdocstrings/python/commit/fde20191cab20f39d9e5e729a95cdfa3390b8f1f) by Timothée Mazzucotelli). [Issue-78](https://github.com/mkdocstrings/python/issues/78) + +## [1.16.6](https://github.com/mkdocstrings/python/releases/tag/1.16.6) - 2025-03-18 + +[Compare with 1.16.5](https://github.com/mkdocstrings/python/compare/1.16.5...1.16.6) + +### Deprecations + +Importing from submodules is now deprecated: the public API is fully exposed under the top-level `mkdocstrings_handler.python` module. + +### Bug Fixes + +- Add back default compiled filters (regression) ([2d83900](https://github.com/mkdocstrings/python/commit/2d83900c9e258399c90ecbac350ad03ff5d8f311) by Timothée Mazzucotelli). [Issue-264](https://github.com/mkdocstrings/python/issues/264) + +### Code Refactoring + +- Start logging warnings instead of info messages about deprecated use of templates ([7606f33](https://github.com/mkdocstrings/python/commit/7606f33559ced6962ecf9a1bc9aa76f24d87f515) by Timothée Mazzucotelli). +- Move modules into internal folder, expose API in top-level module ([93a68d0](https://github.com/mkdocstrings/python/commit/93a68d0d7afce38c78a8264189cfa812d737666c) by Timothée Mazzucotelli). + +## [1.16.5](https://github.com/mkdocstrings/python/releases/tag/1.16.5) - 2025-03-10 + +[Compare with 1.16.4](https://github.com/mkdocstrings/python/compare/1.16.4...1.16.5) + +### Code Refactoring + +- Prepare backlinks support ([56bf627](https://github.com/mkdocstrings/python/commit/56bf627b9483a12228b769ae4690b84733061ea5) by Timothée Mazzucotelli). [Issue-153](https://github.com/mkdocstrings/python/issues/153), [PR-252](https://github.com/mkdocstrings/python/pull/252) + +## [1.16.4](https://github.com/mkdocstrings/python/releases/tag/1.16.4) - 2025-03-10 + +[Compare with 1.16.3](https://github.com/mkdocstrings/python/compare/1.16.3...1.16.4) + +### Bug Fixes + +- Fix de-duplication of summary sections ([dc46ac9](https://github.com/mkdocstrings/python/commit/dc46ac9b4cfc642decd153dceb62e9f45c5c750e) by Timothée Mazzucotelli). + +## [1.16.3](https://github.com/mkdocstrings/python/releases/tag/1.16.3) - 2025-03-08 + +[Compare with 1.16.2](https://github.com/mkdocstrings/python/compare/1.16.2...1.16.3) + +### Build + +- Depend on mkdocstrings 0.28.3 ([9fa4f16](https://github.com/mkdocstrings/python/commit/9fa4f1636af240bb695661b7172f052cb11e0ec9) by Timothée Mazzucotelli). + +### Bug Fixes + +- De-duplicate summary sections ([a657d07](https://github.com/mkdocstrings/python/commit/a657d07499eb82d22337c169aa86b1cdd85543fa) by Timothée Mazzucotelli). [Issue-134](https://github.com/mkdocstrings/python/issues/134) + +### Code Refactoring + +- Import from top-level `mkdocstrings` module ([da2ba13](https://github.com/mkdocstrings/python/commit/da2ba13b1367ce107416d08f382fb9f2384c015c) by Timothée Mazzucotelli). + +## [1.16.2](https://github.com/mkdocstrings/python/releases/tag/1.16.2) - 2025-02-24 + +[Compare with 1.16.1](https://github.com/mkdocstrings/python/compare/1.16.1...1.16.2) + +### Build + +- Depend on mkdocs-autorefs >= 1.4 and mkdocstrings >= 0.28.2 ([ea1ab49](https://github.com/mkdocstrings/python/commit/ea1ab498be836c94eb695ace05c41357b12f2c95) by Timothée Mazzucotelli). + +## [1.16.1](https://github.com/mkdocstrings/python/releases/tag/1.16.1) - 2025-02-18 + +[Compare with 1.16.0](https://github.com/mkdocstrings/python/compare/1.16.0...1.16.1) + +### Bug Fixes + +- Give precedence to user-provided paths when they are already listed in `sys.path` ([0f497d1](https://github.com/mkdocstrings/python/commit/0f497d185ba1860c61555803bfc4b311a410bd39) by Timothée Mazzucotelli). [Issue-248](https://github.com/mkdocstrings/python/discussions/248) + +## [1.16.0](https://github.com/mkdocstrings/python/releases/tag/1.16.0) - 2025-02-17 + +[Compare with 1.15.1](https://github.com/mkdocstrings/python/compare/1.15.1...1.16.0) + +### Features + +- Add option to show/hide overloads ([4a5ee10](https://github.com/mkdocstrings/python/commit/4a5ee10c65de28b7921a56ef2c222d2f3417edaa) by Pete Stenger). [PR-250](https://github.com/mkdocstrings/python/pull/250) + +## [1.15.1](https://github.com/mkdocstrings/python/releases/tag/1.15.1) - 2025-02-17 + +[Compare with 1.15.0](https://github.com/mkdocstrings/python/compare/1.15.0...1.15.1) + +### Bug Fixes + +- Unwrap `Annotated` regardless of `signature_crossrefs` ([d809f1a](https://github.com/mkdocstrings/python/commit/d809f1a9e6a6f4eaf6fe4a18c2ec0e69e5716a12) by Timothée Mazzucotelli). [Issue-249](https://github.com/mkdocstrings/python/issues/249) + +## [1.15.0](https://github.com/mkdocstrings/python/releases/tag/1.15.0) - 2025-02-11 + +[Compare with 1.14.6](https://github.com/mkdocstrings/python/compare/1.14.6...1.15.0) + +### Features + +- Support cross-referencing constructor parameters in instance attribute values ([f07bf58](https://github.com/mkdocstrings/python/commit/f07bf58a7358dea106032c7da27098e7617eefa0) by Timothée Mazzucotelli). + +## [1.14.6](https://github.com/mkdocstrings/python/releases/tag/1.14.6) - 2025-02-07 + +[Compare with 1.14.5](https://github.com/mkdocstrings/python/compare/1.14.5...1.14.6) + +### Bug Fixes + +- Catch alias resolution errors when getting aliases for an identifier ([0aaa260](https://github.com/mkdocstrings/python/commit/0aaa260139afe2e3ab85d62224c90a389df64978) by Timothée Mazzucotelli). [Issue-358](https://github.com/mkdocstrings/griffe/discussions/358) + +### Code Refactoring + +- Improve translations for Simplified Chinese and Japanese ([753a0df](https://github.com/mkdocstrings/python/commit/753a0df8f91f1cf42fb7e56b7fdd312b2bd652ab) by Zhikang Yan). [PR-244](https://github.com/mkdocstrings/python/pull/244) + +## [1.14.5](https://github.com/mkdocstrings/python/releases/tag/1.14.5) - 2025-02-05 + +[Compare with 1.14.4](https://github.com/mkdocstrings/python/compare/1.14.4...1.14.5) + +### Bug Fixes + +- Remove type from property docstring summary in summary sections ([15f2cd4](https://github.com/mkdocstrings/python/commit/15f2cd48b79a1f062086a47ea0c6bc52d89786d8) by Uchechukwu Orji). [PR-242](https://github.com/mkdocstrings/python/pull/242) + +## [1.14.4](https://github.com/mkdocstrings/python/releases/tag/1.14.4) - 2025-02-04 + +[Compare with 1.14.3](https://github.com/mkdocstrings/python/compare/1.14.3...1.14.4) + +### Bug Fixes + +- Deactivate Pydantic validation on Python 3.9 is `eval-type-backport` is not available (for modern typing syntax support) ([0de0e5e](https://github.com/mkdocstrings/python/commit/0de0e5e57f8f22e039b0d19aad6341ce7ab3da9f) by Timothée Mazzucotelli). [Issue-241](https://github.com/mkdocstrings/python/issues/241) + +## [1.14.3](https://github.com/mkdocstrings/python/releases/tag/1.14.3) - 2025-02-04 + +[Compare with 1.14.2](https://github.com/mkdocstrings/python/compare/1.14.2...1.14.3) + +### Bug Fixes + +- Let dataclass implement `__init__` method, set extra fields in `get_options` ([477b9e4](https://github.com/mkdocstrings/python/commit/477b9e447ef9717c6edcb14bd4c53f9cacc555b8) by Timothée Mazzucotelli). + ## [1.14.2](https://github.com/mkdocstrings/python/releases/tag/1.14.2) - 2025-02-03 [Compare with 1.14.1](https://github.com/mkdocstrings/python/compare/1.14.1...1.14.2) @@ -481,7 +643,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Breaking changes -- The signature of the [`format_signature` filter](https://mkdocstrings.github.io/python/reference/mkdocstrings_handlers/python/rendering/#mkdocstrings_handlers.python.rendering.do_format_signature) has changed. +- The signature of the [`format_signature` filter][mkdocstrings_handlers.python.do_format_signature] has changed. If you override templates in your project to customize the output, make sure to update the following templates so that they use the new filter signature: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 255e0eed..2d46305a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,128 +2,79 @@ ## 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 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. +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: +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 +* 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 +* 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 +* 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 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. +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. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -dev@pawamoy.fr. -All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dev@pawamoy.fr. All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. +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: +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. +**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. +**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. +**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. +**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. +**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. +**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. +**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. +**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]. +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]. +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]. +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e3dc294..920ae3a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Contributing -Contributions are welcome, and they are greatly appreciated! -Every little bit helps, and credit will always be given. +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Environment setup @@ -14,11 +13,7 @@ cd python make setup ``` -> NOTE: -> If it fails for some reason, -> you'll need to install -> [uv](https://github.com/astral-sh/uv) -> manually. +> NOTE: If it fails for some reason, you'll need to install [uv](https://github.com/astral-sh/uv) manually. > > You can install it with: > @@ -26,8 +21,7 @@ make setup > curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > -> Now you can try running `make setup` again, -> or simply `uv sync`. +> Now you can try running `make setup` again, or simply `uv sync`. You now have the dependencies installed. @@ -35,15 +29,10 @@ Run `make help` to see all the available actions! ## Tasks -The entry-point to run commands and tasks is the `make` Python script, -located in the `scripts` directory. Try running `make` to show the available commands and tasks. -The *commands* do not need the Python dependencies to be installed, -while the *tasks* do. -The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). +The entry-point to run commands and tasks is the `make` Python script, located in the `scripts` directory. Try running `make` to show the available commands and tasks. The *commands* do not need the Python dependencies to be installed, +while the *tasks* do. The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). -If you work in VSCode, we provide -[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) -for the project. +If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. ## Development @@ -62,17 +51,13 @@ As usual: 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. +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): +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): ``` [(scope)]: Subject @@ -80,10 +65,7 @@ or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg. [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. +**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: @@ -99,9 +81,7 @@ Scope and body are optional. Type can be: - `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: +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. @@ -110,16 +90,9 @@ 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). +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. +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 @@ -144,5 +117,4 @@ And force-push: 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. +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. diff --git a/config/ruff.toml b/config/ruff.toml index 4c91b364..04af7c19 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -47,17 +47,24 @@ ignore = [ ] [lint.per-file-ignores] -"src/*/cli.py" = [ +"src/**/cli.py" = [ "T201", # Print statement ] "src/*/debug.py" = [ "T201", # Print statement ] +"!src/*/*.py" = [ + "D100", # Missing docstring in public module +] +"!src/**.py" = [ + "D101", # Missing docstring in public class + "D103", # Missing docstring in public function +] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement ] -"tests/*.py" = [ +"tests/**.py" = [ "ARG005", # Unused lambda argument "FBT001", # Boolean positional arg in function definition "PLR2004", # Magic value used in comparison diff --git a/config/vscode/launch.json b/config/vscode/launch.json index e3288388..9d632bf0 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -7,7 +7,17 @@ "request": "launch", "program": "${file}", "console": "integratedTerminal", - "justMyCode": false + "justMyCode": false, + "args": "${command:pickArgs}" + }, + { + "name": "run", + "type": "debugpy", + "request": "launch", + "module": "python", + "console": "integratedTerminal", + "justMyCode": false, + "args": "${command:pickArgs}" }, { "name": "docs", diff --git a/docs/.overrides/partials/path-item.html b/docs/.overrides/partials/path-item.html new file mode 100644 index 00000000..a9c95446 --- /dev/null +++ b/docs/.overrides/partials/path-item.html @@ -0,0 +1,22 @@ +{# Fix breadcrumbs for when mkdocs-section-index is used. #} +{# See https://github.com/squidfunk/mkdocs-material/issues/7614. #} + + +{% macro render_content(nav_item) %} + + {{ nav_item.title }} + +{% endmacro %} + + +{% macro render(nav_item, ref=nav_item) %} + {% if nav_item.is_page %} +
  • + + {{ render_content(ref) }} + +
  • + {% elif nav_item.children %} + {{ render(nav_item.children | first, ref) }} + {% endif %} +{% endmacro %} diff --git a/docs/changelog.md b/docs/changelog.md index 786b75d5..0536cbbe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1 +1,5 @@ +--- +title: Changelog +--- + --8<-- "CHANGELOG.md" diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md index 01f2ea20..002b2a04 100644 --- a/docs/code_of_conduct.md +++ b/docs/code_of_conduct.md @@ -1 +1,5 @@ +--- +title: Code of Conduct +--- + --8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md index ea38c9bf..61935e5d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1 +1,5 @@ +--- +title: Contributing +--- + --8<-- "CONTRIBUTING.md" diff --git a/docs/credits.md b/docs/credits.md index f758db87..f6ab1aa2 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -1,10 +1,9 @@ --- +title: Credits hide: - toc --- - ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` - diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 03c39d33..7d66153a 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -25,3 +25,48 @@ a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } + +/* Tree-like output for backlinks. */ +.doc-backlink-list { + --tree-clr: var(--md-default-fg-color); + --tree-font-size: 1rem; + --tree-item-height: 1; + --tree-offset: 1rem; + --tree-thickness: 1px; + --tree-style: solid; + display: grid; + list-style: none !important; +} + +.doc-backlink-list li > span:first-child { + text-indent: .3rem; +} +.doc-backlink-list li { + padding-inline-start: var(--tree-offset); + border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); + position: relative; + margin-left: 0 !important; + + &:last-child { + border-color: transparent; + } + &::before{ + content: ''; + position: absolute; + top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness)); + left: calc(var(--tree-thickness) * -1); + width: calc(var(--tree-offset) + var(--tree-thickness) * 2); + height: calc(var(--tree-item-height) * var(--tree-font-size)); + border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); + border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr); + } + &::after{ + content: ''; + position: absolute; + border-radius: 50%; + background-color: var(--tree-clr); + top: calc(var(--tree-item-height) / 2 * 1rem); + left: var(--tree-offset) ; + translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1); + } +} diff --git a/docs/index.md b/docs/index.md index 8e6f2fb4..82377e21 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ --- +title: Overview hide: - feedback --- diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md index a6c7b907..b5717892 100644 --- a/docs/insiders/changelog.md +++ b/docs/insiders/changelog.md @@ -2,6 +2,18 @@ ## mkdocstrings-python Insiders +### 1.12.0 March 22, 2025 { id="1.12.0" } + +- [Ordering method: `__all__`][option-members_order] + +### 1.11.0 March 20, 2025 { id="1.11.0" } + +- [Filtering method: `public`][option-filters-public] + +### 1.10.0 March 10, 2025 { id="1.10.0" } + +- [Backlinks][backlinks] + ### 1.9.0 September 03, 2024 { id="1.9.0" } - [Relative cross-references][relative_crossrefs] @@ -77,6 +89,6 @@ - Add [cross-references for type annotations in signatures](../usage/configuration/signatures.md#signature_crossrefs). Make sure to update your local templates as the signature of the - [`format_signature` filter][mkdocstrings_handlers.python.rendering.do_format_signature] + [`format_signature` filter][mkdocstrings_handlers.python.do_format_signature] has changed. The templates that must be updated: `class.html`, `expression.html`, `function.html` and `signature.html`. diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml index 16d1d507..71128361 100644 --- a/docs/insiders/goals.yml +++ b/docs/insiders/goals.yml @@ -42,3 +42,12 @@ goals: - name: Scoped cross-references ref: /usage/configuration/docstrings/#scoped_crossrefs since: 2024/09/03 + - name: Backlinks + ref: /usage/configuration/general/#backlinks + since: 2025/03/10 + - name: "Filtering method: `public`" + ref: /usage/configuration/members/#option-filters-public + since: 2025/03/20 + - name: "Ordering method: `__all__`" + ref: /usage/configuration/members/#option-members_order + since: 2025/03/22 \ No newline at end of file diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 288075b1..f184961f 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -1,56 +1,30 @@ +--- +title: Insiders +--- + # Insiders -*mkdocstrings-python* 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]! +*mkdocstrings-python* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][]. Read on to learn [what sponsorships achieve][sponsorship], [how to become a sponsor][sponsors] to get access to Insiders, and [what's in it for you][features]! ## What is Insiders? -*mkdocstrings-python Insiders* is a private fork of *mkdocstrings-python*, 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. +*mkdocstrings-python Insiders* is a private fork of *mkdocstrings-python*, hosted as a private GitHub repository. Almost[^1] [all new features][features] are developed as part of this fork, which means that they are immediately available to all eligible sponsors, as they are granted access to this private repository. - [^1]: - In general, every new feature is first exclusively released to sponsors, but - sometimes upstream dependencies enhance - existing features that must be supported by *mkdocstrings-python*. +[^1]: In general, every new feature is first exclusively released to sponsors, but sometimes upstream dependencies enhance existing features that must be supported by *mkdocstrings-python*. -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 -*mkdocstrings-python* and released for general availability, making them available -to all users. Bugfixes are always released in tandem. +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 *mkdocstrings-python* 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. +[^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] +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 *mkdocstrings-python*, - you can be sure that bugs are fixed quickly and new features are added - regularly. +[^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 *mkdocstrings-python*, 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]! +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? @@ -92,50 +66,23 @@ else: ``` -Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management](https://pawamoy.github.io/backlog/). +Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management][backlog]. ## How to become a sponsor -Thanks for your interest in sponsoring! In order to become an eligible sponsor -with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], -and complete a sponsorship of **$10 a month or more**. -You can use your individual or organization GitHub account for sponsoring. +Thanks for your interest in sponsoring! In order to become an eligible sponsor with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. + +Sponsorships lower than $10 a month are also very much appreciated, and useful. They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. Every sponsorship helps us implementing new features and releasing them to the public. -Sponsorships lower than $10 a month are also very much appreciated, and useful. -They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. -*Every* sponsorship helps us implementing new features and releasing them to the public. +**Important:** By default, when you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, all the publicly visible members of the organization will be invited to join our private repositories. If you wish to only grant access to a subset of users, please send a short email to insiders@pawamoy.fr with the name of your organization and the GitHub accounts of the users that should be granted access. -**Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** -through a GitHub organization, please send a short email -to insiders@pawamoy.fr with the name of your -organization and the GitHub account of the individual -that should be added as a collaborator.[^4] +**Tip:** to ensure that access is not tied to a particular individual GitHub account, you can create a bot account (i.e. a GitHub account that is not tied to a specific individual), and use this account for the sponsoring. After being granted access to our private repositories, the bot account can create private forks of our private repositories into your own organization, which all members of your organization will have access to. You can cancel your sponsorship anytime.[^5] - [^4]: - It's currently not possible to grant access to each member of an - organization, as GitHub only allows for adding users. Thus, after - sponsoring, please send an email to insiders@pawamoy.fr, stating which - account should become a collaborator of the Insiders repository. We're - working on a solution which will make access to organizations much simpler. - To ensure that access is not tied to a particular individual GitHub account, - create a bot account (i.e. a GitHub account that is not tied to a specific - individual), and use this account for the sponsoring. After being added to - the list of collaborators, the bot account can create a private fork of the - private Insiders GitHub repository, and grant access to all members of the - organizations. - - [^5]: - If you cancel your sponsorship, GitHub schedules a cancellation request - which will become effective at the end of the billing cycle. This means - that even though you cancel your sponsorship, you will keep your access to - Insiders as long as your cancellation isn't effective. All charges are - processed by GitHub through Stripe. As we don't receive any information - regarding your payment, and GitHub doesn't offer refunds, sponsorships are - non-refundable. - -[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary } +[^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][github sponsor profile]{ .md-button .md-button--primary }
    @@ -148,23 +95,14 @@ You can cancel your sponsorship anytime.[^5]
    - If you sponsor publicly, you're automatically added here with a link to - your profile and avatar to show your support for *mkdocstrings-python*. - 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. + If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *mkdocstrings-python*. 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. +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(): @@ -174,9 +112,7 @@ for goal in goals.values(): ### 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. +This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): @@ -188,47 +124,28 @@ for goal in goals.values(): ### Compatibility -> We're building an open source project and want to allow outside collaborators -to use *mkdocstrings-python* locally without having access to Insiders. -Is this still possible? +> We're building an open source project and want to allow outside collaborators to use *mkdocstrings-python* locally without having access to Insiders. Is this still possible? -Yes. Insiders is compatible with *mkdocstrings-python*. 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. +Yes. Insiders is compatible with *mkdocstrings-python*. 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). +Yes. You can sponsor on a yearly basis by [switching your GitHub account to a yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). If you have any problems or further questions, please reach out to insiders@pawamoy.fr. ### Terms -> Are we allowed to use Insiders under the same terms and conditions as -*mkdocstrings-python*? - -Yes. Whether you're an individual or a company, you may use *mkdocstrings-python -Insiders* precisely under the same terms as *mkdocstrings-python*, which are given -by the [ISC License][license]. However, we kindly ask you to respect our -**fair use policy**: +> Are we allowed to use Insiders under the same terms and conditions as *mkdocstrings-python*? -- 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. +Yes. Whether you're an individual or a company, you may use *mkdocstrings-python Insiders* precisely under the same terms as *mkdocstrings-python*, which are given by the [ISC license][license]. However, we kindly ask you to respect our **fair use policy**: -- 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]. +- Please **don't distribute the source code** of Insiders. You may freely use it for public, private or commercial projects, privately fork or mirror it, but please don't make the source code public, as it would counteract the sponsorware strategy. +- If you cancel your subscription, your access to the private repository is revoked, and you will miss out on all future updates of Insiders. However, you may **use the latest version** that's available to you **as long as you like**. Just remember that [GitHub deletes private forks][private forks]. +[backlog]: https://pawamoy.github.io/backlog/ [insiders]: #what-is-insiders [sponsorship]: #what-sponsorships-achieve [sponsors]: #how-to-become-a-sponsor diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md index 3588e691..3e20e5d7 100644 --- a/docs/insiders/installation.md +++ b/docs/insiders/installation.md @@ -4,62 +4,38 @@ title: Getting started with Insiders # Getting started with Insiders -*mkdocstrings-python Insiders* is a compatible drop-in replacement for *mkdocstrings-python*, -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 +*mkdocstrings-python Insiders* is a compatible drop-in replacement for *mkdocstrings-python*, and can be installed similarly using `pip` or `git`. Note that in order to access the Insiders repository, you need to [become an eligible sponsor][] of @pawamoy on GitHub. ## Installation -### with 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.). +### with the `insiders` tool -See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) -and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). +[`insiders`][insiders-tool] is a tool that helps you keep up-to-date versions of Insiders projects in the PyPI index of your choice (self-hosted, Google registry, Artifactory, etc.). -**We kindly ask that you do not upload the distributions to public registries, -as it is against our [Terms of use](index.md#terms).** +**We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use][].** ### with pip (ssh/https) -*mkdocstrings-python Insiders* can be installed with `pip` [using SSH][using ssh]: +*mkdocstrings-python Insiders* can be installed with `pip` [using SSH][install-pip-ssh]: ```bash pip install git+ssh://git@github.com/pawamoy-insiders/mkdocstrings-python.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/mkdocstrings-python.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: +>? NOTE: **How to get a GitHub personal access token?** The `GH_TOKEN` environment variable is a GitHub token. It can be obtained by creating a [personal access token][github-pat] for your GitHub account. It will give you access to the Insiders repository, programmatically, from the command line or GitHub Actions workflows: > > 1. Go to https://github.com/settings/tokens -> 2. Click on [Generate a new token] +> 2. Click on [Generate a new token][github-pat-new] > 3. Enter a name and select the [`repo`][scopes] scope > 4. Generate the token and store it in a safe place > -> [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. +> Note that the personal access token must be kept secret at all times, as it allows the owner to access your private repositories. ### with Git @@ -77,12 +53,15 @@ pip install -e mkdocstrings-python ## Upgrading -When upgrading Insiders, you should always check the version of *mkdocstrings-python* -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`. +When upgrading Insiders, you should always check the version of *mkdocstrings-python* 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. +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 +[become an eligible sponsor]: ./index.md#how-to-become-a-sponsor +[changelog]: ./changelog.md +[github-pat]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token +[github-pat-new]: https://github.com/settings/tokens/new +[insiders-tool]: https://pawamoy.github.io/insiders-project/ +[install-pip-ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh +[scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes +[terms of use]: ./index.md#terms diff --git a/docs/license.md b/docs/license.md index e81c0edf..5b25a00f 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,4 +1,5 @@ --- +title: License hide: - feedback --- diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 00000000..587e99db --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,9 @@ +--- +title: API reference +hide: +- navigation +--- + +# ::: mkdocstrings_handlers.python + options: + show_submodules: true diff --git a/docs/usage/configuration/general.md b/docs/usage/configuration/general.md index 3983a616..973658c1 100644 --- a/docs/usage/configuration/general.md +++ b/docs/usage/configuration/general.md @@ -60,6 +60,48 @@ plugins: //// /// +[](){#option-backlinks} +## `backlinks` + +[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — +[:octicons-tag-24: Insiders 1.10.0](../../insiders/changelog.md#1.10.0) + +- **:octicons-package-24: Type Literal["flat", "tree", False] :material-equal: `False`{ title="default value" }** + +The `backlinks` option enables rendering of backlinks within your API documentation. + +When an arbitrary section of your documentation links to an API symbol, this link will be collected as a backlink, and rendered below your API symbol. In short, the API symbol will link back to the section that links to it. Such backlinks will help your users navigate the documentation, as they will immediately which functions return a specific symbol, or where a specific symbol is accepted as parameter, etc.. + +Each backlink is a list of breadcrumbs that represent the navigation, from the root page down to the given section. + +The available styles for rendering backlinks are **`flat`** and **`tree`**. + +- **`flat`** will render backlinks as a single-layer list. This can lead to repetition of breadcrumbs. +- **`tree`** will combine backlinks into a tree, to remove repetition of breadcrumbs. + +WARNING: **Global-only option.** For now, the option only works when set globally in `mkdocs.yml`. + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + backlinks: tree +``` + +/// admonition | Preview + type: preview + +//// tab | Flat +![backlinks-flat](https://github.com/user-attachments/assets/f7a15b01-f194-4c55-8281-50adbb4a74af) +//// + +//// tab | Tree +![backlinks-tree](https://github.com/user-attachments/assets/3457db21-45e1-4e03-bd8c-2e9e75dc778b) +//// +/// + [](){#option-extensions} ## `extensions` diff --git a/docs/usage/configuration/members.md b/docs/usage/configuration/members.md index 363f7e0a..7a5069a1 100644 --- a/docs/usage/configuration/members.md +++ b/docs/usage/configuration/members.md @@ -264,13 +264,14 @@ class Main(Base): [](){#option-members_order} ## `members_order` -- **:octicons-package-24: Type [`str`][] :material-equal: `"alphabetical"`{ title="default value" }** +- **:octicons-package-24: Type `str | list[str]` :material-equal: `"alphabetical"`{ title="default value" }** The members ordering to use. Possible values: -- `alphabetical`: order by the members names. -- `source`: order members as they appear in the source file. +- `__all__` ([:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — [:octicons-tag-24: Insiders 1.12.0](../../insiders/changelog.md#1.12.0)): Order according to `__all__` attributes. Since classes do not define `__all__` attributes, you can specify a second ordering method by using a list. +- `alphabetical`: Order by the members names. +- `source`: Order members as they appear in the source file. The order applies for all members, recursively. The order will be ignored for members that are explicitely sorted using the [`members`][] option. @@ -292,6 +293,12 @@ plugins: members_order: source ``` +```md title="or in docs/some_page.md (local configuration)" +::: package.module + options: + members_order: [__all__, source] +``` + ```python title="package/module.py" """Module docstring.""" @@ -335,10 +342,21 @@ def function_c(): [](){#option-filters} ## `filters` -- **:octicons-package-24: Type list[str] | None :material-equal: `["!^_[^_]"]`{ title="default value" }** +- **:octicons-package-24: Type list[str] | Literal["public"] | None :material-equal: `["!^_[^_]"]`{ title="default value" }** -A list of filters applied to filter objects based on their name. +A list of filters, or `"public"`. + +**Filtering methods** + +[](){#option-filters-public} + +[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } — +[:octicons-tag-24: Insiders 1.11.0](../../insiders/changelog.md#1.11.0) + +The `public` filtering method will include only public objects: those added to the `__all__` attribute of modules, or not starting with a single underscore. Special methods and attributes ("dunder" methods/attributes, starting and ending with two underscores), like `__init__`, `__call__`, `__mult__`, etc., are always considered public. + +**List of filters** Filters are regular expressions. These regular expressions are evaluated by Python and so must match the syntax supported by the [`re`][] module. @@ -379,13 +397,13 @@ plugins: python: options: filters: - - "!^_" + - "!^_[^_]" ``` ```md title="or in docs/some_page.md (local configuration)" ::: package.module options: - filters: [] + filters: public ``` ```python title="package/module.py" diff --git a/docs/usage/configuration/signatures.md b/docs/usage/configuration/signatures.md index c97cb5a6..98c865e5 100644 --- a/docs/usage/configuration/signatures.md +++ b/docs/usage/configuration/signatures.md @@ -433,6 +433,55 @@ function(param1, param2=None) //// /// +[](){#option-show_overloads} +## `show_overloads` + +Whether to render function / method overloads. + +```yaml title="in mkdocs.yml (global configuration)" +plugins: +- mkdocstrings: + handlers: + python: + options: + show_overloads: true +``` + +```md title="or in docs/some_page.md (local configuration)" +::: path.to.module + options: + show_overloads: false +``` + +/// admonition | Preview + type: preview +//// tab | With overloads +

    function

    + + +```python +@overload +function(param1: int): ... + +@overload +function(param1: str): ... + +function(param1: str | int) +``` +Function docstring. + +//// +//// tab | Without overloads +

    function

    + +```python +function(param1: str | int) +``` +Function docstring. + +//// +/// + [](){#option-signature_crossrefs} ## `signature_crossrefs` diff --git a/docs/usage/customization.md b/docs/usage/customization.md index 9e13da66..8239c2e9 100644 --- a/docs/usage/customization.md +++ b/docs/usage/customization.md @@ -283,7 +283,7 @@ for filepath in sorted(path for path in Path(basedir).rglob("*") if "_base" not ) ``` -See them [in the repository](https://github.com/mkdocstrings/python/tree/master/src/mkdocstrings_handlers/python/templates/). +See them [in the repository](https://github.com/mkdocstrings/python/tree/main/src/mkdocstrings_handlers/python/templates/). See the general *mkdocstrings* documentation to learn how to override them: https://mkdocstrings.github.io/theming/#templates. Each one of these templates extends a base version in `theme/_base`. Example: @@ -439,4 +439,4 @@ div.doc-contents:not(.first) { padding-left: 25px; border-left: .05rem solid rgba(200, 200, 200, 0.2); } -``` \ No newline at end of file +``` diff --git a/docs/usage/index.md b/docs/usage/index.md index 84110936..b2a00955 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -87,8 +87,8 @@ plugins: - mkdocstrings: handlers: python: - import: - - https://docs.python-requests.org/en/master/objects.inv + inventories: + - https://docs.python.org/3/objects.inv ``` When loading an inventory, you enable automatic cross-references diff --git a/duties.py b/duties.py index 9e516ce5..18282747 100644 --- a/duties.py +++ b/duties.py @@ -3,11 +3,13 @@ from __future__ import annotations import os +import re import sys from contextlib import contextmanager +from functools import wraps from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable from duty import duty, tools @@ -26,15 +28,30 @@ MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" -def pyprefix(title: str) -> str: # noqa: D103 +def pyprefix(title: str) -> str: if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title +def not_from_insiders(func: Callable) -> Callable: + @wraps(func) + def wrapper(ctx: Context, *args: Any, **kwargs: Any) -> None: + origin = ctx.run("git config --get remote.origin.url", silent=True) + if "pawamoy-insiders/griffe" in origin: + ctx.run( + lambda: False, + title="Not running this task from insiders repository (do that from public repo instead!)", + ) + return + func(ctx, *args, **kwargs) + + return wrapper + + @contextmanager -def material_insiders() -> Iterator[bool]: # noqa: D103 +def material_insiders() -> Iterator[bool]: if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: @@ -45,6 +62,12 @@ def material_insiders() -> Iterator[bool]: # noqa: D103 yield False +def _get_changelog_version() -> str: + changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") + with Path(__file__).parent.joinpath("CHANGELOG.md").open("r", encoding="utf8") as file: + return next(filter(bool, map(changelog_version_re.match, file))).group(1) # type: ignore[union-attr] + + @duty def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. @@ -53,6 +76,7 @@ def changelog(ctx: Context, bump: str = "") -> None: bump: Bump option passed to git-changelog. """ ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") + ctx.run(tools.yore.check(bump=bump or _get_changelog_version()), title="Checking legacy code") @duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) @@ -85,6 +109,7 @@ def check_docs(ctx: Context) -> None: def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" os.environ["MYPYPATH"] = "src" + os.environ["FORCE_COLOR"] = "1" ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), @@ -176,6 +201,7 @@ def build(ctx: Context) -> None: @duty +@not_from_insiders def publish(ctx: Context) -> None: """Publish source and wheel distributions to PyPI.""" if not Path("dist").exists(): @@ -189,18 +215,13 @@ def publish(ctx: Context) -> None: @duty(post=["build", "publish", "docs-deploy"]) +@not_from_insiders def release(ctx: Context, version: str = "") -> None: """Release a new Python package. Parameters: version: The new version number to use. """ - origin = ctx.run("git config --get remote.origin.url", silent=True) - if "pawamoy-insiders/mkdocstrings-python" in origin: - ctx.run( - lambda: False, - title="Not releasing from insiders repository (do that from public repo instead!)", - ) if not (version := (version or input("> Version to release: ")).strip()): ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) diff --git a/mkdocs.yml b/mkdocs.yml index 396f738f..6fc301b0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,9 +37,7 @@ nav: - Advanced: - Customization: usage/customization.md - Extensions: usage/extensions.md -# defer to gen-files + literate-nav -- API reference: - - mkdocstrings-python: reference/ +- API reference: reference/api.md - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md @@ -63,7 +61,8 @@ theme: - content.code.copy - content.tooltips - navigation.footer - - navigation.indexes + - navigation.instant.preview + - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky @@ -140,34 +139,31 @@ markdown_extensions: permalink: "¤" plugins: -- autorefs - search +- autorefs - markdown-exec -- gen-files: - scripts: - - scripts/gen_ref_nav.py -- literate-nav: - nav_file: SUMMARY.md +- section-index # - coverage - mkdocstrings: handlers: python: paths: [src, docs/snippets] - import: + inventories: - https://docs.python.org/3/objects.inv - https://mkdocstrings.github.io/objects.inv - https://mkdocstrings.github.io/autorefs/objects.inv - https://mkdocstrings.github.io/griffe/objects.inv - https://python-markdown.github.io/objects.inv options: + backlinks: tree docstring_options: ignore_init_summary: true docstring_section_style: list - extensions: - - scripts/griffe_extensions.py - filters: ["!^_"] + extensions: [scripts/griffe_extensions.py] + filters: public heading_level: 1 inherited_members: true + line_length: 88 merge_init_into_class: true parameter_headings: true preload_modules: [mkdocstrings] @@ -185,12 +181,26 @@ plugins: signature_crossrefs: true summary: true unwrap_annotated: true +- llmstxt: + files: + - output: llms-full.txt + inputs: + - index.md + - reference/**.md - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true type: timeago - minify: minify_html: !ENV [DEPLOY, false] +- redirects: + redirect_maps: + reference/mkdocstrings_handlers/python/index.md: reference/api.md + reference/mkdocstrings_handlers/python/config.md: reference/api.md#mkdocstrings_handlers.python.config + reference/mkdocstrings_handlers/python/debug.md: reference/api.md#mkdocstrings_handlers.python.debug + reference/mkdocstrings_handlers/python/handler.md: reference/api.md#mkdocstrings_handlers.python.handler + reference/mkdocstrings_handlers/python/rendering.md: reference/api.md#mkdocstrings_handlers.python.rendering + - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: diff --git a/pyproject.toml b/pyproject.toml index b4a453e4..54f27585 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ build-backend = "pdm.backend" name = "mkdocstrings-python" description = "A Python handler for mkdocstrings." authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}] -license = {text = "ISC"} +license = "ISC" +license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" keywords = [] @@ -30,9 +31,9 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "mkdocstrings>=0.28", - "mkdocs-autorefs>=1.2", - "griffe>=0.49", + "mkdocstrings>=0.28.3", + "mkdocs-autorefs>=1.4", + "griffe>=1.6.2", "typing-extensions>=4.0; python_version < '3.11'", ] @@ -78,14 +79,15 @@ data = [ ] [dependency-groups] -dev = [ - # maintenance +maintain = [ "build>=1.2", "git-changelog>=2.5", "twine>=5.1", - - # ci - "duty>=1.4", + "yore>=0.3.3", +] +ci = [ + "black>=25.1", + "duty>=1.6", "ruff>=0.4", "pytest>=8.2", "pytest-cov>=5.0", @@ -96,19 +98,20 @@ dev = [ "mypy>=1.10", "types-markdown>=3.6", "types-pyyaml>=6.0", - - # docs - "black>=24.4", +] + docs = [ "markdown-callouts>=0.4", "markdown-exec>=1.8", "mkdocs>=1.6", "mkdocs-coverage>=1.0", - "mkdocs-gen-files>=0.5", "mkdocs-git-revision-date-localized-plugin>=1.2", - "mkdocs-literate-nav>=0.6", + "mkdocs-llmstxt>=0.1", "mkdocs-material>=9.5", "pydantic>=2.10", "mkdocs-minify-plugin>=0.8", + "mkdocs-redirects>=1.2", + "mkdocs-section-index>=0.3", + "mkdocstrings>=0.29", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] @@ -116,3 +119,6 @@ dev = [ [tool.inline-snapshot] storage-dir = "tests/snapshots" format-command = "ruff format --config config/ruff.toml --stdin-filename {filename}" + +[tool.uv] +default-groups = ["maintain", "ci", "docs"] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 85240535..6a81e239 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -1,4 +1,4 @@ -"""Script to generate the project's credits.""" +# Script to generate the project's credits. from __future__ import annotations @@ -27,7 +27,7 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")] +devdeps = [dep for group in pyproject["dependency-groups"].values() for dep in group if not dep.startswith("-e")] PackageMetadata = dict[str, Union[str, Iterable[str]]] Metadata = dict[str, PackageMetadata] diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py deleted file mode 100644 index 6939e864..00000000 --- a/scripts/gen_ref_nav.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Generate the code reference pages and navigation.""" - -from pathlib import Path - -import mkdocs_gen_files - -nav = mkdocs_gen_files.Nav() -mod_symbol = '' - -root = Path(__file__).parent.parent -src = root / "src" - -for path in sorted(src.rglob("*.py")): - 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"---\ntitle: {ident}\n---\n\n::: {ident}") - - mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) - -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: - nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/get_version.py b/scripts/get_version.py index f4a30a8c..6734e5b6 100644 --- a/scripts/get_version.py +++ b/scripts/get_version.py @@ -1,4 +1,4 @@ -"""Get current project version from Git tags or changelog.""" +# Get current project version from Git tags or changelog. import re from contextlib import suppress @@ -13,7 +13,6 @@ def get_version() -> str: - """Get current project version from Git tags or changelog.""" scm_version = get_version_from_scm(_root) or _default_scm_version if scm_version.version <= Version("0.1"): # Missing Git tags? with suppress(OSError, StopIteration): # noqa: SIM117 diff --git a/scripts/griffe_extensions.py b/scripts/griffe_extensions.py index 4ff0c8cc..eb50f5f2 100644 --- a/scripts/griffe_extensions.py +++ b/scripts/griffe_extensions.py @@ -1,4 +1,4 @@ -"""Custom extensions for Griffe.""" +# Custom extensions for Griffe. from __future__ import annotations @@ -7,7 +7,7 @@ import griffe -logger = griffe.get_logger("griffe_extensions") +_logger = griffe.get_logger("griffe_extensions") class CustomFields(griffe.Extension): @@ -28,14 +28,14 @@ def on_attribute_instance( except AttributeError: return - if field.canonical_path == "mkdocstrings_handler.python.config.Field": + if field.canonical_path == "mkdocstrings_handlers.python._internal.config._Field": description = next( attr.value for attr in field.arguments if isinstance(attr, griffe.ExprKeyword) and attr.name == "description" ) if not isinstance(description, str): - logger.warning(f"Field description of {attr.path} is not a static string") + _logger.warning(f"Field description of {attr.path} is not a static string") description = str(description) attr.docstring = griffe.Docstring( diff --git a/scripts/insiders.py b/scripts/insiders.py index a7da99bc..4cd438d4 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -1,4 +1,4 @@ -"""Functions related to Insiders funding goals.""" +# Functions related to Insiders funding goals. from __future__ import annotations @@ -23,7 +23,7 @@ logger = logging.getLogger(f"mkdocs.logs.{__name__}") -def human_readable_amount(amount: int) -> str: # noqa: D103 +def human_readable_amount(amount: int) -> str: str_amount = str(amount) if len(str_amount) >= 4: # noqa: PLR2004 return f"{str_amount[: len(str_amount) - 3]},{str_amount[-3:]}" @@ -32,16 +32,12 @@ def human_readable_amount(amount: int) -> str: # noqa: D103 @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 @@ -68,8 +64,6 @@ def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: @dataclass class Goal: - """Class representing an Insiders goal.""" - name: str amount: int features: list[Feature] @@ -94,16 +88,6 @@ def render(self, rel_base: str = "..") -> None: # noqa: D102 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( @@ -151,15 +135,6 @@ def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[in 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 = {} @@ -174,18 +149,10 @@ def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 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 +def load_json(url: str) -> str | list | dict: with urlopen(url) as response: # noqa: S310 return json.loads(response.read().decode()) @@ -201,6 +168,6 @@ def load_json(url: str) -> str | list | dict: # noqa: D103 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), + key=lambda ft: cast("date", ft.since), reverse=True, ) diff --git a/scripts/make.py b/scripts/make.py index 3d427296..5a7fb4c2 100755 --- a/scripts/make.py +++ b/scripts/make.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -"""Management commands.""" - from __future__ import annotations import os @@ -16,7 +14,7 @@ from collections.abc import Iterator -PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13").split() def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index bfa74e5c..739f93b3 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,14 +1,14 @@ -"""Generate a JSON schema of the Python handler configuration.""" +# Generate a JSON schema of the Python handler configuration. import json -from dataclasses import fields +from dataclasses import dataclass, fields from os.path import join from typing import Any from mkdocs.config.defaults import MkDocsConfig from mkdocs.plugins import get_plugin_logger -from mkdocstrings_handlers.python.config import PythonInputConfig, PythonInputOptions +from mkdocstrings_handlers.python import PythonInputConfig, PythonInputOptions # TODO: Update when Pydantic supports Python 3.14 (sources and duties as well). try: @@ -17,25 +17,30 @@ TypeAdapter = None # type: ignore[assignment,misc] -logger = get_plugin_logger(__name__) +_logger = get_plugin_logger(__name__) def on_post_build(config: MkDocsConfig, **kwargs: Any) -> None: # noqa: ARG001 """Write `schema.json` to the site directory.""" if TypeAdapter is None: - logger.info("Pydantic is not installed, skipping JSON schema generation") + _logger.info("Pydantic is not installed, skipping JSON schema generation") return - adapter = TypeAdapter(PythonInputConfig) + + @dataclass + class PythonHandlerSchema: + python: PythonInputConfig + + adapter = TypeAdapter(PythonHandlerSchema) schema = adapter.json_schema() schema["$schema"] = "https://json-schema.org/draft-07/schema" with open(join(config.site_dir, "schema.json"), "w") as file: json.dump(schema, file, indent=2) - logger.debug("Generated JSON schema") + _logger.debug("Generated JSON schema") autorefs = config["plugins"]["autorefs"] for field in fields(PythonInputConfig): if f"setting-{field.name}" not in autorefs._primary_url_map: - logger.warning(f"Handler setting `{field.name}` is not documented") + _logger.warning(f"Handler setting `{field.name}` is not documented") for field in fields(PythonInputOptions): if f"option-{field.name}" not in autorefs._primary_url_map: - logger.warning(f"Configuration option `{field.name}` is not documented") + _logger.warning(f"Configuration option `{field.name}` is not documented") diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py index 0432a90d..faa9b9f4 100644 --- a/src/mkdocstrings_handlers/python/__init__.py +++ b/src/mkdocstrings_handlers/python/__init__.py @@ -1,5 +1,70 @@ """Python handler for mkdocstrings.""" -from mkdocstrings_handlers.python.handler import get_handler +from mkdocstrings_handlers.python._internal.config import ( + AutoStyleOptions, + GoogleStyleOptions, + Inventory, + NumpyStyleOptions, + PerStyleOptions, + PythonConfig, + PythonInputConfig, + PythonInputOptions, + PythonOptions, + SphinxStyleOptions, + SummaryOption, +) +from mkdocstrings_handlers.python._internal.handler import PythonHandler, get_handler +from mkdocstrings_handlers.python._internal.rendering import ( + AutorefsHook, + Order, + Tree, + do_as_attributes_section, + do_as_classes_section, + do_as_functions_section, + do_as_modules_section, + do_backlink_tree, + do_crossref, + do_filter_objects, + do_format_attribute, + do_format_code, + do_format_signature, + do_get_template, + do_multi_crossref, + do_order_members, + do_split_path, + do_stash_crossref, +) -__all__ = ["get_handler"] +__all__ = [ + "AutoStyleOptions", + "AutorefsHook", + "GoogleStyleOptions", + "Inventory", + "NumpyStyleOptions", + "Order", + "PerStyleOptions", + "PythonConfig", + "PythonHandler", + "PythonInputConfig", + "PythonInputOptions", + "PythonOptions", + "SphinxStyleOptions", + "SummaryOption", + "Tree", + "do_as_attributes_section", + "do_as_classes_section", + "do_as_functions_section", + "do_as_modules_section", + "do_backlink_tree", + "do_crossref", + "do_filter_objects", + "do_format_attribute", + "do_format_code", + "do_format_signature", + "do_get_template", + "do_multi_crossref", + "do_order_members", + "do_split_path", + "do_stash_crossref", + "get_handler", +] diff --git a/src/mkdocstrings_handlers/python/_internal/__init__.py b/src/mkdocstrings_handlers/python/_internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/mkdocstrings_handlers/python/_internal/config.py b/src/mkdocstrings_handlers/python/_internal/config.py new file mode 100644 index 00000000..210f8fe2 --- /dev/null +++ b/src/mkdocstrings_handlers/python/_internal/config.py @@ -0,0 +1,1058 @@ +# Configuration and options dataclasses. + +from __future__ import annotations + +import re +import sys +from dataclasses import field, fields +from typing import TYPE_CHECKING, Annotated, Any, Literal + +from mkdocstrings import get_logger + +from mkdocstrings_handlers.python._internal.rendering import Order # noqa: TC001 + +# YORE: EOL 3.10: Replace block with line 2. +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + + +_logger = get_logger(__name__) + +_DEFAULT_FILTERS = ["!^_[^_]"] + +try: + # When Pydantic is available, use it to validate options (done automatically). + # Users can therefore opt into validation by installing Pydantic in development/CI. + # When building the docs to deploy them, Pydantic is not required anymore. + + # When building our own docs, Pydantic is always installed (see `docs` group in `pyproject.toml`) + # to allow automatic generation of a JSON Schema. The JSON Schema is then referenced by mkdocstrings, + # which is itself referenced by mkdocs-material's schema system. For example in VSCode: + # + # "yaml.schemas": { + # "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + # } + import pydantic + + if getattr(pydantic, "__version__", "1.").startswith("1."): + raise ImportError # noqa: TRY301 + + # YORE: EOL 3.9: Remove block. + if sys.version_info < (3, 10): + try: + import eval_type_backport # noqa: F401 + except ImportError: + _logger.debug( + "Pydantic needs the `eval-type-backport` package to be installed " + "for modern type syntax to work on Python 3.9. " + "Deactivating Pydantic validation for Python handler options.", + ) + raise + + from inspect import cleandoc + + from pydantic import Field as BaseField + from pydantic.dataclasses import dataclass + + _base_url = "https://mkdocstrings.github.io/python/usage" + + def _Field( # noqa: N802 + *args: Any, + description: str, + group: Literal["general", "headings", "members", "docstrings", "signatures"] | None = None, + parent: str | None = None, + **kwargs: Any, + ) -> None: + def _add_markdown_description(schema: dict[str, Any]) -> None: + url = f"{_base_url}/{f'configuration/{group}/' if group else ''}#{parent or schema['title']}" + schema["markdownDescription"] = f"[DOCUMENTATION]({url})\n\n{schema['description']}" + + return BaseField( + *args, + description=cleandoc(description), + field_title_generator=lambda name, _: name, + json_schema_extra=_add_markdown_description, + **kwargs, + ) +except ImportError: + from dataclasses import dataclass # type: ignore[no-redef] + + def _Field(*args: Any, **kwargs: Any) -> None: # type: ignore[misc] # noqa: N802 + pass + + +if TYPE_CHECKING: + from collections.abc import MutableMapping + + +# YORE: EOL 3.9: Remove block. +_dataclass_options = {"frozen": True} +if sys.version_info >= (3, 10): + _dataclass_options["kw_only"] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class GoogleStyleOptions: + """Google style docstring options.""" + + ignore_init_summary: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Whether to ignore the summary in `__init__` methods' docstrings.", + ), + ] = False + + returns_multiple_items: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse multiple items in `Yields` and `Returns` sections. + + When true, each item's continuation lines must be indented. + When false (single item), no further indentation is required. + """, + ), + ] = True + + returns_named_value: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse `Yields` and `Returns` section items as name and description, rather than type and description. + + When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. + When false, parentheses are optional but the items cannot be named: `int: Description`. + """, + ), + ] = True + + returns_type_in_property_summary: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Whether to parse the return type of properties at the beginning of their summary: `str: Summary of the property`.", + ), + ] = False + + receives_multiple_items: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse multiple items in `Receives` sections. + + When true, each item's continuation lines must be indented. + When false (single item), no further indentation is required. + """, + ), + ] = True + + receives_named_value: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="""Whether to parse `Receives` section items as name and description, rather than type and description. + + When true, type must be wrapped in parentheses: `(int): Description.`. Names are optional: `name (int): Description.`. + When false, parentheses are optional but the items cannot be named: `int: Description`. + """, + ), + ] = True + + trim_doctest_flags: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Whether to remove doctest flags from Python example blocks.", + ), + ] = True + + warn_unknown_params: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Warn about documented parameters not appearing in the signature.", + ), + ] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class NumpyStyleOptions: + """Numpy style docstring options.""" + + ignore_init_summary: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Whether to ignore the summary in `__init__` methods' docstrings.", + ), + ] = False + + trim_doctest_flags: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Whether to remove doctest flags from Python example blocks.", + ), + ] = True + + warn_unknown_params: Annotated[ + bool, + _Field( + group="docstrings", + parent="docstring_options", + description="Warn about documented parameters not appearing in the signature.", + ), + ] = True + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class SphinxStyleOptions: + """Sphinx style docstring options.""" + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PerStyleOptions: + """Per style options.""" + + google: Annotated[ + GoogleStyleOptions, + _Field( + group="docstrings", + parent="docstring_options", + description="Google-style options.", + ), + ] = field(default_factory=GoogleStyleOptions) + + numpy: Annotated[ + NumpyStyleOptions, + _Field( + group="docstrings", + parent="docstring_options", + description="Numpydoc-style options.", + ), + ] = field(default_factory=NumpyStyleOptions) + + sphinx: Annotated[ + SphinxStyleOptions, + _Field( + group="docstrings", + parent="docstring_options", + description="Sphinx-style options.", + ), + ] = field(default_factory=SphinxStyleOptions) + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + if "google" in data: + data["google"] = GoogleStyleOptions(**data["google"]) + if "numpy" in data: + data["numpy"] = NumpyStyleOptions(**data["numpy"]) + if "sphinx" in data: + data["sphinx"] = SphinxStyleOptions(**data["sphinx"]) + return cls(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class AutoStyleOptions: + """Auto style docstring options.""" + + method: Annotated[ + Literal["heuristics", "max_sections"], + _Field( + group="docstrings", + parent="docstring_options", + description="The method to use to determine the docstring style.", + ), + ] = "heuristics" + + style_order: Annotated[ + list[str], + _Field( + group="docstrings", + parent="docstring_options", + description="The order of the docstring styles to try.", + ), + ] = field(default_factory=lambda: ["sphinx", "google", "numpy"]) + + default: Annotated[ + str | None, + _Field( + group="docstrings", + parent="docstring_options", + description="The default docstring style to use if no other style is detected.", + ), + ] = None + + per_style_options: Annotated[ + PerStyleOptions, + _Field( + group="docstrings", + parent="docstring_options", + description="Per-style options.", + ), + ] = field(default_factory=PerStyleOptions) + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + if "per_style_options" in data: + data["per_style_options"] = PerStyleOptions.from_data(**data["per_style_options"]) + return cls(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class SummaryOption: + """Summary option.""" + + attributes: Annotated[ + bool, + _Field( + group="members", + parent="summary", + description="Whether to render summaries of attributes.", + ), + ] = False + + functions: Annotated[ + bool, + _Field( + group="members", + parent="summary", + description="Whether to render summaries of functions (methods).", + ), + ] = False + + classes: Annotated[ + bool, + _Field( + group="members", + parent="summary", + description="Whether to render summaries of classes.", + ), + ] = False + + modules: Annotated[ + bool, + _Field( + group="members", + parent="summary", + description="Whether to render summaries of modules.", + ), + ] = False + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonInputOptions: + """Accepted input options.""" + + allow_inspection: Annotated[ + bool, + _Field( + group="general", + description="Whether to allow inspecting modules when visiting them is not possible.", + ), + ] = True + + force_inspection: Annotated[ + bool, + _Field( + group="general", + description="Whether to force using dynamic analysis when loading data.", + ), + ] = False + + annotations_path: Annotated[ + Literal["brief", "source", "full"], + _Field( + group="signatures", + description="The verbosity for annotations path: `brief` (recommended), `source` (as written in the source), or `full`.", + ), + ] = "brief" + + backlinks: Annotated[ + Literal["flat", "tree", False], + _Field( + group="general", + description="Whether to render backlinks, and how.", + ), + ] = False + + docstring_options: Annotated[ + GoogleStyleOptions | NumpyStyleOptions | SphinxStyleOptions | AutoStyleOptions | None, + _Field( + group="docstrings", + description="""The options for the docstring parser. + + See [docstring parsers](https://mkdocstrings.github.io/griffe/reference/docstrings/) and their options in Griffe docs. + """, + ), + ] = None + + docstring_section_style: Annotated[ + Literal["table", "list", "spacy"], + _Field( + group="docstrings", + description="The style used to render docstring sections.", + ), + ] = "table" + + docstring_style: Annotated[ + Literal["auto", "google", "numpy", "sphinx"] | None, + _Field( + group="docstrings", + description="The docstring style to use: `auto`, `google`, `numpy`, `sphinx`, or `None`.", + ), + ] = "google" + + extensions: Annotated[ + list[str | dict[str, Any]], + _Field( + group="general", + description="A list of Griffe extensions to load.", + ), + ] = field(default_factory=list) + + filters: Annotated[ + list[str] | Literal["public"], + _Field( + group="members", + description="""A list of filters, or `"public"`. + + **List of filters** + + A filter starting with `!` will exclude matching objects instead of including them. + The `members` option takes precedence over `filters` (filters will still be applied recursively + to lower members in the hierarchy). + + **Filtering methods** + + [:octicons-heart-fill-24:{ .pulse } Sponsors only](../insiders/index.md){ .insiders } — + [:octicons-tag-24: Insiders 1.11.0](../insiders/changelog.md#1.11.0) + + The `public` method will include only public objects: + those added to `__all__` or not starting with an underscore (except for special methods/attributes). + """, + ), + ] = field(default_factory=lambda: _DEFAULT_FILTERS.copy()) + + find_stubs_package: Annotated[ + bool, + _Field( + group="general", + description="Whether to load stubs package (package-stubs) when extracting docstrings.", + ), + ] = False + + group_by_category: Annotated[ + bool, + _Field( + group="members", + description="Group the object's children by categories: attributes, classes, functions, and modules.", + ), + ] = True + + heading: Annotated[ + str, + _Field( + group="headings", + description="A custom string to override the autogenerated heading of the root object.", + ), + ] = "" + + heading_level: Annotated[ + int, + _Field( + group="headings", + description="The initial heading level to use.", + ), + ] = 2 + + inherited_members: Annotated[ + bool | list[str], + _Field( + group="members", + description="""A boolean, or an explicit list of inherited members to render. + + If true, select all inherited members, which can then be filtered with `members`. + If false or empty list, do not select any inherited member. + """, + ), + ] = False + + line_length: Annotated[ + int, + _Field( + group="signatures", + description="Maximum line length when formatting code/signatures.", + ), + ] = 60 + + members: Annotated[ + list[str] | bool | None, + _Field( + group="members", + description="""A boolean, or an explicit list of members to render. + + If true, select all members without further filtering. + If false or empty list, do not render members. + If none, select all members and apply further filtering with filters and docstrings. + """, + ), + ] = None + + members_order: Annotated[ + Order | list[Order], + _Field( + group="members", + description="""The members ordering to use. + + - `__all__`: order members according to `__all__` module attributes, if declared; + - `alphabetical`: order members alphabetically; + - `source`: order members as they appear in the source file. + + Since `__all__` is a module-only attribute, it can't be used to sort class members, + therefore the `members_order` option accepts a list of ordering methods, + indicating ordering preferences. + """, + ), + ] = "alphabetical" + + merge_init_into_class: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to merge the `__init__` method into the class' signature and docstring.", + ), + ] = False + + modernize_annotations: Annotated[ + bool, + _Field( + group="signatures", + description="Whether to modernize annotations, for example `Optional[str]` into `str | None`.", + ), + ] = False + + parameter_headings: Annotated[ + bool, + _Field( + group="headings", + description="Whether to render headings for parameters (therefore showing parameters in the ToC).", + ), + ] = False + + preload_modules: Annotated[ + list[str], + _Field( + group="general", + description="""Pre-load modules that are not specified directly in autodoc instructions (`::: identifier`). + + It is useful when you want to render documentation for a particular member of an object, + and this member is imported from another package than its parent. + + For an imported member to be rendered, you need to add it to the `__all__` attribute + of the importing module. + + The modules must be listed as an array of strings. + """, + ), + ] = field(default_factory=list) + + relative_crossrefs: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to enable the relative crossref syntax.", + ), + ] = False + + scoped_crossrefs: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to enable the scoped crossref ability.", + ), + ] = False + + show_overloads: Annotated[ + bool, + _Field( + group="signatures", + description="Show the overloads of a function or method.", + ), + ] = True + + separate_signature: Annotated[ + bool, + _Field( + group="signatures", + description="""Whether to put the whole signature in a code block below the heading. + + If Black or Ruff are installed, the signature is also formatted using them. + """, + ), + ] = False + + show_bases: Annotated[ + bool, + _Field( + group="general", + description="Show the base classes of a class.", + ), + ] = True + + show_category_heading: Annotated[ + bool, + _Field( + group="headings", + description="When grouped by categories, show a heading for each category.", + ), + ] = False + + show_docstring_attributes: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Attributes' section in the object's docstring.", + ), + ] = True + + show_docstring_classes: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Classes' section in the object's docstring.", + ), + ] = True + + show_docstring_description: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the textual block (including admonitions) in the object's docstring.", + ), + ] = True + + show_docstring_examples: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Examples' section in the object's docstring.", + ), + ] = True + + show_docstring_functions: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Functions' or 'Methods' sections in the object's docstring.", + ), + ] = True + + show_docstring_modules: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Modules' section in the object's docstring.", + ), + ] = True + + show_docstring_other_parameters: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Other Parameters' section in the object's docstring.", + ), + ] = True + + show_docstring_parameters: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Parameters' section in the object's docstring.", + ), + ] = True + + show_docstring_raises: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Raises' section in the object's docstring.", + ), + ] = True + + show_docstring_receives: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Receives' section in the object's docstring.", + ), + ] = True + + show_docstring_returns: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Returns' section in the object's docstring.", + ), + ] = True + + show_docstring_warns: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Warns' section in the object's docstring.", + ), + ] = True + + show_docstring_yields: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to display the 'Yields' section in the object's docstring.", + ), + ] = True + + show_if_no_docstring: Annotated[ + bool, + _Field( + group="docstrings", + description="Show the object heading even if it has no docstring or children with docstrings.", + ), + ] = False + + show_inheritance_diagram: Annotated[ + bool, + _Field( + group="docstrings", + description="Show the inheritance diagram of a class using Mermaid.", + ), + ] = False + + show_labels: Annotated[ + bool, + _Field( + group="docstrings", + description="Whether to show labels of the members.", + ), + ] = True + + show_object_full_path: Annotated[ + bool, + _Field( + group="docstrings", + description="Show the full Python path of every object.", + ), + ] = False + + show_root_full_path: Annotated[ + bool, + _Field( + group="docstrings", + description="Show the full Python path for the root object heading.", + ), + ] = True + + show_root_heading: Annotated[ + bool, + _Field( + group="headings", + description="""Show the heading of the object at the root of the documentation tree. + + The root object is the object referenced by the identifier after `:::`. + """, + ), + ] = False + + show_root_members_full_path: Annotated[ + bool, + _Field( + group="headings", + description="Show the full Python path of the root members.", + ), + ] = False + + show_root_toc_entry: Annotated[ + bool, + _Field( + group="headings", + description="If the root heading is not shown, at least add a ToC entry for it.", + ), + ] = True + + show_signature_annotations: Annotated[ + bool, + _Field( + group="signatures", + description="Show the type annotations in methods and functions signatures.", + ), + ] = False + + show_signature: Annotated[ + bool, + _Field( + group="signatures", + description="Show methods and functions signatures.", + ), + ] = True + + show_source: Annotated[ + bool, + _Field( + group="general", + description="Show the source code of this object.", + ), + ] = True + + show_submodules: Annotated[ + bool, + _Field( + group="members", + description="When rendering a module, show its submodules recursively.", + ), + ] = False + + show_symbol_type_heading: Annotated[ + bool, + _Field( + group="headings", + description="Show the symbol type in headings (e.g. mod, class, meth, func and attr).", + ), + ] = False + + show_symbol_type_toc: Annotated[ + bool, + _Field( + group="headings", + description="Show the symbol type in the Table of Contents (e.g. mod, class, methd, func and attr).", + ), + ] = False + + signature_crossrefs: Annotated[ + bool, + _Field( + group="signatures", + description="Whether to render cross-references for type annotations in signatures.", + ), + ] = False + + summary: Annotated[ + bool | SummaryOption, + _Field( + group="members", + description="Whether to render summaries of modules, classes, functions (methods) and attributes.", + ), + ] = field(default_factory=SummaryOption) + + toc_label: Annotated[ + str, + _Field( + group="headings", + description="A custom string to override the autogenerated toc label of the root object.", + ), + ] = "" + + unwrap_annotated: Annotated[ + bool, + _Field( + group="signatures", + description="Whether to unwrap `Annotated` types to show only the type without the annotations.", + ), + ] = False + + extra: Annotated[ + dict[str, Any], + _Field( + group="general", + description="Extra options.", + ), + ] = field(default_factory=dict) + + @classmethod + def _extract_extra(cls, data: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: + field_names = {field.name for field in fields(cls)} + copy = data.copy() + return {name: copy.pop(name) for name in data if name not in field_names}, copy + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + if "docstring_options" in data: + docstring_style = data.get("docstring_style", "google") + docstring_options = data["docstring_options"] + if docstring_options is not None: + if docstring_style == "auto": + docstring_options = AutoStyleOptions.from_data(**docstring_options) + elif docstring_style == "google": + docstring_options = GoogleStyleOptions(**docstring_options) + elif docstring_style == "numpy": + docstring_options = NumpyStyleOptions(**docstring_options) + elif docstring_style == "sphinx": + docstring_options = SphinxStyleOptions(**docstring_options) + data["docstring_options"] = docstring_options + if "summary" in data: + summary = data["summary"] + if summary is True: + summary = SummaryOption(attributes=True, functions=True, classes=True, modules=True) + elif summary is False: + summary = SummaryOption(attributes=False, functions=False, classes=False, modules=False) + else: + summary = SummaryOption(**summary) + data["summary"] = summary + return data + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + return cls(**cls.coerce(**data)) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonOptions(PythonInputOptions): # type: ignore[override,unused-ignore] + """Final options passed as template context.""" + + filters: list[tuple[re.Pattern, bool]] | Literal["public"] = field( # type: ignore[assignment] + default_factory=lambda: [ + (re.compile(filtr.removeprefix("!")), filtr.startswith("!")) for filtr in _DEFAULT_FILTERS + ], + ) + """A list of filters, or `"public"`.""" + + summary: SummaryOption = field(default_factory=SummaryOption) + """Whether to render summaries of modules, classes, functions (methods) and attributes.""" + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Create an instance from a dictionary.""" + if "filters" in data: + # Non-insiders: transform back to default filters. + # Next: `if "filters" in data and not isinstance(data["filters"], str):`. + if data["filters"] == "public": + data["filters"] = _DEFAULT_FILTERS + # Filters are `None` or a sequence of strings (tests use tuples). + data["filters"] = [ + (re.compile(filtr.removeprefix("!")), filtr.startswith("!")) for filtr in data["filters"] or () + ] + return super().coerce(**data) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class Inventory: + """An inventory.""" + + url: Annotated[ + str, + _Field( + parent="inventories", + description="The URL of the inventory.", + ), + ] + + base_url: Annotated[ + str | None, + _Field( + parent="inventories", + description="The base URL of the inventory.", + ), + ] = None + + domains: Annotated[ + list[str], + _Field( + parent="inventories", + description="The domains to load from the inventory.", + ), + ] = field(default_factory=lambda: ["py"]) + + @property + def _config(self) -> dict[str, Any]: + return {"base_url": self.base_url, "domains": self.domains} + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonInputConfig: + """Python handler configuration.""" + + inventories: Annotated[ + list[str | Inventory], + _Field(description="The inventories to load."), + ] = field(default_factory=list) + + paths: Annotated[ + list[str], + _Field(description="The paths in which to search for Python packages."), + ] = field(default_factory=lambda: ["."]) + + load_external_modules: Annotated[ + bool | None, + _Field(description="Whether to always load external modules/packages."), + ] = None + + options: Annotated[ + PythonInputOptions, + _Field(description="Configuration options for collecting and rendering objects."), + ] = field(default_factory=PythonInputOptions) + + locale: Annotated[ + str | None, + _Field(description="The locale to use when translating template strings."), + ] = None + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + return data + + @classmethod + def from_data(cls, **data: Any) -> Self: + """Create an instance from a dictionary.""" + return cls(**cls.coerce(**data)) + + +# YORE: EOL 3.9: Replace `**_dataclass_options` with `frozen=True, kw_only=True` within line. +@dataclass(**_dataclass_options) # type: ignore[call-overload] +class PythonConfig(PythonInputConfig): # type: ignore[override,unused-ignore] + """Python handler configuration.""" + + inventories: Annotated[ + list[Inventory], + _Field(description="The object inventories to load."), + ] = field(default_factory=list) # type: ignore[assignment] + + options: Annotated[ + dict[str, Any], + _Field(description="Configuration options for collecting and rendering objects."), + ] = field(default_factory=dict) # type: ignore[assignment] + + @classmethod + def coerce(cls, **data: Any) -> MutableMapping[str, Any]: + """Coerce data.""" + if "inventories" in data: + data["inventories"] = [ + Inventory(url=inv) if isinstance(inv, str) else Inventory(**inv) for inv in data["inventories"] + ] + return data diff --git a/src/mkdocstrings_handlers/python/debug.py b/src/mkdocstrings_handlers/python/_internal/debug.py similarity index 80% rename from src/mkdocstrings_handlers/python/debug.py rename to src/mkdocstrings_handlers/python/_internal/debug.py index e44f2be5..5fff669f 100644 --- a/src/mkdocstrings_handlers/python/debug.py +++ b/src/mkdocstrings_handlers/python/_internal/debug.py @@ -1,5 +1,3 @@ -"""Debugging utilities.""" - from __future__ import annotations import os @@ -10,7 +8,7 @@ @dataclass -class Variable: +class _Variable: """Dataclass describing an environment variable.""" name: str @@ -20,7 +18,7 @@ class Variable: @dataclass -class Package: +class _Package: """Dataclass describing a Python package.""" name: str @@ -30,7 +28,7 @@ class Package: @dataclass -class Environment: +class _Environment: """Dataclass to store environment information.""" interpreter_name: str @@ -41,9 +39,9 @@ class Environment: """Path to Python executable.""" platform: str """Operating System.""" - packages: list[Package] + packages: list[_Package] """Installed packages.""" - variables: list[Variable] + variables: list[_Variable] """Environment variables.""" @@ -58,7 +56,7 @@ def _interpreter_name_version() -> tuple[str, str]: return "", "0.0.0" -def get_version(dist: str = "mkdocstrings-python") -> str: +def _get_version(dist: str = "mkdocstrings-python") -> str: """Get version of the given distribution. Parameters: @@ -73,28 +71,28 @@ def get_version(dist: str = "mkdocstrings-python") -> str: return "0.0.0" -def get_debug_info() -> Environment: +def _get_debug_info() -> _Environment: """Get debug/environment information. Returns: Environment information. """ py_name, py_version = _interpreter_name_version() - packages = ["mkdocs", "mkdocstrings", "mkdocstrings-python", "griffe"] + packages = ["mkdocstrings-python"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MKDOCSTRINGS_PYTHON")]] - return Environment( + return _Environment( interpreter_name=py_name, interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), - variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], - packages=[Package(pkg, get_version(pkg)) for pkg in packages], + variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], + packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) -def print_debug_info() -> None: +def _print_debug_info() -> None: """Print debug/environment information.""" - info = get_debug_info() + info = _get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") @@ -106,4 +104,4 @@ def print_debug_info() -> None: if __name__ == "__main__": - print_debug_info() + _print_debug_info() diff --git a/src/mkdocstrings_handlers/python/_internal/handler.py b/src/mkdocstrings_handlers/python/_internal/handler.py new file mode 100644 index 00000000..896a70e3 --- /dev/null +++ b/src/mkdocstrings_handlers/python/_internal/handler.py @@ -0,0 +1,416 @@ +# This module implements a handler for the Python language. + +from __future__ import annotations + +import glob +import os +import posixpath +import sys +from contextlib import suppress +from dataclasses import asdict +from pathlib import Path +from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar +from warnings import warn + +from griffe import ( + AliasResolutionError, + GriffeLoader, + LinesCollection, + ModulesCollection, + Parser, + load_extensions, + patch_loggers, +) +from mkdocs.exceptions import PluginError +from mkdocstrings import BaseHandler, CollectionError, CollectorItem, HandlerOptions, Inventory, get_logger + +from mkdocstrings_handlers.python._internal import rendering +from mkdocstrings_handlers.python._internal.config import PythonConfig, PythonOptions + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping, MutableMapping, Sequence + + from mkdocs.config.defaults import MkDocsConfig + + +# YORE: EOL 3.10: Replace block with line 2. +if sys.version_info >= (3, 11): + from contextlib import chdir +else: + from contextlib import contextmanager + + @contextmanager + def chdir(path: str) -> Iterator[None]: + old_wd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(old_wd) + + +_logger = get_logger(__name__) + +patch_loggers(get_logger) + + +# YORE: Bump 2: Remove block. +def _warn_extra_options(names: Sequence[str]) -> None: + warn( + "Passing extra options directly under `options` is deprecated. " + "Instead, pass them under `options.extra`, and update your templates. " + f"Current extra (unrecognized) options: {', '.join(sorted(names))}", + DeprecationWarning, + stacklevel=3, + ) + + +class PythonHandler(BaseHandler): + """The Python handler class.""" + + name: ClassVar[str] = "python" + """The handler's name.""" + + domain: ClassVar[str] = "py" + """The cross-documentation domain/language for this handler.""" + + enable_inventory: ClassVar[bool] = True + """Whether this handler is interested in enabling the creation of the `objects.inv` Sphinx inventory file.""" + + fallback_theme: ClassVar[str] = "material" + """The fallback theme.""" + + def __init__(self, config: PythonConfig, base_dir: Path, **kwargs: Any) -> None: + """Initialize the handler. + + Parameters: + config: The handler configuration. + base_dir: The base directory of the project. + **kwargs: Arguments passed to the parent constructor. + """ + super().__init__(**kwargs) + + self.config = config + """The handler configuration.""" + self.base_dir = base_dir + """The base directory of the project.""" + + # YORE: Bump 2: Remove block. + global_extra, global_options = PythonOptions._extract_extra(config.options) + if global_extra: + _warn_extra_options(global_extra.keys()) # type: ignore[arg-type] + self._global_extra = global_extra + self.global_options = global_options + """The global configuration options (in `mkdocs.yml`).""" + + # YORE: Bump 2: Replace `# ` with `` within block. + # self.global_options = config.options + # """The global configuration options (in `mkdocs.yml`).""" + + # Warn if user overrides base templates. + if self.custom_templates: + for theme_dir in base_dir.joinpath(self.custom_templates, "python").iterdir(): + if theme_dir.joinpath("_base").is_dir(): + _logger.warning( + f"Overriding base template '{theme_dir.name}/_base/