-
diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md
deleted file mode 100644
index 1df4608b..00000000
--- a/docs/insiders/installation.md
+++ /dev/null
@@ -1,67 +0,0 @@
----
-title: Getting started with Insiders
----
-
-# Getting started with Insiders
-
-*mkdocstrings Insiders* is a compatible drop-in replacement for *mkdocstrings*, and can be installed similarly using `pip` or `git`. Note that in order to access the Insiders repository, you need to [become an eligible sponsor][] of @pawamoy on GitHub.
-
-## Installation
-
-### with the `insiders` tool
-
-[`insiders`][insiders-tool] is a tool that helps you keep up-to-date versions of Insiders projects in the PyPI index of your choice (self-hosted, Google registry, Artifactory, etc.).
-
-**We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use][].**
-
-### with pip (ssh/https)
-
-*mkdocstrings Insiders* can be installed with `pip` [using SSH][install-pip-ssh]:
-
-```bash
-pip install git+ssh://git@github.com/pawamoy-insiders/mkdocstrings.git
-```
-
-Or using HTTPS:
-
-```bash
-pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/mkdocstrings.git
-```
-
->? NOTE: **How to get a GitHub personal access token?** The `GH_TOKEN` environment variable is a GitHub token. It can be obtained by creating a [personal access token][github-pat] for your GitHub account. It will give you access to the Insiders repository, programmatically, from the command line or GitHub Actions workflows:
->
-> 1. Go to https://github.com/settings/tokens
-> 2. Click on [Generate a new token][github-pat-new]
-> 3. Enter a name and select the [`repo`][scopes] scope
-> 4. Generate the token and store it in a safe place
->
-> Note that the personal access token must be kept secret at all times, as it allows the owner to access your private repositories.
-
-### with Git
-
-Of course, you can use *mkdocstrings Insiders* directly using Git:
-
-```
-git clone git@github.com:pawamoy-insiders/mkdocstrings
-```
-
-When cloning with Git, the package must be installed:
-
-```
-pip install -e mkdocstrings
-```
-
-## Upgrading
-
-When upgrading Insiders, you should always check the version of *mkdocstrings* which makes up the first part of the version qualifier. For example, a version like `8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`.
-
-If the major version increased, it's a good idea to consult the [changelog][] and go through the steps to ensure your configuration is up to date and all necessary changes have been made.
-
-[become an eligible sponsor]: ./index.md#how-to-become-a-sponsor
-[changelog]: ./changelog.md
-[github-pat]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token
-[github-pat-new]: https://github.com/settings/tokens/new
-[insiders-tool]: https://pawamoy.github.io/insiders-project/
-[install-pip-ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh
-[scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes
-[terms of use]: ./index.md#terms
diff --git a/docs/js/insiders.js b/docs/js/insiders.js
deleted file mode 100644
index 8bb68485..00000000
--- a/docs/js/insiders.js
+++ /dev/null
@@ -1,74 +0,0 @@
-function humanReadableAmount(amount) {
- const strAmount = String(amount);
- if (strAmount.length >= 4) {
- return `${strAmount.slice(0, strAmount.length - 3)},${strAmount.slice(-3)}`;
- }
- return strAmount;
-}
-
-function getJSON(url, callback) {
- var xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.responseType = 'json';
- xhr.onload = function () {
- var status = xhr.status;
- if (status === 200) {
- callback(null, xhr.response);
- } else {
- callback(status, xhr.response);
- }
- };
- xhr.send();
-}
-
-function updatePremiumSponsors(dataURL, rank) {
- let capRank = rank.charAt(0).toUpperCase() + rank.slice(1);
- getJSON(dataURL + `/sponsors${capRank}.json`, function (err, sponsors) {
- const sponsorsDiv = document.getElementById(`${rank}-sponsors`);
- if (sponsors.length > 0) {
- let html = '';
- html += `
${capRank} sponsors`
- sponsors.forEach(function (sponsor) {
- html += `
-
-
-
- `
- });
- html += '
'
- sponsorsDiv.innerHTML = html;
- }
- });
-}
-
-function updateInsidersPage(author_username) {
- const sponsorURL = `https://github.com/sponsors/${author_username}`
- const dataURL = `https://raw.githubusercontent.com/${author_username}/sponsors/main`;
- getJSON(dataURL + '/numbers.json', function (err, numbers) {
- document.getElementById('sponsors-count').innerHTML = numbers.count;
- Array.from(document.getElementsByClassName('sponsors-total')).forEach(function (element) {
- element.innerHTML = '$ ' + humanReadableAmount(numbers.total);
- });
- getJSON(dataURL + '/sponsors.json', function (err, sponsors) {
- const sponsorsElem = document.getElementById('sponsors');
- const privateSponsors = numbers.count - sponsors.length;
- sponsors.forEach(function (sponsor) {
- sponsorsElem.innerHTML += `
-
- `;
- });
- if (privateSponsors > 0) {
- sponsorsElem.innerHTML += `
-
- `;
- }
- });
- });
- updatePremiumSponsors(dataURL, "gold");
- updatePremiumSponsors(dataURL, "silver");
- updatePremiumSponsors(dataURL, "bronze");
-}
diff --git a/docs/reference/mkdocstrings.md b/docs/reference/api.md
similarity index 100%
rename from docs/reference/mkdocstrings.md
rename to docs/reference/api.md
diff --git a/docs/schema.json b/docs/schema.json
index bd646f88..66197827 100644
--- a/docs/schema.json
+++ b/docs/schema.json
@@ -28,6 +28,12 @@
"type": "string",
"default": "python"
},
+ "locale": {
+ "title": "The locale to use for translations.",
+ "markdownDescription": "https://mkdocstrings.github.io/usage/#global-options",
+ "type": "string",
+ "default": null
+ },
"enable_inventory": {
"title": "Whether to enable inventory file generation.",
"markdownDescription": "https://mkdocstrings.github.io/usage/#cross-references-to-other-projects-inventories",
diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md
index b9a01f68..6f326431 100644
--- a/docs/usage/handlers.md
+++ b/docs/usage/handlers.md
@@ -4,12 +4,14 @@ A handler is what makes it possible to collect and render documentation for a pa
## Available handlers
-- [C](https://mkdocstrings.github.io/c/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md)
+- [C](https://mkdocstrings.github.io/c/){ .external }
- [Crystal](https://mkdocstrings.github.io/crystal/){ .external }
+- [GitHub Actions](https://watermarkhu.nl/mkdocstrings-github/){ .external }
- [Python](https://mkdocstrings.github.io/python/){ .external }
- [Python (Legacy)](https://mkdocstrings.github.io/python-legacy/){ .external }
+- [MATLAB](https://watermarkhu.nl/mkdocstrings-matlab/){ .external }
- [Shell](https://mkdocstrings.github.io/shell/){ .external }
-- [TypeScript](https://mkdocstrings.github.io/typescript/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md)
+- [TypeScript](https://mkdocstrings.github.io/typescript/){ .external }
- [VBA](https://pypi.org/project/mkdocstrings-vba/){ .external }
## About the Python handlers
@@ -203,7 +205,7 @@ to use the templates of another handler. In you handler, override the
```python
from pathlib import Path
-from mkdocstrings.handlers.base import BaseHandler
+from mkdocstrings import BaseHandler
class CobraHandler(BaseHandler):
diff --git a/docs/usage/index.md b/docs/usage/index.md
index ea9716cc..64588fdf 100644
--- a/docs/usage/index.md
+++ b/docs/usage/index.md
@@ -16,7 +16,7 @@ The syntax is as follows:
> Here are some resources that other users found useful to better
> understand YAML's peculiarities.
>
-> - [YAML idiosyncrasies](https://docs.saltproject.io/en/3000/topics/troubleshooting/yaml_idiosyncrasies.html)
+> - [YAML idiosyncrasies](https://salt-zh.readthedocs.io/en/latest/topics/troubleshooting/yaml_idiosyncrasies.html)
> - [YAML multiline](https://yaml-multiline.info/)
The `identifier` is a string identifying the object you want to document.
@@ -113,6 +113,7 @@ The above is equivalent to:
- `handlers`: The handlers' global configuration.
- `enable_inventory`: Whether to enable inventory file generation.
See [Cross-references to other projects / inventories](#cross-references-to-other-projects-inventories)
+- `locale`: The locale used for translations. See [Internationalization](#internationalization-i18n).
- `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).
@@ -124,6 +125,7 @@ The above is equivalent to:
enabled: !ENV [ENABLE_MKDOCSTRINGS, true]
custom_templates: templates
default_handler: python
+ locale: en
handlers:
python:
options:
@@ -141,6 +143,16 @@ The above is equivalent to:
Some handlers accept additional global configuration.
Check the documentation for your handler of interest in [Handlers](handlers.md).
+## Internationalization (I18N)
+
+Some handlers support multiple languages.
+
+If the handler supports localization, the locale it uses is determined by the following order of precedence:
+
+- `locale` in [global options](#global-options)
+- `theme.language`: used by the [MkDocs Material theme](https://squidfunk.github.io/mkdocs-material/setup/changing-the-language/)
+- `theme.locale` in [MkDocs configuration](https://www.mkdocs.org/user-guide/configuration/#theme)
+
## Cross-references
Cross-references are written as Markdown *reference-style* links:
diff --git a/duties.py b/duties.py
index 6ee9b08d..dfc81027 100644
--- a/duties.py
+++ b/duties.py
@@ -5,17 +5,12 @@
import os
import re
import sys
-from contextlib import contextmanager
-from functools import wraps
-from importlib.metadata import version as pkgversion
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING
from duty import duty, tools
if TYPE_CHECKING:
- from collections.abc import Iterator
-
from duty.context import Context
@@ -26,6 +21,8 @@
WINDOWS = os.name == "nt"
PTY = not WINDOWS and not CI
MULTIRUN = os.environ.get("MULTIRUN", "0") == "1"
+PY_VERSION = f"{sys.version_info.major}{sys.version_info.minor}"
+PY_DEV = "315"
def pyprefix(title: str) -> str:
@@ -35,37 +32,10 @@ def pyprefix(title: str) -> str:
return title
-def not_from_insiders(func: Callable) -> Callable:
- @wraps(func)
- def wrapper(ctx: Context, *args: Any, **kwargs: Any) -> None:
- origin = ctx.run("git config --get remote.origin.url", silent=True)
- if "pawamoy-insiders/griffe" in origin:
- ctx.run(
- lambda: False,
- title="Not running this task from insiders repository (do that from public repo instead!)",
- )
- return
- func(ctx, *args, **kwargs)
-
- return wrapper
-
-
-@contextmanager
-def material_insiders() -> Iterator[bool]:
- if "+insiders" in pkgversion("mkdocs-material"):
- os.environ["MATERIAL_INSIDERS"] = "true"
- try:
- yield True
- finally:
- os.environ.pop("MATERIAL_INSIDERS")
- else:
- yield False
-
-
def _get_changelog_version() -> str:
changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$")
with Path(__file__).parent.joinpath("CHANGELOG.md").open("r", encoding="utf8") as file:
- return next(filter(bool, map(changelog_version_re.match, file))).group(1) # type: ignore[union-attr]
+ return next(filter(bool, map(changelog_version_re.match, file))).group(1) # ty: ignore[invalid-argument-type,unresolved-attribute]
@duty
@@ -84,39 +54,41 @@ def check(ctx: Context) -> None:
"""Check it all!"""
-@duty
+@duty(nofail=PY_VERSION == PY_DEV)
def check_quality(ctx: Context) -> None:
"""Check the code quality."""
ctx.run(
- tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"),
+ tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", color=True),
title=pyprefix("Checking code quality"),
)
-@duty
+@duty(nofail=PY_VERSION == PY_DEV)
def check_docs(ctx: Context) -> None:
"""Check if the documentation builds correctly."""
- Path("htmlcov").mkdir(parents=True, exist_ok=True)
- Path("htmlcov/index.html").touch(exist_ok=True)
- with material_insiders():
- ctx.run(
- tools.mkdocs.build(strict=True, verbose=True),
- title=pyprefix("Building documentation"),
- )
+ ctx.run(
+ tools.zensical.build(strict=True),
+ title=pyprefix("Building documentation"),
+ )
-@duty
+@duty(nofail=PY_VERSION == PY_DEV)
def check_types(ctx: Context) -> None:
"""Check that the code is correctly typed."""
- os.environ["MYPYPATH"] = "src"
- os.environ["FORCE_COLOR"] = "1"
+ py = f"{sys.version_info.major}.{sys.version_info.minor}"
ctx.run(
- tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"),
+ tools.ty.check(
+ *PY_SRC_LIST,
+ config_file="config/ty.toml",
+ color=True,
+ error_on_warning=True,
+ python_version=py,
+ ),
title=pyprefix("Type-checking"),
)
-@duty
+@duty(nofail=PY_VERSION == PY_DEV)
def check_api(ctx: Context, *cli_args: str) -> None:
"""Check for API breaking changes."""
ctx.run(
@@ -134,48 +106,32 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000
host: The host to serve the docs from.
port: The port to serve the docs on.
"""
- with material_insiders():
- ctx.run(
- tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args),
- title="Serving documentation",
- capture=False,
- )
+ ctx.run(
+ tools.zensical.serve(dev_addr=f"{host}:{port}").add_args(*cli_args),
+ title="Serving documentation",
+ capture=False,
+ )
@duty
-def docs_deploy(ctx: Context, *, force: bool = False) -> None:
- """Deploy the documentation to GitHub pages.
+def docs_deploy(ctx: Context) -> None:
+ """Deploy the documentation to GitHub pages."""
+ from ghp_import import ghp_import # noqa: PLC0415
- Parameters:
- force: Whether to force deployment, even from non-Insiders version.
- """
- os.environ["DEPLOY"] = "true"
- with material_insiders() as insiders:
- if not insiders:
- ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!")
- origin = ctx.run("git config --get remote.origin.url", silent=True, allow_overrides=False)
- if "pawamoy-insiders/mkdocstrings" in origin:
- ctx.run(
- "git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io",
- silent=True,
- nofail=True,
- allow_overrides=False,
- )
- ctx.run(
- tools.mkdocs.gh_deploy(remote_name="org-pages", force=True),
- title="Deploying documentation",
- )
- elif force:
- ctx.run(
- tools.mkdocs.gh_deploy(remote_name="org-pages", force=True),
- title="Deploying documentation",
- )
- else:
- ctx.run(
- lambda: False,
- title="Not deploying docs from public repository (do that from insiders instead!)",
- nofail=True,
- )
+ ctx.run(tools.zensical.build(), title="Building documentation site")
+ ctx.run(
+ ghp_import,
+ kwargs={
+ "srcdir": "site",
+ "mesg": "chore: Update documentation",
+ "push": True,
+ "force": True,
+ "remote": "org-pages",
+ },
+ title="Deploying site to GitHub Pages",
+ command="ghp-import site -r org-pages -fpm 'chore: Update documentation'",
+ pty=PTY,
+ )
@duty
@@ -192,28 +148,26 @@ def format(ctx: Context) -> None:
def build(ctx: Context) -> None:
"""Build source and wheel distributions."""
ctx.run(
- tools.build(),
- title="Building source and wheel distributions",
+ ["uv", "build"],
+ title="Building distributions",
pty=PTY,
)
@duty
-@not_from_insiders
def publish(ctx: Context) -> None:
"""Publish source and wheel distributions to PyPI."""
if not Path("dist").exists():
ctx.run("false", title="No distribution files found")
- dists = [str(dist) for dist in Path("dist").iterdir()]
+ dists = [str(dist) for dist in Path("dist").iterdir() if dist.suffix in (".gz", ".whl")]
ctx.run(
tools.twine.upload(*dists, skip_existing=True),
- title="Publishing source and wheel distributions to PyPI",
+ title="Publishing distributions to PyPI",
pty=PTY,
)
@duty(post=["build", "publish", "docs-deploy"])
-@not_from_insiders
def release(ctx: Context, version: str = "") -> None:
"""Release a new Python package.
@@ -224,7 +178,7 @@ def release(ctx: Context, version: str = "") -> None:
ctx.run("false", title="A version must be provided")
ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY)
ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY)
- ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY)
+ ctx.run(f"git tag -m '' -a {version}", title="Tagging commit", pty=PTY)
ctx.run("git push", title="Pushing commits", pty=False)
ctx.run("git push --tags", title="Pushing tags", pty=False)
@@ -237,20 +191,15 @@ def coverage(ctx: Context) -> None:
ctx.run(tools.coverage.html(rcfile="config/coverage.ini"))
-@duty
-def test(ctx: Context, *cli_args: str, match: str = "") -> None:
- """Run the test suite.
-
- Parameters:
- 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}"
+@duty(nofail=PY_VERSION == PY_DEV)
+def test(ctx: Context, *cli_args: str) -> None:
+ """Run the test suite."""
+ os.environ["COVERAGE_FILE"] = f".coverage.{PY_VERSION}"
+ os.environ["PYTHONWARNDEFAULTENCODING"] = "1"
ctx.run(
tools.pytest(
"tests",
config_file="config/pytest.ini",
- select=match,
color="yes",
).add_args("-n", "auto", *cli_args),
title=pyprefix("Running tests"),
diff --git a/mkdocs.yml b/mkdocs.yml
deleted file mode 100644
index 828a81a4..00000000
--- a/mkdocs.yml
+++ /dev/null
@@ -1,220 +0,0 @@
-site_name: "mkdocstrings"
-site_description: "Automatic documentation from sources, for MkDocs."
-site_url: "https://mkdocstrings.github.io/"
-repo_url: "https://github.com/mkdocstrings/mkdocstrings"
-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:
- - Overview: index.md
- - Changelog: changelog.md
- - Credits: credits.md
- - License: license.md
-- Usage:
- - usage/index.md
- - Theming: usage/theming.md
- - Handlers: usage/handlers.md
- - All handlers:
- - C: https://mkdocstrings.github.io/c/
- - Crystal: https://mkdocstrings.github.io/crystal/
- - Python: https://mkdocstrings.github.io/python/
- - Python (Legacy): https://mkdocstrings.github.io/python-legacy/
- - Shell: https://mkdocstrings.github.io/shell/
- - TypeScript: https://mkdocstrings.github.io/typescript/
- - VBA: https://pypi.org/project/mkdocstrings-vba
- - Guides:
- - Recipes: recipes.md
- - Troubleshooting: troubleshooting.md
-- API reference: reference/mkdocstrings.md
-- Development:
- - Contributing: contributing.md
- - Code of Conduct: code_of_conduct.md
- - Coverage report: coverage.md
-- Insiders:
- - insiders/index.md
- - Getting started:
- - Installation: insiders/installation.md
- - Changelog: insiders/changelog.md
-- Author's website: https://pawamoy.github.io/
-
-theme:
- name: material
- logo: logo.svg
- custom_dir: docs/.overrides
- features:
- - announce.dismiss
- - content.action.edit
- - content.action.view
- - content.code.annotate
- - content.code.copy
- - content.tooltips
- - navigation.footer
- - navigation.instant.preview
- - navigation.path
- - navigation.sections
- - navigation.tabs
- - navigation.tabs.sticky
- - navigation.top
- - search.highlight
- - search.suggest
- - toc.follow
- palette:
- - media: "(prefers-color-scheme)"
- toggle:
- icon: material/brightness-auto
- name: Switch to light mode
- - media: "(prefers-color-scheme: light)"
- scheme: default
- primary: teal
- accent: purple
- toggle:
- icon: material/weather-sunny
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: black
- accent: lime
- toggle:
- icon: material/weather-night
- name: Switch to system preference
-
-extra_css:
-- css/style.css
-- css/material.css
-- css/mkdocstrings.css
-- css/insiders.css
-
-extra_javascript:
-- js/feedback.js
-
-markdown_extensions:
-- attr_list
-- admonition
-- callouts:
- strip_period: false
-- footnotes
-- pymdownx.details
-- pymdownx.emoji:
- emoji_index: !!python/name:material.extensions.emoji.twemoji
- emoji_generator: !!python/name:material.extensions.emoji.to_svg
-- pymdownx.highlight:
- pygments_lang_class: true
-- pymdownx.magiclink
-- pymdownx.snippets:
- base_path: [!relative $config_dir]
- check_paths: true
-- pymdownx.superfences
-- pymdownx.tabbed:
- alternate_style: true
- slugify: !!python/object/apply:pymdownx.slugs.slugify
- kwds:
- case: lower
-- pymdownx.tasklist:
- custom_checkbox: true
-- pymdownx.tilde
-- toc:
- permalink: "¤"
-
-plugins:
-- search
-- autorefs
-- markdown-exec
-- section-index
-- coverage
-- mkdocstrings:
- handlers:
- python:
- inventories:
- - https://docs.python.org/3/objects.inv
- - https://installer.readthedocs.io/en/stable/objects.inv # demonstration purpose in the docs
- - https://mkdocstrings.github.io/autorefs/objects.inv
- - https://www.mkdocs.org/objects.inv
- - https://python-markdown.github.io/objects.inv
- - https://jinja.palletsprojects.com/en/stable/objects.inv
- - https://markupsafe.palletsprojects.com/en/stable/objects.inv
- paths: [src]
- options:
- docstring_options:
- ignore_init_summary: true
- docstring_section_style: list
- filters: ["!^_"]
- heading_level: 1
- inherited_members: true
- merge_init_into_class: true
- parameter_headings: true
- separate_signature: true
- show_root_heading: true
- show_root_full_path: false
- show_signature_annotations: true
- show_source: false
- show_symbol_type_heading: true
- show_symbol_type_toc: true
- signature_crossrefs: true
- summary: true
-- llmstxt:
- files:
- - output: llms-full.txt
- inputs:
- - index.md
- - usage/index.md
- - usage/handlers.md
- - usage/theming.md
- - recipes.md
- - troubleshooting.md
- - reference/**.md
-- git-revision-date-localized:
- enabled: !ENV [DEPLOY, false]
- enable_creation_date: true
- type: timeago
-- redirects:
- redirect_maps:
- theming.md: usage/theming.md
- handlers/overview.md: usage/handlers.md
- reference/index.md: reference/mkdocstrings.md#mkdocstrings
- reference/extension.md: reference/mkdocstrings.md#mkdocstrings.extension
- reference/handlers/index.md: reference/mkdocstrings.md#mkdocstrings.handlers
- reference/handlers/base.md: reference/mkdocstrings.md#mkdocstrings.handlers.base
- reference/handlers/rendering.md: reference/mkdocstrings.md#mkdocstrings.handlers.rendering
- reference/inventory.md: reference/mkdocstrings.md#mkdocstrings.inventory
- reference/loggers.md: reference/mkdocstrings.md#mkdocstrings.loggers
- reference/plugin.md: reference/mkdocstrings.md#mkdocstrings.plugin
-- minify:
- minify_html: !ENV [DEPLOY, false]
-- group:
- enabled: !ENV [MATERIAL_INSIDERS, false]
- plugins:
- - typeset
-
-extra:
- social:
- - icon: fontawesome/brands/github
- link: https://github.com/pawamoy
- - icon: fontawesome/brands/mastodon
- link: https://fosstodon.org/@pawamoy
- - icon: fontawesome/brands/twitter
- link: https://twitter.com/pawamoy
- - icon: fontawesome/brands/gitter
- link: https://gitter.im/mkdocstrings/community
- - icon: fontawesome/brands/python
- link: https://pypi.org/project/mkdocstrings/
- analytics:
- feedback:
- title: Was this page helpful?
- ratings:
- - icon: material/emoticon-happy-outline
- name: This page was helpful
- data: 1
- note: Thanks for your feedback!
- - icon: material/emoticon-sad-outline
- name: This page could be improved
- data: 0
- note: Let us know how we can improve this page.
diff --git a/pyproject.toml b/pyproject.toml
index c3087f61..9c722187 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -9,7 +9,7 @@ authors = [{name = "Timothée Mazzucotelli", email = "dev@pawamoy.fr"}]
license = "ISC"
license-files = ["LICENSE"]
readme = "README.md"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"]
dynamic = ["version"]
classifiers = [
@@ -18,7 +18,6 @@ classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -31,14 +30,12 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
- "Jinja2>=2.11.1",
+ "Jinja2>=3.1",
"Markdown>=3.6",
"MarkupSafe>=1.1",
"mkdocs>=1.6",
"mkdocs-autorefs>=1.4",
"pymdown-extensions>=6.3",
- "importlib-metadata>=4.6; python_version < '3.10'",
- "typing-extensions>=4.1; python_version < '3.10'",
]
[project.optional-dependencies]
@@ -73,7 +70,7 @@ source-includes = [
"share",
"tests",
"duties.py",
- "mkdocs.yml",
+ "zensical.toml",
"*.md",
"LICENSE",
]
@@ -88,7 +85,6 @@ data = [
[dependency-groups]
maintain = [
- "build>=1.2",
"git-changelog>=2.5",
"twine>=5.1",
"yore>=0.3.3",
@@ -96,29 +92,24 @@ maintain = [
ci = [
"dirty-equals>=0.9",
"duty>=1.6",
- "ruff>=0.4",
+ "griffe>=2.0",
"pytest>=8.2",
"pytest-cov>=5.0",
"pytest-randomly>=3.15",
"pytest-xdist>=3.6",
- "mypy>=1.10",
+ "ruff>=0.4",
+ "ty>=0.0.14",
"types-markdown>=3.6",
"types-pyyaml>=6.0",
]
docs = [
+ "ghp-import>=2.1",
"markdown-callouts>=0.4",
"markdown-exec>=1.8",
- "mkdocs>=1.6",
- "mkdocs-coverage>=1.0",
- "mkdocs-git-revision-date-localized-plugin>=1.2",
- "mkdocs-llmstxt>=0.1",
- "mkdocs-material>=9.5",
- "mkdocs-minify-plugin>=0.8",
- "mkdocs-redirects>=1.2.1",
- "mkdocs-section-index>=0.3",
- "mkdocstrings-python>=1.16.2",
+ "mkdocstrings[python]>=0.29",
# YORE: EOL 3.10: Remove line.
"tomli>=2.0; python_version < '3.11'",
+ "zensical>=0.0.21",
]
[tool.uv]
diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py
index b5499b7a..e8712895 100644
--- a/scripts/gen_credits.py
+++ b/scripts/gen_credits.py
@@ -2,7 +2,6 @@
from __future__ import annotations
-import os
import sys
from collections import defaultdict
from collections.abc import Iterable
@@ -10,7 +9,6 @@
from itertools import chain
from pathlib import Path
from textwrap import dedent
-from typing import Union
from jinja2 import StrictUndefined
from jinja2.sandbox import SandboxedEnvironment
@@ -22,14 +20,14 @@
else:
import tomli as tomllib
-project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", "."))
+project_dir = Path.cwd()
with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file:
pyproject = tomllib.load(pyproject_file)
project = pyproject["project"]
project_name = project["name"]
devdeps = [dep for group in pyproject["dependency-groups"].values() for dep in group if not dep.startswith("-e")]
-PackageMetadata = dict[str, Union[str, Iterable[str]]]
+PackageMetadata = dict[str, str | Iterable[str]]
Metadata = dict[str, PackageMetadata]
@@ -47,7 +45,7 @@ def _norm_name(name: str) -> str:
return name.replace("_", "-").replace(".", "-").lower()
-def _requirements(deps: list[str]) -> dict[str, Requirement]:
+def _requirements(deps: Iterable[str]) -> dict[str, Requirement]:
return {_norm_name((req := Requirement(dep)).name): req for dep in deps}
@@ -63,8 +61,8 @@ def _extra_marker(req: Requirement) -> str | None:
def _get_metadata() -> Metadata:
metadata = {}
for pkg in distributions():
- name = _norm_name(pkg.name) # type: ignore[attr-defined,unused-ignore]
- metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type]
+ name = _norm_name(pkg.name)
+ metadata[name] = _merge_fields(pkg.metadata) # ty: ignore[invalid-argument-type]
metadata[name]["spec"] = set()
metadata[name]["extras"] = set()
metadata[name].setdefault("summary", "")
@@ -77,10 +75,11 @@ def _set_license(metadata: PackageMetadata) -> None:
license_name = license_field if isinstance(license_field, str) else " + ".join(license_field)
check_classifiers = license_name in ("UNKNOWN", "Dual License", "") or license_name.count("\n")
if check_classifiers:
- license_names = []
- for classifier in metadata["classifier"]:
- if classifier.startswith("License ::"):
- license_names.append(classifier.rsplit("::", 1)[1].strip())
+ license_names = [
+ classifier.rsplit("::", 1)[1].strip()
+ for classifier in metadata["classifier"]
+ if classifier.startswith("License ::")
+ ]
license_name = " + ".join(license_names)
metadata["license"] = license_name or "?"
@@ -90,8 +89,8 @@ def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata
for dep_name, dep_req in base_deps.items():
if dep_name not in metadata or dep_name == "mkdocstrings":
continue
- metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # type: ignore[operator]
- metadata[dep_name]["extras"] |= dep_req.extras # type: ignore[operator]
+ metadata[dep_name]["spec"] |= {str(spec) for spec in dep_req.specifier} # ty: ignore[unsupported-operator]
+ metadata[dep_name]["extras"] |= dep_req.extras # ty: ignore[unsupported-operator]
deps[dep_name] = metadata[dep_name]
again = True
@@ -109,7 +108,7 @@ def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata
and dep_name != project["name"]
and (not extra_marker or extra_marker in deps[pkg_name]["extras"])
):
- metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # type: ignore[operator]
+ metadata[dep_name]["spec"] |= {str(spec) for spec in requirement.specifier} # ty: ignore[unsupported-operator]
deps[dep_name] = metadata[dep_name]
again = True
@@ -121,7 +120,7 @@ def _render_credits() -> str:
dev_dependencies = _get_deps(_requirements(devdeps), metadata)
prod_dependencies = _get_deps(
_requirements(
- chain( # type: ignore[arg-type]
+ chain(
project.get("dependencies", []),
chain(*project.get("optional-dependencies", {}).values()),
),
diff --git a/scripts/get_version.py b/scripts/get_version.py
index 6734e5b6..d56f5858 100644
--- a/scripts/get_version.py
+++ b/scripts/get_version.py
@@ -4,7 +4,12 @@
from contextlib import suppress
from pathlib import Path
-from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm
+from pdm.backend.hooks.version import ( # ty: ignore[unresolved-import]
+ SCMVersion,
+ Version,
+ default_version_formatter,
+ get_version_from_scm,
+)
_root = Path(__file__).parent.parent
_changelog = _root / "CHANGELOG.md"
@@ -17,7 +22,7 @@ def get_version() -> str:
if scm_version.version <= Version("0.1"): # Missing Git tags?
with suppress(OSError, StopIteration): # noqa: SIM117
with _changelog.open("r", encoding="utf8") as file:
- match = next(filter(None, map(_changelog_version_re.match, file)))
+ match = next(filter(None, map(_changelog_version_re.match, file))) # ty: ignore[invalid-argument-type]
scm_version = scm_version._replace(version=Version(match.group(1)))
return default_version_formatter(scm_version)
diff --git a/scripts/insiders.py b/scripts/insiders.py
deleted file mode 100644
index 6535a31e..00000000
--- a/scripts/insiders.py
+++ /dev/null
@@ -1,173 +0,0 @@
-# Functions related to Insiders funding goals.
-
-from __future__ import annotations
-
-import json
-import logging
-import os
-import posixpath
-from dataclasses import dataclass
-from datetime import date, datetime, timedelta
-from itertools import chain
-from pathlib import Path
-from typing import TYPE_CHECKING, cast
-from urllib.error import HTTPError
-from urllib.parse import urljoin
-from urllib.request import urlopen
-
-import yaml
-
-if TYPE_CHECKING:
- from collections.abc import Iterable
-
-logger = logging.getLogger(f"mkdocs.logs.{__name__}")
-
-
-def human_readable_amount(amount: int) -> str:
- str_amount = str(amount)
- if len(str_amount) >= 4: # noqa: PLR2004
- return f"{str_amount[: len(str_amount) - 3]},{str_amount[-3:]}"
- return str_amount
-
-
-@dataclass
-class Project:
- name: str
- url: str
-
-
-@dataclass
-class Feature:
- name: str
- ref: str | None
- since: date | None
- project: Project | None
-
- def url(self, rel_base: str = "..") -> str | None: # noqa: D102
- if not self.ref:
- return None
- if self.project:
- rel_base = self.project.url
- return posixpath.join(rel_base, self.ref.lstrip("/"))
-
- def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102
- new = ""
- if badge:
- recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011
- if recent:
- ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr]
- new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}'
- project = f"[{self.project.name}]({self.project.url}) — " if self.project else ""
- feature = f"[{self.name}]({self.url(rel_base)})" if self.ref else self.name
- print(f"- [{'x' if self.since else ' '}] {project}{feature}{new}")
-
-
-@dataclass
-class Goal:
- name: str
- amount: int
- features: list[Feature]
- complete: bool = False
-
- @property
- def human_readable_amount(self) -> str: # noqa: D102
- return human_readable_amount(self.amount)
-
- def render(self, rel_base: str = "..") -> None: # noqa: D102
- print(f"#### $ {self.human_readable_amount} — {self.name}\n")
- if self.features:
- for feature in self.features:
- feature.render(rel_base)
- print("")
- else:
- print("There are no features in this goal for this project. ")
- print(
- "[See the features in this goal **for all Insiders projects.**]"
- f"(https://pawamoy.github.io/insiders/#{self.amount}-{self.name.lower().replace(' ', '-')})",
- )
-
-
-def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]:
- goals_data = yaml.safe_load(data)["goals"]
- return {
- amount: Goal(
- name=goal_data["name"],
- amount=amount,
- complete=funding >= amount,
- features=[
- Feature(
- name=feature_data["name"],
- ref=feature_data.get("ref"),
- since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007
- project=project,
- )
- for feature_data in goal_data["features"]
- ],
- )
- for amount, goal_data in goals_data.items()
- }
-
-
-def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]:
- project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".")
- try:
- data = Path(project_dir, path).read_text()
- except OSError as error:
- raise RuntimeError(f"Could not load data from disk: {path}") from error
- return load_goals(data, funding)
-
-
-def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
- project_name, project_url, data_fragment = source_data
- data_url = urljoin(project_url, data_fragment)
- try:
- with urlopen(data_url) as response: # noqa: S310
- data = response.read()
- except HTTPError as error:
- raise RuntimeError(f"Could not load data from network: {data_url}") from error
- return load_goals(data, funding, project=Project(name=project_name, url=project_url))
-
-
-def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
- if isinstance(source, str):
- return _load_goals_from_disk(source, funding)
- return _load_goals_from_url(source, funding)
-
-
-def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]:
- if isinstance(source, str):
- return _load_goals_from_disk(source, funding)
- goals = {}
- for src in source:
- source_goals = _load_goals(src, funding)
- for amount, goal in source_goals.items():
- if amount not in goals:
- goals[amount] = goal
- else:
- goals[amount].features.extend(goal.features)
- return {amount: goals[amount] for amount in sorted(goals)}
-
-
-def feature_list(goals: Iterable[Goal]) -> list[Feature]:
- return list(chain.from_iterable(goal.features for goal in goals))
-
-
-def load_json(url: str) -> str | list | dict:
- with urlopen(url) as response: # noqa: S310
- return json.loads(response.read().decode())
-
-
-data_source = globals()["data_source"]
-sponsor_url = "https://github.com/sponsors/pawamoy"
-data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main"
-numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment]
-sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment]
-current_funding = numbers["total"]
-sponsors_count = numbers["count"]
-goals = funding_goals(data_source, funding=current_funding)
-ongoing_goals = [goal for goal in goals.values() if not goal.complete]
-unreleased_features = sorted(
- (ft for ft in feature_list(ongoing_goals) if ft.since),
- key=lambda ft: cast(date, ft.since),
- reverse=True,
-)
diff --git a/scripts/make.py b/scripts/make.py
index 55679baa..7fa7b56d 100755
--- a/scripts/make.py
+++ b/scripts/make.py
@@ -14,7 +14,8 @@
from collections.abc import Iterator
-PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split()
+PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.10 3.11 3.12 3.13 3.14 3.15").split()
+PYTHON_DEV = "3.15"
def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None:
@@ -67,16 +68,31 @@ def setup() -> None:
uv_install(venv_path)
+class _RunError(subprocess.CalledProcessError):
+ def __init__(self, *args: Any, python_version: str, **kwargs: Any):
+ super().__init__(*args, **kwargs)
+ self.python_version = python_version
+
+
def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None:
"""Run a command in a virtual environment."""
kwargs = {"check": True, **kwargs}
uv_run = ["uv", "run", "--no-sync"]
- if version == "default":
- with environ(UV_PROJECT_ENVIRONMENT=".venv"):
- subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
- else:
- with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"):
- subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
+ try:
+ if version == "default":
+ with environ(UV_PROJECT_ENVIRONMENT=".venv"):
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
+ else:
+ with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"):
+ subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510
+ except subprocess.CalledProcessError as process:
+ raise _RunError(
+ returncode=process.returncode,
+ python_version=version,
+ cmd=process.cmd,
+ output=process.output,
+ stderr=process.stderr,
+ ) from process
def multirun(cmd: str, *args: str, **kwargs: Any) -> None:
@@ -101,8 +117,8 @@ def clean() -> None:
for path in paths_to_clean:
shutil.rmtree(path, ignore_errors=True)
- cache_dirs = {".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"}
- for dirpath in Path(".").rglob("*/"):
+ cache_dirs = {".cache", ".pytest_cache", ".ruff_cache", "__pycache__"}
+ for dirpath in Path().rglob("*/"):
if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs:
shutil.rmtree(dirpath, ignore_errors=True)
@@ -135,7 +151,7 @@ def main() -> int:
),
flush=True,
)
- if os.path.exists(".venv"):
+ if Path(".venv").exists():
print("\nAvailable tasks", flush=True)
run("default", "duty", "--list")
return 0
@@ -144,18 +160,30 @@ def main() -> int:
cmd = args.pop(0)
if cmd == "run":
+ if not args:
+ print("make: run: missing command", file=sys.stderr)
+ return 1
run("default", *args)
return 0
if cmd == "multirun":
+ if not args:
+ print("make: run: missing command", file=sys.stderr)
+ return 1
multirun(*args)
return 0
if cmd == "allrun":
+ if not args:
+ print("make: run: missing command", file=sys.stderr)
+ return 1
allrun(*args)
return 0
if cmd.startswith("3."):
+ if not args:
+ print("make: run: missing command", file=sys.stderr)
+ return 1
run(cmd, *args)
return 0
@@ -183,7 +211,14 @@ def main() -> int:
if __name__ == "__main__":
try:
sys.exit(main())
- except subprocess.CalledProcessError as process:
+ except _RunError as process:
if process.output:
print(process.output, file=sys.stderr)
- sys.exit(process.returncode)
+ if (code := process.returncode) == 139: # noqa: PLR2004
+ print(
+ f"✗ (python{process.python_version}) '{' '.join(process.cmd)}' failed with return code {code} (segfault)",
+ file=sys.stderr,
+ )
+ if process.python_version == PYTHON_DEV:
+ code = 0
+ sys.exit(code)
diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py
index 71720f8a..137811b1 100644
--- a/src/mkdocstrings/__init__.py
+++ b/src/mkdocstrings/__init__.py
@@ -5,7 +5,7 @@
from __future__ import annotations
-from mkdocstrings._internal.extension import AutoDocProcessor, MkdocstringsExtension
+from mkdocstrings._internal.extension import AutoDocProcessor, MkdocstringsExtension, makeExtension
from mkdocstrings._internal.handlers.base import (
BaseHandler,
CollectionError,
@@ -62,4 +62,5 @@
"get_template_logger",
"get_template_logger_function",
"get_template_path",
+ "makeExtension",
]
diff --git a/src/mkdocstrings/_internal/download.py b/src/mkdocstrings/_internal/download.py
index 2beb053a..52bf42f5 100644
--- a/src/mkdocstrings/_internal/download.py
+++ b/src/mkdocstrings/_internal/download.py
@@ -1,19 +1,27 @@
+from __future__ import annotations
+
import base64
import gzip
import os
import re
import urllib.parse
import urllib.request
-from collections.abc import Mapping
-from typing import BinaryIO, Optional
+from typing import TYPE_CHECKING, BinaryIO
from mkdocstrings._internal.loggers import get_logger
-_logger = get_logger(__name__)
+if TYPE_CHECKING:
+ from collections.abc import Mapping
+
+
+_logger = get_logger("mkdocstrings")
# Regex pattern for an environment variable in the form ${ENV_VAR}.
_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
+# Timeout in seconds for downloading.
+_TIMEOUT = 10
+
def _download_url_with_gz(url: str) -> bytes:
url, auth_header = _extract_auth_from_url(url)
@@ -22,14 +30,14 @@ def _download_url_with_gz(url: str) -> bytes:
url,
headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0", **auth_header},
)
- with urllib.request.urlopen(req) as resp: # noqa: S310
+ with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310
content: BinaryIO = resp
if "gzip" in resp.headers.get("content-encoding", ""):
- content = gzip.GzipFile(fileobj=resp) # type: ignore[assignment]
+ content = gzip.GzipFile(fileobj=resp) # ty: ignore[invalid-assignment]
return content.read()
-def _expand_env_vars(credential: str, url: str, env: Optional[Mapping[str, str]] = None) -> str:
+def _expand_env_vars(credential: str, url: str, env: Mapping[str, str] | None = None) -> str:
"""A safe implementation of environment variable substitution.
It only supports the following forms: `${ENV_VAR}`.
diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py
index 182fc563..0722c539 100644
--- a/src/mkdocstrings/_internal/extension.py
+++ b/src/mkdocstrings/_internal/extension.py
@@ -23,8 +23,9 @@
from __future__ import annotations
import re
+from functools import partial
+from inspect import signature
from typing import TYPE_CHECKING, Any
-from warnings import warn
from xml.etree.ElementTree import Element
import yaml
@@ -33,6 +34,7 @@
from markdown.extensions import Extension
from markdown.treeprocessors import Treeprocessor
from mkdocs.exceptions import PluginError
+from mkdocs_autorefs import AutorefsConfig, AutorefsExtension, AutorefsPlugin
from mkdocstrings._internal.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers
from mkdocstrings._internal.loggers import get_logger
@@ -41,10 +43,9 @@
from collections.abc import MutableSequence
from markdown import Markdown
- from mkdocs_autorefs import AutorefsPlugin
-_logger = get_logger(__name__)
+_logger = get_logger("mkdocstrings")
class AutoDocProcessor(BlockProcessor):
@@ -123,7 +124,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
heading_level = match["heading"].count("#")
_logger.debug("Matched '::: %s'", identifier)
- html, handler, data = self._process_block(identifier, block, heading_level)
+ html, handler, _ = self._process_block(identifier, block, heading_level)
el = Element("div", {"class": "mkdocstrings"})
# The final HTML is inserted as opaque to subsequent processing, and only revealed at the end.
el.text = self.md.htmlStash.store(html)
@@ -170,19 +171,7 @@ def _process_block(
# Heading level obtained from Markdown (`##`) takes precedence.
local_options["heading_level"] = heading_level
- # YORE: Bump 1: Replace block with line 2.
- if handler.get_options.__func__ is not BaseHandler.get_options: # type: ignore[attr-defined]
- options = handler.get_options(local_options)
- else:
- warn(
- "mkdocstrings v1 will start using your handler's `get_options` method to build options "
- "instead of merging the global and local options (dictionaries). ",
- DeprecationWarning,
- stacklevel=1,
- )
- handler_config = self._handlers.get_handler_config(handler_name)
- global_options = handler_config.get("options", {})
- options = {**global_options, **local_options}
+ options = handler.get_options(local_options)
_logger.debug("Collecting data")
try:
@@ -197,8 +186,12 @@ def _process_block(
self._updated_envs.add(handler_name)
_logger.debug("Rendering templates")
+ if "locale" in signature(handler.render).parameters:
+ render = partial(handler.render, locale=self._handlers._locale)
+ else:
+ render = handler.render
try:
- rendered = handler.render(data, options)
+ rendered = render(data, options)
except TemplateNotFound as exc:
_logger.error( # noqa: TRY400
"Template '%s' not found for '%s' handler and theme '%s'.",
@@ -245,29 +238,22 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None:
for heading in headings:
rendered_id = heading.attrib["id"]
+
+ skip_inventory = "data-skip-inventory" in heading.attrib
+ if skip_inventory:
+ _logger.debug(
+ "Skipping heading with id %r because data-skip-inventory is present",
+ rendered_id,
+ )
+ continue
+
# The title is registered to be used as tooltip by autorefs.
self._autorefs.register_anchor(page, rendered_id, title=heading.text, primary=True)
# Register all identifiers for this object
# both in the autorefs plugin and in the inventory.
aliases: tuple[str, ...]
- # YORE: Bump 1: Replace block with line 16.
- if hasattr(handler, "get_anchors"):
- warn(
- "The `get_anchors` method is deprecated. "
- "Declare a `get_aliases` method instead, accepting a string (identifier) "
- "instead of a collected object.",
- DeprecationWarning,
- stacklevel=1,
- )
- try:
- data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {}))
- except CollectionError:
- aliases = ()
- else:
- aliases = handler.get_anchors(data_object)
- else:
- aliases = handler.get_aliases(rendered_id)
+ aliases = handler.get_aliases(rendered_id)
for alias in aliases:
if alias != rendered_id:
@@ -315,7 +301,7 @@ def _remove_duplicated_headings(self, parent: Element) -> None:
class _TocLabelsTreeProcessor(Treeprocessor):
def run(self, root: Element) -> None: # noqa: ARG002
- self._override_toc_labels(self.md.toc_tokens) # type: ignore[attr-defined]
+ self._override_toc_labels(self.md.toc_tokens) # ty: ignore[unresolved-attribute]
def _override_toc_labels(self, tokens: list[dict[str, Any]]) -> None:
for token in tokens:
@@ -330,17 +316,26 @@ class MkdocstringsExtension(Extension):
It cannot work outside of `mkdocstrings`.
"""
- def __init__(self, handlers: Handlers, autorefs: AutorefsPlugin, **kwargs: Any) -> None:
+ def __init__(
+ self,
+ handlers: Handlers,
+ autorefs: AutorefsPlugin,
+ *,
+ autorefs_extension: bool = False,
+ **kwargs: Any,
+ ) -> None:
"""Initialize the object.
Arguments:
handlers: The handlers container.
autorefs: The autorefs plugin instance.
+ autorefs_extension: Whether the autorefs extension must be registered.
**kwargs: Keyword arguments used by `markdown.extensions.Extension`.
"""
super().__init__(**kwargs)
self._handlers = handlers
self._autorefs = autorefs
+ self._autorefs_extension = autorefs_extension
def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name)
"""Register the extension.
@@ -350,6 +345,12 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me
Arguments:
md: A `markdown.Markdown` instance.
"""
+ md.registerExtension(self)
+
+ # Zensical integration: get the current page from the Zensical-specific preprocessor.
+ if "zensical_current_page" in md.preprocessors:
+ self._autorefs.current_page = md.preprocessors["zensical_current_page"]
+
md.parser.blockprocessors.register(
AutoDocProcessor(md, handlers=self._handlers, autorefs=self._autorefs),
"mkdocstrings",
@@ -365,3 +366,128 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me
"mkdocstrings_post_toc_labels",
priority=4, # Right after 'toc'.
)
+
+ if self._autorefs_extension:
+ AutorefsExtension(self._autorefs).extendMarkdown(md)
+
+
+# -----------------------------------------------------------------------------
+# The following is only used by Zensical. The goal is to provide temporary
+# compatibility for users migrating from MkDocs (and Material for MkDocs)
+# to Zensical. When detecting the use of the mkdocstrings plugin in mkdocs.yml,
+# Zensical will add the mkdocstrings extension to its Markdown extensions.
+
+_default_config: dict[str, Any] = {
+ "default_handler": "python",
+ "handlers": {},
+ "custom_templates": None,
+ "locale": "en",
+ "enable_inventory": True,
+ "enabled": True,
+}
+
+
+def _split_configs(
+ markdown_extensions: list[str | dict[str, dict[str, Any]] | Extension],
+) -> tuple[list[str | Extension], dict[str, dict[str, Any]]]:
+ # Split markdown extensions and their configs from mkdocs.yml
+ mdx: list[str | Extension] = []
+ mdx_config: dict[str, dict[str, Any]] = {}
+ for item in markdown_extensions:
+ if isinstance(item, (str, Extension)):
+ mdx.append(item)
+ elif isinstance(item, dict):
+ for key, value in item.items():
+ mdx.append(key)
+ mdx_config[key] = value
+ break # Only one item per dict
+ return mdx, mdx_config
+
+
+class _ToolConfig:
+ def __init__(self, config_file_path: str | None = None) -> None:
+ self.config_file_path = config_file_path
+
+
+_AUTOREFS = None
+_HANDLERS = None
+
+
+def makeExtension( # noqa: N802
+ *,
+ default_handler: str | None = None,
+ inventory_project: str | None = None,
+ inventory_version: str | None = None,
+ handlers: dict[str, dict] | None = None,
+ custom_templates: str | None = None,
+ markdown_extensions: list[str | dict | Extension] | None = None,
+ locale: str | None = None,
+ config_file_path: str | None = None,
+) -> MkdocstringsExtension:
+ """Create the extension instance.
+
+ We only support this function being used by Zensical.
+ Consider this function private API.
+ """
+ global _AUTOREFS # noqa: PLW0603
+ if _AUTOREFS is None:
+ _AUTOREFS = AutorefsPlugin()
+ _AUTOREFS.config = AutorefsConfig() # ty:ignore[invalid-assignment]
+ _AUTOREFS.config.resolve_closest = True
+ _AUTOREFS.config.link_titles = "auto"
+ _AUTOREFS.config.strip_title_tags = "auto"
+ _AUTOREFS.scan_toc = True
+ _AUTOREFS._link_titles = "external"
+ _AUTOREFS._strip_title_tags = False
+
+ global _HANDLERS # noqa: PLW0603
+ if _HANDLERS is None:
+ mdx, mdx_config = _split_configs(markdown_extensions or [])
+ tool_config = _ToolConfig(config_file_path=config_file_path)
+ mdx.append(AutorefsExtension(_AUTOREFS))
+ _HANDLERS = Handlers(
+ theme="material",
+ default=default_handler or _default_config["default_handler"],
+ inventory_project=inventory_project or "Project",
+ inventory_version=inventory_version or "0.0.0",
+ handlers_config=handlers or _default_config["handlers"],
+ custom_templates=custom_templates or _default_config["custom_templates"],
+ mdx=mdx,
+ mdx_config=mdx_config,
+ locale=locale or _default_config["locale"],
+ tool_config=tool_config,
+ )
+
+ _HANDLERS._download_inventories()
+ register = _AUTOREFS.register_url
+ for identifier, url in _HANDLERS._yield_inventory_items():
+ register(identifier, url)
+
+ return MkdocstringsExtension(
+ handlers=_HANDLERS,
+ autorefs=_AUTOREFS,
+ autorefs_extension=True,
+ )
+
+
+def _reset() -> None:
+ global _AUTOREFS, _HANDLERS # noqa: PLW0603
+ _AUTOREFS = None
+ _HANDLERS = None
+
+
+def _get_autorefs() -> dict[str, Any]:
+ if _AUTOREFS:
+ return {
+ "primary": _AUTOREFS._primary_url_map,
+ "secondary": _AUTOREFS._secondary_url_map,
+ "inventory": _AUTOREFS._abs_url_map,
+ "titles": _AUTOREFS._title_map,
+ }
+ return {}
+
+
+def _get_inventory() -> bytes:
+ if _HANDLERS:
+ return _HANDLERS.inventory.format_sphinx()
+ return b""
diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py
index 7784f007..799c4499 100644
--- a/src/mkdocstrings/_internal/handlers/base.py
+++ b/src/mkdocstrings/_internal/handlers/base.py
@@ -6,9 +6,9 @@
import datetime
import importlib
-import inspect
-import sys
+import ssl
from concurrent import futures
+from importlib.metadata import entry_points
from io import BytesIO
from pathlib import Path
from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, cast
@@ -17,7 +17,6 @@
from jinja2 import Environment, FileSystemLoader
from markdown import Markdown
-from markdown.extensions.toc import TocTreeprocessor
from markupsafe import Markup
from mkdocs.utils.cache import download_and_cache_url
from mkdocs_autorefs import AutorefsInlineProcessor, BacklinksTreeProcessor
@@ -33,19 +32,14 @@
from mkdocstrings._internal.inventory import Inventory
from mkdocstrings._internal.loggers import get_logger, get_template_logger
-# TODO: remove once support for Python 3.9 is dropped
-if sys.version_info < (3, 10):
- from importlib_metadata import entry_points
-else:
- from importlib.metadata import entry_points
-
if TYPE_CHECKING:
from collections.abc import Iterable, Iterator, Mapping, Sequence
from markdown import Extension
+ from markdown.extensions.toc import TocTreeprocessor
from mkdocs_autorefs import AutorefsHookInterface, Backlink
-_logger = get_logger(__name__)
+_logger = get_logger("mkdocstrings")
CollectorItem = Any
"""The type of the item returned by the `collect` method of a handler."""
@@ -103,21 +97,15 @@ class BaseHandler:
To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates.
"""
- # YORE: Bump 1: Replace ` = ""` with `` within line.
- name: ClassVar[str] = ""
+ name: ClassVar[str]
"""The handler's name, for example "python"."""
- # YORE: Bump 1: Replace ` = ""` with `` within line.
- domain: ClassVar[str] = ""
+ domain: ClassVar[str]
"""The handler's domain, used to register objects in the inventory, for example "py"."""
enable_inventory: ClassVar[bool] = False
"""Whether the inventory creation is enabled."""
- # YORE: Bump 1: Remove block.
- fallback_config: ClassVar[dict] = {}
- """Fallback configuration when searching anchors for identifiers."""
-
fallback_theme: ClassVar[str] = ""
"""Fallback theme to use when a template isn't found in the configured theme."""
@@ -126,16 +114,11 @@ class BaseHandler:
def __init__(
self,
- # YORE: Bump 1: Remove line.
- *args: Any,
- # YORE: Bump 1: Remove line.
- **kwargs: Any,
- # YORE: Bump 1: Replace `# ` with `` within block.
- # *,
- # theme: str,
- # custom_templates: str | None,
- # mdx: Sequence[str | Extension],
- # mdx_config: Mapping[str, Any],
+ *,
+ theme: str,
+ custom_templates: str | None,
+ mdx: Sequence[str | Extension],
+ mdx_config: Mapping[str, Any],
) -> None:
"""Initialize the object.
@@ -148,58 +131,6 @@ def __init__(
mdx (list[str | Extension]): A list of Markdown extensions to use.
mdx_config (Mapping[str, Mapping[str, Any]]): Configuration for the Markdown extensions.
"""
- # YORE: Bump 1: Remove block.
- handler = ""
- theme = ""
- custom_templates = None
- if args:
- handler, args = args[0], args[1:]
- if args:
- theme, args = args[0], args[1:]
- warn(
- "The `theme` argument must be passed as a keyword argument.",
- DeprecationWarning,
- stacklevel=2,
- )
- if args:
- custom_templates, args = args[0], args[1:]
- warn(
- "The `custom_templates` argument must be passed as a keyword argument.",
- DeprecationWarning,
- stacklevel=2,
- )
- handler = kwargs.pop("handler", handler)
- theme = kwargs.pop("theme", theme)
- custom_templates = kwargs.pop("custom_templates", custom_templates)
- mdx = kwargs.pop("mdx", None)
- mdx_config = kwargs.pop("mdx_config", None)
- if handler:
- if not self.name:
- type(self).name = handler
- warn(
- "The `handler` argument is deprecated. The handler name must be specified as a class attribute.",
- DeprecationWarning,
- stacklevel=2,
- )
- if not self.domain:
- warn(
- "The `domain` attribute must be specified as a class attribute.",
- DeprecationWarning,
- stacklevel=2,
- )
- if mdx is None:
- warn(
- "The `mdx` argument must be provided (as a keyword argument).",
- DeprecationWarning,
- stacklevel=2,
- )
- if mdx_config is None:
- warn(
- "The `mdx_config` argument must be provided (as a keyword argument).",
- DeprecationWarning,
- stacklevel=2,
- )
-
self.theme = theme
"""The selected theme."""
self.custom_templates = custom_templates
@@ -219,16 +150,14 @@ def __init__(
# add extended theme templates
extended_templates_dirs = self.get_extended_templates_dirs(self.name)
- for templates_dir in extended_templates_dirs:
- paths.append(templates_dir / self.theme)
+ paths.extend(templates_dir / self.theme for templates_dir in extended_templates_dirs)
# add fallback theme templates
if self.fallback_theme and self.fallback_theme != self.theme:
paths.append(themes_dir / self.fallback_theme)
# add fallback theme of extended templates
- for templates_dir in extended_templates_dirs:
- paths.append(templates_dir / self.fallback_theme)
+ paths.extend(templates_dir / self.fallback_theme for templates_dir in extended_templates_dirs)
for path in paths:
css_path = path / "style.css"
@@ -249,7 +178,7 @@ def __init__(
self.env.filters["convert_markdown"] = self.do_convert_markdown
self.env.filters["heading"] = self.do_heading
self.env.filters["any"] = do_any
- self.env.globals["log"] = get_template_logger(self.name)
+ self.env.globals["log"] = get_template_logger(self.name) # ty:ignore[invalid-assignment]
@property
def md(self) -> Markdown:
@@ -310,7 +239,7 @@ def collect(self, identifier: str, options: HandlerOptions) -> CollectorItem:
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 would be 'mkdocstrings.BaseHandler' to collect documentation about the BaseHandler class.
It can be anything that you can feed to the tool of your choice.
options: The final configuration options.
@@ -319,20 +248,29 @@ def collect(self, identifier: str, options: HandlerOptions) -> CollectorItem:
"""
raise NotImplementedError
- def render(self, data: CollectorItem, options: HandlerOptions) -> str:
+ def render(self, data: CollectorItem, options: HandlerOptions, *, locale: str | None = None) -> str:
"""Render a template using provided data and configuration options.
Arguments:
data: The collected data to render.
options: The final configuration options.
+ locale: The locale to use for translations, if any.
Returns:
The rendered template as HTML.
"""
raise NotImplementedError
- def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: ARG002
- """Render backlinks."""
+ def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]], *, locale: str | None = None) -> str: # noqa: ARG002
+ """Render backlinks.
+
+ Parameters:
+ backlinks: A mapping of identifiers to backlinks.
+ locale: The locale to use for translations, if any.
+
+ Returns:
+ The rendered backlinks as HTML.
+ """
return ""
def teardown(self) -> None:
@@ -359,7 +297,7 @@ def get_templates_dir(self, handler: str | None = None) -> Path:
"""
handler = handler or self.name
try:
- import mkdocstrings_handlers
+ import mkdocstrings_handlers # noqa: PLC0415
except ModuleNotFoundError as error:
raise ModuleNotFoundError(f"Handler '{handler}' not found, is it installed?") from error
@@ -421,24 +359,24 @@ def do_convert_markdown(
global _markdown_conversion_layer # noqa: PLW0603
_markdown_conversion_layer += 1
treeprocessors = self.md.treeprocessors
- treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level # type: ignore[attr-defined]
- treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--" # type: ignore[attr-defined]
- treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph # type: ignore[attr-defined]
+ treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level
+ treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--"
+ treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph
if BacklinksTreeProcessor.name in treeprocessors:
- treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id # type: ignore[attr-defined]
-
- if autoref_hook:
- self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined]
+ treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id
+ if autoref_hook and AutorefsInlineProcessor.name in self.md.inlinePatterns:
+ self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # ty: ignore[unresolved-attribute]
try:
return Markup(self.md.convert(text))
finally:
- treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0 # type: ignore[attr-defined]
- treeprocessors[IdPrependingTreeprocessor.name].id_prefix = "" # type: ignore[attr-defined]
- treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined]
+ treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0
+ treeprocessors[IdPrependingTreeprocessor.name].id_prefix = ""
+ treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False
if BacklinksTreeProcessor.name in treeprocessors:
- treeprocessors[BacklinksTreeProcessor.name].initial_id = None # type: ignore[attr-defined]
- self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined]
+ treeprocessors[BacklinksTreeProcessor.name].initial_id = None
+ if AutorefsInlineProcessor.name in self.md.inlinePatterns:
+ self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None
self.md.reset()
_markdown_conversion_layer -= 1
@@ -450,6 +388,7 @@ def do_heading(
role: str | None = None,
hidden: bool = False,
toc_label: str | None = None,
+ skip_inventory: bool = False,
**attributes: str,
) -> Markup:
"""Render an HTML heading and register it for the table of contents. For use inside templates.
@@ -460,6 +399,7 @@ def do_heading(
role: An optional role for the object bound to this heading.
hidden: If True, only register it for the table of contents, don't render anything.
toc_label: The title to use in the table of contents ('data-toc-label' attribute).
+ skip_inventory: Flag element to not be registered in the inventory (by setting a `data-skip-inventory` attribute).
**attributes: Any extra HTML attributes of the heading.
Returns:
@@ -479,6 +419,8 @@ def do_heading(
if toc_label is None:
toc_label = content.unescape() if isinstance(content, Markup) else content
el.set("data-toc-label", toc_label)
+ if skip_inventory:
+ el.set("data-skip-inventory", "true")
if role:
el.set("data-role", role)
if content:
@@ -493,7 +435,7 @@ def do_heading(
el = Element(f"h{heading_level}", attributes)
el.append(Element("mkdocstrings-placeholder"))
# Tell the inner 'toc' extension to make its additions if configured so.
- toc = cast(TocTreeprocessor, self.md.treeprocessors["toc"])
+ toc = cast("TocTreeprocessor", self.md.treeprocessors["toc"])
if toc.use_anchors:
toc.add_anchor(el, attributes["id"])
if toc.use_permalinks:
@@ -519,20 +461,11 @@ def get_headings(self) -> Sequence[Element]:
self._headings.clear()
return result
- # YORE: Bump 1: Replace `*args: Any, **kwargs: Any` with `config: Any`.
- def update_env(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
+ def update_env(self, config: Any) -> None:
"""Update the Jinja environment."""
- # YORE: Bump 1: Remove line.
- warn("No need to call `super().update_env()` anymore.", DeprecationWarning, stacklevel=2)
def _update_env(self, md: Markdown, *, config: Any | None = None) -> None:
"""Update our handler to point to our configured Markdown instance, grabbing some of the config from `md`."""
- # YORE: Bump 1: Remove block.
- if self.mdx is None and config is not None:
- self.mdx = config.get("mdx", None) or config.get("markdown_extensions", None) or ()
- if self.mdx_config is None and config is not None:
- self.mdx_config = config.get("mdx_config", None) or config.get("mdx_configs", None) or {}
-
extensions: list[str | Extension] = [*self.mdx, MkdocstringsInnerExtension(self._headings)]
new_md = Markdown(extensions=extensions, extension_configs=self.mdx_config)
@@ -540,24 +473,18 @@ def _update_env(self, md: Markdown, *, config: Any | None = None) -> None:
# MkDocs adds its own (required) extension that's not part of the config. Propagate it.
if "relpath" in md.treeprocessors:
relpath = md.treeprocessors["relpath"]
- new_relpath = type(relpath)(relpath.file, relpath.files, relpath.config) # type: ignore[attr-defined,call-arg]
+ new_relpath = type(relpath)(relpath.file, relpath.files, relpath.config)
new_md.treeprocessors.register(new_relpath, "relpath", priority=0)
+ elif "zrelpath" in md.treeprocessors:
+ zrelpath = md.treeprocessors["zrelpath"]
+ new_zrelpath = type(zrelpath)(new_md, zrelpath.path, zrelpath.use_directory_urls)
+ new_md.treeprocessors.register(new_zrelpath, "zrelpath", priority=0)
self._md = new_md
self.env.filters["highlight"] = Highlighter(new_md).highlight
- # YORE: Bump 1: Replace block with `self.update_env(config)`.
- parameters = inspect.signature(self.update_env).parameters
- if "md" in parameters:
- warn(
- "The `update_env(md)` parameter is deprecated. Use `self.md` instead.",
- DeprecationWarning,
- stacklevel=1,
- )
- self.update_env(new_md, config)
- elif "config" in parameters:
- self.update_env(config)
+ self.update_env(config)
class Handlers:
@@ -578,6 +505,7 @@ def __init__(
custom_templates: str | None = None,
mdx: Sequence[str | Extension] | None = None,
mdx_config: Mapping[str, Any] | None = None,
+ locale: str = "en",
tool_config: Any,
) -> None:
"""Initialize the object.
@@ -591,6 +519,7 @@ def __init__(
custom_templates: The path to custom templates.
mdx: A list of Markdown extensions to use.
mdx_config: Configuration for the Markdown extensions.
+ locale: The locale to use for translations.
tool_config: Tool configuration to pass down to handlers.
"""
self._theme = theme
@@ -600,6 +529,7 @@ def __init__(
self._mdx = mdx or []
self._mdx_config = mdx_config or {}
self._handlers: dict[str, BaseHandler] = {}
+ self._locale = locale
self._tool_config = tool_config
self.inventory: Inventory = Inventory(project=inventory_project, version=inventory_version)
@@ -607,35 +537,6 @@ def __init__(
self._inv_futures: dict[futures.Future, tuple[BaseHandler, str, Any]] = {}
- # YORE: Bump 1: Remove block.
- def get_anchors(self, identifier: str) -> tuple[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.BaseHandler.collect] can accept).
-
- Returns:
- A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it.
- """
- for handler in self._handlers.values():
- try:
- if hasattr(handler, "get_anchors"):
- warn(
- "The `get_anchors` method is deprecated. "
- "Declare a `get_aliases` method instead, accepting a string (identifier) "
- "instead of a collected object.",
- DeprecationWarning,
- stacklevel=1,
- )
- aliases = handler.get_anchors(handler.collect(identifier, getattr(handler, "fallback_config", {})))
- else:
- aliases = handler.get_aliases(identifier)
- except CollectionError:
- continue
- if aliases:
- return aliases
- return ()
-
def get_handler_name(self, config: dict) -> str:
"""Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler.
@@ -661,7 +562,7 @@ def get_handler_config(self, name: str) -> dict:
def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHandler:
"""Get a handler thanks to its name.
- This function dynamically imports a module named "mkdocstrings.handlers.NAME", calls its
+ This function dynamically imports a module named "mkdocstrings_handlers.NAME", calls its
`get_handler` method to get an instance of a handler, and caches it in dictionary.
It means that during one run (for each reload when serving, or once when building),
a handler is instantiated only once, and reused for each "autodoc" instruction asking for it.
@@ -679,34 +580,14 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand
handler_config = self._handlers_config.get(name, {})
module = importlib.import_module(f"mkdocstrings_handlers.{name}")
- # YORE: Bump 1: Remove block.
- kwargs = {
- "theme": self._theme,
- "custom_templates": self._custom_templates,
- "mdx": self._mdx,
- "mdx_config": self._mdx_config,
- "handler_config": handler_config,
- "tool_config": self._tool_config,
- }
- if "config_file_path" in inspect.signature(module.get_handler).parameters:
- kwargs["config_file_path"] = self._tool_config.get("config_file_path")
- warn(
- "The `config_file_path` argument in `get_handler` functions is deprecated. "
- "Use `tool_config.get('config_file_path')` instead.",
- DeprecationWarning,
- stacklevel=1,
- )
- self._handlers[name] = module.get_handler(**kwargs)
-
- # YORE: Bump 1: Replace `# ` with `` within block.
- # self._handlers[name] = module.get_handler(
- # theme=self._theme,
- # custom_templates=self._custom_templates,
- # mdx=self._mdx,
- # mdx_config=self._mdx_config,
- # handler_config=handler_config,
- # tool_config=self._tool_config,
- # )
+ self._handlers[name] = module.get_handler(
+ theme=self._theme,
+ custom_templates=self._custom_templates,
+ mdx=self._mdx,
+ mdx_config=self._mdx_config,
+ handler_config=handler_config,
+ tool_config=self._tool_config,
+ )
return self._handlers[name]
def _download_inventories(self) -> None:
@@ -720,7 +601,7 @@ def _download_inventories(self) -> None:
for handler_name, conf in self._handlers_config.items():
handler = self.get_handler(handler_name)
- if handler.get_inventory_urls.__func__ is BaseHandler.get_inventory_urls: # type: ignore[attr-defined]
+ if handler.get_inventory_urls.__func__ is BaseHandler.get_inventory_urls:
if inv_configs := conf.pop("import", ()):
warn(
"mkdocstrings v1 will stop handling 'import' in handlers configuration. "
@@ -737,6 +618,11 @@ def _download_inventories(self) -> None:
to_download.extend((handler, url, conf) for url, conf in inv_configs)
if to_download:
+ # YORE: EOL 3.12: Remove block.
+ # NOTE: Create context in main thread to fix issue
+ # https://github.com/mkdocstrings/mkdocstrings/issues/796.
+ _ = ssl.create_default_context()
+
thread_pool = futures.ThreadPoolExecutor(4)
for handler, url, conf in to_download:
_logger.debug("Downloading inventory from %s", url)
@@ -757,7 +643,7 @@ def _yield_inventory_items(self) -> Iterator[tuple[str, str]]:
for fut, (handler, url, conf) in reversed(self._inv_futures.items()):
try:
yield from handler.load_inventory(BytesIO(fut.result()), url, **conf)
- except Exception as error: # noqa: BLE001
+ except Exception as error: # noqa: BLE001,PERF203
_logger.error("Couldn't load inventory %s through handler '%s': %s", url, handler.name, error) # noqa: TRY400
self._inv_futures = {}
diff --git a/src/mkdocstrings/_internal/handlers/rendering.py b/src/mkdocstrings/_internal/handlers/rendering.py
index 25db87e1..264a77ef 100644
--- a/src/mkdocstrings/_internal/handlers/rendering.py
+++ b/src/mkdocstrings/_internal/handlers/rendering.py
@@ -84,7 +84,7 @@ def __init__(self, md: Markdown):
self._css_class = config.pop("css_class", "highlight")
super().__init__(**{name: opt for name, opt in config.items() if name in self._highlight_config_keys})
- def highlight(
+ def highlight( # ty: ignore[invalid-method-override]
self,
src: str,
language: str | None = None,
@@ -113,7 +113,7 @@ def highlight(
src = textwrap.dedent(src)
kwargs.setdefault("css_class", self._css_class)
- old_linenums = self.linenums # type: ignore[has-type]
+ old_linenums = self.linenums
if linenums is not None:
self.linenums = linenums
try:
@@ -240,7 +240,7 @@ def __init__(self, md: Markdown, headings: list[Element]):
def run(self, root: Element) -> None:
"""Record all heading elements encountered in the document."""
- permalink_class = self.md.treeprocessors["toc"].permalink_class # type: ignore[attr-defined]
+ permalink_class = self.md.treeprocessors["toc"].permalink_class
for el in root.iter():
if self.regex.fullmatch(el.tag):
el = copy.copy(el) # noqa: PLW2901
diff --git a/src/mkdocstrings/_internal/inventory.py b/src/mkdocstrings/_internal/inventory.py
index 471e3633..241bbb12 100644
--- a/src/mkdocstrings/_internal/inventory.py
+++ b/src/mkdocstrings/_internal/inventory.py
@@ -8,7 +8,7 @@
import re
import zlib
from textwrap import dedent
-from typing import TYPE_CHECKING, BinaryIO
+from typing import TYPE_CHECKING, BinaryIO, Literal, overload
if TYPE_CHECKING:
from collections.abc import Collection
@@ -66,11 +66,21 @@ def format_sphinx(self) -> str:
sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$")
"""Regex to parse a Sphinx v2 inventory line."""
+ @overload
@classmethod
- def parse_sphinx(cls, line: str) -> InventoryItem:
+ def parse_sphinx(cls, line: str, *, return_none: Literal[False]) -> InventoryItem: ...
+
+ @overload
+ @classmethod
+ def parse_sphinx(cls, line: str, *, return_none: Literal[True]) -> InventoryItem | None: ...
+
+ @classmethod
+ def parse_sphinx(cls, line: str, *, return_none: bool = False) -> InventoryItem | None:
"""Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it."""
match = cls.sphinx_item_regex.search(line)
if not match:
+ if return_none:
+ return None
raise ValueError(line)
name, domain, role, priority, uri, dispname = match.groups()
if uri.endswith("$"):
@@ -167,7 +177,9 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ())
for _ in range(4):
in_file.readline()
lines = zlib.decompress(in_file.read()).splitlines()
- items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines]
+ items: list[InventoryItem] = [
+ item for line in lines if (item := InventoryItem.parse_sphinx(line.decode("utf8"), return_none=True))
+ ]
if domain_filter:
items = [item for item in items if item.domain in domain_filter]
return cls(items)
diff --git a/src/mkdocstrings/_internal/loggers.py b/src/mkdocstrings/_internal/loggers.py
index d56d09c3..6c8817ac 100644
--- a/src/mkdocstrings/_internal/loggers.py
+++ b/src/mkdocstrings/_internal/loggers.py
@@ -5,15 +5,12 @@
import logging
from contextlib import suppress
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable
+from typing import TYPE_CHECKING, Any
-try:
- from jinja2 import pass_context
-except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped
- from jinja2 import contextfunction as pass_context # type: ignore[attr-defined,no-redef]
+from jinja2 import pass_context
if TYPE_CHECKING:
- from collections.abc import MutableMapping, Sequence
+ from collections.abc import Callable, MutableMapping, Sequence
from jinja2.runtime import Context
@@ -85,7 +82,7 @@ def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None:
if (key := (self, str(msg))) in self._logged:
return
self._logged.add(key)
- super().log(level, msg, *args, **kwargs) # type: ignore[arg-type]
+ super().log(level, msg, *args, **kwargs) # ty: ignore[invalid-argument-type]
class TemplateLogger:
diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py
index d7adf1c6..4f9bd29d 100644
--- a/src/mkdocstrings/_internal/plugin.py
+++ b/src/mkdocstrings/_internal/plugin.py
@@ -15,9 +15,10 @@
import os
import re
+from functools import partial
+from inspect import signature
from re import Match
from typing import TYPE_CHECKING, Any
-from warnings import catch_warnings, simplefilter
from mkdocs.config import Config
from mkdocs.config import config_options as opt
@@ -35,7 +36,7 @@
from mkdocs.structure.files import Files
-_logger = get_logger(__name__)
+_logger = get_logger("mkdocstrings")
class PluginConfig(Config):
@@ -73,6 +74,8 @@ class PluginConfig(Config):
"""Whether to enable object inventory creation."""
enabled = opt.Type(bool, default=True)
"""Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything."""
+ locale = opt.Optional(opt.Type(str))
+ """The locale to use for translations."""
class MkdocstringsPlugin(BasePlugin[PluginConfig]):
@@ -131,15 +134,19 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
return config
_logger.debug("Adding extension to the list")
+ locale = self.config.locale or config.theme.get("language") or config.theme.get("locale") or "en"
+ locale = str(locale).replace("_", "-")
+
handlers = Handlers(
default=self.config.default_handler,
handlers_config=self.config.handlers,
- theme=config.theme.name or os.path.dirname(config.theme.dirs[0]),
+ theme=config.theme.name or os.path.dirname(config.theme.dirs[0]), # noqa: PTH120
custom_templates=self.config.custom_templates,
mdx=config.markdown_extensions,
mdx_config=config.mdx_configs,
inventory_project=config.site_name,
inventory_version="0.0.0", # TODO: Find a way to get actual version.
+ locale=locale,
tool_config=config,
)
@@ -149,22 +156,18 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None:
autorefs: AutorefsPlugin
try:
# If autorefs plugin is explicitly enabled, just use it.
- autorefs = config.plugins["autorefs"] # type: ignore[assignment]
+ autorefs = config.plugins["autorefs"] # ty: ignore[invalid-assignment]
_logger.debug("Picked up existing autorefs instance %r", autorefs)
except KeyError:
# Otherwise, add a limited instance of it that acts only on what's added through `register_anchor`.
autorefs = AutorefsPlugin()
- autorefs.config = AutorefsConfig()
+ autorefs.config = AutorefsConfig() # ty:ignore[invalid-assignment]
autorefs.scan_toc = False
config.plugins["autorefs"] = autorefs
_logger.debug("Added a subdued autorefs instance %r", autorefs)
- # YORE: Bump 1: Remove block.
- with catch_warnings():
- simplefilter("ignore", category=DeprecationWarning)
- autorefs.get_fallback_anchor = handlers.get_anchors
mkdocstrings_extension = MkdocstringsExtension(handlers, autorefs)
- config.markdown_extensions.append(mkdocstrings_extension) # type: ignore[arg-type]
+ config.markdown_extensions.append(mkdocstrings_extension) # ty: ignore[invalid-argument-type]
config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files.
@@ -196,7 +199,7 @@ def plugin_enabled(self) -> bool:
@event_priority(50) # Early, before autorefs' starts applying cross-refs and collecting backlinks.
def _on_env_load_inventories(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
if self.plugin_enabled and self._handlers:
- register = config.plugins["autorefs"].register_url # type: ignore[attr-defined]
+ register = config.plugins["autorefs"].register_url # ty: ignore[unresolved-attribute]
for identifier, url in self._handlers._yield_inventory_items():
register(identifier, url)
@@ -204,14 +207,14 @@ def _on_env_load_inventories(self, env: Environment, config: MkDocsConfig, *args
def _on_env_add_css(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
if self.plugin_enabled and self._handlers:
css_content = "\n".join(handler.extra_css for handler in self.handlers.seen_handlers)
- write_file(css_content.encode("utf-8"), os.path.join(config.site_dir, self.css_filename))
+ write_file(css_content.encode("utf-8"), os.path.join(config.site_dir, self.css_filename)) # noqa: PTH118
@event_priority(-20) # Late, not important.
def _on_env_write_inventory(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
if self.plugin_enabled and self._handlers and self.inventory_enabled:
_logger.debug("Creating inventory file objects.inv")
inv_contents = self.handlers.inventory.format_sphinx()
- write_file(inv_contents, os.path.join(config.site_dir, "objects.inv"))
+ write_file(inv_contents, os.path.join(config.site_dir, "objects.inv")) # noqa: PTH118
@event_priority(-100) # Last, after autorefs has finished applying cross-refs and collecting backlinks.
def _on_env_apply_backlinks(self, env: Environment, /, *, config: MkDocsConfig, files: Files) -> Environment: # noqa: ARG002
@@ -223,18 +226,23 @@ def repl(match: Match) -> str:
# The handler doesn't implement backlinks,
# return early to avoid computing them.
- if handler.render_backlinks.__func__ is BaseHandler.render_backlinks: # type: ignore[attr-defined]
+ if handler.render_backlinks.__func__ is BaseHandler.render_backlinks:
return ""
identifier = match.group(1)
aliases = handler.get_aliases(identifier)
- backlinks = self._autorefs.get_backlinks(identifier, *aliases, from_url=file.page.url) # type: ignore[union-attr]
+ backlinks = self._autorefs.get_backlinks(identifier, *aliases, from_url=file.page.url)
# No backlinks, avoid calling the handler's method.
if not backlinks:
return ""
- return handler.render_backlinks(backlinks)
+ if "locale" in signature(handler.render_backlinks).parameters:
+ render_backlinks = partial(handler.render_backlinks, locale=self.handlers._locale)
+ else:
+ render_backlinks = handler.render_backlinks
+
+ return render_backlinks(backlinks)
for file in files:
if file.page and file.page.content:
diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py
deleted file mode 100644
index 15a84cc8..00000000
--- a/src/mkdocstrings/extension.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal import extension
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.extension` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(extension, name)
diff --git a/src/mkdocstrings/handlers/__init__.py b/src/mkdocstrings/handlers/__init__.py
deleted file mode 100644
index af032e98..00000000
--- a/src/mkdocstrings/handlers/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py
deleted file mode 100644
index 82ee3edb..00000000
--- a/src/mkdocstrings/handlers/base.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal.handlers import base
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.handlers.base` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(base, name)
diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py
deleted file mode 100644
index 940f3a9c..00000000
--- a/src/mkdocstrings/handlers/rendering.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal.handlers import rendering
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.handlers.rendering` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(rendering, name)
diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py
deleted file mode 100644
index b5c8adea..00000000
--- a/src/mkdocstrings/inventory.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal import inventory
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.inventory` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(inventory, name)
diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py
deleted file mode 100644
index ce805362..00000000
--- a/src/mkdocstrings/loggers.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal import loggers
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.loggers` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(loggers, name)
diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py
deleted file mode 100644
index b4edf945..00000000
--- a/src/mkdocstrings/plugin.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""Deprecated. Import from `mkdocstrings` directly."""
-
-import warnings
-from typing import Any
-
-from mkdocstrings._internal import plugin
-
-
-def __getattr__(name: str) -> Any:
- warnings.warn(
- "Importing from `mkdocstrings.plugin` is deprecated. Import from `mkdocstrings` directly.",
- DeprecationWarning,
- stacklevel=2,
- )
- return getattr(plugin, name)
diff --git a/tests/conftest.py b/tests/conftest.py
index 8a132e29..a2a40652 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -23,7 +23,7 @@ def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Itera
"""Yield a MkDocs configuration object."""
conf = MkDocsConfig()
while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"):
- request = request._parent_request
+ request = request._parent_request # ty: ignore[invalid-assignment]
conf_dict = {
"site_name": "foo",
@@ -33,7 +33,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", [])))
+ mdx_configs: dict[str, Any] = dict(ChainMap(*conf_dict.get("markdown_extensions", []))) # ty: ignore[invalid-argument-type,invalid-assignment]
conf.load_dict(conf_dict)
assert conf.validate() == ([], [])
diff --git a/tests/test_api.py b/tests/test_api.py
index 4b13aac7..7ae732cb 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -50,7 +50,7 @@ def _yield_public_objects(
if modules:
yield member
yield from _yield_public_objects(
- member, # type: ignore[arg-type]
+ member, # ty: ignore[invalid-argument-type]
modules=modules,
modulelevel=modulelevel,
inherited=inherited,
@@ -62,7 +62,7 @@ def _yield_public_objects(
continue
if member.is_class and not modulelevel:
yield from _yield_public_objects(
- member, # type: ignore[arg-type]
+ member, # ty: ignore[invalid-argument-type]
modules=modules,
modulelevel=False,
inherited=inherited,
@@ -91,7 +91,7 @@ def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | g
def _fixture_inventory() -> Inventory:
inventory_file = Path(__file__).parent.parent / "site" / "objects.inv"
if not inventory_file.exists():
- raise pytest.skip("The objects inventory is not available.")
+ pytest.skip("The objects inventory is not available.")
with inventory_file.open("rb") as file:
return Inventory.parse_sphinx(file)
@@ -137,9 +137,11 @@ def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe
"""All public objects are added to the inventory."""
ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"}
not_in_inventory = [
- obj.path for obj in public_objects if obj.name not in ignore_names and obj.path not in inventory
+ f"{obj.relative_filepath}:{obj.lineno}: {obj.path}"
+ for obj in public_objects
+ if obj.name not in ignore_names and obj.path not in inventory
]
- msg = "Objects not in the inventory (try running `make run mkdocs build`):\n{paths}"
+ msg = "Objects not in the inventory (try running `make run zensical build --clean`):\n{paths}"
assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory)))
@@ -150,20 +152,19 @@ def test_inventory_matches_api(
) -> None:
"""The inventory doesn't contain any additional Python object."""
not_in_api = []
- # YORE: Bump 1: Remove line.
- deprecated_modules = {"extension", "handlers", "inventory", "loggers", "plugin"}
public_api_paths = {obj.path for obj in public_objects}
public_api_paths.add("mkdocstrings")
for item in inventory.values():
- if item.domain == "py" and "(" not in item.name:
+ if (
+ item.domain == "py"
+ and "(" not in item.name
+ and (item.name == "mkdocstrings" or item.name.startswith("mkdocstrings."))
+ ):
obj = loader.modules_collection[item.name]
- # YORE: Bump 1: Remove block.
- if any(obj.path.startswith(f"mkdocstrings.{module}") for module in deprecated_modules):
- continue
if obj.path not in public_api_paths and not any(path in public_api_paths for path in obj.aliases):
not_in_api.append(item.name)
- msg = "Inventory objects not in public API (try running `make run mkdocs build`):\n{paths}"
+ msg = "Inventory objects not in public API (try running `make run zensical build --clean`):\n{paths}"
assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api)))
diff --git a/tests/test_extension.py b/tests/test_extension.py
index b7c1c742..5b031842 100644
--- a/tests/test_extension.py
+++ b/tests/test_extension.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import re
-import sys
from textwrap import dedent
from typing import TYPE_CHECKING
@@ -60,7 +59,6 @@ def test_reference_inside_autodoc(ext_markdown: Markdown) -> None:
assert re.search(r"Link to <.*something\.Else.*>something\.Else<.*>\.", output)
-@pytest.mark.skipif(sys.version_info < (3, 8), reason="typing.Literal requires Python 3.8")
def test_quote_inside_annotation(ext_markdown: Markdown) -> None:
"""Assert that inline highlighting doesn't double-escape HTML."""
output = ext_markdown.convert("::: tests.fixtures.string_annotation.Foo")
@@ -101,7 +99,7 @@ def test_no_double_toc(ext_markdown: Markdown, expect_permalink: str) -> None:
)
assert output.count(expect_permalink) == 5
assert 'id="tests.fixtures.headings--foo"' in output
- assert ext_markdown.toc_tokens == [ # type: ignore[attr-defined] # the member gets populated only with 'toc' extension
+ assert ext_markdown.toc_tokens == [ # ty: ignore[unresolved-attribute]
{
"level": 1,
"id": "aa",
@@ -156,10 +154,10 @@ def test_use_custom_handler(ext_markdown: Markdown) -> None:
def test_register_every_identifier_alias(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]
+ handler = plugin._handlers.get_handler("python") # ty: ignore[unresolved-attribute]
ids = ("id1", "id2", "id3")
- handler.get_aliases = lambda _: ids # type: ignore[method-assign]
- autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined]
+ handler.get_aliases = lambda _: ids # ty: ignore[invalid-assignment]
+ autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs
class Page:
url = "foo"
@@ -200,7 +198,6 @@ def test_removing_duplicated_headings(ext_markdown: Markdown) -> None:
assert output.count(">Heading one<") == 1
assert output.count(">Heading two<") == 1
assert output.count(">Heading three<") == 1
- assert output.count('class="mkdocstrings') == 0
def _assert_contains_in_order(items: list[str], string: str) -> None:
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index 30bdbdfc..a1ef4ee6 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -62,7 +62,7 @@ def test_extended_templates(tmp_path: Path, plugin: MkdocstringsPlugin) -> None:
tmp_path: Temporary folder.
plugin: Instance of our plugin.
"""
- handler = plugin._handlers.get_handler("python") # type: ignore[union-attr]
+ handler = plugin._handlers.get_handler("python") # ty: ignore[unresolved-attribute]
# monkeypatch Jinja env search path
search_paths = [
@@ -71,7 +71,7 @@ def test_extended_templates(tmp_path: Path, plugin: MkdocstringsPlugin) -> None:
extended_theme := tmp_path / "extended_theme",
extended_fallback_theme := tmp_path / "extended_fallback_theme",
]
- handler.env.loader.searchpath = search_paths # type: ignore[union-attr]
+ handler.env.loader.searchpath = search_paths # ty: ignore[invalid-assignment]
# assert "new" template is not found
with pytest.raises(expected_exception=TemplateNotFound):
@@ -117,7 +117,7 @@ def test_nested_autodoc(ext_markdown: Markdown) -> None:
)
assert 'id="tests.fixtures.nesting.Class"' in output
assert 'id="tests.fixtures.nesting.Class.method"' in output
- assert ext_markdown.toc_tokens == [ # type: ignore[attr-defined]
+ assert ext_markdown.toc_tokens == [ # ty: ignore[unresolved-attribute]
{
"level": 1,
"id": "tests.fixtures.nesting.Class",
diff --git a/tests/test_inventory.py b/tests/test_inventory.py
index eb008661..858ac340 100644
--- a/tests/test_inventory.py
+++ b/tests/test_inventory.py
@@ -2,9 +2,9 @@
from __future__ import annotations
-import sys
from io import BytesIO
from os.path import join
+from pathlib import Path
import pytest
from mkdocs.commands.build import build
@@ -12,8 +12,6 @@
from mkdocstrings import Inventory, InventoryItem
-sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
-
@pytest.mark.parametrize(
"our_inv",
@@ -22,10 +20,13 @@
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url")]),
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#object_path")]),
Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]),
+ Inventory([InventoryItem(name="o", domain="py", role="obj", uri="u#o", dispname="first line\nsecond line")]),
],
)
def test_sphinx_load_inventory_file(our_inv: Inventory) -> None:
"""Perform the 'live' inventory load test."""
+ sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
+
buffer = BytesIO(our_inv.format_sphinx())
sphinx_inv = sphinx.InventoryFile.load(buffer, "", join)
@@ -36,9 +37,10 @@ def test_sphinx_load_inventory_file(our_inv: Inventory) -> None:
assert item.name in sphinx_inv[f"{item.domain}:{item.role}"]
-@pytest.mark.skipif(sys.version_info < (3, 7), reason="using plugins that require Python 3.7")
def test_sphinx_load_mkdocstrings_inventory_file() -> None:
"""Perform the 'live' inventory load test on mkdocstrings own inventory."""
+ sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed")
+
mkdocs_config = load_config()
mkdocs_config["plugins"].run_event("startup", command="build", dirty=False)
try:
@@ -47,7 +49,7 @@ def test_sphinx_load_mkdocstrings_inventory_file() -> None:
mkdocs_config["plugins"].run_event("shutdown")
own_inv = mkdocs_config["plugins"]["mkdocstrings"].handlers.inventory
- with open("site/objects.inv", "rb") as fp:
+ with Path("site/objects.inv").open("rb") as fp:
sphinx_inv = sphinx.InventoryFile.load(fp, "", join)
sphinx_inv_length = sum(len(sphinx_inv[key]) for key in sphinx_inv)
@@ -55,3 +57,29 @@ def test_sphinx_load_mkdocstrings_inventory_file() -> None:
for item in own_inv.values():
assert item.name in sphinx_inv[f"{item.domain}:{item.role}"]
+
+
+@pytest.mark.parametrize(
+ "our_inv",
+ [
+ Inventory(),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url")]),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#object_path")]),
+ Inventory([InventoryItem(name="object_path", domain="py", role="obj", uri="page_url#other_anchor")]),
+ Inventory([InventoryItem(name="o", domain="py", role="obj", uri="u#o", dispname="first line\nsecond line")]),
+ ],
+)
+def test_mkdocstrings_roundtrip_inventory_file(our_inv: Inventory) -> None:
+ """Save some inventory files, then load them in again."""
+ buffer = BytesIO(our_inv.format_sphinx())
+ round_tripped = Inventory.parse_sphinx(buffer)
+
+ assert our_inv.keys() == round_tripped.keys()
+ for key, value in our_inv.items():
+ round_tripped_item = round_tripped[key]
+ assert round_tripped_item.name == value.name
+ assert round_tripped_item.domain == value.domain
+ assert round_tripped_item.role == value.role
+ assert round_tripped_item.uri == value.uri
+ assert round_tripped_item.priority == value.priority
+ assert round_tripped_item.dispname == value.dispname.splitlines()[0]
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index af8a5594..833de692 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -41,7 +41,7 @@ def test_disabling_plugin(tmp_path: Path) -> None:
mkdocs_config["plugins"].run_event("shutdown")
# make sure the instruction was not processed
- assert "::: mkdocstrings" in site_dir.joinpath("index.html").read_text()
+ assert "::: mkdocstrings" in site_dir.joinpath("index.html").read_text(encoding="utf8")
def test_plugin_default_config(tmp_path: Path) -> None:
@@ -57,6 +57,7 @@ def test_plugin_default_config(tmp_path: Path) -> None:
"custom_templates": None,
"enable_inventory": None,
"enabled": True,
+ "locale": None,
}
@@ -77,4 +78,5 @@ def test_plugin_config_custom_templates(tmp_path: Path) -> None:
"custom_templates": str(template_dir),
"enable_inventory": None,
"enabled": True,
+ "locale": None,
}
diff --git a/zensical.toml b/zensical.toml
new file mode 100644
index 00000000..d04f4896
--- /dev/null
+++ b/zensical.toml
@@ -0,0 +1,214 @@
+[project]
+site_name = "mkdocstrings"
+site_description = "Automatic documentation from sources, for MkDocs."
+site_author = "Timothée Mazzucotelli"
+site_url = "https://mkdocstrings.github.io/mkdocstrings"
+repo_url = "https://github.com/mkdocstrings/mkdocstrings"
+repo_name = "mkdocstrings/mkdocstrings"
+copyright = "Copyright © 2019 Timothée Mazzucotelli"
+extra_css = ["css/apidocs.css"]
+extra_javascript = ["js/feedback.js"]
+nav = [
+ { "Home" = [
+ { "Overview" = "index.md" },
+ { "Changelog" = "changelog.md" },
+ { "Credits" = "credits.md" },
+ { "License" = "license.md" },
+ ] },
+ { "Usage" = [
+ "usage/index.md",
+ { "Theming" = "usage/theming.md" },
+ { "Handlers" = "usage/handlers.md" },
+ { "All handlers" = [
+ { "C" = "https://mkdocstrings.github.io/c/" },
+ { "Crystal" = "https://mkdocstrings.github.io/crystal/" },
+ { "GitHub Actions" = "https://watermarkhu.nl/mkdocstrings-github/" },
+ { "Python" = "https://mkdocstrings.github.io/python/" },
+ { "Python (Legacy)" = "https://mkdocstrings.github.io/python-legacy/" },
+ { "MATLAB" = "https://watermarkhu.nl/mkdocstrings-matlab/" },
+ { "Shell" = "https://mkdocstrings.github.io/shell/" },
+ { "TypeScript" = "https://mkdocstrings.github.io/typescript/" },
+ { "VBA" = "https://pypi.org/project/mkdocstrings-vba" },
+ ] },
+ { "Guides" = [
+ { "Recipes" = "recipes.md" },
+ { "Troubleshooting" = "troubleshooting.md" },
+ ] },
+ ] },
+ { "API reference" = "reference/api.md" },
+ { "Development" = [
+ { "Contributing" = "contributing.md" },
+ { "Code of Conduct" = "code_of_conduct.md" },
+ ] },
+ { "Author's website" = "https://pawamoy.github.io" },
+]
+
+# ----------------------------------------------------------------------------
+# Theme configuration
+# ----------------------------------------------------------------------------
+[project.theme]
+logo = "logo.svg"
+custom_dir = "docs/.overrides"
+language = "en"
+features = [
+ "announce.dismiss",
+ "content.action.edit",
+ "content.action.view",
+ "content.code.annotate",
+ "content.code.copy",
+ "content.code.select",
+ "content.footnote.tooltips",
+ "content.tabs.link",
+ "content.tooltips",
+ "navigation.footer",
+ "navigation.indexes",
+ "navigation.instant",
+ "navigation.instant.prefetch",
+ "navigation.path",
+ "navigation.sections",
+ "navigation.tabs",
+ "navigation.tabs.sticky",
+ "navigation.top",
+ "search.highlight",
+ "toc.follow",
+]
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme)"
+toggle.icon = "material/brightness-auto"
+toggle.name = "Switch to light mode"
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme: light)"
+scheme = "default"
+primary = "teal"
+accent = "purple"
+toggle.icon = "material/weather-sunny"
+toggle.name = "Switch to dark mode"
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme: dark)"
+scheme = "slate"
+primary = "black"
+accent = "lime"
+toggle.icon = "material/weather-night"
+toggle.name = "Switch to system preference"
+
+[project.theme.icon]
+logo = "material/currency-sign"
+
+# ----------------------------------------------------------------------------
+# Social configuration
+# ----------------------------------------------------------------------------
+[[project.extra.social]]
+icon = "fontawesome/brands/github"
+link = "https://github.com/pawamoy"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/mastodon"
+link = "https://fosstodon.org/@pawamoy"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/twitter"
+link = "https://twitter.com/pawamoy"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/gitter"
+link = "https://gitter.im/mkdocstrings/community"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/python"
+link = "https://pypi.org/project/mkdocstrings/"
+
+[project.extra.analytics.feedback]
+title = "Was this page helpful?"
+
+[[project.extra.analytics.feedback.ratings]]
+icon = "material/emoticon-happy-outline"
+name = "This page was helpful"
+data = 1
+note = "Thank you for your feedback!"
+
+[[project.extra.analytics.feedback.ratings]]
+icon = "material/emoticon-sad-outline"
+name = "This page could be improved"
+data = 0
+note = "Let us know how we can improve this page."
+
+# ----------------------------------------------------------------------------
+# Markdown extensions configuration
+# ----------------------------------------------------------------------------
+[project.markdown_extensions.abbr]
+[project.markdown_extensions.admonition]
+[project.markdown_extensions.attr_list]
+[project.markdown_extensions.callouts]
+[project.markdown_extensions.def_list]
+[project.markdown_extensions.footnotes]
+[project.markdown_extensions.md_in_html]
+[project.markdown_extensions.toc]
+permalink = "¤"
+[project.markdown_extensions.pymdownx.arithmatex]
+generic = true
+[project.markdown_extensions.pymdownx.betterem]
+smart_enable = "all"
+[project.markdown_extensions.pymdownx.caret]
+[project.markdown_extensions.pymdownx.details]
+[project.markdown_extensions.pymdownx.emoji]
+emoji_generator = "zensical.extensions.emoji.to_svg"
+emoji_index = "zensical.extensions.emoji.twemoji"
+[project.markdown_extensions.pymdownx.highlight]
+[project.markdown_extensions.pymdownx.inlinehilite]
+[project.markdown_extensions.pymdownx.keys]
+[project.markdown_extensions.pymdownx.magiclink]
+[project.markdown_extensions.pymdownx.mark]
+[project.markdown_extensions.pymdownx.smartsymbols]
+[project.markdown_extensions.pymdownx.snippets]
+check_paths = true
+[project.markdown_extensions.pymdownx.superfences]
+[[project.markdown_extensions.pymdownx.superfences.custom_fences]]
+name = "python"
+class = "python"
+validator = "markdown_exec.validator"
+format = "markdown_exec.formatter"
+[project.markdown_extensions.pymdownx.tabbed]
+alternate_style = true
+[project.markdown_extensions.pymdownx.tasklist]
+custom_checkbox = true
+[project.markdown_extensions.pymdownx.tilde]
+[project.markdown_extensions.zensical.extensions.preview]
+configurations = [{targets.include = ["reference/api.md"]}]
+
+# ----------------------------------------------------------------------------
+# Plugins configuration
+# ----------------------------------------------------------------------------
+[project.plugins.mkdocstrings.handlers.python]
+paths = ["src"]
+inventories = [
+ "https://docs.python.org/3/objects.inv",
+ "https://installer.readthedocs.io/en/stable/objects.inv", # demonstration purpose in the docs
+ "https://mkdocstrings.github.io/autorefs/objects.inv",
+ "https://www.mkdocs.org/objects.inv",
+ "https://python-markdown.github.io/objects.inv",
+ "https://jinja.palletsprojects.com/en/stable/objects.inv",
+ "https://markupsafe.palletsprojects.com/en/stable/objects.inv",
+]
+
+[project.plugins.mkdocstrings.handlers.python.options]
+backlinks = "tree"
+docstring_options = { "ignore_init_summary" = true }
+docstring_section_style = "list"
+filters = "public"
+heading_level = 1
+inherited_members = true
+merge_init_into_class = true
+parameter_headings = true
+separate_signature = true
+show_root_heading = true
+show_root_full_path = false
+show_signature_annotations = true
+show_source = true
+show_symbol_type_heading = true
+show_symbol_type_toc = true
+signature_crossrefs = true
+summary = true
+unwrap_annotated = true