diff --git a/.copier-answers.yml b/.copier-answers.yml index 62b9e3c8..c720007f 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.15.7 +_commit: 0.16.6 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: Timothée Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b5ed158..f4a3f0cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: pull_request: branches: - - master + - main defaults: run: @@ -26,6 +26,9 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Fetch all tags + run: git fetch --depth=1 --tags + - name: Set up PDM uses: pdm-project/setup-pdm@v3 with: @@ -52,8 +55,29 @@ jobs: - name: Check for breaking changes in the API run: pdm run duty check-api + exclude-test-jobs: + runs-on: ubuntu-latest + outputs: + jobs: ${{ steps.exclude-jobs.outputs.jobs }} + steps: + - id: exclude-jobs + run: | + if ${{ github.repository_owner == 'pawamoy-insiders' }}; then + echo 'jobs=[ + {"os": "macos-latest"}, + {"os": "windows-latest"}, + {"python-version": "3.9"}, + {"python-version": "3.10"}, + {"python-version": "3.11"}, + {"python-version": "3.12"} + ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT + else + echo 'jobs=[]' >> $GITHUB_OUTPUT + fi + tests: + needs: exclude-test-jobs strategy: max-parallel: 4 matrix: @@ -62,13 +86,14 @@ jobs: - macos-latest - windows-latest python-version: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - + - "3.12" + exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.python-version == '3.12' }} steps: - name: Checkout @@ -78,6 +103,7 @@ jobs: uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} + allow-python-prereleases: true - name: Resolving dependencies run: pdm lock -v --no-cross-platform -G ci-tests diff --git a/.github/workflows/dists.yml b/.github/workflows/dists.yml deleted file mode 100644 index 41833b63..00000000 --- a/.github/workflows/dists.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: dists - -on: push -permissions: - contents: write - -jobs: - build: - name: Build dists - runs-on: ubuntu-latest - if: github.repository_owner == 'pawamoy-insiders' - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Python - uses: actions/setup-python@v3 - - name: Install build - run: python -m pip install build - - name: Build dists - run: python -m build - - name: Upload dists artifact - uses: actions/upload-artifact@v3 - with: - name: mkdocstrings-insiders - path: ./dist/* - - name: Create release and upload assets - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: ./dist/* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1baebea4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: release + +on: push +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Fetch all tags + run: git fetch --depth=1 --tags + - name: Setup Python + uses: actions/setup-python@v4 + - name: Install build + if: github.repository_owner == 'pawamoy-insiders' + run: python -m pip install build + - name: Build dists + if: github.repository_owner == 'pawamoy-insiders' + run: python -m build + - name: Upload dists artifact + uses: actions/upload-artifact@v3 + if: github.repository_owner == 'pawamoy-insiders' + with: + name: mkdocstrings-insiders + path: ./dist/* + - name: Install git-changelog + if: github.repository_owner != 'pawamoy-insiders' + run: pip install git-changelog + - name: Prepare release notes + if: github.repository_owner != 'pawamoy-insiders' + run: git-changelog --release-notes > release-notes.md + - name: Create release with assets + uses: softprops/action-gh-release@v1 + if: github.repository_owner == 'pawamoy-insiders' + with: + files: ./dist/* + - name: Create release + uses: softprops/action-gh-release@v1 + if: github.repository_owner != 'pawamoy-insiders' + with: + body_path: release-notes.md diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index 33f285c2..0e6d9d35 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -1,7 +1,6 @@ FROM gitpod/workspace-full USER gitpod ENV PIP_USER=no -ENV PYTHON_VERSIONS= RUN pip3 install pipx; \ pipx install pdm; \ pipx ensurepath diff --git a/CHANGELOG.md b/CHANGELOG.md index bbeb8ce6..fc3a0fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ 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.23.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.23.0) - 2023-08-28 + +[Compare with 0.22.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.22.0...0.23.0) + +### Breaking Changes + +- Removed `BaseCollector` and `BaseRenderer` classes: they were merged into the `BaseHandler` class. +- Removed the watch feature, as MkDocs now provides it natively. +- Removed support for `selection` and `rendering` keys in YAML blocks: use `options` instead. +- Removed support for loading handlers from the `mkdocstrings.handler` namespace. + Handlers must now be packaged under the `mkdocstrings_handlers` namespace. + +### Features + +- Register all anchors for each object in the inventory ([228fb73](https://github.com/mkdocstrings/mkdocstrings/commit/228fb737caca4e20e600053bf59cbfa3e9c73906) by Timothée Mazzucotelli). + +### Bug Fixes + +- Don't add `codehilite` CSS class to inline code ([7690d41](https://github.com/mkdocstrings/mkdocstrings/commit/7690d41e2871997464367e673023585c4fb05e26) by Timothée Mazzucotelli). +- Support cross-references for API docs rendered in top-level index page ([b194452](https://github.com/mkdocstrings/mkdocstrings/commit/b194452be93aee33b3c28a468762b4d96c501f4f) by Timothée Mazzucotelli). + +### Code Refactoring + +- Sort inventories before writing them to disk ([9371e9f](https://github.com/mkdocstrings/mkdocstrings/commit/9371e9fc7dd68506b73aa1580a12c5f5cd779aba) by Timothée Mazzucotelli). +- Stop accepting sets as return value of `get_anchors` (only tuples), to preserve order ([2e10374](https://github.com/mkdocstrings/mkdocstrings/commit/2e10374be258e9713b26f73dd06d0c2520ec07a5) by Timothée Mazzucotelli). +- Remove deprecated parts ([0a90a47](https://github.com/mkdocstrings/mkdocstrings/commit/0a90a474c8dcbd95821700d7dab63f03e392c40f) by Timothée Mazzucotelli). +- Use proper parameters in `Inventory.register` method ([433c6e0](https://github.com/mkdocstrings/mkdocstrings/commit/433c6e01aab9333589f755e483f124db0836f143) by Timothée Mazzucotelli). + ## [0.22.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.22.0) - 2023-05-25 [Compare with 0.21.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.21.2...0.22.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ecc0fa0..0b86ff4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,20 +39,19 @@ Run `make help` to see all the available actions! This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task -on multiple Python versions, you can do one of the following: - -1. `export PYTHON_VERSIONS= `: this will run the task - with only the current Python version -2. run the task directly with `pdm run duty TASK` +on multiple Python versions, you run the task directly with `pdm run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. +If you work in VSCode, +[see examples of tasks and run configurations](https://pawamoy.github.io/copier-pdm/work/#vscode-setup). + ## Development As usual: -1. create a new branch: `git checkout -b feature-or-bugfix-name` +1. create a new branch: `git switch -c feature-or-bugfix-name` 1. edit the code and/or the documentation **Before committing:** @@ -138,7 +137,7 @@ git commit --fixup=SHA Once all the changes are approved, you can squash your commits: ```bash -git rebase -i --autosquash master +git rebase -i --autosquash main ``` And force-push: diff --git a/Makefile b/Makefile index b71d86ce..5696baac 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .DEFAULT_GOAL := help SHELL := bash - -DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo pdm run) duty +DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty +export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files diff --git a/README.md b/README.md index 52300c96..06872728 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,18 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo - **Reasonable defaults:** you should be able to just drop the plugin in your configuration and enjoy your auto-generated docs. +## Used by + +*mkdocstrings* is used by well-known companies, projects and scientific teams: +[Ansible](https://molecule.readthedocs.io/configuration/), +[Apache](https://streampipes.apache.org/docs/docs/python/latest/reference/client/client/), +[Google](https://docs.kidger.site/jaxtyping/api/runtime-type-checking/), +[Jitsi](https://jitsi.github.io/jiwer/reference/alignment/), +[Microsoft](https://microsoft.github.io/presidio/api/analyzer_python/), +[Prefect](https://docs.prefect.io/2.10.12/api-ref/prefect/agent/), +[Pydantic](https://docs.pydantic.dev/dev-v2/api/main/), +[and more...](https://github.com/mkdocstrings/mkdocstrings/network/dependents) + ## Installation With `pip`: diff --git a/config/mypy.ini b/config/mypy.ini index cb0dd886..814e2ac8 100644 --- a/config/mypy.ini +++ b/config/mypy.ini @@ -3,5 +3,3 @@ ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true -namespace_packages = true -explicit_package_bases = true diff --git a/config/pytest.ini b/config/pytest.ini index 5a493959..6b0d5c7a 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -20,3 +20,7 @@ filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist + # TODO: https://github.com/Python-Markdown/markdown/issues/1355 + ignore:.*Testing:DeprecationWarning:markdown + # TODO: https://github.com/facelessuser/pymdown-extensions/issues/2113 + ignore:.*Testing:DeprecationWarning:pymdownx diff --git a/config/ruff.toml b/config/ruff.toml index 53824875..c6e4a55c 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py37" +target-version = "py38" line-length = 132 exclude = [ "fixtures", @@ -65,7 +65,6 @@ ignore = [ "E501", # Line too long "ERA001", # Commented out code "G004", # Logging statement uses f-string - "INP001", # File is part of an implicit namespace package "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call diff --git a/docs/css/insiders.css b/docs/css/insiders.css index 81dbd756..b5547bd1 100644 --- a/docs/css/insiders.css +++ b/docs/css/insiders.css @@ -95,4 +95,31 @@ a.insiders { transition: all .25s; vertical-align: bottom !important; width: 1.2rem; +} + +.premium-sponsors { + text-align: center; +} + +#silver-sponsors img { + height: 140px; +} + +#bronze-sponsors img { + height: 140px; +} + +#bronze-sponsors p { + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +#bronze-sponsors a { + display: block; + flex-shrink: 0; +} + +.sponsors-total { + font-weight: bold; } \ No newline at end of file diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index af758331..3960e49e 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -5,24 +5,23 @@ div.doc-contents:not(.first) { } /* Mark external links as such. */ +a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ - background-image: url('data:image/svg+xml,'); + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; vertical-align: middle; position: relative; - bottom: 0.1em; - margin-left: 0.2em; - margin-right: 0.1em; - height: 0.7em; - width: 0.7em; - border-radius: 100%; + height: 1em; + width: 1em; background-color: var(--md-typeset-a-color); } +a.external:hover::after, a.autorefs-external:hover::after { background-color: var(--md-accent-fg-color); } diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml index 896b9240..a96ac51b 100644 --- a/docs/insiders/goals.yml +++ b/docs/insiders/goals.yml @@ -1 +1 @@ -goals: {} +goals: {} \ No newline at end of file diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 0f5b0de9..bfb2d428 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -59,6 +59,8 @@ a handful of them, [thanks to our awesome sponsors][sponsors]! --> data_source = [ "docs/insiders/goals.yml", ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), + ("griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), + ("griffe-typing-deprecated", "https://mkdocstrings.github.io/griffe-typing-deprecated/", "insiders/goals.yml"), ] ``` @@ -68,10 +70,10 @@ data_source = [ ```python exec="1" session="insiders" print(f"""The moment you become a sponsor, you'll get **immediate -access to {len(completed_features)} additional features** that you can start using right away, and +access to {len(unreleased_features)} additional features** that you can start using right away, and which are currently exclusively available to sponsors:\n""") -for feature in completed_features: +for feature in unreleased_features: feature.render(badge=True) ``` @@ -112,17 +114,20 @@ You can cancel your sponsorship anytime.[^5] regarding your payment, and GitHub doesn't offer refunds, sponsorships are non-refundable. -```python exec="1" session="insiders" -print_join_sponsors_button() -``` +[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary } -
+
+
-```python exec="1" session="insiders" -print_sponsors() -``` +
+
+ +
-

+
+ +
+
If you sponsor publicly, you're automatically added here with a link to @@ -132,11 +137,7 @@ print_sponsors() afterwards. -## Funding - -```python exec="1" session="insiders" -print(f"Current funding is at **$ {human_readable_amount(current_funding)} a month**.") -``` +## Funding ### Goals @@ -218,5 +219,8 @@ by the [ISC License][license]. However, we kindly ask you to respect our [goals completed]: #goals-completed [github sponsor profile]: https://github.com/sponsors/pawamoy [billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle -[license]: ../license/ +[license]: ../license.md [private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository + + + diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md index 9852182b..b7af7d2e 100644 --- a/docs/insiders/installation.md +++ b/docs/insiders/installation.md @@ -13,6 +13,16 @@ 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.). + +See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) +and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). + ### with pip (ssh/https) *mkdocstrings Insiders* can be installed with `pip` [using SSH][using ssh]: @@ -97,7 +107,7 @@ or installing a package (with pip), and depending on the registry you are using Please check the documentation of your registry to learn how to configure your environment. **We kindly ask that you do not upload the distributions to public registries, -as it is against our [Terms of use](../#terms).** +as it is against our [Terms of use](index.md#terms).** >? TIP: **Full example with `pypiserver`** > In this example we use [pypiserver] to serve a local PyPI index. diff --git a/docs/js/insiders.js b/docs/js/insiders.js new file mode 100644 index 00000000..03bcb404 --- /dev/null +++ b/docs/js/insiders.js @@ -0,0 +1,67 @@ +function humanReadableAmount(amount) { + const strAmount = String(amount); + if (strAmount.length >= 4) { + return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`; + } + return strAmount; +} + +function getJSON(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'json'; + xhr.onload = function () { + var status = xhr.status; + if (status === 200) { + callback(null, xhr.response); + } else { + callback(status, xhr.response); + } + }; + xhr.send(); +} + +function updateInsidersPage(author_username) { + const sponsorURL = `https://github.com/sponsors/${author_username}` + const dataURL = `https://raw.githubusercontent.com/${author_username}/sponsors/main`; + getJSON(dataURL + '/numbers.json', function (err, numbers) { + document.getElementById('sponsors-count').innerHTML = numbers.count; + Array.from(document.getElementsByClassName('sponsors-total')).forEach(function (element) { + element.innerHTML = '$ ' + humanReadableAmount(numbers.total); + }); + getJSON(dataURL + '/sponsors.json', function (err, sponsors) { + const sponsorsElem = document.getElementById('sponsors'); + const privateSponsors = numbers.count - sponsors.length; + sponsors.forEach(function (sponsor) { + sponsorsElem.innerHTML += ` + + + + `; + }); + if (privateSponsors > 0) { + sponsorsElem.innerHTML += ` + + +${privateSponsors} + + `; + } + }); + }); + getJSON(dataURL + '/sponsorsBronze.json', function (err, sponsors) { + const bronzeSponsors = document.getElementById("bronze-sponsors"); + if (sponsors) { + let html = ''; + html += 'Bronze sponsors

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

' + bronzeSponsors.innerHTML = html; + } + }); +} diff --git a/docs/license.md b/docs/license.md index cdacdfef..a873d2b5 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,3 +1,5 @@ +# License + ``` --8<-- "LICENSE" ``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4d2b074e..bc1da01b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -21,6 +21,10 @@ markdown_extensions: - pymdownx.superfences ``` +For code blocks in docstrings, make sure to escape newlines (`\n` -> `\\n`), +or prefix the entire docstring with 'r' to make it a raw-docstring: `r"""`. +Indeed, docstrings are still strings and therefore subject to how Python parses strings. + ## Footnotes are duplicated or overridden Before version 0.14, footnotes could be duplicated over a page. diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index e381090e..d2c25420 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -14,11 +14,8 @@ Since version 0.18, a new, experimental Python handler is available. It is based on [Griffe](https://github.com/mkdocstrings/griffe), which is an improved version of [pytkdocs](https://github.com/mkdocstrings/pytkdocs). -Note that the experimental handler does not yet support third-party libraries -like Django, Marshmallow, Pydantic, etc. -It is also not completely ready to handle dynamically built objects, -like classes built with a call to `type(...)`. -For most other cases, the experimental handler will work just fine. +Note that the experimental handler does not yet support all third-party libraries +that the legacy handler supported. If you want to keep using the legacy handler as long as possible, you can depend on `mkdocstrings-python-legacy` directly, @@ -51,18 +48,13 @@ dependencies = [ ] ``` -#### Handler options - -- `setup_commands` is not yet implemented. In most cases, you won't need it, - since by default the new handler does not execute the code. - #### Selection options WARNING: Since *mkdocstrings* 0.19, the YAML `selection` key is merged into the `options` key. - [x] `filters` is implemented, and used as before. - [x] `members` is implemented, and used as before. -- [ ] `inherited_members` is not yet implemented. +- [x] `inherited_members` is implemented. - [x] `docstring_style` is implemented, and used as before, except for the `restructured-text` style which is renamed `sphinx`. Numpy-style is now built-in, so you can stop depending on `pytkdocs[numpy-style]` @@ -83,13 +75,13 @@ WARNING: Since *mkdocstrings* 0.19, the YAML `rendering` key is merged into the Every previous option is supported. Additional options are available: -- `separate_signature`: Render the signature (or attribute value) in a code block below the heading, +- [x] `separate_signature`: Render the signature (or attribute value) in a code block below the heading, instead as inline code. Useful for long signatures. If Black is installed, the signature is formatted. Default: `False`. -- `line_length`: The maximum line length to use when formatting signatures. Default: `60`. -- `show_submodules`: Whether to render submodules of a module when iterating on children. +- [x] `line_length`: The maximum line length to use when formatting signatures. Default: `60`. +- [x] `show_submodules`: Whether to render submodules of a module when iterating on children. Default: `False`. -- `docstring_section_style`: The style to use to render docstring sections such as attributes, +- [x] `docstring_section_style`: The style to use to render docstring sections such as attributes, parameters, etc. Available styles: `table` (default), `list` and `spacy`. The SpaCy style is a poor implementation of their [table style](https://spacy.io/api/doc/#init). We are open to improvements through PRs! @@ -99,34 +91,8 @@ See [all the handler's options](https://mkdocstrings.github.io/python/usage/). #### Templates Templates are mostly the same as before, but the file layout has changed, -as well as some file names. Here is the new tree: - -``` -📁 theme/ -├── 📄 attribute.html -├── 📄 children.html -├── 📄 class.html -├── 📁 docstring/ -│   ├── 📄 admonition.html -│   ├── 📄 attributes.html -│   ├── 📄 examples.html -│   ├── 📄 other_parameters.html -│   ├── 📄 parameters.html -│   ├── 📄 raises.html -│   ├── 📄 receives.html -│   ├── 📄 returns.html -│   ├── 📄 warns.html -│   └── 📄 yields.html -├── 📄 docstring.html -├── 📄 expression.html -├── 📄 function.html -├── 📄 labels.html -├── 📄 module.html -└── 📄 signature.html -``` - -See them [in the handler repository](https://github.com/mkdocstrings/python/tree/8fc8ea5b112627958968823ef500cfa46b63613e/src/mkdocstrings_handlers/python/templates/material). See the documentation about the Python handler templates: -https://mkdocstrings.github.io/python/customization/#templates. +as well as some file names. +See [the documentation about the Python handler templates](https://mkdocstrings.github.io/python/usage/customization/#templates). ## Custom handlers @@ -213,7 +179,7 @@ If your theme's HTML requires CSS to go along with it, put it into a file named `mkdocstrings_handlers/custom_handler/templates/some_theme/style.css`, then this will be included into the final site automatically if this handler is ever used. Alternatively, you can put the CSS as a string into the `extra_css` variable of -your renderer. +your handler. Finally, it's possible to entirely omit templates, and tell *mkdocstrings* to use the templates of another handler. In you handler, override the @@ -225,7 +191,7 @@ from mkdocstrings.handlers.base import BaseHandler class CobraHandler(BaseHandler): - def get_templates_dir(self, handler: str) -> Path: + def get_templates_dir(self, handler: str | None = None) -> Path: # use the python handler templates # (it assumes the python handler is installed) return super().get_templates_dir("python") @@ -268,7 +234,7 @@ could add specific support for another Python library. NOTE: This feature is intended for developers. If you are a user and want to customize how objects are rendered, -see [Theming / Customization](../theming/#customization). +see [Theming / Customization](theming.md#customization). Such extensions can register additional template folders that will be used when rendering collected data. diff --git a/docs/usage/index.md b/docs/usage/index.md index 3318c053..7599f9f1 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -116,9 +116,6 @@ The above is equivalent to: - `enabled` **(New in version 0.20)**: Whether to enable the plugin. Defaults to `true`. Can be used to reduce build times when doing local development. Especially useful when used with environment variables (see example below). -- `watch` **(deprecated)**: A list of directories to watch while serving the documentation. - See [Watch directories](#watch-directories). Deprecated in favor of the now built-in - [`watch` feature of MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). !!! example ```yaml title="mkdocs.yml" @@ -334,30 +331,3 @@ plugins: - mkdocstrings: enable_inventory: false ``` - -## Watch directories - -DANGER: **Deprecated since version 0.19.** -Instead, use the built-in [`watch` feature of MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). - -You can add directories to watch with the `watch` key. -It accepts a list of paths. - -```yaml title="mkdocs.yml" -plugins: - - mkdocstrings: - watch: - - src/my_package_1 - - src/my_package_2 -``` - -When serving your documentation -and a change occur in one of the listed path, -MkDocs will rebuild the site and reload the current page. - -NOTE: **The `watch` feature doesn't have special effects.** -Adding directories to the `watch` list doesn't have any other effect than watching for changes. -For example, it will not tell the Python handler to look for packages in these paths -(the paths are not added to the `PYTHONPATH` variable). -If you want to tell Python where to look for packages and modules, -see [Python Handler: Finding modules](https://mkdocstrings.github.io/python/usage/#finding-modules). diff --git a/duties.py b/duties.py index 77410349..644b2ffb 100644 --- a/duties.py +++ b/duties.py @@ -53,10 +53,10 @@ def merge(d1: Any, d2: Any) -> Any: # noqa: D103 def mkdocs_config() -> str: # noqa: D103 - from mkdocs import utils + import mergedeep - # patch YAML loader to merge arrays - utils.merge = merge + # force YAML loader to merge arrays + mergedeep.merge = merge if "+insiders" in pkgversion("mkdocs-material"): return "mkdocs.insiders.yml" @@ -108,6 +108,7 @@ def check_quality(ctx: Context) -> None: ctx.run( ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), + command=f"ruff check --config config/ruff.toml {PY_SRC}", ) @@ -125,7 +126,11 @@ def check_dependencies(ctx: Context) -> None: allow_overrides=False, ) - ctx.run(safety.check(requirements), title="Checking dependencies") + ctx.run( + safety.check(requirements), + title="Checking dependencies", + command="pdm export -f requirements --without-hashes | safety check --stdin", + ) @duty @@ -137,7 +142,12 @@ def check_docs(ctx: Context) -> None: """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) - ctx.run(mkdocs.build(strict=True, config_file=mkdocs_config()), title=pyprefix("Building documentation")) + config = mkdocs_config() + ctx.run( + mkdocs.build(strict=True, config_file=config, verbose=True), + title=pyprefix("Building documentation"), + command=f"mkdocs build -vsf {config}", + ) @duty @@ -151,6 +161,7 @@ def check_types(ctx: Context) -> None: ctx.run( mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), + command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @@ -165,8 +176,9 @@ def check_api(ctx: Context) -> None: griffe_check = lazy(g_check, name="griffe.check") ctx.run( - griffe_check("mkdocstrings", search_paths=["src"]), + griffe_check("mkdocstrings", search_paths=["src"], color=True), title="Checking for API breaking changes", + command="griffe check -ssrc mkdocstrings", nofail=True, ) @@ -298,6 +310,7 @@ def test(ctx: Context, match: str = "") -> None: py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( - pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match), + pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), title=pyprefix("Running tests"), + command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) diff --git a/mkdocs.yml b/mkdocs.yml index f544387c..fdd6898b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,11 +2,16 @@ site_name: "mkdocstrings" site_description: "Automatic documentation from sources, for MkDocs." site_url: "https://mkdocstrings.github.io/" repo_url: "https://github.com/mkdocstrings/mkdocstrings" -edit_uri: "blob/master/docs/" repo_name: "mkdocstrings/mkdocstrings" site_dir: "site" watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocstrings] copyright: Copyright © 2019 Timothée Mazzucotelli +edit_uri: edit/main/docs/ + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn nav: - Home: @@ -26,7 +31,8 @@ nav: - Recipes: recipes.md - Troubleshooting: troubleshooting.md # defer to gen-files + literate-nav -- Code Reference: reference/ +- API reference: + - mkdocstrings: reference/ - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md @@ -124,10 +130,19 @@ plugins: - https://installer.readthedocs.io/en/stable/objects.inv # demonstration purpose in the docs - https://mkdocstrings.github.io/autorefs/objects.inv options: - separate_signature: true - merge_init_into_class: true docstring_options: ignore_init_summary: true + docstring_section_style: list + heading_level: 1 + merge_init_into_class: true + separate_signature: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + show_symbol_type_heading: true + show_symbol_type_toc: true + signature_crossrefs: true + summary: true - git-committers: enabled: !ENV [DEPLOY, false] repository: mkdocstrings/mkdocstrings diff --git a/pyproject.toml b/pyproject.toml index a36f6d25..78af6bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Automatic documentation from sources, for MkDocs." authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}] license = {text = "ISC"} readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"] dynamic = ["version"] classifiers = [ @@ -17,11 +17,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -65,7 +65,6 @@ plugins = [ [tool.pdm.build] package-dir = "src" -includes = ["src/mkdocstrings"] editable-backend = "editables" [tool.pdm.dev-dependencies] @@ -76,7 +75,7 @@ docs = [ "black>=23.1", "markdown-callouts>=0.2", "markdown-exec>=0.5", - "mkdocs>=1.3", + "mkdocs>=1.5", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", "mkdocs-git-committers-plugin-2>=1.1", diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 85ac9041..bc01c0bd 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -89,6 +89,8 @@ def _render_credits() -> str: } template_text = dedent( """ + # Credits + These projects were used to build *{{ project_name }}*. **Thank you!** [`python`](https://www.python.org/) | diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py index 65e70cfe..249530b1 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -5,10 +5,11 @@ import mkdocs_gen_files nav = mkdocs_gen_files.Nav() +mod_symbol = '' for path in sorted(Path("src").rglob("*.py")): module_path = path.relative_to("src").with_suffix("") - doc_path = path.relative_to("src", "mkdocstrings").with_suffix(".md") + doc_path = path.relative_to("src/mkdocstrings").with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) @@ -20,13 +21,14 @@ elif parts[-1].startswith("_"): continue - nav[parts] = doc_path.as_posix() + nav_parts = [f"{mod_symbol} {part}" for part in parts] + nav[tuple(nav_parts)] = doc_path.as_posix() with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) fd.write(f"::: {ident}") - mkdocs_gen_files.set_edit_path(full_doc_path, Path("../") / path) + mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/insiders.py b/scripts/insiders.py index 0d23a45a..6f8d0d84 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -9,7 +9,6 @@ from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path -from textwrap import dedent from typing import Iterable, cast from urllib.error import HTTPError from urllib.parse import urljoin @@ -101,7 +100,7 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d Feature( name=feature_data["name"], ref=feature_data["ref"], - since=feature_data["since"] + since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 project=project, ) @@ -185,32 +184,9 @@ def load_json(url: str) -> str | list | dict: # noqa: D103 current_funding = numbers["total"] sponsors_count = numbers["count"] goals = funding_goals(data_source, funding=current_funding) -all_features = feature_list(goals.values()) -completed_features = sorted( - (ft for ft in all_features if ft.since), +ongoing_goals = [goal for goal in goals.values() if not goal.complete] +unreleased_features = sorted( + (ft for ft in feature_list(ongoing_goals) if ft.since), key=lambda ft: cast(date, ft.since), reverse=True, ) - - -def print_join_sponsors_button() -> None: # noqa: D103 - btn_classes = "{ .md-button .md-button--primary }" - print( - dedent( - f""" - [:octicons-heart-fill-24:{{ .pulse }} -   Join our {sponsors_count} awesome sponsors]({sponsor_url}){btn_classes} - """, - ), - ) - - -def print_sponsors() -> None: # noqa: D103 - private_sponsors_count = sponsors_count - len(sponsors) - for sponsor in sponsors: - print( - f"""""" - f"""""", - ) - if private_sponsors_count: - print(f"""+{private_sponsors_count}""") diff --git a/scripts/setup.sh b/scripts/setup.sh index f6c90de5..4b4cb0fb 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash set -e -PYTHON_VERSIONS="${PYTHON_VERSIONS-3.7 3.8 3.9 3.10 3.11}" - if ! command -v pdm &>/dev/null; then if ! command -v pipx &>/dev/null; then python3 -m pip install --user pipx @@ -13,8 +11,8 @@ if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then pdm install --plugins fi -if [ -n "${PYTHON_VERSIONS}" ]; then - pdm multirun -vi ${PYTHON_VERSIONS// /,} pdm install --dev +if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then + pdm multirun -v pdm install --dev else pdm install --dev fi diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py new file mode 100644 index 00000000..03550f9b --- /dev/null +++ b/src/mkdocstrings/__init__.py @@ -0,0 +1,4 @@ +"""mkdocstrings package. + +Automatic documentation from sources, for MkDocs. +""" diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 62d837ca..139030ba 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -12,19 +12,17 @@ ```yaml ::: some.identifier handler: python - selection: + options: option1: value1 option2: - - value2a - - value2b - rendering: + - value2a + - value2b option_x: etc ``` """ from __future__ import annotations -import functools import re from collections import ChainMap from typing import TYPE_CHECKING, Any, MutableSequence @@ -133,19 +131,35 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: el.extend(headings) page = self._autorefs.current_page - if page: + if page is not None: for heading in headings: - anchor = heading.attrib["id"] - self._autorefs.register_anchor(page, anchor) + rendered_anchor = heading.attrib["id"] + self._autorefs.register_anchor(page, rendered_anchor) if "data-role" in heading.attrib: self._handlers.inventory.register( - name=anchor, + name=rendered_anchor, domain=handler.domain, role=heading.attrib["data-role"], - uri=f"{page}#{anchor}", + priority=1, # register with standard priority + uri=f"{page}#{rendered_anchor}", ) + # also register other anchors for this object in the inventory + try: + data_object = handler.collect(rendered_anchor, handler.fallback_config) + except CollectionError: + continue + for anchor in handler.get_anchors(data_object): + if anchor not in self._handlers.inventory: + self._handlers.inventory.register( + name=anchor, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=2, # register with lower priority + uri=f"{page}#{rendered_anchor}", + ) + parent.append(el) if the_rest: @@ -183,13 +197,7 @@ def _process_block( global_options = handler_config.get("options", {}) local_options = config.get("options", {}) - deprecated_global_options = ChainMap(handler_config.get("selection", {}), handler_config.get("rendering", {})) - deprecated_local_options = ChainMap(config.get("selection", {}), config.get("rendering", {})) - - options = ChainMap(local_options, deprecated_local_options, global_options, deprecated_global_options) - - if deprecated_global_options or deprecated_local_options: - self._warn_about_options_key() + options = ChainMap(local_options, global_options) if heading_level: options = ChainMap(options, {"heading_level": heading_level}) # like setdefault @@ -199,12 +207,12 @@ def _process_block( data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: log.error(str(exception)) # noqa: TRY400 - if PluginError is SystemExit: # When MkDocs 1.2 is sufficiently common, this can be dropped. + if PluginError is SystemExit: # TODO: when MkDocs 1.2 is sufficiently common, this can be dropped. log.error(f"Error reading page '{self._autorefs.current_page}':") # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. - log.debug("Updating renderer's env") + log.debug("Updating handler's rendering env") handler._update_env(self.md, self._config) self._updated_envs.add(handler_name) @@ -220,11 +228,6 @@ def _process_block( return rendered, handler, data - @classmethod - @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_options_key(cls) -> None: - log.info("DEPRECATION: 'selection' and 'rendering' are deprecated and merged into a single 'options' YAML key") - class _PostProcessor(Treeprocessor): def run(self, root: Element) -> None: diff --git a/src/mkdocstrings/handlers/__init__.py b/src/mkdocstrings/handlers/__init__.py new file mode 100644 index 00000000..b9e2a29c --- /dev/null +++ b/src/mkdocstrings/handlers/__init__.py @@ -0,0 +1 @@ +"""Handlers module.""" diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index b6ecd4fa..700a0565 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -1,21 +1,14 @@ """Base module for handlers. -This module contains the base classes for implementing collectors, renderers, and the combination of the two: handlers. - -It also provides two methods: - -- `get_handler`, that will cache handlers into the `HANDLERS_CACHE` dictionary. -- `teardown`, that will teardown all the cached handlers, and then clear the cache. +This module contains the base classes for implementing handlers. """ from __future__ import annotations import importlib import sys -import warnings -from contextlib import suppress from pathlib import Path -from typing import Any, BinaryIO, Iterable, Iterator, Mapping, MutableMapping, Sequence +from typing import Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader @@ -66,21 +59,32 @@ def do_any(seq: Sequence, attribute: str | None = None) -> bool: return any(_[attribute] for _ in seq) -class BaseRenderer: - """The base renderer class. +class BaseHandler: + """The base handler class. - Inherit from this class to implement a renderer. + Inherit from this class to implement a handler. - You will have to implement the `render` method. - You can also override the `update_env` method, to add more filters to the Jinja environment, + You will have to implement the `collect` and `render` methods. + You can also implement the `teardown` method, + and override the `update_env` method, to add more filters to the Jinja environment, making them available in your Jinja templates. To define a fallback theme, add a `fallback_theme` class-variable. To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates. """ + name: str = "" + """The handler's name, for example "python".""" + domain: str = "default" + """The handler's domain, used to register objects in the inventory, for example "py".""" + enable_inventory: bool = False + """Whether the inventory creation is enabled.""" + fallback_config: ClassVar[dict] = {} + """Fallback configuration when searching anchors for identifiers.""" fallback_theme: str = "" + """Fallback theme to use when a template isn't found in the configured theme.""" extra_css = "" + """Extra CSS.""" def __init__(self, handler: str, theme: str, custom_templates: str | None = None) -> None: """Initialize the object. @@ -95,11 +99,6 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None """ paths = [] - # TODO: remove once BaseRenderer is merged into BaseHandler - self._handler = handler - self._theme = theme - self._custom_templates = custom_templates - # add selected theme templates themes_dir = self.get_templates_dir(handler) paths.append(themes_dir / theme) @@ -137,6 +136,44 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None self._headings: list[Element] = [] self._md: Markdown = None # type: ignore[assignment] # To be populated in `update_env`. + @classmethod + def load_inventory( + cls, + in_file: BinaryIO, # noqa: ARG003 + url: str, # noqa: ARG003 + base_url: str | None = None, # noqa: ARG003 + **kwargs: Any, # noqa: ARG003 + ) -> Iterator[tuple[str, str]]: + """Yield items and their URLs from an inventory file streamed from `in_file`. + + Arguments: + 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. + **kwargs: Ignore additional arguments passed from the config. + + Yields: + Tuples of (item identifier, item URL). + """ + yield from () + + def collect(self, identifier: str, config: MutableMapping[str, Any]) -> CollectorItem: + """Collect data given an identifier and user configuration. + + In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into + a Python dictionary for example, though the implementation is completely free. + + Arguments: + identifier: An identifier for which to collect data. For example, in Python, + it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. + It can be anything that you can feed to the tool of your choice. + config: The handler's configuration options. + + Returns: + Anything you want, as long as you can feed it to the handler's `render` method. + """ + raise NotImplementedError + def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: """Render a template using provided data and configuration options. @@ -149,7 +186,14 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: """ raise NotImplementedError - def get_templates_dir(self, handler: str) -> Path: + def teardown(self) -> None: + """Teardown the handler. + + This method should be implemented to, for example, terminate a subprocess + that was started when creating the handler instance. + """ + + def get_templates_dir(self, handler: str | None = None) -> Path: """Return the path to the handler's templates directory. Override to customize how the templates directory is found. @@ -158,41 +202,21 @@ def get_templates_dir(self, handler: str) -> Path: handler: The name of the handler to get the templates directory of. Raises: + ModuleNotFoundError: When no such handler is installed. FileNotFoundError: When the templates directory cannot be found. Returns: The templates directory path. """ - # Templates can be found in 2 different logical locations: - # - in mkdocstrings_handlers/HANDLER/templates: our new migration target - # - in mkdocstrings/templates/HANDLER: current situation, this should be avoided - # These two other locations are forbidden: - # - in mkdocstrings_handlers/templates/HANDLER: sub-namespace packages are too annoying to deal with - # - in mkdocstrings/handlers/HANDLER/templates: not currently supported, - # and mkdocstrings will stop being a namespace - - with suppress(ModuleNotFoundError): # TODO: catch at some point to warn about missing handlers + handler = handler or self.name + try: import mkdocstrings_handlers + except ModuleNotFoundError as error: + raise ModuleNotFoundError(f"Handler '{handler}' not found, is it installed?") from error - for path in mkdocstrings_handlers.__path__: - theme_path = Path(path, handler, "templates") - if theme_path.exists(): - return theme_path - - # TODO: remove import and loop at some point, - # as mkdocstrings will stop being a namespace package - import mkdocstrings - - for path in mkdocstrings.__path__: - theme_path = Path(path, "templates", handler) + for path in mkdocstrings_handlers.__path__: + theme_path = Path(path, handler, "templates") if theme_path.exists(): - if handler != "python": - warnings.warn( - "Exposing templates in the mkdocstrings.templates namespace is deprecated. " - "Put them in a templates folder inside your handler package instead.", - DeprecationWarning, - stacklevel=1, - ) return theme_path raise FileNotFoundError(f"Can't find 'templates' folder for handler '{handler}'") @@ -209,7 +233,7 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: + def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: """Return the possible identifiers (HTML anchors) for a collected item. Arguments: @@ -218,7 +242,7 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: Returns: The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. """ - # TODO: remove this at some point + # TODO: remove this when https://github.com/mkdocstrings/crystal/pull/6 is merged and released try: return (self.get_anchor(data),) # type: ignore[attr-defined] except AttributeError: @@ -346,181 +370,6 @@ def _update_env(self, md: Markdown, config: dict) -> None: self.update_env(new_md, config) -class BaseCollector: - """The base collector class. - - Inherit from this class to implement a collector. - - You will have to implement the `collect` method. - You can also implement the `teardown` method. - """ - - def collect(self, identifier: str, config: MutableMapping[str, Any]) -> CollectorItem: - """Collect data given an identifier and selection configuration. - - In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into - a Python dictionary for example, though the implementation is completely free. - - Arguments: - identifier: An identifier for which to collect data. For example, in Python, - it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. - It can be anything that you can feed to the tool of your choice. - config: The handler's configuration options. - - Returns: - Anything you want, as long as you can feed it to the renderer's `render` method. - """ - raise NotImplementedError - - def teardown(self) -> None: - """Teardown the collector. - - This method should be implemented to, for example, terminate a subprocess - that was started when creating the collector instance. - """ - - -class BaseHandler(BaseCollector, BaseRenderer): - """The base handler class. - - Inherit from this class to implement a handler. - - It's usually just a combination of a collector and a renderer, but you can make it as complex as you need. - - Attributes: - domain: The cross-documentation domain/language for this handler. - enable_inventory: Whether this handler is interested in enabling the creation - of the `objects.inv` Sphinx inventory file. - fallback_config: The configuration used to collect item during autorefs fallback. - """ - - domain: str = "default" - enable_inventory: bool = False - fallback_config: dict = {} - - # TODO: once the BaseCollector and BaseRenderer classes are removed, - # stop accepting the 'handler' parameter, and instead set a 'name' attribute on the Handler class. - # Then make the 'handler' parameter in 'get_templates_dir' optional, and use the class 'name' by default. - def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | BaseCollector | BaseRenderer) -> None: - """Initialize the object. - - Arguments: - *args: Collector and renderer, or handler name, theme and custom_templates. - **kwargs: Same thing, but with keyword arguments. - - Raises: - ValueError: When the given parameters are invalid. - """ - # The method accepts *args and **kwargs temporarily, - # to support the transition period where the BaseCollector - # and BaseRenderer are deprecated, and the BaseHandler - # can be instantiated with both instances of collector/renderer, - # or renderer parameters, as positional parameters. - - collector = None - renderer = None - - # parsing positional arguments - str_args = [] - for arg in args: - if isinstance(arg, BaseCollector): - collector = arg - elif isinstance(arg, BaseRenderer): - renderer = arg - elif isinstance(arg, str): - str_args.append(arg) - - while len(str_args) != 3: # noqa: PLR2004 - str_args.append(None) # type: ignore[arg-type] - - handler, theme, custom_templates = str_args - - # fetching values from keyword arguments - if "collector" in kwargs: - collector = kwargs.pop("collector") # type: ignore[assignment] - if "renderer" in kwargs: - renderer = kwargs.pop("renderer") # type: ignore[assignment] - if "handler" in kwargs: - handler = kwargs.pop("handler") # type: ignore[assignment] - if "theme" in kwargs: - theme = kwargs.pop("theme") # type: ignore[assignment] - if "custom_templates" in kwargs: - custom_templates = kwargs.pop("custom_templates") # type: ignore[assignment] - - if collector is None and renderer is not None or collector is not None and renderer is None: - raise ValueError("both 'collector' and 'renderer' must be provided") - - if collector is not None: - warnings.warn( - DeprecationWarning( - "The BaseCollector class is deprecated, and passing an instance of it " - "to your handler is deprecated as well. Instead, define the `collect` and `teardown` " - "methods directly on your handler class.", - ), - stacklevel=1, - ) - self.collector = collector - self.collect = collector.collect # type: ignore[method-assign] - self.teardown = collector.teardown # type: ignore[method-assign] - - if renderer is not None: - if {handler, theme, custom_templates} != {None}: - raise ValueError( - "'handler', 'theme' and 'custom_templates' must all be None when providing a renderer instance", - ) - warnings.warn( - DeprecationWarning( - "The BaseRenderer class is deprecated, and passing an instance of it " - "to your handler is deprecated as well. Instead, define the `render` method " - "directly on your handler class (as well as other methods and attributes like " - "`get_templates_dir`, `get_anchors`, `update_env` and `fallback_theme`, `extra_css`).", - ), - stacklevel=1, - ) - self.renderer = renderer - self.render = renderer.render # type: ignore[method-assign] - self.get_templates_dir = renderer.get_templates_dir # type: ignore[method-assign] - self.get_anchors = renderer.get_anchors # type: ignore[method-assign] - self.do_convert_markdown = renderer.do_convert_markdown # type: ignore[method-assign] - self.do_heading = renderer.do_heading # type: ignore[method-assign] - self.get_headings = renderer.get_headings # type: ignore[method-assign] - self.update_env = renderer.update_env # type: ignore[method-assign] - self._update_env = renderer._update_env # type: ignore[method-assign] - self.fallback_theme = renderer.fallback_theme - self.extra_css = renderer.extra_css - renderer.__class__.__init__( - self, - renderer._handler, - renderer._theme, - renderer._custom_templates, - ) - else: - if handler is None or theme is None: - raise ValueError("'handler' and 'theme' cannot be None") - BaseRenderer.__init__(self, handler, theme, custom_templates) - - @classmethod - def load_inventory( - cls, - in_file: BinaryIO, # noqa: ARG003 - url: str, # noqa: ARG003 - base_url: str | None = None, # noqa: ARG003 - **kwargs: Any, # noqa: ARG003 - ) -> Iterator[tuple[str, str]]: - """Yield items and their URLs from an inventory file streamed from `in_file`. - - Arguments: - 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. - **kwargs: Ignore additional arguments passed from the config. - - Yields: - Tuples of (item identifier, item URL). - """ - yield from () - - class Handlers: """A collection of handlers. @@ -543,7 +392,7 @@ def get_anchors(self, identifier: str) -> tuple[str, ...] | set[str]: """Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it. Arguments: - identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept). + identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseHandler.collect] can accept). Returns: A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it. @@ -606,18 +455,7 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand if handler_config is None: handler_config = self.get_handler_config(name) handler_config.update(self._config) - try: - module = importlib.import_module(f"mkdocstrings_handlers.{name}") - except ModuleNotFoundError: - module = importlib.import_module(f"mkdocstrings.handlers.{name}") - if name != "python": - warnings.warn( - DeprecationWarning( - "Using the mkdocstrings.handlers namespace is deprecated. " - "Handlers must now use the mkdocstrings_handlers namespace.", - ), - stacklevel=1, - ) + module = importlib.import_module(f"mkdocstrings_handlers.{name}") self._handlers[name] = module.get_handler( theme=self._config["theme_name"], custom_templates=self._config["mkdocstrings"]["custom_templates"], diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index c3fb236a..6009935a 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -68,11 +68,14 @@ def __init__(self, md: Markdown): md: The Markdown instance to read configs from. """ config: dict[str, Any] = {} + self._highlighter: str | None = None for ext in md.registeredExtensions: if isinstance(ext, HighlightExtension) and (ext.enabled or not config): + self._highlighter = "highlight" config = ext.getConfigs() break # This one takes priority, no need to continue looking if isinstance(ext, CodeHiliteExtension) and not config: + self._highlighter = "codehilite" config = ext.getConfigs() config["language_prefix"] = config["lang_prefix"] self._css_class = config.pop("css_class", "highlight") @@ -116,7 +119,11 @@ def highlight( self.linenums = old_linenums if inline: - return Markup(f'{result.text}') + # From the maintainer of codehilite, the codehilite CSS class, as defined by the user, + # should never be added to inline code, because codehilite does not support inline code. + # See https://github.com/Python-Markdown/markdown/issues/1220#issuecomment-1692160297. + css_class = "" if self._highlighter == "codehilite" else kwargs["css_class"] + return Markup(f'{result.text}') return Markup(result) diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index 42e9b3db..f1c8962a 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -20,7 +20,7 @@ def __init__( domain: str, role: str, uri: str, - priority: str = "1", + priority: int = 1, dispname: str | None = None, ): """Initialize the object. @@ -30,14 +30,14 @@ def __init__( domain: The item domain, like 'python' or 'crystal'. role: The item role, like 'class' or 'method'. uri: The item URI. - priority: The item priority. It can help for inventory suggestions. + priority: The item priority. Only used internally by mkdocstrings and Sphinx. dispname: The item display name. """ self.name: str = name self.domain: str = domain self.role: str = role self.uri: str = uri - self.priority: str = priority + self.priority: int = priority self.dispname: str = dispname or name def format_sphinx(self) -> str: @@ -67,7 +67,7 @@ def parse_sphinx(cls, line: str) -> InventoryItem: uri = uri[:-1] + name if dispname == "-": dispname = name - return cls(name, domain, role, uri, priority, dispname) + return cls(name, domain, role, uri, int(priority), dispname) class Inventory(dict): @@ -88,15 +88,33 @@ def __init__(self, items: list[InventoryItem] | None = None, project: str = "pro self.project = project self.version = version - def register(self, *args: str, **kwargs: str) -> None: + def register( + self, + name: str, + domain: str, + role: str, + uri: str, + priority: int = 1, + dispname: str | None = None, + ) -> None: """Create and register an item. Arguments: - *args: Arguments passed to [InventoryItem][mkdocstrings.inventory.InventoryItem]. - **kwargs: Keyword arguments passed to [InventoryItem][mkdocstrings.inventory.InventoryItem]. + name: The item name. + domain: The item domain, like 'python' or 'crystal'. + role: The item role, like 'class' or 'method'. + uri: The item URI. + priority: The item priority. Only used internally by mkdocstrings and Sphinx. + dispname: The item display name. """ - item = InventoryItem(*args, **kwargs) - self[item.name] = item + self[name] = InventoryItem( + name=name, + domain=domain, + role=role, + uri=uri, + priority=priority, + dispname=dispname, + ) def format_sphinx(self) -> bytes: """Format this inventory as a Sphinx `objects.inv` file. @@ -117,7 +135,10 @@ def format_sphinx(self) -> bytes: .encode("utf8") ) - lines = [item.format_sphinx().encode("utf8") for item in self.values()] + lines = [ + item.format_sphinx().encode("utf8") + for item in sorted(self.values(), key=lambda item: (item.domain, item.name)) + ] return header + zlib.compress(b"\n".join(lines) + b"\n", 9) @classmethod @@ -129,7 +150,7 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) domain_filter: A collection of domain values to allow (and filter out all other ones). Returns: - An `Inventory` containing the collected `InventoryItem`s. + An inventory containing the collected items. """ for _ in range(4): in_file.readline() diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 4c902935..484d3ead 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -34,7 +34,7 @@ if TYPE_CHECKING: from jinja2.environment import Environment from mkdocs.config import Config - from mkdocs.livereload import LiveReloadServer + from mkdocs.config.defaults import MkDocsConfig if sys.version_info < (3, 10): from typing_extensions import ParamSpec @@ -43,11 +43,6 @@ log = get_logger(__name__) -SELECTION_OPTS_KEY: str = "selection" -"""Deprecated. The name of the selection parameter in YAML configuration blocks.""" -RENDERING_OPTS_KEY: str = "rendering" -"""Deprecated. The name of the rendering parameter in YAML configuration blocks.""" - InventoryImportType = List[Tuple[str, Mapping[str, Any]]] InventoryLoaderType = Callable[..., Iterable[Tuple[str, str]]] @@ -75,14 +70,12 @@ class MkdocstringsPlugin(BasePlugin): - `on_config` - `on_env` - `on_post_build` - - `on_serve` Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs` for more information about its plugin system. """ - config_scheme: tuple[tuple[str, MkType]] = ( - ("watch", MkType(list, default=[])), # type: ignore[assignment] + config_scheme: tuple[tuple[str, MkType]] = ( # type: ignore[assignment] ("handlers", MkType(dict, default={})), ("default_handler", MkType(str, default="python")), ("custom_templates", MkType(str, default=None)), @@ -94,13 +87,12 @@ class MkdocstringsPlugin(BasePlugin): Available options are: - - **`watch` (deprecated)**: A list of directories to watch. Only used when serving the documentation with mkdocs. - Whenever a file changes in one of directories, the whole documentation is built again, and the browser refreshed. - Deprecated in favor of the now built-in `watch` feature of MkDocs. - - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". - - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. - **`handlers`**: Global configuration of handlers. You can set global configuration per handler, applied everywhere, but overridable in each "autodoc" instruction. Example: + - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". + - **`custom_templates`**: Custom templates to use when rendering API objects. + - **`enable_inventory`**: Whether to enable object inventory creation. + - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. ```yaml plugins: @@ -108,11 +100,11 @@ class MkdocstringsPlugin(BasePlugin): handlers: python: options: - selection_opt: true - rendering_opt: "value" + option1: true + option2: "value" rust: options: - selection_opt: 2 + option9: 2 ``` """ @@ -137,37 +129,7 @@ def handlers(self) -> Handlers: raise RuntimeError("The plugin hasn't been initialized with a config yet") return self._handlers - # TODO: remove once watch feature is removed - def on_serve( - self, - server: LiveReloadServer, - config: Config, # noqa: ARG002 - builder: Callable, - *args: Any, # noqa: ARG002 - **kwargs: Any, # noqa: ARG002 - ) -> None: - """Watch directories. - - Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve). - In this hook, we add the directories specified in the plugin's configuration to the list of directories - watched by `mkdocs`. Whenever a change occurs in one of these directories, the documentation is built again - and the site reloaded. - - Arguments: - server: The `livereload` server instance. - config: The MkDocs config object (unused). - builder: The function to build the site. - *args: Additional arguments passed by MkDocs. - **kwargs: Additional arguments passed by MkDocs. - """ - if not self.plugin_enabled: - return - if self.config["watch"]: - for element in self.config["watch"]: - log.debug(f"Adding directory '{element}' to watcher") - server.watch(element, builder) - - def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 + def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Instantiate our Markdown extension. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). @@ -179,7 +141,6 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 Arguments: config: The MkDocs config object. - **kwargs: Additional arguments passed by MkDocs. Returns: The modified config. @@ -238,9 +199,6 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 self._inv_futures[future] = (loader, import_item) inv_loader.shutdown(wait=False) - if self.config["watch"]: - self._warn_about_watch_option() - return config @property @@ -311,7 +269,7 @@ def on_post_build( For example, a handler could open a subprocess in the background and keep it open to feed it "autodoc" instructions and get back JSON data. If so, it should then close the subprocess at some point: - the proper place to do this is in the collector's `teardown` method, which is indirectly called by this hook. + the proper place to do this is in the handler's `teardown` method, which is indirectly called by this hook. Arguments: config: The MkDocs config object. @@ -362,11 +320,3 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - result = dict(loader(content, url=url, **kwargs)) log.debug(f"Loaded inventory from {url!r}: {len(result)} items") return result - - @classmethod - @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_watch_option(cls) -> None: - log.info( - "DEPRECATION: mkdocstrings' watch feature is deprecated in favor of MkDocs' watch feature, " - "see https://www.mkdocs.org/user-guide/configuration/#watch", - ) diff --git a/tests/conftest.py b/tests/conftest.py index a2ea6b26..2119d1f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Itera request = request._parent_request conf_dict = { + "config_file_path": "mkdocs_tests.yml", "site_name": "foo", "site_url": "https://example.org/", "site_dir": str(tmp_path), @@ -31,7 +32,7 @@ def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Itera **getattr(request, "param", {}), } # Re-create it manually as a workaround for https://github.com/mkdocs/mkdocs/issues/2289 - mdx_configs: dict[str, Any] = dict(ChainMap(*conf_dict.get("markdown_extensions", []))) # type: ignore[arg-type] + mdx_configs: dict[str, Any] = dict(ChainMap(*conf_dict.get("markdown_extensions", []))) conf.load_dict(conf_dict) assert conf.validate() == ([], []) diff --git a/tests/test_extension.py b/tests/test_extension.py index f7c3cecc..8c687629 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import re import sys from textwrap import dedent @@ -138,7 +137,7 @@ def test_use_custom_handler(ext_markdown: Markdown) -> None: def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> None: """Assert that we don't preemptively register all identifiers of a rendered object.""" handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] - ids = {"id1", "id2", "id3"} + ids = ("id1", "id2", "id3") handler.get_anchors = lambda _: ids # type: ignore[method-assign] ext_markdown.convert("::: tests.fixtures.headings") autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs @@ -147,14 +146,7 @@ def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ex assert identifier not in autorefs._abs_url_map -def test_use_deprecated_yaml_keys(ext_markdown: Markdown, caplog: pytest.LogCaptureFixture) -> None: - """Check that using the deprecated 'selection' and 'rendering' YAML keys emits a deprecation warning.""" - caplog.set_level(logging.INFO) - assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n rendering:\n heading_level: 2") - assert "single 'options' YAML key" in caplog.text - - -def test_use_new_options_yaml_key(ext_markdown: Markdown) -> None: - """Check that using the new 'options' YAML key works as expected.""" +def test_use_options_yaml_key(ext_markdown: Markdown) -> None: + """Check that using the 'options' YAML key works as expected.""" assert "h1" in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 1") assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 777173e8..4a07e98b 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -2,17 +2,17 @@ from __future__ import annotations -from contextlib import suppress -from pathlib import Path from typing import TYPE_CHECKING import pytest from jinja2.exceptions import TemplateNotFound from markdown import Markdown -from mkdocstrings.handlers.base import BaseRenderer, Highlighter +from mkdocstrings.handlers.base import Highlighter if TYPE_CHECKING: + from pathlib import Path + from mkdocstrings.plugin import MkdocstringsPlugin @@ -32,7 +32,7 @@ def test_highlighter_without_pygments(extension_name: str) -> None: ) assert ( hl.highlight("import foo", language="python", inline=True) - == 'import foo' + == f'import foo' ) @@ -53,30 +53,25 @@ def test_highlighter_basic(extension_name: str | None, inline: bool) -> None: assert "import foo" not in actual # Highlighting has split it up. -@pytest.fixture(name="extended_templates") -def fixture_extended_templates(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: # noqa: D103 - monkeypatch.setattr(BaseRenderer, "get_extended_templates_dirs", lambda self, handler: [tmp_path]) - return tmp_path - - -def test_extended_templates(extended_templates: Path, plugin: MkdocstringsPlugin) -> None: +def test_extended_templates(tmp_path: Path, plugin: MkdocstringsPlugin) -> None: """Test the extended templates functionality. Parameters: - extended_templates: Temporary folder. + tmp_path: Temporary folder. plugin: Instance of our plugin. """ handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] - # assert mocked method added temp path to loader - search_paths = handler.env.loader.searchpath # type: ignore[union-attr] - assert any(str(extended_templates) in path for path in search_paths) + # monkeypatch Jinja env search path + search_paths = [ + base_theme := tmp_path / "base_theme", + base_fallback_theme := tmp_path / "base_fallback_theme", + extended_theme := tmp_path / "extended_theme", + extended_fallback_theme := tmp_path / "extended_fallback_theme", + ] + handler.env.loader.searchpath = search_paths # type: ignore[union-attr] # assert "new" template is not found - for path in search_paths: - # TODO: use missing_ok=True once support for Python 3.7 is dropped - with suppress(FileNotFoundError): - Path(path).joinpath("new.html").unlink() with pytest.raises(expected_exception=TemplateNotFound): handler.env.get_template("new.html") @@ -84,20 +79,18 @@ def test_extended_templates(extended_templates: Path, plugin: MkdocstringsPlugin # start with last one and go back up handler.env.cache = None - extended_fallback_theme = extended_templates.joinpath(handler.fallback_theme) extended_fallback_theme.mkdir() extended_fallback_theme.joinpath("new.html").write_text("extended fallback new") assert handler.env.get_template("new.html").render() == "extended fallback new" - extended_theme = extended_templates.joinpath("mkdocs") extended_theme.mkdir() extended_theme.joinpath("new.html").write_text("extended new") assert handler.env.get_template("new.html").render() == "extended new" - base_fallback_theme = Path(search_paths[1]) + base_fallback_theme.mkdir() base_fallback_theme.joinpath("new.html").write_text("base fallback new") assert handler.env.get_template("new.html").render() == "base fallback new" - base_theme = Path(search_paths[0]) + base_theme.mkdir() base_theme.joinpath("new.html").write_text("base new") assert handler.env.get_template("new.html").render() == "base new"