diff --git a/.copier-answers.yml b/.copier-answers.yml index 8cacdedb..f7a640c0 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.10.6 +_commit: 0.11.2 _src_path: gh:pawamoy/copier-pdm author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9abeb46..f7fd9bb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: python-version: "3.8" - name: Resolving dependencies - run: pdm lock + run: pdm lock -v - name: Install dependencies run: pdm install -G duty -G docs -G quality -G typing -G security @@ -74,6 +74,9 @@ jobs: with: python-version: ${{ matrix.python-version }} + - name: Resolving dependencies + run: pdm lock -v + - name: Install dependencies run: pdm install --no-editable -G duty -G tests -G docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ea412e..3f6dd2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.9.0](https://github.com/mkdocstrings/python/releases/tag/0.9.0) - 2023-04-03 + +[Compare with 0.8.3](https://github.com/mkdocstrings/python/compare/0.8.3...0.9.0) + +### Features + +- Allow resolving alias to external modules ([02052e2](https://github.com/mkdocstrings/python/commit/02052e248b125a113ab788faa9a075adbdc92ca6) by Gilad). [PR #61](https://github.com/mkdocstrings/python/pull/61), [Follow-up of PR #60](https://github.com/mkdocstrings/python/pull/60) +- Allow pre-loading modules ([36002cb](https://github.com/mkdocstrings/python/commit/36002cb9c89fba35d23afb07a866dd8c6877f742) by Gilad). [Issue mkdocstrings/mkdocstrings#503](https://github.com/mkdocstrings/mkdocstrings/issues/503), [PR #60](https://github.com/mkdocstrings/python/pull/60) +- Add show options for docstrings ([a6c55fb](https://github.com/mkdocstrings/python/commit/a6c55fb52f362dd49b1a7e334a631f6ea3b1b963) by Jeremy Goh). [Issue mkdocstrings/mkdocstrings#466](https://github.com/mkdocstrings/mkdocstrings/issues/466), [PR #56](https://github.com/mkdocstrings/python/pull/56) +- Allow custom list of domains for inventories ([f5ea6fd](https://github.com/mkdocstrings/python/commit/f5ea6fd81f7a531e8a97bb0e48267188d72936c1) by Sorin Sbarnea). [Issue mkdocstrings/mkdocstrings#510](https://github.com/mkdocstrings/mkdocstrings/issues/510), [PR #49](https://github.com/mkdocstrings/python/pull/49) + +### Bug Fixes + +- Prevent alias resolution error when searching for anchors ([a190e2c](https://github.com/mkdocstrings/python/commit/a190e2c4a752e74a05ad03702837a0914c198742) by Timothée Mazzucotelli). [Issue #64](https://github.com/mkdocstrings/python/issues/64) + +### Code Refactoring + +- Support Griffe 0.26 ([075735c](https://github.com/mkdocstrings/python/commit/075735ce8d86921fbf092d7ad1d009bbb3a2e0bb) by Timothée Mazzucotelli). +- Log (debug) unresolved aliases ([9164742](https://github.com/mkdocstrings/python/commit/9164742f87362e8241dea11bec0fd96f6b9d9dda) by Timothée Mazzucotelli). + ## [0.8.3](https://github.com/mkdocstrings/python/releases/tag/0.8.3) - 2023-01-04 [Compare with 0.8.2](https://github.com/mkdocstrings/python/compare/0.8.2...0.8.3) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 35f1f538..fe3eefbf 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,73 +2,132 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, 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. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* 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 electronic - address, without explicit permission +* 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 -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +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. -Project maintainers 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, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +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 by contacting the project team at pawamoy@pm.me. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +pawamoy@pm.me. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba0c5d2b..488292a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,14 +19,14 @@ make setup > you'll need to install > [PDM](https://github.com/pdm-project/pdm) > manually. -> +> > You can install it with: -> +> > ```bash > python3 -m pip install --user pipx > pipx install pdm > ``` -> +> > Now you can try running `make setup` again, > or simply `pdm install`. @@ -75,8 +75,9 @@ Don't bother updating the changelog, we will take care of this. ## Commit message convention -Commits messages must follow the -[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message): +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 @@ -84,34 +85,52 @@ Commits messages must follow the [Body] ``` +**Subject and body must be valid Markdown.** +Subject must have proper casing (uppercase for first letter +if it makes sense), but no dot at the end, and no punctuation +in general. + Scope and body are optional. Type can be: - `build`: About packaging, building wheels, etc. - `chore`: About packaging or repo/files management. - `ci`: About Continuous Integration. +- `deps`: Dependencies update. - `docs`: About documentation. - `feat`: New feature. - `fix`: Bug fix. - `perf`: About performance. -- `refactor`: Changes which are not features nor bug fixes. +- `refactor`: Changes that are not features or bug fixes. - `style`: A change in code style/format. - `tests`: About tests. -**Subject (and body) must be valid Markdown.** -If you write a body, please add issues references at the end: +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. -References: #10, #11. -Fixes #15. +Issue #10: https://github.com/namespace/project/issues/10 +Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15 ``` +These "trailers" must appear at the end of the body, +without any blank lines between them. The trailer title +can contain any character except colons `:`. +We expect a full URI for each trailer, not just GitHub autolinks +(for example, full GitHub URLs for commits and issues, +not the hash or the #issue-number). + +We do not enforce a line length on commit messages summary and body, +but please avoid very long summaries, and very long lines in the body, +unless they are part of code blocks that must not be wrapped. + ## Pull requests guidelines Link to any related issue in the Pull Request message. -During review, we recommend using fixups: +During the review, we recommend using fixups: ```bash # SHA is the SHA of the commit you want to fix diff --git a/Makefile b/Makefile index 58291575..b034ffff 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ setup: .PHONY: check check: - @bash scripts/multirun.sh duty check-quality check-types check-docs + @pdm multirun duty check-quality check-types check-docs @$(DUTY) check-dependencies .PHONY: $(BASIC_DUTIES) @@ -50,4 +50,4 @@ $(BASIC_DUTIES): .PHONY: $(QUALITY_DUTIES) $(QUALITY_DUTIES): - @bash scripts/multirun.sh duty $@ $(call args,$@) + @pdm multirun duty $@ $(call args,$@) diff --git a/README.md b/README.md index f446ee8a..b59516ef 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ dependencies = [ ] ``` -You can also explicitely depend on the handler: +You can also explicitly depend on the handler: ```toml title="pyproject.toml" # PEP 621 dependencies declaration @@ -59,11 +59,11 @@ dependencies = [ [Griffe](https://github.com/mkdocstrings/griffe). - **Support for type annotations:** Griffe collects your type annotations and *mkdocstrings* uses them - to display parameters types or return types. It is even able to automatically add cross-references - to other objects from your API, from the standard library or from third-party libraries! + to display parameter types or return types. It is even able to automatically add cross-references + to other objects from your API, from the standard library or third-party libraries! See [how to load inventories](https://mkdocstrings.github.io/usage/#cross-references-to-other-projects-inventories) to enable it. -- **Recursive documentation of Python objects:** just use the module dotted-path as identifier, and you get the full +- **Recursive documentation of Python objects:** just use the module dotted-path as an identifier, and you get the full module docs. You don't need to inject documentation for each class, function, etc. - **Support for documented attributes:** attributes (variables) followed by a docstring (triple-quoted string) will @@ -77,7 +77,7 @@ dependencies = [ *We do not support nested admonitions in docstrings!* - **Every object has a TOC entry:** we render a heading for each object, meaning *MkDocs* picks them into the Table - of Contents, which is nicely display by the Material theme. Thanks to *mkdocstrings* cross-reference ability, + of Contents, which is nicely displayed by the Material theme. Thanks to *mkdocstrings* cross-reference ability, you can reference other objects within your docstrings, with the classic Markdown syntax: `[this object][package.module.object]` or directly with `[package.module.object][]` diff --git a/config/black.toml b/config/black.toml new file mode 100644 index 00000000..d24affe5 --- /dev/null +++ b/config/black.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 120 +exclude = "tests/fixtures" diff --git a/config/coverage.ini b/config/coverage.ini index 418d1bb9..19b34d9b 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -11,11 +11,15 @@ equivalent = __pypackages__/ [coverage:report] +include_namespace_packages = true precision = 2 omit = src/*/__init__.py src/*/__main__.py tests/__init__.py +exclude_lines = + pragma: no cover + if TYPE_CHECKING [coverage:json] output = htmlcov/coverage.json diff --git a/config/flake8.ini b/config/flake8.ini deleted file mode 100644 index 4126bd51..00000000 --- a/config/flake8.ini +++ /dev/null @@ -1,108 +0,0 @@ -[flake8] -exclude = fixtures,site -max-line-length = 132 -docstring-convention = google -ban-relative-imports = true -ignore = - # redundant with W0622 (builtin override), which is more precise about line number - A001 - # missing docstring in magic method - D105 - # multi-line docstring summary should start at the first line - D212 - # does not support Parameters sections - D417 - # whitespace before ':' (incompatible with Black) - E203 - # redundant with E0602 (undefined variable) - F821 - # black already deals with quoting - Q000 - # use of assert - S101 - # we are not parsing XML - S405 - # line break before binary operator (incompatible with Black) - W503 - # two-lowercase-letters variable DO conform to snake_case naming style - C0103 - # redundant with D102 (missing docstring) - C0116 - # line too long - C0301 - # too many instance attributes - R0902 - # too few public methods - R0903 - # too many public methods - R0904 - # too many branches - R0912 - # too many methods - R0913 - # too many local variables - R0914 - # too many statements - R0915 - # redundant with F401 (unused import) - W0611 - # lazy formatting for logging calls - W1203 - # short name - VNE001 - # f-strings - WPS305 - # common variable names (too annoying) - WPS110 - # redundant with W0622 (builtin override), which is more precise about line number - WPS125 - # too many imports - WPS201 - # too many module members - WPS202 - # overused expression - WPS204 - # too many local variables - WPS210 - # too many arguments - WPS211 - # too many expressions - WPS213 - # too many methods - WPS214 - # too deep nesting - WPS220 - # high Jones complexity - WPS221 - # too many elif branches - WPS223 - # string over-use: can't disable it per file? - WPS226 - # too many public instance attributes - WPS230 - # too complex f-string - WPS237 - # too cumbersome, asks to write class A(object) - WPS306 - # multi-line parameters (incompatible with Black) - WPS317 - # multi-line strings (incompatible with attributes docstrings) - WPS322 - # implicit string concatenation - WPS326 - # explicit string concatenation - WPS336 - # noqa overuse - WPS402 - # __init__ modules with logic - WPS412 - # print statements - WPS421 - # statement with no effect (not compatible with attribute docstrings) - WPS428 - # redundant with C0415 (not top-level import) - WPS433 - # multiline attribute docstring - WPS462 - # implicit dict.get usage (generally false-positive) - WPS529 diff --git a/config/pytest.ini b/config/pytest.ini index ad72bbe6..5a493959 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -14,3 +14,9 @@ addopts = --cov-config config/coverage.ini testpaths = tests + +# action:message_regex:warning_class:module_regex:line +filterwarnings = + error + # TODO: remove once pytest-xdist 4 is released + ignore:.*rsyncdir:DeprecationWarning:xdist diff --git a/config/ruff.toml b/config/ruff.toml new file mode 100644 index 00000000..55bab1a8 --- /dev/null +++ b/config/ruff.toml @@ -0,0 +1,100 @@ +target-version = "py37" +line-length = 132 +exclude = [ + "fixtures", + "site", +] +select = [ + "A", + "ANN", + "ARG", + "B", + "BLE", + "C", + "C4", + "COM", + "D", + "DTZ", + "E", + "ERA", + "EXE", + "F", + "FBT", + "G", + "I", + "ICN", + "INP", + "ISC", + "N", + "PGH", + "PIE", + "PL", + "PLC", + "PLE", + "PLR", + "PLW", + "PT", + "PYI", + "Q", + "RUF", + "RSE", + "RET", + "S", + "SIM", + "SLF", + "T", + "T10", + "T20", + "TCH", + "TID", + "TRY", + "UP", + "W", + "YTT", +] +ignore = [ + "A001", # Variable is shadowing a Python builtin + "ANN101", # Missing type annotation for self + "ANN102", # Missing type annotation for cls + "ANN204", # Missing return type annotation for special method __str__ + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed + "ARG005", # Unused lambda argument + "C901", # Too complex + "D105", # Missing docstring in magic method + "D417", # Missing argument description in the docstring + "E501", # Line too long + "G004", # Logging statement uses f-string + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments to function call + "PLR0915", # Too many statements + "SLF001", # Private member accessed + "TRY003", # Avoid specifying long messages outside the exception class +] + +[per-file-ignores] +"src/*/cli.py" = [ + "T201", # Print statement +] +"scripts/*.py" = [ + "INP001", # File is part of an implicit namespace package + "T201", # Print statement +] +"tests/*.py" = [ + "ARG005", # Unused lambda argument + "FBT001", # Boolean positional arg in function definition + "PLR2004", # Magic value used in comparison + "S101", # Use of assert detected +] + +[flake8-quotes] +docstring-quotes = "double" + +[flake8-tidy-imports] +ban-relative-imports = "all" + +[isort] +known-first-party = ["mkdocstrings_handlers"] + +[pydocstyle] +convention = "google" diff --git a/docs/credits.md b/docs/credits.md index 02e1dd81..9db45873 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -1,3 +1,8 @@ +--- +hide: +- toc +--- + ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` diff --git a/docs/customization.md b/docs/customization.md index d1d02cca..5e729675 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -124,7 +124,7 @@ See them [in the repository](https://github.com/mkdocstrings/python/tree/master/ See the general *mkdocstrings* documentation to learn how to override them: https://mkdocstrings.github.io/theming/#templates. In preparation for Jinja2 blocks, which will improve customization, -each one of these templates extends in fact a base version in `theme/_base`. Example: +each one of these templates extends a base version in `theme/_base`. Example: ```html+jinja title="theme/docstring/admonition.html" {% extends "_base/docstring/admonition.html" %} @@ -139,7 +139,7 @@ each one of these templates extends in fact a base version in `theme/_base`. Exa ``` It means you will be able to customize only *parts* of a template -without having to fully copy-paste it in your project: +without having to fully copy-paste it into your project: ```jinja title="templates/theme/docstring.html" {% extends "_base/docstring.html" %} diff --git a/docs/schema.json b/docs/schema.json index a68b9041..2d2a29f7 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -26,6 +26,14 @@ "base_url": { "title": "Base URL used to build references URLs.", "type": "string" + }, + "domains": { + "title": "Domains to import from the inventory.", + "description": "If not defined it will only import 'py' domain.", + "type": "array", + "items": { + "type": "string" + } } } } @@ -41,6 +49,12 @@ "format": "path" } }, + "load_external_modules": { + "title": "Load external modules to resolve aliases.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#global-only-options", + "type": "boolean", + "default": false + }, "options": { "title": "Options for collecting and rendering objects.", "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", @@ -132,8 +146,68 @@ "type": "boolean", "default": false }, + "show_docstring_attributes": { + "title": "Whether to display the \"Attributes\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_description": { + "title": "Whether to display the textual block (including admonitions) in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_examples": { + "title": "Whether to display the \"Examples\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_other_parameters": { + "title": "Whether to display the \"Other Parameters\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_parameters": { + "title": "Whether to display the \"Parameters\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_raises": { + "title": "Whether to display the \"Raises\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_receives": { + "title": "Whether to display the \"Receives\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_returns": { + "title": "Whether to display the \"Returns\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_warns": { + "title": "Whether to display the \"Warns\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, + "show_docstring_yields": { + "title": "Whether to display the \"Yields\" section in the object's docstring.", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "boolean", + "default": true + }, "show_source": { - "title": "Show the source code of this object..", + "title": "Show the source code of this object.", "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", "type": "boolean", "default": true @@ -194,6 +268,14 @@ "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", "enum": ["brief", "source"], "default": "brief" + }, + "preload_modules": { + "title": "Pre-load modules. It permits to resolve aliases pointing to these modules (packages), and therefore render members of an object that are external to the given object (originating from another package).", + "markdownDescription": "https://mkdocstrings.github.io/python/usage/#globallocal-options", + "type": "array", + "items": { + "type":"string" + } } }, "additionalProperties": false @@ -203,4 +285,4 @@ } }, "additionalProperties": false -} \ No newline at end of file +} diff --git a/docs/usage.md b/docs/usage.md index de28ca16..332a72ad 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -52,6 +52,15 @@ Some options are **global only**, and go directly under the handler's name. More details at [Finding modules](#finding-modules). +- `load_external_modules`: + this option allows resolving aliases to any external module. + Enabling this option will tell handler that when it encounters an import that is made public + through the `__all__` variable, it will resolve it recursively to *any* module. + **Use with caution:** this can load a *lot* of modules, slowing down your build + or triggering errors that we do not yet handle. + **We recommend using the `preload_modules` option instead**, + which acts as an include-list rather than as include-all. + ## Global/local options The other options can be used both globally *and* locally, under the `options` key. @@ -74,7 +83,7 @@ plugins: do_something: false ``` -These options affect how the documentation is collected from sources and renderered: +These options affect how the documentation is collected from sources and rendered: headings, members, docstrings, etc. ::: mkdocstrings_handlers.python.handler.PythonHandler.default_config @@ -202,7 +211,7 @@ TIP: **This is the recommended method.** ``` Except for case 1, which is supported by default, **we strongly recommend -to set the path to your packages using this option, even if it works without it** +setting the path to your packages using this option, even if it works without it** (for example because your project manager automatically adds `src` to PYTHONPATH), to make sure anyone can build your docs from any location on their filesystem. @@ -211,7 +220,7 @@ to make sure anyone can build your docs from any location on their filesystem. WARNING: **This method has limitations.** This method might work for you, with your current setup, but not for others trying your build your docs with their own setup/environment. -We recommend to use the [`paths` method](#using-the-paths-option) instead. +We recommend using the [`paths` method](#using-the-paths-option) instead. You can take advantage of the usual Python loading mechanisms. In Bash and other shells, you can run your command like this @@ -270,7 +279,7 @@ In Bash and other shells, you can run your command like this WARNING: **This method has limitations.** This method might work for you, with your current setup, but not for others trying your build your docs with their own setup/environment. -We recommend to use the [`paths` method](#using-the-paths-option) instead. +We recommend using the [`paths` method](#using-the-paths-option) instead. Install your package in the current environment, and run MkDocs: diff --git a/duties.py b/duties.py index 1b64bcda..51cef860 100644 --- a/duties.py +++ b/duties.py @@ -1,153 +1,90 @@ """Development tasks.""" -import importlib +from __future__ import annotations + import os -import re import sys -from io import StringIO from pathlib import Path -from typing import List, Optional, Pattern -from urllib.request import urlopen +from typing import TYPE_CHECKING from duty import duty +from duty.callables import black, blacken_docs, coverage, lazy, mkdocs, mypy, pytest, ruff, safety + +if TYPE_CHECKING: + from duty.context import Context -PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "docs")) +PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) TESTING = os.environ.get("TESTING", "0") in {"1", "true"} CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI +MULTIRUN = os.environ.get("PDM_MULTIRUN", "0") == "1" -def _latest(lines: List[str], regex: Pattern) -> Optional[str]: - for line in lines: - match = regex.search(line) - if match: - return match.groupdict()["version"] - return None - - -def _unreleased(versions, last_release): - for index, version in enumerate(versions): - if version.tag == last_release: - return versions[:index] - return versions - - -def update_changelog( - inplace_file: str, - marker: str, - version_regex: str, - template_url: str, -) -> None: - """ - Update the given changelog file in place. - - Arguments: - inplace_file: The file to update in-place. - marker: The line after which to insert new contents. - version_regex: A regular expression to find currently documented versions in the file. - template_url: The URL to the Jinja template used to render contents. - """ - from git_changelog.build import Changelog - from git_changelog.commit import AngularStyle - from jinja2.sandbox import SandboxedEnvironment - - AngularStyle.DEFAULT_RENDER.insert(0, AngularStyle.TYPES["build"]) - env = SandboxedEnvironment(autoescape=False) - template_text = urlopen(template_url).read().decode("utf8") # noqa: S310 - template = env.from_string(template_text) - changelog = Changelog(".", style="angular") - - if len(changelog.versions_list) == 1: - last_version = changelog.versions_list[0] - if last_version.planned_tag is None: - planned_tag = "0.1.0" - last_version.tag = planned_tag - last_version.url += planned_tag - last_version.compare_url = last_version.compare_url.replace("HEAD", planned_tag) - - with open(inplace_file, "r") as changelog_file: - lines = changelog_file.read().splitlines() - - last_released = _latest(lines, re.compile(version_regex)) - if last_released: - changelog.versions_list = _unreleased(changelog.versions_list, last_released) - rendered = template.render(changelog=changelog, inplace=True) - lines[lines.index(marker)] = rendered - - with open(inplace_file, "w") as changelog_file: # noqa: WPS440 - changelog_file.write("\n".join(lines).rstrip("\n") + "\n") +def pyprefix(title: str) -> str: # noqa: D103 + if MULTIRUN: + prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" + return f"{prefix:14}{title}" + return title @duty -def changelog(ctx): - """ - Update the changelog in-place with latest commits. +def changelog(ctx: Context) -> None: + """Update the changelog in-place with latest commits. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ - commit = "166758a98d5e544aaa94fda698128e00733497f4" - template_url = f"https://raw.githubusercontent.com/pawamoy/jinja-templates/{commit}/keepachangelog.md" + from git_changelog.cli import build_and_render + + git_changelog = lazy("git_changelog")(build_and_render) ctx.run( - update_changelog, - kwargs={ - "inplace_file": "CHANGELOG.md", - "marker": "", - "version_regex": r"^## \[v?(?P[^\]]+)", - "template_url": template_url, - }, + git_changelog( + repository=".", + output="CHANGELOG.md", + convention="angular", + template="keepachangelog", + parse_trailers=True, + parse_refs=False, + sections=("build", "deps", "feat", "fix", "refactor"), + bump_latest=True, + in_place=True, + ), title="Updating changelog", - pty=PTY, ) @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies"]) -def check(ctx): - """ - Check it all! +def check(ctx: Context) -> None: # noqa: ARG001 + """Check it all! - Arguments: + Parameters: ctx: The context instance (passed automatically). """ @duty -def check_quality(ctx, files=PY_SRC): - """ - Check the code quality. +def check_quality(ctx: Context) -> None: + """Check the code quality. - Arguments: + Parameters: ctx: The context instance (passed automatically). - files: The files to check. """ - ctx.run(f"flake8 --config=config/flake8.ini {files}", title="Checking code quality", pty=PTY) + ctx.run( + ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), + title=pyprefix("Checking code quality"), + ) @duty -def check_dependencies(ctx): - """ - Check for vulnerabilities in dependencies. +def check_dependencies(ctx: Context) -> None: + """Check for vulnerabilities in dependencies. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ - # undo possible patching - # see https://github.com/pyupio/safety/issues/348 - for module in sys.modules: # noqa: WPS528 - if module.startswith("safety.") or module == "safety": - del sys.modules[module] # noqa: WPS420 - - importlib.invalidate_caches() - - # reload original, unpatched safety - from safety.formatter import SafetyFormatter - from safety.safety import calculate_remediations - from safety.safety import check as safety_check - from safety.util import read_requirements - # retrieve the list of dependencies requirements = ctx.run( ["pdm", "export", "-f", "requirements", "--without-hashes"], @@ -155,57 +92,40 @@ def check_dependencies(ctx): allow_overrides=False, ) - # check using safety as a library - def safety(): # noqa: WPS430 - packages = list(read_requirements(StringIO(requirements))) - vulns, db_full = safety_check(packages=packages, ignore_vulns="") - remediations = calculate_remediations(vulns, db_full) - output_report = SafetyFormatter("text").render_vulnerabilities( - announcements=[], - vulnerabilities=vulns, - remediations=remediations, - full=True, - packages=packages, - ) - if vulns: - print(output_report) - return False - return True - - ctx.run(safety, title="Checking dependencies") + ctx.run(safety.check(requirements), title="Checking dependencies") @duty -def check_docs(ctx): - """ - Check if the documentation builds correctly. +def check_docs(ctx: Context) -> None: + """Check if the documentation builds correctly. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) - ctx.run("mkdocs build -s", title="Building documentation", pty=PTY) + ctx.run(mkdocs.build(strict=True), title=pyprefix("Building documentation")) -@duty # noqa: WPS231 -def check_types(ctx): # noqa: WPS231 - """ - Check that the code is correctly typed. +@duty +def check_types(ctx: Context) -> None: + """Check that the code is correctly typed. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ os.environ["MYPYPATH"] = "src" - ctx.run(f"mypy --config-file config/mypy.ini {PY_SRC}", title="Type-checking", pty=PTY) + ctx.run( + mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), + title=pyprefix("Type-checking"), + ) @duty(silent=True) -def clean(ctx): - """ - Delete temporary files. +def clean(ctx: Context) -> None: + """Delete temporary files. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ ctx.run("rm -rf .coverage*") @@ -222,63 +142,65 @@ def clean(ctx): @duty -def docs(ctx): - """ - Build the documentation locally. +def docs(ctx: Context) -> None: + """Build the documentation locally. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ - ctx.run("mkdocs build", title="Building documentation") + ctx.run(mkdocs.build, title="Building documentation") @duty -def docs_serve(ctx, host="127.0.0.1", port=8000): - """ - Serve the documentation (localhost:8000). +def docs_serve(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: + """Serve the documentation (localhost:8000). - Arguments: + Parameters: ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ - ctx.run(f"mkdocs serve -a {host}:{port}", title="Serving documentation", capture=False) + ctx.run( + mkdocs.serve(dev_addr=f"{host}:{port}"), + title="Serving documentation", + capture=False, + ) @duty -def docs_deploy(ctx): - """ - Deploy the documentation on GitHub pages. +def docs_deploy(ctx: Context) -> None: + """Deploy the documentation on GitHub pages. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ - ctx.run("mkdocs gh-deploy", title="Deploying documentation") + ctx.run(mkdocs.gh_deploy, title="Deploying documentation") @duty -def format(ctx): - """ - Run formatting tools on the code. +def format(ctx: Context) -> None: + """Run formatting tools on the code. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ ctx.run( - f"autoflake -ir --exclude tests/fixtures --remove-all-unused-imports {PY_SRC}", - title="Removing unused imports", - pty=PTY, + ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), + title="Auto-fixing code", + ) + ctx.run(black.run(*PY_SRC_LIST, config="config/black.toml"), title="Formatting code") + ctx.run( + blacken_docs.run(*PY_SRC_LIST, "docs", exts=["py", "md"], line_length=120), + title="Formatting docs", + nofail=True, ) - ctx.run(f"isort {PY_SRC}", title="Ordering imports", pty=PTY) - ctx.run(f"black {PY_SRC}", title="Formatting code", pty=PTY) @duty -def release(ctx, version): - """ - Release a new Python package. +def release(ctx: Context, version: str) -> None: + """Release a new Python package. - Arguments: + Parameters: ctx: The context instance (passed automatically). version: The new version number to use. """ @@ -293,32 +215,29 @@ def release(ctx, version): docs_deploy.run() -@duty(silent=True) -def coverage(ctx): - """ - Report coverage as text and HTML. +@duty(silent=True, aliases=["coverage"]) +def cov(ctx: Context) -> None: + """Report coverage as text and HTML. - Arguments: + Parameters: ctx: The context instance (passed automatically). """ - ctx.run("coverage combine", nofail=True) - ctx.run("coverage report --rcfile=config/coverage.ini", capture=False) - ctx.run("coverage html --rcfile=config/coverage.ini") + ctx.run(coverage.combine, nofail=True) + ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) + ctx.run(coverage.html(rcfile="config/coverage.ini")) @duty -def test(ctx, match: str = ""): - """ - Run the test suite. +def test(ctx: Context, match: str = "") -> None: + """Run the test suite. - Arguments: + Parameters: ctx: The context instance (passed automatically). match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( - ["pytest", "-c", "config/pytest.ini", "-n", "auto", "-k", match, "tests"], - title="Running tests", - pty=PTY, + pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match), + title=pyprefix("Running tests"), ) diff --git a/mkdocs.yml b/mkdocs.yml index 6b851eed..f07a73d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,8 @@ theme: - navigation.tabs - navigation.tabs.sticky - navigation.top + - search.highlight + - search.suggest palette: - media: "(prefers-color-scheme: light)" scheme: default @@ -69,9 +71,9 @@ plugins: - markdown-exec - gen-files: scripts: - - docs/gen_ref_nav.py + - scripts/gen_ref_nav.py - literate-nav: - nav_file: SUMMARY.md + nav_file: SUMMARY.txt - coverage - section-index - mkdocstrings: @@ -95,5 +97,7 @@ extra: social: - icon: fontawesome/brands/github link: https://github.com/pawamoy + - icon: fontawesome/brands/mastodon + link: https://fosstodon.org/@pawamoy - icon: fontawesome/brands/twitter link: https://twitter.com/pawamoy diff --git a/pyproject.toml b/pyproject.toml index 914bd58b..673d3c8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "mkdocstrings>=0.19", + "mkdocstrings>=0.20", "griffe>=0.24", ] @@ -52,7 +52,7 @@ includes = ["src/mkdocstrings_handlers"] editable-backend = "editables" [tool.pdm.dev-dependencies] -duty = ["duty>=0.7"] +duty = ["duty>=0.8"] docs = [ "mkdocs>=1.3", "mkdocs-coverage>=0.2", @@ -64,32 +64,13 @@ docs = [ "markdown-exec>=0.5", "toml>=0.10", ] -format = [ - "autoflake>=1.4", - "black>=21.10b0", - "isort>=5.10", -] maintain = [ - "git-changelog>=0.4", + "black>=23.1", + "blacken-docs>=1.13", + "git-changelog>=1.0", ] quality = [ - # TODO: remove once importlib-metadata version conflict is resolved - "importlib-metadata<5; python_version < '3.8'", - "flake8>=4; python_version >= '3.8'", - - "darglint>=1.8", - "flake8-bandit>=2.1", - "flake8-black>=0.2", - "flake8-bugbear>=21.9", - "flake8-builtins>=1.5", - "flake8-comprehensions>=3.7", - "flake8-docstrings>=1.6", - "flake8-pytest-style>=1.5", - "flake8-string-format>=0.3", - "flake8-tidy-imports>=4.5", - "flake8-variables-names>=0.0", - "pep8-naming>=0.12", - "wps-light>=0.15", + "ruff>=0.0.246", ] tests = [ "pytest>=6.2", @@ -98,22 +79,8 @@ tests = [ "pytest-xdist>=2.4", ] typing = [ - "mypy>=0.910", + "mypy>=0.911", "types-markdown>=3.3", "types-toml>=0.10", ] security = ["safety>=2"] - -[tool.black] -line-length = 120 -exclude = "tests/fixtures" - -[tool.isort] -line_length = 120 -not_skip = "__init__.py" -multi_line_output = 3 -force_single_line = false -balanced_wrapping = true -default_section = "THIRDPARTY" -known_first_party = "mkdocstrings_handlers" -include_trailing_comma = true diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 10f5647d..7f59f8f9 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -1,16 +1,22 @@ +"""Script to generate the project's credits.""" + +from __future__ import annotations + import re +import sys from itertools import chain from pathlib import Path from textwrap import dedent +from typing import Mapping, cast import toml from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment -try: - from importlib.metadata import metadata, PackageNotFoundError -except ImportError: - from importlib_metadata import metadata, PackageNotFoundError +if sys.version_info < (3, 8): + from importlib_metadata import PackageNotFoundError, metadata +else: + from importlib.metadata import PackageNotFoundError, metadata project_dir = Path(".") pyproject = toml.load(project_dir / "pyproject.toml") @@ -21,31 +27,33 @@ project_name = project["name"] regex = re.compile(r"(?P[\w.-]+)(?P.*)$") -def get_license(pkg_name): + +def _get_license(pkg_name: str) -> str: try: data = metadata(pkg_name) except PackageNotFoundError: return "?" - license = data.get("License", "").strip() - multiple_lines = bool(license.count("\n")) + license_name = cast(dict, data).get("License", "").strip() + multiple_lines = bool(license_name.count("\n")) # TODO: remove author logic once all my packages licenses are fixed author = "" - if multiple_lines or not license or license == "UNKNOWN": - for header, value in data.items(): + if multiple_lines or not license_name or license_name == "UNKNOWN": + for header, value in cast(dict, data).items(): if header == "Classifier" and value.startswith("License ::"): - license = value.rsplit("::", 1)[1].strip() + license_name = value.rsplit("::", 1)[1].strip() elif header == "Author-email": author = value - if license == "Other/Proprietary License" and "pawamoy" in author: - license = "ISC" - return license or "?" + if license_name == "Other/Proprietary License" and "pawamoy" in author: + license_name = "ISC" + return license_name or "?" -def get_deps(base_deps): + +def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: deps = {} for dep in base_deps: - parsed = regex.match(dep).groupdict() + parsed = regex.match(dep).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() - deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True while again: @@ -53,58 +61,63 @@ def get_deps(base_deps): for pkg_name in lock_pkgs: if pkg_name in deps: for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): - parsed = regex.match(pkg_dependency).groupdict() + parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] dep_name = parsed["dist"].lower() - if dep_name not in deps: - deps[dep_name] = {"license": get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + if dep_name not in deps and dep_name != project["name"]: + deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} again = True return deps -dev_dependencies = get_deps(chain(*pdm.get("dev-dependencies", {}).values())) -prod_dependencies = get_deps( - chain( - project.get("dependencies", []), - chain(*project.get("optional-dependencies", {}).values()), + +def _render_credits() -> str: + dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] + prod_dependencies = _get_deps( + chain( # type: ignore[arg-type] + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ), ) -) - -template_data = { - "project_name": project_name, - "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), - "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), - "more_credits": "http://pawamoy.github.io/credits/", -} -template_text = dedent( - """ - These projects were used to build `{{ project_name }}`. **Thank you!** - - [`python`](https://www.python.org/) | - [`pdm`](https://pdm.fming.dev/) | - [`copier-pdm`](https://github.com/pawamoy/copier-pdm) - - {% macro dep_line(dep) -%} - [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} - {%- endmacro %} - - ### Runtime dependencies - - Project | Summary | Version (accepted) | Version (last resolved) | License - ------- | ------- | ------------------ | ----------------------- | ------- - {% for dep in prod_dependencies -%} - {{ dep_line(dep) }} - {% endfor %} - - ### Development dependencies - - Project | Summary | Version (accepted) | Version (last resolved) | License - ------- | ------- | ------------------ | ----------------------- | ------- - {% for dep in dev_dependencies -%} - {{ dep_line(dep) }} - {% endfor %} - - {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} - """ -) -jinja_env = SandboxedEnvironment(undefined=StrictUndefined) -print(jinja_env.from_string(template_text).render(**template_data)) + + template_data = { + "project_name": project_name, + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), + "more_credits": "http://pawamoy.github.io/credits/", + } + template_text = dedent( + """ + These projects were used to build `{{ project_name }}`. **Thank you!** + + [`python`](https://www.python.org/) | + [`pdm`](https://pdm.fming.dev/) | + [`copier-pdm`](https://github.com/pawamoy/copier-pdm) + + {% macro dep_line(dep) -%} + [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + {%- endmacro %} + + ### Runtime dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in prod_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + ### Development dependencies + + Project | Summary | Version (accepted) | Version (last resolved) | License + ------- | ------- | ------------------ | ----------------------- | ------- + {% for dep in dev_dependencies -%} + {{ dep_line(dep) }} + {% endfor %} + + {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} + """, + ) + jinja_env = SandboxedEnvironment(undefined=StrictUndefined) + return jinja_env.from_string(template_text).render(**template_data) + + +print(_render_credits()) diff --git a/docs/gen_ref_nav.py b/scripts/gen_ref_nav.py old mode 100755 new mode 100644 similarity index 88% rename from docs/gen_ref_nav.py rename to scripts/gen_ref_nav.py index 14f0f4ad..97d8b5a7 --- a/docs/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -17,7 +17,7 @@ parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1] == "__main__": + elif parts[-1].startswith("_"): continue nav[parts] = doc_path.as_posix() @@ -28,5 +28,5 @@ mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: +with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/multirun.sh b/scripts/multirun.sh deleted file mode 100755 index a55d1746..00000000 --- a/scripts/multirun.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -set -e - -PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}" - -restore_previous_python_version() { - if pdm use -f "$1" &>/dev/null; then - echo "> Restored previous Python version: ${1##*/}" - fi -} - -if [ -n "${PYTHON_VERSIONS}" ]; then - old_python_version="$(pdm config python.path)" - echo "> Currently selected Python version: ${old_python_version##*/}" - trap "restore_previous_python_version ${old_python_version}" EXIT - for python_version in ${PYTHON_VERSIONS}; do - if pdm use -f "python${python_version}" &>/dev/null; then - echo "> pdm run $@ (python${python_version})" - pdm run "$@" - else - echo "> pdm use -f python${python_version}: Python interpreter not available?" >&2 - fi - done -else - pdm run "$@" -fi diff --git a/scripts/setup.sh b/scripts/setup.sh index 188eaebc..9e7ab1ff 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -3,36 +3,18 @@ set -e PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}" -install_with_pipx() { - if ! command -v "$1" &>/dev/null; then - if ! command -v pipx &>/dev/null; then - python3 -m pip install --user pipx - fi - pipx install "$1" +if ! command -v pdm &>/dev/null; then + if ! command -v pipx &>/dev/null; then + python3 -m pip install --user pipx fi -} - -install_with_pipx pdm - -restore_previous_python_version() { - if pdm use -f "$1" &>/dev/null; then - echo "> Restored previous Python version: ${1##*/}" - fi -} + pipx install pdm +fi +if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then + pipx inject pdm pdm-multirun +fi if [ -n "${PYTHON_VERSIONS}" ]; then - if old_python_version="$(pdm config python.path 2>/dev/null)"; then - echo "> Currently selected Python version: ${old_python_version##*/}" - trap "restore_previous_python_version ${old_python_version}" EXIT - fi - for python_version in ${PYTHON_VERSIONS}; do - if pdm use -f "python${python_version}" &>/dev/null; then - echo "> Using Python ${python_version} interpreter" - pdm install - else - echo "> pdm use -f python${python_version}: Python interpreter not available?" >&2 - fi - done + pdm multirun -vi ${PYTHON_VERSIONS// /,} pdm install else pdm install fi diff --git a/src/mkdocstrings_handlers/python/__init__.py b/src/mkdocstrings_handlers/python/__init__.py index 706d85ee..f93ab20e 100644 --- a/src/mkdocstrings_handlers/python/__init__.py +++ b/src/mkdocstrings_handlers/python/__init__.py @@ -2,7 +2,7 @@ from mkdocstrings_handlers.python.handler import get_handler -__all__ = ["get_handler"] # noqa: WPS410 +__all__ = ["get_handler"] # TODO: CSS classes everywhere in templates # TODO: name normalization (filenames, Jinja2 variables, HTML tags, CSS classes) diff --git a/src/mkdocstrings_handlers/python/handler.py b/src/mkdocstrings_handlers/python/handler.py index 940f2731..ffb645ea 100644 --- a/src/mkdocstrings_handlers/python/handler.py +++ b/src/mkdocstrings_handlers/python/handler.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import glob import os import posixpath @@ -9,15 +10,13 @@ import sys from collections import ChainMap from contextlib import suppress -from typing import Any, BinaryIO, Iterator, Optional, Tuple +from typing import TYPE_CHECKING, Any, BinaryIO, Iterator, Mapping -from griffe.agents.extensions import load_extensions from griffe.collections import LinesCollection, ModulesCollection from griffe.docstrings.parsers import Parser from griffe.exceptions import AliasResolutionError from griffe.loader import GriffeLoader from griffe.logger import patch_loggers -from markdown import Markdown from mkdocstrings.extension import PluginError from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem from mkdocstrings.inventory import Inventory @@ -25,14 +24,24 @@ from mkdocstrings_handlers.python import rendering +try: + from griffe.extensions import load_extensions +except ImportError: + # TODO: remove once support for Griffe 0.25 is dropped + from griffe.agents.extensions import load_extensions # type: ignore[no-redef] + +if TYPE_CHECKING: + from markdown import Markdown + + if sys.version_info >= (3, 11): from contextlib import chdir else: # TODO: remove once support for Python 3.10 is dropped from contextlib import contextmanager - @contextmanager # noqa: WPS440 - def chdir(path: str): # noqa: D103,WPS440 + @contextmanager + def chdir(path: str) -> Iterator[None]: # noqa: D103 old_wd = os.getcwd() os.chdir(path) try: @@ -78,6 +87,16 @@ class PythonHandler(BaseHandler): "separate_signature": False, "line_length": 60, "merge_init_into_class": False, + "show_docstring_attributes": True, + "show_docstring_description": True, + "show_docstring_examples": True, + "show_docstring_other_parameters": True, + "show_docstring_parameters": True, + "show_docstring_raises": True, + "show_docstring_receives": True, + "show_docstring_returns": True, + "show_docstring_warns": True, + "show_docstring_yields": True, "show_source": True, "show_bases": True, "show_submodules": False, @@ -88,6 +107,8 @@ class PythonHandler(BaseHandler): "members": None, "filters": ["!^_[^_]"], "annotations_path": "brief", + "preload_modules": None, + "load_external_modules": False, } """ Attributes: Headings options: @@ -118,6 +139,16 @@ class PythonHandler(BaseHandler): line_length (int): Maximum line length when formatting code/signatures. Default: `60`. merge_init_into_class (bool): Whether to merge the `__init__` method into the class' signature and docstring. Default: `False`. show_if_no_docstring (bool): Show the object heading even if it has no docstring or children with docstrings. Default: `False`. + show_docstring_attributes (bool): Whether to display the "Attributes" section in the object's docstring. Default: `True`. + show_docstring_description (bool): Whether to display the textual block (including admonitions) in the object's docstring. Default: `True`. + show_docstring_examples (bool): Whether to display the "Examples" section in the object's docstring. Default: `True`. + show_docstring_other_parameters (bool): Whether to display the "Other Parameters" section in the object's docstring. Default: `True`. + show_docstring_parameters (bool): Whether to display the "Parameters" section in the object's docstring. Default: `True`. + show_docstring_raises (bool): Whether to display the "Raises" section in the object's docstring. Default: `True`. + show_docstring_receives (bool): Whether to display the "Receives" section in the object's docstring. Default: `True`. + show_docstring_returns (bool): Whether to display the "Returns" section in the object's docstring. Default: `True`. + show_docstring_warns (bool): Whether to display the "Warns" section in the object's docstring. Default: `True`. + show_docstring_yields (bool): Whether to display the "Yields" section in the object's docstring. Default: `True`. Attributes: Signatures/annotations options: annotations_path (str): The verbosity for annotations path: `brief` (recommended), or `source` (as written in the source). Default: `"brief"`. @@ -129,10 +160,24 @@ class PythonHandler(BaseHandler): Attributes: Additional options: show_bases (bool): Show the base classes of a class. Default: `True`. show_source (bool): Show the source code of this object. Default: `True`. - """ # noqa: E501 + preload_modules (list[str] | None): 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. Default: `None`. + + """ def __init__( - self, *args: Any, config_file_path: str | None = None, paths: list[str] | None = None, **kwargs: Any + self, + *args: Any, + config_file_path: str | None = None, + paths: list[str] | None = None, + **kwargs: Any, ) -> None: """Initialize the handler. @@ -153,9 +198,8 @@ def __init__( paths.append(os.path.dirname(config_file_path)) search_paths = [path for path in sys.path if path] # eliminate empty path for path in reversed(paths): - if not os.path.isabs(path): - if config_file_path: - path = os.path.abspath(os.path.join(os.path.dirname(config_file_path), path)) + if not os.path.isabs(path) and config_file_path: + path = os.path.abspath(os.path.join(os.path.dirname(config_file_path), path)) # noqa: PLW2901 if path not in search_paths: search_paths.insert(0, path) self._paths = search_paths @@ -167,9 +211,10 @@ def load_inventory( cls, in_file: BinaryIO, url: str, - base_url: Optional[str] = None, - **kwargs: Any, - ) -> Iterator[Tuple[str, str]]: + base_url: str | None = None, + domains: list[str] | None = None, + **kwargs: Any, # noqa: ARG003 + ) -> Iterator[tuple[str, str]]: """Yield items and their URLs from an inventory file streamed from `in_file`. This implements mkdocstrings' `load_inventory` "protocol" (see [`mkdocstrings.plugin`][mkdocstrings.plugin]). @@ -178,24 +223,28 @@ def load_inventory( in_file: The binary file-like object to read the inventory from. url: The URL that this file is being streamed from (used to guess `base_url`). base_url: The URL that this inventory's sub-paths are relative to. + domains: A list of domain strings to filter the inventory by, when not passed, "py" will be used. **kwargs: Ignore additional arguments passed from the config. Yields: Tuples of (item identifier, item URL). """ + domains = domains or ["py"] if base_url is None: base_url = posixpath.dirname(url) - for item in Inventory.parse_sphinx(in_file, domain_filter=("py",)).values(): # noqa: WPS526 + for item in Inventory.parse_sphinx(in_file, domain_filter=domains).values(): yield item.name, posixpath.join(base_url, item.uri) - def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102,WPS231 + def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: # noqa: D102 module_name = identifier.split(".", 1)[0] unknown_module = module_name not in self._modules_collection if config.get("fallback", False) and unknown_module: raise CollectionError("Not loading additional modules during fallback") - final_config = ChainMap(config, self.default_config) + # See: https://github.com/python/typeshed/issues/8430 + mutable_config = dict(copy.deepcopy(config)) + final_config = ChainMap(mutable_config, self.default_config) parser_name = final_config["docstring_style"] parser_options = final_config["docstring_options"] parser = parser_name and Parser(parser_name) @@ -210,19 +259,26 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102 lines_collection=self._lines_collection, ) try: + for pre_loaded_module in final_config.get("preload_modules") or []: + if pre_loaded_module not in self._modules_collection: + loader.load_module(pre_loaded_module) loader.load_module(module_name) except ImportError as error: raise CollectionError(str(error)) from error - - unresolved, iterations = loader.resolve_aliases(implicit=False, external=False) + unresolved, iterations = loader.resolve_aliases( + implicit=False, + external=final_config["load_external_modules"], + ) if unresolved: logger.debug(f"{len(unresolved)} aliases were still unresolved after {iterations} iterations") logger.debug(f"Unresolved aliases: {', '.join(sorted(unresolved))}") try: doc_object = self._modules_collection[identifier] - except KeyError as error: # noqa: WPS440 + except KeyError as error: raise CollectionError(f"{identifier} could not be found") from error + except AliasResolutionError as error: + raise CollectionError(str(error)) from error if not unknown_module: with suppress(AliasResolutionError): @@ -232,8 +288,10 @@ def collect(self, identifier: str, config: dict) -> CollectorItem: # noqa: D102 return doc_object - def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignore missing docstring) - final_config = ChainMap(config, self.default_config) + def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa: D102 (ignore missing docstring) + # See https://github.com/python/typeshed/issues/8430 + mutabled_config = dict(copy.deepcopy(config)) + final_config = ChainMap(mutabled_config, self.default_config) template = self.env.get_template(f"{data.kind.value}.html") @@ -243,9 +301,11 @@ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignor heading_level = final_config["heading_level"] try: final_config["members_order"] = rendering.Order(final_config["members_order"]) - except ValueError: + except ValueError as error: choices = "', '".join(item.value for item in rendering.Order) - raise PluginError(f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.") + raise PluginError( + f"Unknown members_order '{final_config['members_order']}', choose between '{choices}'.", + ) from error if final_config["filters"]: final_config["filters"] = [ @@ -276,11 +336,11 @@ def get_anchors(self, data: CollectorItem) -> list[str]: # noqa: D102 (ignore m def get_handler( - theme: str, # noqa: W0613 (unused argument config) - custom_templates: Optional[str] = None, + theme: str, + custom_templates: str | None = None, config_file_path: str | None = None, paths: list[str] | None = None, - **config: Any, + **config: Any, # noqa: ARG001 ) -> PythonHandler: """Simply return an instance of `PythonHandler`. diff --git a/src/mkdocstrings_handlers/python/rendering.py b/src/mkdocstrings_handlers/python/rendering.py index 8e5f7d85..9ee91769 100644 --- a/src/mkdocstrings_handlers/python/rendering.py +++ b/src/mkdocstrings_handlers/python/rendering.py @@ -6,13 +6,15 @@ import re import sys from functools import lru_cache -from typing import Any, Pattern, Sequence +from typing import TYPE_CHECKING, Any, Callable, Match, Pattern, Sequence -from griffe.dataclasses import Alias, Object from markupsafe import Markup -from mkdocstrings.handlers.base import CollectorItem from mkdocstrings.loggers import get_logger +if TYPE_CHECKING: + from griffe.dataclasses import Alias, Object + from mkdocstrings.handlers.base import CollectorItem + logger = get_logger(__name__) @@ -102,7 +104,7 @@ def do_order_members( return sorted(members, key=order_map[order]) -def do_crossref(path: str, brief: bool = True) -> Markup: +def do_crossref(path: str, *, brief: bool = True) -> Markup: """Filter to create cross-references. Parameters: @@ -118,7 +120,7 @@ def do_crossref(path: str, brief: bool = True) -> Markup: return Markup("{path}").format(full_path=full_path, path=path) -def do_multi_crossref(text: str, code: bool = True) -> Markup: +def do_multi_crossref(text: str, *, code: bool = True) -> Markup: """Filter to create cross-references. Parameters: @@ -131,8 +133,8 @@ def do_multi_crossref(text: str, code: bool = True) -> Markup: group_number = 0 variables = {} - def repl(match): # noqa: WPS430 - nonlocal group_number # noqa: WPS420 + def repl(match: Match) -> str: + nonlocal group_number group_number += 1 path = match.group() path_var = f"path{group_number}" @@ -145,7 +147,7 @@ def repl(match): # noqa: WPS430 return Markup(text).format(**variables) -def _keep_object(name, filters): +def _keep_object(name: str, filters: Sequence[tuple[Pattern, bool]]) -> bool: keep = None rules = set() for regex, exclude in filters: @@ -153,7 +155,7 @@ def _keep_object(name, filters): if regex.search(name): keep = not exclude if keep is None: - if rules == {False}: # noqa: WPS531 + if rules == {False}: # only included stuff, no match = reject return False # only excluded stuff, or included and excluded stuff, no match = keep @@ -163,7 +165,8 @@ def _keep_object(name, filters): def do_filter_objects( objects_dictionary: dict[str, Object | Alias], - filters: list[tuple[bool, Pattern]] | None = None, + *, + filters: Sequence[tuple[Pattern, bool]] | None = None, members_list: list[str] | None = None, keep_no_docstrings: bool = True, ) -> list[Object | Alias]: @@ -195,14 +198,14 @@ def do_filter_objects( @lru_cache(maxsize=1) -def _get_black_formatter(): +def _get_black_formatter() -> Callable[[str, int], str]: try: from black import Mode, format_str except ModuleNotFoundError: logger.warning("Formatting signatures requires Black to be installed.") return lambda text, _: text - def formatter(code, line_length): # noqa: WPS430 + def formatter(code: str, line_length: int) -> str: mode = Mode(line_length=line_length) return format_str(code, mode=mode) diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/children.html b/src/mkdocstrings_handlers/python/templates/material/_base/children.html index 71755ea7..9e27ed0f 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/children.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/children.html @@ -19,7 +19,7 @@ {% set extra_level = 0 %} {% endif %} - {% with attributes = obj.attributes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% with attributes = obj.attributes|filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring) %} {% if attributes %} {% if config.show_category_heading %} {% filter heading(heading_level, id=html_id ~ "-attributes") %}Attributes{% endfilter %} @@ -34,7 +34,7 @@ {% endif %} {% endwith %} - {% with classes = obj.classes|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% with classes = obj.classes|filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring) %} {% if classes %} {% if config.show_category_heading %} {% filter heading(heading_level, id=html_id ~ "-classes") %}Classes{% endfilter %} @@ -49,7 +49,7 @@ {% endif %} {% endwith %} - {% with functions = obj.functions|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% with functions = obj.functions|filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring) %} {% if functions %} {% if config.show_category_heading %} {% filter heading(heading_level, id=html_id ~ "-functions") %}Functions{% endfilter %} @@ -67,7 +67,7 @@ {% endwith %} {% if config.show_submodules %} - {% with modules = obj.modules|filter_objects(config.filters, members_list, config.show_if_no_docstring) %} + {% with modules = obj.modules|filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring) %} {% if modules %} {% if config.show_category_heading %} {% filter heading(heading_level, id=html_id ~ "-modules") %}Modules{% endfilter %} @@ -88,7 +88,7 @@ {% else %} {% for child in obj.members| - filter_objects(config.filters, members_list, config.show_if_no_docstring)| + filter_objects(filters=config.filters, members_list=members_list, keep_no_docstrings=config.show_if_no_docstring)| order_members(config.members_order, members_list) %} {% if not (obj.kind.value == "class" and child.name == "__init__" and config.merge_init_into_class) %} diff --git a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html index bd1b6963..1f840771 100644 --- a/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html +++ b/src/mkdocstrings_handlers/python/templates/material/_base/docstring.html @@ -1,27 +1,27 @@ {% if docstring_sections %} {{ log.debug("Rendering docstring") }} {% for section in docstring_sections %} - {% if section.kind.value == "text" %} + {% if config.show_docstring_description and section.kind.value == "text" %} {{ section.value|convert_markdown(heading_level, html_id) }} - {% elif section.kind.value == "attributes" %} + {% elif config.show_docstring_attributes and section.kind.value == "attributes" %} {% include "docstring/attributes.html" with context %} - {% elif section.kind.value == "parameters" %} + {% elif config.show_docstring_parameters and section.kind.value == "parameters" %} {% include "docstring/parameters.html" with context %} - {% elif section.kind.value == "other parameters" %} + {% elif config.show_docstring_other_parameters and section.kind.value == "other parameters" %} {% include "docstring/other_parameters.html" with context %} - {% elif section.kind.value == "raises" %} + {% elif config.show_docstring_raises and section.kind.value == "raises" %} {% include "docstring/raises.html" with context %} - {% elif section.kind.value == "warns" %} - {% include "docstring/warns.html" with context %} - {% elif section.kind.value == "yields" %} + {% elif config.show_docstring_warns and section.kind.value == "warns" %} + {% include "docstring/warns.html" with context %} + {% elif config.show_docstring_yields and section.kind.value == "yields" %} {% include "docstring/yields.html" with context %} - {% elif section.kind.value == "receives" %} - {% include "docstring/receives.html" with context %} - {% elif section.kind.value == "returns" %} + {% elif config.show_docstring_receives and section.kind.value == "receives" %} + {% include "docstring/receives.html" with context %} + {% elif config.show_docstring_returns and section.kind.value == "returns" %} {% include "docstring/returns.html" with context %} - {% elif section.kind.value == "examples" %} + {% elif config.show_docstring_examples and section.kind.value == "examples" %} {% include "docstring/examples.html" with context %} - {% elif section.kind.value == "admonition" %} + {% elif config.show_docstring_description and section.kind.value == "admonition" %} {% include "docstring/admonition.html" with context %} {% endif %} {% endfor %} diff --git a/tests/conftest.py b/tests/conftest.py index ce71a665..5a34bd77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,15 +3,23 @@ from __future__ import annotations from collections import ChainMap +from typing import TYPE_CHECKING, Any, Iterator import pytest from markdown.core import Markdown from mkdocs import config from mkdocs.config.defaults import get_schema +if TYPE_CHECKING: + from pathlib import Path + + from mkdocstrings.plugin import MkdocstringsPlugin + + from mkdocstrings_handlers.python.handler import PythonHandler + @pytest.fixture(name="mkdocs_conf") -def fixture_mkdocs_conf(request, tmp_path): +def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[config.Config]: """Yield a MkDocs configuration object. Parameters: @@ -21,9 +29,9 @@ def fixture_mkdocs_conf(request, tmp_path): Yields: MkDocs config. """ - conf = config.Config(schema=get_schema()) - while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): # noqa: WPS437 - request = request._parent_request # noqa: WPS437 + conf = config.Config(schema=get_schema()) # type: ignore[call-arg] + while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): + request = request._parent_request conf_dict = { "site_name": "foo", @@ -33,7 +41,7 @@ def fixture_mkdocs_conf(request, tmp_path): **getattr(request, "param", {}), } # Re-create it manually as a workaround for https://github.com/mkdocs/mkdocs/issues/2289 - mdx_configs = dict(ChainMap(*conf_dict.get("markdown_extensions", []))) + mdx_configs: dict[str, Any] = dict(ChainMap(*conf_dict.get("markdown_extensions", []))) # type: ignore[arg-type] conf.load_dict(conf_dict) assert conf.validate() == ([], []) @@ -48,7 +56,7 @@ def fixture_mkdocs_conf(request, tmp_path): @pytest.fixture(name="plugin") -def fixture_plugin(mkdocs_conf): +def fixture_plugin(mkdocs_conf: config.Config) -> MkdocstringsPlugin: """Return a plugin instance. Parameters: @@ -57,26 +65,24 @@ def fixture_plugin(mkdocs_conf): Returns: mkdocstrings plugin instance. """ - plugin = mkdocs_conf["plugins"]["mkdocstrings"] - plugin.md = Markdown(extensions=mkdocs_conf["markdown_extensions"], extension_configs=mkdocs_conf["mdx_configs"]) - return plugin + return mkdocs_conf["plugins"]["mkdocstrings"] @pytest.fixture(name="ext_markdown") -def fixture_ext_markdown(plugin): +def fixture_ext_markdown(mkdocs_conf: config.Config) -> Markdown: """Return a Markdown instance with MkdocstringsExtension. Parameters: - plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. + mkdocs_conf: Pytest fixture: [tests.conftest.fixture_mkdocs_conf][]. Returns: A Markdown instance. """ - return plugin.md + return Markdown(extensions=mkdocs_conf["markdown_extensions"], extension_configs=mkdocs_conf["mdx_configs"]) @pytest.fixture(name="handler") -def fixture_handler(plugin): +def fixture_handler(plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> PythonHandler: """Return a handler instance. Parameters: @@ -86,5 +92,5 @@ def fixture_handler(plugin): A handler instance. """ handler = plugin.handlers.get_handler("python") - handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 - return handler + handler._update_env(ext_markdown, plugin.handlers._config) + return handler # type: ignore[return-value] diff --git a/tests/test_handler.py b/tests/test_handler.py index 93148d5f..fc31942c 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,35 +1,41 @@ """Tests for the `handler` module.""" +from __future__ import annotations + import os from glob import glob +from typing import TYPE_CHECKING import pytest from griffe.docstrings.dataclasses import DocstringSectionExamples, DocstringSectionKind from mkdocstrings_handlers.python.handler import CollectionError, PythonHandler, get_handler +if TYPE_CHECKING: + from pathlib import Path + -def test_collect_missing_module(): +def test_collect_missing_module() -> None: """Assert error is raised for missing modules.""" handler = get_handler(theme="material") with pytest.raises(CollectionError): handler.collect("aaaaaaaa", {}) -def test_collect_missing_module_item(): +def test_collect_missing_module_item() -> None: """Assert error is raised for missing items within existing modules.""" handler = get_handler(theme="material") with pytest.raises(CollectionError): handler.collect("mkdocstrings.aaaaaaaa", {}) -def test_collect_module(): +def test_collect_module() -> None: """Assert existing module can be collected.""" handler = get_handler(theme="material") assert handler.collect("mkdocstrings", {}) -def test_collect_with_null_parser(): +def test_collect_with_null_parser() -> None: """Assert we can pass `None` as parser when collecting.""" handler = get_handler(theme="material") assert handler.collect("mkdocstrings", {"docstring_style": None}) @@ -44,7 +50,7 @@ def test_collect_with_null_parser(): ], indirect=["handler"], ) -def test_render_docstring_examples_section(handler): +def test_render_docstring_examples_section(handler: PythonHandler) -> None: """Assert docstrings' examples section can be rendered. Parameters: @@ -63,7 +69,7 @@ def test_render_docstring_examples_section(handler): assert "Hello" in rendered -def test_expand_globs(tmp_path): +def test_expand_globs(tmp_path: Path) -> None: """Assert globs are correctly expanded. Parameters: @@ -81,14 +87,14 @@ def test_expand_globs(tmp_path): handler = PythonHandler( handler="python", theme="material", - config_file_path=tmp_path / "mkdocs.yml", + config_file_path=str(tmp_path.joinpath("mkdocs.yml")), paths=["*exp*"], ) - for path in globbed_paths: # noqa: WPS440 - assert str(path) in handler._paths # noqa: WPS437 + for path in globbed_paths: + assert str(path) in handler._paths -def test_expand_globs_without_changing_directory(): +def test_expand_globs_without_changing_directory() -> None: """Assert globs are correctly expanded when we are already in the right directory.""" handler = PythonHandler( handler="python", @@ -97,4 +103,4 @@ def test_expand_globs_without_changing_directory(): paths=["*.md"], ) for path in list(glob(os.path.abspath(".") + "/*.md")): - assert path in handler._paths # noqa: WPS437 + assert path in handler._paths diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 533eaf34..45b6048b 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -1,14 +1,17 @@ """Tests for the `rendering` module.""" +from __future__ import annotations + import re from dataclasses import dataclass +from typing import Any import pytest from mkdocstrings_handlers.python import rendering -def test_format_code_and_signature(): +def test_format_code_and_signature() -> None: """Assert code and signatures can be Black-formatted.""" assert rendering.do_format_code("print('Hello')", 100) assert rendering.do_format_code('print("Hello")', 100) @@ -28,7 +31,7 @@ class _FakeObject: (["aa", "ab", "ac", "da"], {"members_list": ["aa", "ab"]}, {"aa", "ab"}), ], ) -def test_filter_objects(names, filter_params, expected_names): +def test_filter_objects(names: list[str], filter_params: dict[str, Any], expected_names: set[str]) -> None: """Assert the objects filter works correctly. Parameters: @@ -37,6 +40,6 @@ def test_filter_objects(names, filter_params, expected_names): expected_names: Names expected to be kept. """ objects = {name: _FakeObject(name) for name in names} - filtered = rendering.do_filter_objects(objects, **filter_params) + filtered = rendering.do_filter_objects(objects, **filter_params) # type: ignore[arg-type] filtered_names = {obj.name for obj in filtered} assert set(filtered_names) == set(expected_names) diff --git a/tests/test_themes.py b/tests/test_themes.py index b1e7d5d5..bedcc806 100644 --- a/tests/test_themes.py +++ b/tests/test_themes.py @@ -1,9 +1,16 @@ """Tests for the different themes we claim to support.""" +from __future__ import annotations + import sys +from typing import TYPE_CHECKING import pytest +if TYPE_CHECKING: + from markdown import Markdown + from mkdocstrings.plugin import MkdocstringsPlugin + @pytest.mark.parametrize( "plugin", @@ -27,7 +34,7 @@ ], ) @pytest.mark.skipif(sys.version_info < (3, 7), reason="material is not installed on Python 3.6") -def test_render_themes_templates_python(module, plugin): +def test_render_themes_templates_python(module: str, plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> None: """Test rendering of a given theme's templates. Parameters: @@ -35,6 +42,6 @@ def test_render_themes_templates_python(module, plugin): plugin: Pytest fixture: [tests.conftest.fixture_plugin][]. """ handler = plugin.handlers.get_handler("python") - handler._update_env(plugin.md, plugin.handlers._config) # noqa: WPS437 + handler._update_env(ext_markdown, plugin.handlers._config) data = handler.collect(module, {}) handler.render(data, {})