From 38b43f30efb8a552beb5ea5a7028995eec6a6c8a Mon Sep 17 00:00:00 2001 From: ssweber <57631333+ssweber@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:56:36 -0400 Subject: [PATCH 001/223] docs: Add note about newline handling --- docs/troubleshooting.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4d2b074e..bc1da01b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -21,6 +21,10 @@ markdown_extensions: - pymdownx.superfences ``` +For code blocks in docstrings, make sure to escape newlines (`\n` -> `\\n`), +or prefix the entire docstring with 'r' to make it a raw-docstring: `r"""`. +Indeed, docstrings are still strings and therefore subject to how Python parses strings. + ## Footnotes are duplicated or overridden Before version 0.14, footnotes could be duplicated over a page. From e362ef7985aa0cea27fb83dd1041dbd4cc6a6f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 2 Aug 2023 22:02:17 +0200 Subject: [PATCH 002/223] ci: Fix typing for MkDocs 1.5 --- src/mkdocstrings/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 4c902935..7d6a719c 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from jinja2.environment import Environment from mkdocs.config import Config + from mkdocs.config.defaults import MkDocsConfig from mkdocs.livereload import LiveReloadServer if sys.version_info < (3, 10): @@ -167,7 +168,7 @@ def on_serve( log.debug(f"Adding directory '{element}' to watcher") server.watch(element, builder) - def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 + def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Instantiate our Markdown extension. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). @@ -179,7 +180,6 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002 Arguments: config: The MkDocs config object. - **kwargs: Additional arguments passed by MkDocs. Returns: The modified config. From 40c4693b9988bd67027911216a61d73bdb1b6dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 2 Aug 2023 22:02:27 +0200 Subject: [PATCH 003/223] tests: Fix tests for MkDocs 1.5 --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index ebfc2ad8..2119d1f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Itera request = request._parent_request conf_dict = { + "config_file_path": "mkdocs_tests.yml", "site_name": "foo", "site_url": "https://example.org/", "site_dir": str(tmp_path), From 0a90a474c8dcbd95821700d7dab63f03e392c40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 17 Jul 2023 19:16:51 +0200 Subject: [PATCH 004/223] refactor: Remove deprecated parts - Base collector class - Base renderer class - Watch feature - Selection and rendering keys support (YAML options) - `mkdocstrings.handlers` namespace (when importing handlers and finding templates) --- config/mypy.ini | 2 - config/ruff.toml | 1 - docs/usage/handlers.md | 56 +---- docs/usage/index.md | 30 --- pyproject.toml | 1 - src/mkdocstrings/__init__.py | 4 + src/mkdocstrings/extension.py | 25 +-- src/mkdocstrings/handlers/__init__.py | 1 + src/mkdocstrings/handlers/base.py | 309 ++++++-------------------- src/mkdocstrings/plugin.py | 68 +----- tests/test_extension.py | 12 +- 11 files changed, 107 insertions(+), 402 deletions(-) create mode 100644 src/mkdocstrings/__init__.py create mode 100644 src/mkdocstrings/handlers/__init__.py diff --git a/config/mypy.ini b/config/mypy.ini index cb0dd886..814e2ac8 100644 --- a/config/mypy.ini +++ b/config/mypy.ini @@ -3,5 +3,3 @@ ignore_missing_imports = true exclude = tests/fixtures/ warn_unused_ignores = true show_error_codes = true -namespace_packages = true -explicit_package_bases = true diff --git a/config/ruff.toml b/config/ruff.toml index 8f390641..c6e4a55c 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -65,7 +65,6 @@ ignore = [ "E501", # Line too long "ERA001", # Commented out code "G004", # Logging statement uses f-string - "INP001", # File is part of an implicit namespace package "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments to function call diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index e381090e..f4c474f9 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -14,11 +14,8 @@ Since version 0.18, a new, experimental Python handler is available. It is based on [Griffe](https://github.com/mkdocstrings/griffe), which is an improved version of [pytkdocs](https://github.com/mkdocstrings/pytkdocs). -Note that the experimental handler does not yet support third-party libraries -like Django, Marshmallow, Pydantic, etc. -It is also not completely ready to handle dynamically built objects, -like classes built with a call to `type(...)`. -For most other cases, the experimental handler will work just fine. +Note that the experimental handler does not yet support all third-party libraries +that the legacy handler supported. If you want to keep using the legacy handler as long as possible, you can depend on `mkdocstrings-python-legacy` directly, @@ -51,18 +48,13 @@ dependencies = [ ] ``` -#### Handler options - -- `setup_commands` is not yet implemented. In most cases, you won't need it, - since by default the new handler does not execute the code. - #### Selection options WARNING: Since *mkdocstrings* 0.19, the YAML `selection` key is merged into the `options` key. - [x] `filters` is implemented, and used as before. - [x] `members` is implemented, and used as before. -- [ ] `inherited_members` is not yet implemented. +- [x] `inherited_members` is implemented. - [x] `docstring_style` is implemented, and used as before, except for the `restructured-text` style which is renamed `sphinx`. Numpy-style is now built-in, so you can stop depending on `pytkdocs[numpy-style]` @@ -83,13 +75,13 @@ WARNING: Since *mkdocstrings* 0.19, the YAML `rendering` key is merged into the Every previous option is supported. Additional options are available: -- `separate_signature`: Render the signature (or attribute value) in a code block below the heading, +- [x] `separate_signature`: Render the signature (or attribute value) in a code block below the heading, instead as inline code. Useful for long signatures. If Black is installed, the signature is formatted. Default: `False`. -- `line_length`: The maximum line length to use when formatting signatures. Default: `60`. -- `show_submodules`: Whether to render submodules of a module when iterating on children. +- [x] `line_length`: The maximum line length to use when formatting signatures. Default: `60`. +- [x] `show_submodules`: Whether to render submodules of a module when iterating on children. Default: `False`. -- `docstring_section_style`: The style to use to render docstring sections such as attributes, +- [x] `docstring_section_style`: The style to use to render docstring sections such as attributes, parameters, etc. Available styles: `table` (default), `list` and `spacy`. The SpaCy style is a poor implementation of their [table style](https://spacy.io/api/doc/#init). We are open to improvements through PRs! @@ -99,34 +91,8 @@ See [all the handler's options](https://mkdocstrings.github.io/python/usage/). #### Templates Templates are mostly the same as before, but the file layout has changed, -as well as some file names. Here is the new tree: - -``` -๐Ÿ“ theme/ -โ”œโ”€โ”€ ๐Ÿ“„ attribute.html -โ”œโ”€โ”€ ๐Ÿ“„ children.html -โ”œโ”€โ”€ ๐Ÿ“„ class.html -โ”œโ”€โ”€ ๐Ÿ“ docstring/ -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ admonition.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ attributes.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ examples.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ other_parameters.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ parameters.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ raises.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ receives.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ returns.html -โ”‚ย ย  โ”œโ”€โ”€ ๐Ÿ“„ warns.html -โ”‚ย ย  โ””โ”€โ”€ ๐Ÿ“„ yields.html -โ”œโ”€โ”€ ๐Ÿ“„ docstring.html -โ”œโ”€โ”€ ๐Ÿ“„ expression.html -โ”œโ”€โ”€ ๐Ÿ“„ function.html -โ”œโ”€โ”€ ๐Ÿ“„ labels.html -โ”œโ”€โ”€ ๐Ÿ“„ module.html -โ””โ”€โ”€ ๐Ÿ“„ signature.html -``` - -See them [in the handler repository](https://github.com/mkdocstrings/python/tree/8fc8ea5b112627958968823ef500cfa46b63613e/src/mkdocstrings_handlers/python/templates/material). See the documentation about the Python handler templates: -https://mkdocstrings.github.io/python/customization/#templates. +as well as some file names. +See [the documentation about the Python handler templates](https://mkdocstrings.github.io/python/usage/customization/#templates). ## Custom handlers @@ -213,7 +179,7 @@ If your theme's HTML requires CSS to go along with it, put it into a file named `mkdocstrings_handlers/custom_handler/templates/some_theme/style.css`, then this will be included into the final site automatically if this handler is ever used. Alternatively, you can put the CSS as a string into the `extra_css` variable of -your renderer. +your handler. Finally, it's possible to entirely omit templates, and tell *mkdocstrings* to use the templates of another handler. In you handler, override the @@ -225,7 +191,7 @@ from mkdocstrings.handlers.base import BaseHandler class CobraHandler(BaseHandler): - def get_templates_dir(self, handler: str) -> Path: + def get_templates_dir(self, handler: str | None = None) -> Path: # use the python handler templates # (it assumes the python handler is installed) return super().get_templates_dir("python") diff --git a/docs/usage/index.md b/docs/usage/index.md index 3318c053..7599f9f1 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -116,9 +116,6 @@ The above is equivalent to: - `enabled` **(New in version 0.20)**: Whether to enable the plugin. Defaults to `true`. Can be used to reduce build times when doing local development. Especially useful when used with environment variables (see example below). -- `watch` **(deprecated)**: A list of directories to watch while serving the documentation. - See [Watch directories](#watch-directories). Deprecated in favor of the now built-in - [`watch` feature of MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). !!! example ```yaml title="mkdocs.yml" @@ -334,30 +331,3 @@ plugins: - mkdocstrings: enable_inventory: false ``` - -## Watch directories - -DANGER: **Deprecated since version 0.19.** -Instead, use the built-in [`watch` feature of MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). - -You can add directories to watch with the `watch` key. -It accepts a list of paths. - -```yaml title="mkdocs.yml" -plugins: - - mkdocstrings: - watch: - - src/my_package_1 - - src/my_package_2 -``` - -When serving your documentation -and a change occur in one of the listed path, -MkDocs will rebuild the site and reload the current page. - -NOTE: **The `watch` feature doesn't have special effects.** -Adding directories to the `watch` list doesn't have any other effect than watching for changes. -For example, it will not tell the Python handler to look for packages in these paths -(the paths are not added to the `PYTHONPATH` variable). -If you want to tell Python where to look for packages and modules, -see [Python Handler: Finding modules](https://mkdocstrings.github.io/python/usage/#finding-modules). diff --git a/pyproject.toml b/pyproject.toml index d43ac7ca..57f09f4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,6 @@ plugins = [ [tool.pdm.build] package-dir = "src" -includes = ["src/mkdocstrings"] editable-backend = "editables" [tool.pdm.dev-dependencies] diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py new file mode 100644 index 00000000..03550f9b --- /dev/null +++ b/src/mkdocstrings/__init__.py @@ -0,0 +1,4 @@ +"""mkdocstrings package. + +Automatic documentation from sources, for MkDocs. +""" diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index e5e2c507..c33f37ef 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -12,19 +12,17 @@ ```yaml ::: some.identifier handler: python - selection: + options: option1: value1 option2: - - value2a - - value2b - rendering: + - value2a + - value2b option_x: etc ``` """ from __future__ import annotations -import functools import re from collections import ChainMap from typing import TYPE_CHECKING, Any, MutableSequence @@ -184,13 +182,7 @@ def _process_block( global_options = handler_config.get("options", {}) local_options = config.get("options", {}) - deprecated_global_options = ChainMap(handler_config.get("selection", {}), handler_config.get("rendering", {})) - deprecated_local_options = ChainMap(config.get("selection", {}), config.get("rendering", {})) - - options = ChainMap(local_options, deprecated_local_options, global_options, deprecated_global_options) - - if deprecated_global_options or deprecated_local_options: - self._warn_about_options_key() + options = ChainMap(local_options, global_options) if heading_level: options = ChainMap(options, {"heading_level": heading_level}) # like setdefault @@ -200,12 +192,12 @@ def _process_block( data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: log.error(str(exception)) # noqa: TRY400 - if PluginError is SystemExit: # When MkDocs 1.2 is sufficiently common, this can be dropped. + if PluginError is SystemExit: # TODO: when MkDocs 1.2 is sufficiently common, this can be dropped. log.error(f"Error reading page '{self._autorefs.current_page}':") # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. - log.debug("Updating renderer's env") + log.debug("Updating handler's rendering env") handler._update_env(self.md, self._config) self._updated_envs.add(handler_name) @@ -221,11 +213,6 @@ def _process_block( return rendered, handler, data - @classmethod - @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_options_key(cls) -> None: - log.info("DEPRECATION: 'selection' and 'rendering' are deprecated and merged into a single 'options' YAML key") - class _PostProcessor(Treeprocessor): def run(self, root: Element) -> None: diff --git a/src/mkdocstrings/handlers/__init__.py b/src/mkdocstrings/handlers/__init__.py new file mode 100644 index 00000000..b9e2a29c --- /dev/null +++ b/src/mkdocstrings/handlers/__init__.py @@ -0,0 +1 @@ +"""Handlers module.""" diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index ad53e961..33304351 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -1,6 +1,6 @@ """Base module for handlers. -This module contains the base classes for implementing collectors, renderers, and the combination of the two: handlers. +This module contains the base classes for implementing handlers. It also provides two methods: @@ -12,8 +12,6 @@ import importlib import sys -import warnings -from contextlib import suppress from pathlib import Path from typing import Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence from xml.etree.ElementTree import Element, tostring @@ -66,21 +64,32 @@ def do_any(seq: Sequence, attribute: str | None = None) -> bool: return any(_[attribute] for _ in seq) -class BaseRenderer: - """The base renderer class. +class BaseHandler: + """The base handler class. - Inherit from this class to implement a renderer. + Inherit from this class to implement a handler. - You will have to implement the `render` method. - You can also override the `update_env` method, to add more filters to the Jinja environment, + You will have to implement the `collect` and `render` methods. + You can also implement the `teardown` method, + and override the `update_env` method, to add more filters to the Jinja environment, making them available in your Jinja templates. To define a fallback theme, add a `fallback_theme` class-variable. To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates. """ + name: str = "" + """The handler's name, for example "python".""" + domain: str = "default" + """The handler's domain, used to register objects in the inventory, for example "py".""" + enable_inventory: bool = False + """Whether the inventory creation is enabled.""" + fallback_config: ClassVar[dict] = {} + """Fallback configuration when searching anchors for identifiers.""" fallback_theme: str = "" + """Fallback theme to use when a template isn't found in the configured theme.""" extra_css = "" + """Extra CSS.""" def __init__(self, handler: str, theme: str, custom_templates: str | None = None) -> None: """Initialize the object. @@ -95,11 +104,6 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None """ paths = [] - # TODO: remove once BaseRenderer is merged into BaseHandler - self._handler = handler - self._theme = theme - self._custom_templates = custom_templates - # add selected theme templates themes_dir = self.get_templates_dir(handler) paths.append(themes_dir / theme) @@ -137,6 +141,44 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None self._headings: list[Element] = [] self._md: Markdown = None # type: ignore[assignment] # To be populated in `update_env`. + @classmethod + def load_inventory( + cls, + in_file: BinaryIO, # noqa: ARG003 + url: str, # noqa: ARG003 + base_url: str | None = None, # noqa: ARG003 + **kwargs: Any, # noqa: ARG003 + ) -> Iterator[tuple[str, str]]: + """Yield items and their URLs from an inventory file streamed from `in_file`. + + Arguments: + in_file: The binary file-like object to read the inventory from. + url: The URL that this file is being streamed from (used to guess `base_url`). + base_url: The URL that this inventory's sub-paths are relative to. + **kwargs: Ignore additional arguments passed from the config. + + Yields: + Tuples of (item identifier, item URL). + """ + yield from () + + def collect(self, identifier: str, config: MutableMapping[str, Any]) -> CollectorItem: + """Collect data given an identifier and user configuration. + + In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into + a Python dictionary for example, though the implementation is completely free. + + Arguments: + identifier: An identifier for which to collect data. For example, in Python, + it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. + It can be anything that you can feed to the tool of your choice. + config: The handler's configuration options. + + Returns: + Anything you want, as long as you can feed it to the handler's `render` method. + """ + raise NotImplementedError + def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: """Render a template using provided data and configuration options. @@ -149,7 +191,14 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: """ raise NotImplementedError - def get_templates_dir(self, handler: str) -> Path: + def teardown(self) -> None: + """Teardown the handler. + + This method should be implemented to, for example, terminate a subprocess + that was started when creating the handler instance. + """ + + def get_templates_dir(self, handler: str | None = None) -> Path: """Return the path to the handler's templates directory. Override to customize how the templates directory is found. @@ -158,41 +207,21 @@ def get_templates_dir(self, handler: str) -> Path: handler: The name of the handler to get the templates directory of. Raises: + ModuleNotFoundError: When no such handler is installed. FileNotFoundError: When the templates directory cannot be found. Returns: The templates directory path. """ - # Templates can be found in 2 different logical locations: - # - in mkdocstrings_handlers/HANDLER/templates: our new migration target - # - in mkdocstrings/templates/HANDLER: current situation, this should be avoided - # These two other locations are forbidden: - # - in mkdocstrings_handlers/templates/HANDLER: sub-namespace packages are too annoying to deal with - # - in mkdocstrings/handlers/HANDLER/templates: not currently supported, - # and mkdocstrings will stop being a namespace - - with suppress(ModuleNotFoundError): # TODO: catch at some point to warn about missing handlers + handler = handler or self.name + try: import mkdocstrings_handlers + except ModuleNotFoundError as error: + raise ModuleNotFoundError(f"Handler '{handler}' not found, is it installed?") from error - for path in mkdocstrings_handlers.__path__: - theme_path = Path(path, handler, "templates") - if theme_path.exists(): - return theme_path - - # TODO: remove import and loop at some point, - # as mkdocstrings will stop being a namespace package - import mkdocstrings - - for path in mkdocstrings.__path__: - theme_path = Path(path, "templates", handler) + for path in mkdocstrings_handlers.__path__: + theme_path = Path(path, handler, "templates") if theme_path.exists(): - if handler != "python": - warnings.warn( - "Exposing templates in the mkdocstrings.templates namespace is deprecated. " - "Put them in a templates folder inside your handler package instead.", - DeprecationWarning, - stacklevel=1, - ) return theme_path raise FileNotFoundError(f"Can't find 'templates' folder for handler '{handler}'") @@ -209,7 +238,7 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: + def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: # noqa: ARG002 """Return the possible identifiers (HTML anchors) for a collected item. Arguments: @@ -218,11 +247,7 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: Returns: The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. """ - # TODO: remove this at some point - try: - return (self.get_anchor(data),) # type: ignore[attr-defined] - except AttributeError: - return () + return () def do_convert_markdown( self, @@ -346,181 +371,6 @@ def _update_env(self, md: Markdown, config: dict) -> None: self.update_env(new_md, config) -class BaseCollector: - """The base collector class. - - Inherit from this class to implement a collector. - - You will have to implement the `collect` method. - You can also implement the `teardown` method. - """ - - def collect(self, identifier: str, config: MutableMapping[str, Any]) -> CollectorItem: - """Collect data given an identifier and selection configuration. - - In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into - a Python dictionary for example, though the implementation is completely free. - - Arguments: - identifier: An identifier for which to collect data. For example, in Python, - it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. - It can be anything that you can feed to the tool of your choice. - config: The handler's configuration options. - - Returns: - Anything you want, as long as you can feed it to the renderer's `render` method. - """ - raise NotImplementedError - - def teardown(self) -> None: - """Teardown the collector. - - This method should be implemented to, for example, terminate a subprocess - that was started when creating the collector instance. - """ - - -class BaseHandler(BaseCollector, BaseRenderer): - """The base handler class. - - Inherit from this class to implement a handler. - - It's usually just a combination of a collector and a renderer, but you can make it as complex as you need. - - Attributes: - domain: The cross-documentation domain/language for this handler. - enable_inventory: Whether this handler is interested in enabling the creation - of the `objects.inv` Sphinx inventory file. - fallback_config: The configuration used to collect item during autorefs fallback. - """ - - domain: str = "default" - enable_inventory: bool = False - fallback_config: ClassVar[dict] = {} - - # TODO: once the BaseCollector and BaseRenderer classes are removed, - # stop accepting the 'handler' parameter, and instead set a 'name' attribute on the Handler class. - # Then make the 'handler' parameter in 'get_templates_dir' optional, and use the class 'name' by default. - def __init__(self, *args: str | BaseCollector | BaseRenderer, **kwargs: str | BaseCollector | BaseRenderer) -> None: - """Initialize the object. - - Arguments: - *args: Collector and renderer, or handler name, theme and custom_templates. - **kwargs: Same thing, but with keyword arguments. - - Raises: - ValueError: When the given parameters are invalid. - """ - # The method accepts *args and **kwargs temporarily, - # to support the transition period where the BaseCollector - # and BaseRenderer are deprecated, and the BaseHandler - # can be instantiated with both instances of collector/renderer, - # or renderer parameters, as positional parameters. - - collector = None - renderer = None - - # parsing positional arguments - str_args = [] - for arg in args: - if isinstance(arg, BaseCollector): - collector = arg - elif isinstance(arg, BaseRenderer): - renderer = arg - elif isinstance(arg, str): - str_args.append(arg) - - while len(str_args) != 3: # noqa: PLR2004 - str_args.append(None) # type: ignore[arg-type] - - handler, theme, custom_templates = str_args - - # fetching values from keyword arguments - if "collector" in kwargs: - collector = kwargs.pop("collector") # type: ignore[assignment] - if "renderer" in kwargs: - renderer = kwargs.pop("renderer") # type: ignore[assignment] - if "handler" in kwargs: - handler = kwargs.pop("handler") # type: ignore[assignment] - if "theme" in kwargs: - theme = kwargs.pop("theme") # type: ignore[assignment] - if "custom_templates" in kwargs: - custom_templates = kwargs.pop("custom_templates") # type: ignore[assignment] - - if collector is None and renderer is not None or collector is not None and renderer is None: - raise ValueError("both 'collector' and 'renderer' must be provided") - - if collector is not None: - warnings.warn( - DeprecationWarning( - "The BaseCollector class is deprecated, and passing an instance of it " - "to your handler is deprecated as well. Instead, define the `collect` and `teardown` " - "methods directly on your handler class.", - ), - stacklevel=1, - ) - self.collector = collector - self.collect = collector.collect # type: ignore[method-assign] - self.teardown = collector.teardown # type: ignore[method-assign] - - if renderer is not None: - if {handler, theme, custom_templates} != {None}: - raise ValueError( - "'handler', 'theme' and 'custom_templates' must all be None when providing a renderer instance", - ) - warnings.warn( - DeprecationWarning( - "The BaseRenderer class is deprecated, and passing an instance of it " - "to your handler is deprecated as well. Instead, define the `render` method " - "directly on your handler class (as well as other methods and attributes like " - "`get_templates_dir`, `get_anchors`, `update_env` and `fallback_theme`, `extra_css`).", - ), - stacklevel=1, - ) - self.renderer = renderer - self.render = renderer.render # type: ignore[method-assign] - self.get_templates_dir = renderer.get_templates_dir # type: ignore[method-assign] - self.get_anchors = renderer.get_anchors # type: ignore[method-assign] - self.do_convert_markdown = renderer.do_convert_markdown # type: ignore[method-assign] - self.do_heading = renderer.do_heading # type: ignore[method-assign] - self.get_headings = renderer.get_headings # type: ignore[method-assign] - self.update_env = renderer.update_env # type: ignore[method-assign] - self._update_env = renderer._update_env # type: ignore[method-assign] - self.fallback_theme = renderer.fallback_theme - self.extra_css = renderer.extra_css - renderer.__class__.__init__( - self, - renderer._handler, - renderer._theme, - renderer._custom_templates, - ) - else: - if handler is None or theme is None: - raise ValueError("'handler' and 'theme' cannot be None") - BaseRenderer.__init__(self, handler, theme, custom_templates) - - @classmethod - def load_inventory( - cls, - in_file: BinaryIO, # noqa: ARG003 - url: str, # noqa: ARG003 - base_url: str | None = None, # noqa: ARG003 - **kwargs: Any, # noqa: ARG003 - ) -> Iterator[tuple[str, str]]: - """Yield items and their URLs from an inventory file streamed from `in_file`. - - Arguments: - in_file: The binary file-like object to read the inventory from. - url: The URL that this file is being streamed from (used to guess `base_url`). - base_url: The URL that this inventory's sub-paths are relative to. - **kwargs: Ignore additional arguments passed from the config. - - Yields: - Tuples of (item identifier, item URL). - """ - yield from () - - class Handlers: """A collection of handlers. @@ -543,7 +393,7 @@ def get_anchors(self, identifier: str) -> tuple[str, ...] | set[str]: """Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it. Arguments: - identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept). + identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseHandler.collect] can accept). Returns: A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it. @@ -606,18 +456,7 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand if handler_config is None: handler_config = self.get_handler_config(name) handler_config.update(self._config) - try: - module = importlib.import_module(f"mkdocstrings_handlers.{name}") - except ModuleNotFoundError: - module = importlib.import_module(f"mkdocstrings.handlers.{name}") - if name != "python": - warnings.warn( - DeprecationWarning( - "Using the mkdocstrings.handlers namespace is deprecated. " - "Handlers must now use the mkdocstrings_handlers namespace.", - ), - stacklevel=1, - ) + module = importlib.import_module(f"mkdocstrings_handlers.{name}") self._handlers[name] = module.get_handler( theme=self._config["theme_name"], custom_templates=self._config["mkdocstrings"]["custom_templates"], diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 7d6a719c..484d3ead 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -35,7 +35,6 @@ from jinja2.environment import Environment from mkdocs.config import Config from mkdocs.config.defaults import MkDocsConfig - from mkdocs.livereload import LiveReloadServer if sys.version_info < (3, 10): from typing_extensions import ParamSpec @@ -44,11 +43,6 @@ log = get_logger(__name__) -SELECTION_OPTS_KEY: str = "selection" -"""Deprecated. The name of the selection parameter in YAML configuration blocks.""" -RENDERING_OPTS_KEY: str = "rendering" -"""Deprecated. The name of the rendering parameter in YAML configuration blocks.""" - InventoryImportType = List[Tuple[str, Mapping[str, Any]]] InventoryLoaderType = Callable[..., Iterable[Tuple[str, str]]] @@ -76,14 +70,12 @@ class MkdocstringsPlugin(BasePlugin): - `on_config` - `on_env` - `on_post_build` - - `on_serve` Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs` for more information about its plugin system. """ - config_scheme: tuple[tuple[str, MkType]] = ( - ("watch", MkType(list, default=[])), # type: ignore[assignment] + config_scheme: tuple[tuple[str, MkType]] = ( # type: ignore[assignment] ("handlers", MkType(dict, default={})), ("default_handler", MkType(str, default="python")), ("custom_templates", MkType(str, default=None)), @@ -95,13 +87,12 @@ class MkdocstringsPlugin(BasePlugin): Available options are: - - **`watch` (deprecated)**: A list of directories to watch. Only used when serving the documentation with mkdocs. - Whenever a file changes in one of directories, the whole documentation is built again, and the browser refreshed. - Deprecated in favor of the now built-in `watch` feature of MkDocs. - - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". - - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. - **`handlers`**: Global configuration of handlers. You can set global configuration per handler, applied everywhere, but overridable in each "autodoc" instruction. Example: + - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". + - **`custom_templates`**: Custom templates to use when rendering API objects. + - **`enable_inventory`**: Whether to enable object inventory creation. + - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. ```yaml plugins: @@ -109,11 +100,11 @@ class MkdocstringsPlugin(BasePlugin): handlers: python: options: - selection_opt: true - rendering_opt: "value" + option1: true + option2: "value" rust: options: - selection_opt: 2 + option9: 2 ``` """ @@ -138,36 +129,6 @@ def handlers(self) -> Handlers: raise RuntimeError("The plugin hasn't been initialized with a config yet") return self._handlers - # TODO: remove once watch feature is removed - def on_serve( - self, - server: LiveReloadServer, - config: Config, # noqa: ARG002 - builder: Callable, - *args: Any, # noqa: ARG002 - **kwargs: Any, # noqa: ARG002 - ) -> None: - """Watch directories. - - Hook for the [`on_serve` event](https://www.mkdocs.org/user-guide/plugins/#on_serve). - In this hook, we add the directories specified in the plugin's configuration to the list of directories - watched by `mkdocs`. Whenever a change occurs in one of these directories, the documentation is built again - and the site reloaded. - - Arguments: - server: The `livereload` server instance. - config: The MkDocs config object (unused). - builder: The function to build the site. - *args: Additional arguments passed by MkDocs. - **kwargs: Additional arguments passed by MkDocs. - """ - if not self.plugin_enabled: - return - if self.config["watch"]: - for element in self.config["watch"]: - log.debug(f"Adding directory '{element}' to watcher") - server.watch(element, builder) - def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Instantiate our Markdown extension. @@ -238,9 +199,6 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: self._inv_futures[future] = (loader, import_item) inv_loader.shutdown(wait=False) - if self.config["watch"]: - self._warn_about_watch_option() - return config @property @@ -311,7 +269,7 @@ def on_post_build( For example, a handler could open a subprocess in the background and keep it open to feed it "autodoc" instructions and get back JSON data. If so, it should then close the subprocess at some point: - the proper place to do this is in the collector's `teardown` method, which is indirectly called by this hook. + the proper place to do this is in the handler's `teardown` method, which is indirectly called by this hook. Arguments: config: The MkDocs config object. @@ -362,11 +320,3 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - result = dict(loader(content, url=url, **kwargs)) log.debug(f"Loaded inventory from {url!r}: {len(result)} items") return result - - @classmethod - @functools.lru_cache(maxsize=None) # Warn only once - def _warn_about_watch_option(cls) -> None: - log.info( - "DEPRECATION: mkdocstrings' watch feature is deprecated in favor of MkDocs' watch feature, " - "see https://www.mkdocs.org/user-guide/configuration/#watch", - ) diff --git a/tests/test_extension.py b/tests/test_extension.py index f7c3cecc..57a1c903 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import re import sys from textwrap import dedent @@ -147,14 +146,7 @@ def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ex assert identifier not in autorefs._abs_url_map -def test_use_deprecated_yaml_keys(ext_markdown: Markdown, caplog: pytest.LogCaptureFixture) -> None: - """Check that using the deprecated 'selection' and 'rendering' YAML keys emits a deprecation warning.""" - caplog.set_level(logging.INFO) - assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n rendering:\n heading_level: 2") - assert "single 'options' YAML key" in caplog.text - - -def test_use_new_options_yaml_key(ext_markdown: Markdown) -> None: - """Check that using the new 'options' YAML key works as expected.""" +def test_use_options_yaml_key(ext_markdown: Markdown) -> None: + """Check that using the 'options' YAML key works as expected.""" assert "h1" in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 1") assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") From 2e10374be258e9713b26f73dd06d0c2520ec07a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 21 Aug 2023 16:52:31 +0200 Subject: [PATCH 005/223] refactor: Stop accepting sets as return value of `get_anchors` (only tuples), to preserve order --- src/mkdocstrings/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 33304351..4fa79724 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -238,7 +238,7 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]: # noqa: ARG002 + def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 """Return the possible identifiers (HTML anchors) for a collected item. Arguments: From b6ddf373579ed4c997fc4362996034ba38c5dc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:01:32 +0200 Subject: [PATCH 006/223] tests: Fix typing in tests --- tests/test_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_extension.py b/tests/test_extension.py index 57a1c903..8c687629 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -137,7 +137,7 @@ def test_use_custom_handler(ext_markdown: Markdown) -> None: def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ext_markdown: Markdown) -> None: """Assert that we don't preemptively register all identifiers of a rendered object.""" handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] - ids = {"id1", "id2", "id3"} + ids = ("id1", "id2", "id3") handler.get_anchors = lambda _: ids # type: ignore[method-assign] ext_markdown.convert("::: tests.fixtures.headings") autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs From 7690d41e2871997464367e673023585c4fb05e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 15:00:24 +0200 Subject: [PATCH 007/223] fix: Don't add `codehilite` CSS class to inline code --- src/mkdocstrings/handlers/rendering.py | 9 ++++++++- tests/test_handlers.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index c3fb236a..6009935a 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -68,11 +68,14 @@ def __init__(self, md: Markdown): md: The Markdown instance to read configs from. """ config: dict[str, Any] = {} + self._highlighter: str | None = None for ext in md.registeredExtensions: if isinstance(ext, HighlightExtension) and (ext.enabled or not config): + self._highlighter = "highlight" config = ext.getConfigs() break # This one takes priority, no need to continue looking if isinstance(ext, CodeHiliteExtension) and not config: + self._highlighter = "codehilite" config = ext.getConfigs() config["language_prefix"] = config["lang_prefix"] self._css_class = config.pop("css_class", "highlight") @@ -116,7 +119,11 @@ def highlight( self.linenums = old_linenums if inline: - return Markup(f'{result.text}') + # From the maintainer of codehilite, the codehilite CSS class, as defined by the user, + # should never be added to inline code, because codehilite does not support inline code. + # See https://github.com/Python-Markdown/markdown/issues/1220#issuecomment-1692160297. + css_class = "" if self._highlighter == "codehilite" else kwargs["css_class"] + return Markup(f'{result.text}') return Markup(result) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 510ebd8a..4a07e98b 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -32,7 +32,7 @@ def test_highlighter_without_pygments(extension_name: str) -> None: ) assert ( hl.highlight("import foo", language="python", inline=True) - == 'import foo' + == f'import foo' ) From 43960110c9c2edf741351727c0d1a31d2eabc7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 15:44:06 +0200 Subject: [PATCH 008/223] chore: Template upgrade --- .copier-answers.yml | 2 +- docs/css/mkdocstrings.css | 1 + docs/insiders/index.md | 2 +- docs/insiders/installation.md | 2 +- duties.py | 6 +++--- mkdocs.yml | 5 +++++ pyproject.toml | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index ab242b1c..c720007f 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.16.4 +_commit: 0.16.6 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: Timothรฉe Mazzucotelli diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 2db20680..3960e49e 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -9,6 +9,7 @@ a.external::after, a.autorefs-external::after { /* https://primer.style/octicons/arrow-up-right-24 */ mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); content: ' '; display: inline-block; diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 3a55ad5d..bfb2d428 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -219,7 +219,7 @@ by the [ISC License][license]. However, we kindly ask you to respect our [goals completed]: #goals-completed [github sponsor profile]: https://github.com/sponsors/pawamoy [billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle -[license]: ../license/ +[license]: ../license.md [private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md index 368f1b47..b7af7d2e 100644 --- a/docs/insiders/installation.md +++ b/docs/insiders/installation.md @@ -107,7 +107,7 @@ or installing a package (with pip), and depending on the registry you are using Please check the documentation of your registry to learn how to configure your environment. **We kindly ask that you do not upload the distributions to public registries, -as it is against our [Terms of use](../#terms).** +as it is against our [Terms of use](index.md#terms).** >? TIP: **Full example with `pypiserver`** > In this example we use [pypiserver] to serve a local PyPI index. diff --git a/duties.py b/duties.py index 2e1248bf..644b2ffb 100644 --- a/duties.py +++ b/duties.py @@ -53,10 +53,10 @@ def merge(d1: Any, d2: Any) -> Any: # noqa: D103 def mkdocs_config() -> str: # noqa: D103 - from mkdocs import utils + import mergedeep - # patch YAML loader to merge arrays - utils.merge = merge + # force YAML loader to merge arrays + mergedeep.merge = merge if "+insiders" in pkgversion("mkdocs-material"): return "mkdocs.insiders.yml" diff --git a/mkdocs.yml b/mkdocs.yml index d7500280..65541ffc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,11 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index 57f09f4d..78af6bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ docs = [ "black>=23.1", "markdown-callouts>=0.2", "markdown-exec>=0.5", - "mkdocs>=1.3", + "mkdocs>=1.5", "mkdocs-coverage>=0.2", "mkdocs-gen-files>=0.3", "mkdocs-git-committers-plugin-2>=1.1", From 0b06d6a7ea1e45edfb066cfd3acb4bfca9927b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:02:31 +0200 Subject: [PATCH 009/223] docs: Fix link (MkDocs 1.5) --- docs/usage/handlers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index f4c474f9..d2c25420 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -234,7 +234,7 @@ could add specific support for another Python library. NOTE: This feature is intended for developers. If you are a user and want to customize how objects are rendered, -see [Theming / Customization](../theming/#customization). +see [Theming / Customization](theming.md#customization). Such extensions can register additional template folders that will be used when rendering collected data. From 228fb737caca4e20e600053bf59cbfa3e9c73906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:06:07 +0200 Subject: [PATCH 010/223] feat: Register all anchors for each object in the inventory --- src/mkdocstrings/extension.py | 29 ++++++++++++++++++++++------- src/mkdocstrings/inventory.py | 29 ++++++++++++++--------------- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index c33f37ef..139030ba 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -137,13 +137,28 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: self._autorefs.register_anchor(page, rendered_anchor) if "data-role" in heading.attrib: - for anchor in sorted({rendered_anchor, *handler.get_anchors(data)}): - self._handlers.inventory.register( - name=anchor, - domain=handler.domain, - role=heading.attrib["data-role"], - uri=f"{page}#{rendered_anchor}", - ) + self._handlers.inventory.register( + name=rendered_anchor, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=1, # register with standard priority + uri=f"{page}#{rendered_anchor}", + ) + + # also register other anchors for this object in the inventory + try: + data_object = handler.collect(rendered_anchor, handler.fallback_config) + except CollectionError: + continue + for anchor in handler.get_anchors(data_object): + if anchor not in self._handlers.inventory: + self._handlers.inventory.register( + name=anchor, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=2, # register with lower priority + uri=f"{page}#{rendered_anchor}", + ) parent.append(el) diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index 4f2157ca..98dd347a 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -20,7 +20,7 @@ def __init__( domain: str, role: str, uri: str, - priority: str = "1", + priority: int = 1, dispname: str | None = None, ): """Initialize the object. @@ -30,14 +30,14 @@ def __init__( domain: The item domain, like 'python' or 'crystal'. role: The item role, like 'class' or 'method'. uri: The item URI. - priority: The item priority. It can help for inventory suggestions. + priority: The item priority. Only used internally by mkdocstrings and Sphinx. dispname: The item display name. """ self.name: str = name self.domain: str = domain self.role: str = role self.uri: str = uri - self.priority: str = priority + self.priority: int = priority self.dispname: str = dispname or name def format_sphinx(self) -> str: @@ -67,7 +67,7 @@ def parse_sphinx(cls, line: str) -> InventoryItem: uri = uri[:-1] + name if dispname == "-": dispname = name - return cls(name, domain, role, uri, priority, dispname) + return cls(name, domain, role, uri, int(priority), dispname) class Inventory(dict): @@ -94,7 +94,7 @@ def register( domain: str, role: str, uri: str, - priority: str = "1", + priority: int = 1, dispname: str | None = None, ) -> None: """Create and register an item. @@ -104,18 +104,17 @@ def register( domain: The item domain, like 'python' or 'crystal'. role: The item role, like 'class' or 'method'. uri: The item URI. - priority: The item priority. It can help for inventory suggestions. + priority: The item priority. Only used internally by mkdocstrings and Sphinx. dispname: The item display name. """ - if name not in self: - self[name] = InventoryItem( - name=name, - domain=domain, - role=role, - uri=uri, - priority=priority, - dispname=dispname, - ) + self[name] = InventoryItem( + name=name, + domain=domain, + role=role, + uri=uri, + priority=priority, + dispname=dispname, + ) def format_sphinx(self) -> bytes: """Format this inventory as a Sphinx `objects.inv` file. From 9371e9fc7dd68506b73aa1580a12c5f5cd779aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:06:45 +0200 Subject: [PATCH 011/223] refactor: Sort inventories --- src/mkdocstrings/inventory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index 98dd347a..2a1e6865 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -135,7 +135,10 @@ def format_sphinx(self) -> bytes: .encode("utf8") ) - lines = [item.format_sphinx().encode("utf8") for item in self.values()] + lines = [ + item.format_sphinx().encode("utf8") + for item in sorted(self.values(), key=lambda item: (item.domain, item.name)) + ] return header + zlib.compress(b"\n".join(lines) + b"\n", 9) @classmethod @@ -155,4 +158,4 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines] if domain_filter: items = [item for item in items if item.domain in domain_filter] - return cls(items) + return cls(sorted(items, key=lambda item: (item.domain, item.name))) From b89bb2de34b23908edff0aa82813404702d29600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:21:04 +0200 Subject: [PATCH 012/223] docs: Remove old contents --- src/mkdocstrings/handlers/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 4fa79724..06430da6 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -1,11 +1,6 @@ """Base module for handlers. This module contains the base classes for implementing handlers. - -It also provides two methods: - -- `get_handler`, that will cache handlers into the `HANDLERS_CACHE` dictionary. -- `teardown`, that will teardown all the cached handlers, and then clear the cache. """ from __future__ import annotations From fe8e3c5044c7dffa5c2de0c7532fe015cfd9383a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:21:14 +0200 Subject: [PATCH 013/223] docs: Enable auto-summaries --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 65541ffc..fdd6898b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -142,6 +142,7 @@ plugins: show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true + summary: true - git-committers: enabled: !ENV [DEPLOY, false] repository: mkdocstrings/mkdocstrings From 9397460bc7a6de4c73e0a545b9bc148db29893cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 28 Aug 2023 16:29:10 +0200 Subject: [PATCH 014/223] refactor: Try calling deprecated `get_anchor` for a bit longer --- src/mkdocstrings/handlers/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 06430da6..700a0565 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -233,7 +233,7 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 + def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: """Return the possible identifiers (HTML anchors) for a collected item. Arguments: @@ -242,7 +242,11 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 Returns: The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. """ - return () + # TODO: remove this when https://github.com/mkdocstrings/crystal/pull/6 is merged and released + try: + return (self.get_anchor(data),) # type: ignore[attr-defined] + except AttributeError: + return () def do_convert_markdown( self, From 3ed3453b893b53ee129cbd9fece3402c7e7083fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 2 Sep 2023 11:31:06 +0200 Subject: [PATCH 015/223] refactor: Don't sort inventories when reading them, it's useless --- src/mkdocstrings/inventory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index 2a1e6865..f1c8962a 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -150,7 +150,7 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) domain_filter: A collection of domain values to allow (and filter out all other ones). Returns: - An `Inventory` containing the collected `InventoryItem`s. + An inventory containing the collected items. """ for _ in range(4): in_file.readline() @@ -158,4 +158,4 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines] if domain_filter: items = [item for item in items if item.domain in domain_filter] - return cls(sorted(items, key=lambda item: (item.domain, item.name))) + return cls(items) From c071740f20ab7b5d5d1a4ef1fc7193663280a903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 2 Sep 2023 11:31:54 +0200 Subject: [PATCH 016/223] chore: Prepare release 0.23.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbeb8ce6..fc3a0fb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.23.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.23.0) - 2023-08-28 + +[Compare with 0.22.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.22.0...0.23.0) + +### Breaking Changes + +- Removed `BaseCollector` and `BaseRenderer` classes: they were merged into the `BaseHandler` class. +- Removed the watch feature, as MkDocs now provides it natively. +- Removed support for `selection` and `rendering` keys in YAML blocks: use `options` instead. +- Removed support for loading handlers from the `mkdocstrings.handler` namespace. + Handlers must now be packaged under the `mkdocstrings_handlers` namespace. + +### Features + +- Register all anchors for each object in the inventory ([228fb73](https://github.com/mkdocstrings/mkdocstrings/commit/228fb737caca4e20e600053bf59cbfa3e9c73906) by Timothรฉe Mazzucotelli). + +### Bug Fixes + +- Don't add `codehilite` CSS class to inline code ([7690d41](https://github.com/mkdocstrings/mkdocstrings/commit/7690d41e2871997464367e673023585c4fb05e26) by Timothรฉe Mazzucotelli). +- Support cross-references for API docs rendered in top-level index page ([b194452](https://github.com/mkdocstrings/mkdocstrings/commit/b194452be93aee33b3c28a468762b4d96c501f4f) by Timothรฉe Mazzucotelli). + +### Code Refactoring + +- Sort inventories before writing them to disk ([9371e9f](https://github.com/mkdocstrings/mkdocstrings/commit/9371e9fc7dd68506b73aa1580a12c5f5cd779aba) by Timothรฉe Mazzucotelli). +- Stop accepting sets as return value of `get_anchors` (only tuples), to preserve order ([2e10374](https://github.com/mkdocstrings/mkdocstrings/commit/2e10374be258e9713b26f73dd06d0c2520ec07a5) by Timothรฉe Mazzucotelli). +- Remove deprecated parts ([0a90a47](https://github.com/mkdocstrings/mkdocstrings/commit/0a90a474c8dcbd95821700d7dab63f03e392c40f) by Timothรฉe Mazzucotelli). +- Use proper parameters in `Inventory.register` method ([433c6e0](https://github.com/mkdocstrings/mkdocstrings/commit/433c6e01aab9333589f755e483f124db0836f143) by Timothรฉe Mazzucotelli). + ## [0.22.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.22.0) - 2023-05-25 [Compare with 0.21.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.21.2...0.22.0) From b7c23d3626de272b94484901071efd860d0b4637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 3 Sep 2023 15:21:30 +0200 Subject: [PATCH 017/223] docs: Update watch feature mention in README --- README.md | 5 ++--- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 06872728..da11f957 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,8 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo each handler can be configured globally in `mkdocs.yml`, and locally for each "autodoc" instruction. -- [**Watch source code directories:**](https://mkdocstrings.github.io/usage/#watch-directories) - you can tell *mkdocstrings* to add directories to be watched by *MkDocs* when - serving the documentation, for auto-reload. +- ~~**Watch source code directories:**~~ + this feature was removed as it is now [built in MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). - **Reasonable defaults:** you should be able to just drop the plugin in your configuration and enjoy your auto-generated docs. diff --git a/mkdocs.yml b/mkdocs.yml index fdd6898b..a8e17f73 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,6 +110,7 @@ markdown_extensions: case: lower - pymdownx.tasklist: custom_checkbox: true +- pymdownx.tilde - toc: permalink: "ยค" From 4df74ab58bc5ba1be4dadd46fc8e7f7c0345cab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 4 Sep 2023 15:41:10 +0200 Subject: [PATCH 018/223] docs: Advertise shell handler --- README.md | 3 ++- docs/insiders/index.md | 3 ++- docs/usage/handlers.md | 1 + mkdocs.yml | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da11f957..ae6d0f24 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo It means you can use it with any programming language, as long as there is a [**handler**](https://mkdocstrings.github.io/reference/handlers/base/) for it. We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) - for the [Crystal](https://mkdocstrings.github.io/crystal/) and [Python](https://mkdocstrings.github.io/python/) languages. + for the [Crystal](https://mkdocstrings.github.io/crystal/) and [Python](https://mkdocstrings.github.io/python/) languages, + as well as for [shell scripts/libraries](https://mkdocstrings.github.io/shell/). Maybe you'd like to add another one to the list? :wink: - [**Multiple themes support:**](https://mkdocstrings.github.io/theming/) diff --git a/docs/insiders/index.md b/docs/insiders/index.md index bfb2d428..eb8feea0 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -58,9 +58,10 @@ a handful of them, [thanks to our awesome sponsors][sponsors]! --> ```python exec="1" session="insiders" data_source = [ "docs/insiders/goals.yml", - ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), ("griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), ("griffe-typing-deprecated", "https://mkdocstrings.github.io/griffe-typing-deprecated/", "insiders/goals.yml"), + ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), + ("mkdocstrings-shell", "https://mkdocstrings.github.io/shell/", "insiders/goals.yml"), ] ``` diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index d2c25420..bd7e5823 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -7,6 +7,7 @@ A handler is what makes it possible to collect and render documentation for a pa - Crystal - Python - Python (Legacy) +- Shell ## About the Python handlers diff --git a/mkdocs.yml b/mkdocs.yml index a8e17f73..d22db25f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,6 +27,7 @@ nav: - 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/ - Guides: - Recipes: recipes.md - Troubleshooting: troubleshooting.md From ea61f3dda3a0d0b88e410bdb0134fc570d4e6076 Mon Sep 17 00:00:00 2001 From: Nick Renieris Date: Thu, 7 Sep 2023 22:14:24 +0300 Subject: [PATCH 019/223] docs: Fix link to Python CSS customization (#606) --- docs/usage/theming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/theming.md b/docs/usage/theming.md index 73b7e0b3..083d3840 100644 --- a/docs/usage/theming.md +++ b/docs/usage/theming.md @@ -82,7 +82,7 @@ Since each handler provides its own set of templates, with their own CSS classes we cannot list them all here. See the documentation about CSS classes for: - the Crystal handler: https://mkdocstrings.github.io/crystal/styling.html#custom-styles -- the Python handler: https://mkdocstrings.github.io/python/customization/#css-classes +- the Python handler: https://mkdocstrings.github.io/python/usage/customization/#css-classes ### Syntax highlighting From 2e9d352066d4877ac30422183454824378a254a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 14 Sep 2023 13:33:06 +0200 Subject: [PATCH 020/223] chore: Template upgrade --- .copier-answers.yml | 2 +- duties.py | 8 ++------ mkdocs.yml | 3 +++ scripts/insiders.py | 11 +++++++---- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index c720007f..9977da68 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.16.6 +_commit: 0.16.9 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: Timothรฉe Mazzucotelli diff --git a/duties.py b/duties.py index 644b2ffb..93311189 100644 --- a/duties.py +++ b/duties.py @@ -4,21 +4,17 @@ import os import sys +from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING, Any from duty import duty from duty.callables import black, blacken_docs, coverage, lazy, mkdocs, mypy, pytest, ruff, safety -if sys.version_info < (3, 8): - from importlib_metadata import version as pkgversion -else: - from importlib.metadata import version as pkgversion - - if TYPE_CHECKING: from duty.context import Context + PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts")) PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS) PY_SRC = " ".join(PY_SRC_LIST) diff --git a/mkdocs.yml b/mkdocs.yml index d22db25f..c5d191f9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -131,11 +131,14 @@ plugins: - 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 + 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 separate_signature: true show_root_heading: true diff --git a/scripts/insiders.py b/scripts/insiders.py index 6f8d0d84..28ca1c87 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -39,11 +39,13 @@ class Feature: """Class representing an Insiders feature.""" name: str - ref: str + ref: str | None since: date | None project: Project | None - def url(self, rel_base: str = "..") -> str: # noqa: D102 + 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("/")) @@ -56,7 +58,8 @@ def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: 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 "" - print(f"- [{'x' if self.since else ' '}] {project}[{self.name}]({self.url(rel_base)}){new}") + 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 @@ -99,7 +102,7 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d features=[ Feature( name=feature_data["name"], - ref=feature_data["ref"], + ref=feature_data.get("ref"), since=feature_data.get("since") and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007 project=project, From e2123a935edea0abdc1b439e2c2b76e002c76e2b Mon Sep 17 00:00:00 2001 From: Perceval Wajsburt Date: Mon, 18 Sep 2023 11:38:34 +0200 Subject: [PATCH 021/223] fix: Remove duplicated headings for docstrings nested in tabs/admonitions Issue #609: https://github.com/mkdocstrings/mkdocstrings/issues/609 PR #610: https://github.com/mkdocstrings/mkdocstrings/pull/610 --- src/mkdocstrings/extension.py | 15 ++++++++++++--- tests/test_extension.py | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 139030ba..f7634a28 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -231,17 +231,26 @@ def _process_block( class _PostProcessor(Treeprocessor): def run(self, root: Element) -> None: + self._remove_duplicated_headings(root) + + def _remove_duplicated_headings(self, parent: Element) -> bool: carry_text = "" - for el in reversed(root): # Reversed mainly for the ability to mutate during iteration. + found = False + for el in reversed(parent): # Reversed mainly for the ability to mutate during iteration. if el.tag == "div" and el.get("class") == "mkdocstrings": # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML). carry_text = (el.text or "") + carry_text - root.remove(el) + parent.remove(el) + found = True elif carry_text: el.tail = (el.tail or "") + carry_text carry_text = "" + elif self._remove_duplicated_headings(el): + found = True + break if carry_text: - root.text = (root.text or "") + carry_text + parent.text = (parent.text or "") + carry_text + return found class MkdocstringsExtension(Extension): diff --git a/tests/test_extension.py b/tests/test_extension.py index 8c687629..2d50ef4d 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -150,3 +150,21 @@ def test_use_options_yaml_key(ext_markdown: Markdown) -> None: """Check that using the 'options' YAML key works as expected.""" assert "h1" in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 1") assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") + + +@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"pymdownx.tabbed": {"alternate_style": True}}]}], indirect=["ext_markdown"]) +def test_removing_duplicated_headings(ext_markdown: Markdown) -> None: + """Assert duplicated headings are removed from the output.""" + output = ext_markdown.convert( + dedent( + """ + === "Tab A" + + ::: tests.fixtures.headings + + """, + ), + ) + assert output.count("Foo") == 1 + assert output.count("Bar") == 1 + assert output.count("Baz") == 1 From f4a94f7d8b8eb1ac01d65bb7237f0077e320ddac Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Mon, 18 Sep 2023 22:17:18 +0200 Subject: [PATCH 022/223] fix: Properly fix duplicated headings for nested docstrings PR #613: https://github.com/mkdocstrings/mkdocstrings/pull/613 --- src/mkdocstrings/extension.py | 17 +++++++---------- tests/fixtures/headings_many.py | 10 ++++++++++ tests/test_extension.py | 16 ++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/headings_many.py diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index f7634a28..8e83c62e 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -233,24 +233,21 @@ class _PostProcessor(Treeprocessor): def run(self, root: Element) -> None: self._remove_duplicated_headings(root) - def _remove_duplicated_headings(self, parent: Element) -> bool: + def _remove_duplicated_headings(self, parent: Element) -> None: carry_text = "" - found = False for el in reversed(parent): # Reversed mainly for the ability to mutate during iteration. if el.tag == "div" and el.get("class") == "mkdocstrings": # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML). carry_text = (el.text or "") + carry_text parent.remove(el) - found = True - elif carry_text: - el.tail = (el.tail or "") + carry_text - carry_text = "" - elif self._remove_duplicated_headings(el): - found = True - break + else: + if carry_text: + el.tail = (el.tail or "") + carry_text + carry_text = "" + self._remove_duplicated_headings(el) + if carry_text: parent.text = (parent.text or "") + carry_text - return found class MkdocstringsExtension(Extension): diff --git a/tests/fixtures/headings_many.py b/tests/fixtures/headings_many.py new file mode 100644 index 00000000..fa643a48 --- /dev/null +++ b/tests/fixtures/headings_many.py @@ -0,0 +1,10 @@ +def heading_1(): + """## Heading one""" + + +def heading_2(): + """### Heading two""" + + +def heading_3(): + """#### Heading three""" diff --git a/tests/test_extension.py b/tests/test_extension.py index 2d50ef4d..6011be7f 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -152,19 +152,23 @@ def test_use_options_yaml_key(ext_markdown: Markdown) -> None: assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") -@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"pymdownx.tabbed": {"alternate_style": True}}]}], indirect=["ext_markdown"]) +@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"admonition": {}}]}], indirect=["ext_markdown"]) def test_removing_duplicated_headings(ext_markdown: Markdown) -> None: """Assert duplicated headings are removed from the output.""" output = ext_markdown.convert( dedent( """ - === "Tab A" + ::: tests.fixtures.headings_many.heading_1 - ::: tests.fixtures.headings + !!! note + ::: tests.fixtures.headings_many.heading_2 + + ::: tests.fixtures.headings_many.heading_3 """, ), ) - assert output.count("Foo") == 1 - assert output.count("Bar") == 1 - assert output.count("Baz") == 1 + assert output.count(">Heading one<") == 1 + assert output.count(">Heading two<") == 1 + assert output.count(">Heading three<") == 1 + assert output.count('class="mkdocstrings') == 0 From 370a61d12b33f3fb61f6bddb3939eb8ff6018620 Mon Sep 17 00:00:00 2001 From: Waylan Limberg Date: Thu, 26 Oct 2023 03:06:17 -0400 Subject: [PATCH 023/223] fix: Make `custom_templates` relative to the config file Issue #477: https://github.com/mkdocstrings/mkdocstrings/issues/477 PR #627: https://github.com/mkdocstrings/mkdocstrings/pull/627 --- docs/usage/index.md | 2 +- docs/usage/theming.md | 6 +++--- src/mkdocstrings/plugin.py | 6 ++++-- tests/test_plugin.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index 7599f9f1..1348b9cc 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -108,7 +108,7 @@ The above is equivalent to: - `default_handler`: The handler that is used by default when no handler is specified. - `custom_templates`: The path to a directory containing custom templates. - The path is relative to the current working directory. + The path is relative to the MkDocs configuration file. See [Theming](theming.md). - `handlers`: The handlers' global configuration. - `enable_inventory`: Whether to enable inventory file generation. diff --git a/docs/usage/theming.md b/docs/usage/theming.md index 083d3840..b5d6f7b3 100644 --- a/docs/usage/theming.md +++ b/docs/usage/theming.md @@ -17,9 +17,9 @@ so you can tweak the look and feel with extra CSS rules. ### Templates -To use custom templates and override the theme ones, -specify the relative path to your templates directory -with the `custom_templates` global configuration option: +To use custom templates and override the theme ones, specify the relative path from your +configuration file to your templates directory with the `custom_templates` global +configuration option: ```yaml title="mkdocs.yml" plugins: diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 484d3ead..682310cc 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -23,6 +23,7 @@ from urllib import request from mkdocs.config.config_options import Type as MkType +from mkdocs.config.config_options import Dir, Optional from mkdocs.plugins import BasePlugin from mkdocs.utils import write_file from mkdocs_autorefs.plugin import AutorefsPlugin @@ -78,7 +79,7 @@ class MkdocstringsPlugin(BasePlugin): config_scheme: tuple[tuple[str, MkType]] = ( # type: ignore[assignment] ("handlers", MkType(dict, default={})), ("default_handler", MkType(str, default="python")), - ("custom_templates", MkType(str, default=None)), + ("custom_templates", Optional(Dir(exists=True))), ("enable_inventory", MkType(bool, default=None)), ("enabled", MkType(bool, default=True)), ) @@ -90,7 +91,8 @@ class MkdocstringsPlugin(BasePlugin): - **`handlers`**: Global configuration of handlers. You can set global configuration per handler, applied everywhere, but overridable in each "autodoc" instruction. Example: - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". - - **`custom_templates`**: Custom templates to use when rendering API objects. + - **`custom_templates`**: Location of custom templates to use when rendering API objects. Value should be the path of + a directory relative to the MkDocs configuration file. - **`enable_inventory`**: Whether to enable object inventory creation. - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. diff --git a/tests/test_plugin.py b/tests/test_plugin.py index b8e8d2a5..26c4031a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -6,6 +6,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config +from mkdocstrings.plugin import MkdocstringsPlugin if TYPE_CHECKING: from pathlib import Path @@ -31,3 +32,38 @@ def test_disabling_plugin(tmp_path: Path) -> None: # make sure the instruction was not processed assert "::: mkdocstrings" in site_dir.joinpath("index.html").read_text() + + +def test_plugin_default_config(tmp_path: Path) -> None: + """Test default config options are set for Plugin.""" + config_file_path = tmp_path / "mkdocs.yml" + plugin = MkdocstringsPlugin() + errors, warnings = plugin.load_config({}, config_file_path=str(config_file_path)) + assert errors == [] + assert warnings == [] + assert plugin.config == { + "handlers": {}, + "default_handler": "python", + "custom_templates": None, + "enable_inventory": None, + "enabled": True, + } + +def test_plugin_config_custom_templates(tmp_path: Path) -> None: + """Test custom_templates option is relative to config file.""" + config_file_path = tmp_path / "mkdocs.yml" + options = {"custom_templates": "docs/templates"} + template_dir = tmp_path / options["custom_templates"] + # Path must exist or config validation will fail. + template_dir.mkdir(parents=True) + plugin = MkdocstringsPlugin() + errors, warnings = plugin.load_config(options, config_file_path=str(config_file_path)) + assert errors == [] + assert warnings == [] + assert plugin.config == { + "handlers": {}, + "default_handler": "python", + "custom_templates": str(template_dir), + "enable_inventory": None, + "enabled": True, + } From b61d4d15258c66b14266aa04b456f191f101b2c6 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Fri, 27 Oct 2023 00:36:02 +0200 Subject: [PATCH 024/223] refactor: Drop support for MkDocs < 1.4, modernize usages PR #629: https://github.com/mkdocstrings/mkdocstrings/pull/629 --- pyproject.toml | 2 +- src/mkdocstrings/extension.py | 3 +- src/mkdocstrings/plugin.py | 96 ++++++++++++++++++----------------- tests/conftest.py | 6 +-- 4 files changed, 54 insertions(+), 53 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78af6bff..7077ccf8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "Jinja2>=2.11.1", "Markdown>=3.3", "MarkupSafe>=1.1", - "mkdocs>=1.2", + "mkdocs>=1.4", "mkdocs-autorefs>=0.3.1", "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 8e83c62e..a819f14b 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -72,8 +72,7 @@ def __init__( Arguments: parser: A `markdown.blockparser.BlockParser` instance. md: A `markdown.Markdown` instance. - config: The [configuration][mkdocstrings.plugin.MkdocstringsPlugin.config_scheme] - of the `mkdocstrings` plugin. + config: The [configuration][mkdocstrings.plugin.PluginConfig] of the `mkdocstrings` plugin. handlers: The handlers container. autorefs: The autorefs plugin instance. """ diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 682310cc..2415cf45 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -22,8 +22,8 @@ from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Iterable, List, Mapping, Tuple, TypeVar from urllib import request -from mkdocs.config.config_options import Type as MkType -from mkdocs.config.config_options import Dir, Optional +from mkdocs.config import Config +from mkdocs.config import config_options as opt from mkdocs.plugins import BasePlugin from mkdocs.utils import write_file from mkdocs_autorefs.plugin import AutorefsPlugin @@ -34,7 +34,6 @@ if TYPE_CHECKING: from jinja2.environment import Environment - from mkdocs.config import Config from mkdocs.config.defaults import MkDocsConfig if sys.version_info < (3, 10): @@ -63,38 +62,15 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: return wrapper -class MkdocstringsPlugin(BasePlugin): - """An `mkdocs` plugin. - - This plugin defines the following event hooks: - - - `on_config` - - `on_env` - - `on_post_build` +class PluginConfig(Config): + """The configuration options of `mkdocstrings`, written in `mkdocs.yml`.""" - Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs` - for more information about its plugin system. - """ - - config_scheme: tuple[tuple[str, MkType]] = ( # type: ignore[assignment] - ("handlers", MkType(dict, default={})), - ("default_handler", MkType(str, default="python")), - ("custom_templates", Optional(Dir(exists=True))), - ("enable_inventory", MkType(bool, default=None)), - ("enabled", MkType(bool, default=True)), - ) + handlers = opt.Type(dict, default={}) """ - The configuration options of `mkdocstrings`, written in `mkdocs.yml`. + Global configuration of handlers. - Available options are: - - - **`handlers`**: Global configuration of handlers. You can set global configuration per handler, applied everywhere, - but overridable in each "autodoc" instruction. Example: - - **`default_handler`**: The default handler to use. The value is the name of the handler module. Default is "python". - - **`custom_templates`**: Location of custom templates to use when rendering API objects. Value should be the path of - a directory relative to the MkDocs configuration file. - - **`enable_inventory`**: Whether to enable object inventory creation. - - **`enabled`**: Whether to enable the plugin. Default is true. If false, *mkdocstrings* will not collect or render anything. + You can set global configuration per handler, applied everywhere, + but overridable in each "autodoc" instruction. Example: ```yaml plugins: @@ -110,6 +86,32 @@ class MkdocstringsPlugin(BasePlugin): ``` """ + default_handler = opt.Type(str, default="python") + """The default handler to use. The value is the name of the handler module. Default is "python".""" + custom_templates = opt.Optional(opt.Dir(exists=True)), + """Location of custom templates to use when rendering API objects. + + Value should be the path of a directory relative to the MkDocs configuration file. + """ + enable_inventory = opt.Optional(opt.Type(bool)) + """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.""" + + +class MkdocstringsPlugin(BasePlugin[PluginConfig]): + """An `mkdocs` plugin. + + This plugin defines the following event hooks: + + - `on_config` + - `on_env` + - `on_post_build` + + Check the [Developing Plugins](https://www.mkdocs.org/user-guide/plugins/#developing-plugins) page of `mkdocs` + for more information about its plugin system. + """ + css_filename = "assets/_mkdocstrings.css" def __init__(self) -> None: @@ -152,10 +154,10 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: return config log.debug("Adding extension to the list") - theme_name = config["theme"].name or os.path.dirname(config["theme"].dirs[0]) + theme_name = config.theme.name or os.path.dirname(config.theme.dirs[0]) to_import: InventoryImportType = [] - for handler_name, conf in self.config["handlers"].items(): + for handler_name, conf in self.config.handlers.items(): for import_item in conf.pop("import", ()): if isinstance(import_item, str): import_item = {"url": import_item} # noqa: PLW2901 @@ -163,8 +165,8 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: extension_config = { "theme_name": theme_name, - "mdx": config["markdown_extensions"], - "mdx_configs": config["mdx_configs"], + "mdx": config.markdown_extensions, + "mdx_configs": config.mdx_configs, "mkdocstrings": self.config, "mkdocs": config, } @@ -172,21 +174,21 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: try: # If autorefs plugin is explicitly enabled, just use it. - autorefs = config["plugins"]["autorefs"] + autorefs = config.plugins["autorefs"] log.debug(f"Picked up existing autorefs instance {autorefs!r}") except KeyError: # Otherwise, add a limited instance of it that acts only on what's added through `register_anchor`. autorefs = AutorefsPlugin() autorefs.scan_toc = False - config["plugins"]["autorefs"] = autorefs + config.plugins["autorefs"] = autorefs log.debug(f"Added a subdued autorefs instance {autorefs!r}") # Add collector-based fallback in either case. autorefs.get_fallback_anchor = self.handlers.get_anchors mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) - config["markdown_extensions"].append(mkdocstrings_extension) + config.markdown_extensions.append(mkdocstrings_extension) - config["extra_css"].insert(0, self.css_filename) # So that it has lower priority than user files. + config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files. self._inv_futures = {} if to_import: @@ -210,7 +212,7 @@ def inventory_enabled(self) -> bool: Returns: Whether the inventory is enabled. """ - inventory_enabled = self.config["enable_inventory"] + inventory_enabled = self.config.enable_inventory if inventory_enabled is None: inventory_enabled = any(handler.enable_inventory for handler in self.handlers.seen_handlers) return inventory_enabled @@ -222,9 +224,9 @@ def plugin_enabled(self) -> bool: Returns: Whether the plugin is enabled. """ - return self.config["enabled"] + return self.config.enabled - def on_env(self, env: Environment, config: Config, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 """Extra actions that need to happen after all Markdown rendering and before HTML rendering. Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). @@ -236,12 +238,12 @@ def on_env(self, env: Environment, config: Config, *args: Any, **kwargs: Any) -> return if 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)) if self.inventory_enabled: log.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")) if self._inv_futures: log.debug(f"Waiting for {len(self._inv_futures)} inventory download(s)") @@ -256,12 +258,12 @@ def on_env(self, env: Environment, config: Config, *args: Any, **kwargs: Any) -> loader_name = loader.__func__.__qualname__ log.error(f"Couldn't load inventory {import_item} through {loader_name}: {error}") # noqa: TRY400 for page, identifier in results.items(): - config["plugins"]["autorefs"].register_url(page, identifier) + config.plugins["autorefs"].register_url(page, identifier) self._inv_futures = {} def on_post_build( self, - config: Config, # noqa: ARG002 + config: MkDocsConfig, # noqa: ARG002 **kwargs: Any, # noqa: ARG002 ) -> None: """Teardown the handlers. diff --git a/tests/conftest.py b/tests/conftest.py index 2119d1f3..59bac65a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ import pytest from markdown.core import Markdown from mkdocs import config -from mkdocs.config.defaults import get_schema +from mkdocs.config.defaults import MkDocsConfig if TYPE_CHECKING: from pathlib import Path @@ -19,7 +19,7 @@ @pytest.fixture(name="mkdocs_conf") def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[config.Config]: """Yield a MkDocs configuration object.""" - conf = config.Config(schema=get_schema()) # type: ignore[call-arg] + conf = MkDocsConfig() while hasattr(request, "_parent_request") and hasattr(request._parent_request, "_parent_request"): request = request._parent_request @@ -53,6 +53,6 @@ def fixture_plugin(mkdocs_conf: config.Config) -> MkdocstringsPlugin: @pytest.fixture(name="ext_markdown") -def fixture_ext_markdown(mkdocs_conf: config.Config) -> Markdown: +def fixture_ext_markdown(mkdocs_conf: MkDocsConfig) -> Markdown: """Return a Markdown instance with MkdocstringsExtension.""" return Markdown(extensions=mkdocs_conf["markdown_extensions"], extension_configs=mkdocs_conf["mdx_configs"]) From afc4ea4e178d27c755528f22adb7c1a6fce736f2 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sat, 28 Oct 2023 18:12:11 +0200 Subject: [PATCH 025/223] fix: `custom_templates` config was dropped in previous commit (#630) --- src/mkdocstrings/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 2415cf45..3f8ce8cd 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -88,9 +88,9 @@ class PluginConfig(Config): default_handler = opt.Type(str, default="python") """The default handler to use. The value is the name of the handler module. Default is "python".""" - custom_templates = opt.Optional(opt.Dir(exists=True)), + custom_templates = opt.Optional(opt.Dir(exists=True)) """Location of custom templates to use when rendering API objects. - + Value should be the path of a directory relative to the MkDocs configuration file. """ enable_inventory = opt.Optional(opt.Type(bool)) From 39694acbdcbb22bbd84b02c05870a8ac816a416e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Nov 2023 01:22:13 +0100 Subject: [PATCH 026/223] chore: Template upgrade --- .copier-answers.yml | 4 +- .github/ISSUE_TEMPLATE/bug_report.md | 75 +++++++---- .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.md | 23 ++-- CONTRIBUTING.md | 5 +- Makefile | 3 +- README.md | 4 +- config/git-changelog.toml | 8 ++ config/ruff.toml | 3 + config/vscode/launch.json | 36 ++++++ config/vscode/settings.json | 52 ++++++++ config/vscode/tasks.json | 93 ++++++++++++++ docs/credits.md | 2 + docs/css/insiders.css | 5 +- docs/insiders/index.md | 14 +-- duties.py | 146 +++++++++++----------- mkdocs.insiders.yml | 5 - mkdocs.yml | 13 +- pyproject.toml | 49 ++++---- scripts/gen_credits.py | 20 +-- scripts/gen_ref_nav.py | 10 +- scripts/insiders.py | 6 +- src/mkdocstrings/debug.py | 106 ++++++++++++++++ tests/test_plugin.py | 2 + 24 files changed, 514 insertions(+), 175 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 config/git-changelog.toml create mode 100644 config/vscode/launch.json create mode 100644 config/vscode/settings.json create mode 100644 config/vscode/tasks.json delete mode 100644 mkdocs.insiders.yml create mode 100644 src/mkdocstrings/debug.py diff --git a/.copier-answers.yml b/.copier-answers.yml index 9977da68..0c51afe2 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 0.16.9 +_commit: 1.1.3 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: Timothรฉe Mazzucotelli @@ -9,8 +9,10 @@ copyright_holder: Timothรฉe Mazzucotelli copyright_holder_email: pawamoy@pm.me copyright_license: ISC License insiders: true +insiders_repository_name: mkdocstrings project_description: Automatic documentation from sources, for MkDocs. project_name: mkdocstrings +public_release: true python_package_command_line_name: '' python_package_distribution_name: mkdocstrings python_package_import_name: mkdocstrings diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 149c6ce0..6ed84b16 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,36 +1,61 @@ --- name: Bug report -about: Create a report to help us improve -title: '' +about: Create a bug report to help us improve. +title: "bug: " labels: unconfirmed -assignees: '' - +assignees: [pawamoy] --- -**Please open an issue on [Griffe](https://github.com/mkdocstrings/griffe/issues) (new Python handler) -or [pytkdocs](https://github.com/mkdocstrings/pytkdocs/issues) (legacy Python handler) instead -if this is related to Python docstrings parsing or the collection of Python objects!** +### Description of the bug + + +### To Reproduce + + +``` +WRITE MRE / INSTRUCTIONS HERE +``` + +### Full traceback + + +
Full traceback + +```python +PASTE TRACEBACK HERE +``` -**Describe the bug** -A clear and concise description of what the bug is. +
-**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +### Expected behavior + -**Expected behavior** -A clear and concise description of what you expected to happen. +### Environment information + -**Screenshots** -If applicable, add screenshots to help explain your problem. +```bash +python -m mkdocstrings.debug # | xclip -selection clipboard +``` -**Information (please complete the following information):** -- OS: [e.g. iOS] -- Browser: [e.g. chrome, safari] -- `mkdocstrings` version: [e.g. 0.10.2] +PASTE OUTPUT HERE -**Additional context** -Add any other context about the problem here. +### Additional context + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..23000298 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: +- name: I have a question / I need help + url: https://github.com/mkdocstrings/mkdocstrings/discussions/new?category=q-a + about: Ask and answer questions in the Discussions tab. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 4fe86d5e..2df98fbc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,19 @@ --- name: Feature request -about: Suggest an idea for this project -title: '' +about: Suggest an idea for this project. +title: "feature: " labels: feature -assignees: '' - +assignees: pawamoy --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +### Is your feature request related to a problem? Please describe. + -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +### Describe the solution you'd like + -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. +### Describe alternatives you've considered + -**Additional context** -Add any other context or screenshots about the feature request here. +### Additional context + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b86ff4b..ff84c305 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,8 +44,9 @@ on multiple Python versions, you run the task directly with `pdm run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. -If you work in VSCode, -[see examples of tasks and run configurations](https://pawamoy.github.io/copier-pdm/work/#vscode-setup). +If you work in VSCode, we provide +[an action to configure VSCode](https://pawamoy.github.io/copier-pdm/work/#vscode-setup) +for the project. ## Development diff --git a/Makefile b/Makefile index 5696baac..f441a5c5 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,8 @@ BASIC_DUTIES = \ docs \ docs-deploy \ format \ - release + release \ + vscode QUALITY_DUTIES = \ check-quality \ diff --git a/README.md b/README.md index ae6d0f24..15d41d7a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) -[![gitter](https://badges.gitter.im/join%20chat.svg)](https://gitter.im/mkdocstrings/community) +[![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) Automatic documentation from sources, for [MkDocs](https://mkdocs.org/). Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdocstrings/community). @@ -77,6 +77,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo ## Installation With `pip`: + ```bash pip install mkdocstrings ``` @@ -90,6 +91,7 @@ pip install 'mkdocstrings[crystal,python]' See the [available language handlers](https://mkdocstrings.github.io/handlers/overview/). With `conda`: + ```bash conda install -c conda-forge mkdocstrings ``` diff --git a/config/git-changelog.toml b/config/git-changelog.toml new file mode 100644 index 00000000..44e2b1fb --- /dev/null +++ b/config/git-changelog.toml @@ -0,0 +1,8 @@ +bump = "auto" +convention = "angular" +in-place = true +output = "CHANGELOG.md" +parse-refs = false +parse-trailers = true +sections = ["build", "deps", "feat", "fix", "refactor"] +template = "keepachangelog" diff --git a/config/ruff.toml b/config/ruff.toml index c6e4a55c..ad45b6c9 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -77,6 +77,9 @@ ignore = [ "src/*/cli.py" = [ "T201", # Print statement ] +"src/*/debug.py" = [ + "T201", # Print statement +] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement diff --git a/config/vscode/launch.json b/config/vscode/launch.json new file mode 100644 index 00000000..2e0d651e --- /dev/null +++ b/config/vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "python (current file)", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "test", + "type": "python", + "request": "launch", + "module": "pytest", + "justMyCode": false, + "args": [ + "-c=config/pytest.ini", + "-vvv", + "--no-cov", + "--dist=no", + "tests", + "-k=${input:tests_selection}" + ] + } + ], + "inputs": [ + { + "id": "tests_selection", + "type": "promptString", + "description": "Tests selection", + "default": "" + } + ] +} \ No newline at end of file diff --git a/config/vscode/settings.json b/config/vscode/settings.json new file mode 100644 index 00000000..17beee4b --- /dev/null +++ b/config/vscode/settings.json @@ -0,0 +1,52 @@ +{ + "files.watcherExclude": { + "**/__pypackages__/**": true, + "**/.venv*/**": true, + "**/venv*/**": true + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "python.autoComplete.extraPaths": [ + "__pypackages__/3.8/lib", + "__pypackages__/3.9/lib", + "__pypackages__/3.10/lib", + "__pypackages__/3.11/lib", + "__pypackages__/3.12/lib" + ], + "python.analysis.extraPaths": [ + "__pypackages__/3.8/lib", + "__pypackages__/3.9/lib", + "__pypackages__/3.10/lib", + "__pypackages__/3.11/lib", + "__pypackages__/3.12/lib" + ], + "black-formatter.args": [ + "--config=config/black.toml" + ], + "mypy-type-checker.args": [ + "--config-file=config/mypy.ini" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": [ + "--config-file=config/pytest.ini" + ], + "ruff.format.args": [ + "--config=config/ruff.toml" + ], + "ruff.lint.args": [ + "--config=config/ruff.toml" + ], + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", + "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format" + ] +} \ No newline at end of file diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json new file mode 100644 index 00000000..80cd13d2 --- /dev/null +++ b/config/vscode/tasks.json @@ -0,0 +1,93 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "changelog", + "type": "shell", + "command": "pdm run duty changelog" + }, + { + "label": "check", + "type": "shell", + "command": "pdm run duty check" + }, + { + "label": "check-quality", + "type": "shell", + "command": "pdm run duty check-quality" + }, + { + "label": "check-types", + "type": "shell", + "command": "pdm run duty check-types" + }, + { + "label": "check-docs", + "type": "shell", + "command": "pdm run duty check-docs" + }, + { + "label": "check-dependencies", + "type": "shell", + "command": "pdm run duty check-dependencies" + }, + { + "label": "check-api", + "type": "shell", + "command": "pdm run duty check-api" + }, + { + "label": "clean", + "type": "shell", + "command": "pdm run duty clean" + }, + { + "label": "docs", + "type": "shell", + "command": "pdm run duty docs" + }, + { + "label": "docs-deploy", + "type": "shell", + "command": "pdm run duty docs-deploy" + }, + { + "label": "format", + "type": "shell", + "command": "pdm run duty format" + }, + { + "label": "lock", + "type": "shell", + "command": "pdm lock -G:all" + }, + { + "label": "release", + "type": "shell", + "command": "pdm run duty release ${input:version}" + }, + { + "label": "setup", + "type": "shell", + "command": "bash scripts/setup.sh" + }, + { + "label": "test", + "type": "shell", + "command": "pdm run duty test coverage", + "group": "test" + }, + { + "label": "vscode", + "type": "shell", + "command": "pdm run duty vscode" + } + ], + "inputs": [ + { + "id": "version", + "type": "promptString", + "description": "Version" + } + ] +} \ No newline at end of file diff --git a/docs/credits.md b/docs/credits.md index 9db45873..f758db87 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -3,6 +3,8 @@ hide: - toc --- + ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` + diff --git a/docs/css/insiders.css b/docs/css/insiders.css index b5547bd1..e7b9c74f 100644 --- a/docs/css/insiders.css +++ b/docs/css/insiders.css @@ -53,11 +53,10 @@ a.insiders { } .sponsorship-item { - float: left; border-radius: 100%; - display: block; + display: inline-block; height: 1.6rem; - margin: .2rem; + margin: 0.1rem; overflow: hidden; width: 1.6rem; } diff --git a/docs/insiders/index.md b/docs/insiders/index.md index eb8feea0..99761b96 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -65,18 +65,21 @@ data_source = [ ] ``` + ```python exec="1" session="insiders" --8<-- "scripts/insiders.py" ``` -```python exec="1" session="insiders" -print(f"""The moment you become a sponsor, you'll get **immediate -access to {len(unreleased_features)} additional features** that you can start using right away, and -which are currently exclusively available to sponsors:\n""") +print( + f"""The moment you become a sponsor, you'll get **immediate + access to {len(unreleased_features)} additional features** that you can start using right away, and + which are currently exclusively available to sponsors:\n""" +) for feature in unreleased_features: feature.render(badge=True) ``` + ## How to become a sponsor @@ -127,9 +130,6 @@ You can cancel your sponsorship anytime.[^5]
-
-
- If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *mkdocstrings*. diff --git a/duties.py b/duties.py index 93311189..43ae357a 100644 --- a/duties.py +++ b/duties.py @@ -4,12 +4,13 @@ import os import sys +from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Iterator from duty import duty -from duty.callables import black, blacken_docs, coverage, lazy, mkdocs, mypy, pytest, ruff, safety +from duty.callables import black, coverage, lazy, mkdocs, mypy, pytest, ruff, safety if TYPE_CHECKING: from duty.context import Context @@ -31,32 +32,16 @@ def pyprefix(title: str) -> str: # noqa: D103 return title -def merge(d1: Any, d2: Any) -> Any: # noqa: D103 - basic_types = (int, float, str, bool, complex) - if isinstance(d1, dict) and isinstance(d2, dict): - for key, value in d2.items(): - if key in d1: - if isinstance(d1[key], basic_types): - d1[key] = value - else: - d1[key] = merge(d1[key], value) - else: - d1[key] = value - return d1 - if isinstance(d1, list) and isinstance(d2, list): - return d1 + d2 - return d2 - - -def mkdocs_config() -> str: # noqa: D103 - import mergedeep - - # force YAML loader to merge arrays - mergedeep.merge = merge - +@contextmanager +def material_insiders() -> Iterator[bool]: # noqa: D103 if "+insiders" in pkgversion("mkdocs-material"): - return "mkdocs.insiders.yml" - return "mkdocs.yml" + os.environ["MATERIAL_INSIDERS"] = "true" + try: + yield True + finally: + os.environ.pop("MATERIAL_INSIDERS") + else: + yield False @duty @@ -66,23 +51,9 @@ def changelog(ctx: Context) -> None: Parameters: ctx: The context instance (passed automatically). """ - from git_changelog.cli import build_and_render + from git_changelog.cli import main as git_changelog - git_changelog = lazy(build_and_render, name="git_changelog") - ctx.run( - git_changelog( - repository=".", - output="CHANGELOG.md", - convention="angular", - template="keepachangelog", - parse_trailers=True, - parse_refs=False, - sections=["build", "deps", "feat", "fix", "refactor"], - bump_latest=True, - in_place=True, - ), - title="Updating changelog", - ) + ctx.run(git_changelog, args=[[]], title="Updating changelog") @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) @@ -138,12 +109,12 @@ def check_docs(ctx: Context) -> None: """ Path("htmlcov").mkdir(parents=True, exist_ok=True) Path("htmlcov/index.html").touch(exist_ok=True) - config = mkdocs_config() - ctx.run( - mkdocs.build(strict=True, config_file=config, verbose=True), - title=pyprefix("Building documentation"), - command=f"mkdocs build -vsf {config}", - ) + with material_insiders(): + ctx.run( + mkdocs.build(strict=True, verbose=True), + title=pyprefix("Building documentation"), + command="mkdocs build -vs", + ) @duty @@ -208,11 +179,12 @@ def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: host: The host to serve the docs from. port: The port to serve the docs on. """ - ctx.run( - mkdocs.serve(dev_addr=f"{host}:{port}", config_file=mkdocs_config()), - title="Serving documentation", - capture=False, - ) + with material_insiders(): + ctx.run( + mkdocs.serve(dev_addr=f"{host}:{port}"), + title="Serving documentation", + capture=False, + ) @duty @@ -223,22 +195,26 @@ def docs_deploy(ctx: Context) -> None: ctx: The context instance (passed automatically). """ os.environ["DEPLOY"] = "true" - config_file = mkdocs_config() - if config_file == "mkdocs.yml": - ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") - origin = ctx.run("git config --get remote.origin.url", silent=True) - if "pawamoy-insiders/mkdocstrings" in origin: - ctx.run("git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", silent=True, nofail=True) - ctx.run( - mkdocs.gh_deploy(config_file=config_file, 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, - ) + with material_insiders() as insiders: + if not insiders: + ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!") + origin = ctx.run("git config --get remote.origin.url", silent=True) + if "pawamoy-insiders/mkdocstrings" in origin: + ctx.run( + "git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", + silent=True, + nofail=True, + ) + ctx.run( + mkdocs.gh_deploy(remote_name="upstream", force=True), + title="Deploying documentation", + ) + else: + ctx.run( + lambda: False, + title="Not deploying docs from public repository (do that from insiders instead!)", + nofail=True, + ) @duty @@ -253,11 +229,6 @@ def format(ctx: Context) -> None: title="Auto-fixing code", ) ctx.run(black.run(*PY_SRC_LIST, config="config/black.toml"), title="Formatting code") - ctx.run( - blacken_docs.run(*PY_SRC_LIST, "docs", exts=["py", "md"], line_length=120), - title="Formatting docs", - nofail=True, - ) @duty(post=["docs-deploy"]) @@ -310,3 +281,28 @@ def test(ctx: Context, match: str = "") -> None: title=pyprefix("Running tests"), command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) + + +@duty +def vscode(ctx: Context) -> None: + """Configure VSCode. + + This task will overwrite the following files, + so make sure to back them up: + + - `.vscode/launch.json` + - `.vscode/settings.json` + - `.vscode/tasks.json` + + Parameters: + ctx: The context instance (passed automatically). + """ + + def update_config(filename: str) -> None: + source_file = Path("config", "vscode", filename) + target_file = Path(".vscode", filename) + target_file.parent.mkdir(exist_ok=True) + target_file.write_text(source_file.read_text()) + + for filename in ("launch.json", "settings.json", "tasks.json"): + ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml deleted file mode 100644 index a93edcc3..00000000 --- a/mkdocs.insiders.yml +++ /dev/null @@ -1,5 +0,0 @@ -INHERIT: mkdocs.yml - -# waiting for https://github.com/squidfunk/mkdocs-material/issues/5446 -# plugins: -# - typeset diff --git a/mkdocs.yml b/mkdocs.yml index c5d191f9..37a98cf4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -96,12 +96,13 @@ markdown_extensions: - admonition - callouts - footnotes -- pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg - pymdownx.details +- pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.magiclink - pymdownx.snippets: + base_path: [!relative $config_dir] check_paths: true - pymdownx.superfences - pymdownx.tabbed: @@ -122,7 +123,7 @@ plugins: scripts: - scripts/gen_ref_nav.py - literate-nav: - nav_file: SUMMARY.txt + nav_file: SUMMARY.md - coverage - mkdocstrings: handlers: @@ -157,6 +158,10 @@ plugins: handlers/overview.md: usage/handlers.md - minify: minify_html: !ENV [DEPLOY, false] +- group: + enabled: !ENV [MATERIAL_INSIDERS, false] + plugins: + - typeset extra: social: diff --git a/pyproject.toml b/pyproject.toml index 7077ccf8..1e687fdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,44 +72,43 @@ duty = ["duty>=0.10"] ci-quality = ["mkdocstrings[duty,docs,quality,typing,security]"] ci-tests = ["mkdocstrings[duty,docs,tests]"] docs = [ - "black>=23.1", - "markdown-callouts>=0.2", - "markdown-exec>=0.5", + "black>=23.9", + "markdown-callouts>=0.3", + "markdown-exec>=1.7", "mkdocs>=1.5", - "mkdocs-coverage>=0.2", - "mkdocs-gen-files>=0.3", - "mkdocs-git-committers-plugin-2>=1.1", - "mkdocs-literate-nav>=0.4", - "mkdocs-material>=7.3", - "mkdocs-minify-plugin>=0.6.4", - "mkdocs-redirects>=1.2.0", - "mkdocstrings-python>=0.5.1", - "toml>=0.10", + "mkdocs-coverage>=1.0", + "mkdocs-gen-files>=0.5", + "mkdocs-git-committers-plugin-2>=1.2", + "mkdocs-literate-nav>=0.6", + "mkdocs-material>=9.4", + "mkdocs-minify-plugin>=0.7", + "mkdocs-redirects>=1.2", + "mkdocstrings-python>=1.7", + "tomli>=2.0; python_version < '3.11'", ] maintain = [ - "black>=23.1", - "blacken-docs>=1.13", - "git-changelog>=1.0", + "black>=23.9", + "blacken-docs>=1.16", + "git-changelog>=2.3", ] quality = [ - "ruff>=0.0.246", + "ruff>=0.0", ] tests = [ "docutils", "pygments>=2.10", # python 3.6 - "pytest>=6.2", - "pytest-cov>=3.0", - "pytest-randomly>=3.10", - "pytest-xdist>=2.4", + "pytest>=7.4", + "pytest-cov>=4.1", + "pytest-randomly>=3.15", + "pytest-xdist>=3.3", "sphinx", ] typing = [ - "mypy>=0.911", - "types-docutils", - "types-markdown>=3.3", + "mypy>=1.5", + "types-docutils>=0.20,", + "types-markdown>=3.5", "types-pyyaml>=6.0", - "types-toml>=0.10", ] security = [ - "safety>=2", + "safety>=2.3", ] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index bc01c0bd..bf35f0da 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -2,27 +2,31 @@ from __future__ import annotations +import os import re import sys +from importlib.metadata import PackageNotFoundError, metadata from itertools import chain from pathlib import Path from textwrap import dedent from typing import Mapping, cast -import toml from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment -if sys.version_info < (3, 8): - from importlib_metadata import PackageNotFoundError, metadata +# TODO: Remove once support for Python 3.10 is dropped. +if sys.version_info >= (3, 11): + import tomllib else: - from importlib.metadata import PackageNotFoundError, metadata + import tomli as tomllib -project_dir = Path(".") -pyproject = toml.load(project_dir / "pyproject.toml") +project_dir = Path(os.getenv("MKDOCS_CONFIG_DIR", ".")) +with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: + pyproject = tomllib.load(pyproject_file) project = pyproject["project"] pdm = pyproject["tool"]["pdm"] -lock_data = toml.load(project_dir / "pdm.lock") +with project_dir.joinpath("pdm.lock").open("rb") as lock_file: + lock_data = tomllib.load(lock_file) lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] regex = re.compile(r"(?P[\w.-]+)(?P.*)$") @@ -35,7 +39,7 @@ def _get_license(pkg_name: str) -> str: return "?" license_name = cast(dict, data).get("License", "").strip() multiple_lines = bool(license_name.count("\n")) - # TODO: remove author logic once all my packages licenses are fixed + # TODO: Remove author logic once all my packages licenses are fixed. author = "" if multiple_lines or not license_name or license_name == "UNKNOWN": for header, value in cast(dict, data).items(): diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py index 249530b1..9c041ede 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -7,9 +7,11 @@ nav = mkdocs_gen_files.Nav() mod_symbol = '' -for path in sorted(Path("src").rglob("*.py")): - module_path = path.relative_to("src").with_suffix("") - doc_path = path.relative_to("src/mkdocstrings").with_suffix(".md") +src = Path(__file__).parent.parent / "src" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src / "mkdocstrings").with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) @@ -30,5 +32,5 @@ mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) -with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/insiders.py b/scripts/insiders.py index 28ca1c87..8f5e215e 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -4,6 +4,7 @@ import json import logging +import os import posixpath from dataclasses import dataclass from datetime import date, datetime, timedelta @@ -115,8 +116,9 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]: + project_dir = os.getenv("MKDOCS_CONFIG_DIR", ".") try: - data = Path(path).read_text() + 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) @@ -159,7 +161,7 @@ def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = goals[amount] = goal else: goals[amount].features.extend(goal.features) - return goals + return {amount: goals[amount] for amount in sorted(goals)} def feature_list(goals: Iterable[Goal]) -> list[Feature]: diff --git a/src/mkdocstrings/debug.py b/src/mkdocstrings/debug.py new file mode 100644 index 00000000..35ede743 --- /dev/null +++ b/src/mkdocstrings/debug.py @@ -0,0 +1,106 @@ +"""Debugging utilities.""" + +from __future__ import annotations + +import os +import platform +import sys +from dataclasses import dataclass +from importlib import metadata + + +@dataclass +class Variable: + """Dataclass describing an environment variable.""" + + name: str + """Variable name.""" + value: str + """Variable value.""" + + +@dataclass +class Package: + """Dataclass describing a Python package.""" + + name: str + """Package name.""" + version: str + """Package version.""" + + +@dataclass +class Environment: + """Dataclass to store environment information.""" + + interpreter_name: str + """Python interpreter name.""" + interpreter_version: str + """Python interpreter version.""" + platform: str + """Operating System.""" + packages: list[Package] + """Installed packages.""" + variables: list[Variable] + """Environment variables.""" + + +def _interpreter_name_version() -> tuple[str, str]: + if hasattr(sys, "implementation"): + impl = sys.implementation.version + version = f"{impl.major}.{impl.minor}.{impl.micro}" + kind = impl.releaselevel + if kind != "final": + version += kind[0] + str(impl.serial) + return sys.implementation.name, version + return "", "0.0.0" + + +def get_version(dist: str = "mkdocstrings") -> str: + """Get version of the given distribution. + + Parameters: + dist: A distribution name. + + Returns: + A version number. + """ + try: + return metadata.version(dist) + except metadata.PackageNotFoundError: + return "0.0.0" + + +def get_debug_info() -> Environment: + """Get debug/environment information. + + Returns: + Environment information. + """ + py_name, py_version = _interpreter_name_version() + packages = ["mkdocstrings"] + variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MKDOCSTRINGS")]] + return Environment( + interpreter_name=py_name, + interpreter_version=py_version, + platform=platform.platform(), + variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], + packages=[Package(pkg, get_version(pkg)) for pkg in packages], + ) + + +def print_debug_info() -> None: + """Print debug/environment information.""" + info = get_debug_info() + print(f"- __System__: {info.platform}") + print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}") + print("- __Environment variables__:") + for var in info.variables: + print(f" - `{var.name}`: `{var.value}`") + print("- __Installed packages__:") + for pkg in info.packages: + print(f" - `{pkg.name}` v{pkg.version}") + + +if __name__ == "__main__": + print_debug_info() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 26c4031a..3342e2aa 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -6,6 +6,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config + from mkdocstrings.plugin import MkdocstringsPlugin if TYPE_CHECKING: @@ -49,6 +50,7 @@ def test_plugin_default_config(tmp_path: Path) -> None: "enabled": True, } + def test_plugin_config_custom_templates(tmp_path: Path) -> None: """Test custom_templates option is relative to config file.""" config_file_path = tmp_path / "mkdocs.yml" From 4dbb6d6a2579d81d65244c0ab1df7e0ee0827fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Nov 2023 01:24:32 +0100 Subject: [PATCH 027/223] ci: Ruff auto-fix --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 59bac65a..be4802a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,13 @@ import pytest from markdown.core import Markdown -from mkdocs import config from mkdocs.config.defaults import MkDocsConfig if TYPE_CHECKING: from pathlib import Path + from mkdocs import config + from mkdocstrings.plugin import MkdocstringsPlugin From d74fada8721e366fce81d61eda06d86c15b9a8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Nov 2023 01:52:51 +0100 Subject: [PATCH 028/223] tests: Stop passing config file path to MkDocsConfig --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index be4802a2..9bb09368 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,6 @@ def fixture_mkdocs_conf(request: pytest.FixtureRequest, tmp_path: Path) -> Itera request = request._parent_request conf_dict = { - "config_file_path": "mkdocs_tests.yml", "site_name": "foo", "site_url": "https://example.org/", "site_dir": str(tmp_path), From b3edf89572db5693688bccbd9642822ef4673095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Nov 2023 01:55:05 +0100 Subject: [PATCH 029/223] ci: Some typing fixes/ignore --- src/mkdocstrings/handlers/base.py | 19 ++++++++++--------- src/mkdocstrings/handlers/rendering.py | 3 ++- src/mkdocstrings/plugin.py | 7 ++++--- tests/test_extension.py | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 700a0565..f52e17dc 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -8,11 +8,12 @@ import importlib import sys from pathlib import Path -from typing import Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence +from typing import Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence, cast from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader from markdown import Markdown +from markdown.extensions.toc import TocTreeprocessor from markupsafe import Markup from mkdocstrings.handlers.rendering import ( @@ -268,15 +269,15 @@ def do_convert_markdown( An HTML string. """ treeprocessors = self._md.treeprocessors - treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = heading_level - treeprocessors[IdPrependingTreeprocessor.name].id_prefix = html_id and html_id + "--" - treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph + 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] try: return Markup(self._md.convert(text)) finally: - treeprocessors[HeadingShiftingTreeprocessor.name].shift_by = 0 - treeprocessors[IdPrependingTreeprocessor.name].id_prefix = "" - treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False + 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] self._md.reset() def do_heading( @@ -319,7 +320,7 @@ def do_heading( el = Element(f"h{heading_level}", attributes) el.append(Element("mkdocstrings-placeholder")) # Tell the 'toc' extension to make its additions if configured so. - toc = 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: @@ -388,7 +389,7 @@ def __init__(self, config: dict) -> None: self._handlers: dict[str, BaseHandler] = {} self.inventory: Inventory = Inventory(project=self._config["mkdocs"]["site_name"]) - def get_anchors(self, identifier: str) -> tuple[str, ...] | set[str]: + 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: diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index 6009935a..2cba2538 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -211,12 +211,13 @@ def __init__(self, md: Markdown, headings: list[Element]): self.headings = headings def run(self, root: Element) -> None: + permalink_class = self.md.treeprocessors["toc"].permalink_class # type: ignore[attr-defined] for el in root.iter(): if self.regex.fullmatch(el.tag): el = copy.copy(el) # noqa: PLW2901 # 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML. # Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension. - if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: + if len(el) > 0 and el[-1].get("class") == permalink_class: del el[-1] self.headings.append(el) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 3f8ce8cd..dd720330 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -172,9 +172,10 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: } self._handlers = Handlers(extension_config) + autorefs: AutorefsPlugin try: # If autorefs plugin is explicitly enabled, just use it. - autorefs = config.plugins["autorefs"] + autorefs = config.plugins["autorefs"] # type: ignore[assignment] log.debug(f"Picked up existing autorefs instance {autorefs!r}") except KeyError: # Otherwise, add a limited instance of it that acts only on what's added through `register_anchor`. @@ -186,7 +187,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: autorefs.get_fallback_anchor = self.handlers.get_anchors mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) - config.markdown_extensions.append(mkdocstrings_extension) + config.markdown_extensions.append(mkdocstrings_extension) # type: ignore[arg-type] config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files. @@ -258,7 +259,7 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A loader_name = loader.__func__.__qualname__ log.error(f"Couldn't load inventory {import_item} through {loader_name}: {error}") # noqa: TRY400 for page, identifier in results.items(): - config.plugins["autorefs"].register_url(page, identifier) + config.plugins["autorefs"].register_url(page, identifier) # type: ignore[attr-defined] self._inv_futures = {} def on_post_build( diff --git a/tests/test_extension.py b/tests/test_extension.py index 6011be7f..4b470647 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -140,7 +140,7 @@ def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, ex ids = ("id1", "id2", "id3") handler.get_anchors = lambda _: ids # type: ignore[method-assign] ext_markdown.convert("::: tests.fixtures.headings") - autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs + autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] for identifier in ids: assert identifier not in autorefs._url_map assert identifier not in autorefs._abs_url_map From 4a97755e700529b48abb8c15a85cfbae2e17a09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Nov 2023 17:52:37 +0100 Subject: [PATCH 030/223] docs: Make recipe work with MkDocs `-f` option --- docs/recipes.md | 143 +++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 62 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index c33130f0..8ea849fc 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -17,15 +17,15 @@ Let say you have a project called `project`. This project has a lot of source files, or modules, which live in the `src` folder: -``` -๐Ÿ“ repo -โ””โ”€โ•ด๐Ÿ“ src - โ””โ”€โ•ด๐Ÿ“ project - โ”œโ”€โ•ด๐Ÿ“„ lorem - โ”œโ”€โ•ด๐Ÿ“„ ipsum - โ”œโ”€โ•ด๐Ÿ“„ dolor - โ”œโ”€โ•ด๐Ÿ“„ sit - โ””โ”€โ•ด๐Ÿ“„ amet +```tree +repo/ + src/ + project/ + lorem + ipsum + dolor + sit + amet ``` Without an automatic process, you will have to manually @@ -49,10 +49,10 @@ and configure it like so: ```yaml title="mkdocs.yml" plugins: -- search # (1) +- search # (1)! - gen-files: scripts: - - docs/gen_ref_pages.py # (2) + - scripts/gen_ref_pages.py # (2)! - mkdocstrings ``` @@ -60,76 +60,91 @@ plugins: 2. The magic happens here, see below how it works. mkdocs-gen-files is able to run Python scripts at build time. -The Python script that we will execute lives in the docs folder, +The Python script that we will execute lives in a scripts folder, and is named `gen_ref_pages.py`, like "generate code reference pages". -```python title="docs/gen_ref_pages.py" +```tree +repo/ + docs/ + index.md + scripts/ + gen_ref_pages.py + src/ + project/ + mkdocs.yml +``` + +```python title="scripts/gen_ref_pages.py" """Generate the code reference pages.""" from pathlib import Path import mkdocs_gen_files -for path in sorted(Path("src").rglob("*.py")): # (1) - module_path = path.relative_to("src").with_suffix("") # (2) - doc_path = path.relative_to("src").with_suffix(".md") # (3) - full_doc_path = Path("reference", doc_path) # (4) +src = Path(__file__).parent.parent / "src" # (1)! + +for path in sorted(src.rglob("*.py")): # (2)! + module_path = path.relative_to(src).with_suffix("") # (3)! + doc_path = path.relative_to(src).with_suffix(".md") # (4)! + full_doc_path = Path("reference", doc_path) # (5)! parts = list(module_path.parts) - if parts[-1] == "__init__": # (5) + if parts[-1] == "__init__": # (6)! parts = parts[:-1] elif parts[-1] == "__main__": continue - with mkdocs_gen_files.open(full_doc_path, "w") as fd: # (6) - identifier = ".".join(parts) # (7) - print("::: " + identifier, file=fd) # (8) + with mkdocs_gen_files.open(full_doc_path, "w") as fd: # (7)! + identifier = ".".join(parts) # (8)! + print("::: " + identifier, file=fd) # (9)! - mkdocs_gen_files.set_edit_path(full_doc_path, path) # (9) + mkdocs_gen_files.set_edit_path(full_doc_path, path) # (10)! ``` -1. Here we recursively list all `.py` files, but you can adapt the code to list +1. It's important to build a path relative to the script itself, + to make it possible to build the docs with MkDocs' + [`-f` option](https://www.mkdocs.org/user-guide/cli/#mkdocs-build). +2. Here we recursively list all `.py` files, but you can adapt the code to list files with other extensions of course, supporting other languages than Python. -2. The module path will look like `project/lorem`. +3. The module path will look like `project/lorem`. It will be used to build the *mkdocstrings* autodoc identifier. -3. This is the relative path to the Markdown page. -4. This is the absolute path to the Markdown page. Here we put all reference pages - into a `reference` folder. -5. This part is only relevant for Python modules. We skip `__main__` modules and +4. This is the partial path of the Markdown page for the module. +5. This is the full path of the Markdown page within the docs. + Here we put all reference pages into a `reference` folder. +6. This part is only relevant for Python modules. We skip `__main__` modules and remove `__init__` from the module parts as it's implicit during imports. -6. Magic! Add the file to MkDocs pages, without actually writing it in the docs folder. -7. Build the autodoc identifier. Here we document Python modules, so the identifier +7. Magic! Add the file to MkDocs pages, without actually writing it in the docs folder. +8. Build the autodoc identifier. Here we document Python modules, so the identifier is a dot-separated path, like `project.lorem`. -8. Actually write to the magic file. -9. We can even set the `edit_uri` on the pages. +9. Actually write to the magic file. +10. We can even set the `edit_uri` on the pages. > NOTE: > It is important to look out for correct edit page behaviour when using generated pages. > For example, if we have `edit_uri` set to `blob/master/docs/` and the following > file structure: > -> ``` -> ๐Ÿ“ repo -> โ”œโ”€ ๐Ÿ“„ mkdocs.yml -> โ”‚ -> โ”œโ”€ ๐Ÿ“ docs -> โ”‚ โ”œโ”€โ•ด๐Ÿ“„ index.md -> โ”‚ โ””โ”€โ•ด๐Ÿ“„ gen_ref_pages.py -> โ”‚ -> โ””โ”€โ•ด๐Ÿ“ src -> โ””โ”€โ•ด๐Ÿ“ project -> โ”œโ”€โ•ด๐Ÿ“„ lorem.py -> โ”œโ”€โ•ด๐Ÿ“„ ipsum.py -> โ”œโ”€โ•ด๐Ÿ“„ dolor.py -> โ”œโ”€โ•ด๐Ÿ“„ sit.py -> โ””โ”€โ•ด๐Ÿ“„ amet.py +> ```tree +> repo/ +> mkdocs.yml +> docs/ +> index.md +> scripts/ +> gen_ref_pages.py +> src/ +> project/ +> lorem.py +> ipsum.py +> dolor.py +> sit.py +> amet.py > ``` > > Then we will have to change our `set_edit_path` call to: > > ```python -> mkdocs_gen_files.set_edit_path(full_doc_path, Path("../") / path) # (1) +> mkdocs_gen_files.set_edit_path(full_doc_path, Path("../") / path) # (1)! > ``` > > 1. Path can be used to traverse the structure in any way you may need, but @@ -180,7 +195,7 @@ plugins: - search - gen-files: scripts: - - docs/gen_ref_pages.py + - scripts/gen_ref_pages.py - literate-nav: nav_file: SUMMARY.md - mkdocstrings @@ -188,7 +203,7 @@ plugins: Then, the previous script is updated like so: -```python title="docs/gen_ref_pages.py" hl_lines="7 21 29 30" +```python title="scripts/gen_ref_pages.py" hl_lines="7 23 31 32" """Generate the code reference pages and navigation.""" from pathlib import Path @@ -197,9 +212,11 @@ import mkdocs_gen_files nav = mkdocs_gen_files.Nav() -for path in sorted(Path("src").rglob("*.py")): - module_path = path.relative_to("src").with_suffix("") - doc_path = path.relative_to("src").with_suffix(".md") +src = Path(__file__).parent.parent / "src" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) @@ -209,7 +226,7 @@ for path in sorted(Path("src").rglob("*.py")): elif parts[-1] == "__main__": continue - nav[parts] = doc_path.as_posix() # (1) + nav[parts] = doc_path.as_posix() # (1)! with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) @@ -217,8 +234,8 @@ for path in sorted(Path("src").rglob("*.py")): mkdocs_gen_files.set_edit_path(full_doc_path, path) -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: # (2) - nav_file.writelines(nav.build_literate_nav()) # (3) +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: # (2)! + nav_file.writelines(nav.build_literate_nav()) # (3)! ``` 1. Progressively build the navigation object. @@ -232,7 +249,7 @@ and replace it with a single line! nav: # rest of the navigation... # defer to gen-files + literate-nav -- Code Reference: reference/ # (1) +- Code Reference: reference/ # (1)! # rest of the navigation... ``` @@ -259,7 +276,7 @@ Well, this is possible thanks to a third plugin: Update the script like this: -```python title="docs/gen_ref_pages.py" hl_lines="18 19" +```python title="scripts/gen_ref_pages.py" hl_lines="20 21" """Generate the code reference pages and navigation.""" from pathlib import Path @@ -268,9 +285,11 @@ import mkdocs_gen_files nav = mkdocs_gen_files.Nav() -for path in sorted(Path("src").rglob("*.py")): - module_path = path.relative_to("src").with_suffix("") - doc_path = path.relative_to("src").with_suffix(".md") +src = Path(__file__).parent.parent / "src" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) @@ -301,7 +320,7 @@ plugins: - search - gen-files: scripts: - - docs/gen_ref_pages.py + - scripts/gen_ref_pages.py - literate-nav: nav_file: SUMMARY.md - section-index From ce84dd57dc5cd3bf3f4be9623ddaa73e1c1868f0 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sun, 12 Nov 2023 18:01:10 +0100 Subject: [PATCH 031/223] feat: Cache downloaded inventories as local file PR #632: https://github.com/mkdocstrings/mkdocstrings/pull/632 --- pyproject.toml | 2 + src/mkdocstrings/_cache.py | 76 ++++++++++++++++++++++++++++++++++++++ src/mkdocstrings/plugin.py | 15 +++----- 3 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 src/mkdocstrings/_cache.py diff --git a/pyproject.toml b/pyproject.toml index 1e687fdf..1be96db8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,11 +29,13 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + "click>=7.0", "Jinja2>=2.11.1", "Markdown>=3.3", "MarkupSafe>=1.1", "mkdocs>=1.4", "mkdocs-autorefs>=0.3.1", + "platformdirs>=2.2.0", "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", "typing-extensions>=4.1; python_version < '3.10'", diff --git a/src/mkdocstrings/_cache.py b/src/mkdocstrings/_cache.py new file mode 100644 index 00000000..8737f317 --- /dev/null +++ b/src/mkdocstrings/_cache.py @@ -0,0 +1,76 @@ +import datetime +import gzip +import hashlib +import os +import urllib.parse +import urllib.request +from typing import BinaryIO, Callable + +import click +import platformdirs + +from mkdocstrings.loggers import get_logger + +log = get_logger(__name__) + + +def download_url_with_gz(url: str) -> bytes: + req = urllib.request.Request( # noqa: S310 + url, + headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0"}, + ) + with urllib.request.urlopen(req) as resp: # noqa: S310 + content: BinaryIO = resp + if "gzip" in resp.headers.get("content-encoding", ""): + content = gzip.GzipFile(fileobj=resp) # type: ignore[assignment] + return content.read() + + +# This is mostly a copy of https://github.com/mkdocs/mkdocs/blob/master/mkdocs/utils/cache.py +# In the future maybe they can be deduplicated. + + +def download_and_cache_url( + url: str, + download: Callable[[str], bytes], + cache_duration: datetime.timedelta, + comment: bytes = b"# ", +) -> bytes: + """Downloads a file from the URL, stores it under ~/.cache/, and returns its content. + + For tracking the age of the content, a prefix is inserted into the stored file, rather than relying on mtime. + + Args: + url: URL to use. + download: Callback that will accept the URL and actually perform the download. + cache_duration: How long to consider the URL content cached. + comment: The appropriate comment prefix for this file format. + """ + directory = os.path.join(platformdirs.user_cache_dir("mkdocs"), "mkdocstrings_url_cache") + name_hash = hashlib.sha256(url.encode()).hexdigest()[:32] + path = os.path.join(directory, name_hash + os.path.splitext(url)[1]) + + now = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) + prefix = b"%s%s downloaded at timestamp " % (comment, url.encode()) + # Check for cached file and try to return it + if os.path.isfile(path): + try: + with open(path, "rb") as f: + line = f.readline() + if line.startswith(prefix): + line = line[len(prefix) :] + timestamp = int(line) + if datetime.timedelta(seconds=(now - timestamp)) <= cache_duration: + log.debug(f"Using cached '{path}' for '{url}'") + return f.read() + except (OSError, ValueError) as e: + log.debug(f"{type(e).__name__}: {e}") + + # Download and cache the file + log.debug(f"Downloading '{url}' to '{path}'") + content = download(url) + os.makedirs(directory, exist_ok=True) + with click.open_file(path, "wb", atomic=True) as f: + f.write(b"%s%d\n" % (prefix, now)) + f.write(content) + return content diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index dd720330..48a7d1ab 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -14,13 +14,13 @@ from __future__ import annotations +import datetime import functools -import gzip import os import sys from concurrent import futures -from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Iterable, List, Mapping, Tuple, TypeVar -from urllib import request +from io import BytesIO +from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Mapping, Tuple, TypeVar from mkdocs.config import Config from mkdocs.config import config_options as opt @@ -28,6 +28,7 @@ from mkdocs.utils import write_file from mkdocs_autorefs.plugin import AutorefsPlugin +from mkdocstrings._cache import download_and_cache_url, download_url_with_gz from mkdocstrings.extension import MkdocstringsExtension from mkdocstrings.handlers.base import BaseHandler, Handlers from mkdocstrings.loggers import get_logger @@ -317,11 +318,7 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - A mapping from identifier to absolute URL. """ log.debug(f"Downloading inventory from {url!r}") - req = request.Request(url, headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0"}) - with request.urlopen(req) as resp: # noqa: S310 (URL audit OK: comes from a checked-in config) - content: BinaryIO = resp - if "gzip" in resp.headers.get("content-encoding", ""): - content = gzip.GzipFile(fileobj=resp) # type: ignore[assignment] - result = dict(loader(content, url=url, **kwargs)) + content = download_and_cache_url(url, download_url_with_gz, datetime.timedelta(days=1)) + result = dict(loader(BytesIO(content), url=url, **kwargs)) log.debug(f"Loaded inventory from {url!r}: {len(result)} items") return result From 032e4175799ab15a329a9b05b5df7b74650cea4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 14 Nov 2023 18:46:07 +0100 Subject: [PATCH 032/223] chore: Prepare release 0.24.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc3a0fb0..c24f6724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.24.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.0) - 2023-11-14 + +[Compare with 0.23.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.23.0...0.24.0) + +### Features + +- Cache downloaded inventories as local file ([ce84dd5](https://github.com/mkdocstrings/mkdocstrings/commit/ce84dd57dc5cd3bf3f4be9623ddaa73e1c1868f0) by Oleh Prypin). [PR #632](https://github.com/mkdocstrings/mkdocstrings/pull/632) + +### Bug Fixes + +- Make `custom_templates` relative to the config file ([370a61d](https://github.com/mkdocstrings/mkdocstrings/commit/370a61d12b33f3fb61f6bddb3939eb8ff6018620) by Waylan Limberg). [Issue #477](https://github.com/mkdocstrings/mkdocstrings/issues/477), [PR #627](https://github.com/mkdocstrings/mkdocstrings/pull/627) +- Remove duplicated headings for docstrings nested in tabs/admonitions ([e2123a9](https://github.com/mkdocstrings/mkdocstrings/commit/e2123a935edea0abdc1b439e2c2b76e002c76e2b) by Perceval Wajsburt, [f4a94f7](https://github.com/mkdocstrings/mkdocstrings/commit/f4a94f7d8b8eb1ac01d65bb7237f0077e320ddac) by Oleh Prypin). [Issue #609](https://github.com/mkdocstrings/mkdocstrings/issues/609), [PR #610](https://github.com/mkdocstrings/mkdocstrings/pull/610), [PR #613](https://github.com/mkdocstrings/mkdocstrings/pull/613) + +### Code Refactoring + +- Drop support for MkDocs < 1.4, modernize usages ([b61d4d1](https://github.com/mkdocstrings/mkdocstrings/commit/b61d4d15258c66b14266aa04b456f191f101b2c6) by Oleh Prypin). [PR #629](https://github.com/mkdocstrings/mkdocstrings/pull/629) + ## [0.23.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.23.0) - 2023-08-28 [Compare with 0.22.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.22.0...0.23.0) From 34f16dfdd1415413f9f594519b27306f4199a9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Gonz=C3=A1lez?= Date: Thu, 4 Jan 2024 19:36:48 +0100 Subject: [PATCH 033/223] docs: Be consistent in auto-pages recipe examples --- docs/recipes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.md b/docs/recipes.md index 8ea849fc..575fb527 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -88,7 +88,7 @@ for path in sorted(src.rglob("*.py")): # (2)! doc_path = path.relative_to(src).with_suffix(".md") # (4)! full_doc_path = Path("reference", doc_path) # (5)! - parts = list(module_path.parts) + parts = tuple(module_path.parts) if parts[-1] == "__init__": # (6)! parts = parts[:-1] From e014a8842c568e0f6adf6f0c1ebfe3a9ee06df6e Mon Sep 17 00:00:00 2001 From: MrCurtis <4184070+MrCurtis@users.noreply.github.com> Date: Sat, 27 Jan 2024 16:13:20 +0000 Subject: [PATCH 034/223] docs: Circumvent error with mkdocs link Currently navigating to https://mkdocs.org/ causes a certification error. Changing the link to https://www.mkdocs.org/ circumvents this. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15d41d7a..460a94b0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) -Automatic documentation from sources, for [MkDocs](https://mkdocs.org/). +Automatic documentation from sources, for [MkDocs](https://www.mkdocs.org/). Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdocstrings/community). --- From 628f3af226bbd76b58d51baf0628cbbee4260ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 30 Jan 2024 18:24:46 +0100 Subject: [PATCH 035/223] chore: Template upgrade --- .copier-answers.yml | 2 +- Makefile | 1 + scripts/insiders.py | 2 +- scripts/setup.sh | 7 +++++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 0c51afe2..9d3294b3 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.1.3 +_commit: 1.2.0 _src_path: gh:pawamoy/copier-pdm.git author_email: pawamoy@pm.me author_fullname: Timothรฉe Mazzucotelli diff --git a/Makefile b/Makefile index f441a5c5..969b8e18 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ SHELL := bash DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 +export PDM_MULTIRUN_USE_VENVS ?= $(if $(shell pdm config python.use_venv | grep True),1,0) args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) check_quality_args = files diff --git a/scripts/insiders.py b/scripts/insiders.py index 8f5e215e..85ecbd0e 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -155,7 +155,7 @@ def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = return _load_goals_from_disk(source, funding) goals = {} for src in source: - source_goals = _load_goals(src) + source_goals = _load_goals(src, funding) for amount, goal in source_goals.items(): if amount not in goals: goals[amount] = goal diff --git a/scripts/setup.sh b/scripts/setup.sh index 4b4cb0fb..e4d8187e 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -12,6 +12,13 @@ if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then fi if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then + if [ "${PDM_MULTIRUN_USE_VENVS}" -eq "1" ]; then + for version in ${PDM_MULTIRUN_VERSIONS}; do + if ! pdm venv --path "${version}" &>/dev/null; then + pdm venv create --name "${version}" "${version}" + fi + done + fi pdm multirun -v pdm install --dev else pdm install --dev From b5236b4333ebde9648c84f6e4b0f4c2b10f3ecd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 22 Feb 2024 14:58:49 +0100 Subject: [PATCH 036/223] refactor: Backup anchors with id and no href, for compatibility with autorefs' Markdown anchors PR-#651: https://github.com/mkdocstrings/mkdocstrings/pull/651 Related-to-mkdocs-autorefs#39: https://github.com/mkdocstrings/autorefs/pull/39 Co-authored-by: Oleh Prypin --- src/mkdocstrings/handlers/rendering.py | 32 +++++++++++++----- tests/fixtures/markdown_anchors.py | 16 +++++++++ tests/test_extension.py | 47 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/markdown_anchors.py diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index 2cba2538..bd28289d 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -146,19 +146,35 @@ def __init__(self, md: Markdown, id_prefix: str): self.id_prefix = id_prefix def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) - if not self.id_prefix: - return - for el in root.iter(): - id_attr = el.get("id") - if id_attr: - el.set("id", self.id_prefix + id_attr) + if self.id_prefix: + self._prefix_ids(root) + def _prefix_ids(self, root: Element) -> None: + index = len(root) + for el in reversed(root): # Reversed mainly for the ability to mutate during iteration. + index -= 1 + + self._prefix_ids(el) href_attr = el.get("href") + + if id_attr := el.get("id"): + if el.tag == "a" and not href_attr: + # An anchor with id and no href is used by autorefs: + # leave it untouched and insert a copy with updated id after it. + new_el = copy.deepcopy(el) + new_el.set("id", self.id_prefix + id_attr) + root.insert(index + 1, new_el) + else: + # Anchors with id and href are not used by autorefs: + # update in place. + el.set("id", self.id_prefix + id_attr) + + # Always update hrefs, names and labels-for: + # there will always be a corresponding id. if href_attr and href_attr.startswith("#"): el.set("href", "#" + self.id_prefix + href_attr[1:]) - name_attr = el.get("name") - if name_attr: + if name_attr := el.get("name"): el.set("name", self.id_prefix + name_attr) if el.tag == "label": diff --git a/tests/fixtures/markdown_anchors.py b/tests/fixtures/markdown_anchors.py new file mode 100644 index 00000000..74cea744 --- /dev/null +++ b/tests/fixtures/markdown_anchors.py @@ -0,0 +1,16 @@ +"""Module docstring. + +[](){#anchor} + +Paragraph. + +[](){#heading-anchor-1} +[](){#heading-anchor-2} +[](){#heading-anchor-3} +## Heading + +[](#has-href1) +[](#has-href2){#with-id} + +Pararaph. +""" \ No newline at end of file diff --git a/tests/test_extension.py b/tests/test_extension.py index 4b470647..5f268c07 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -172,3 +172,50 @@ def test_removing_duplicated_headings(ext_markdown: Markdown) -> None: 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: + index = 0 + for item in items: + assert item in string[index:] + index = string.index(item, index) + len(item) + + +@pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"attr_list": {}}]}], indirect=["ext_markdown"]) +def test_backup_of_anchors(ext_markdown: Markdown) -> None: + """Anchors with empty `href` are backed up.""" + output = ext_markdown.convert("::: tests.fixtures.markdown_anchors") + + # Anchors with id and no href have been backed up and updated. + _assert_contains_in_order( + [ + 'id="anchor"', + 'id="tests.fixtures.markdown_anchors--anchor"', + 'id="heading-anchor-1"', + 'id="tests.fixtures.markdown_anchors--heading-anchor-1"', + 'id="heading-anchor-2"', + 'id="tests.fixtures.markdown_anchors--heading-anchor-2"', + 'id="heading-anchor-3"', + 'id="tests.fixtures.markdown_anchors--heading-anchor-3"', + ], + output, + ) + + # Anchors with href and with or without id have been updated but not backed up. + _assert_contains_in_order( + [ + 'id="tests.fixtures.markdown_anchors--with-id"', + ], + output, + ) + assert 'id="with-id"' not in output + + _assert_contains_in_order( + [ + 'href="#tests.fixtures.markdown_anchors--has-href1"', + 'href="#tests.fixtures.markdown_anchors--has-href2"', + ], + output, + ) + assert 'href="#has-href1"' not in output + assert 'href="#has-href2"' not in output From a7a29079aebcd79be84ac38ce275797920e4c75e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 22 Feb 2024 15:05:47 +0100 Subject: [PATCH 037/223] refactor: Support new pymdownx-highlight options --- src/mkdocstrings/handlers/rendering.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index bd28289d..1db3c8f1 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -43,6 +43,7 @@ class Highlighter(Highlight): ( "css_class", "guess_lang", + "default_lang", "pygments_style", "noclasses", "use_pygments", @@ -58,6 +59,8 @@ class Highlighter(Highlight): "line_spans", "anchor_linenums", "line_anchors", + "pygments_lang_class", + "stripnl", ), ) From f5536840902883faabf8d2d9d7f04d8452dc63b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 22 Feb 2024 15:06:01 +0100 Subject: [PATCH 038/223] ci: Type ignore comment waiting for fix in autorefs --- src/mkdocstrings/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 48a7d1ab..b1cd51f9 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -185,7 +185,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: config.plugins["autorefs"] = autorefs log.debug(f"Added a subdued autorefs instance {autorefs!r}") # Add collector-based fallback in either case. - autorefs.get_fallback_anchor = self.handlers.get_anchors + autorefs.get_fallback_anchor = self.handlers.get_anchors # type: ignore[assignment] mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) config.markdown_extensions.append(mkdocstrings_extension) # type: ignore[arg-type] From 21380ae4665659c32d5a568808556f73ec8bb7fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:50:13 +0100 Subject: [PATCH 039/223] docs: Remove mention of directory watching --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 460a94b0..966e9029 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,6 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo each handler can be configured globally in `mkdocs.yml`, and locally for each "autodoc" instruction. -- ~~**Watch source code directories:**~~ - this feature was removed as it is now [built in MkDocs](https://www.mkdocs.org/user-guide/configuration/#watch). - - **Reasonable defaults:** you should be able to just drop the plugin in your configuration and enjoy your auto-generated docs. From cdd946c397cd73902adac48d9dfc6a8dd68bf660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:50:36 +0100 Subject: [PATCH 040/223] docs: Enable lang class and automatic inline highlight --- docs/usage/handlers.md | 2 +- mkdocs.yml | 4 ++++ src/mkdocstrings/extension.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index bd7e5823..10b8aac4 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -83,7 +83,7 @@ Additional options are available: - [x] `show_submodules`: Whether to render submodules of a module when iterating on children. Default: `False`. - [x] `docstring_section_style`: The style to use to render docstring sections such as attributes, - parameters, etc. Available styles: `table` (default), `list` and `spacy`. The SpaCy style + parameters, etc. Available styles: `"table"` (default), `"list"` and `"spacy"`. The SpaCy style is a poor implementation of their [table style](https://spacy.io/api/doc/#init). We are open to improvements through PRs! diff --git a/mkdocs.yml b/mkdocs.yml index 37a98cf4..da9f10dc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,6 +100,10 @@ markdown_extensions: - 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.inlinehilite: + style_plain_text: python - pymdownx.magiclink - pymdownx.snippets: base_path: [!relative $config_dir] diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index a819f14b..7830d441 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -1,7 +1,7 @@ """This module holds the code of the Markdown extension responsible for matching "autodoc" instructions. The extension is composed of a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockparser) -that matches indented blocks starting with a line like '::: identifier'. +that matches indented blocks starting with a line like `::: identifier`. For each of these blocks, it uses a [handler][mkdocstrings.handlers.base.BaseHandler] to collect documentation about the given identifier and render it with Jinja templates. From 080ddada3ebb332177fa48e5f9f262e7520c673b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:50:46 +0100 Subject: [PATCH 041/223] docs: Fix insiders page --- docs/insiders/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 99761b96..44d11547 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -68,7 +68,6 @@ data_source = [ ```python exec="1" session="insiders" --8<-- "scripts/insiders.py" -``` print( f"""The moment you become a sponsor, you'll get **immediate From 2fe2b478895b53d5b9ee532edcdc5e07c5ea9f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:54:33 +0100 Subject: [PATCH 042/223] chore: Template upgrade --- .copier-answers.yml | 7 ++++--- .github/workflows/ci.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- .gitignore | 1 + CODE_OF_CONDUCT.md | 2 +- config/ruff.toml | 6 +++++- config/vscode/launch.json | 4 ++-- docs/insiders/index.md | 6 +++--- duties.py | 4 ++-- pyproject.toml | 2 +- 10 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 9d3294b3..01da3106 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,14 +1,15 @@ # Changes here will be overwritten by Copier -_commit: 1.2.0 +_commit: 1.2.3 _src_path: gh:pawamoy/copier-pdm.git -author_email: pawamoy@pm.me +author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli author_username: pawamoy copyright_date: '2019' copyright_holder: Timothรฉe Mazzucotelli -copyright_holder_email: pawamoy@pm.me +copyright_holder_email: dev@pawamoy.fr copyright_license: ISC License insiders: true +insiders_email: insiders@pawamoy.fr insiders_repository_name: mkdocstrings project_description: Automatic documentation from sources, for MkDocs. project_name: mkdocstrings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4a3f0cb..3c863cf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Set up PDM - uses: pdm-project/setup-pdm@v3 + uses: pdm-project/setup-pdm@v4 with: python-version: "3.8" @@ -97,10 +97,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up PDM - uses: pdm-project/setup-pdm@v3 + uses: pdm-project/setup-pdm@v4 with: python-version: ${{ matrix.python-version }} allow-python-prereleases: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1baebea4..769e7f71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Fetch all tags run: git fetch --depth=1 --tags - name: Setup Python @@ -22,7 +22,7 @@ jobs: if: github.repository_owner == 'pawamoy-insiders' run: python -m build - name: Upload dists artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: github.repository_owner == 'pawamoy-insiders' with: name: mkdocstrings-insiders diff --git a/.gitignore b/.gitignore index 97dc958b..1c5aabc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea/ +.vscode/ __pycache__/ *.py[cod] dist/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index fe3eefbf..255e0eed 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,7 +60,7 @@ representative at an online or offline event. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -pawamoy@pm.me. +dev@pawamoy.fr. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/config/ruff.toml b/config/ruff.toml index ad45b6c9..4cd69ff0 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,5 +1,5 @@ target-version = "py38" -line-length = 132 +line-length = 120 exclude = [ "fixtures", "site", @@ -102,3 +102,7 @@ known-first-party = ["mkdocstrings"] [pydocstyle] convention = "google" + +[format] +docstring-code-format = true +docstring-code-line-length = 80 diff --git a/config/vscode/launch.json b/config/vscode/launch.json index 2e0d651e..d056ccee 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -3,7 +3,7 @@ "configurations": [ { "name": "python (current file)", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", @@ -11,7 +11,7 @@ }, { "name": "test", - "type": "python", + "type": "debugpy", "request": "launch", "module": "pytest", "justMyCode": false, diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 44d11547..6babfb67 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -89,7 +89,7 @@ You can use your individual or organization GitHub account for sponsoring. **Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email -to pawamoy@pm.me with the name of your +to insiders@pawamoy.fr with the name of your organization and the GitHub account of the individual that should be added as a collaborator.[^4] @@ -98,7 +98,7 @@ You can cancel your sponsorship anytime.[^5] [^4]: It's currently not possible to grant access to each member of an organization, as GitHub only allows for adding users. Thus, after - sponsoring, please send an email to pawamoy@pm.me, stating which + sponsoring, please send an email to insiders@pawamoy.fr, stating which account should become a collaborator of the Insiders repository. We're working on a solution which will make access to organizations much simpler. To ensure that access is not tied to a particular individual GitHub account, @@ -189,7 +189,7 @@ yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). -If you have any problems or further questions, please reach out to pawamoy@pm.me. +If you have any problems or further questions, please reach out to insiders@pawamoy.fr. ### Terms diff --git a/duties.py b/duties.py index 43ae357a..ca77beb8 100644 --- a/duties.py +++ b/duties.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Iterator from duty import duty -from duty.callables import black, coverage, lazy, mkdocs, mypy, pytest, ruff, safety +from duty.callables import coverage, lazy, mkdocs, mypy, pytest, ruff, safety if TYPE_CHECKING: from duty.context import Context @@ -228,7 +228,7 @@ def format(ctx: Context) -> None: ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) - ctx.run(black.run(*PY_SRC_LIST, config="config/black.toml"), title="Formatting code") + ctx.run(ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") @duty(post=["docs-deploy"]) diff --git a/pyproject.toml b/pyproject.toml index 1be96db8..6ff482ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "pdm.backend" [project] name = "mkdocstrings" description = "Automatic documentation from sources, for MkDocs." -authors = [{name = "Timothรฉe Mazzucotelli", email = "pawamoy@pm.me"}] +authors = [{name = "Timothรฉe Mazzucotelli", email = "dev@pawamoy.fr"}] license = {text = "ISC"} readme = "README.md" requires-python = ">=3.8" From 89f752ad6f90d6b867b82946bbb884a848b5c28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:54:52 +0100 Subject: [PATCH 043/223] style: Format --- scripts/insiders.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/insiders.py b/scripts/insiders.py index 85ecbd0e..0cee92cb 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -104,8 +104,7 @@ def load_goals(data: str, funding: int = 0, project: Project | None = None) -> d 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 + 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"] From c161d26e5e83c924a125269f4bc43169d3005ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 16:56:17 +0100 Subject: [PATCH 044/223] ci: Remove type ignore comment now that it's fixed in autorefs --- src/mkdocstrings/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index b1cd51f9..48a7d1ab 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -185,7 +185,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: config.plugins["autorefs"] = autorefs log.debug(f"Added a subdued autorefs instance {autorefs!r}") # Add collector-based fallback in either case. - autorefs.get_fallback_anchor = self.handlers.get_anchors # type: ignore[assignment] + autorefs.get_fallback_anchor = self.handlers.get_anchors mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) config.markdown_extensions.append(mkdocstrings_extension) # type: ignore[arg-type] From d716a88f6a3473c687c951379e9d9a975044e86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 27 Feb 2024 17:06:19 +0100 Subject: [PATCH 045/223] chore: Prepare release 0.24.1 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c24f6724..1934f8db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.24.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.1) - 2024-02-27 + +[Compare with 0.24.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.0...0.24.1) + +### Code Refactoring + +- Support new pymdownx-highlight options ([a7a2907](https://github.com/mkdocstrings/mkdocstrings/commit/a7a29079aebcd79be84ac38ce275797920e4c75e) by Timothรฉe Mazzucotelli). +- Backup anchors with id and no href, for compatibility with autorefs' Markdown anchors ([b5236b4](https://github.com/mkdocstrings/mkdocstrings/commit/b5236b4333ebde9648c84f6e4b0f4c2b10f3ecd4) by Timothรฉe Mazzucotelli). [PR-#651](https://github.com/mkdocstrings/mkdocstrings/pull/651), [Related-to-mkdocs-autorefs#39](https://github.com/mkdocstrings/autorefs/pull/39), Co-authored-by: Oleh Prypin + ## [0.24.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.0) - 2023-11-14 [Compare with 0.23.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.23.0...0.24.0) From 02bee24503f39dac91ced718346b5df249123c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 29 Feb 2024 15:04:56 +0100 Subject: [PATCH 046/223] docs: Fix view/edit URIs in recipe when generating reference pages --- docs/recipes.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 575fb527..953f3879 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -81,7 +81,8 @@ from pathlib import Path import mkdocs_gen_files -src = Path(__file__).parent.parent / "src" # (1)! +root = Path(__file__).parent.parent +src = root / "src" # (1)! for path in sorted(src.rglob("*.py")): # (2)! module_path = path.relative_to(src).with_suffix("") # (3)! @@ -99,7 +100,7 @@ for path in sorted(src.rglob("*.py")): # (2)! identifier = ".".join(parts) # (8)! print("::: " + identifier, file=fd) # (9)! - mkdocs_gen_files.set_edit_path(full_doc_path, path) # (10)! + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) # (10)! ``` 1. It's important to build a path relative to the script itself, @@ -212,7 +213,8 @@ import mkdocs_gen_files nav = mkdocs_gen_files.Nav() -src = Path(__file__).parent.parent / "src" +root = Path(__file__).parent.parent +src = root / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") @@ -232,7 +234,7 @@ for path in sorted(src.rglob("*.py")): ident = ".".join(parts) fd.write(f"::: {ident}") - mkdocs_gen_files.set_edit_path(full_doc_path, path) + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: # (2)! nav_file.writelines(nav.build_literate_nav()) # (3)! @@ -285,7 +287,8 @@ import mkdocs_gen_files nav = mkdocs_gen_files.Nav() -src = Path(__file__).parent.parent / "src" +root = Path(__file__).parent.parent +src = root / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") @@ -307,7 +310,7 @@ for path in sorted(src.rglob("*.py")): ident = ".".join(parts) fd.write(f"::: {ident}") - mkdocs_gen_files.set_edit_path(full_doc_path, path) + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) From 2277131ecfbbecd2ab459a29d60b16fefe1e8302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 29 Feb 2024 15:06:22 +0100 Subject: [PATCH 047/223] chore: Template upgrade --- .copier-answers.yml | 2 +- scripts/gen_ref_nav.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 01da3106..9a8c9131 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.2.3 +_commit: 1.2.4 _src_path: gh:pawamoy/copier-pdm.git author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py index 9c041ede..9565765e 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -7,7 +7,8 @@ nav = mkdocs_gen_files.Nav() mod_symbol = '' -src = Path(__file__).parent.parent / "src" +root = Path(__file__).parent.parent +src = root / "src" for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") @@ -30,7 +31,7 @@ ident = ".".join(parts) fd.write(f"::: {ident}") - mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) + mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) From eef46ae16e85271476f7c9a0f2cdf274fa390df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 13 Mar 2024 18:29:14 +0100 Subject: [PATCH 048/223] chore: Template upgrade --- .copier-answers.yml | 2 +- docs/insiders/index.md | 38 ++++++++++++++++++++++++++------------ docs/js/insiders.js | 39 +++++++++++++++++++++++---------------- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 9a8c9131..708cdd5e 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.2.4 +_commit: 1.2.6 _src_path: gh:pawamoy/copier-pdm.git author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 6babfb67..9e50dcbf 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -66,17 +66,31 @@ data_source = [ ``` -```python exec="1" session="insiders" +```python exec="1" session="insiders" idprefix="" --8<-- "scripts/insiders.py" -print( - f"""The moment you become a sponsor, you'll get **immediate - access to {len(unreleased_features)} additional features** that you can start using right away, and - which are currently exclusively available to sponsors:\n""" -) - -for feature in unreleased_features: - feature.render(badge=True) +if unreleased_features: + print( + "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get **immediate " + f"access to {len(unreleased_features)} additional features** that you can start using right away, and " + "which are currently exclusively available to sponsors:\n" + ) + + for feature in unreleased_features: + feature.render(badge=True) + + print( + "\n\nThese are just the features related to this project. " + "[See the complete feature list on the author's main Insiders page](https://pawamoy.github.io/insiders/#whats-in-it-for-me)." + ) +else: + print( + "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get immediate " + "access to all released features that you can start using right away, and " + "which are exclusively available to sponsors. At this moment, there are no " + "Insiders features for this project, but checkout the [next funding goals](#goals) " + "to see what's coming, as well as **[the feature list for all Insiders projects](https://pawamoy.github.io/insiders/#whats-in-it-for-me).**" + ) ``` @@ -121,10 +135,10 @@ You can cancel your sponsorship anytime.[^5]
- -
+
+
+
-
diff --git a/docs/js/insiders.js b/docs/js/insiders.js index 03bcb404..8bb68485 100644 --- a/docs/js/insiders.js +++ b/docs/js/insiders.js @@ -21,6 +21,26 @@ function getJSON(url, callback) { 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 += ` + + ${sponsor.name} + + ` + }); + 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`; @@ -48,20 +68,7 @@ function updateInsidersPage(author_username) { } }); }); - getJSON(dataURL + '/sponsorsBronze.json', function (err, sponsors) { - const bronzeSponsors = document.getElementById("bronze-sponsors"); - if (sponsors) { - let html = ''; - html += 'Bronze sponsors

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

' - bronzeSponsors.innerHTML = html; - } - }); + updatePremiumSponsors(dataURL, "gold"); + updatePremiumSponsors(dataURL, "silver"); + updatePremiumSponsors(dataURL, "bronze"); } From 1a6955fb015a29090ecb786f7138b9da04b2a8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 13 Mar 2024 18:32:28 +0100 Subject: [PATCH 049/223] docs: List mkdocstrings-typescript insiders project --- docs/insiders/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 9e50dcbf..e4071c0d 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -62,6 +62,7 @@ data_source = [ ("griffe-typing-deprecated", "https://mkdocstrings.github.io/griffe-typing-deprecated/", "insiders/goals.yml"), ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), ("mkdocstrings-shell", "https://mkdocstrings.github.io/shell/", "insiders/goals.yml"), + ("mkdocstrings-typescript", "https://mkdocstrings.github.io/typescript/", "insiders/goals.yml"), ] ``` From f071d5deed49e6a1cb06e227609b9d50511c11ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 13 Mar 2024 18:33:14 +0100 Subject: [PATCH 050/223] chore: Switch to Copier UV template --- .copier-answers.yml | 4 +- .envrc | 1 + .github/workflows/ci.yml | 41 +++++----- .gitignore | 10 +-- .gitpod.dockerfile | 2 +- CONTRIBUTING.md | 10 +-- Makefile | 57 ++++--------- config/black.toml | 3 - config/coverage.ini | 3 +- config/pytest.ini | 6 -- config/ruff.toml | 67 +++++----------- config/vscode/launch.json | 11 +++ config/vscode/settings.json | 23 +----- config/vscode/tasks.json | 80 +++++++++++-------- devdeps.txt | 28 +++++++ docs/insiders/goals.yml | 14 +++- docs/insiders/index.md | 6 +- duties.py | 10 +-- pyproject.toml | 49 ------------ scripts/gen_credits.py | 150 +++++++++++++++++++++++----------- scripts/insiders.py | 13 ++- scripts/make | 155 ++++++++++++++++++++++++++++++++++++ scripts/setup.sh | 25 ------ 23 files changed, 447 insertions(+), 321 deletions(-) create mode 100644 .envrc delete mode 100644 config/black.toml create mode 100644 devdeps.txt create mode 100755 scripts/make delete mode 100755 scripts/setup.sh diff --git a/.copier-answers.yml b/.copier-answers.yml index 708cdd5e..19189116 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # Changes here will be overwritten by Copier -_commit: 1.2.6 -_src_path: gh:pawamoy/copier-pdm.git +_commit: 1.0.8 +_src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli author_username: pawamoy diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..f9d77ee3 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +PATH_add scripts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c863cf0..3e7e1a62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ env: LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 PYTHONPATH: docs + PYTHON_VERSIONS: "" jobs: @@ -29,31 +30,31 @@ jobs: - name: Fetch all tags run: git fetch --depth=1 --tags - - name: Set up PDM - uses: pdm-project/setup-pdm@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.11" - - name: Resolving dependencies - run: pdm lock -v --no-cross-platform -G ci-quality + - name: Install uv + run: pip install uv - name: Install dependencies - run: pdm install -G ci-quality + run: make setup - name: Check if the documentation builds correctly - run: pdm run duty check-docs + run: make check-docs - name: Check the code quality - run: pdm run duty check-quality + run: make check-quality - name: Check if the code is correctly typed - run: pdm run duty check-types + run: make check-types - name: Check for vulnerabilities in dependencies - run: pdm run duty check-dependencies + run: make check-dependencies - name: Check for breaking changes in the API - run: pdm run duty check-api + run: make check-api exclude-test-jobs: runs-on: ubuntu-latest @@ -79,7 +80,6 @@ jobs: needs: exclude-test-jobs strategy: - max-parallel: 4 matrix: os: - ubuntu-latest @@ -99,17 +99,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set up PDM - uses: pdm-project/setup-pdm@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - allow-python-prereleases: true + allow-prereleases: true - - name: Resolving dependencies - run: pdm lock -v --no-cross-platform -G ci-tests + - name: Install uv + run: pip install uv - name: Install dependencies - run: pdm install --no-editable -G ci-tests + run: | + uv venv + uv pip install -r devdeps.txt + uv pip install "mkdocstrings @ ." - name: Run the test suite - run: pdm run duty test + run: make test diff --git a/.gitignore b/.gitignore index 1c5aabc0..246951cc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,13 +9,9 @@ htmlcov/ .coverage* pip-wheel-metadata/ .pytest_cache/ -.python-version -site/ -pdm.lock -pdm.toml -.pdm-plugins/ -.pdm-python -__pypackages__/ .mypy_cache/ +.ruff_cache/ +site/ .venv/ +.venvs/ .cache/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index 0e6d9d35..1590b415 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -2,5 +2,5 @@ FROM gitpod/workspace-full USER gitpod ENV PIP_USER=no RUN pip3 install pipx; \ - pipx install pdm; \ + pipx install uv; \ pipx ensurepath diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff84c305..5f86ff10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,18 +17,18 @@ make setup > NOTE: > If it fails for some reason, > you'll need to install -> [PDM](https://github.com/pdm-project/pdm) +> [uv](https://github.com/astral-sh/uv) > manually. > > You can install it with: > > ```bash > python3 -m pip install --user pipx -> pipx install pdm +> pipx install uv > ``` > > Now you can try running `make setup` again, -> or simply `pdm install`. +> or simply `uv install`. You now have the dependencies installed. @@ -39,13 +39,13 @@ Run `make help` to see all the available actions! This project uses [duty](https://github.com/pawamoy/duty) to run tasks. A Makefile is also provided. The Makefile will try to run certain tasks on multiple Python versions. If for some reason you don't want to run the task -on multiple Python versions, you run the task directly with `pdm run duty TASK`. +on multiple Python versions, you run the task directly with `make run duty TASK`. The Makefile detects if a virtual environment is activated, so `make` will work the same with the virtualenv activated or not. If you work in VSCode, we provide -[an action to configure VSCode](https://pawamoy.github.io/copier-pdm/work/#vscode-setup) +[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. ## Development diff --git a/Makefile b/Makefile index 969b8e18..771b333c 100644 --- a/Makefile +++ b/Makefile @@ -1,54 +1,27 @@ -.DEFAULT_GOAL := help -SHELL := bash -DUTY := $(if $(VIRTUAL_ENV),,pdm run) duty -export PDM_MULTIRUN_VERSIONS ?= 3.8 3.9 3.10 3.11 3.12 -export PDM_MULTIRUN_USE_VENVS ?= $(if $(shell pdm config python.use_venv | grep True),1,0) +# If you have `direnv` loaded in your shell, and allow it in the repository, +# the `make` command will point at the `scripts/make` shell script. +# This Makefile is just here to allow auto-completion in the terminal. -args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)")) -check_quality_args = files -docs_args = host port -release_args = version -test_args = match - -BASIC_DUTIES = \ +actions = \ changelog \ + check \ check-api \ check-dependencies \ + check-docs \ + check-quality \ + check-types \ clean \ coverage \ docs \ docs-deploy \ format \ + help \ release \ + run \ + setup \ + test \ vscode -QUALITY_DUTIES = \ - check-quality \ - check-docs \ - check-types \ - test - -.PHONY: help -help: - @$(DUTY) --list - -.PHONY: lock -lock: - @pdm lock --dev - -.PHONY: setup -setup: - @bash scripts/setup.sh - -.PHONY: check -check: - @pdm multirun duty check-quality check-types check-docs - @$(DUTY) check-dependencies check-api - -.PHONY: $(BASIC_DUTIES) -$(BASIC_DUTIES): - @$(DUTY) $@ $(call args,$@) - -.PHONY: $(QUALITY_DUTIES) -$(QUALITY_DUTIES): - @pdm multirun duty $@ $(call args,$@) +.PHONY: $(actions) +$(actions): + @bash scripts/make "$@" diff --git a/config/black.toml b/config/black.toml deleted file mode 100644 index d24affe5..00000000 --- a/config/black.toml +++ /dev/null @@ -1,3 +0,0 @@ -[tool.black] -line-length = 120 -exclude = "tests/fixtures" diff --git a/config/coverage.ini b/config/coverage.ini index 1bcf0935..18365bd2 100644 --- a/config/coverage.ini +++ b/config/coverage.ini @@ -8,7 +8,8 @@ source = [coverage:paths] equivalent = src/ - __pypackages__/ + .venv/lib/*/site-packages/ + .venvs/*/lib/*/site-packages/ [coverage:report] ignore_errors = True diff --git a/config/pytest.ini b/config/pytest.ini index 6b0d5c7a..5b5bd2e7 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -1,10 +1,4 @@ [pytest] -norecursedirs = - .git - .tox - .env - dist - build python_files = test_*.py *_test.py diff --git a/config/ruff.toml b/config/ruff.toml index 4cd69ff0..71dcc391 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,53 +1,27 @@ target-version = "py38" line-length = 120 + +[lint] exclude = [ - "fixtures", + "tests/fixtures/*.py", "site", ] select = [ - "A", - "ANN", - "ARG", - "B", - "BLE", - "C", - "C4", + "A", "ANN", "ARG", + "B", "BLE", + "C", "C4", "COM", - "D", - "DTZ", - "E", - "ERA", - "EXE", - "F", - "FBT", + "D", "DTZ", + "E", "ERA", "EXE", + "F", "FBT", "G", - "I", - "ICN", - "INP", - "ISC", + "I", "ICN", "INP", "ISC", "N", - "PGH", - "PIE", - "PL", - "PLC", - "PLE", - "PLR", - "PLW", - "PT", - "PYI", + "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", "Q", - "RUF", - "RSE", - "RET", - "S", - "SIM", - "SLF", - "T", - "T10", - "T20", - "TCH", - "TID", - "TRY", + "RUF", "RSE", "RET", + "S", "SIM", "SLF", + "T", "T10", "T20", "TCH", "TID", "TRY", "UP", "W", "YTT", @@ -73,7 +47,7 @@ ignore = [ "TRY003", # Avoid specifying long messages outside the exception class ] -[per-file-ignores] +[lint.per-file-ignores] "src/*/cli.py" = [ "T201", # Print statement ] @@ -91,18 +65,21 @@ ignore = [ "S101", # Use of assert detected ] -[flake8-quotes] +[lint.flake8-quotes] docstring-quotes = "double" -[flake8-tidy-imports] +[lint.flake8-tidy-imports] ban-relative-imports = "all" -[isort] +[lint.isort] known-first-party = ["mkdocstrings"] -[pydocstyle] +[lint.pydocstyle] convention = "google" [format] docstring-code-format = true docstring-code-line-length = 80 +exclude = [ + "tests/fixtures/*.py", +] \ No newline at end of file diff --git a/config/vscode/launch.json b/config/vscode/launch.json index d056ccee..e3288388 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -9,6 +9,17 @@ "console": "integratedTerminal", "justMyCode": false }, + { + "name": "docs", + "type": "debugpy", + "request": "launch", + "module": "mkdocs", + "justMyCode": false, + "args": [ + "serve", + "-v" + ] + }, { "name": "test", "type": "debugpy", diff --git a/config/vscode/settings.json b/config/vscode/settings.json index 17beee4b..949856d1 100644 --- a/config/vscode/settings.json +++ b/config/vscode/settings.json @@ -1,29 +1,9 @@ { "files.watcherExclude": { - "**/__pypackages__/**": true, "**/.venv*/**": true, + "**/.venvs*/**": true, "**/venv*/**": true }, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter" - }, - "python.autoComplete.extraPaths": [ - "__pypackages__/3.8/lib", - "__pypackages__/3.9/lib", - "__pypackages__/3.10/lib", - "__pypackages__/3.11/lib", - "__pypackages__/3.12/lib" - ], - "python.analysis.extraPaths": [ - "__pypackages__/3.8/lib", - "__pypackages__/3.9/lib", - "__pypackages__/3.10/lib", - "__pypackages__/3.11/lib", - "__pypackages__/3.12/lib" - ], - "black-formatter.args": [ - "--config=config/black.toml" - ], "mypy-type-checker.args": [ "--config-file=config/mypy.ini" ], @@ -32,6 +12,7 @@ "python.testing.pytestArgs": [ "--config-file=config/pytest.ini" ], + "ruff.enable": true, "ruff.format.args": [ "--config=config/ruff.toml" ], diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json index 80cd13d2..30008cf2 100644 --- a/config/vscode/tasks.json +++ b/config/vscode/tasks.json @@ -3,84 +3,94 @@ "tasks": [ { "label": "changelog", - "type": "shell", - "command": "pdm run duty changelog" + "type": "process", + "command": "scripts/make", + "args": ["changelog"] }, { "label": "check", - "type": "shell", - "command": "pdm run duty check" + "type": "process", + "command": "scripts/make", + "args": ["check"] }, { "label": "check-quality", - "type": "shell", - "command": "pdm run duty check-quality" + "type": "process", + "command": "scripts/make", + "args": ["check-quality"] }, { "label": "check-types", - "type": "shell", - "command": "pdm run duty check-types" + "type": "process", + "command": "scripts/make", + "args": ["check-types"] }, { "label": "check-docs", - "type": "shell", - "command": "pdm run duty check-docs" + "type": "process", + "command": "scripts/make", + "args": ["check-docs"] }, { "label": "check-dependencies", - "type": "shell", - "command": "pdm run duty check-dependencies" + "type": "process", + "command": "scripts/make", + "args": ["check-dependencies"] }, { "label": "check-api", - "type": "shell", - "command": "pdm run duty check-api" + "type": "process", + "command": "scripts/make", + "args": ["check-api"] }, { "label": "clean", - "type": "shell", - "command": "pdm run duty clean" + "type": "process", + "command": "scripts/make", + "args": ["clean"] }, { "label": "docs", - "type": "shell", - "command": "pdm run duty docs" + "type": "process", + "command": "scripts/make", + "args": ["docs"] }, { "label": "docs-deploy", - "type": "shell", - "command": "pdm run duty docs-deploy" + "type": "process", + "command": "scripts/make", + "args": ["docs-deploy"] }, { "label": "format", - "type": "shell", - "command": "pdm run duty format" - }, - { - "label": "lock", - "type": "shell", - "command": "pdm lock -G:all" + "type": "process", + "command": "scripts/make", + "args": ["format"] }, { "label": "release", - "type": "shell", - "command": "pdm run duty release ${input:version}" + "type": "process", + "command": "scripts/make", + "args": ["release", "${input:version}"] }, { "label": "setup", - "type": "shell", - "command": "bash scripts/setup.sh" + "type": "process", + "command": "scripts/make", + "args": ["setup"] }, { "label": "test", - "type": "shell", - "command": "pdm run duty test coverage", + "type": "process", + "command": "scripts/make", + "args": ["test", "coverage"], "group": "test" }, { "label": "vscode", - "type": "shell", - "command": "pdm run duty vscode" + "type": "process", + "command": "scripts/make", + "args": ["vscode"] } ], "inputs": [ diff --git a/devdeps.txt b/devdeps.txt new file mode 100644 index 00000000..58700f78 --- /dev/null +++ b/devdeps.txt @@ -0,0 +1,28 @@ +build>=1.0 +duty>=0.10 +black>=23.9 +markdown-callouts>=0.3 +markdown-exec>=1.7 +mkdocs>=1.5 +mkdocs-coverage>=1.0 +mkdocs-gen-files>=0.5 +mkdocs-git-committers-plugin-2>=1.2 +mkdocs-literate-nav>=0.6 +mkdocs-material>=9.4 +mkdocs-minify-plugin>=0.7 +mkdocs-redirects>=1.2 +mkdocstrings[python]>=0.23 +tomli>=2.0; python_version < '3.11' +black>=23.9 +blacken-docs>=1.16 +git-changelog>=2.3 +ruff>=0.0 +pytest>=7.4 +pytest-cov>=4.1 +pytest-randomly>=3.15 +pytest-xdist>=3.3 +mypy>=1.5 +types-markdown>=3.5 +types-pyyaml>=6.0 +safety>=2.3 +twine>=5.0 diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml index a96ac51b..0e27b997 100644 --- a/docs/insiders/goals.yml +++ b/docs/insiders/goals.yml @@ -1 +1,13 @@ -goals: {} \ No newline at end of file +goals: + 500: + name: PlasmaVac User Guide + features: [] + 1000: + name: GraviFridge Fluid Renewal + features: [] + 1500: + name: HyperLamp Navigation Tips + features: [] + 2000: + name: FusionDrive Ejection Configuration + features: [] diff --git a/docs/insiders/index.md b/docs/insiders/index.md index e4071c0d..1e091d59 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -169,17 +169,17 @@ for goal in goals.values(): goal.render() ``` - +``` ## Frequently asked questions diff --git a/duties.py b/duties.py index ca77beb8..a295388c 100644 --- a/duties.py +++ b/duties.py @@ -22,7 +22,7 @@ CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""} WINDOWS = os.name == "nt" PTY = not WINDOWS and not CI -MULTIRUN = os.environ.get("PDM_MULTIRUN", "0") == "1" +MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" def pyprefix(title: str) -> str: # noqa: D103 @@ -88,15 +88,15 @@ def check_dependencies(ctx: Context) -> None: """ # retrieve the list of dependencies requirements = ctx.run( - ["pdm", "export", "-f", "requirements", "--without-hashes"], - title="Exporting dependencies as requirements", + ["uv", "pip", "freeze"], + silent=True, allow_overrides=False, ) ctx.run( safety.check(requirements), title="Checking dependencies", - command="pdm export -f requirements --without-hashes | safety check --stdin", + command="uv pip freeze | safety check --stdin", ) @@ -250,7 +250,7 @@ def release(ctx: Context, version: str) -> None: ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) - ctx.run("pdm build", title="Building dist/wheel", pty=PTY) + ctx.run("pyproject-build", title="Building dist/wheel", pty=PTY) ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) diff --git a/pyproject.toml b/pyproject.toml index 6ff482ef..b35e8dc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,56 +61,7 @@ mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin" [tool.pdm] version = {source = "scm"} -plugins = [ - "pdm-multirun", -] [tool.pdm.build] package-dir = "src" editable-backend = "editables" - -[tool.pdm.dev-dependencies] -duty = ["duty>=0.10"] -ci-quality = ["mkdocstrings[duty,docs,quality,typing,security]"] -ci-tests = ["mkdocstrings[duty,docs,tests]"] -docs = [ - "black>=23.9", - "markdown-callouts>=0.3", - "markdown-exec>=1.7", - "mkdocs>=1.5", - "mkdocs-coverage>=1.0", - "mkdocs-gen-files>=0.5", - "mkdocs-git-committers-plugin-2>=1.2", - "mkdocs-literate-nav>=0.6", - "mkdocs-material>=9.4", - "mkdocs-minify-plugin>=0.7", - "mkdocs-redirects>=1.2", - "mkdocstrings-python>=1.7", - "tomli>=2.0; python_version < '3.11'", -] -maintain = [ - "black>=23.9", - "blacken-docs>=1.16", - "git-changelog>=2.3", -] -quality = [ - "ruff>=0.0", -] -tests = [ - "docutils", - "pygments>=2.10", # python 3.6 - "pytest>=7.4", - "pytest-cov>=4.1", - "pytest-randomly>=3.15", - "pytest-xdist>=3.3", - "sphinx", -] -typing = [ - "mypy>=1.5", - "types-docutils>=0.20,", - "types-markdown>=3.5", - "types-pyyaml>=6.0", -] -security = [ - "safety>=2.3", -] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index bf35f0da..a1115f55 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -3,16 +3,17 @@ from __future__ import annotations import os -import re import sys -from importlib.metadata import PackageNotFoundError, metadata +from collections import defaultdict +from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent -from typing import Mapping, cast +from typing import Dict, Iterable, Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment +from packaging.requirements import Requirement # TODO: Remove once support for Python 3.10 is dropped. if sys.version_info >= (3, 11): @@ -24,71 +25,120 @@ with project_dir.joinpath("pyproject.toml").open("rb") as pyproject_file: pyproject = tomllib.load(pyproject_file) project = pyproject["project"] -pdm = pyproject["tool"]["pdm"] -with project_dir.joinpath("pdm.lock").open("rb") as lock_file: - lock_data = tomllib.load(lock_file) -lock_pkgs = {pkg["name"].lower(): pkg for pkg in lock_data["package"]} project_name = project["name"] -regex = re.compile(r"(?P[\w.-]+)(?P.*)$") +with open("devdeps.txt") as devdeps_file: + devdeps = [line.strip() for line in devdeps_file if not line.startswith("-e")] +PackageMetadata = Dict[str, Union[str, Iterable[str]]] +Metadata = Dict[str, PackageMetadata] -def _get_license(pkg_name: str) -> str: + +def _merge_fields(metadata: dict) -> PackageMetadata: + fields = defaultdict(list) + for header, value in metadata.items(): + fields[header.lower()].append(value.strip()) + return { + field: value if len(value) > 1 or field in ("classifier", "requires-dist") else value[0] + for field, value in fields.items() + } + + +def _norm_name(name: str) -> str: + return name.replace("_", "-").replace(".", "-").lower() + + +def _norm_spec(spec: str) -> set[str]: + clean_spec = spec.split("]", 1)[-1].split(";", 1)[0].replace("(", "").replace(")", "").replace(" ", "").strip() + if clean_spec: + return set(clean_spec.split(",")) + return set() + + +def _requirements(deps: list[str]) -> dict[str, Requirement]: + return {_norm_name((req := Requirement(dep)).name): req for dep in deps} + + +def _extra_marker(req: Requirement) -> str | None: + if not req.marker: + return None try: - data = metadata(pkg_name) - except PackageNotFoundError: - return "?" - license_name = cast(dict, data).get("License", "").strip() - multiple_lines = bool(license_name.count("\n")) - # TODO: Remove author logic once all my packages licenses are fixed. - author = "" - if multiple_lines or not license_name or license_name == "UNKNOWN": - for header, value in cast(dict, data).items(): - if header == "Classifier" and value.startswith("License ::"): - license_name = value.rsplit("::", 1)[1].strip() - elif header == "Author-email": - author = value - if license_name == "Other/Proprietary License" and "pawamoy" in author: - license_name = "ISC" - return license_name or "?" - - -def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str, str]]: + return next(marker[2].value for marker in req.marker._markers if getattr(marker[0], "value", None) == "extra") + except StopIteration: + return 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] + metadata[name]["spec"] = set() + metadata[name]["extras"] = set() + _set_license(metadata[name]) + return metadata + + +def _set_license(metadata: PackageMetadata) -> None: + license_field = metadata.get("license-expression", metadata.get("license", "")) + 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_name = " + ".join(license_names) + metadata["license"] = license_name or "?" + + +def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} - for dep in base_deps: - parsed = regex.match(dep).groupdict() # type: ignore[union-attr] - dep_name = parsed["dist"].lower() - if dep_name not in lock_pkgs: + for dep_name, dep_req in base_deps.items(): + if dep_name not in metadata: continue - deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + 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] + deps[dep_name] = metadata[dep_name] again = True while again: again = False - for pkg_name in lock_pkgs: + for pkg_name in metadata: if pkg_name in deps: - for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []): - parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr] - dep_name = parsed["dist"].lower() - if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]: - deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]} + for pkg_dependency in metadata[pkg_name].get("requires-dist", []): + requirement = Requirement(pkg_dependency) + dep_name = _norm_name(requirement.name) + extra_marker = _extra_marker(requirement) + if ( + dep_name in metadata + and dep_name not in deps + 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] + deps[dep_name] = metadata[dep_name] again = True return deps def _render_credits() -> str: - dev_dependencies = _get_deps(chain(*pdm.get("dev-dependencies", {}).values())) # type: ignore[arg-type] + metadata = _get_metadata() + dev_dependencies = _get_deps(_requirements(devdeps), metadata) prod_dependencies = _get_deps( - chain( # type: ignore[arg-type] - project.get("dependencies", []), - chain(*project.get("optional-dependencies", {}).values()), + _requirements( + chain( # type: ignore[arg-type] + project.get("dependencies", []), + chain(*project.get("optional-dependencies", {}).values()), + ), ), + metadata, ) template_data = { "project_name": project_name, - "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: dep["name"]), - "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: dep["name"]), + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"])), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"])), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( @@ -98,13 +148,14 @@ def _render_credits() -> str: These projects were used to build *{{ project_name }}*. **Thank you!** [`python`](https://www.python.org/) | - [`pdm`](https://pdm.fming.dev/) | - [`copier-pdm`](https://github.com/pawamoy/copier-pdm) + [`uv`](https://github.com/astral-sh/uv) | + [`copier-uv`](https://github.com/pawamoy/copier-uv) {% macro dep_line(dep) -%} - [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} + {% if prod_dependencies -%} ### Runtime dependencies Project | Summary | Version (accepted) | Version (last resolved) | License @@ -113,6 +164,8 @@ def _render_credits() -> str: {{ dep_line(dep) }} {% endfor %} + {% endif -%} + {% if dev_dependencies -%} ### Development dependencies Project | Summary | Version (accepted) | Version (last resolved) | License @@ -121,6 +174,7 @@ def _render_credits() -> str: {{ dep_line(dep) }} {% endfor %} + {% endif -%} {% if more_credits %}**[More credits from the author]({{ more_credits }})**{% endif %} """, ) diff --git a/scripts/insiders.py b/scripts/insiders.py index 0cee92cb..15212486 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -78,9 +78,16 @@ def human_readable_amount(self) -> str: # noqa: D102 def render(self, rel_base: str = "..") -> None: # noqa: D102 print(f"#### $ {self.human_readable_amount} โ€” {self.name}\n") - for feature in self.features: - feature.render(rel_base) - print("") + 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]: diff --git a/scripts/make b/scripts/make new file mode 100755 index 00000000..4190622e --- /dev/null +++ b/scripts/make @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +set -e +export PYTHON_VERSIONS=${PYTHON_VERSIONS-3.8 3.9 3.10 3.11 3.12} + +exe="" +prefix="" + + +# Install runtime and development dependencies, +# as well as current project in editable mode. +uv_install() { + uv pip compile pyproject.toml devdeps.txt | uv pip install -r - + uv pip install -e . +} + + +# Setup the development environment by installing dependencies +# in multiple Python virtual environments with uv: +# one venv per Python version in `.venvs/$py`, +# and an additional default venv in `.venv`. +setup() { + if ! command -v uv &>/dev/null; then + echo "make: setup: uv must be installed, see https://github.com/astral-sh/uv" >&2 + return 1 + fi + + if [ -n "${PYTHON_VERSIONS}" ]; then + for version in ${PYTHON_VERSIONS}; do + if [ ! -d ".venvs/${version}" ]; then + uv venv --seed --python "${version}" ".venvs/${version}" + fi + VIRTUAL_ENV="${PWD}/.venvs/${version}" uv_install + done + fi + + if [ ! -d .venv ]; then uv venv --seed --python python; fi + uv_install +} + + +# Activate a Python virtual environments. +# The annoying operating system also requires +# that we set some global variables to help it find commands... +activate() { + local path + if [ -f "$1/bin/activate" ]; then + source "$1/bin/activate" + return 0 + fi + if [ -f "$1/Scripts/activate.bat" ]; then + "$1/Scripts/activate.bat" + exe=".exe" + prefix="$1/Scripts/" + return 0 + fi + echo "run: Cannot activate venv $1" >&2 + return 1 +} + + +# Run a command in all configured Python virtual environments. +# We handle the case when the `PYTHON_VERSIONS` environment variable +# is unset or empty, for robustness. +multirun() { + local cmd="$1" + shift + + if [ -n "${PYTHON_VERSIONS}" ]; then + for version in ${PYTHON_VERSIONS}; do + (activate ".venvs/${version}" && MULTIRUN=1 "${prefix}${cmd}${exe}" "$@") + done + else + (activate .venv && "${prefix}${cmd}${exe}" "$@") + fi +} + + +# Run a command in the default Python virtual environment. +# We rely on `multirun`'s handling of empty `PYTHON_VERSIONS`. +singlerun() { + PYTHON_VERSIONS= multirun "$@" +} + + +# Record options following a command name, +# until a non-option argument is met or there are no more arguments. +# Output each option on a new line, so the parent caller can store them in an array. +# Return the number of times the parent caller must shift arguments. +options() { + local shift_count=0 + for arg in "$@"; do + if [[ "${arg}" =~ ^- || "${arg}" =~ ^.+= ]]; then + echo "${arg}" + ((shift_count++)) + else + break + fi + done + return ${shift_count} +} + + +# Main function. +main() { + local cmd + while [ $# -ne 0 ]; do + cmd="$1" + shift + + # Handle `run` early to simplify `case` below. + if [ "${cmd}" = "run" ]; then + singlerun "$@" + exit $? + fi + + # Handle `multirun` early to simplify `case` below. + if [ "${cmd}" = "multirun" ]; then + multirun "$@" + exit $? + fi + + # All commands except `run` and `multirun` can be chained on a single line. + # Some of them accept options in two formats: `-f`, `--flag` and `param=value`. + # Some of them don't, and will print warnings/errors if options were given. + opts=($(options "$@")) || shift $? + + case "${cmd}" in + # The following commands require special handling. + help|"") + singlerun duty --list ;; + setup) + setup ;; + check) + multirun duty check-quality check-types check-docs + singlerun duty check-dependencies check-api + ;; + + # The following commands run in all venvs. + check-quality|\ + check-docs|\ + check-types|\ + test) + multirun duty "${cmd}" "${opts[@]}" ;; + + # The following commands run in the default venv only. + *) + singlerun duty "${cmd}" "${opts[@]}" ;; + esac + done +} + + +# Execute the main function. +main "$@" diff --git a/scripts/setup.sh b/scripts/setup.sh deleted file mode 100755 index e4d8187e..00000000 --- a/scripts/setup.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -set -e - -if ! command -v pdm &>/dev/null; then - if ! command -v pipx &>/dev/null; then - python3 -m pip install --user pipx - fi - pipx install pdm -fi -if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then - pdm install --plugins -fi - -if [ -n "${PDM_MULTIRUN_VERSIONS}" ]; then - if [ "${PDM_MULTIRUN_USE_VENVS}" -eq "1" ]; then - for version in ${PDM_MULTIRUN_VERSIONS}; do - if ! pdm venv --path "${version}" &>/dev/null; then - pdm venv create --name "${version}" "${version}" - fi - done - fi - pdm multirun -v pdm install --dev -else - pdm install --dev -fi From 9e1bf62a06ba72f7ec4d35e2fb2a448b9daebdd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 23 Mar 2024 11:00:34 +0100 Subject: [PATCH 051/223] docs: Fix link to Griffe extension --- docs/insiders/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/insiders/index.md b/docs/insiders/index.md index 1e091d59..d88e8e7c 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -59,7 +59,7 @@ a handful of them, [thanks to our awesome sponsors][sponsors]! --> data_source = [ "docs/insiders/goals.yml", ("griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), - ("griffe-typing-deprecated", "https://mkdocstrings.github.io/griffe-typing-deprecated/", "insiders/goals.yml"), + ("griffe-warnings-deprecated", "https://mkdocstrings.github.io/griffe-warnings-deprecated/", "insiders/goals.yml"), ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), ("mkdocstrings-shell", "https://mkdocstrings.github.io/shell/", "insiders/goals.yml"), ("mkdocstrings-typescript", "https://mkdocstrings.github.io/typescript/", "insiders/goals.yml"), From 56cf7d50f7982b06c70390c77385d76eae6d80ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 23 Mar 2024 11:00:40 +0100 Subject: [PATCH 052/223] docs: List VBA handler --- README.md | 3 ++- mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 966e9029..44158d4a 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,8 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo It means you can use it with any programming language, as long as there is a [**handler**](https://mkdocstrings.github.io/reference/handlers/base/) for it. We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) - for the [Crystal](https://mkdocstrings.github.io/crystal/) and [Python](https://mkdocstrings.github.io/python/) languages, + for the [Crystal](https://mkdocstrings.github.io/crystal/), [Python](https://mkdocstrings.github.io/python/), + and [VBA](https://pypi.org/project/mkdocstrings-vba/) languages, as well as for [shell scripts/libraries](https://mkdocstrings.github.io/shell/). Maybe you'd like to add another one to the list? :wink: diff --git a/mkdocs.yml b/mkdocs.yml index da9f10dc..860ce66e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - Python: https://mkdocstrings.github.io/python/ - Python (Legacy): https://mkdocstrings.github.io/python-legacy/ - Shell: https://mkdocstrings.github.io/shell/ + - VBA: https://pypi.org/project/mkdocstrings-vba - Guides: - Recipes: recipes.md - Troubleshooting: troubleshooting.md From ccbbbf1c74a2d29c1ce290ba4a3649c93a0fb27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 23 Mar 2024 11:06:49 +0100 Subject: [PATCH 053/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/FUNDING.yml | 1 + .github/workflows/ci.yml | 5 +---- .gitignore | 27 +++++++++++++++++---------- config/ruff.toml | 7 +++---- duties.py | 22 +++++++++++----------- scripts/gen_credits.py | 18 ++++++------------ scripts/make | 10 +++++++--- src/mkdocstrings/debug.py | 5 ++++- src/mkdocstrings/loggers.py | 2 +- 10 files changed, 52 insertions(+), 47 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 19189116..0e8a2418 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.0.8 +_commit: 1.1.0 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 01e293ac..a502284a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,5 @@ github: pawamoy ko_fi: pawamoy +polar: pawamoy custom: - https://www.paypal.me/pawamoy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e7e1a62..3248b1a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,10 +109,7 @@ jobs: run: pip install uv - name: Install dependencies - run: | - uv venv - uv pip install -r devdeps.txt - uv pip install "mkdocstrings @ ." + run: make setup - name: Run the test suite run: make test diff --git a/.gitignore b/.gitignore index 246951cc..41fee62d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,24 @@ +# editors .idea/ .vscode/ -__pycache__/ -*.py[cod] -dist/ + +# python *.egg-info/ -build/ -htmlcov/ +*.py[cod] +.venv/ +.venvs/ +/build/ +/dist/ + +# tools .coverage* -pip-wheel-metadata/ +/.pdm-build/ +/htmlcov/ +/site/ + +# cache +.cache/ .pytest_cache/ .mypy_cache/ .ruff_cache/ -site/ -.venv/ -.venvs/ -.cache/ +__pycache__/ diff --git a/config/ruff.toml b/config/ruff.toml index 71dcc391..a8b26e1e 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -4,7 +4,6 @@ line-length = 120 [lint] exclude = [ "tests/fixtures/*.py", - "site", ] select = [ "A", "ANN", "ARG", @@ -78,8 +77,8 @@ known-first-party = ["mkdocstrings"] convention = "google" [format] -docstring-code-format = true -docstring-code-line-length = 80 exclude = [ "tests/fixtures/*.py", -] \ No newline at end of file +] +docstring-code-format = true +docstring-code-line-length = 80 diff --git a/duties.py b/duties.py index a295388c..30bf7c63 100644 --- a/duties.py +++ b/duties.py @@ -157,17 +157,17 @@ def clean(ctx: Context) -> None: Parameters: ctx: The context instance (passed automatically). """ - ctx.run("rm -rf .coverage*") - ctx.run("rm -rf .mypy_cache") - ctx.run("rm -rf .pytest_cache") - ctx.run("rm -rf tests/.pytest_cache") - ctx.run("rm -rf build") - ctx.run("rm -rf dist") - ctx.run("rm -rf htmlcov") - ctx.run("rm -rf pip-wheel-metadata") - ctx.run("rm -rf site") - ctx.run("find . -type d -name __pycache__ | xargs rm -rf") - ctx.run("find . -name '*.rej' -delete") + + def _rm(*targets: str) -> None: + for target in targets: + ctx.run(f"rm -rf {target}") + + def _find_rm(*targets: str) -> None: + for target in targets: + ctx.run(f"find . -type d -name '{target}' | xargs rm -rf") + + _rm("build", "dist", ".coverage*", "htmlcov", "site", ".pdm-build") + _find_rm(".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__") @duty diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index a1115f55..27f94d67 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -26,7 +26,7 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -with open("devdeps.txt") as devdeps_file: +with project_dir.joinpath("devdeps.txt").open() as devdeps_file: devdeps = [line.strip() for line in devdeps_file if not line.startswith("-e")] PackageMetadata = Dict[str, Union[str, Iterable[str]]] @@ -47,13 +47,6 @@ def _norm_name(name: str) -> str: return name.replace("_", "-").replace(".", "-").lower() -def _norm_spec(spec: str) -> set[str]: - clean_spec = spec.split("]", 1)[-1].split(";", 1)[0].replace("(", "").replace(")", "").replace(" ", "").strip() - if clean_spec: - return set(clean_spec.split(",")) - return set() - - def _requirements(deps: list[str]) -> dict[str, Requirement]: return {_norm_name((req := Requirement(dep)).name): req for dep in deps} @@ -74,6 +67,7 @@ def _get_metadata() -> Metadata: metadata[name] = _merge_fields(pkg.metadata) # type: ignore[arg-type] metadata[name]["spec"] = set() metadata[name]["extras"] = set() + metadata[name].setdefault("summary", "") _set_license(metadata[name]) return metadata @@ -147,12 +141,12 @@ def _render_credits() -> str: These projects were used to build *{{ project_name }}*. **Thank you!** - [`python`](https://www.python.org/) | - [`uv`](https://github.com/astral-sh/uv) | - [`copier-uv`](https://github.com/pawamoy/copier-uv) + [Python](https://www.python.org/) | + [uv](https://github.com/astral-sh/uv) | + [copier-uv](https://github.com/pawamoy/copier-uv) {% macro dep_line(dep) -%} - [`{{ dep.name }}`](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} + [{{ dep.name }}](https://pypi.org/project/{{ dep.name }}/) | {{ dep.summary }} | {{ ("`" ~ dep.spec|sort(reverse=True)|join(", ") ~ "`") if dep.spec else "" }} | `{{ dep.version }}` | {{ dep.license }} {%- endmacro %} {% if prod_dependencies -%} diff --git a/scripts/make b/scripts/make index 4190622e..f690126e 100755 --- a/scripts/make +++ b/scripts/make @@ -11,7 +11,11 @@ prefix="" # as well as current project in editable mode. uv_install() { uv pip compile pyproject.toml devdeps.txt | uv pip install -r - - uv pip install -e . + if [ -z "${CI}" ]; then + uv pip install -e . + else + uv pip install "mkdocstrings @ ." + fi } @@ -28,13 +32,13 @@ setup() { if [ -n "${PYTHON_VERSIONS}" ]; then for version in ${PYTHON_VERSIONS}; do if [ ! -d ".venvs/${version}" ]; then - uv venv --seed --python "${version}" ".venvs/${version}" + uv venv --python "${version}" ".venvs/${version}" fi VIRTUAL_ENV="${PWD}/.venvs/${version}" uv_install done fi - if [ ! -d .venv ]; then uv venv --seed --python python; fi + if [ ! -d .venv ]; then uv venv --python python; fi uv_install } diff --git a/src/mkdocstrings/debug.py b/src/mkdocstrings/debug.py index 35ede743..b5da78f2 100644 --- a/src/mkdocstrings/debug.py +++ b/src/mkdocstrings/debug.py @@ -37,6 +37,8 @@ class Environment: """Python interpreter name.""" interpreter_version: str """Python interpreter version.""" + interpreter_path: str + """Path to Python executable.""" platform: str """Operating System.""" packages: list[Package] @@ -83,6 +85,7 @@ def get_debug_info() -> Environment: return Environment( interpreter_name=py_name, interpreter_version=py_version, + interpreter_path=sys.executable, platform=platform.platform(), variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[Package(pkg, get_version(pkg)) for pkg in packages], @@ -93,7 +96,7 @@ def print_debug_info() -> None: """Print debug/environment information.""" info = get_debug_info() print(f"- __System__: {info.platform}") - print(f"- __Python__: {info.interpreter_name} {info.interpreter_version}") + print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") for var in info.variables: print(f" - `{var.name}`: `{var.value}`") diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 63502474..c8dd0432 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -17,7 +17,7 @@ except ImportError: TEMPLATES_DIRS: Sequence[Path] = () else: - TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type] + TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) if TYPE_CHECKING: From c0d009000678a2ccbfb0c8adfeff3dc83845ee41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 2 Apr 2024 20:26:53 +0200 Subject: [PATCH 054/223] fix: Support HTML toc labels with Python-Markdown 3.6+ --- src/mkdocstrings/extension.py | 22 +++++++++++++++++++--- tests/test_extension.py | 24 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 7830d441..b22d0888 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -228,7 +228,7 @@ def _process_block( return rendered, handler, data -class _PostProcessor(Treeprocessor): +class _HeadingsPostProcessor(Treeprocessor): def run(self, root: Element) -> None: self._remove_duplicated_headings(root) @@ -249,6 +249,17 @@ def _remove_duplicated_headings(self, parent: Element) -> None: parent.text = (parent.text or "") + carry_text +class _TocLabelsTreeProcessor(Treeprocessor): + def run(self, root: Element) -> None: # noqa: ARG002 + self._override_toc_labels(self.md.toc_tokens) # type: ignore[attr-defined] + + def _override_toc_labels(self, tokens: list[dict[str, Any]]) -> None: + for token in tokens: + if (label := token.get("data-toc-label")) and token["name"] != label: + token["name"] = label + self._override_toc_labels(token["children"]) + + class MkdocstringsExtension(Extension): """Our Markdown extension. @@ -284,7 +295,12 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me priority=75, # Right before markdown.blockprocessors.HashHeaderProcessor ) md.treeprocessors.register( - _PostProcessor(md), - "mkdocstrings_post", + _HeadingsPostProcessor(md), + "mkdocstrings_post_headings", priority=4, # Right after 'toc'. ) + # md.treeprocessors.register( + # _TocLabelsTreeProcessor(md), + # "mkdocstrings_post_toc_labels", + # priority=4, # Right after 'toc'. + # ) diff --git a/tests/test_extension.py b/tests/test_extension.py index 5f268c07..affd6c6a 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -105,26 +105,46 @@ def test_no_double_toc(ext_markdown: Markdown, expect_permalink: str) -> None: { "level": 1, "id": "aa", + "html": "aa", "name": "aa", + "data-toc-label": "", "children": [ { "level": 2, "id": "tests.fixtures.headings--foo", + "html": "Foo", "name": "Foo", + "data-toc-label": "", "children": [ { "level": 4, "id": "tests.fixtures.headings--bar", + "html": "Bar", "name": "Bar", + "data-toc-label": "", "children": [ - {"level": 6, "id": "tests.fixtures.headings--baz", "name": "Baz", "children": []}, + { + "level": 6, + "id": "tests.fixtures.headings--baz", + "html": "Baz", + "name": "Baz", + "data-toc-label": "", + "children": [], + }, ], }, ], }, ], }, - {"level": 1, "id": "bb", "name": "bb", "children": []}, + { + "level": 1, + "id": "bb", + "html": "bb", + "name": "bb", + "data-toc-label": "", + "children": [], + }, ] From 024ac41024a19cbf45f4d127c75cb709134683db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 2 Apr 2024 20:40:05 +0200 Subject: [PATCH 055/223] ci: Ignore mypy warning --- src/mkdocstrings/loggers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index c8dd0432..63502474 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -17,7 +17,7 @@ except ImportError: TEMPLATES_DIRS: Sequence[Path] = () else: - TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) + TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type] if TYPE_CHECKING: From 17bfc87a8d23de5585b4630fd8c2b4595ac88a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 2 Apr 2024 21:22:12 +0200 Subject: [PATCH 056/223] chore: Use PEP 440 versioning scheme for changelog --- config/git-changelog.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/git-changelog.toml b/config/git-changelog.toml index 44e2b1fb..57114e0c 100644 --- a/config/git-changelog.toml +++ b/config/git-changelog.toml @@ -6,3 +6,4 @@ parse-refs = false parse-trailers = true sections = ["build", "deps", "feat", "fix", "refactor"] template = "keepachangelog" +versioning = "pep440" From 7b9827c97e396bd76f77315d40baa6596ee8e17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 2 Apr 2024 21:22:50 +0200 Subject: [PATCH 057/223] chore: Prepare release 0.24.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1934f8db..63336bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.24.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.2) - 2024-04-02 + +[Compare with 0.24.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.1...0.24.2) + +### Bug Fixes + +- Support HTML toc labels with Python-Markdown 3.6+ ([c0d0090](https://github.com/mkdocstrings/mkdocstrings/commit/c0d009000678a2ccbfb0c8adfeff3dc83845ee41) by Timothรฉe Mazzucotelli). [Issue-mkdocstrings/python-143](https://github.com/mkdocstrings/python/issues/143) + ## [0.24.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.1) - 2024-02-27 [Compare with 0.24.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.0...0.24.1) From 7fe3e5f28239c08094fb656728369998f52630ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 5 Apr 2024 16:54:20 +0200 Subject: [PATCH 058/223] fix: Support HTML toc labels with Python-Markdown 3.6+ (uncomment code...) --- src/mkdocstrings/extension.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index b22d0888..bef8c799 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -299,8 +299,8 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me "mkdocstrings_post_headings", priority=4, # Right after 'toc'. ) - # md.treeprocessors.register( - # _TocLabelsTreeProcessor(md), - # "mkdocstrings_post_toc_labels", - # priority=4, # Right after 'toc'. - # ) + md.treeprocessors.register( + _TocLabelsTreeProcessor(md), + "mkdocstrings_post_toc_labels", + priority=4, # Right after 'toc'. + ) From 828bd5921dba610e0ce33be780ac16af0eb0bef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 5 Apr 2024 16:56:13 +0200 Subject: [PATCH 059/223] chore: Prepare release 0.24.3 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63336bdc..723eb24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.24.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.3) - 2024-04-05 + +[Compare with 0.24.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.2...0.24.3) + +### Bug Fixes + +- Support HTML toc labels with Python-Markdown 3.6+ (uncomment code...) ([7fe3e5f](https://github.com/mkdocstrings/mkdocstrings/commit/7fe3e5f28239c08094fb656728369998f52630ea) by Timothรฉe Mazzucotelli). + ## [0.24.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.2) - 2024-04-02 [Compare with 0.24.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.1...0.24.2) From d799d2f3903bce44fb751f8cf3fb8078d25549da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 17:11:10 +0200 Subject: [PATCH 060/223] feat: Support blank line between `::: path` and YAML options Issue-450: https://github.com/mkdocstrings/mkdocstrings/issues/450 --- src/mkdocstrings/extension.py | 4 ++++ tests/test_extension.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index bef8c799..23e90cff 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -116,6 +116,10 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: block, the_rest = self.detab(block) + if not block and blocks and blocks[0].startswith((" handler:", " options:")): + # YAML options were separated from the `:::` line by a blank line. + block = blocks.pop(0) + if match: identifier = match["name"] heading_level = match["heading"].count("#") diff --git a/tests/test_extension.py b/tests/test_extension.py index affd6c6a..976f376c 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -172,6 +172,11 @@ def test_use_options_yaml_key(ext_markdown: Markdown) -> None: assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n options:\n heading_level: 2") +def test_use_yaml_options_after_blank_line(ext_markdown: Markdown) -> None: + """Check that YAML options are detected even after a blank line.""" + assert "h1" not in ext_markdown.convert("::: tests.fixtures.headings\n\n options:\n heading_level: 2") + + @pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"admonition": {}}]}], indirect=["ext_markdown"]) def test_removing_duplicated_headings(ext_markdown: Markdown) -> None: """Assert duplicated headings are removed from the output.""" From 1532b59a6efd99fed846cf7edfd0b26525700d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 17:12:48 +0200 Subject: [PATCH 061/223] feat: Support `once` parameter in logging methods, allowing to log a message only once with a given logger This will be useful when issuing warning messages in templates, for example when deprecating things, as we don't want to show the message dozens of time (each time the template is used), but rather just once. --- src/mkdocstrings/loggers.py | 51 ++++++++++++++++++++++++++--- tests/test_loggers.py | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 tests/test_loggers.py diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 63502474..9d7408cc 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -17,7 +17,7 @@ except ImportError: TEMPLATES_DIRS: Sequence[Path] = () else: - TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) # type: ignore[arg-type] + TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) if TYPE_CHECKING: @@ -25,7 +25,25 @@ class LoggerAdapter(logging.LoggerAdapter): - """A logger adapter to prefix messages.""" + """A logger adapter to prefix messages. + + This adapter also adds an additional parameter to logging methods + called `once`: if `True`, the message will only be logged once. + + Examples: + In Python code: + + >>> logger = get_logger("myplugin") + >>> logger.debug("This is a debug message.") + >>> logger.info("This is an info message.", once=True) + + In Jinja templates (logger available in context as `log`): + + ```jinja + {{ log.debug("This is a debug message.") }} + {{ log.info("This is an info message.", once=True) }} + ``` + """ def __init__(self, prefix: str, logger: logging.Logger): """Initialize the object. @@ -36,6 +54,7 @@ def __init__(self, prefix: str, logger: logging.Logger): """ super().__init__(logger, {}) self.prefix = prefix + self._logged: set[tuple[LoggerAdapter, str]] = set() def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]: """Process the message. @@ -49,11 +68,32 @@ def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any] """ return f"{self.prefix}: {msg}", kwargs + def log(self, level: int, msg: object, *args: object, **kwargs: object) -> None: + """Log a message. + + Arguments: + level: The logging level. + msg: The message. + *args: Additional arguments passed to parent method. + **kwargs: Additional keyword arguments passed to parent method. + """ + if kwargs.pop("once", False): + if (key := (self, str(msg))) in self._logged: + return + self._logged.add(key) + super().log(level, msg, *args, **kwargs) # type: ignore[arg-type] + class TemplateLogger: """A wrapper class to allow logging in templates. - Attributes: + The logging methods provided by this class all accept + two parameters: + + - `msg`: The message to log. + - `once`: If `True`, the message will only be logged once. + + Methods: debug: Function to log a DEBUG message. info: Function to log an INFO message. warning: Function to log a WARNING message. @@ -85,18 +125,19 @@ def get_template_logger_function(logger_func: Callable) -> Callable: """ @pass_context - def wrapper(context: Context, msg: str | None = None) -> str: + def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str: """Log a message. Arguments: context: The template context, automatically provided by Jinja. msg: The message to log. + **kwargs: Additional arguments passed to the logger function. Returns: An empty string. """ template_path = get_template_path(context) - logger_func(f"{template_path}: {msg or 'Rendering'}") + logger_func(f"{template_path}: {msg or 'Rendering'}", **kwargs) return "" return wrapper diff --git a/tests/test_loggers.py b/tests/test_loggers.py new file mode 100644 index 00000000..1644c0f0 --- /dev/null +++ b/tests/test_loggers.py @@ -0,0 +1,64 @@ +"""Tests for the loggers module.""" + +from unittest.mock import MagicMock + +import pytest + +from mkdocstrings.loggers import get_logger, get_template_logger + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"once": False}, + {"once": True}, + ], +) +def test_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None: + """Test logger methods. + + Parameters: + kwargs: Keyword arguments passed to the logger methods. + """ + logger = get_logger("mkdocstrings.test") + caplog.set_level(0) + for _ in range(2): + logger.debug("Debug message", **kwargs) + logger.info("Info message", **kwargs) + logger.warning("Warning message", **kwargs) + logger.error("Error message", **kwargs) + logger.critical("Critical message", **kwargs) + if kwargs.get("once", False): + assert len(caplog.records) == 5 + else: + assert len(caplog.records) == 10 + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"once": False}, + {"once": True}, + ], +) +def test_template_logger(kwargs: dict, caplog: pytest.LogCaptureFixture) -> None: + """Test template logger methods. + + Parameters: + kwargs: Keyword arguments passed to the template logger methods. + """ + logger = get_template_logger() + mock = MagicMock() + caplog.set_level(0) + for _ in range(2): + logger.debug(mock, "Debug message", **kwargs) + logger.info(mock, "Info message", **kwargs) + logger.warning(mock, "Warning message", **kwargs) + logger.error(mock, "Error message", **kwargs) + logger.critical(mock, "Critical message", **kwargs) + if kwargs.get("once", False): + assert len(caplog.records) == 5 + else: + assert len(caplog.records) == 10 From 253d215426f28939d544502fb1032b2c796c34ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 17:13:45 +0200 Subject: [PATCH 062/223] docs: Load inventories for MkDocs and Markdown --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 860ce66e..b46e9872 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -137,6 +137,8 @@ plugins: - 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 paths: [src] options: docstring_options: From 7ff1681d417bd68b8a7ce6f9487638bda03e3710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 17:13:54 +0200 Subject: [PATCH 063/223] docs: Enable parameter headings --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index b46e9872..30afc977 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -148,6 +148,7 @@ plugins: 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 From c5b5f697c83271d961c7ac795412d6b4964ba2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 17:53:41 +0200 Subject: [PATCH 064/223] refactor: Allow specifying name of template loggers --- src/mkdocstrings/handlers/base.py | 3 ++- src/mkdocstrings/loggers.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index f52e17dc..27c22db1 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -74,6 +74,7 @@ class BaseHandler: To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates. """ + # TODO: Make name mandatory? name: str = "" """The handler's name, for example "python".""" domain: str = "default" @@ -132,7 +133,7 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None auto_reload=False, # Editing a template in the middle of a build is not useful. ) self.env.filters["any"] = do_any - self.env.globals["log"] = get_template_logger() + self.env.globals["log"] = get_template_logger(self.name) self._headings: list[Element] = [] self._md: Markdown = None # type: ignore[assignment] # To be populated in `update_env`. diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 9d7408cc..240e1808 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -177,10 +177,14 @@ def get_logger(name: str) -> LoggerAdapter: return LoggerAdapter(name.split(".", 1)[0], logger) -def get_template_logger() -> TemplateLogger: +def get_template_logger(handler_name: str | None = None) -> TemplateLogger: """Return a logger usable in templates. + Parameters: + handler_name: The name of the handler. + Returns: A template logger. """ - return TemplateLogger(get_logger("mkdocstrings.templates")) + handler_name = handler_name or "base" + return TemplateLogger(get_logger(f"mkdocstrings_handlers.{handler_name}.templates")) From 87d82299773a0203329d9d45ce3e1210c3320375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 18:12:23 +0200 Subject: [PATCH 065/223] chore: Prepare release 0.25.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 723eb24c..7066ff44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.25.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.0) - 2024-04-27 + +[Compare with 0.24.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.3...0.25.0) + +### Features + +- Support `once` parameter in logging methods, allowing to log a message only once with a given logger ([1532b59](https://github.com/mkdocstrings/mkdocstrings/commit/1532b59a6efd99fed846cf7edfd0b26525700d3f) by Timothรฉe Mazzucotelli). +- Support blank line between `::: path` and YAML options ([d799d2f](https://github.com/mkdocstrings/mkdocstrings/commit/d799d2f3903bce44fb751f8cf3fb8078d25549da) by Timothรฉe Mazzucotelli). [Issue-450](https://github.com/mkdocstrings/mkdocstrings/issues/450) + +### Code Refactoring + +- Allow specifying name of template loggers ([c5b5f69](https://github.com/mkdocstrings/mkdocstrings/commit/c5b5f697c83271d961c7ac795412d6b4964ba2b7) by Timothรฉe Mazzucotelli). + ## [0.24.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.24.3) - 2024-04-05 [Compare with 0.24.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.2...0.24.3) From bc25b6a00cb1515f0dc8c433ae595092fcddfba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 18:26:09 +0200 Subject: [PATCH 066/223] docs: Remove requirements link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44158d4a..10b3b8ff 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo --- -**[Features](#features)** - **[Requirements](#requirements)** - **[Installation](#installation)** - **[Quick usage](#quick-usage)** +**[Features](#features)** - **[Installation](#installation)** - **[Quick usage](#quick-usage)** ![mkdocstrings_gif1](https://user-images.githubusercontent.com/3999221/77157604-fb807480-6aa1-11ea-99e0-d092371d4de0.gif) From a86ca7d24a57b1217ee9d21c96d133ef31dcbd62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 27 Apr 2024 18:26:22 +0200 Subject: [PATCH 067/223] docs: Customize parameters color in ToC --- docs/css/mkdocstrings.css | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 3960e49e..9e17377f 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -29,4 +29,14 @@ a.autorefs-external:hover::after { /* Avoid breaking parameters name, etc. in table cells. */ td code { word-break: normal !important; -} \ No newline at end of file +} + +[data-md-color-scheme="default"] { + --doc-symbol-parameter-fg-color: #d3a81b; + --doc-symbol-parameter-bg-color: #d3a81b1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-parameter-fg-color: #dfbe50; + --doc-symbol-parameter-bg-color: #dfbe501a; +} From cb86e08bbc5e8057393aa1cd7ca29bc2b40ab5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 5 May 2024 15:44:43 +0200 Subject: [PATCH 068/223] fix: Always descend into sub-headings when re-applying their label Issue-mkdocstrings/python-158: https://github.com/mkdocstrings/python/issues/158 --- src/mkdocstrings/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 23e90cff..9ecffebc 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -261,7 +261,7 @@ def _override_toc_labels(self, tokens: list[dict[str, Any]]) -> None: for token in tokens: if (label := token.get("data-toc-label")) and token["name"] != label: token["name"] = label - self._override_toc_labels(token["children"]) + self._override_toc_labels(token["children"]) class MkdocstringsExtension(Extension): From e135869681dc712a3721a7753c9825b8c040bb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 5 May 2024 16:08:59 +0200 Subject: [PATCH 069/223] chore: Prepare release 0.25.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7066ff44..105df260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.25.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.1) - 2024-05-05 + +[Compare with 0.25.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.0...0.25.1) + +### Bug Fixes + +- Always descend into sub-headings when re-applying their label ([cb86e08](https://github.com/mkdocstrings/mkdocstrings/commit/cb86e08bbc5e8057393aa1cd7ca29bc2b40ab5eb) by Timothรฉe Mazzucotelli). [Issue-mkdocstrings/python-158](https://github.com/mkdocstrings/python/issues/158) + ## [0.25.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.0) - 2024-04-27 [Compare with 0.24.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.24.3...0.25.0) From 8013be4f59f8d77f7d51adfe568edbbcd186ca04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 12:40:36 +0200 Subject: [PATCH 070/223] chore: Clean up unused condition --- src/mkdocstrings/extension.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 9ecffebc..a1614aa3 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -210,8 +210,6 @@ def _process_block( data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: log.error(str(exception)) # noqa: TRY400 - if PluginError is SystemExit: # TODO: when MkDocs 1.2 is sufficiently common, this can be dropped. - log.error(f"Error reading page '{self._autorefs.current_page}':") # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. From 64c5ff603f9c416ad1185e2fd1a960fe68ade728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 12:41:54 +0200 Subject: [PATCH 071/223] chore: Improve code comments --- src/mkdocstrings/extension.py | 7 ++++++- src/mkdocstrings/handlers/base.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index a1614aa3..de4f793a 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -129,9 +129,14 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: 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) - # So we need to duplicate the headings directly (and delete later), just so 'toc' can pick them up. + # We need to duplicate the headings directly, just so 'toc' can pick them up, + # otherwise they wouldn't appear in the final table of contents. + # These headings are generated by the `BaseHandler.do_heading` method (Jinja filter), + # which runs in the inner Markdown conversion layer, and not in the outer one where we are now. headings = handler.get_headings() el.extend(headings) + # These duplicated headings will later be removed by our `_HeadingsPostProcessor` processor, + # which runs right after 'toc' (see `MkdocstringsExtension.extendMarkdown`). page = self._autorefs.current_page if page is not None: diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 27c22db1..84225df3 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -304,7 +304,16 @@ def do_heading( Returns: An HTML string. """ - # First, produce the "fake" heading, for ToC only. + # Produce a heading element that will be used later, in `AutoDocProcessor.run`, to: + # - register it in the ToC: right now we're in the inner Markdown conversion layer, + # so we have to bubble up the information to the outer Markdown conversion layer, + # for the ToC extension to pick it up. + # - register it in autorefs: right now we don't know what page is being rendered, + # so we bubble up the information again to where autorefs knows the page, + # and can correctly register the heading anchor (id) to its full URL. + # - register it in the objects inventory: same as for autorefs, + # we don't know the page here, or the handler (and its domain), + # so we bubble up the information to where the mkdocstrings extension knows that. el = Element(f"h{heading_level}", attributes) if toc_label is None: toc_label = content.unescape() if isinstance(content, Markup) else content @@ -320,7 +329,7 @@ def do_heading( # Start with a heading that has just attributes (no text), and add a placeholder into it. el = Element(f"h{heading_level}", attributes) el.append(Element("mkdocstrings-placeholder")) - # Tell the 'toc' extension to make its additions if configured so. + # Tell the inner 'toc' extension to make its additions if configured so. toc = cast(TocTreeprocessor, self._md.treeprocessors["toc"]) if toc.use_anchors: toc.add_anchor(el, attributes["id"]) From fb194d8b32cd3494fedb1e9cbadaeccd76aa8b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 12:42:23 +0200 Subject: [PATCH 072/223] chore: Clean up `get_anchors` --- src/mkdocstrings/handlers/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 84225df3..ad961597 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -244,11 +244,7 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: Returns: The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. """ - # TODO: remove this when https://github.com/mkdocstrings/crystal/pull/6 is merged and released - try: - return (self.get_anchor(data),) # type: ignore[attr-defined] - except AttributeError: - return () + return () def do_convert_markdown( self, From 2e5f89e8cef11e6447425d3700c29558cd6d241b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 12:43:43 +0200 Subject: [PATCH 073/223] refactor: Give precedence to Markdown heading level (`##`) --- src/mkdocstrings/extension.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index de4f793a..19326720 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -208,7 +208,8 @@ def _process_block( options = ChainMap(local_options, global_options) if heading_level: - options = ChainMap(options, {"heading_level": heading_level}) # like setdefault + # Heading level obtained from Markdown (`##`) takes precedence. + options = ChainMap({"heading_level": heading_level}, options) log.debug("Collecting data") try: From 80ab4981b92b69a14a72c5c4562145bdbad04ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 16:25:28 +0200 Subject: [PATCH 074/223] chore: Clean up pytest warning filters --- config/pytest.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index 5b5bd2e7..ebdeb484 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -14,7 +14,3 @@ filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist - # TODO: https://github.com/Python-Markdown/markdown/issues/1355 - ignore:.*Testing:DeprecationWarning:markdown - # TODO: https://github.com/facelessuser/pymdown-extensions/issues/2113 - ignore:.*Testing:DeprecationWarning:pymdownx From da216b0af460681b6b9dc912837d68103f941479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 16:25:40 +0200 Subject: [PATCH 075/223] ci: Ignore unused arg --- src/mkdocstrings/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index ad961597..d86e9df1 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -235,7 +235,7 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: + def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 """Return the possible identifiers (HTML anchors) for a collected item. Arguments: From 924ecd818e69890708ec43bb88195c8a64dbab44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 16:38:42 +0200 Subject: [PATCH 076/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .../{bug_report.md => 1-bug.md} | 2 +- .../{feature_request.md => 2-feature.md} | 0 .github/ISSUE_TEMPLATE/3-docs.md | 16 + .github/ISSUE_TEMPLATE/4-change.md | 18 + .github/workflows/ci.yml | 19 +- CONTRIBUTING.md | 12 +- Makefile | 5 +- README.md | 4 +- config/pytest.ini | 2 - config/vscode/tasks.json | 6 - devdeps.txt | 51 +-- docs/.overrides/main.html | 8 +- docs/.overrides/partials/comments.html | 57 +++ docs/css/mkdocstrings.css | 2 +- docs/index.md | 5 + docs/insiders/index.md | 4 + docs/insiders/installation.md | 124 +----- docs/js/feedback.js | 14 + docs/license.md | 5 + duties.py | 206 +++------- mkdocs.yml | 15 + pyproject.toml | 19 + scripts/gen_credits.py | 8 +- scripts/gen_ref_nav.py | 2 +- scripts/make | 367 ++++++++++-------- 26 files changed, 493 insertions(+), 480 deletions(-) rename .github/ISSUE_TEMPLATE/{bug_report.md => 1-bug.md} (98%) rename .github/ISSUE_TEMPLATE/{feature_request.md => 2-feature.md} (100%) create mode 100644 .github/ISSUE_TEMPLATE/3-docs.md create mode 100644 .github/ISSUE_TEMPLATE/4-change.md create mode 100644 docs/.overrides/partials/comments.html create mode 100644 docs/js/feedback.js diff --git a/.copier-answers.yml b/.copier-answers.yml index 0e8a2418..47271588 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.1.0 +_commit: 1.4.0 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/1-bug.md similarity index 98% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/1-bug.md index 6ed84b16..e775cc1f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -53,7 +53,7 @@ PASTE TRACEBACK HERE python -m mkdocstrings.debug # | xclip -selection clipboard ``` -PASTE OUTPUT HERE +PASTE MARKDOWN OUTPUT HERE ### Additional context + +### Relevant code snippets + + +### Link to the relevant documentation section + diff --git a/.github/ISSUE_TEMPLATE/4-change.md b/.github/ISSUE_TEMPLATE/4-change.md new file mode 100644 index 00000000..dc9a8f17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-change.md @@ -0,0 +1,18 @@ +--- +name: Change request +about: Suggest any other kind of change for this project. +title: "change: " +assignees: pawamoy +--- + +### Is your change request related to a problem? Please describe. + + +### Describe the solution you'd like + + +### Describe alternatives you've considered + + +### Additional context + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3248b1a9..8469f091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,9 +50,6 @@ jobs: - name: Check if the code is correctly typed run: make check-types - - name: Check for vulnerabilities in dependencies - run: make check-dependencies - - name: Check for breaking changes in the API run: make check-api @@ -70,10 +67,14 @@ jobs: {"python-version": "3.9"}, {"python-version": "3.10"}, {"python-version": "3.11"}, - {"python-version": "3.12"} + {"python-version": "3.12"}, + {"python-version": "3.13"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else - echo 'jobs=[]' >> $GITHUB_OUTPUT + echo 'jobs=[ + {"os": "macos-latest", "resolution": "lowest-direct"}, + {"os": "windows-latest", "resolution": "lowest-direct"} + ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT fi tests: @@ -91,9 +92,13 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" + resolution: + - highest + - lowest-direct exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.12' }} + continue-on-error: ${{ matrix.python-version == '3.13' }} steps: - name: Checkout @@ -109,6 +114,8 @@ jobs: run: pip install uv - name: Install dependencies + env: + UV_RESOLUTION: ${{ matrix.resolution }} run: make setup - name: Run the test suite diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f86ff10..b04a64fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,13 +36,11 @@ Run `make help` to see all the available actions! ## Tasks -This project uses [duty](https://github.com/pawamoy/duty) to run tasks. -A Makefile is also provided. The Makefile will try to run certain tasks -on multiple Python versions. If for some reason you don't want to run the task -on multiple Python versions, you run the task directly with `make run duty TASK`. - -The Makefile detects if a virtual environment is activated, -so `make` will work the same with the virtualenv activated or not. +The entry-point to run commands and tasks is the `make` Python script, +located in the `scripts` directory. Try running `make` to show the available commands and tasks. +The *commands* do not need the Python dependencies to be installed, +while the *tasks* do. +The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) diff --git a/Makefile b/Makefile index 771b333c..5e88121d 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ # This Makefile is just here to allow auto-completion in the terminal. actions = \ + allrun \ changelog \ check \ check-api \ - check-dependencies \ check-docs \ check-quality \ check-types \ @@ -16,6 +16,7 @@ actions = \ docs-deploy \ format \ help \ + multirun \ release \ run \ setup \ @@ -24,4 +25,4 @@ actions = \ .PHONY: $(actions) $(actions): - @bash scripts/make "$@" + @python scripts/make "$@" diff --git a/README.md b/README.md index 10b3b8ff..80822c52 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # mkdocstrings [![ci](https://github.com/mkdocstrings/mkdocstrings/workflows/ci/badge.svg)](https://github.com/mkdocstrings/mkdocstrings/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://mkdocstrings.github.io/) +[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/) [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-blue.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) +[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) Automatic documentation from sources, for [MkDocs](https://www.mkdocs.org/). diff --git a/config/pytest.ini b/config/pytest.ini index ebdeb484..052a2f18 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -1,8 +1,6 @@ [pytest] python_files = test_*.py - *_test.py - tests.py addopts = --cov --cov-config config/coverage.ini diff --git a/config/vscode/tasks.json b/config/vscode/tasks.json index 30008cf2..73145eec 100644 --- a/config/vscode/tasks.json +++ b/config/vscode/tasks.json @@ -31,12 +31,6 @@ "command": "scripts/make", "args": ["check-docs"] }, - { - "label": "check-dependencies", - "type": "process", - "command": "scripts/make", - "args": ["check-dependencies"] - }, { "label": "check-api", "type": "process", diff --git a/devdeps.txt b/devdeps.txt index 58700f78..2a987e77 100644 --- a/devdeps.txt +++ b/devdeps.txt @@ -1,28 +1,33 @@ -build>=1.0 -duty>=0.10 -black>=23.9 -markdown-callouts>=0.3 -markdown-exec>=1.7 -mkdocs>=1.5 +# dev +editables>=0.5 + +# maintenance +build>=1.2 +git-changelog>=2.5 +twine>=5.0; python_version < '3.13' + +# ci +duty>=1.4 +ruff>=0.4 +pytest>=8.2 +pytest-cov>=5.0 +pytest-randomly>=3.15 +pytest-xdist>=3.6 +mypy>=1.10 +types-markdown>=3.6 +types-pyyaml>=6.0 + +# docs +black>=24.4 +markdown-callouts>=0.4 +markdown-exec>=1.8 +mkdocs>=1.6 mkdocs-coverage>=1.0 mkdocs-gen-files>=0.5 -mkdocs-git-committers-plugin-2>=1.2 +mkdocs-git-committers-plugin-2>=2.3 mkdocs-literate-nav>=0.6 -mkdocs-material>=9.4 -mkdocs-minify-plugin>=0.7 +mkdocs-material>=9.5 +mkdocs-minify-plugin>=0.8 mkdocs-redirects>=1.2 -mkdocstrings[python]>=0.23 +mkdocstrings[python]>=0.25 tomli>=2.0; python_version < '3.11' -black>=23.9 -blacken-docs>=1.16 -git-changelog>=2.3 -ruff>=0.0 -pytest>=7.4 -pytest-cov>=4.1 -pytest-randomly>=3.15 -pytest-xdist>=3.3 -mypy>=1.5 -types-markdown>=3.5 -types-pyyaml>=6.0 -safety>=2.3 -twine>=5.0 diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index cf8adeb7..1e956857 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -2,17 +2,19 @@ {% block announce %} - Sponsorship - is now available! + Fund this project through + sponsorship {% include ".icons/octicons/heart-fill-16.svg" %} — - For updates follow @pawamoy on + Follow + @pawamoy on {% include ".icons/fontawesome/brands/mastodon.svg" %} Fosstodon + for updates {% endblock %} diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html new file mode 100644 index 00000000..3976b0d6 --- /dev/null +++ b/docs/.overrides/partials/comments.html @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 9e17377f..05f1088b 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -18,7 +18,7 @@ a.autorefs-external::after { height: 1em; width: 1em; - background-color: var(--md-typeset-a-color); + background-color: currentColor; } a.external:hover::after, diff --git a/docs/index.md b/docs/index.md index 612c7a5e..8e6f2fb4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,6 @@ +--- +hide: +- feedback +--- + --8<-- "README.md" diff --git a/docs/insiders/index.md b/docs/insiders/index.md index d88e8e7c..f164974c 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -102,6 +102,10 @@ with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profi and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. +Sponsorships lower than $10 a month are also very much appreciated, and useful. +They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. +*Every* sponsorship helps us implementing new features and releasing them to the public. + **Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, please send a short email to insiders@pawamoy.fr with the name of your diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md index b7af7d2e..5671f0da 100644 --- a/docs/insiders/installation.md +++ b/docs/insiders/installation.md @@ -23,6 +23,9 @@ of Insiders projects in the PyPI index of your choice See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). +**We kindly ask that you do not upload the distributions to public registries, +as it is against our [Terms of use](index.md#terms).** + ### with pip (ssh/https) *mkdocstrings Insiders* can be installed with `pip` [using SSH][using ssh]: @@ -58,130 +61,15 @@ pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/mkdocstrings.git > token must be kept secret at all times, as it allows the owner to access your > private repositories. -### with pip (self-hosted) - -Self-hosting the Insiders package makes it possible to depend on *mkdocstrings* normally, -while transparently downloading and installing the Insiders version locally. -It means that you can specify your dependencies normally, and your contributors without access -to Insiders will get the public version, while you get the Insiders version on your machine. - -WARNING: **Limitation** -With this method, there is no way to force the installation of an Insiders version -rather than a public version. If there is a public version that is more recent -than your self-hosted Insiders version, the public version will take precedence. -Remember to regularly update your self-hosted versions by uploading latest distributions. - -You can build the distributions for Insiders yourself, by cloning the repository -and using [build] to build the distributions, -or you can download them from our [GitHub Releases]. -You can upload these distributions to a private PyPI-like registry -([Artifactory], [Google Cloud], [pypiserver], etc.) -with [Twine]: - - [build]: https://pypi.org/project/build/ - [Artifactory]: https://jfrog.com/help/r/jfrog-artifactory-documentation/pypi-repositories - [Google Cloud]: https://cloud.google.com/artifact-registry/docs/python - [pypiserver]: https://pypi.org/project/pypiserver/ - [Github Releases]: https://github.com/pawamoy-insiders/mkdocstrings/releases - [Twine]: https://pypi.org/project/twine/ - -```bash -# download distributions in ~/dists, then upload with: -twine upload --repository-url https://your-private-index.com ~/dists/* -``` - -You might also need to provide a username and password/token to authenticate against the registry. -Please check [Twine's documentation][twine docs]. - - [twine docs]: https://twine.readthedocs.io/en/stable/ - -You can then configure pip (or other tools) to look for packages into your package index. -For example, with pip: - -```bash -pip config set global.extra-index-url https://your-private-index.com/simple -``` - -Note that the URL might differ depending on whether your are uploading a package (with Twine) -or installing a package (with pip), and depending on the registry you are using (Artifactory, Google Cloud, etc.). -Please check the documentation of your registry to learn how to configure your environment. - -**We kindly ask that you do not upload the distributions to public registries, -as it is against our [Terms of use](index.md#terms).** +### with Git ->? TIP: **Full example with `pypiserver`** -> In this example we use [pypiserver] to serve a local PyPI index. -> -> ```bash -> pip install --user pypiserver -> # or pipx install pypiserver -> -> # create a packages directory -> mkdir -p ~/.local/pypiserver/packages -> -> # run the pypi server without authentication -> pypi-server run -p 8080 -a . -P . ~/.local/pypiserver/packages & -> ``` -> -> We can configure the credentials to access the server in [`~/.pypirc`][pypirc]: -> -> [pypirc]: https://packaging.python.org/en/latest/specifications/pypirc/ -> -> ```ini title=".pypirc" -> [distutils] -> index-servers = -> local -> -> [local] -> repository: http://localhost:8080 -> username: -> password: -> ``` -> -> We then clone the Insiders repository, build distributions and upload them to our local server: -> -> ```bash -> # clone the repository -> git clone git@github.com:pawamoy-insiders/mkdocstrings -> cd mkdocstrings -> -> # install build -> pip install --user build -> # or pipx install build -> -> # checkout latest tag -> git checkout $(git describe --tags --abbrev=0) -> -> # build the distributions -> pyproject-build -> -> # upload them to our local server -> twine upload -r local dist/* --skip-existing -> ``` -> -> Finally, we configure pip, and for example [PDM][pdm], to use our local index to find packages: -> -> ```bash -> pip config set global.extra-index-url http://localhost:8080/simple -> pdm config pypi.extra.url http://localhost:8080/simple -> ``` -> -> [pdm]: https://pdm.fming.dev/latest/ -> -> Now when running `pip install mkdocstrings`, -> or resolving dependencies with PDM, -> both tools will look into our local index and find the Insiders version. -> **Remember to update your local index regularly!** - -### with git - -Of course, you can use *mkdocstrings Insiders* directly from `git`: +Of course, you can use *mkdocstrings Insiders* directly using Git: ``` git clone git@github.com:pawamoy-insiders/mkdocstrings ``` -When cloning from `git`, the package must be installed: +When cloning with Git, the package must be installed: ``` pip install -e mkdocstrings diff --git a/docs/js/feedback.js b/docs/js/feedback.js new file mode 100644 index 00000000..f97321a5 --- /dev/null +++ b/docs/js/feedback.js @@ -0,0 +1,14 @@ +const feedback = document.forms.feedback; +feedback.hidden = false; + +feedback.addEventListener("submit", function(ev) { + ev.preventDefault(); + const commentElement = document.getElementById("feedback"); + commentElement.style.display = "block"; + feedback.firstElementChild.disabled = true; + const data = ev.submitter.getAttribute("data-md-value"); + const note = feedback.querySelector(".md-feedback__note [data-md-value='" + data + "']"); + if (note) { + note.hidden = false; + } +}) diff --git a/docs/license.md b/docs/license.md index a873d2b5..e81c0edf 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,3 +1,8 @@ +--- +hide: +- feedback +--- + # License ``` diff --git a/duties.py b/duties.py index 30bf7c63..655cfdfc 100644 --- a/duties.py +++ b/duties.py @@ -9,8 +9,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterator -from duty import duty -from duty.callables import coverage, lazy, mkdocs, mypy, pytest, ruff, safety +from duty import duty, tools if TYPE_CHECKING: from duty.context import Context @@ -45,143 +44,72 @@ def material_insiders() -> Iterator[bool]: # noqa: D103 @duty -def changelog(ctx: Context) -> None: +def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. Parameters: - ctx: The context instance (passed automatically). + bump: Bump option passed to git-changelog. """ - from git_changelog.cli import main as git_changelog - - ctx.run(git_changelog, args=[[]], title="Updating changelog") + ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) def check(ctx: Context) -> None: # noqa: ARG001 - """Check it all! - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check it all!""" @duty def check_quality(ctx: Context) -> None: - """Check the code quality. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check the code quality.""" ctx.run( - ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml"), title=pyprefix("Checking code quality"), - command=f"ruff check --config config/ruff.toml {PY_SRC}", - ) - - -@duty -def check_dependencies(ctx: Context) -> None: - """Check for vulnerabilities in dependencies. - - Parameters: - ctx: The context instance (passed automatically). - """ - # retrieve the list of dependencies - requirements = ctx.run( - ["uv", "pip", "freeze"], - silent=True, - allow_overrides=False, - ) - - ctx.run( - safety.check(requirements), - title="Checking dependencies", - command="uv pip freeze | safety check --stdin", ) @duty def check_docs(ctx: Context) -> None: - """Check if the documentation builds correctly. - - Parameters: - ctx: The context instance (passed automatically). - """ + """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( - mkdocs.build(strict=True, verbose=True), + tools.mkdocs.build(strict=True, verbose=True), title=pyprefix("Building documentation"), - command="mkdocs build -vs", ) @duty def check_types(ctx: Context) -> None: - """Check that the code is correctly typed. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Check that the code is correctly typed.""" os.environ["MYPYPATH"] = "src" ctx.run( - mypy.run(*PY_SRC_LIST, config_file="config/mypy.ini"), + tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), - command=f"mypy --config-file config/mypy.ini {PY_SRC}", ) @duty -def check_api(ctx: Context) -> None: - """Check for API breaking changes. - - Parameters: - ctx: The context instance (passed automatically). - """ - from griffe.cli import check as g_check - - griffe_check = lazy(g_check, name="griffe.check") +def check_api(ctx: Context, *cli_args: str) -> None: + """Check for API breaking changes.""" ctx.run( - griffe_check("mkdocstrings", search_paths=["src"], color=True), + tools.griffe.check("mkdocstrings", search=["src"], color=True).add_args(*cli_args), title="Checking for API breaking changes", - command="griffe check -ssrc mkdocstrings", nofail=True, ) -@duty(silent=True) -def clean(ctx: Context) -> None: - """Delete temporary files. - - Parameters: - ctx: The context instance (passed automatically). - """ - - def _rm(*targets: str) -> None: - for target in targets: - ctx.run(f"rm -rf {target}") - - def _find_rm(*targets: str) -> None: - for target in targets: - ctx.run(f"find . -type d -name '{target}' | xargs rm -rf") - - _rm("build", "dist", ".coverage*", "htmlcov", "site", ".pdm-build") - _find_rm(".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__") - - @duty -def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: +def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000) -> None: """Serve the documentation (localhost:8000). Parameters: - ctx: The context instance (passed automatically). host: The host to serve the docs from. port: The port to serve the docs on. """ with material_insiders(): ctx.run( - mkdocs.serve(dev_addr=f"{host}:{port}"), + tools.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), title="Serving documentation", capture=False, ) @@ -189,11 +117,7 @@ def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None: @duty def docs_deploy(ctx: Context) -> None: - """Deploy the documentation on GitHub pages. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Deploy the documentation to GitHub pages.""" os.environ["DEPLOY"] = "true" with material_insiders() as insiders: if not insiders: @@ -206,7 +130,7 @@ def docs_deploy(ctx: Context) -> None: nofail=True, ) ctx.run( - mkdocs.gh_deploy(remote_name="upstream", force=True), + tools.mkdocs.gh_deploy(remote_name="upstream", force=True), title="Deploying documentation", ) else: @@ -219,24 +143,42 @@ def docs_deploy(ctx: Context) -> None: @duty def format(ctx: Context) -> None: - """Run formatting tools on the code. - - Parameters: - ctx: The context instance (passed automatically). - """ + """Run formatting tools on the code.""" ctx.run( - ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), + tools.ruff.check(*PY_SRC_LIST, config="config/ruff.toml", fix_only=True, exit_zero=True), title="Auto-fixing code", ) - ctx.run(ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") + ctx.run(tools.ruff.format(*PY_SRC_LIST, config="config/ruff.toml"), title="Formatting code") + + +@duty +def build(ctx: Context) -> None: + """Build source and wheel distributions.""" + ctx.run( + tools.build(), + title="Building source and wheel distributions", + pty=PTY, + ) + + +@duty +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()] + ctx.run( + tools.twine.upload(*dists, skip_existing=True), + title="Publishing source and wheel distributions to PyPI", + pty=PTY, + ) -@duty(post=["docs-deploy"]) -def release(ctx: Context, version: str) -> None: +@duty(post=["build", "publish", "docs-deploy"]) +def release(ctx: Context, version: str = "") -> None: """Release a new Python package. Parameters: - ctx: The context instance (passed automatically). version: The new version number to use. """ origin = ctx.run("git config --get remote.origin.url", silent=True) @@ -245,64 +187,38 @@ def release(ctx: Context, version: str) -> None: lambda: False, title="Not releasing from insiders repository (do that from public repo instead!)", ) + if not (version := (version or input("> Version to release: ")).strip()): + ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY) ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY) ctx.run("git push", title="Pushing commits", pty=False) ctx.run("git push --tags", title="Pushing tags", pty=False) - ctx.run("pyproject-build", title="Building dist/wheel", pty=PTY) - ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY) - -@duty(silent=True, aliases=["coverage"]) -def cov(ctx: Context) -> None: - """Report coverage as text and HTML. - Parameters: - ctx: The context instance (passed automatically). - """ - ctx.run(coverage.combine, nofail=True) - ctx.run(coverage.report(rcfile="config/coverage.ini"), capture=False) - ctx.run(coverage.html(rcfile="config/coverage.ini")) +@duty(silent=True, aliases=["cov"]) +def coverage(ctx: Context) -> None: + """Report coverage as text and HTML.""" + ctx.run(tools.coverage.combine(), nofail=True) + ctx.run(tools.coverage.report(rcfile="config/coverage.ini"), capture=False) + ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) @duty -def test(ctx: Context, match: str = "") -> None: +def test(ctx: Context, *cli_args: str, match: str = "") -> None: """Run the test suite. Parameters: - ctx: The context instance (passed automatically). match: A pytest expression to filter selected tests. """ py_version = f"{sys.version_info.major}{sys.version_info.minor}" os.environ["COVERAGE_FILE"] = f".coverage.{py_version}" ctx.run( - pytest.run("-n", "auto", "tests", config_file="config/pytest.ini", select=match, color="yes"), + tools.pytest( + "tests", + config_file="config/pytest.ini", + select=match, + color="yes", + ).add_args("-n", "auto", *cli_args), title=pyprefix("Running tests"), - command=f"pytest -c config/pytest.ini -n auto -k{match!r} --color=yes tests", ) - - -@duty -def vscode(ctx: Context) -> None: - """Configure VSCode. - - This task will overwrite the following files, - so make sure to back them up: - - - `.vscode/launch.json` - - `.vscode/settings.json` - - `.vscode/tasks.json` - - Parameters: - ctx: The context instance (passed automatically). - """ - - def update_config(filename: str) -> None: - source_file = Path("config", "vscode", filename) - target_file = Path(".vscode", filename) - target_file.parent.mkdir(exist_ok=True) - target_file.write_text(source_file.read_text()) - - for filename in ("launch.json", "settings.json", "tasks.json"): - ctx.run(update_config, args=[filename], title=f"Update .vscode/{filename}") diff --git a/mkdocs.yml b/mkdocs.yml index 30afc977..3522d7db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,6 +92,9 @@ extra_css: - css/mkdocstrings.css - css/insiders.css +extra_javascript: +- js/feedback.js + markdown_extensions: - attr_list - admonition @@ -183,3 +186,15 @@ extra: 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 b35e8dc7..e7c02d17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -65,3 +66,21 @@ version = {source = "scm"} [tool.pdm.build] package-dir = "src" editable-backend = "editables" +excludes = ["**/.pytest_cache"] +source-includes = [ + "config", + "docs", + "scripts", + "share", + "tests", + "devdeps.txt", + "duties.py", + "mkdocs.yml", + "*.md", + "LICENSE", +] + +[tool.pdm.build.wheel-data] +data = [ + {path = "share/**/*", relative-to = "."}, +] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 27f94d67..eaa1c7f4 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -27,7 +27,7 @@ project = pyproject["project"] project_name = project["name"] with project_dir.joinpath("devdeps.txt").open() as devdeps_file: - devdeps = [line.strip() for line in devdeps_file if not line.startswith("-e")] + devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))] PackageMetadata = Dict[str, Union[str, Iterable[str]]] Metadata = Dict[str, PackageMetadata] @@ -88,7 +88,7 @@ def _set_license(metadata: PackageMetadata) -> None: def _get_deps(base_deps: dict[str, Requirement], metadata: Metadata) -> Metadata: deps = {} for dep_name, dep_req in base_deps.items(): - if dep_name not in metadata: + 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] @@ -131,8 +131,8 @@ def _render_credits() -> str: template_data = { "project_name": project_name, - "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"])), - "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"])), + "prod_dependencies": sorted(prod_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), + "dev_dependencies": sorted(dev_dependencies.values(), key=lambda dep: str(dep["name"]).lower()), "more_credits": "http://pawamoy.github.io/credits/", } template_text = dedent( diff --git a/scripts/gen_ref_nav.py b/scripts/gen_ref_nav.py index 9565765e..676981b6 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_ref_nav.py @@ -29,7 +29,7 @@ with mkdocs_gen_files.open(full_doc_path, "w") as fd: ident = ".".join(parts) - fd.write(f"::: {ident}") + fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) diff --git a/scripts/make b/scripts/make index f690126e..d898022e 100755 --- a/scripts/make +++ b/scripts/make @@ -1,159 +1,210 @@ -#!/usr/bin/env bash - -set -e -export PYTHON_VERSIONS=${PYTHON_VERSIONS-3.8 3.9 3.10 3.11 3.12} - -exe="" -prefix="" - - -# Install runtime and development dependencies, -# as well as current project in editable mode. -uv_install() { - uv pip compile pyproject.toml devdeps.txt | uv pip install -r - - if [ -z "${CI}" ]; then - uv pip install -e . - else - uv pip install "mkdocstrings @ ." - fi -} - - -# Setup the development environment by installing dependencies -# in multiple Python virtual environments with uv: -# one venv per Python version in `.venvs/$py`, -# and an additional default venv in `.venv`. -setup() { - if ! command -v uv &>/dev/null; then - echo "make: setup: uv must be installed, see https://github.com/astral-sh/uv" >&2 - return 1 - fi - - if [ -n "${PYTHON_VERSIONS}" ]; then - for version in ${PYTHON_VERSIONS}; do - if [ ! -d ".venvs/${version}" ]; then - uv venv --python "${version}" ".venvs/${version}" - fi - VIRTUAL_ENV="${PWD}/.venvs/${version}" uv_install - done - fi - - if [ ! -d .venv ]; then uv venv --python python; fi - uv_install -} - - -# Activate a Python virtual environments. -# The annoying operating system also requires -# that we set some global variables to help it find commands... -activate() { - local path - if [ -f "$1/bin/activate" ]; then - source "$1/bin/activate" +#!/usr/bin/env python3 +"""Management commands.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from typing import Any, Iterator + +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() + +exe = "" +prefix = "" + + +def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None: + """Run a shell command.""" + if capture_output: + return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 + subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 + return None + + +@contextmanager +def environ(**kwargs: str) -> Iterator[None]: + """Temporarily set environment variables.""" + original = dict(os.environ) + os.environ.update(kwargs) + try: + yield + finally: + os.environ.clear() + os.environ.update(original) + + +def uv_install() -> None: + """Install dependencies using uv.""" + uv_opts = "" + if "UV_RESOLUTION" in os.environ: + uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" + requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) + shell("uv pip install -r -", input=requirements, text=True) + if "CI" not in os.environ: + shell("uv pip install --no-deps -e .") + else: + shell("uv pip install --no-deps .") + + +def setup() -> None: + """Setup the project.""" + if not shutil.which("uv"): + raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") + + print("Installing dependencies (default environment)") # noqa: T201 + default_venv = Path(".venv") + if not default_venv.exists(): + shell("uv venv --python python") + uv_install() + + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + print(f"\nInstalling dependencies (python{version})") # noqa: T201 + venv_path = Path(f".venvs/{version}") + if not venv_path.exists(): + shell(f"uv venv --python {version} {venv_path}") + with environ(VIRTUAL_ENV=str(venv_path.resolve())): + uv_install() + + +def activate(path: str) -> None: + """Activate a virtual environment.""" + global exe, prefix # noqa: PLW0603 + + if (bin := Path(path, "bin")).exists(): + activate_script = bin / "activate_this.py" + elif (scripts := Path(path, "Scripts")).exists(): + activate_script = scripts / "activate_this.py" + exe = ".exe" + prefix = f"{path}/Scripts/" + else: + raise ValueError(f"make: activate: Cannot find activation script in {path}") + + if not activate_script.exists(): + raise ValueError(f"make: activate: Cannot find activation script in {path}") + + exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 + + +def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in a virtual environment.""" + kwargs = {"check": True, **kwargs} + if version == "default": + activate(".venv") + subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + else: + activate(f".venvs/{version}") + os.environ["MULTIRUN"] = "1" + subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + + +def multirun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command for all configured Python versions.""" + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + run(version, cmd, *args, **kwargs) + else: + run("default", cmd, *args, **kwargs) + + +def allrun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in all virtual environments.""" + run("default", cmd, *args, **kwargs) + if PYTHON_VERSIONS: + multirun(cmd, *args, **kwargs) + + +def clean() -> None: + """Delete build artifacts and cache files.""" + paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] + for path in paths_to_clean: + shell(f"rm -rf {path}") + + cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] + for dirpath in Path(".").rglob("*"): + if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")): + shutil.rmtree(path, ignore_errors=True) + + +def vscode() -> None: + """Configure VSCode to work on this project.""" + Path(".vscode").mkdir(parents=True, exist_ok=True) + shell("cp -v config/vscode/* .vscode") + + +def main() -> int: + """Main entry point.""" + args = list(sys.argv[1:]) + if not args or args[0] == "help": + if len(args) > 1: + run("default", "duty", "--help", args[1]) + else: + print("Available commands") # noqa: T201 + print(" help Print this help. Add task name to print help.") # noqa: T201 + print(" setup Setup all virtual environments (install dependencies).") # noqa: T201 + print(" run Run a command in the default virtual environment.") # noqa: T201 + print(" multirun Run a command for all configured Python versions.") # noqa: T201 + print(" allrun Run a command in all virtual environments.") # noqa: T201 + print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201 + print(" clean Delete build artifacts and cache files.") # noqa: T201 + print(" vscode Configure VSCode to work on this project.") # noqa: T201 + try: + run("default", "python", "-V", capture_output=True) + except (subprocess.CalledProcessError, ValueError): + pass + else: + print("\nAvailable tasks") # noqa: T201 + run("default", "duty", "--list") return 0 - fi - if [ -f "$1/Scripts/activate.bat" ]; then - "$1/Scripts/activate.bat" - exe=".exe" - prefix="$1/Scripts/" - return 0 - fi - echo "run: Cannot activate venv $1" >&2 - return 1 -} - - -# Run a command in all configured Python virtual environments. -# We handle the case when the `PYTHON_VERSIONS` environment variable -# is unset or empty, for robustness. -multirun() { - local cmd="$1" - shift - - if [ -n "${PYTHON_VERSIONS}" ]; then - for version in ${PYTHON_VERSIONS}; do - (activate ".venvs/${version}" && MULTIRUN=1 "${prefix}${cmd}${exe}" "$@") - done - else - (activate .venv && "${prefix}${cmd}${exe}" "$@") - fi -} - - -# Run a command in the default Python virtual environment. -# We rely on `multirun`'s handling of empty `PYTHON_VERSIONS`. -singlerun() { - PYTHON_VERSIONS= multirun "$@" -} - - -# Record options following a command name, -# until a non-option argument is met or there are no more arguments. -# Output each option on a new line, so the parent caller can store them in an array. -# Return the number of times the parent caller must shift arguments. -options() { - local shift_count=0 - for arg in "$@"; do - if [[ "${arg}" =~ ^- || "${arg}" =~ ^.+= ]]; then - echo "${arg}" - ((shift_count++)) - else - break - fi - done - return ${shift_count} -} - - -# Main function. -main() { - local cmd - while [ $# -ne 0 ]; do - cmd="$1" - shift - - # Handle `run` early to simplify `case` below. - if [ "${cmd}" = "run" ]; then - singlerun "$@" - exit $? - fi - - # Handle `multirun` early to simplify `case` below. - if [ "${cmd}" = "multirun" ]; then - multirun "$@" - exit $? - fi - - # All commands except `run` and `multirun` can be chained on a single line. - # Some of them accept options in two formats: `-f`, `--flag` and `param=value`. - # Some of them don't, and will print warnings/errors if options were given. - opts=($(options "$@")) || shift $? - - case "${cmd}" in - # The following commands require special handling. - help|"") - singlerun duty --list ;; - setup) - setup ;; - check) - multirun duty check-quality check-types check-docs - singlerun duty check-dependencies check-api - ;; - - # The following commands run in all venvs. - check-quality|\ - check-docs|\ - check-types|\ - test) - multirun duty "${cmd}" "${opts[@]}" ;; - - # The following commands run in the default venv only. - *) - singlerun duty "${cmd}" "${opts[@]}" ;; - esac - done -} - - -# Execute the main function. -main "$@" + + while args: + cmd = args.pop(0) + + if cmd == "run": + run("default", *args) + return 0 + + if cmd == "multirun": + multirun(*args) + return 0 + + if cmd == "allrun": + allrun(*args) + return 0 + + if cmd.startswith("3."): + run(cmd, *args) + return 0 + + opts = [] + while args and (args[0].startswith("-") or "=" in args[0]): + opts.append(args.pop(0)) + + if cmd == "clean": + clean() + elif cmd == "setup": + setup() + elif cmd == "vscode": + vscode() + elif cmd == "check": + multirun("duty", "check-quality", "check-types", "check-docs") + run("default", "duty", "check-api") + elif cmd in {"check-quality", "check-docs", "check-types", "test"}: + multirun("duty", cmd, *opts) + else: + run("default", "duty", cmd, *opts) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as process: + if process.output: + print(process.output, file=sys.stderr) # noqa: T201 + sys.exit(process.returncode) From e7c8abdcfd016d1d5cc97643cfbebd5fce520adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 16:53:22 +0200 Subject: [PATCH 077/223] tests: Ignore deprecation warnings for now --- config/pytest.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/pytest.ini b/config/pytest.ini index 052a2f18..1a0d99c6 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -12,3 +12,8 @@ filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist + # TODO: remove once griffe and mkdocstrings-python release new versions + ignore:.*`get_logger`:DeprecationWarning:_griffe + ignore:.*`name`:DeprecationWarning:_griffe + ignore:.*Importing from `griffe:DeprecationWarning:mkdocstrings_handlers + ignore:.*`patch_loggers`:DeprecationWarning:_griffe From afb2a2fd27876d052b4a6c153aa5c42778c59a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 16:54:48 +0200 Subject: [PATCH 078/223] chore: Prepare release 0.25.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105df260..c092ed42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.25.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.2) - 2024-07-25 + +[Compare with 0.25.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.1...0.25.2) + +### Code Refactoring + +- Give precedence to Markdown heading level (`##`) ([2e5f89e](https://github.com/mkdocstrings/mkdocstrings/commit/2e5f89e8cef11e6447425d3700c29558cd6d241b) by Timothรฉe Mazzucotelli). + ## [0.25.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.1) - 2024-05-05 [Compare with 0.25.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.0...0.25.1) From 33aa573efb17b13e7b9da77e29aeccb3fbddd8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 25 Jul 2024 17:02:25 +0200 Subject: [PATCH 079/223] deps: Depend on mkdocs-autorefs v1 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7c02d17..0b1cdad9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ dependencies = [ "Markdown>=3.3", "MarkupSafe>=1.1", "mkdocs>=1.4", - "mkdocs-autorefs>=0.3.1", - "platformdirs>=2.2.0", + "mkdocs-autorefs>=1.0", + "platformdirs>=2.2", "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", "typing-extensions>=4.1; python_version < '3.10'", From 3ee464ad2dc0bc12972b9d4d829dd3ab757202ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 4 Aug 2024 15:45:48 +0200 Subject: [PATCH 080/223] chore: Clean up after template upgrade --- .github/ISSUE_TEMPLATE/question.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/question.md diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index c65012f1..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Question -about: Ask a question about mkdocstrings usage -title: '' -labels: question -assignees: '' - ---- - -**Add detailed information, like** -- project folder structure (`tree -L 2`) -- `mkdocs.yml` configuration file contents -- *mkdocstrings* version: [e.g. 0.10.2] From b9bd56722b7e52a141f84d9be6bc36e237577367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 4 Aug 2024 15:46:02 +0200 Subject: [PATCH 081/223] docs: List griffe-typedoc and mkdocstrings-c in insiders projects --- docs/insiders/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/insiders/index.md b/docs/insiders/index.md index f164974c..ccbca99a 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -59,7 +59,9 @@ a handful of them, [thanks to our awesome sponsors][sponsors]! --> data_source = [ "docs/insiders/goals.yml", ("griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), + ("griffe-typedoc", "https://mkdocstrings.github.io/griffe-typedoc/", "insiders/goals.yml"), ("griffe-warnings-deprecated", "https://mkdocstrings.github.io/griffe-warnings-deprecated/", "insiders/goals.yml"), + ("mkdocstrings-c", "https://mkdocstrings.github.io/c/", "insiders/goals.yml"), ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), ("mkdocstrings-shell", "https://mkdocstrings.github.io/shell/", "insiders/goals.yml"), ("mkdocstrings-typescript", "https://mkdocstrings.github.io/typescript/", "insiders/goals.yml"), From d01d6f537497c88734c159e2a92c3aa5d64fe589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 4 Aug 2024 16:09:39 +0200 Subject: [PATCH 082/223] docs: Don't auto-highlight inline code --- mkdocs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 3522d7db..e76dc8f8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,8 +106,6 @@ markdown_extensions: emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.highlight: pygments_lang_class: true -- pymdownx.inlinehilite: - style_plain_text: python - pymdownx.magiclink - pymdownx.snippets: base_path: [!relative $config_dir] From 4a47f4fec5418de70dc1e2508c93c5c15d4c728f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 4 Aug 2024 16:10:15 +0200 Subject: [PATCH 083/223] docs: Update handlers lists --- README.md | 9 ++++++--- docs/usage/handlers.md | 11 +++++++---- mkdocs.yml | 2 ++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 80822c52..4ada4cb0 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,12 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo just like *MkDocs*, *mkdocstrings* is written in Python but is language-agnostic. It means you can use it with any programming language, as long as there is a [**handler**](https://mkdocstrings.github.io/reference/handlers/base/) for it. - We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) - for the [Crystal](https://mkdocstrings.github.io/crystal/), [Python](https://mkdocstrings.github.io/python/), - and [VBA](https://pypi.org/project/mkdocstrings-vba/) languages, + We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) for the + [C](https://mkdocstrings.github.io/c/), + [Crystal](https://mkdocstrings.github.io/crystal/), + [Python](https://mkdocstrings.github.io/python/), + [TypeScript](https://mkdocstrings.github.io/typescript/), and + [VBA](https://pypi.org/project/mkdocstrings-vba/) languages, as well as for [shell scripts/libraries](https://mkdocstrings.github.io/shell/). Maybe you'd like to add another one to the list? :wink: diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index 10b8aac4..6871d72b 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -4,10 +4,13 @@ A handler is what makes it possible to collect and render documentation for a pa ## Available handlers -- Crystal -- Python -- Python (Legacy) -- Shell +- [C](https://mkdocstrings.github.io/c/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md) +- [Crystal](https://mkdocstrings.github.io/crystal/){ .external } +- [Python](https://mkdocstrings.github.io/python/){ .external } +- [Python (Legacy)](https://mkdocstrings.github.io/python-legacy/){ .external } +- [Shell](https://mkdocstrings.github.io/shell/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md) +- [TypeScript](https://mkdocstrings.github.io/typescript/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md) +- [VBA](https://pypi.org/project/mkdocstrings-vba/){ .external } ## About the Python handlers diff --git a/mkdocs.yml b/mkdocs.yml index e76dc8f8..2f6d9947 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,10 +24,12 @@ nav: - 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 From 4787d032ee1d253eeb57243aee8ce7a9fb2056a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 4 Aug 2024 16:10:31 +0200 Subject: [PATCH 084/223] docs: Add FastAPI to "Used by" section in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ada4cb0..03e82afb 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo *mkdocstrings* is used by well-known companies, projects and scientific teams: [Ansible](https://molecule.readthedocs.io/configuration/), [Apache](https://streampipes.apache.org/docs/docs/python/latest/reference/client/client/), +[FastAPI](https://fastapi.tiangolo.com/reference/fastapi/), [Google](https://docs.kidger.site/jaxtyping/api/runtime-type-checking/), [Jitsi](https://jitsi.github.io/jiwer/reference/alignment/), [Microsoft](https://microsoft.github.io/presidio/api/analyzer_python/), From 8041ef35c4b3379d74d2afec75c967315b2b5f36 Mon Sep 17 00:00:00 2001 From: Jeremy Feng <44312563+jeremy-feng@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:59:48 +0800 Subject: [PATCH 085/223] docs: Update code highlight lines --- docs/recipes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 953f3879..cb2d9eb1 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -204,7 +204,7 @@ plugins: Then, the previous script is updated like so: -```python title="scripts/gen_ref_pages.py" hl_lines="7 23 31 32" +```python title="scripts/gen_ref_pages.py" hl_lines="7 24 32 33" """Generate the code reference pages and navigation.""" from pathlib import Path @@ -278,7 +278,7 @@ Well, this is possible thanks to a third plugin: Update the script like this: -```python title="scripts/gen_ref_pages.py" hl_lines="20 21" +```python title="scripts/gen_ref_pages.py" hl_lines="21 22" """Generate the code reference pages and navigation.""" from pathlib import Path From c6ca5228339926da8f140dbed4602043add51416 Mon Sep 17 00:00:00 2001 From: fritz-astronomer <80706212+fritz-astronomer@users.noreply.github.com> Date: Thu, 15 Aug 2024 05:48:07 -0400 Subject: [PATCH 086/223] docs: Fix dead link --- docs/usage/theming.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/theming.md b/docs/usage/theming.md index b5d6f7b3..09ee92fd 100644 --- a/docs/usage/theming.md +++ b/docs/usage/theming.md @@ -62,7 +62,7 @@ to modify small part of the templates without copy-pasting the whole files. See the documentation about templates for: - the Crystal handler: https://mkdocstrings.github.io/crystal/styling.html -- the Python handler: https://mkdocstrings.github.io/python/customization/#templates +- the Python handler: https://mkdocstrings.github.io/python/usage/customization/#templates #### Debugging From 52fad1163127e94d130bbbd479b51e8e38405f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 1 Sep 2024 20:36:36 +0200 Subject: [PATCH 087/223] style: Format --- duties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/duties.py b/duties.py index 655cfdfc..3d287036 100644 --- a/duties.py +++ b/duties.py @@ -54,7 +54,7 @@ def changelog(ctx: Context, bump: str = "") -> None: @duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) -def check(ctx: Context) -> None: # noqa: ARG001 +def check(ctx: Context) -> None: """Check it all!""" From 28565f97f21bf81b2bc554679c641fba3f639882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 1 Sep 2024 23:39:24 +0200 Subject: [PATCH 088/223] build: Upgrade Python-Markdown lower bound to 3.6 Version 3.6 refactored TOC sanitation, and we rely on it for TOC labels. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0b1cdad9..d660b0ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ dependencies = [ "click>=7.0", "Jinja2>=2.11.1", - "Markdown>=3.3", + "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.4", "mkdocs-autorefs>=1.0", From 3c878b7f53f2b13a61f2facb5a91a949955e688e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 1 Sep 2024 23:40:50 +0200 Subject: [PATCH 089/223] chore: Upgrade mkdocs-redirects lower bound to avoid deprecation warning --- devdeps.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devdeps.txt b/devdeps.txt index 2a987e77..90b0f095 100644 --- a/devdeps.txt +++ b/devdeps.txt @@ -28,6 +28,6 @@ mkdocs-git-committers-plugin-2>=2.3 mkdocs-literate-nav>=0.6 mkdocs-material>=9.5 mkdocs-minify-plugin>=0.8 -mkdocs-redirects>=1.2 +mkdocs-redirects>=1.2.1 mkdocstrings[python]>=0.25 tomli>=2.0; python_version < '3.11' From b63e72691a8f92dd59b56304125de4a19e0d028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 1 Sep 2024 23:58:57 +0200 Subject: [PATCH 090/223] feat: Allow hooking into autorefs when converting Markdown docstrings Based-on-PR-autorefs#46: https://github.com/mkdocstrings/autorefs/pull/46 --- pyproject.toml | 2 +- src/mkdocstrings/handlers/base.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d660b0ee..6985ba3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.4", - "mkdocs-autorefs>=1.0", + "mkdocs-autorefs>=1.2", "platformdirs>=2.2", "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index d86e9df1..44c79e35 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -8,13 +8,14 @@ import importlib import sys from pathlib import Path -from typing import Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence, cast +from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence, cast from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader from markdown import Markdown from markdown.extensions.toc import TocTreeprocessor from markupsafe import Markup +from mkdocs_autorefs.references import AutorefsInlineProcessor from mkdocstrings.handlers.rendering import ( HeadingShiftingTreeprocessor, @@ -32,6 +33,9 @@ else: from importlib.metadata import entry_points +if TYPE_CHECKING: + from mkdocs_autorefs.references import AutorefsHookInterface + CollectorItem = Any @@ -253,6 +257,7 @@ def do_convert_markdown( html_id: str = "", *, strip_paragraph: bool = False, + autoref_hook: AutorefsHookInterface | None = None, ) -> Markup: """Render Markdown text; for use inside templates. @@ -269,12 +274,17 @@ def do_convert_markdown( 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] + + if autoref_hook: + self._md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined] + 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] + self._md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] self._md.reset() def do_heading( From b1aa042f26c346dbdbf10efb20612c0dc5f8834f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 2 Sep 2024 00:29:01 +0200 Subject: [PATCH 091/223] chore: Prepare release 0.26.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c092ed42..2617c8cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.26.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.0) - 2024-09-02 + +[Compare with 0.25.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.2...0.26.0) + +### Build + +- Upgrade Python-Markdown lower bound to 3.6 ([28565f9](https://github.com/mkdocstrings/mkdocstrings/commit/28565f97f21bf81b2bc554679c641fba3f639882) by Timothรฉe Mazzucotelli). + +### Dependencies + +- Depend on mkdocs-autorefs v1 ([33aa573](https://github.com/mkdocstrings/mkdocstrings/commit/33aa573efb17b13e7b9da77e29aeccb3fbddd8e8) by Timothรฉe Mazzucotelli). + +### Features + +- Allow hooking into autorefs when converting Markdown docstrings ([b63e726](https://github.com/mkdocstrings/mkdocstrings/commit/b63e72691a8f92dd59b56304125de4a19e0d028c) by Timothรฉe Mazzucotelli). [Based-on-PR-autorefs#46](https://github.com/mkdocstrings/autorefs/pull/46) + ## [0.25.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.25.2) - 2024-07-25 [Compare with 0.25.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.1...0.25.2) From a65035a51312b57a8556e6ae02033e05463b96fb Mon Sep 17 00:00:00 2001 From: Dan King Date: Thu, 5 Sep 2024 11:07:31 -0400 Subject: [PATCH 092/223] docs: Clarify that Installation section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Timothรฉe Mazzucotelli --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 03e82afb..f0fe22a4 100644 --- a/README.md +++ b/README.md @@ -78,24 +78,31 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo ## Installation -With `pip`: +The `mkdocstrings` package doesn't provide support for any language: it's just a common base for language handlers. +It means you likely want to install it with one or more official handlers, using [extras](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#extras). +For example, to install it with Python support: ```bash -pip install mkdocstrings +pip install 'mkdocstrings[python]' ``` -You can install support for specific languages using extras, for example: +Alternatively, you can directly install the language handlers themselves, +which depend on `mkdocstrings` anyway: ```bash -pip install 'mkdocstrings[crystal,python]' +pip install mkdocstrings-python ``` -See the [available language handlers](https://mkdocstrings.github.io/handlers/overview/). +This will give you more control over the accepted range of versions for the handlers themselves. + +See the [official language handlers](https://mkdocstrings.github.io/handlers/overview/). + +--- With `conda`: ```bash -conda install -c conda-forge mkdocstrings +conda install -c conda-forge mkdocstrings mkdocstrings-python ``` ## Quick usage From db2ab3403a95034987d574a517ddc426a4b4e1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 6 Sep 2024 12:24:54 +0200 Subject: [PATCH 093/223] fix: Instantiate config of the autorefs plugin when it is not enabled by the user Issue-autorefs#57: https://github.com/mkdocstrings/autorefs/issues/57 --- src/mkdocstrings/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 48a7d1ab..17f9fb13 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -26,7 +26,7 @@ from mkdocs.config import config_options as opt from mkdocs.plugins import BasePlugin from mkdocs.utils import write_file -from mkdocs_autorefs.plugin import AutorefsPlugin +from mkdocs_autorefs.plugin import AutorefsConfig, AutorefsPlugin from mkdocstrings._cache import download_and_cache_url, download_url_with_gz from mkdocstrings.extension import MkdocstringsExtension @@ -181,6 +181,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: 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.scan_toc = False config.plugins["autorefs"] = autorefs log.debug(f"Added a subdued autorefs instance {autorefs!r}") From 651d17642c54db49f4b6ba68b3ea216a6996907d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 6 Sep 2024 12:25:44 +0200 Subject: [PATCH 094/223] chore: Prepare release 0.26.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2617c8cb..06af1405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.26.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.1) - 2024-09-06 + +[Compare with 0.26.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.0...0.26.1) + +### Bug Fixes + +- Instantiate config of the autorefs plugin when it is not enabled by the user ([db2ab34](https://github.com/mkdocstrings/mkdocstrings/commit/db2ab3403a95034987d574a517ddc426a4b4e1bd) by Timothรฉe Mazzucotelli). [Issue-autorefs#57](https://github.com/mkdocstrings/autorefs/issues/57) + ## [0.26.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.0) - 2024-09-02 [Compare with 0.25.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.25.2...0.26.0) From 7f35f56eb88e0cdc5a00a14ec1effc7148bb8f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 17:53:02 +0200 Subject: [PATCH 095/223] docs: Remove sponsors only mention for mkdocstrings-shell --- docs/usage/handlers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index 6871d72b..37da4b67 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -8,7 +8,7 @@ A handler is what makes it possible to collect and render documentation for a pa - [Crystal](https://mkdocstrings.github.io/crystal/){ .external } - [Python](https://mkdocstrings.github.io/python/){ .external } - [Python (Legacy)](https://mkdocstrings.github.io/python-legacy/){ .external } -- [Shell](https://mkdocstrings.github.io/shell/){ .external } [:octicons-heart-fill-24:{ .heart .pulse title="Sponsors only" }](../insiders/index.md) +- [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) - [VBA](https://pypi.org/project/mkdocstrings-vba/){ .external } From b3835272a5d214687688e2f064009538a90f6895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 18:44:38 +0200 Subject: [PATCH 096/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 29 ++++++---- .github/workflows/release.yml | 20 +++---- .gitignore | 1 + .gitpod.dockerfile | 6 -- .gitpod.yml | 13 ----- CONTRIBUTING.md | 5 +- config/ruff.toml | 2 +- devdeps.txt | 33 ----------- duties.py | 22 +++++-- mkdocs.yml | 6 +- pyproject.toml | 43 +++++++++++++- scripts/gen_credits.py | 12 ++-- scripts/insiders.py | 5 +- scripts/make | 104 ++++++++++++++-------------------- 15 files changed, 145 insertions(+), 158 deletions(-) delete mode 100644 .gitpod.dockerfile delete mode 100644 .gitpod.yml delete mode 100644 devdeps.txt diff --git a/.copier-answers.yml b/.copier-answers.yml index 47271588..a5618521 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.4.0 +_commit: 1.5.0 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8469f091..5d332f31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,13 +30,16 @@ jobs: - name: Fetch all tags run: git fetch --depth=1 --tags - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml - name: Install dependencies run: make setup @@ -64,11 +67,11 @@ jobs: echo 'jobs=[ {"os": "macos-latest"}, {"os": "windows-latest"}, - {"python-version": "3.9"}, {"python-version": "3.10"}, {"python-version": "3.11"}, {"python-version": "3.12"}, - {"python-version": "3.13"} + {"python-version": "3.13"}, + {"python-version": "3.14"} ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT else echo 'jobs=[ @@ -87,31 +90,35 @@ jobs: - macos-latest - windows-latest python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" + - "3.14" resolution: - highest - lowest-direct exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.13' }} + continue-on-error: ${{ matrix.python-version == '3.14' }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up Python + - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install uv - run: pip install uv + - name: Setup uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: pyproject.toml + cache-suffix: py${{ matrix.python-version }} - name: Install dependencies env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 769e7f71..193f980f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,32 +14,30 @@ jobs: - name: Fetch all tags run: git fetch --depth=1 --tags - name: Setup Python - uses: actions/setup-python@v4 - - name: Install build - if: github.repository_owner == 'pawamoy-insiders' - run: python -m pip install build + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Setup uv + uses: astral-sh/setup-uv@v3 - name: Build dists if: github.repository_owner == 'pawamoy-insiders' - run: python -m build + run: uv tool run --from build pyproject-build - name: Upload dists artifact uses: actions/upload-artifact@v4 if: github.repository_owner == 'pawamoy-insiders' with: name: mkdocstrings-insiders path: ./dist/* - - name: Install git-changelog - if: github.repository_owner != 'pawamoy-insiders' - run: pip install git-changelog - name: Prepare release notes if: github.repository_owner != 'pawamoy-insiders' - run: git-changelog --release-notes > release-notes.md + run: uv tool run git-changelog --release-notes > release-notes.md - name: Create release with assets - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: github.repository_owner == 'pawamoy-insiders' with: files: ./dist/* - name: Create release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: github.repository_owner != 'pawamoy-insiders' with: body_path: release-notes.md diff --git a/.gitignore b/.gitignore index 41fee62d..9fea0472 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /.pdm-build/ /htmlcov/ /site/ +uv.lock # cache .cache/ diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile deleted file mode 100644 index 1590b415..00000000 --- a/.gitpod.dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM gitpod/workspace-full -USER gitpod -ENV PIP_USER=no -RUN pip3 install pipx; \ - pipx install uv; \ - pipx ensurepath diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 23a3c2b7..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,13 +0,0 @@ -vscode: - extensions: - - ms-python.python - -image: - file: .gitpod.dockerfile - -ports: -- port: 8000 - onOpen: notify - -tasks: -- init: make setup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b04a64fd..b1b169b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,12 +23,11 @@ make setup > You can install it with: > > ```bash -> python3 -m pip install --user pipx -> pipx install uv +> curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > > Now you can try running `make setup` again, -> or simply `uv install`. +> or simply `uv sync`. You now have the dependencies installed. diff --git a/config/ruff.toml b/config/ruff.toml index a8b26e1e..fbe31d5b 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,4 +1,4 @@ -target-version = "py38" +target-version = "py39" line-length = 120 [lint] diff --git a/devdeps.txt b/devdeps.txt deleted file mode 100644 index 90b0f095..00000000 --- a/devdeps.txt +++ /dev/null @@ -1,33 +0,0 @@ -# dev -editables>=0.5 - -# maintenance -build>=1.2 -git-changelog>=2.5 -twine>=5.0; python_version < '3.13' - -# ci -duty>=1.4 -ruff>=0.4 -pytest>=8.2 -pytest-cov>=5.0 -pytest-randomly>=3.15 -pytest-xdist>=3.6 -mypy>=1.10 -types-markdown>=3.6 -types-pyyaml>=6.0 - -# docs -black>=24.4 -markdown-callouts>=0.4 -markdown-exec>=1.8 -mkdocs>=1.6 -mkdocs-coverage>=1.0 -mkdocs-gen-files>=0.5 -mkdocs-git-committers-plugin-2>=2.3 -mkdocs-literate-nav>=0.6 -mkdocs-material>=9.5 -mkdocs-minify-plugin>=0.8 -mkdocs-redirects>=1.2.1 -mkdocstrings[python]>=0.25 -tomli>=2.0; python_version < '3.11' diff --git a/duties.py b/duties.py index 3d287036..0f283217 100644 --- a/duties.py +++ b/duties.py @@ -7,11 +7,13 @@ from contextlib import contextmanager from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING from duty import duty, tools if TYPE_CHECKING: + from collections.abc import Iterator + from duty.context import Context @@ -53,7 +55,7 @@ def changelog(ctx: Context, bump: str = "") -> None: ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") -@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"]) +@duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) def check(ctx: Context) -> None: """Check it all!""" @@ -116,23 +118,33 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000 @duty -def docs_deploy(ctx: Context) -> None: - """Deploy the documentation to GitHub pages.""" +def docs_deploy(ctx: Context, *, force: bool = False) -> None: + """Deploy the documentation to GitHub pages. + + 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) + 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="upstream", force=True), title="Deploying documentation", ) + elif force: + ctx.run( + tools.mkdocs.gh_deploy(force=True), + title="Deploying documentation", + ) else: ctx.run( lambda: False, diff --git a/mkdocs.yml b/mkdocs.yml index 2f6d9947..3b4fefb2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -156,13 +156,15 @@ plugins: 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 -- git-committers: +- git-revision-date-localized: enabled: !ENV [DEPLOY, false] - repository: mkdocstrings/mkdocstrings + enable_creation_date: true + type: timeago - redirects: redirect_maps: theming.md: usage/theming.md diff --git a/pyproject.toml b/pyproject.toml index 6985ba3f..867747f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Automatic documentation from sources, for MkDocs." authors = [{name = "Timothรฉe Mazzucotelli", email = "dev@pawamoy.fr"}] license = {text = "ISC"} readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"] dynamic = ["version"] classifiers = [ @@ -17,12 +17,12 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Documentation", "Topic :: Software Development", "Topic :: Software Development :: Documentation", @@ -73,7 +73,6 @@ source-includes = [ "scripts", "share", "tests", - "devdeps.txt", "duties.py", "mkdocs.yml", "*.md", @@ -84,3 +83,41 @@ source-includes = [ data = [ {path = "share/**/*", relative-to = "."}, ] + +[tool.uv] +dev-dependencies = [ + # dev + "editables>=0.5", + + # maintenance + "build>=1.2", + "git-changelog>=2.5", + "twine>=5.1", + + # ci + "duty>=1.4", + "ruff>=0.4", + "pytest>=8.2", + "pytest-cov>=5.0", + "pytest-randomly>=3.15", + "pytest-xdist>=3.6", + "mypy>=1.10", + "types-markdown>=3.6", + "types-pyyaml>=6.0", + + # docs + "black>=24.4", + "markdown-callouts>=0.4", + "markdown-exec>=1.8", + "mkdocs>=1.6", + "mkdocs-coverage>=1.0", + "mkdocs-gen-files>=0.5", + "mkdocs-git-revision-date-localized-plugin>=1.2", + "mkdocs-literate-nav>=0.6", + "mkdocs-material>=9.5", + "mkdocs-minify-plugin>=0.8", + "mkdocs-redirects>=1.2", + "mkdocstrings[python]>=0.25", + # YORE: EOL 3.10: Remove line. + "tomli>=2.0; python_version < '3.11'", +] \ No newline at end of file diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index eaa1c7f4..bd2dcbf2 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -5,17 +5,18 @@ import os import sys from collections import defaultdict +from collections.abc import Iterable from importlib.metadata import distributions from itertools import chain from pathlib import Path from textwrap import dedent -from typing import Dict, Iterable, Union +from typing import Union from jinja2 import StrictUndefined from jinja2.sandbox import SandboxedEnvironment from packaging.requirements import Requirement -# TODO: Remove once support for Python 3.10 is dropped. +# YORE: EOL 3.10: Replace block with line 2. if sys.version_info >= (3, 11): import tomllib else: @@ -26,11 +27,10 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -with project_dir.joinpath("devdeps.txt").open() as devdeps_file: - devdeps = [line.strip() for line in devdeps_file if line.strip() and not line.strip().startswith(("-e", "#"))] +devdeps = [dep for dep in pyproject["tool"]["uv"]["dev-dependencies"] if not dep.startswith("-e")] -PackageMetadata = Dict[str, Union[str, Iterable[str]]] -Metadata = Dict[str, PackageMetadata] +PackageMetadata = dict[str, Union[str, Iterable[str]]] +Metadata = dict[str, PackageMetadata] def _merge_fields(metadata: dict) -> PackageMetadata: diff --git a/scripts/insiders.py b/scripts/insiders.py index 15212486..849c6314 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -10,13 +10,16 @@ from datetime import date, datetime, timedelta from itertools import chain from pathlib import Path -from typing import Iterable, cast +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__}") diff --git a/scripts/make b/scripts/make index d898022e..ac430624 100755 --- a/scripts/make +++ b/scripts/make @@ -9,12 +9,10 @@ import subprocess import sys from contextlib import contextmanager from pathlib import Path +from textwrap import dedent from typing import Any, Iterator -PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.8 3.9 3.10 3.11 3.12 3.13").split() - -exe = "" -prefix = "" +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None: @@ -37,17 +35,13 @@ def environ(**kwargs: str) -> Iterator[None]: os.environ.update(original) -def uv_install() -> None: +def uv_install(venv: Path) -> None: """Install dependencies using uv.""" - uv_opts = "" - if "UV_RESOLUTION" in os.environ: - uv_opts = f"--resolution={os.getenv('UV_RESOLUTION')}" - requirements = shell(f"uv pip compile {uv_opts} pyproject.toml devdeps.txt", capture_output=True) - shell("uv pip install -r -", input=requirements, text=True) - if "CI" not in os.environ: - shell("uv pip install --no-deps -e .") - else: - shell("uv pip install --no-deps .") + with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): + if "CI" in os.environ: + shell("uv sync --no-editable") + else: + shell("uv sync") def setup() -> None: @@ -59,7 +53,7 @@ def setup() -> None: default_venv = Path(".venv") if not default_venv.exists(): shell("uv venv --python python") - uv_install() + uv_install(default_venv) if PYTHON_VERSIONS: for version in PYTHON_VERSIONS: @@ -67,39 +61,22 @@ def setup() -> None: venv_path = Path(f".venvs/{version}") if not venv_path.exists(): shell(f"uv venv --python {version} {venv_path}") - with environ(VIRTUAL_ENV=str(venv_path.resolve())): - uv_install() - - -def activate(path: str) -> None: - """Activate a virtual environment.""" - global exe, prefix # noqa: PLW0603 - - if (bin := Path(path, "bin")).exists(): - activate_script = bin / "activate_this.py" - elif (scripts := Path(path, "Scripts")).exists(): - activate_script = scripts / "activate_this.py" - exe = ".exe" - prefix = f"{path}/Scripts/" - else: - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - if not activate_script.exists(): - raise ValueError(f"make: activate: Cannot find activation script in {path}") - - exec(activate_script.read_text(), {"__file__": str(activate_script)}) # noqa: S102 + with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): + uv_install(venv_path) -def run(version: str, cmd: str, *args: str, **kwargs: Any) -> None: +def run(version: str, cmd: str, *args: str, no_sync: bool = False, **kwargs: Any) -> None: """Run a command in a virtual environment.""" kwargs = {"check": True, **kwargs} + uv_run = ["uv", "run"] + if no_sync: + uv_run.append("--no-sync") if version == "default": - activate(".venv") - subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + with environ(UV_PROJECT_ENVIRONMENT=".venv"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 else: - activate(f".venvs/{version}") - os.environ["MULTIRUN"] = "1" - subprocess.run([f"{prefix}{cmd}{exe}", *args], **kwargs) # noqa: S603, PLW1510 + with environ(UV_PROJECT_ENVIRONMENT=f".venvs/{version}", MULTIRUN="1"): + subprocess.run([*uv_run, cmd, *args], **kwargs) # noqa: S603, PLW1510 def multirun(cmd: str, *args: str, **kwargs: Any) -> None: @@ -124,10 +101,10 @@ def clean() -> None: for path in paths_to_clean: shell(f"rm -rf {path}") - cache_dirs = [".cache", ".pytest_cache", ".mypy_cache", ".ruff_cache", "__pycache__"] - for dirpath in Path(".").rglob("*"): - if any(dirpath.match(pattern) for pattern in cache_dirs) and not (dirpath.match(".venv") or dirpath.match(".venvs")): - shutil.rmtree(path, ignore_errors=True) + cache_dirs = {".cache", ".pytest_cache", ".mypy_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) def vscode() -> None: @@ -143,22 +120,25 @@ def main() -> int: if len(args) > 1: run("default", "duty", "--help", args[1]) else: - print("Available commands") # noqa: T201 - print(" help Print this help. Add task name to print help.") # noqa: T201 - print(" setup Setup all virtual environments (install dependencies).") # noqa: T201 - print(" run Run a command in the default virtual environment.") # noqa: T201 - print(" multirun Run a command for all configured Python versions.") # noqa: T201 - print(" allrun Run a command in all virtual environments.") # noqa: T201 - print(" 3.x Run a command in the virtual environment for Python 3.x.") # noqa: T201 - print(" clean Delete build artifacts and cache files.") # noqa: T201 - print(" vscode Configure VSCode to work on this project.") # noqa: T201 - try: - run("default", "python", "-V", capture_output=True) - except (subprocess.CalledProcessError, ValueError): - pass - else: - print("\nAvailable tasks") # noqa: T201 - run("default", "duty", "--list") + print( + dedent( + """ + Available commands + help Print this help. Add task name to print help. + setup Setup all virtual environments (install dependencies). + run Run a command in the default virtual environment. + multirun Run a command for all configured Python versions. + allrun Run a command in all virtual environments. + 3.x Run a command in the virtual environment for Python 3.x. + clean Delete build artifacts and cache files. + vscode Configure VSCode to work on this project. + """ + ), + flush=True, + ) # noqa: T201 + if os.path.exists(".venv"): + print("\nAvailable tasks", flush=True) # noqa: T201 + run("default", "duty", "--list", no_sync=True) return 0 while args: From f26edebe01337caa802a98c13240acdd8332a5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 18:53:01 +0200 Subject: [PATCH 097/223] build: Drop support for Python 3.8 --- src/mkdocstrings/extension.py | 4 +++- src/mkdocstrings/handlers/base.py | 4 +++- src/mkdocstrings/inventory.py | 5 ++++- src/mkdocstrings/loggers.py | 4 +++- src/mkdocstrings/plugin.py | 9 +++++---- tests/conftest.py | 3 ++- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 19326720..e0b88a08 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -25,7 +25,7 @@ import re from collections import ChainMap -from typing import TYPE_CHECKING, Any, MutableSequence +from typing import TYPE_CHECKING, Any from xml.etree.ElementTree import Element import yaml @@ -39,6 +39,8 @@ from mkdocstrings.loggers import get_logger if TYPE_CHECKING: + from collections.abc import MutableSequence + from markdown import Markdown from markdown.blockparser import BlockParser from mkdocs_autorefs.plugin import AutorefsPlugin diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 44c79e35..d0b9456a 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -8,7 +8,7 @@ import importlib import sys from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, Iterable, Iterator, Mapping, MutableMapping, Sequence, cast +from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, cast from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader @@ -34,6 +34,8 @@ from importlib.metadata import entry_points if TYPE_CHECKING: + from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence + from mkdocs_autorefs.references import AutorefsHookInterface CollectorItem = Any diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index f1c8962a..fb2d0018 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -8,7 +8,10 @@ import re import zlib from textwrap import dedent -from typing import BinaryIO, Collection +from typing import TYPE_CHECKING, BinaryIO + +if TYPE_CHECKING: + from collections.abc import Collection class InventoryItem: diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 240e1808..008782ed 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -5,7 +5,7 @@ import logging from contextlib import suppress from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, MutableMapping, Sequence +from typing import TYPE_CHECKING, Any, Callable try: from jinja2 import pass_context @@ -21,6 +21,8 @@ if TYPE_CHECKING: + from collections.abc import MutableMapping, Sequence + from jinja2.runtime import Context diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 17f9fb13..12ed67b6 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -18,9 +18,10 @@ import functools import os import sys +from collections.abc import Iterable, Mapping from concurrent import futures from io import BytesIO -from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Mapping, Tuple, TypeVar +from typing import TYPE_CHECKING, Any, Callable, TypeVar from mkdocs.config import Config from mkdocs.config import config_options as opt @@ -44,8 +45,8 @@ log = get_logger(__name__) -InventoryImportType = List[Tuple[str, Mapping[str, Any]]] -InventoryLoaderType = Callable[..., Iterable[Tuple[str, str]]] +InventoryImportType = list[tuple[str, Mapping[str, Any]]] +InventoryLoaderType = Callable[..., Iterable[tuple[str, str]]] P = ParamSpec("P") R = TypeVar("R") @@ -306,7 +307,7 @@ def get_handler(self, handler_name: str) -> BaseHandler: @classmethod # lru_cache does not allow mutable arguments such lists, but that is what we load from YAML config. @list_to_tuple - @functools.lru_cache(maxsize=None) + @functools.cache def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) -> Mapping[str, str]: """Download and process inventory files using a handler. diff --git a/tests/conftest.py b/tests/conftest.py index 9bb09368..74688fe7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,13 +3,14 @@ from __future__ import annotations from collections import ChainMap -from typing import TYPE_CHECKING, Any, Iterator +from typing import TYPE_CHECKING, Any import pytest from markdown.core import Markdown from mkdocs.config.defaults import MkDocsConfig if TYPE_CHECKING: + from collections.abc import Iterator from pathlib import Path from mkdocs import config From bcdfc70323eeb407c32a26621b81b35042eabb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 12 Oct 2024 18:56:36 +0200 Subject: [PATCH 098/223] chore: Prepare release 0.26.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06af1405..e52ab88f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.26.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.2) - 2024-10-12 + +[Compare with 0.26.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.1...0.26.2) + +### Build + +- Drop support for Python 3.8 ([f26edeb](https://github.com/mkdocstrings/mkdocstrings/commit/f26edebe01337caa802a98c13240acdd8332a5fa) by Timothรฉe Mazzucotelli). + ## [0.26.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.1) - 2024-09-06 [Compare with 0.26.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.0...0.26.1) From e732aaa15925a7600bbe6dee82a6ae8ee7db2a54 Mon Sep 17 00:00:00 2001 From: Stefan Mejlgaard Date: Mon, 4 Nov 2024 15:21:49 +0100 Subject: [PATCH 099/223] docs: Update contributing document to include tag pulling instructions Issue-706: https://github.com/mkdocstrings/mkdocstrings/issues/706 PR-708: https://github.com/mkdocstrings/mkdocstrings/pull/708 --- CONTRIBUTING.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1b169b9..ab0c308b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,19 @@ Every little bit helps, and credit will always be given. Nothing easier! -Fork and clone the repository, then: +Fork and clone the repository. The project uses dynamic versioning, so to get the correct package version when building, make sure to pull Git tags: ```bash cd mkdocstrings + +# Assuming you authenticate with SSH. +git remote add upstream git@github.com:mkdocstrings/mkdocstrings +git pull upstream --tags +``` + +Then: + +```bash make setup ``` From 31b3b37acc15d3d765f425fd65716a833b37224b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 4 Nov 2024 15:34:15 +0100 Subject: [PATCH 100/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 9 ++++++--- .github/workflows/release.yml | 5 +++-- README.md | 1 - 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index a5618521..6a772352 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.5.0 +_commit: 1.5.2 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d332f31..d95b7809 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,9 +26,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - - name: Fetch all tags - run: git fetch --depth=1 --tags + with: + fetch-depth: 0 + fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 @@ -106,6 +106,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 193f980f..db7a223e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,8 +11,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Fetch all tags - run: git fetch --depth=1 --tags + with: + fetch-depth: 0 + fetch-tags: true - name: Setup Python uses: actions/setup-python@v5 with: diff --git a/README.md b/README.md index f0fe22a4..e9f9fbb2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/) [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) -[![gitpod](https://img.shields.io/badge/gitpod-workspace-708FCC.svg?style=flat)](https://gitpod.io/#https://github.com/mkdocstrings/mkdocstrings) [![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) Automatic documentation from sources, for [MkDocs](https://www.mkdocs.org/). From 1c23c1b0fc4a9bdec5e0eb43c8647beab66fec55 Mon Sep 17 00:00:00 2001 From: Stefan Mejlgaard Date: Fri, 8 Nov 2024 16:35:39 +0100 Subject: [PATCH 101/223] feat: Add support for authentication in inventory file URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue-707: https://github.com/mkdocstrings/mkdocstrings/issues/707 PR-710: https://github.com/mkdocstrings/mkdocstrings/pull/710 Co-authored-by: Timothรฉe Mazzucotelli --- docs/usage/index.md | 22 +++++++- src/mkdocstrings/_cache.py | 60 ++++++++++++++++++++- tests/test_cache.py | 103 +++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 tests/test_cache.py diff --git a/docs/usage/index.md b/docs/usage/index.md index 1348b9cc..77129362 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -31,8 +31,8 @@ The YAML block is optional, and contains some configuration options: `default_handler` key, or `"python"`. - `options`: a dictionary of options passed to the handler's methods responsible both for collecting and rendering the documentation. These options can be defined - globally (in `mkdocs.yml`, see [Global options](#global-options)), - locally (as described here), or both. + globally (in `mkdocs.yml`, see [Global options](#global-options)), + locally (as described here), or both. !!! example "Example with the Python handler" === "docs/my_page.md" @@ -319,6 +319,24 @@ plugins: Absolute URLs to cross-referenced items will then be based on `https://docs.example.com/version/` instead of `https://cdn.example.com/version/`. +If you need authentication to access the inventory file, you can provide the credentials in the URL, either as `username:password`: + +```yaml +- url: https://username:password@private.example.com/version/objects.inv +``` + +...or with token authentication: + +```yaml +- url: https://token123@private.example.com/version/objects.inv +``` + +The credentials can also be specified using environment variables in the form `${ENV_VAR}`: + +```yaml +- url: https://${USERNAME}:${PASSWORD}@private.example.com/version/objects.inv +``` + Reciprocally, *mkdocstrings* also allows to *generate* an inventory file in the Sphinx format. It will be enabled by default if the Python handler is used, and generated as `objects.inv` in the final site directory. Other projects will be able to cross-reference items from your project. diff --git a/src/mkdocstrings/_cache.py b/src/mkdocstrings/_cache.py index 8737f317..d0481dd5 100644 --- a/src/mkdocstrings/_cache.py +++ b/src/mkdocstrings/_cache.py @@ -1,10 +1,13 @@ +import base64 import datetime import gzip import hashlib import os +import re import urllib.parse import urllib.request -from typing import BinaryIO, Callable +from collections.abc import Mapping +from typing import BinaryIO, Callable, Optional import click import platformdirs @@ -13,11 +16,16 @@ log = get_logger(__name__) +# Regex pattern for an environment variable in the form ${ENV_VAR}. +ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") + def download_url_with_gz(url: str) -> bytes: + url, auth_header = _extract_auth_from_url(url) + req = urllib.request.Request( # noqa: S310 url, - headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0"}, + headers={"Accept-Encoding": "gzip", "User-Agent": "mkdocstrings/0.15.0", **auth_header}, ) with urllib.request.urlopen(req) as resp: # noqa: S310 content: BinaryIO = resp @@ -26,6 +34,54 @@ def download_url_with_gz(url: str) -> bytes: return content.read() +def _expand_env_vars(credential: str, url: str, env: Optional[Mapping[str, str]] = None) -> str: + """A safe implementation of environment variable substitution. + + It only supports the following forms: `${ENV_VAR}`. + Neither `$ENV_VAR` or `%ENV_VAR` are supported. + """ + if env is None: + env = os.environ + + def replace_func(match: re.Match) -> str: + try: + return env[match.group(1)] + except KeyError: + log.warning(f"Environment variable '{match.group(1)}' is not set, but is used in inventory URL {url}") + return match.group(0) + + return re.sub(ENV_VAR_PATTERN, replace_func, credential) + + +# Implementation adapted from PDM: https://github.com/pdm-project/pdm. +def _extract_auth_from_url(url: str) -> tuple[str, dict[str, str]]: + """Extract credentials from the URL if present, and return the URL and the appropriate auth header for the credentials.""" + if "@" not in url: + return url, {} + + scheme, netloc, *rest = urllib.parse.urlparse(url) + auth, host = netloc.split("@", 1) + auth = _expand_env_vars(credential=auth, url=url) + auth_header = _create_auth_header(credential=auth, url=url) + + url = urllib.parse.urlunparse((scheme, host, *rest)) + return url, auth_header + + +def _create_auth_header(credential: str, url: str) -> dict[str, str]: + """Create the Authorization header for basic or bearer authentication, depending on credential.""" + if ":" not in credential: + # We assume that the user is using a token. + log.debug(f"Using bearer token for authentication for {url}") + return {"Authorization": f"Bearer {credential}"} + + # Else, we assume that the user is using user:password. + user, pwd = credential.split(":", 1) + log.debug(f"Using basic authentication for {url}") + credentials = base64.encodebytes(f"{user}:{pwd}".encode()).decode().strip() + return {"Authorization": f"Basic {credentials}"} + + # This is mostly a copy of https://github.com/mkdocs/mkdocs/blob/master/mkdocs/utils/cache.py # In the future maybe they can be deduplicated. diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 00000000..b56e3d3c --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,103 @@ +"""Tests for the internal mkdocstrings _cache module.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +import pytest + +from mkdocstrings import _cache + +if TYPE_CHECKING: + from collections.abc import Mapping + + +@pytest.mark.parametrize( + ("credential", "expected", "env"), + [ + ("USER", "USER", {"USER": "testuser"}), + ("$USER", "$USER", {"USER": "testuser"}), + ("${USER", "${USER", {"USER": "testuser"}), + ("$USER}", "$USER}", {"USER": "testuser"}), + ("${TOKEN}", "testtoken", {"TOKEN": "testtoken"}), + ("${USER}:${PASSWORD}", "${USER}:testpass", {"PASSWORD": "testpass"}), + ("${USER}:${PASSWORD}", "testuser:testpass", {"USER": "testuser", "PASSWORD": "testpass"}), + ( + "user_prefix_${USER}_user_$uffix:pwd_prefix_${PASSWORD}_pwd_${uffix", + "user_prefix_testuser_user_$uffix:pwd_prefix_testpass_pwd_${uffix", + {"USER": "testuser", "PASSWORD": "testpass"}, + ), + ], +) +def test_expand_env_vars(credential: str, expected: str, env: Mapping[str, str]) -> None: + """Test expanding environment variables.""" + assert _cache._expand_env_vars(credential, url="https://test.example.com", env=env) == expected + + +def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) -> None: + """Test expanding environment variables with a missing environment variable.""" + caplog.set_level(logging.WARNING, logger="mkdocs.plugins.mkdocstrings._cache") + + credential = "${USER}" + env: dict[str, str] = {} + assert _cache._expand_env_vars(credential, url="https://test.example.com", env=env) == "${USER}" + + output = caplog.records[0].getMessage() + assert "'USER' is not set" in output + + +@pytest.mark.parametrize( + ("url", "expected_url"), + [ + ("http://host/path", "http://host/path"), + ("http://token@host/path", "http://host/path"), + ("http://${token}@host/path", "http://host/path"), + ("http://username:password@host/path", "http://host/path"), + ("http://username:${PASSWORD}@host/path", "http://host/path"), + ("http://${USERNAME}:${PASSWORD}@host/path", "http://host/path"), + ("http://prefix${USERNAME}suffix:prefix${PASSWORD}suffix@host/path", "http://host/path"), + ], +) +def test_extract_auth_from_url(monkeypatch: pytest.MonkeyPatch, url: str, expected_url: str) -> None: + """Test extracting the auth part from the URL.""" + monkeypatch.setattr(_cache, "_create_auth_header", lambda *args, **kwargs: {}) + result_url, _result_auth_header = _cache._extract_auth_from_url(url) + assert result_url == expected_url + + +def test_create_auth_header_basic_auth() -> None: + """Test creating the Authorization header for basic authentication.""" + auth_header = _cache._create_auth_header(credential="testuser:testpass", url="https://test.example.com") + assert auth_header == {"Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3M="} + + +def test_create_auth_header_bearer_auth() -> None: + """Test creating the Authorization header for bearer token authentication.""" + auth_header = _cache._create_auth_header(credential="token123", url="https://test.example.com") + assert auth_header == {"Authorization": "Bearer token123"} + + +@pytest.mark.parametrize( + ("var", "match"), + [ + ("${var}", "var"), + ("${VAR}", "VAR"), + ("${_}", "_"), + ("${_VAR}", "_VAR"), + ("${VAR123}", "VAR123"), + ("${VAR123_}", "VAR123_"), + ("VAR", None), + ("$1VAR", None), + ("${1VAR}", None), + ("${}", None), + ("${ }", None), + ], +) +def test_env_var_pattern(var: str, match: str | None) -> None: + """Test the environment variable regex pattern.""" + _match = _cache.ENV_VAR_PATTERN.match(var) + if _match is None: + assert match is _match + else: + assert _match.group(1) == match From 0bbb8caddf34b0a4faa0ed6f26e33102dc892fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 8 Nov 2024 17:52:41 +0100 Subject: [PATCH 102/223] refactor: Use %-formatting for logging messages In most cases: no impact. In some cases: performance improvement, since we then only compute an object's string representation if needed (if the message level is enabled). Typically useful for debug messages, which are often hidden and should avoid runtime costs. --- src/mkdocstrings/_cache.py | 12 ++++++------ src/mkdocstrings/extension.py | 11 +++++++---- src/mkdocstrings/plugin.py | 12 ++++++------ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/mkdocstrings/_cache.py b/src/mkdocstrings/_cache.py index d0481dd5..0bd0d90e 100644 --- a/src/mkdocstrings/_cache.py +++ b/src/mkdocstrings/_cache.py @@ -47,7 +47,7 @@ def replace_func(match: re.Match) -> str: try: return env[match.group(1)] except KeyError: - log.warning(f"Environment variable '{match.group(1)}' is not set, but is used in inventory URL {url}") + log.warning("Environment variable '%s' is not set, but is used in inventory URL %s", match.group(1), url) return match.group(0) return re.sub(ENV_VAR_PATTERN, replace_func, credential) @@ -72,12 +72,12 @@ def _create_auth_header(credential: str, url: str) -> dict[str, str]: """Create the Authorization header for basic or bearer authentication, depending on credential.""" if ":" not in credential: # We assume that the user is using a token. - log.debug(f"Using bearer token for authentication for {url}") + log.debug("Using bearer token authentication for %s", url) return {"Authorization": f"Bearer {credential}"} # Else, we assume that the user is using user:password. user, pwd = credential.split(":", 1) - log.debug(f"Using basic authentication for {url}") + log.debug("Using basic authentication for %s", url) credentials = base64.encodebytes(f"{user}:{pwd}".encode()).decode().strip() return {"Authorization": f"Basic {credentials}"} @@ -117,13 +117,13 @@ def download_and_cache_url( line = line[len(prefix) :] timestamp = int(line) if datetime.timedelta(seconds=(now - timestamp)) <= cache_duration: - log.debug(f"Using cached '{path}' for '{url}'") + log.debug("Using cached '%s' for '%s'", path, url) return f.read() except (OSError, ValueError) as e: - log.debug(f"{type(e).__name__}: {e}") + log.debug("%s: %s", type(e).__name__, e) # Download and cache the file - log.debug(f"Downloading '{url}' to '{path}'") + log.debug("Downloading '%s' to '%s'", url, path) content = download(url) os.makedirs(directory, exist_ok=True) with click.open_file(path, "wb", atomic=True) as f: diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index e0b88a08..266d642f 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -125,7 +125,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: if match: identifier = match["name"] heading_level = match["heading"].count("#") - log.debug(f"Matched '::: {identifier}'") + log.debug("Matched '::: %s'", identifier) html, handler, data = self._process_block(identifier, block, heading_level) el = Element("div", {"class": "mkdocstrings"}) @@ -201,7 +201,7 @@ def _process_block( config = yaml.safe_load(yaml_block) or {} handler_name = self._handlers.get_handler_name(config) - log.debug(f"Using handler '{handler_name}'") + log.debug("Using handler '%s'", handler_name) handler_config = self._handlers.get_handler_config(handler_name) handler = self._handlers.get_handler(handler_name, handler_config) @@ -217,7 +217,7 @@ def _process_block( try: data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: - log.error(str(exception)) # noqa: TRY400 + log.error("%s", exception) # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. @@ -231,7 +231,10 @@ def _process_block( except TemplateNotFound as exc: theme_name = self._config["theme_name"] log.error( # noqa: TRY400 - f"Template '{exc.name}' not found for '{handler_name}' handler and theme '{theme_name}'.", + "Template '%s' not found for '%s' handler and theme '%s'.", + exc.name, + handler_name, + theme_name, ) raise diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 12ed67b6..28060b6b 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -178,14 +178,14 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: try: # If autorefs plugin is explicitly enabled, just use it. autorefs = config.plugins["autorefs"] # type: ignore[assignment] - log.debug(f"Picked up existing autorefs instance {autorefs!r}") + log.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.scan_toc = False config.plugins["autorefs"] = autorefs - log.debug(f"Added a subdued autorefs instance {autorefs!r}") + log.debug("Added a subdued autorefs instance %r", autorefs) # Add collector-based fallback in either case. autorefs.get_fallback_anchor = self.handlers.get_anchors @@ -250,7 +250,7 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A write_file(inv_contents, os.path.join(config.site_dir, "objects.inv")) if self._inv_futures: - log.debug(f"Waiting for {len(self._inv_futures)} inventory download(s)") + log.debug("Waiting for %s inventory download(s)", len(self._inv_futures)) futures.wait(self._inv_futures, timeout=30) results = {} # Reversed order so that pages from first futures take precedence: @@ -260,7 +260,7 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A except Exception as error: # noqa: BLE001 loader, import_item = self._inv_futures[fut] loader_name = loader.__func__.__qualname__ - log.error(f"Couldn't load inventory {import_item} through {loader_name}: {error}") # noqa: TRY400 + log.error("Couldn't load inventory %s through %s: %s", import_item, loader_name, error) # noqa: TRY400 for page, identifier in results.items(): config.plugins["autorefs"].register_url(page, identifier) # type: ignore[attr-defined] self._inv_futures = {} @@ -319,8 +319,8 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - Returns: A mapping from identifier to absolute URL. """ - log.debug(f"Downloading inventory from {url!r}") + log.debug("Downloading inventory from %s", url) content = download_and_cache_url(url, download_url_with_gz, datetime.timedelta(days=1)) result = dict(loader(BytesIO(content), url=url, **kwargs)) - log.debug(f"Loaded inventory from {url!r}: {len(result)} items") + log.debug("Loaded inventory from %s: %s items", url, len(result)) return result From 5648e5aca80a5d8ba9e5456efb36b517b9f3cdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 8 Nov 2024 17:57:12 +0100 Subject: [PATCH 103/223] perf: Reduce footprint of template debug messages Templates usually log at least one message (that's what the Python handler does). It can easily mean thousands of log messages for even small code bases. Previously, each log message computed the template path, regardless of whether the message's level was enabled or not. Now, this template path is only computed if the message's level is enabled. That means we shave off thousands of (relatively expensive) function calls when MkDocs verbose mode isn't enabled. --- src/mkdocstrings/loggers.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index 008782ed..89f3d7f8 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -116,6 +116,27 @@ def __init__(self, logger: LoggerAdapter): self.critical = get_template_logger_function(logger.critical) +class _Lazy: + unset = object() + + def __init__(self, func: Callable, *args: Any, **kwargs: Any): + self.func = func + self.args = args + self.kwargs = kwargs + self.result = self.unset + + def __call__(self): + if self.result is self.unset: + self.result = self.func(*self.args, **self.kwargs) + return self.result + + def __str__(self) -> str: + return str(self()) + + def __repr__(self) -> str: + return repr(self()) + + def get_template_logger_function(logger_func: Callable) -> Callable: """Create a wrapper function that automatically receives the Jinja template context. @@ -127,7 +148,7 @@ def get_template_logger_function(logger_func: Callable) -> Callable: """ @pass_context - def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str: + def wrapper(context: Context, msg: str | None = None, *args: Any, **kwargs: Any) -> str: """Log a message. Arguments: @@ -138,8 +159,7 @@ def wrapper(context: Context, msg: str | None = None, **kwargs: Any) -> str: Returns: An empty string. """ - template_path = get_template_path(context) - logger_func(f"{template_path}: {msg or 'Rendering'}", **kwargs) + logger_func(f"%s: {msg or 'Rendering'}", _Lazy(get_template_path, context), *args, **kwargs) return "" return wrapper From e0af8006aeb51eeda5cbd54fdf44433199b2f81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 8 Nov 2024 18:07:24 +0100 Subject: [PATCH 104/223] chore: Prepare release 0.27.0 --- CHANGELOG.md | 17 +++++++++++++++++ config/git-changelog.toml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52ab88f..a062c06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,27 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.27.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.27.0) - 2024-11-08 + +[Compare with 0.26.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.2...0.27.0) + +### Features + +- Add support for authentication in inventory file URLs ([1c23c1b](https://github.com/mkdocstrings/mkdocstrings/commit/1c23c1b0fc4a9bdec5e0eb43c8647beab66fec55) by Stefan Mejlgaard). [Issue-707](https://github.com/mkdocstrings/mkdocstrings/issues/707), [PR-710](https://github.com/mkdocstrings/mkdocstrings/pull/710) + +### Performance Improvements + +- Reduce footprint of template debug messages ([5648e5a](https://github.com/mkdocstrings/mkdocstrings/commit/5648e5aca80a5d8ba9e5456efb36b517b9f3cdeb) by Timothรฉe Mazzucotelli). + +### Code Refactoring + +- Use %-formatting for logging messages ([0bbb8ca](https://github.com/mkdocstrings/mkdocstrings/commit/0bbb8caddf34b0a4faa0ed6f26e33102dc892fc8) by Timothรฉe Mazzucotelli). + ## [0.26.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.26.2) - 2024-10-12 [Compare with 0.26.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.1...0.26.2) diff --git a/config/git-changelog.toml b/config/git-changelog.toml index 57114e0c..e6bb5b91 100644 --- a/config/git-changelog.toml +++ b/config/git-changelog.toml @@ -4,6 +4,6 @@ in-place = true output = "CHANGELOG.md" parse-refs = false parse-trailers = true -sections = ["build", "deps", "feat", "fix", "refactor"] +sections = ["build", "deps", "feat", "fix", "perf", "refactor"] template = "keepachangelog" versioning = "pep440" From 6561ad5e1afa272c7cc0d53e33150fb75fe72e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 14 Nov 2024 13:32:17 +0100 Subject: [PATCH 105/223] chore: Fix docs-deploy duty to deploy to org-pages remote, not upstream --- duties.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/duties.py b/duties.py index 0f283217..eae95cc1 100644 --- a/duties.py +++ b/duties.py @@ -137,12 +137,12 @@ def docs_deploy(ctx: Context, *, force: bool = False) -> None: allow_overrides=False, ) ctx.run( - tools.mkdocs.gh_deploy(remote_name="upstream", force=True), + tools.mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation", ) elif force: ctx.run( - tools.mkdocs.gh_deploy(force=True), + tools.mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation", ) else: From 12c8f82e9a959ce32cada09f0d2b5c651a705fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 25 Nov 2024 15:08:12 +0100 Subject: [PATCH 106/223] fix: Fix broken table of contents when nesting autodoc instructions Issue-348: https://github.com/mkdocstrings/mkdocstrings/issues/348 --- src/mkdocstrings/extension.py | 101 +++++++++++++++++++----------- src/mkdocstrings/handlers/base.py | 17 +++++ tests/fixtures/nesting.py | 10 +++ tests/test_handlers.py | 41 ++++++++++++ 4 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 tests/fixtures/nesting.py diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 266d642f..bd20b48e 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -131,44 +131,9 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: 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) - # We need to duplicate the headings directly, just so 'toc' can pick them up, - # otherwise they wouldn't appear in the final table of contents. - # These headings are generated by the `BaseHandler.do_heading` method (Jinja filter), - # which runs in the inner Markdown conversion layer, and not in the outer one where we are now. - headings = handler.get_headings() - el.extend(headings) - # These duplicated headings will later be removed by our `_HeadingsPostProcessor` processor, - # which runs right after 'toc' (see `MkdocstringsExtension.extendMarkdown`). - - page = self._autorefs.current_page - if page is not None: - for heading in headings: - rendered_anchor = heading.attrib["id"] - self._autorefs.register_anchor(page, rendered_anchor) - - if "data-role" in heading.attrib: - self._handlers.inventory.register( - name=rendered_anchor, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=1, # register with standard priority - uri=f"{page}#{rendered_anchor}", - ) - - # also register other anchors for this object in the inventory - try: - data_object = handler.collect(rendered_anchor, handler.fallback_config) - except CollectionError: - continue - for anchor in handler.get_anchors(data_object): - if anchor not in self._handlers.inventory: - self._handlers.inventory.register( - name=anchor, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=2, # register with lower priority - uri=f"{page}#{rendered_anchor}", - ) + + if handler.outer_layer: + self._process_headings(handler, el) parent.append(el) @@ -240,6 +205,66 @@ def _process_block( return rendered, handler, data + def _process_headings(self, handler: BaseHandler, element: Element) -> None: + # We're in the outer handler layer, as well as the outer extension layer. + # + # The "handler layer" tracks the nesting of the autodoc blocks, which can appear in docstrings. + # + # - Render ::: Object1 # Outer handler layer + # - Render Object1's docstring # Outer handler layer + # - Docstring renders ::: Object2 # Inner handler layers + # - etc. # Inner handler layers + # + # The "extension layer" tracks whether we're converting an autodoc instruction + # or nested content within it, like docstrings. Markdown conversion within Markdown conversion. + # + # - Render ::: Object1 # Outer extension layer + # - Render Object1's docstring # Inner extension layer + # + # The generated HTML was just stashed, and the `toc` extension won't be able to see headings. + # We need to duplicate the headings directly, just so `toc` can pick them up, + # otherwise they wouldn't appear in the final table of contents. + # + # These headings are generated by the `BaseHandler.do_heading` method (Jinja filter), + # which runs in the inner extension layer, and not in the outer one where we are now. + headings = handler.get_headings() + element.extend(headings) + # These duplicated headings will later be removed by our `_HeadingsPostProcessor` processor, + # which runs right after `toc` (see `MkdocstringsExtension.extendMarkdown`). + + # If we were in an inner handler layer, we wouldn't do any of this + # and would just let headings bubble up to the outer handler layer. + + page = self._autorefs.current_page + if page is not None: + for heading in headings: + rendered_anchor = heading.attrib["id"] + self._autorefs.register_anchor(page, rendered_anchor) + + if "data-role" in heading.attrib: + self._handlers.inventory.register( + name=rendered_anchor, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=1, # Register with standard priority. + uri=f"{page}#{rendered_anchor}", + ) + + # Also register other anchors for this object in the inventory. + try: + data_object = handler.collect(rendered_anchor, handler.fallback_config) + except CollectionError: + continue + for anchor in handler.get_anchors(data_object): + if anchor not in self._handlers.inventory: + self._handlers.inventory.register( + name=anchor, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=2, # Register with lower priority. + uri=f"{page}#{rendered_anchor}", + ) + class _HeadingsPostProcessor(Treeprocessor): def run(self, root: Element) -> None: diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index d0b9456a..77e10388 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -41,6 +41,15 @@ CollectorItem = Any +# Autodoc instructions can appear in nested Markdown, +# so we need to keep track of the Markdown conversion layer we're in. +# Since any handler can be called from any Markdown conversion layer, +# we need to keep track of the layer globally. +# This global variable is incremented/decremented in `do_convert_markdown`, +# and used in `outer_layer`. +_markdown_conversion_layer: int = 0 + + class CollectionError(Exception): """An exception raised when some collection of data failed.""" @@ -252,6 +261,11 @@ def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 """ return () + @property + def outer_layer(self) -> bool: + """Whether we're in the outer Markdown conversion layer.""" + return _markdown_conversion_layer == 0 + def do_convert_markdown( self, text: str, @@ -272,6 +286,8 @@ def do_convert_markdown( Returns: An HTML string. """ + 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] @@ -288,6 +304,7 @@ def do_convert_markdown( treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined] self._md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] self._md.reset() + _markdown_conversion_layer -= 1 def do_heading( self, diff --git a/tests/fixtures/nesting.py b/tests/fixtures/nesting.py new file mode 100644 index 00000000..92f7a9ee --- /dev/null +++ b/tests/fixtures/nesting.py @@ -0,0 +1,10 @@ +class Class: + """A class. + + ## ::: tests.fixtures.nesting.Class.method + options: + show_root_heading: true + """ + + def method(self) -> None: + """A method.""" diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 4a07e98b..cea80657 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -2,6 +2,7 @@ from __future__ import annotations +from textwrap import dedent from typing import TYPE_CHECKING import pytest @@ -94,3 +95,43 @@ def test_extended_templates(tmp_path: Path, plugin: MkdocstringsPlugin) -> None: base_theme.mkdir() base_theme.joinpath("new.html").write_text("base new") assert handler.env.get_template("new.html").render() == "base new" + + +@pytest.mark.parametrize( + "ext_markdown", + [{"markdown_extensions": [{"toc": {"permalink": True}}]}], + indirect=["ext_markdown"], +) +def test_nested_autodoc(ext_markdown: Markdown) -> None: + """Assert that nested autodocs render well and do not mess up the TOC.""" + output = ext_markdown.convert( + dedent( + """ + # ::: tests.fixtures.nesting.Class + options: + members: false + show_root_heading: true + """, + ), + ) + 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] + { + "level": 1, + "id": "tests.fixtures.nesting.Class", + "html": "", + "name": "Class", + "data-toc-label": "Class", + "children": [ + { + "level": 2, + "id": "tests.fixtures.nesting.Class.method", + "html": "", + "name": "method", + "data-toc-label": "method", + "children": [], + }, + ], + }, + ] From c7b27fbefaf0e98b4b9d20bfa3ab86c35d756fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 17:23:51 +0100 Subject: [PATCH 107/223] docs: Add workaround to hide docstrings from source code blocks Issue-249: https://github.com/mkdocstrings/mkdocstrings/issues/249 --- docs/recipes.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/recipes.md b/docs/recipes.md index cb2d9eb1..72613965 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -405,3 +405,24 @@ Try to select the following code block's text: ... print(word, end=" ") Hello mkdocstrings! ``` + +## Hide documentation strings from source code blocks + +Since documentation strings are rendered by handlers, it can sometimes feel redundant to show these same documentation strings in source code blocks (when handlers render those). + +There is a general workaround to hide these docstrings from source blocks using CSS: + +```css +/* These CSS classes depend on the handler. */ +.doc-contents details .highlight code { + line-height: 0; +} +.doc-contents details .highlight code > * { + line-height: initial; +} +.doc-contents details .highlight code > .sd { /* Literal.String.Doc */ + display: none; +} +``` + +Note that this is considered a workaround and not a proper solution, because it has side-effects like also removing blank lines. From 326cccde2199adedeb9d720e8dd6fef1a958a3f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 19:18:49 +0100 Subject: [PATCH 108/223] docs: Hint at default language for syntax highlight of indented code blocks Issue-mkdocstrings/python#187: https://github.com/mkdocstrings/python/issues/187 --- docs/recipes.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/recipes.md b/docs/recipes.md index 72613965..1c5c6823 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -426,3 +426,29 @@ There is a general workaround to hide these docstrings from source blocks using ``` Note that this is considered a workaround and not a proper solution, because it has side-effects like also removing blank lines. + +## Automatic highlighting for indented code blocks in docstrings + +Depending on the language used in your code base and the mkdocstrings handler used to document it, you might want to set a default syntax for code blocks added to your docstrings. For example, to default to the Python syntax: + +```yaml title="mkdocs.yml" +markdown_extensions: +- pymdownx.highlight: + default_lang: python +``` + +Then in your docstrings, indented code blocks will be highlighted as Python code: + +``` +def my_function(): + """This is my function. + + The following code will be highlighted as Python: + + result = my_function() + print(result) + + End of the docstring. + """ + ... +``` From a17fc0316111c080c7e22b0a69b9f38e8641a785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 19:26:45 +0100 Subject: [PATCH 109/223] docs: Document limitation about footnotes in Python docstrings Issue-mkdocstrings/python#199: https://github.com/mkdocstrings/python/issues/199 --- docs/troubleshooting.md | 51 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index bc1da01b..66e93622 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -238,5 +238,56 @@ def my_function(*args, **kwargs): print(*args, **kwargs) ``` +## Footnotes do not render + +The library that parses docstrings, [Griffe](https://mkdocstrings.github.io/griffe/), splits docstrings in several "sections" (example: [Google-style sections syntax](https://mkdocstrings.github.io/griffe/reference/docstrings/#google-syntax)). If a footnote is used in a section, while referenced in another, mkdocstrings won't be able to render it correctly. The footnote and its reference must appear in the same section. + +```python +def my_function(): + """Summary. + + This is the first section[^1]. + + Note: + This is the second section[^2]. + + Note: + This is the third section[^3]. + + References at the end are part of yet another section (fourth here)[^4]. + + [^1]: Some text. + [^2]: Some text. + [^3]: Some text. + [^4]: Some text. + """ +``` + +Here only the fourth footnote will work, because it is the only one that appear in the same section as its reference. To fix this, make sure all footnotes appear in the same section as their references: + +```python +def my_function(): + """Summary. + + This is the first section[^1]. + + [^1]: Some text. + + Note: + This is the second section[^2]. + + [^2]: Some text. + + Note: + This is the third section[^3]. + + [^3]: Some text. + + References at the end are part of yet another section (fourth here)[^4]. + + [^4]: Some text. + """ +``` + [bugtracker]: https://github.com/mkdocstrings/mkdocstrings [markdown-katex]: https://gitlab.com/mbarkhau/markdown-katex From b4a70f041b927b3cee30f8524876ce445fa98699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 19:39:51 +0100 Subject: [PATCH 110/223] docs: Mention mkdocs-autoapi Issue-mkdocstrings/python#199: https://github.com/mkdocstrings/python/issues/199 --- docs/recipes.md | 2 ++ mkdocs.yml | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/recipes.md b/docs/recipes.md index 1c5c6823..7d0ab37d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -3,6 +3,8 @@ for *mkdocstrings* and more generally Markdown documentation. ## Automatic code reference pages +TIP: **[mkdocs-autoapi](https://github.com/jcayers20/mkdocs-autoapi) is a MkDocs plugin that automatically generates API documentation from your project's source code. It was inspired by the recipe below.** + *mkdocstrings* allows to inject documentation for any object into Markdown pages. But as the project grows, it quickly becomes quite tedious to keep the autodoc instructions, or even the dedicated diff --git a/mkdocs.yml b/mkdocs.yml index 3b4fefb2..610b224f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -100,7 +100,8 @@ extra_javascript: markdown_extensions: - attr_list - admonition -- callouts +- callouts: + strip_period: false - footnotes - pymdownx.details - pymdownx.emoji: From 9ca7ddeecff44b36df16b107d895ff994c636f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 19:40:05 +0100 Subject: [PATCH 111/223] chore: Update code block in docs --- docs/recipes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 7d0ab37d..6a81b090 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -441,7 +441,7 @@ markdown_extensions: Then in your docstrings, indented code blocks will be highlighted as Python code: -``` +```python def my_function(): """This is my function. @@ -452,5 +452,5 @@ def my_function(): End of the docstring. """ - ... + pass ``` From 15abd977f88c26f17a0c1f3f65677a536ff4dab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 20:11:50 +0100 Subject: [PATCH 112/223] docs: Fix heading level --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 66e93622..2027a094 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -238,7 +238,7 @@ def my_function(*args, **kwargs): print(*args, **kwargs) ``` -## Footnotes do not render +### Footnotes do not render The library that parses docstrings, [Griffe](https://mkdocstrings.github.io/griffe/), splits docstrings in several "sections" (example: [Google-style sections syntax](https://mkdocstrings.github.io/griffe/reference/docstrings/#google-syntax)). If a footnote is used in a section, while referenced in another, mkdocstrings won't be able to render it correctly. The footnote and its reference must appear in the same section. From 9fb89d829b8dc9f7928e27e16227cec6b7e71488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 3 Dec 2024 20:12:16 +0100 Subject: [PATCH 113/223] docs: Add troubleshooting note about `show_submodules` Issue-mkdocstrings/python#199: https://github.com/mkdocstrings/python/issues/199 --- docs/troubleshooting.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 2027a094..7548eafe 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -289,5 +289,18 @@ def my_function(): """ ``` +### Submodules are not rendered + +In previous versions of mkdocstrings-python, submodules were rendered by default. This was changed and you now need to set the following option: + +```yaml title="mkdocs.yml" +plugins: +- mkdocstrings: + handlers: + python: + options: + show_submodules: true +``` + [bugtracker]: https://github.com/mkdocstrings/mkdocstrings [markdown-katex]: https://gitlab.com/mbarkhau/markdown-katex From 2f6ddbe379ea8f1eabad1527f2e8e620c3b973d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 5 Dec 2024 16:05:48 +0100 Subject: [PATCH 114/223] docs: Add items to "Used by" README section --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index e9f9fbb2..8d4b8bb0 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,13 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo [Apache](https://streampipes.apache.org/docs/docs/python/latest/reference/client/client/), [FastAPI](https://fastapi.tiangolo.com/reference/fastapi/), [Google](https://docs.kidger.site/jaxtyping/api/runtime-type-checking/), +[IBM](https://ds4sd.github.io/docling/api_reference/document_converter/), [Jitsi](https://jitsi.github.io/jiwer/reference/alignment/), [Microsoft](https://microsoft.github.io/presidio/api/analyzer_python/), +[NVIDIA](https://nvidia.github.io/bionemo-framework/API_reference/bionemo/core/api/), [Prefect](https://docs.prefect.io/2.10.12/api-ref/prefect/agent/), [Pydantic](https://docs.pydantic.dev/dev-v2/api/main/), +[Textual](https://textual.textualize.io/api/app/), [and more...](https://github.com/mkdocstrings/mkdocstrings/network/dependents) ## Installation From b8e87036e0e1ec5c181b4a2ec5931f1a60636a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:20:36 +0100 Subject: [PATCH 115/223] refactor: Clean up data passed down from plugin to extension and handlers These changes straighten up what arguments are passed to the extension, the `Handlers` class, and the handlers' `get_handler` functions. Previously there was a lot of redundancy, building and passing mutable dictionaries around. Now we pass exactly what each class/function needs, as well as the global MkDocs config (renamed "tool config" to decouple ourselves a bit from MkDocs) in case users need it (`get_handler` functions and `update_env` methods, mainly). These changes bring a few deprecations with relevant messages, as well as a few breaking changes in mkdocstrings' API for which we didn't identify any public use on GitHub. PR-726: https://github.com/mkdocstrings/mkdocstrings/pull/726 --- config/pytest.ini | 11 +- src/mkdocstrings/extension.py | 21 +-- src/mkdocstrings/handlers/base.py | 254 ++++++++++++++++++++++-------- src/mkdocstrings/plugin.py | 29 ++-- 4 files changed, 219 insertions(+), 96 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index 1a0d99c6..eb7af02a 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -12,8 +12,9 @@ filterwarnings = error # TODO: remove once pytest-xdist 4 is released ignore:.*rsyncdir:DeprecationWarning:xdist - # TODO: remove once griffe and mkdocstrings-python release new versions - ignore:.*`get_logger`:DeprecationWarning:_griffe - ignore:.*`name`:DeprecationWarning:_griffe - ignore:.*Importing from `griffe:DeprecationWarning:mkdocstrings_handlers - ignore:.*`patch_loggers`:DeprecationWarning:_griffe + # TODO: remove once mkdocstrings-python releases a new version + ignore:.*`handler` argument:DeprecationWarning:mkdocstrings_handlers + ignore:.*`mdx` argument:DeprecationWarning:mkdocstrings_handlers + ignore:.*`mdx_config` argument:DeprecationWarning:mkdocstrings_handlers + ignore:.*`update_env\(md\)` parameter:DeprecationWarning:mkdocstrings + ignore:.*`super\(\).update_env\(\)` anymore:DeprecationWarning:mkdocstrings_handlers diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index bd20b48e..b9fc0a79 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -42,7 +42,6 @@ from collections.abc import MutableSequence from markdown import Markdown - from markdown.blockparser import BlockParser from mkdocs_autorefs.plugin import AutorefsPlugin @@ -63,24 +62,20 @@ class AutoDocProcessor(BlockProcessor): def __init__( self, - parser: BlockParser, md: Markdown, - config: dict, + *, handlers: Handlers, autorefs: AutorefsPlugin, ) -> None: """Initialize the object. Arguments: - parser: A `markdown.blockparser.BlockParser` instance. md: A `markdown.Markdown` instance. - config: The [configuration][mkdocstrings.plugin.PluginConfig] of the `mkdocstrings` plugin. handlers: The handlers container. autorefs: The autorefs plugin instance. """ - super().__init__(parser=parser) + super().__init__(parser=md.parser) self.md = md - self._config = config self._handlers = handlers self._autorefs = autorefs self._updated_envs: set = set() @@ -187,19 +182,18 @@ def _process_block( if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. log.debug("Updating handler's rendering env") - handler._update_env(self.md, self._config) + handler._update_env(self.md, config=self._handlers._tool_config) self._updated_envs.add(handler_name) log.debug("Rendering templates") try: rendered = handler.render(data, options) except TemplateNotFound as exc: - theme_name = self._config["theme_name"] log.error( # noqa: TRY400 "Template '%s' not found for '%s' handler and theme '%s'.", exc.name, handler_name, - theme_name, + self._handlers._theme, ) raise @@ -304,18 +298,15 @@ class MkdocstringsExtension(Extension): It cannot work outside of `mkdocstrings`. """ - def __init__(self, config: dict, handlers: Handlers, autorefs: AutorefsPlugin, **kwargs: Any) -> None: + def __init__(self, handlers: Handlers, autorefs: AutorefsPlugin, **kwargs: Any) -> None: """Initialize the object. Arguments: - config: The configuration items from `mkdocs` and `mkdocstrings` that must be passed to the block processor - when instantiated in [`extendMarkdown`][mkdocstrings.extension.MkdocstringsExtension.extendMarkdown]. handlers: The handlers container. autorefs: The autorefs plugin instance. **kwargs: Keyword arguments used by `markdown.extensions.Extension`. """ super().__init__(**kwargs) - self._config = config self._handlers = handlers self._autorefs = autorefs @@ -328,7 +319,7 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me md: A `markdown.Markdown` instance. """ md.parser.blockprocessors.register( - AutoDocProcessor(md.parser, md, self._config, self._handlers, self._autorefs), + AutoDocProcessor(md, handlers=self._handlers, autorefs=self._autorefs), "mkdocstrings", priority=75, # Right before markdown.blockprocessors.HashHeaderProcessor ) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 77e10388..427e5af9 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -6,9 +6,11 @@ from __future__ import annotations import importlib +import inspect import sys from pathlib import Path from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, cast +from warnings import warn from xml.etree.ElementTree import Element, tostring from jinja2 import Environment, FileSystemLoader @@ -34,11 +36,14 @@ from importlib.metadata import entry_points if TYPE_CHECKING: - from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence + from collections.abc import Iterable, Iterator, Mapping, Sequence + from markdown import Extension from mkdocs_autorefs.references import AutorefsHookInterface + CollectorItem = Any +HandlerConfig = Any # Autodoc instructions can appear in nested Markdown, @@ -89,44 +94,122 @@ class BaseHandler: To add custom CSS, add an `extra_css` variable or create an 'style.css' file beside the templates. """ - # TODO: Make name mandatory? - name: str = "" + # YORE: Bump 1: Replace ` = ""` with `` within line. + name: ClassVar[str] = "" """The handler's name, for example "python".""" - domain: str = "default" + + # YORE: Bump 1: Replace ` = ""` with `` within line. + domain: ClassVar[str] = "" """The handler's domain, used to register objects in the inventory, for example "py".""" - enable_inventory: bool = False + + enable_inventory: ClassVar[bool] = False """Whether the inventory creation is enabled.""" + fallback_config: ClassVar[dict] = {} """Fallback configuration when searching anchors for identifiers.""" - fallback_theme: str = "" + + fallback_theme: ClassVar[str] = "" """Fallback theme to use when a template isn't found in the configured theme.""" - extra_css = "" + + extra_css: str = "" """Extra CSS.""" - def __init__(self, handler: str, theme: str, custom_templates: str | None = None) -> None: + 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], + ) -> None: """Initialize the object. If the given theme is not supported (it does not exist), it will look for a `fallback_theme` attribute in `self` to use as a fallback theme. - Arguments: - handler: The name of the handler. - theme: The name of theme to use. - custom_templates: Directory containing custom templates. + Keyword Arguments: + theme (str): The theme to use. + custom_templates (str | None): The path to custom templates. + 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 + self.custom_templates = custom_templates + self.mdx = mdx + self.mdx_config = mdx_config + self._md: Markdown | None = None + self._headings: list[Element] = [] + paths = [] # add selected theme templates - themes_dir = self.get_templates_dir(handler) - paths.append(themes_dir / theme) + themes_dir = self.get_templates_dir(self.name) + paths.append(themes_dir / self.theme) # add extended theme templates - extended_templates_dirs = self.get_extended_templates_dirs(handler) + extended_templates_dirs = self.get_extended_templates_dirs(self.name) for templates_dir in extended_templates_dirs: - paths.append(templates_dir / theme) + paths.append(templates_dir / self.theme) # add fallback theme templates - if self.fallback_theme and self.fallback_theme != theme: + if self.fallback_theme and self.fallback_theme != self.theme: paths.append(themes_dir / self.fallback_theme) # add fallback theme of extended templates @@ -139,8 +222,8 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None self.extra_css += "\n" + css_path.read_text(encoding="utf-8") break - if custom_templates is not None: - paths.insert(0, Path(custom_templates) / handler / theme) + if self.custom_templates is not None: + paths.insert(0, Path(self.custom_templates) / self.name / self.theme) self.env = Environment( autoescape=True, @@ -150,8 +233,16 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None self.env.filters["any"] = do_any self.env.globals["log"] = get_template_logger(self.name) - self._headings: list[Element] = [] - self._md: Markdown = None # type: ignore[assignment] # To be populated in `update_env`. + @property + def md(self) -> Markdown: + """The Markdown instance. + + Raises: + RuntimeError: When the Markdown instance is not set yet. + """ + if self._md is None: + raise RuntimeError("Markdown instance not set yet") + return self._md @classmethod def load_inventory( @@ -174,7 +265,7 @@ def load_inventory( """ yield from () - def collect(self, identifier: str, config: MutableMapping[str, Any]) -> CollectorItem: + def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: """Collect data given an identifier and user configuration. In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into @@ -288,22 +379,22 @@ def do_convert_markdown( """ global _markdown_conversion_layer # noqa: PLW0603 _markdown_conversion_layer += 1 - treeprocessors = self._md.treeprocessors + 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] if autoref_hook: - self._md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined] + self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined] try: - return Markup(self._md.convert(text)) + 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] - self._md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] - self._md.reset() + self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] + self.md.reset() _markdown_conversion_layer -= 1 def do_heading( @@ -355,7 +446,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: @@ -381,29 +472,45 @@ def get_headings(self) -> Sequence[Element]: self._headings.clear() return result - def update_env(self, md: Markdown, config: dict) -> None: # noqa: ARG002 - """Update the Jinja environment. + # YORE: Bump 1: Replace `*args: Any, **kwargs: Any` with `config: Any`. + def update_env(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 + """Update the Jinja environment.""" + # YORE: Bump 1: Remove line. + warn("No need to call `super().update_env()` anymore.", DeprecationWarning, stacklevel=2) - Arguments: - md: The Markdown instance. Useful to add functions able to convert Markdown into the environment filters. - config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code - of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary. - """ - self._md = md - self.env.filters["highlight"] = Highlighter(md).highlight - self.env.filters["convert_markdown"] = self.do_convert_markdown - self.env.filters["heading"] = self.do_heading - - def _update_env(self, md: Markdown, config: dict) -> None: + 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`.""" - extensions = config["mdx"] + [MkdocstringsInnerExtension(self._headings)] + # 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) - new_md = Markdown(extensions=extensions, extension_configs=config["mdx_configs"]) # MkDocs adds its own (required) extension that's not part of the config. Propagate it. if "relpath" in md.treeprocessors: new_md.treeprocessors.register(md.treeprocessors["relpath"], "relpath", priority=0) - self.update_env(new_md, config) + self._md = new_md + + self.env.filters["highlight"] = Highlighter(new_md).highlight + self.env.filters["convert_markdown"] = self.do_convert_markdown + self.env.filters["heading"] = self.do_heading + + # 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) class Handlers: @@ -413,16 +520,42 @@ class Handlers: this for the purpose of caching. Use [mkdocstrings.plugin.MkdocstringsPlugin.get_handler][] for convenient access. """ - def __init__(self, config: dict) -> None: + def __init__( + self, + *, + theme: str, + default: str, + inventory_project: str, + inventory_version: str = "0.0.0", + handlers_config: dict[str, HandlerConfig] | None = None, + custom_templates: str | None = None, + mdx: Sequence[str | Extension] | None = None, + mdx_config: Mapping[str, Any] | None = None, + tool_config: Any, + ) -> None: """Initialize the object. Arguments: - config: Configuration options for `mkdocs` and `mkdocstrings`, read from `mkdocs.yml`. See the source code - of [mkdocstrings.plugin.MkdocstringsPlugin.on_config][] to see what's in this dictionary. + theme: The theme to use. + default: The default handler to use. + inventory_project: The project name to use in the inventory. + inventory_version: The project version to use in the inventory. + handlers_config: The handlers configuration. + custom_templates: The path to custom templates. + mdx: A list of Markdown extensions to use. + mdx_config: Configuration for the Markdown extensions. + tool_config: Tool configuration to pass down to handlers. """ - self._config = config + self._theme = theme + self._default = default + self._handlers_config = handlers_config or {} + self._custom_templates = custom_templates + self._mdx = mdx or [] + self._mdx_config = mdx_config or {} self._handlers: dict[str, BaseHandler] = {} - self.inventory: Inventory = Inventory(project=self._config["mkdocs"]["site_name"]) + self._tool_config = tool_config + + self.inventory: Inventory = Inventory(project=inventory_project, version=inventory_version) 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. @@ -452,10 +585,7 @@ def get_handler_name(self, config: dict) -> str: Returns: The name of the handler to use. """ - global_config = self._config["mkdocstrings"] - if "handler" in config: - return config["handler"] - return global_config["default_handler"] + return config.get("handler", self._default) def get_handler_config(self, name: str) -> dict: """Return the global configuration of the given handler. @@ -466,10 +596,7 @@ def get_handler_config(self, name: str) -> dict: Returns: The global configuration of the given handler. It can be an empty dictionary. """ - handlers = self._config["mkdocstrings"].get("handlers", {}) - if handlers: - return handlers.get(name, {}) - return {} + return self._handlers_config.get(name, None) or {} def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHandler: """Get a handler thanks to its name. @@ -489,14 +616,15 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand """ if name not in self._handlers: if handler_config is None: - handler_config = self.get_handler_config(name) - handler_config.update(self._config) + handler_config = self._handlers_config.get(name, {}) module = importlib.import_module(f"mkdocstrings_handlers.{name}") self._handlers[name] = module.get_handler( - theme=self._config["theme_name"], - custom_templates=self._config["mkdocstrings"]["custom_templates"], - config_file_path=self._config["mkdocs"]["config_file_path"], - **handler_config, + 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] diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 28060b6b..386fece3 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -156,8 +156,6 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: return config log.debug("Adding extension to the list") - theme_name = config.theme.name or os.path.dirname(config.theme.dirs[0]) - to_import: InventoryImportType = [] for handler_name, conf in self.config.handlers.items(): for import_item in conf.pop("import", ()): @@ -165,14 +163,17 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: import_item = {"url": import_item} # noqa: PLW2901 to_import.append((handler_name, import_item)) - extension_config = { - "theme_name": theme_name, - "mdx": config.markdown_extensions, - "mdx_configs": config.mdx_configs, - "mkdocstrings": self.config, - "mkdocs": config, - } - self._handlers = Handlers(extension_config) + handlers = Handlers( + default=self.config.default_handler, + handlers_config=self.config.handlers, + theme=config.theme.name or os.path.dirname(config.theme.dirs[0]), + 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. + tool_config=config, + ) autorefs: AutorefsPlugin try: @@ -187,18 +188,20 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: config.plugins["autorefs"] = autorefs log.debug("Added a subdued autorefs instance %r", autorefs) # Add collector-based fallback in either case. - autorefs.get_fallback_anchor = self.handlers.get_anchors + autorefs.get_fallback_anchor = handlers.get_anchors - mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs) + mkdocstrings_extension = MkdocstringsExtension(handlers, autorefs) config.markdown_extensions.append(mkdocstrings_extension) # type: ignore[arg-type] config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files. + self._handlers = handlers + self._inv_futures = {} if to_import: inv_loader = futures.ThreadPoolExecutor(4) for handler_name, import_item in to_import: - loader = self.get_handler(handler_name).load_inventory + loader = handlers.get_handler(handler_name).load_inventory future = inv_loader.submit( self._load_inventory, # type: ignore[misc] loader, From 4f4a40a564fabfcb7f0b5fda10e9d169888ebbdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:21:29 +0100 Subject: [PATCH 116/223] docs: Load Jinja and MarkupSafe object inventories --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 610b224f..0288678d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -143,6 +143,8 @@ plugins: - 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: From bb87cd833f2333e77cb2c2926aa24a434c97391f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:25:09 +0100 Subject: [PATCH 117/223] refactor: Use mkdocs-get-deps' download utility to remove duplicated code --- pyproject.toml | 3 +- src/mkdocstrings/_cache.py | 57 +------------------------------------- src/mkdocstrings/plugin.py | 7 +++-- 3 files changed, 7 insertions(+), 60 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 867747f8..9bb08588 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,13 +30,12 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "click>=7.0", "Jinja2>=2.11.1", "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.4", "mkdocs-autorefs>=1.2", - "platformdirs>=2.2", + "mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5. "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", "typing-extensions>=4.1; python_version < '3.10'", diff --git a/src/mkdocstrings/_cache.py b/src/mkdocstrings/_cache.py index 0bd0d90e..b9af327d 100644 --- a/src/mkdocstrings/_cache.py +++ b/src/mkdocstrings/_cache.py @@ -1,16 +1,11 @@ import base64 -import datetime import gzip -import hashlib import os import re import urllib.parse import urllib.request from collections.abc import Mapping -from typing import BinaryIO, Callable, Optional - -import click -import platformdirs +from typing import BinaryIO, Optional from mkdocstrings.loggers import get_logger @@ -80,53 +75,3 @@ def _create_auth_header(credential: str, url: str) -> dict[str, str]: log.debug("Using basic authentication for %s", url) credentials = base64.encodebytes(f"{user}:{pwd}".encode()).decode().strip() return {"Authorization": f"Basic {credentials}"} - - -# This is mostly a copy of https://github.com/mkdocs/mkdocs/blob/master/mkdocs/utils/cache.py -# In the future maybe they can be deduplicated. - - -def download_and_cache_url( - url: str, - download: Callable[[str], bytes], - cache_duration: datetime.timedelta, - comment: bytes = b"# ", -) -> bytes: - """Downloads a file from the URL, stores it under ~/.cache/, and returns its content. - - For tracking the age of the content, a prefix is inserted into the stored file, rather than relying on mtime. - - Args: - url: URL to use. - download: Callback that will accept the URL and actually perform the download. - cache_duration: How long to consider the URL content cached. - comment: The appropriate comment prefix for this file format. - """ - directory = os.path.join(platformdirs.user_cache_dir("mkdocs"), "mkdocstrings_url_cache") - name_hash = hashlib.sha256(url.encode()).hexdigest()[:32] - path = os.path.join(directory, name_hash + os.path.splitext(url)[1]) - - now = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) - prefix = b"%s%s downloaded at timestamp " % (comment, url.encode()) - # Check for cached file and try to return it - if os.path.isfile(path): - try: - with open(path, "rb") as f: - line = f.readline() - if line.startswith(prefix): - line = line[len(prefix) :] - timestamp = int(line) - if datetime.timedelta(seconds=(now - timestamp)) <= cache_duration: - log.debug("Using cached '%s' for '%s'", path, url) - return f.read() - except (OSError, ValueError) as e: - log.debug("%s: %s", type(e).__name__, e) - - # Download and cache the file - log.debug("Downloading '%s' to '%s'", url, path) - content = download(url) - os.makedirs(directory, exist_ok=True) - with click.open_file(path, "wb", atomic=True) as f: - f.write(b"%s%d\n" % (prefix, now)) - f.write(content) - return content diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 386fece3..97c7722f 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -29,7 +29,10 @@ from mkdocs.utils import write_file from mkdocs_autorefs.plugin import AutorefsConfig, AutorefsPlugin -from mkdocstrings._cache import download_and_cache_url, download_url_with_gz +# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. +from mkdocs_get_deps.cache import download_and_cache_url + +from mkdocstrings._cache import download_url_with_gz from mkdocstrings.extension import MkdocstringsExtension from mkdocstrings.handlers.base import BaseHandler, Handlers from mkdocstrings.loggers import get_logger @@ -323,7 +326,7 @@ def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) - A mapping from identifier to absolute URL. """ log.debug("Downloading inventory from %s", url) - content = download_and_cache_url(url, download_url_with_gz, datetime.timedelta(days=1)) + content = download_and_cache_url(url, datetime.timedelta(days=1), download=download_url_with_gz) result = dict(loader(BytesIO(content), url=url, **kwargs)) log.debug("Loaded inventory from %s: %s items", url, len(result)) return result From 310f7002a22f33786303279598a71d4b72277e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:25:40 +0100 Subject: [PATCH 118/223] docs: Update troubleshooting entry about Sphinx comments --- docs/troubleshooting.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7548eafe..d531997a 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -167,9 +167,9 @@ def math_function(x, y): ### My docstrings in comments (`#:`) are not picked up -It's because we do not support type annotations in comments. +We only support docstrings in comments through the [griffe-sphinx](https://mkdocstrings.github.io/griffe-sphinx) extension. -So instead of: +Alternatively, instead of: ```python import enum @@ -187,15 +187,11 @@ import enum class MyEnum(enum.Enum): - """My enum. - - Attributes: - v1: The first choice. - v2: The second choice. - """ - v1 = 1 + """The first choice.""" + v2 = 2 + """The second choice.""" ``` Or: @@ -205,11 +201,15 @@ import enum class MyEnum(enum.Enum): - v1 = 1 - """The first choice.""" + """My enum. + + Attributes: + v1: The first choice. + v2: The second choice. + """ + v1 = 1 v2 = 2 - """The second choice.""" ``` ### My wrapped function shows documentation/code for its wrapper instead of its own From 06adb0ce07aed6baaedeb64bf2e9a39d9b3fed64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:26:29 +0100 Subject: [PATCH 119/223] style: Move TYPE_CHECKING block below at the end of imports --- src/mkdocstrings/plugin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 97c7722f..03d34df9 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -37,15 +37,16 @@ from mkdocstrings.handlers.base import BaseHandler, Handlers from mkdocstrings.loggers import get_logger -if TYPE_CHECKING: - from jinja2.environment import Environment - from mkdocs.config.defaults import MkDocsConfig - if sys.version_info < (3, 10): from typing_extensions import ParamSpec else: from typing import ParamSpec +if TYPE_CHECKING: + from jinja2.environment import Environment + from mkdocs.config.defaults import MkDocsConfig + + log = get_logger(__name__) InventoryImportType = list[tuple[str, Mapping[str, Any]]] From 434d8c7cd1e3edbdb9d4c45a9b44b290b19d88f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Jan 2025 01:44:21 +0100 Subject: [PATCH 120/223] refactor: Register all identifiers of rendered objects into autorefs Previously we only registered in the inventory, because autorefs didn't let us register an "alias" anchor. See commit c7ac04324d005d9cf7d2c1f3b2c39f212275d451. Now that autorefs supports Markdown anchors, we can actually use that mechanism to register different identifiers that point to the same anchor. See commit a215a97a057b54e11ebec8865c64e93429edde63. --- pyproject.toml | 2 +- src/mkdocstrings/extension.py | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9bb08588..d05d0e35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.4", - "mkdocs-autorefs>=1.2", + "mkdocs-autorefs>=1.3", "mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5. "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index b9fc0a79..3d2dc3a9 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -233,7 +233,20 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: if page is not None: for heading in headings: rendered_anchor = heading.attrib["id"] - self._autorefs.register_anchor(page, rendered_anchor) + self._autorefs.register_anchor(page, rendered_anchor, primary=True) + + # Register all identifiers for this object + # both in the autorefs plugin and in the inventory. + try: + data_object = handler.collect(rendered_anchor, handler.fallback_config) + except CollectionError: + anchors = () + else: + anchors = handler.get_anchors(data_object) + + for anchor in anchors: + if anchor != rendered_anchor: + self._autorefs.register_anchor(page, anchor, rendered_anchor, primary=False) if "data-role" in heading.attrib: self._handlers.inventory.register( @@ -243,13 +256,7 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: priority=1, # Register with standard priority. uri=f"{page}#{rendered_anchor}", ) - - # Also register other anchors for this object in the inventory. - try: - data_object = handler.collect(rendered_anchor, handler.fallback_config) - except CollectionError: - continue - for anchor in handler.get_anchors(data_object): + for anchor in anchors: if anchor not in self._handlers.inventory: self._handlers.inventory.register( name=anchor, From d2e9c21318a4ca5c9eaece2f60331b016036d343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 17:27:04 +0100 Subject: [PATCH 121/223] style: Attach comments together --- src/mkdocstrings/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 3d2dc3a9..28b2af13 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -225,7 +225,7 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: element.extend(headings) # These duplicated headings will later be removed by our `_HeadingsPostProcessor` processor, # which runs right after `toc` (see `MkdocstringsExtension.extendMarkdown`). - + # # If we were in an inner handler layer, we wouldn't do any of this # and would just let headings bubble up to the outer handler layer. From 7a668f0f731401b07123bd02aafbbfc55cd24c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 17:46:18 +0100 Subject: [PATCH 122/223] refactor: Deprecate base handler's `get_anchors` method in favor of `get_aliases` method First, `get_anchors` accepts a data object as argument. This forces mkdocstrings to (re)collect this data object through the handler's `collect` method just to pass it again to the handler. Why not giving back control to the handler itself? For example, the Python handler would not call `collect` again, but just look into its cached objects collection. Second, "anchors" in this context doesn't make much sense, so we rename the method `get_aliases`, because it really returns aliases of a given identifier, not "HTML anchors". Plus this makes the deprecation easier (method rename instead of signature change). --- config/pytest.ini | 1 + src/mkdocstrings/extension.py | 45 ++++++++++++++++++++----------- src/mkdocstrings/handlers/base.py | 25 +++++++++++------ 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index eb7af02a..edcaffb5 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -18,3 +18,4 @@ filterwarnings = ignore:.*`mdx_config` argument:DeprecationWarning:mkdocstrings_handlers ignore:.*`update_env\(md\)` parameter:DeprecationWarning:mkdocstrings ignore:.*`super\(\).update_env\(\)` anymore:DeprecationWarning:mkdocstrings_handlers + ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 28b2af13..74e03530 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -26,6 +26,7 @@ import re from collections import ChainMap from typing import TYPE_CHECKING, Any +from warnings import warn from xml.etree.ElementTree import Element import yaml @@ -232,38 +233,50 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: page = self._autorefs.current_page if page is not None: for heading in headings: - rendered_anchor = heading.attrib["id"] - self._autorefs.register_anchor(page, rendered_anchor, primary=True) + rendered_id = heading.attrib["id"] + self._autorefs.register_anchor(page, rendered_id, primary=True) # Register all identifiers for this object # both in the autorefs plugin and in the inventory. - try: - data_object = handler.collect(rendered_anchor, handler.fallback_config) - except CollectionError: - anchors = () + 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, handler.fallback_config) + except CollectionError: + aliases = () + else: + aliases = handler.get_anchors(data_object) else: - anchors = handler.get_anchors(data_object) + aliases = handler.get_aliases(rendered_id) - for anchor in anchors: - if anchor != rendered_anchor: - self._autorefs.register_anchor(page, anchor, rendered_anchor, primary=False) + for alias in aliases: + if alias != rendered_id: + self._autorefs.register_anchor(page, alias, rendered_id, primary=False) if "data-role" in heading.attrib: self._handlers.inventory.register( - name=rendered_anchor, + name=rendered_id, domain=handler.domain, role=heading.attrib["data-role"], priority=1, # Register with standard priority. - uri=f"{page}#{rendered_anchor}", + uri=f"{page}#{rendered_id}", ) - for anchor in anchors: - if anchor not in self._handlers.inventory: + for alias in aliases: + if alias not in self._handlers.inventory: self._handlers.inventory.register( - name=anchor, + name=alias, domain=handler.domain, role=heading.attrib["data-role"], priority=2, # Register with lower priority. - uri=f"{page}#{rendered_anchor}", + uri=f"{page}#{rendered_id}", ) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 427e5af9..10f2a9a3 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -341,14 +341,14 @@ def get_extended_templates_dirs(self, handler: str) -> list[Path]: discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates") return [extension.load()() for extension in discovered_extensions] - def get_anchors(self, data: CollectorItem) -> tuple[str, ...]: # noqa: ARG002 - """Return the possible identifiers (HTML anchors) for a collected item. + def get_aliases(self, identifier: str) -> tuple[str, ...]: # noqa: ARG002 + """Return the possible aliases for a given identifier. Arguments: - data: The collected data. + identifier: The identifier to get the aliases of. Returns: - The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor. + A tuple of strings - aliases. """ return () @@ -567,13 +567,22 @@ def get_anchors(self, identifier: str) -> tuple[str, ...]: 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(): - fallback_config = getattr(handler, "fallback_config", {}) try: - anchors = handler.get_anchors(handler.collect(identifier, fallback_config)) + 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, handler.fallback_config)) + else: + aliases = handler.get_aliases(identifier) except CollectionError: continue - if anchors: - return anchors + if aliases: + return aliases return () def get_handler_name(self, config: dict) -> str: From f80ef5d98b82d9b97c467825ce606f1e4a7110c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 17:47:07 +0100 Subject: [PATCH 123/223] tests: Assert identifier aliases are registered early in mkdocs-autorefs --- tests/test_extension.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_extension.py b/tests/test_extension.py index 976f376c..5dc84382 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -154,16 +154,16 @@ def test_use_custom_handler(ext_markdown: Markdown) -> None: ext_markdown.convert("::: tests.fixtures.headings\n handler: not_here") -def test_dont_register_every_identifier_as_anchor(plugin: MkdocstringsPlugin, 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] ids = ("id1", "id2", "id3") handler.get_anchors = lambda _: ids # type: ignore[method-assign] - ext_markdown.convert("::: tests.fixtures.headings") autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] + autorefs.current_page = "foo" + ext_markdown.convert("::: tests.fixtures.headings") for identifier in ids: - assert identifier not in autorefs._url_map - assert identifier not in autorefs._abs_url_map + assert identifier in autorefs._secondary_url_map def test_use_options_yaml_key(ext_markdown: Markdown) -> None: From c00de7a42b9072cbaa47ecbf18e3e15a6d5ab634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 22:30:12 +0100 Subject: [PATCH 124/223] refactor: Give back control to handlers on how they want to handle global/local options Issue-719: https://github.com/mkdocstrings/mkdocstrings/issues/719 --- src/mkdocstrings/extension.py | 29 +++++++++++++++++++---------- src/mkdocstrings/handlers/base.py | 24 ++++++++++++++++++++---- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 74e03530..ab7e12b5 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -24,7 +24,6 @@ from __future__ import annotations import re -from collections import ChainMap from typing import TYPE_CHECKING, Any from warnings import warn from xml.etree.ElementTree import Element @@ -159,20 +158,30 @@ def _process_block( Returns: Rendered HTML, the handler that was used, and the collected item. """ - config = yaml.safe_load(yaml_block) or {} - handler_name = self._handlers.get_handler_name(config) + local_config = yaml.safe_load(yaml_block) or {} + handler_name = self._handlers.get_handler_name(local_config) log.debug("Using handler '%s'", handler_name) - handler_config = self._handlers.get_handler_config(handler_name) - handler = self._handlers.get_handler(handler_name, handler_config) - - global_options = handler_config.get("options", {}) - local_options = config.get("options", {}) - options = ChainMap(local_options, global_options) + handler = self._handlers.get_handler(handler_name) + local_options = local_config.get("options", {}) if heading_level: # Heading level obtained from Markdown (`##`) takes precedence. - options = ChainMap({"heading_level": heading_level}, options) + local_options["heading_level"] = heading_level + + # YORE: Bump 1: Replace block with line 2. + if handler.get_options is not BaseHandler.get_options: + 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} log.debug("Collecting data") try: diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 10f2a9a3..2d2e8c66 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -44,6 +44,7 @@ CollectorItem = Any HandlerConfig = Any +HandlerOptions = Any # Autodoc instructions can appear in nested Markdown, @@ -265,7 +266,22 @@ def load_inventory( """ yield from () - def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: + def get_options(self, local_options: Mapping[str, Any]) -> HandlerOptions: + """Get combined options. + + Override this method to customize how options are combined, + for example by merging the global options with the local options. + By combining options here, you don't have to do it twice in `collect` and `render`. + + Arguments: + local_options: The local options. + + Returns: + The combined options. + """ + return local_options + + def collect(self, identifier: str, options: HandlerOptions) -> CollectorItem: """Collect data given an identifier and user configuration. In the implementation, you typically call a subprocess that returns JSON, and load that JSON again into @@ -275,19 +291,19 @@ def collect(self, identifier: str, config: Mapping[str, Any]) -> CollectorItem: identifier: An identifier for which to collect data. For example, in Python, it would be 'mkdocstrings.handlers' to collect documentation about the handlers module. It can be anything that you can feed to the tool of your choice. - config: The handler's configuration options. + options: The final configuration options. Returns: Anything you want, as long as you can feed it to the handler's `render` method. """ raise NotImplementedError - def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: + def render(self, data: CollectorItem, options: HandlerOptions) -> str: """Render a template using provided data and configuration options. Arguments: data: The collected data to render. - config: The handler's configuration options. + options: The final configuration options. Returns: The rendered template as HTML. From b3f4d38da5c23cc4f31b0647464d0dddf6e2dc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 22:30:51 +0100 Subject: [PATCH 125/223] style: Declare filters earlier, no need to wait for these ones --- src/mkdocstrings/handlers/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 2d2e8c66..7f637700 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -231,6 +231,8 @@ def __init__( loader=FileSystemLoader(paths), auto_reload=False, # Editing a template in the middle of a build is not useful. ) + 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) @@ -513,8 +515,6 @@ def _update_env(self, md: Markdown, *, config: Any | None = None) -> None: self._md = new_md self.env.filters["highlight"] = Highlighter(new_md).highlight - self.env.filters["convert_markdown"] = self.do_convert_markdown - self.env.filters["heading"] = self.do_heading # YORE: Bump 1: Replace block with `self.update_env(config)`. parameters = inspect.signature(self.update_env).parameters From 29b5398de45acfa875428f607b656863c28a83af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 10 Jan 2025 22:31:59 +0100 Subject: [PATCH 126/223] chore: Add Yore comments to stop using autorefs' fallback mechanism in v1 --- src/mkdocstrings/handlers/base.py | 1 + src/mkdocstrings/plugin.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 7f637700..aa924910 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -573,6 +573,7 @@ def __init__( self.inventory: Inventory = Inventory(project=inventory_project, version=inventory_version) + # 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. diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 03d34df9..844ee0f4 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -191,7 +191,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: autorefs.scan_toc = False config.plugins["autorefs"] = autorefs log.debug("Added a subdued autorefs instance %r", autorefs) - # Add collector-based fallback in either case. + # YORE: Bump 1: Remove line. autorefs.get_fallback_anchor = handlers.get_anchors mkdocstrings_extension = MkdocstringsExtension(handlers, autorefs) From 8450426aad6f609536b619d08b57fb5e1974ffcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 11 Jan 2025 00:43:30 +0100 Subject: [PATCH 127/223] tests: Update test for previous refactor --- tests/test_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_extension.py b/tests/test_extension.py index 5dc84382..c3784b2e 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -158,7 +158,7 @@ def test_register_every_identifier_alias(plugin: MkdocstringsPlugin, ext_markdow """Assert that we don't preemptively register all identifiers of a rendered object.""" handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] ids = ("id1", "id2", "id3") - handler.get_anchors = lambda _: ids # type: ignore[method-assign] + handler.get_aliases = lambda _: ids # type: ignore[method-assign] autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] autorefs.current_page = "foo" ext_markdown.convert("::: tests.fixtures.headings") From 649c221bc560ef093229f2c6723c08656dcb39f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 11 Jan 2025 00:53:07 +0100 Subject: [PATCH 128/223] chore: Template upgrade --- .copier-answers.yml | 2 +- docs/insiders/index.md | 2 + pyproject.toml | 18 ++-- scripts/gen_credits.py | 2 +- scripts/get_version.py | 27 ++++++ scripts/make | 191 +---------------------------------------- scripts/make.py | 191 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 232 insertions(+), 201 deletions(-) create mode 100644 scripts/get_version.py mode change 100755 => 120000 scripts/make create mode 100755 scripts/make.py diff --git a/.copier-answers.yml b/.copier-answers.yml index 6a772352..b3437df7 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: 1.5.2 +_commit: 1.5.6 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/docs/insiders/index.md b/docs/insiders/index.md index ccbca99a..daa4731c 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -97,6 +97,8 @@ else: ``` +Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management](https://pawamoy.github.io/backlog/). + ## How to become a sponsor Thanks for your interest in sponsoring! In order to become an eligible sponsor diff --git a/pyproject.toml b/pyproject.toml index d05d0e35..49b3b818 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,12 +59,12 @@ Funding = "https://github.com/sponsors/pawamoy" [project.entry-points."mkdocs.plugins"] mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin" -[tool.pdm] -version = {source = "scm"} +[tool.pdm.version] +source = "call" +getter = "scripts.get_version:get_version" [tool.pdm.build] -package-dir = "src" -editable-backend = "editables" +# Include as much as possible in the source distribution, to help redistributors. excludes = ["**/.pytest_cache"] source-includes = [ "config", @@ -79,15 +79,15 @@ source-includes = [ ] [tool.pdm.build.wheel-data] +# Manual pages can be included in the wheel. +# Depending on the installation tool, they will be accessible to users. +# pipx supports it, uv does not yet, see https://github.com/astral-sh/uv/issues/4731. data = [ {path = "share/**/*", relative-to = "."}, ] -[tool.uv] -dev-dependencies = [ - # dev - "editables>=0.5", - +[dependency-groups] +dev = [ # maintenance "build>=1.2", "git-changelog>=2.5", diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index bd2dcbf2..721ac05d 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -27,7 +27,7 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -devdeps = [dep for dep in pyproject["tool"]["uv"]["dev-dependencies"] if not dep.startswith("-e")] +devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")] PackageMetadata = dict[str, Union[str, Iterable[str]]] Metadata = dict[str, PackageMetadata] diff --git a/scripts/get_version.py b/scripts/get_version.py new file mode 100644 index 00000000..f4a30a8c --- /dev/null +++ b/scripts/get_version.py @@ -0,0 +1,27 @@ +"""Get current project version from Git tags or changelog.""" + +import re +from contextlib import suppress +from pathlib import Path + +from pdm.backend.hooks.version import SCMVersion, Version, default_version_formatter, get_version_from_scm + +_root = Path(__file__).parent.parent +_changelog = _root / "CHANGELOG.md" +_changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") +_default_scm_version = SCMVersion(Version("0.0.0"), None, False, None, None) # noqa: FBT003 + + +def get_version() -> str: + """Get current project version from Git tags or changelog.""" + scm_version = get_version_from_scm(_root) or _default_scm_version + if scm_version.version <= Version("0.1"): # Missing Git tags? + with suppress(OSError, StopIteration): # noqa: SIM117 + with _changelog.open("r", encoding="utf8") as file: + match = next(filter(None, map(_changelog_version_re.match, file))) + scm_version = scm_version._replace(version=Version(match.group(1))) + return default_version_formatter(scm_version) + + +if __name__ == "__main__": + print(get_version()) diff --git a/scripts/make b/scripts/make deleted file mode 100755 index ac430624..00000000 --- a/scripts/make +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env python3 -"""Management commands.""" - -from __future__ import annotations - -import os -import shutil -import subprocess -import sys -from contextlib import contextmanager -from pathlib import Path -from textwrap import dedent -from typing import Any, Iterator - -PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() - - -def shell(cmd: str, capture_output: bool = False, **kwargs: Any) -> str | None: - """Run a shell command.""" - if capture_output: - return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 - subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 - return None - - -@contextmanager -def environ(**kwargs: str) -> Iterator[None]: - """Temporarily set environment variables.""" - original = dict(os.environ) - os.environ.update(kwargs) - try: - yield - finally: - os.environ.clear() - os.environ.update(original) - - -def uv_install(venv: Path) -> None: - """Install dependencies using uv.""" - with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): - if "CI" in os.environ: - shell("uv sync --no-editable") - else: - shell("uv sync") - - -def setup() -> None: - """Setup the project.""" - if not shutil.which("uv"): - raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") - - print("Installing dependencies (default environment)") # noqa: T201 - default_venv = Path(".venv") - if not default_venv.exists(): - shell("uv venv --python python") - uv_install(default_venv) - - if PYTHON_VERSIONS: - for version in PYTHON_VERSIONS: - print(f"\nInstalling dependencies (python{version})") # noqa: T201 - venv_path = Path(f".venvs/{version}") - if not venv_path.exists(): - shell(f"uv venv --python {version} {venv_path}") - with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): - uv_install(venv_path) - - -def run(version: str, cmd: str, *args: str, no_sync: bool = False, **kwargs: Any) -> None: - """Run a command in a virtual environment.""" - kwargs = {"check": True, **kwargs} - uv_run = ["uv", "run"] - if no_sync: - uv_run.append("--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 - - -def multirun(cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command for all configured Python versions.""" - if PYTHON_VERSIONS: - for version in PYTHON_VERSIONS: - run(version, cmd, *args, **kwargs) - else: - run("default", cmd, *args, **kwargs) - - -def allrun(cmd: str, *args: str, **kwargs: Any) -> None: - """Run a command in all virtual environments.""" - run("default", cmd, *args, **kwargs) - if PYTHON_VERSIONS: - multirun(cmd, *args, **kwargs) - - -def clean() -> None: - """Delete build artifacts and cache files.""" - paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] - for path in paths_to_clean: - shell(f"rm -rf {path}") - - cache_dirs = {".cache", ".pytest_cache", ".mypy_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) - - -def vscode() -> None: - """Configure VSCode to work on this project.""" - Path(".vscode").mkdir(parents=True, exist_ok=True) - shell("cp -v config/vscode/* .vscode") - - -def main() -> int: - """Main entry point.""" - args = list(sys.argv[1:]) - if not args or args[0] == "help": - if len(args) > 1: - run("default", "duty", "--help", args[1]) - else: - print( - dedent( - """ - Available commands - help Print this help. Add task name to print help. - setup Setup all virtual environments (install dependencies). - run Run a command in the default virtual environment. - multirun Run a command for all configured Python versions. - allrun Run a command in all virtual environments. - 3.x Run a command in the virtual environment for Python 3.x. - clean Delete build artifacts and cache files. - vscode Configure VSCode to work on this project. - """ - ), - flush=True, - ) # noqa: T201 - if os.path.exists(".venv"): - print("\nAvailable tasks", flush=True) # noqa: T201 - run("default", "duty", "--list", no_sync=True) - return 0 - - while args: - cmd = args.pop(0) - - if cmd == "run": - run("default", *args) - return 0 - - if cmd == "multirun": - multirun(*args) - return 0 - - if cmd == "allrun": - allrun(*args) - return 0 - - if cmd.startswith("3."): - run(cmd, *args) - return 0 - - opts = [] - while args and (args[0].startswith("-") or "=" in args[0]): - opts.append(args.pop(0)) - - if cmd == "clean": - clean() - elif cmd == "setup": - setup() - elif cmd == "vscode": - vscode() - elif cmd == "check": - multirun("duty", "check-quality", "check-types", "check-docs") - run("default", "duty", "check-api") - elif cmd in {"check-quality", "check-docs", "check-types", "test"}: - multirun("duty", cmd, *opts) - else: - run("default", "duty", cmd, *opts) - - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except subprocess.CalledProcessError as process: - if process.output: - print(process.output, file=sys.stderr) # noqa: T201 - sys.exit(process.returncode) diff --git a/scripts/make b/scripts/make new file mode 120000 index 00000000..c2eda0df --- /dev/null +++ b/scripts/make @@ -0,0 +1 @@ +make.py \ No newline at end of file diff --git a/scripts/make.py b/scripts/make.py new file mode 100755 index 00000000..3d427296 --- /dev/null +++ b/scripts/make.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Management commands.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from contextlib import contextmanager +from pathlib import Path +from textwrap import dedent +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Iterator + + +PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() + + +def shell(cmd: str, *, capture_output: bool = False, **kwargs: Any) -> str | None: + """Run a shell command.""" + if capture_output: + return subprocess.check_output(cmd, shell=True, text=True, **kwargs) # noqa: S602 + subprocess.run(cmd, shell=True, check=True, stderr=subprocess.STDOUT, **kwargs) # noqa: S602 + return None + + +@contextmanager +def environ(**kwargs: str) -> Iterator[None]: + """Temporarily set environment variables.""" + original = dict(os.environ) + os.environ.update(kwargs) + try: + yield + finally: + os.environ.clear() + os.environ.update(original) + + +def uv_install(venv: Path) -> None: + """Install dependencies using uv.""" + with environ(UV_PROJECT_ENVIRONMENT=str(venv), PYO3_USE_ABI3_FORWARD_COMPATIBILITY="1"): + if "CI" in os.environ: + shell("uv sync --no-editable") + else: + shell("uv sync") + + +def setup() -> None: + """Setup the project.""" + if not shutil.which("uv"): + raise ValueError("make: setup: uv must be installed, see https://github.com/astral-sh/uv") + + print("Installing dependencies (default environment)") + default_venv = Path(".venv") + if not default_venv.exists(): + shell("uv venv") + uv_install(default_venv) + + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + print(f"\nInstalling dependencies (python{version})") + venv_path = Path(f".venvs/{version}") + if not venv_path.exists(): + shell(f"uv venv --python {version} {venv_path}") + with environ(UV_PROJECT_ENVIRONMENT=str(venv_path.resolve())): + uv_install(venv_path) + + +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 + + +def multirun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command for all configured Python versions.""" + if PYTHON_VERSIONS: + for version in PYTHON_VERSIONS: + run(version, cmd, *args, **kwargs) + else: + run("default", cmd, *args, **kwargs) + + +def allrun(cmd: str, *args: str, **kwargs: Any) -> None: + """Run a command in all virtual environments.""" + run("default", cmd, *args, **kwargs) + if PYTHON_VERSIONS: + multirun(cmd, *args, **kwargs) + + +def clean() -> None: + """Delete build artifacts and cache files.""" + paths_to_clean = ["build", "dist", "htmlcov", "site", ".coverage*", ".pdm-build"] + 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("*/"): + if dirpath.parts[0] not in (".venv", ".venvs") and dirpath.name in cache_dirs: + shutil.rmtree(dirpath, ignore_errors=True) + + +def vscode() -> None: + """Configure VSCode to work on this project.""" + shutil.copytree("config/vscode", ".vscode", dirs_exist_ok=True) + + +def main() -> int: + """Main entry point.""" + args = list(sys.argv[1:]) + if not args or args[0] == "help": + if len(args) > 1: + run("default", "duty", "--help", args[1]) + else: + print( + dedent( + """ + Available commands + help Print this help. Add task name to print help. + setup Setup all virtual environments (install dependencies). + run Run a command in the default virtual environment. + multirun Run a command for all configured Python versions. + allrun Run a command in all virtual environments. + 3.x Run a command in the virtual environment for Python 3.x. + clean Delete build artifacts and cache files. + vscode Configure VSCode to work on this project. + """, + ), + flush=True, + ) + if os.path.exists(".venv"): + print("\nAvailable tasks", flush=True) + run("default", "duty", "--list") + return 0 + + while args: + cmd = args.pop(0) + + if cmd == "run": + run("default", *args) + return 0 + + if cmd == "multirun": + multirun(*args) + return 0 + + if cmd == "allrun": + allrun(*args) + return 0 + + if cmd.startswith("3."): + run(cmd, *args) + return 0 + + opts = [] + while args and (args[0].startswith("-") or "=" in args[0]): + opts.append(args.pop(0)) + + if cmd == "clean": + clean() + elif cmd == "setup": + setup() + elif cmd == "vscode": + vscode() + elif cmd == "check": + multirun("duty", "check-quality", "check-types", "check-docs") + run("default", "duty", "check-api") + elif cmd in {"check-quality", "check-docs", "check-types", "test"}: + multirun("duty", cmd, *opts) + else: + run("default", "duty", cmd, *opts) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as process: + if process.output: + print(process.output, file=sys.stderr) + sys.exit(process.returncode) From 095d2f3e42351340aa3759289515ffc13d2f8a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 22:55:50 +0100 Subject: [PATCH 129/223] docs: Update docs for creating custom handlers --- docs/usage/handlers.md | 48 ++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index 37da4b67..dcf4c5e3 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -14,13 +14,10 @@ A handler is what makes it possible to collect and render documentation for a pa ## About the Python handlers -Since version 0.18, a new, experimental Python handler is available. +Since version 0.18, a new Python handler is available. It is based on [Griffe](https://github.com/mkdocstrings/griffe), which is an improved version of [pytkdocs](https://github.com/mkdocstrings/pytkdocs). -Note that the experimental handler does not yet support all third-party libraries -that the legacy handler supported. - If you want to keep using the legacy handler as long as possible, you can depend on `mkdocstrings-python-legacy` directly, or specify the `python-legacy` extra when depending on *mkdocstrings*: @@ -37,9 +34,9 @@ dependencies = [ The legacy handler will continue to "work" for many releases, as long as the new handler does not cover all previous use-cases. -### Migrate to the experimental Python handler +### Migrate to the new Python handler -To use the new, experimental Python handler, +To use the new Python handler, you can depend on `mkdocstrings-python` directly, or specify the `python` extra when depending on *mkdocstrings*: @@ -131,26 +128,41 @@ NOTE: **Note the absence of `__init__.py` module in `mkdocstrings_handlers`!** ### Code A handler is a subclass of the base handler provided by *mkdocstrings*. - See the documentation for the [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler]. -Subclasses of the base handler must implement the `collect` and `render` methods at least. -The `collect` method is responsible for collecting and returning data (extracting -documentation from source code, loading introspecting objects in memory, other sources? etc.) -while the `render` method is responsible for actually rendering the data to HTML, -using the Jinja templates provided by your package. -You must implement a `get_handler` method at the module level. +Subclasses of the base handler must declare a `name` and `domain` as class attributes, +as well as implement the following methods: + +- `collect(identifier, options)` (**required**): method responsible for collecting and returning data (extracting + documentation from source code, loading introspecting objects in memory, other sources? etc.) +- `render(identifier, options)` (**required**): method responsible for actually rendering the data to HTML, + using the Jinja templates provided by your package. +- `get_options(local_options)` (**required**): method responsible for combining global options with local ones. +- `get_aliases(identifier)` (**recommended**): method responsible for returning known aliases of object identifiers, + in order to register cross-references in the autorefs plugin. +- `get_inventory_urls()` (optional): method responsible for returning a list of URLs to download (object inventories) + along with configuration options (for loading the inventory with `load_inventory`). +- `load_inventory(in_file, url, **options)` (optional): method responsible for loading an inventory (binary file-handle) + and yielding tuples of identifiers and URLs. +- `update_env(config)` (optional): Gives you a chance to customize the Jinja environment used to render templates, + for examples by adding/removing Jinja filters and global context variables. +- `teardown()` (optional): Clean up / teardown anything that needs it at the end of the build. + +You must implement a `get_handler` method at the module level, +which returns an instance of your handler. This function takes the following parameters: - `theme` (string, theme name) - `custom_templates` (optional string, path to custom templates directory) -- `config_file_path` (optional string, path to the config file) +- `mdx` (list, Markdown extensions) +- `mdx_config` (dict, extensions configuration) +- `handler_config` (dict, handle configuration) +- `tool_config` (dict, the whole MkDocs configuration) These arguments are all passed as keyword arguments, so you can ignore them -by adding `**kwargs` or similar to your signature. You can also accept -additional parameters: the handler's global-only options and/or the root -config options. This gives flexibility and access to the mkdocs config, mkdocstring -config etc.. You should never modify the root config but can use it to get +by adding `**kwargs` or similar to your signature. + +You should not modify the MkDocs config but can use it to get information about the MkDocs instance such as where the current `site_dir` lives. See the [Mkdocs Configuration](https://www.mkdocs.org/user-guide/configuration/) for more info about what is accessible from it. From 8eb71e373ba14a6d1964c1a279d3a54373094be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 22:57:39 +0100 Subject: [PATCH 130/223] chore: Fix detection of `get_options` user implementation --- src/mkdocstrings/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index ab7e12b5..93fd5b20 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -170,7 +170,7 @@ def _process_block( local_options["heading_level"] = heading_level # YORE: Bump 1: Replace block with line 2. - if handler.get_options is not BaseHandler.get_options: + if handler.get_options.__func__ is not BaseHandler.get_options: # type: ignore[attr-defined] options = handler.get_options(local_options) else: warn( From 848d331f981eea085d68345d1592af2abec9d567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 22:59:27 +0100 Subject: [PATCH 131/223] chore: Mark `fallback_config` with Yore removal comment --- src/mkdocstrings/extension.py | 2 +- src/mkdocstrings/handlers/base.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 93fd5b20..4dcbdf99 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -258,7 +258,7 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: stacklevel=1, ) try: - data_object = handler.collect(rendered_id, handler.fallback_config) + data_object = handler.collect(rendered_id, getattr(handler, "fallback_config", {})) except CollectionError: aliases = () else: diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index aa924910..d4428d91 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -106,6 +106,7 @@ class BaseHandler: 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.""" @@ -593,7 +594,7 @@ def get_anchors(self, identifier: str) -> tuple[str, ...]: DeprecationWarning, stacklevel=1, ) - aliases = handler.get_anchors(handler.collect(identifier, handler.fallback_config)) + aliases = handler.get_anchors(handler.collect(identifier, getattr(handler, "fallback_config", {}))) else: aliases = handler.get_aliases(identifier) except CollectionError: From b84653f2b175824c73bd0291fafff8343ba80125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 23:26:11 +0100 Subject: [PATCH 132/223] refactor: Give back inventory control to handlers With this change, handlers gain back control on inventory configuration. They can expose which URLs to download, and which options to use when loading inventories through their existing `load_inventory` method, thanks to the new `get_inventory_urls` method. Handlers will have to store inventory configuration instead of relying on mkdocstrings' hardcoded use of an `import` setting. This change also moves inventory-related code into the `Handlers` class, and renames the `_cache` module to `_download`. Related-to-issue-719: https://github.com/mkdocstrings/mkdocstrings/issues/719 --- src/mkdocstrings/{_cache.py => _download.py} | 0 src/mkdocstrings/handlers/base.py | 70 +++++++++++++++++- src/mkdocstrings/plugin.py | 74 ++------------------ tests/{test_cache.py => test_download.py} | 20 +++--- tests/test_inventory.py | 12 ---- 5 files changed, 85 insertions(+), 91 deletions(-) rename src/mkdocstrings/{_cache.py => _download.py} (100%) rename tests/{test_cache.py => test_download.py} (80%) diff --git a/src/mkdocstrings/_cache.py b/src/mkdocstrings/_download.py similarity index 100% rename from src/mkdocstrings/_cache.py rename to src/mkdocstrings/_download.py diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index d4428d91..c245e1b4 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -5,9 +5,12 @@ from __future__ import annotations +import datetime import importlib import inspect import sys +from concurrent import futures +from io import BytesIO from pathlib import Path from typing import TYPE_CHECKING, Any, BinaryIO, ClassVar, cast from warnings import warn @@ -19,6 +22,10 @@ from markupsafe import Markup from mkdocs_autorefs.references import AutorefsInlineProcessor +# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. +from mkdocs_get_deps.cache import download_and_cache_url + +from mkdocstrings._download import download_url_with_gz from mkdocstrings.handlers.rendering import ( HeadingShiftingTreeprocessor, Highlighter, @@ -27,7 +34,7 @@ ParagraphStrippingTreeprocessor, ) from mkdocstrings.inventory import Inventory -from mkdocstrings.loggers import get_template_logger +from mkdocstrings.loggers import get_logger, get_template_logger # TODO: remove once support for Python 3.9 is dropped if sys.version_info < (3, 10): @@ -41,6 +48,7 @@ from markdown import Extension from mkdocs_autorefs.references import AutorefsHookInterface +log = get_logger(__name__) CollectorItem = Any HandlerConfig = Any @@ -248,6 +256,10 @@ def md(self) -> Markdown: raise RuntimeError("Markdown instance not set yet") return self._md + def get_inventory_urls(self) -> list[tuple[str, dict[str, Any]]]: + """Return the URLs (and configuration options) of the inventory files to download.""" + return [] + @classmethod def load_inventory( cls, @@ -574,6 +586,8 @@ def __init__( self.inventory: Inventory = Inventory(project=inventory_project, version=inventory_version) + 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. @@ -655,6 +669,58 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand ) return self._handlers[name] + def _download_inventories(self) -> None: + """Download an inventory file from an URL. + + Arguments: + url: The URL of the inventory. + """ + to_download: list[tuple[BaseHandler, str, Any]] = [] + + 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 inv_configs := conf.pop("import", ()): + warn( + "mkdocstrings v1 will stop handling 'import' in handlers configuration. " + "Instead your handler must define a `get_inventory_urls` method " + "that returns a list of URLs to download. ", + DeprecationWarning, + stacklevel=1, + ) + inv_configs = [{"url": inv} if isinstance(inv, str) else inv for inv in inv_configs] + inv_configs = [(inv.pop("url"), inv) for inv in inv_configs] + else: + inv_configs = handler.get_inventory_urls() + + to_download.extend((handler, url, conf) for url, conf in inv_configs) + + if to_download: + thread_pool = futures.ThreadPoolExecutor(4) + for handler, url, conf in to_download: + log.debug("Downloading inventory from %s", url) + future = thread_pool.submit( + download_and_cache_url, + url, + datetime.timedelta(days=1), + download=download_url_with_gz, + ) + self._inv_futures[future] = (handler, url, conf) + thread_pool.shutdown(wait=False) + + def _yield_inventory_items(self) -> Iterator[tuple[str, str]]: + if self._inv_futures: + log.debug("Waiting for %s inventory download(s)", len(self._inv_futures)) + futures.wait(self._inv_futures, timeout=30) + # Reversed order so that pages from first futures take precedence: + 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 + log.error("Couldn't load inventory %s through handler '%s': %s", conf, handler.name, error) # noqa: TRY400 + self._inv_futures = {} + @property def seen_handlers(self) -> Iterable[BaseHandler]: """Get the handlers that were encountered so far throughout the build. @@ -667,6 +733,8 @@ def seen_handlers(self) -> Iterable[BaseHandler]: def teardown(self) -> None: """Teardown all cached handlers and clear the cache.""" + for future in self._inv_futures: + future.cancel() for handler in self.seen_handlers: handler.teardown() self._handlers.clear() diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 844ee0f4..8f931fef 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -14,13 +14,9 @@ from __future__ import annotations -import datetime -import functools import os import sys from collections.abc import Iterable, Mapping -from concurrent import futures -from io import BytesIO from typing import TYPE_CHECKING, Any, Callable, TypeVar from mkdocs.config import Config @@ -29,10 +25,6 @@ from mkdocs.utils import write_file from mkdocs_autorefs.plugin import AutorefsConfig, AutorefsPlugin -# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. -from mkdocs_get_deps.cache import download_and_cache_url - -from mkdocstrings._cache import download_url_with_gz from mkdocstrings.extension import MkdocstringsExtension from mkdocstrings.handlers.base import BaseHandler, Handlers from mkdocstrings.loggers import get_logger @@ -160,13 +152,6 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: return config log.debug("Adding extension to the list") - to_import: InventoryImportType = [] - for handler_name, conf in self.config.handlers.items(): - for import_item in conf.pop("import", ()): - if isinstance(import_item, str): - import_item = {"url": import_item} # noqa: PLW2901 - to_import.append((handler_name, import_item)) - handlers = Handlers( default=self.config.default_handler, handlers_config=self.config.handlers, @@ -179,6 +164,8 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: tool_config=config, ) + handlers._download_inventories() + autorefs: AutorefsPlugin try: # If autorefs plugin is explicitly enabled, just use it. @@ -200,20 +187,6 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files. self._handlers = handlers - - self._inv_futures = {} - if to_import: - inv_loader = futures.ThreadPoolExecutor(4) - for handler_name, import_item in to_import: - loader = handlers.get_handler(handler_name).load_inventory - future = inv_loader.submit( - self._load_inventory, # type: ignore[misc] - loader, - **import_item, - ) - self._inv_futures[future] = (loader, import_item) - inv_loader.shutdown(wait=False) - return config @property @@ -247,6 +220,7 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A """ if not self.plugin_enabled: return + if 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)) @@ -256,21 +230,9 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A inv_contents = self.handlers.inventory.format_sphinx() write_file(inv_contents, os.path.join(config.site_dir, "objects.inv")) - if self._inv_futures: - log.debug("Waiting for %s inventory download(s)", len(self._inv_futures)) - futures.wait(self._inv_futures, timeout=30) - results = {} - # Reversed order so that pages from first futures take precedence: - for fut in reversed(list(self._inv_futures)): - try: - results.update(fut.result()) - except Exception as error: # noqa: BLE001 - loader, import_item = self._inv_futures[fut] - loader_name = loader.__func__.__qualname__ - log.error("Couldn't load inventory %s through %s: %s", import_item, loader_name, error) # noqa: TRY400 - for page, identifier in results.items(): - config.plugins["autorefs"].register_url(page, identifier) # type: ignore[attr-defined] - self._inv_futures = {} + register = config.plugins["autorefs"].register_url # type: ignore[attr-defined] + for identifier, url in self._handlers._yield_inventory_items(): + register(identifier, url) def on_post_build( self, @@ -293,9 +255,6 @@ def on_post_build( if not self.plugin_enabled: return - for future in self._inv_futures: - future.cancel() - if self._handlers: log.debug("Tearing handlers down") self.handlers.teardown() @@ -310,24 +269,3 @@ def get_handler(self, handler_name: str) -> BaseHandler: An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler]. """ return self.handlers.get_handler(handler_name) - - @classmethod - # lru_cache does not allow mutable arguments such lists, but that is what we load from YAML config. - @list_to_tuple - @functools.cache - def _load_inventory(cls, loader: InventoryLoaderType, url: str, **kwargs: Any) -> Mapping[str, str]: - """Download and process inventory files using a handler. - - Arguments: - loader: A function returning a sequence of pairs (identifier, url). - url: The URL to download and process. - **kwargs: Extra arguments to pass to the loader. - - Returns: - A mapping from identifier to absolute URL. - """ - log.debug("Downloading inventory from %s", url) - content = download_and_cache_url(url, datetime.timedelta(days=1), download=download_url_with_gz) - result = dict(loader(BytesIO(content), url=url, **kwargs)) - log.debug("Loaded inventory from %s: %s items", url, len(result)) - return result diff --git a/tests/test_cache.py b/tests/test_download.py similarity index 80% rename from tests/test_cache.py rename to tests/test_download.py index b56e3d3c..95dc0233 100644 --- a/tests/test_cache.py +++ b/tests/test_download.py @@ -1,4 +1,4 @@ -"""Tests for the internal mkdocstrings _cache module.""" +"""Tests for the internal mkdocstrings _download module.""" from __future__ import annotations @@ -7,7 +7,7 @@ import pytest -from mkdocstrings import _cache +from mkdocstrings import _download if TYPE_CHECKING: from collections.abc import Mapping @@ -32,16 +32,16 @@ ) def test_expand_env_vars(credential: str, expected: str, env: Mapping[str, str]) -> None: """Test expanding environment variables.""" - assert _cache._expand_env_vars(credential, url="https://test.example.com", env=env) == expected + assert _download._expand_env_vars(credential, url="https://test.example.com", env=env) == expected def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) -> None: """Test expanding environment variables with a missing environment variable.""" - caplog.set_level(logging.WARNING, logger="mkdocs.plugins.mkdocstrings._cache") + caplog.set_level(logging.WARNING, logger="mkdocs.plugins.mkdocstrings._download") credential = "${USER}" env: dict[str, str] = {} - assert _cache._expand_env_vars(credential, url="https://test.example.com", env=env) == "${USER}" + assert _download._expand_env_vars(credential, url="https://test.example.com", env=env) == "${USER}" output = caplog.records[0].getMessage() assert "'USER' is not set" in output @@ -61,20 +61,20 @@ def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) ) def test_extract_auth_from_url(monkeypatch: pytest.MonkeyPatch, url: str, expected_url: str) -> None: """Test extracting the auth part from the URL.""" - monkeypatch.setattr(_cache, "_create_auth_header", lambda *args, **kwargs: {}) - result_url, _result_auth_header = _cache._extract_auth_from_url(url) + monkeypatch.setattr(_download, "_create_auth_header", lambda *args, **kwargs: {}) + result_url, _result_auth_header = _download._extract_auth_from_url(url) assert result_url == expected_url def test_create_auth_header_basic_auth() -> None: """Test creating the Authorization header for basic authentication.""" - auth_header = _cache._create_auth_header(credential="testuser:testpass", url="https://test.example.com") + auth_header = _download._create_auth_header(credential="testuser:testpass", url="https://test.example.com") assert auth_header == {"Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3M="} def test_create_auth_header_bearer_auth() -> None: """Test creating the Authorization header for bearer token authentication.""" - auth_header = _cache._create_auth_header(credential="token123", url="https://test.example.com") + auth_header = _download._create_auth_header(credential="token123", url="https://test.example.com") assert auth_header == {"Authorization": "Bearer token123"} @@ -96,7 +96,7 @@ def test_create_auth_header_bearer_auth() -> None: ) def test_env_var_pattern(var: str, match: str | None) -> None: """Test the environment variable regex pattern.""" - _match = _cache.ENV_VAR_PATTERN.match(var) + _match = _download.ENV_VAR_PATTERN.match(var) if _match is None: assert match is _match else: diff --git a/tests/test_inventory.py b/tests/test_inventory.py index ce707296..ecbb3cd2 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -5,7 +5,6 @@ import sys from io import BytesIO from os.path import join -from typing import TYPE_CHECKING import pytest from mkdocs.commands.build import build @@ -13,8 +12,6 @@ from mkdocstrings.inventory import Inventory, InventoryItem -if TYPE_CHECKING: - from mkdocstrings.plugin import MkdocstringsPlugin sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") @@ -58,12 +55,3 @@ 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}"] - - -def test_load_inventory(plugin: MkdocstringsPlugin) -> None: - """Test the plugin inventory loading method. - - Parameters: - plugin: A mkdocstrings plugin instance. - """ - plugin._load_inventory(loader=lambda *args, **kwargs: (), url="https://example.com", domains=["a", "b"]) # type: ignore[misc,arg-type] From 8fa19891a4813d0b84cf3a0dc86e91a6590af2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 23:26:26 +0100 Subject: [PATCH 133/223] tests: Fix test while waiting for mkdocstrings-python release --- tests/test_extension.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_extension.py b/tests/test_extension.py index c3784b2e..b7f1685f 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -158,6 +158,8 @@ def test_register_every_identifier_alias(plugin: MkdocstringsPlugin, ext_markdow """Assert that we don't preemptively register all identifiers of a rendered object.""" handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] ids = ("id1", "id2", "id3") + # TODO: Remove line when Python handler removes its `get_anchors` method. + handler.get_anchors = lambda _: ids # type: ignore[union-attr] handler.get_aliases = lambda _: ids # type: ignore[method-assign] autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] autorefs.current_page = "foo" From 9383fecf8312e032835733b6d406ac14ce00af32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 23:26:34 +0100 Subject: [PATCH 134/223] tests: Ignore deprecation warnings --- config/pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pytest.ini b/config/pytest.ini index edcaffb5..459c99de 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -19,3 +19,5 @@ filterwarnings = ignore:.*`update_env\(md\)` parameter:DeprecationWarning:mkdocstrings ignore:.*`super\(\).update_env\(\)` anymore:DeprecationWarning:mkdocstrings_handlers ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings + ignore:.*fallback anchor function:DeprecationWarning:mkdocstrings + ignore:.*v1.*`get_options` method:DeprecationWarning:mkdocstrings \ No newline at end of file From 4105a92b2cb9e0cefd80068ca7d22bf66f5445a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 12 Jan 2025 23:34:36 +0100 Subject: [PATCH 135/223] style: Format --- scripts/insiders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/insiders.py b/scripts/insiders.py index 849c6314..a7da99bc 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -26,7 +26,7 @@ def human_readable_amount(amount: int) -> str: # noqa: D103 str_amount = str(amount) if len(str_amount) >= 4: # noqa: PLR2004 - return f"{str_amount[:len(str_amount)-3]},{str_amount[-3:]}" + return f"{str_amount[: len(str_amount) - 3]},{str_amount[-3:]}" return str_amount From 060e437485fa5026d70c941a17df4049ebbb904e Mon Sep 17 00:00:00 2001 From: Jeremy Feng <44312563+jeremy-feng@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:30:32 +0800 Subject: [PATCH 136/223] docs: Fix link to arithmatex docs in Material for MkDocs --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d531997a..5e5386e3 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -145,7 +145,7 @@ See [Python handler: Finding modules](https://mkdocstrings.github.io/python/usag ### LaTeX in docstrings is not rendered correctly If you are using a Markdown extension like -[Arithmatex Mathjax](https://squidfunk.github.io/mkdocs-material/extensions/pymdown/#arithmatex-mathjax) +[Arithmatex Mathjax](https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/#arithmatex) or [`markdown-katex`][markdown-katex] to render LaTeX, add `r` in front of your docstring to make sure nothing is escaped. You'll still maybe have to play with escaping to get things right. From 48625a2d37c90653ac275031dc8a3cf1c607408b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 19 Jan 2025 14:20:12 +0100 Subject: [PATCH 137/223] chore: Fix inventory loading error log message --- src/mkdocstrings/handlers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index c245e1b4..c9436ab1 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -718,7 +718,7 @@ def _yield_inventory_items(self) -> Iterator[tuple[str, str]]: try: yield from handler.load_inventory(BytesIO(fut.result()), url, **conf) except Exception as error: # noqa: BLE001 - log.error("Couldn't load inventory %s through handler '%s': %s", conf, handler.name, error) # noqa: TRY400 + log.error("Couldn't load inventory %s through handler '%s': %s", url, handler.name, error) # noqa: TRY400 self._inv_futures = {} @property From 8c476ee0b82c09a5b20d7a773ecaf4be17b9e4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 30 Jan 2025 17:46:44 +0100 Subject: [PATCH 138/223] refactor: Pass `config_file_path` to `get_handler` if it expects it --- config/pytest.ini | 3 ++- src/mkdocstrings/handlers/base.py | 37 ++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index 459c99de..b54cfdfa 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -20,4 +20,5 @@ filterwarnings = ignore:.*`super\(\).update_env\(\)` anymore:DeprecationWarning:mkdocstrings_handlers ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings ignore:.*fallback anchor function:DeprecationWarning:mkdocstrings - ignore:.*v1.*`get_options` method:DeprecationWarning:mkdocstrings \ No newline at end of file + ignore:.*v1.*`get_options` method:DeprecationWarning:mkdocstrings + ignore:.*`config_file_path` argument:DeprecationWarning:mkdocstrings diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index c9436ab1..6929c0c1 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -659,14 +659,35 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand if handler_config is None: handler_config = self._handlers_config.get(name, {}) module = importlib.import_module(f"mkdocstrings_handlers.{name}") - 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, - ) + + # 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, + # ) return self._handlers[name] def _download_inventories(self) -> None: From 52ea2f216b6481c625c2ee15333cb64ec386b370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 30 Jan 2025 17:58:30 +0100 Subject: [PATCH 139/223] ci: Upgrade dev-deps to prevent warnings in tests --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 49b3b818..87368ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,8 +115,8 @@ dev = [ "mkdocs-literate-nav>=0.6", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", - "mkdocs-redirects>=1.2", - "mkdocstrings[python]>=0.25", + "mkdocs-redirects>=1.2.1", + "mkdocstrings-python>=1.13", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] \ No newline at end of file From 9618e17a66cd6bf3cdf4445dddde97c1680039a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 31 Jan 2025 13:11:37 +0100 Subject: [PATCH 140/223] docs: Mention mkdocs-api-autonav in recipe --- docs/recipes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/recipes.md b/docs/recipes.md index 6a81b090..a52347bd 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -3,7 +3,7 @@ for *mkdocstrings* and more generally Markdown documentation. ## Automatic code reference pages -TIP: **[mkdocs-autoapi](https://github.com/jcayers20/mkdocs-autoapi) is a MkDocs plugin that automatically generates API documentation from your project's source code. It was inspired by the recipe below.** +TIP: **[mkdocs-autoapi](https://github.com/jcayers20/mkdocs-autoapi) and [mkdocs-api-autonav](https://github.com/tlambert03/mkdocs-api-autonav) are MkDocs plugins that automatically generate API documentation from your project's source code. They were inspired by the recipe below.** *mkdocstrings* allows to inject documentation for any object into Markdown pages. But as the project grows, it quickly becomes @@ -366,16 +366,16 @@ extra_css: > To target `pycon` code blocks more specifically, you can configure the > `pymdownx.highlight` extension to use Pygments and set language classes > on code blocks: -> +> > ```yaml title="mkdocs.yml" > markdown_extensions: > - pymdownx.highlight: > use_pygments: true > pygments_lang_class: true > ``` -> +> > Then you can update the CSS selector like this: -> +> > ```css title="docs/css/code_select.css" > .language-pycon .gp, .language-pycon .go { /* Generic.Prompt, Generic.Output */ > user-select: none; From f3ecf5868c33944b344a31a2423952d7f4eb1952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 16:53:26 +0100 Subject: [PATCH 141/223] chore: Ignore autorefs fallback deprecation warning --- src/mkdocstrings/plugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 8f931fef..9962f48d 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -18,6 +18,7 @@ import sys from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, Callable, TypeVar +from warnings import catch_warnings, simplefilter from mkdocs.config import Config from mkdocs.config import config_options as opt @@ -178,8 +179,10 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: autorefs.scan_toc = False config.plugins["autorefs"] = autorefs log.debug("Added a subdued autorefs instance %r", autorefs) - # YORE: Bump 1: Remove line. - autorefs.get_fallback_anchor = handlers.get_anchors + # 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] From 3cf7d51704378adc50d4ea50080aacae39e0e731 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Mon, 3 Feb 2025 10:57:09 -0500 Subject: [PATCH 142/223] fix: Update handlers in JSON schema to be an object instead of an array Issue-733: https://github.com/mkdocstrings/mkdocstrings/issues/733 PR-734: https://github.com/mkdocstrings/mkdocstrings/pull/734 --- docs/schema.json | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/schema.json b/docs/schema.json index a74dabf3..7632af66 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -37,15 +37,11 @@ "handlers": { "title": "The handlers global configuration.", "markdownDescription": "https://mkdocstrings.github.io/handlers/overview/", - "type": "object", - "default": null, - "items": { - "oneOf": [ - { - "$ref": "https://raw.githubusercontent.com/mkdocstrings/python/master/docs/schema.json" - } - ] - } + "anyOf": [ + { + "$ref": "https://raw.githubusercontent.com/mkdocstrings/python/main/docs/schema.json" + } + ] } }, "additionalProperties": false From 6ef141222d0b5ad47ced9049472243cf5887ec0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 17:01:00 +0100 Subject: [PATCH 143/223] chore: Prepare release 0.28.0 --- CHANGELOG.md | 197 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a062c06b..e353cf6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,203 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.28.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.0) - 2025-02-03 + +[Compare with 0.27.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.27.0...0.28.0) + +### Breaking Changes + +Although the following changes are "breaking" in terms of public API, we didn't find any public use of these classes and methods on GitHub. + +- `mkdocstrings.extension.AutoDocProcessor.__init__(parser)`: *Parameter was removed* +- `mkdocstrings.extension.AutoDocProcessor.__init__(md)`: *Positional parameter was moved* +- `mkdocstrings.extension.AutoDocProcessor.__init__(config)`: *Parameter was removed* +- `mkdocstrings.extension.AutoDocProcessor.__init__(handlers)`: *Parameter kind was changed*: `positional or keyword` -> `keyword-only` +- `mkdocstrings.extension.AutoDocProcessor.__init__(autorefs)`: *Parameter kind was changed*: `positional or keyword` -> `keyword-only` +- `mkdocstrings.extension.MkdocstringsExtension.__init__(config)`: *Parameter was removed* +- `mkdocstrings.extension.MkdocstringsExtension.__init__(handlers)`: *Positional parameter was moved* +- `mkdocstrings.extension.MkdocstringsExtension.__init__(autorefs)`: *Positional parameter was moved* +- `mkdocstrings.handlers.base.Handlers.__init__(config)`: *Parameter was removed* +- `mkdocstrings.handlers.base.Handlers.__init__(theme)`: *Parameter was added as required* +- `mkdocstrings.handlers.base.Handlers.__init__(default)`: *Parameter was added as required* +- `mkdocstrings.handlers.base.Handlers.__init__(inventory_project)`: *Parameter was added as required* +- `mkdocstrings.handlers.base.Handlers.__init__(tool_config)`: *Parameter was added as required* + +Similarly, the following parameters were renamed, but the methods are only called from our own code, using positional arguments. + +- `mkdocstrings.handlers.base.BaseHandler.collect(config)`: *Parameter was renamed `options`* +- `mkdocstrings.handlers.base.BaseHandler.render(config)`: *Parameter was renamed `options`* + +Finally, the following method was removed, but this is again taken into account in our own code: + +- `mkdocstrings.handlers.base.BaseHandler.get_anchors`: *Public object was removed* + +For these reasons, and because we're still in v0, we do not bump to v1 yet. See following deprecations. + +### Deprecations + +*mkdocstrings* 0.28 will start emitting these deprecations warnings: + +> The `handler` argument is deprecated. The handler name must be specified as a class attribute. + +Previously, the `get_handler` function would pass a `handler` (name) argument to the handler constructor. This name must now be set on the handler's class directly. + +```python +class MyHandler: + name = "myhandler" +``` + +> The `domain` attribute must be specified as a class attribute. + +The `domain` class attribute on handlers is now mandatory and cannot be an empty string. + +```python +class MyHandler: + domain = "mh" +``` + +> The `theme` argument must be passed as a keyword argument. + +This argument could previously be passed as a positional argument (from the `get_handler` function), and must now be passed as a keyword argument. + +> The `custom_templates` argument must be passed as a keyword argument. + +Same as for `theme`, but with `custom_templates`. + +> The `mdx` argument must be provided (as a keyword argument). + +The `get_handler` function now receives a `mdx` argument, which it must forward to the handler constructor and then to the base handler, either explicitly or through `**kwargs`: + +=== "Explicitly" + + ```python + def get_handler(..., mdx, ...): + return MyHandler(..., mdx=mdx, ...) + + + class MyHandler: + def __init__(self, ..., mdx, ...): + super().__init__(..., mdx=mdx, ...) + ``` + +=== "Through `**kwargs`" + + ```python + def get_handler(..., **kwargs): + return MyHandler(..., **kwargs) + + + class MyHandler: + def __init__(self, ..., **kwargs): + super().__init__(**kwargs) + ``` + +In the meantime we still retrieve this `mdx` value at a different moment, by reading it from the MkDocs configuration. + +> The `mdx_config` argument must be provided (as a keyword argument). + +Same as for `mdx`, but with `mdx_config`. + +> mkdocstrings v1 will stop handling 'import' in handlers configuration. Instead your handler must define a `get_inventory_urls` method that returns a list of URLs to download. + +Previously, mkdocstrings would pop the `import` key from a handler's configuration to download each item (URLs). Items could be strings, or dictionaries with a `url` key. Now mkdocstrings gives back control to handlers, which must store this inventory configuration within them, and expose it again through a `get_inventory_urls` method. This method returns a list of tuples: an URL, and a dictionary of options that will be passed again to their `load_inventory` method. Handlers have now full control over the "inventory" setting. + +```python +from copy import deepcopy + + +def get_handler(..., handler_config, ...): + return MyHandler(..., config=handler_config, ...) + + +class MyHandler: + def __init__(self, ..., config, ...): + self.config = config + + def get_inventory_urls(self): + config = deepcopy(self.config["import"]) + return [(inv, {}) if isinstance(inv, str) else (inv.pop("url"), inv) for inv in config] +``` + +Changing the name of the key (for example from `import` to `inventories`) involves a change in user configuration, and both keys will have to be supported by your handler for some time. + +```python +def get_handler(..., handler_config, ...): + if "inventories" not in handler_config and "import" in handler_config: + warn("The 'import' key is renamed 'inventories'", FutureWarning) + handler_config["inventories"] = handler_config.pop("import") + return MyHandler(..., config=handler_config, ...) +``` + +> Setting a fallback anchor function is deprecated and will be removed in a future release. + +This comes from mkdocstrings and mkdocs-autorefs, and will disappear with mkdocstrings v0.28. + +> mkdocstrings v1 will start using your handler's `get_options` method to build options instead of merging the global and local options (dictionaries). + +Handlers must now store their own global options (in an instance attribute), and implement a `get_options` method that receives `local_options` (a dict) and returns combined options (dict or custom object). These combined options are then passed to `collect` and `render`, so that these methods can use them right away. + +```python +def get_handler(..., handler_config, ...): + return MyHandler(..., config=handler_config, ...) + + +class MyHandler: + def __init__(self, ..., config, ...): + self.config = config + + def get_options(local_options): + return {**self.default_options, **self.config["options"], **local_options} +``` + +> The `update_env(md)` parameter is deprecated. Use `self.md` instead. + +Handlers can remove the `md` parameter from their `update_env` method implementation, and use `self.md` instead, if they need it. + +> No need to call `super().update_env()` anymore. + +Handlers don't have to call the parent `update_env` method from their own implementation anymore, and can just drop the call. + +> The `get_anchors` method is deprecated. Declare a `get_aliases` method instead, accepting a string (identifier) instead of a collected object. + +Previously, handlers would implement a `get_anchors` method that received a data object (typed `CollectorItem`) to return aliases for this object. This forced mkdocstrings to collect this object through the handler's `collect` method, which then required some logic with "fallback config" as to prevent unwanted collection. mkdocstrings gives back control to handlers and now calls `get_aliases` instead, which accepts an `identifier` (string) and lets the handler decide how to return aliases for this identifier. For example, it can replicate previous behavior by calling its own `collect` method with its own "fallback config", or do something different (cache lookup, etc.). + +```python +class MyHandler: + def get_aliases(identifier): + try: + obj = self.collect(identifier, self.fallback_config) + # or obj = self._objects_cache[identifier] + except CollectionError: # or KeyError + return () + return ... # previous logic in `get_anchors` +``` + +> The `config_file_path` argument in `get_handler` functions is deprecated. Use `tool_config.get('config_file_path')` instead. + +The `config_file_path` argument is now deprecated and only passed to `get_handler` functions if they accept it. If you used it to compute a "base directory", you can now use the `tool_config` argument instead, which is the configuration of the SSG tool in use (here MkDocs): + +```python +base_dir = Path(tool_config.config_file_path or "./mkdocs.yml").parent +``` + +**Most of these warnings will disappear with the next version of mkdocstrings-python.** + +### Bug Fixes + +- Update handlers in JSON schema to be an object instead of an array ([3cf7d51](https://github.com/mkdocstrings/mkdocstrings/commit/3cf7d51704378adc50d4ea50080aacae39e0e731) by Matthew Messinger). [Issue-733](https://github.com/mkdocstrings/mkdocstrings/issues/733), [PR-734](https://github.com/mkdocstrings/mkdocstrings/pull/734) +- Fix broken table of contents when nesting autodoc instructions ([12c8f82](https://github.com/mkdocstrings/mkdocstrings/commit/12c8f82e9a959ce32cada09f0d2b5c651a705fdb) by Timothรฉe Mazzucotelli). [Issue-348](https://github.com/mkdocstrings/mkdocstrings/issues/348) + +### Code Refactoring + +- Pass `config_file_path` to `get_handler` if it expects it ([8c476ee](https://github.com/mkdocstrings/mkdocstrings/commit/8c476ee0b82c09a5b20d7a773ecaf4be17b9e4d1) by Timothรฉe Mazzucotelli). +- Give back inventory control to handlers ([b84653f](https://github.com/mkdocstrings/mkdocstrings/commit/b84653f2b175824c73bd0291fafff8343ba80125) by Timothรฉe Mazzucotelli). [Related-to-issue-719](https://github.com/mkdocstrings/mkdocstrings/issues/719) +- Give back control to handlers on how they want to handle global/local options ([c00de7a](https://github.com/mkdocstrings/mkdocstrings/commit/c00de7a42b9072cbaa47ecbf18e3e15a6d5ab634) by Timothรฉe Mazzucotelli). [Issue-719](https://github.com/mkdocstrings/mkdocstrings/issues/719) +- Deprecate base handler's `get_anchors` method in favor of `get_aliases` method ([7a668f0](https://github.com/mkdocstrings/mkdocstrings/commit/7a668f0f731401b07123bd02aafbbfc55cd24c0d) by Timothรฉe Mazzucotelli). +- Register all identifiers of rendered objects into autorefs ([434d8c7](https://github.com/mkdocstrings/mkdocstrings/commit/434d8c7cd1e3edbdb9d4c45a9b44b290b19d88f1) by Timothรฉe Mazzucotelli). +- Use mkdocs-get-deps' download utility to remove duplicated code ([bb87cd8](https://github.com/mkdocstrings/mkdocstrings/commit/bb87cd833f2333e77cb2c2926aa24a434c97391f) by Timothรฉe Mazzucotelli). +- Clean up data passed down from plugin to extension and handlers ([b8e8703](https://github.com/mkdocstrings/mkdocstrings/commit/b8e87036e0e1ec5c181b4a2ec5931f1a60636a32) by Timothรฉe Mazzucotelli). [PR-726](https://github.com/mkdocstrings/mkdocstrings/pull/726) + ## [0.27.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.27.0) - 2024-11-08 [Compare with 0.26.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.26.2...0.27.0) From 1cb9177b9063448307ca07834be334989350d21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 17:27:39 +0100 Subject: [PATCH 144/223] chore: Update location of the Python handler's JSON schema --- docs/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/schema.json b/docs/schema.json index 7632af66..bd646f88 100644 --- a/docs/schema.json +++ b/docs/schema.json @@ -39,7 +39,7 @@ "markdownDescription": "https://mkdocstrings.github.io/handlers/overview/", "anyOf": [ { - "$ref": "https://raw.githubusercontent.com/mkdocstrings/python/main/docs/schema.json" + "$ref": "https://mkdocstrings.github.io/python/schema.json" } ] } From 698a3216739e31f349b08f3073c4c3a6b24b07c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 17:55:32 +0100 Subject: [PATCH 145/223] chore: Update mkdocstrings-python dev-dep to force uv to install it --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87368ae8..4ac4f14e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ dev = [ "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", - "mkdocstrings-python>=1.13", + "mkdocstrings-python>=1.14", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] \ No newline at end of file From 926dd7ea0571e87731496db2c670087e3cfb3dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 18:06:06 +0100 Subject: [PATCH 146/223] docs: Remove trailing spaces --- docs/usage/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index 77129362..f59b5269 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -176,7 +176,7 @@ is possible to link to with `[example][full.path.object1]`, regardless of the cu ### Cross-references to any Markdown heading -TIP: **Changed in version 0.15.** +TIP: **Changed in version 0.15.** Linking to any Markdown heading used to be the default, but now opt-in is required. If you want to link to *any* Markdown heading, not just *mkdocstrings*-inserted items, please From e1eb99c69789412ab619e8427ebe2ec8e2e37eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 18:06:25 +0100 Subject: [PATCH 147/223] docs: Use `inventories` instead of `import` for Python example --- docs/usage/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index f59b5269..133b1251 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -271,7 +271,7 @@ plugins: - mkdocstrings: handlers: python: - import: + inventories: - https://installer.readthedocs.io/en/stable/objects.inv ``` @@ -298,7 +298,7 @@ plugins: - mkdocstrings: handlers: python: - import: + inventories: # latest instead of stable - https://installer.readthedocs.io/en/latest/objects.inv ``` @@ -311,7 +311,7 @@ plugins: - mkdocstrings: handlers: python: - import: + inventories: - url: https://cdn.example.com/version/objects.inv base_url: https://docs.example.com/version ``` From ede19411c64365ece8e784b55ef09d8c1e71432e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 18:12:03 +0100 Subject: [PATCH 148/223] chore: Increase mkdocstrings-python lower bound again --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4ac4f14e..12541620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ dev = [ "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", - "mkdocstrings-python>=1.14", + "mkdocstrings-python>=1.14.1", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] \ No newline at end of file From 4ab180d01964c3ef8005cd72c8d91ba3fd241e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 3 Feb 2025 20:49:26 +0100 Subject: [PATCH 149/223] fix: Renew MkDocs' `relpath` processor instead of using same instance Issue-mkdocs-3919: https://github.com/mkdocs/mkdocs/issues/3919 --- src/mkdocstrings/handlers/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 6929c0c1..f6bca074 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -523,7 +523,9 @@ 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: - new_md.treeprocessors.register(md.treeprocessors["relpath"], "relpath", priority=0) + relpath = md.treeprocessors["relpath"] + new_relpath = type(relpath)(relpath.file, relpath.files, relpath.config) # type: ignore[attr-defined,call-arg] + new_md.treeprocessors.register(new_relpath, "relpath", priority=0) self._md = new_md From 145954ce2414a80a9c1aa298817339ff9454ad7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 14 Feb 2025 13:39:58 +0100 Subject: [PATCH 150/223] chore: Prepare release 0.28.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e353cf6c..331472de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.28.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.1) - 2025-02-14 + +[Compare with 0.28.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.0...0.28.1) + +### Bug Fixes + +- Renew MkDocs' `relpath` processor instead of using same instance ([4ab180d](https://github.com/mkdocstrings/mkdocstrings/commit/4ab180d01964c3ef8005cd72c8d91ba3fd241e27) by Timothรฉe Mazzucotelli). [Issue-mkdocs-3919](https://github.com/mkdocs/mkdocs/issues/3919) + ## [0.28.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.0) - 2025-02-03 [Compare with 0.27.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.27.0...0.28.0) From 2c22bdc49f6bf5600aefd5ec711747686fda96a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 17:03:15 +0100 Subject: [PATCH 151/223] build: Depend on mkdocs-autorefs >= 1.4 --- config/pytest.ini | 1 + pyproject.toml | 2 +- src/mkdocstrings/extension.py | 2 +- src/mkdocstrings/handlers/base.py | 4 ++-- src/mkdocstrings/plugin.py | 2 +- tests/test_extension.py | 6 +++++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index b54cfdfa..024fffc9 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -22,3 +22,4 @@ filterwarnings = ignore:.*fallback anchor function:DeprecationWarning:mkdocstrings ignore:.*v1.*`get_options` method:DeprecationWarning:mkdocstrings ignore:.*`config_file_path` argument:DeprecationWarning:mkdocstrings + ignore:.*from 'mkdocs_autorefs.:DeprecationWarning:mkdocstrings_handlers.python diff --git a/pyproject.toml b/pyproject.toml index 12541620..846cc15e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.4", - "mkdocs-autorefs>=1.3", + "mkdocs-autorefs>=1.4", "mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5. "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 4dcbdf99..ea38b83f 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -42,7 +42,7 @@ from collections.abc import MutableSequence from markdown import Markdown - from mkdocs_autorefs.plugin import AutorefsPlugin + from mkdocs_autorefs import AutorefsPlugin log = get_logger(__name__) diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index f6bca074..e967af5f 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -20,7 +20,7 @@ from markdown import Markdown from markdown.extensions.toc import TocTreeprocessor from markupsafe import Markup -from mkdocs_autorefs.references import AutorefsInlineProcessor +from mkdocs_autorefs import AutorefsInlineProcessor # TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. from mkdocs_get_deps.cache import download_and_cache_url @@ -46,7 +46,7 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from markdown import Extension - from mkdocs_autorefs.references import AutorefsHookInterface + from mkdocs_autorefs import AutorefsHookInterface log = get_logger(__name__) diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index 9962f48d..9cda9696 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -24,7 +24,7 @@ from mkdocs.config import config_options as opt from mkdocs.plugins import BasePlugin from mkdocs.utils import write_file -from mkdocs_autorefs.plugin import AutorefsConfig, AutorefsPlugin +from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin from mkdocstrings.extension import MkdocstringsExtension from mkdocstrings.handlers.base import BaseHandler, Handlers diff --git a/tests/test_extension.py b/tests/test_extension.py index b7f1685f..d7e5b88a 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -162,7 +162,11 @@ def test_register_every_identifier_alias(plugin: MkdocstringsPlugin, ext_markdow handler.get_anchors = lambda _: ids # type: ignore[union-attr] handler.get_aliases = lambda _: ids # type: ignore[method-assign] autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] - autorefs.current_page = "foo" + + class Page: + url = "foo" + + autorefs.current_page = Page() ext_markdown.convert("::: tests.fixtures.headings") for identifier in ids: assert identifier in autorefs._secondary_url_map From 2b9b4f09450955ee59a060dc61a96233dcdab689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 17:12:28 +0100 Subject: [PATCH 152/223] chore: Prepare release 0.28.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 331472de..455b6ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.28.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.2) - 2025-02-24 + +[Compare with 0.28.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.1...0.28.2) + +### Build + +- Depend on mkdocs-autorefs >= 1.4 ([2c22bdc](https://github.com/mkdocstrings/mkdocstrings/commit/2c22bdc49f6bf5600aefd5ec711747686fda96a8) by Timothรฉe Mazzucotelli). + ## [0.28.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.1) - 2025-02-14 [Compare with 0.28.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.0...0.28.1) From 122cdb6dcb2416d6b6d9b220f2112328865d0b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 19:32:59 +0100 Subject: [PATCH 153/223] tests: Remove unused pytest warning filters --- config/pytest.ini | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/config/pytest.ini b/config/pytest.ini index 024fffc9..4c6549d9 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -10,16 +10,4 @@ testpaths = # action:message_regex:warning_class:module_regex:line filterwarnings = error - # TODO: remove once pytest-xdist 4 is released - ignore:.*rsyncdir:DeprecationWarning:xdist - # TODO: remove once mkdocstrings-python releases a new version - ignore:.*`handler` argument:DeprecationWarning:mkdocstrings_handlers - ignore:.*`mdx` argument:DeprecationWarning:mkdocstrings_handlers - ignore:.*`mdx_config` argument:DeprecationWarning:mkdocstrings_handlers - ignore:.*`update_env\(md\)` parameter:DeprecationWarning:mkdocstrings - ignore:.*`super\(\).update_env\(\)` anymore:DeprecationWarning:mkdocstrings_handlers ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings - ignore:.*fallback anchor function:DeprecationWarning:mkdocstrings - ignore:.*v1.*`get_options` method:DeprecationWarning:mkdocstrings - ignore:.*`config_file_path` argument:DeprecationWarning:mkdocstrings - ignore:.*from 'mkdocs_autorefs.:DeprecationWarning:mkdocstrings_handlers.python From 83a76c4a0bdb714d3249b9c84ea9e47334a95d0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:30:20 +0100 Subject: [PATCH 154/223] chore: Stop using Black for signature formatting --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 846cc15e..43936dbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,6 @@ dev = [ "types-pyyaml>=6.0", # docs - "black>=24.4", "markdown-callouts>=0.4", "markdown-exec>=1.8", "mkdocs>=1.6", @@ -119,4 +118,4 @@ dev = [ "mkdocstrings-python>=1.14.1", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", -] \ No newline at end of file +] From 7824297a5cd728995cf61828f4ad178c694be8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:30:34 +0100 Subject: [PATCH 155/223] tests: Use dirty-equals to simplify tests --- pyproject.toml | 1 + tests/test_handlers.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 43936dbc..1a40bd67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ dev = [ "twine>=5.1", # ci + "dirty-equals>=0.9.0", "duty>=1.4", "ruff>=0.4", "pytest>=8.2", diff --git a/tests/test_handlers.py b/tests/test_handlers.py index cea80657..87a2ce84 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -8,6 +8,7 @@ import pytest from jinja2.exceptions import TemplateNotFound from markdown import Markdown +from dirty_equals import IsStr from mkdocstrings.handlers.base import Highlighter @@ -120,14 +121,14 @@ def test_nested_autodoc(ext_markdown: Markdown) -> None: { "level": 1, "id": "tests.fixtures.nesting.Class", - "html": "", + "html": IsStr(), "name": "Class", "data-toc-label": "Class", "children": [ { "level": 2, "id": "tests.fixtures.nesting.Class.method", - "html": "", + "html": IsStr(), "name": "method", "data-toc-label": "method", "children": [], From e239cf368029e281af19c496fe7d1dc8d3f8f11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:49:19 +0100 Subject: [PATCH 156/223] chore: Format, clean up --- tests/test_extension.py | 2 -- tests/test_handlers.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_extension.py b/tests/test_extension.py index d7e5b88a..12723cf5 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -158,8 +158,6 @@ def test_register_every_identifier_alias(plugin: MkdocstringsPlugin, ext_markdow """Assert that we don't preemptively register all identifiers of a rendered object.""" handler = plugin._handlers.get_handler("python") # type: ignore[union-attr] ids = ("id1", "id2", "id3") - # TODO: Remove line when Python handler removes its `get_anchors` method. - handler.get_anchors = lambda _: ids # type: ignore[union-attr] handler.get_aliases = lambda _: ids # type: ignore[method-assign] autorefs = ext_markdown.parser.blockprocessors["mkdocstrings"]._autorefs # type: ignore[attr-defined] diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 87a2ce84..cb39023d 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -6,9 +6,9 @@ from typing import TYPE_CHECKING import pytest +from dirty_equals import IsStr from jinja2.exceptions import TemplateNotFound from markdown import Markdown -from dirty_equals import IsStr from mkdocstrings.handlers.base import Highlighter From ba9003e96c8e5e01900743d5c464cbd228d732f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:56:03 +0100 Subject: [PATCH 157/223] build: Make `python` extra depend on latest mkdocstrings-python (1.16.2) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a40bd67..45222bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ [project.optional-dependencies] crystal = ["mkdocstrings-crystal>=0.3.4"] python-legacy = ["mkdocstrings-python-legacy>=0.2.1"] -python = ["mkdocstrings-python>=0.5.2"] +python = ["mkdocstrings-python>=1.16.2"] [project.urls] Homepage = "https://mkdocstrings.github.io" @@ -116,7 +116,7 @@ dev = [ "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", - "mkdocstrings-python>=1.14.1", + "mkdocstrings-python>=1.16.2", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] From 9f16e5f93466c02553349a3ed2e796bc19cdc393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 16:30:58 +0100 Subject: [PATCH 158/223] docs: Remove intermediate section in preparation for backlinks --- mkdocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0288678d..4bc3ea2a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,8 +35,7 @@ nav: - Recipes: recipes.md - Troubleshooting: troubleshooting.md # defer to gen-files + literate-nav -- API reference: - - mkdocstrings: reference/ +- API reference: reference/ - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md From ec038ebd34aa8dde3d9434e0d1991a02726a5058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 16:31:09 +0100 Subject: [PATCH 159/223] docs: Enable autorefs plugin in preparation for backlinks --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 4bc3ea2a..1a2a0a7e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -126,6 +126,7 @@ markdown_extensions: plugins: - search +- autorefs - markdown-exec - gen-files: scripts: From 94d829961a5103ba4f83960f78117b0845624cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 16:51:23 +0100 Subject: [PATCH 160/223] chore: Template upgrade --- .copier-answers.yml | 6 +- .github/FUNDING.yml | 3 - .github/ISSUE_TEMPLATE/1-bug.md | 2 +- .github/workflows/ci.yml | 22 ++- .github/workflows/release.yml | 2 +- CODE_OF_CONDUCT.md | 99 +++--------- CONTRIBUTING.md | 54 ++----- config/ruff.toml | 11 +- config/vscode/launch.json | 12 +- docs/.overrides/partials/comments.html | 2 +- docs/.overrides/partials/path-item.html | 22 +++ docs/changelog.md | 4 + docs/code_of_conduct.md | 4 + docs/contributing.md | 4 + docs/credits.md | 3 +- docs/index.md | 1 + docs/insiders/index.md | 149 ++++------------- docs/insiders/installation.md | 61 +++---- docs/license.md | 1 + duties.py | 39 +++-- mkdocs.yml | 21 ++- pyproject.toml | 24 +-- scripts/{gen_ref_nav.py => gen_api_ref.py} | 9 +- scripts/gen_credits.py | 4 +- scripts/get_version.py | 3 +- scripts/insiders.py | 39 +---- scripts/make.py | 2 - src/mkdocstrings/__init__.py | 4 + src/mkdocstrings/_internal/__init__.py | 0 src/mkdocstrings/{ => _internal}/debug.py | 28 ++-- tests/test_api.py | 177 +++++++++++++++++++++ 31 files changed, 439 insertions(+), 373 deletions(-) create mode 100644 docs/.overrides/partials/path-item.html rename scripts/{gen_ref_nav.py => gen_api_ref.py} (80%) create mode 100644 src/mkdocstrings/_internal/__init__.py rename src/mkdocstrings/{ => _internal}/debug.py (82%) create mode 100644 tests/test_api.py diff --git a/.copier-answers.yml b/.copier-answers.yml index b3437df7..bda5cd0c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ -# Changes here will be overwritten by Copier -_commit: 1.5.6 +# Changes here will be overwritten by Copier. +_commit: 1.7.1 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli @@ -7,7 +7,7 @@ author_username: pawamoy copyright_date: '2019' copyright_holder: Timothรฉe Mazzucotelli copyright_holder_email: dev@pawamoy.fr -copyright_license: ISC License +copyright_license: ISC insiders: true insiders_email: insiders@pawamoy.fr insiders_repository_name: mkdocstrings diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a502284a..812789e6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,2 @@ github: pawamoy -ko_fi: pawamoy polar: pawamoy -custom: -- https://www.paypal.me/pawamoy diff --git a/.github/ISSUE_TEMPLATE/1-bug.md b/.github/ISSUE_TEMPLATE/1-bug.md index e775cc1f..5677ad2a 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.md +++ b/.github/ISSUE_TEMPLATE/1-bug.md @@ -50,7 +50,7 @@ PASTE TRACEBACK HERE redacting sensitive information. --> ```bash -python -m mkdocstrings.debug # | xclip -selection clipboard +python -m mkdocstrings._internal.debug # | xclip -selection clipboard ``` PASTE MARKDOWN OUTPUT HERE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d95b7809..2dd6da41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml @@ -56,6 +56,12 @@ jobs: - name: Check for breaking changes in the API run: make check-api + - name: Store objects inventory for tests + uses: actions/upload-artifact@v4 + with: + name: objects.inv + path: site/objects.inv + exclude-test-jobs: runs-on: ubuntu-latest outputs: @@ -82,7 +88,9 @@ jobs: tests: - needs: exclude-test-jobs + needs: + - quality + - exclude-test-jobs strategy: matrix: os: @@ -117,16 +125,22 @@ jobs: allow-prereleases: true - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 with: enable-cache: true cache-dependency-glob: pyproject.toml - cache-suffix: py${{ matrix.python-version }} + cache-suffix: ${{ matrix.resolution }} - name: Install dependencies env: UV_RESOLUTION: ${{ matrix.resolution }} run: make setup + - name: Download objects inventory + uses: actions/download-artifact@v4 + with: + name: objects.inv + path: site/ + - name: Run the test suite run: make test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db7a223e..73347dad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: with: python-version: "3.12" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v5 - name: Build dists if: github.repository_owner == 'pawamoy-insiders' run: uv tool run --from build pyproject-build diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 255e0eed..2d46305a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,128 +2,79 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, caste, color, religion, or sexual -identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to a positive environment for our -community include: +Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall - community +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or advances of - any kind +* The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email address, - without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -dev@pawamoy.fr. -All complaints will be reviewed and investigated promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dev@pawamoy.fr. All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. +All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning -**Community Impact**: A violation through a single incident or series of -actions. +**Community Impact**: A violation through a single incident or series of actions. -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or permanent -ban. +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within the -community. +**Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.1, available at -[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. -Community Impact Guidelines were inspired by -[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. -For answers to common questions about this code of conduct, see the FAQ at -[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at -[https://www.contributor-covenant.org/translations][translations]. +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab0c308b..78db50f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Contributing -Contributions are welcome, and they are greatly appreciated! -Every little bit helps, and credit will always be given. +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ## Environment setup @@ -23,11 +22,7 @@ Then: make setup ``` -> NOTE: -> If it fails for some reason, -> you'll need to install -> [uv](https://github.com/astral-sh/uv) -> manually. +> NOTE: If it fails for some reason, you'll need to install [uv](https://github.com/astral-sh/uv) manually. > > You can install it with: > @@ -35,8 +30,7 @@ make setup > curl -LsSf https://astral.sh/uv/install.sh | sh > ``` > -> Now you can try running `make setup` again, -> or simply `uv sync`. +> Now you can try running `make setup` again, or simply `uv sync`. You now have the dependencies installed. @@ -44,15 +38,10 @@ Run `make help` to see all the available actions! ## Tasks -The entry-point to run commands and tasks is the `make` Python script, -located in the `scripts` directory. Try running `make` to show the available commands and tasks. -The *commands* do not need the Python dependencies to be installed, -while the *tasks* do. -The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). +The entry-point to run commands and tasks is the `make` Python script, located in the `scripts` directory. Try running `make` to show the available commands and tasks. The *commands* do not need the Python dependencies to be installed, +while the *tasks* do. The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). -If you work in VSCode, we provide -[an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) -for the project. +If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. ## Development @@ -71,17 +60,13 @@ As usual: 1. go to http://localhost:8000 and check that everything looks good 1. follow our [commit message convention](#commit-message-convention) -If you are unsure about how to fix or ignore a warning, -just let the continuous integration fail, -and we will help you during the review. +If you are unsure about how to fix or ignore a warning, just let the continuous integration fail, and we will help you during review. Don't bother updating the changelog, we will take care of this. ## Commit message convention -Commit messages must follow our convention based on the -[Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) -or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): +Commit messages must follow our convention based on the [Angular style](https://gist.github.com/stephenparish/9941e89d80e2bc58a153#format-of-the-commit-message) or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg.html): ``` [(scope)]: Subject @@ -89,10 +74,7 @@ or the [Karma convention](https://karma-runner.github.io/4.0/dev/git-commit-msg. [Body] ``` -**Subject and body must be valid Markdown.** -Subject must have proper casing (uppercase for first letter -if it makes sense), but no dot at the end, and no punctuation -in general. +**Subject and body must be valid Markdown.** Subject must have proper casing (uppercase for first letter if it makes sense), but no dot at the end, and no punctuation in general. Scope and body are optional. Type can be: @@ -108,9 +90,7 @@ Scope and body are optional. Type can be: - `style`: A change in code style/format. - `tests`: About tests. -If you write a body, please add trailers at the end -(for example issues and PR references, or co-authors), -without relying on GitHub's flavored Markdown: +If you write a body, please add trailers at the end (for example issues and PR references, or co-authors), without relying on GitHub's flavored Markdown: ``` Body. @@ -119,16 +99,9 @@ Issue #10: https://github.com/namespace/project/issues/10 Related to PR namespace/other-project#15: https://github.com/namespace/other-project/pull/15 ``` -These "trailers" must appear at the end of the body, -without any blank lines between them. The trailer title -can contain any character except colons `:`. -We expect a full URI for each trailer, not just GitHub autolinks -(for example, full GitHub URLs for commits and issues, -not the hash or the #issue-number). +These "trailers" must appear at the end of the body, without any blank lines between them. The trailer title can contain any character except colons `:`. We expect a full URI for each trailer, not just GitHub autolinks (for example, full GitHub URLs for commits and issues, not the hash or the #issue-number). -We do not enforce a line length on commit messages summary and body, -but please avoid very long summaries, and very long lines in the body, -unless they are part of code blocks that must not be wrapped. +We do not enforce a line length on commit messages summary and body, but please avoid very long summaries, and very long lines in the body, unless they are part of code blocks that must not be wrapped. ## Pull requests guidelines @@ -153,5 +126,4 @@ And force-push: git push -f ``` -If this seems all too complicated, you can push or force-push each new commit, -and we will squash them ourselves if needed, before merging. +If this seems all too complicated, you can push or force-push each new commit, and we will squash them ourselves if needed, before merging. diff --git a/config/ruff.toml b/config/ruff.toml index fbe31d5b..655a158c 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -47,17 +47,24 @@ ignore = [ ] [lint.per-file-ignores] -"src/*/cli.py" = [ +"src/**/cli.py" = [ "T201", # Print statement ] "src/*/debug.py" = [ "T201", # Print statement ] +"!src/*/*.py" = [ + "D100", # Missing docstring in public module +] +"!src/**.py" = [ + "D101", # Missing docstring in public class + "D103", # Missing docstring in public function +] "scripts/*.py" = [ "INP001", # File is part of an implicit namespace package "T201", # Print statement ] -"tests/*.py" = [ +"tests/**.py" = [ "ARG005", # Unused lambda argument "FBT001", # Boolean positional arg in function definition "PLR2004", # Magic value used in comparison diff --git a/config/vscode/launch.json b/config/vscode/launch.json index e3288388..5f3742be 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -7,7 +7,17 @@ "request": "launch", "program": "${file}", "console": "integratedTerminal", - "justMyCode": false + "justMyCode": false, + "args": "${command:pickArgs}" + }, + { + "name": "run", + "type": "debugpy", + "request": "launch", + "module": "mkdocstrings", + "console": "integratedTerminal", + "justMyCode": false, + "args": "${command:pickArgs}" }, { "name": "docs", diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html index 3976b0d6..cc341ba9 100644 --- a/docs/.overrides/partials/comments.html +++ b/docs/.overrides/partials/comments.html @@ -31,7 +31,7 @@

Feedback

: "light" // Instruct Giscus to set theme - giscus.setAttribute("data-theme", theme) + giscus.setAttribute("data-theme", theme) } // Register event handlers after documented loaded diff --git a/docs/.overrides/partials/path-item.html b/docs/.overrides/partials/path-item.html new file mode 100644 index 00000000..a9c95446 --- /dev/null +++ b/docs/.overrides/partials/path-item.html @@ -0,0 +1,22 @@ +{# Fix breadcrumbs for when mkdocs-section-index is used. #} +{# See https://github.com/squidfunk/mkdocs-material/issues/7614. #} + + +{% macro render_content(nav_item) %} + + {{ nav_item.title }} + +{% endmacro %} + + +{% macro render(nav_item, ref=nav_item) %} + {% if nav_item.is_page %} +
  • + + {{ render_content(ref) }} + +
  • + {% elif nav_item.children %} + {{ render(nav_item.children | first, ref) }} + {% endif %} +{% endmacro %} diff --git a/docs/changelog.md b/docs/changelog.md index 786b75d5..0536cbbe 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1 +1,5 @@ +--- +title: Changelog +--- + --8<-- "CHANGELOG.md" diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md index 01f2ea20..002b2a04 100644 --- a/docs/code_of_conduct.md +++ b/docs/code_of_conduct.md @@ -1 +1,5 @@ +--- +title: Code of Conduct +--- + --8<-- "CODE_OF_CONDUCT.md" diff --git a/docs/contributing.md b/docs/contributing.md index ea38c9bf..61935e5d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1 +1,5 @@ +--- +title: Contributing +--- + --8<-- "CONTRIBUTING.md" diff --git a/docs/credits.md b/docs/credits.md index f758db87..f6ab1aa2 100644 --- a/docs/credits.md +++ b/docs/credits.md @@ -1,10 +1,9 @@ --- +title: Credits hide: - toc --- - ```python exec="yes" --8<-- "scripts/gen_credits.py" ``` - diff --git a/docs/index.md b/docs/index.md index 8e6f2fb4..82377e21 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ --- +title: Overview hide: - feedback --- diff --git a/docs/insiders/index.md b/docs/insiders/index.md index daa4731c..ce59f6bb 100644 --- a/docs/insiders/index.md +++ b/docs/insiders/index.md @@ -1,57 +1,30 @@ +--- +title: Insiders +--- + # Insiders -*mkdocstrings* follows the **sponsorware** release strategy, which means -that new features are first exclusively released to sponsors as part of -[Insiders][insiders]. Read on to learn [what sponsorships achieve][sponsorship], -[how to become a sponsor][sponsors] to get access to Insiders, -and [what's in it for you][features]! +*mkdocstrings* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][]. Read on to learn [what sponsorships achieve][sponsorship], [how to become a sponsor][sponsors] to get access to Insiders, and [what's in it for you][features]! ## What is Insiders? -*mkdocstrings Insiders* is a private fork of *mkdocstrings*, hosted as -a private GitHub repository. Almost[^1] [all new features][features] -are developed as part of this fork, which means that they are immediately -available to all eligible sponsors, as they are made collaborators of this -repository. +*mkdocstrings Insiders* is a private fork of *mkdocstrings*, hosted as a private GitHub repository. Almost[^1] [all new features][features] are developed as part of this fork, which means that they are immediately available to all eligible sponsors, as they are granted access to this private repository. - [^1]: - In general, every new feature is first exclusively released to sponsors, but - sometimes upstream dependencies enhance - existing features that must be supported by *mkdocstrings*. +[^1]: In general, every new feature is first exclusively released to sponsors, but sometimes upstream dependencies enhance existing features that must be supported by *mkdocstrings*. -Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a -funding goal is hit, the features that are tied to it are merged back into -*mkdocstrings* and released for general availability, making them available -to all users. Bugfixes are always released in tandem. +Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a funding goal is hit, the features that are tied to it are merged back into *mkdocstrings* and released for general availability, making them available to all users. Bugfixes are always released in tandem. Sponsorships start as low as [**$10 a month**][sponsors].[^2] - [^2]: - Note that $10 a month is the minimum amount to become eligible for - Insiders. While GitHub Sponsors also allows to sponsor lower amounts or - one-time amounts, those can't be granted access to Insiders due to - technical reasons. Such contributions are still very much welcome as - they help ensuring the project's sustainability. - +[^2]: Note that $10 a month is the minimum amount to become eligible for Insiders. While GitHub Sponsors also allows to sponsor lower amounts or one-time amounts, those can't be granted access to Insiders due to technical reasons. Such contributions are still very much welcome as they help ensuring the project's sustainability. ## What sponsorships achieve -Sponsorships make this project sustainable, as they buy the maintainers of this -project time โ€“ a very scarce resource โ€“ which is spent on the development of new -features, bug fixing, stability improvement, issue triage and general support. -The biggest bottleneck in Open Source is time.[^3] +Sponsorships make this project sustainable, as they buy the maintainers of this project time โ€“ a very scarce resource โ€“ which is spent on the development of new features, bug fixing, stability improvement, issue triage and general support. The biggest bottleneck in Open Source is time.[^3] - [^3]: - Making an Open Source project sustainable is exceptionally hard: maintainers - burn out, projects are abandoned. That's not great and very unpredictable. - The sponsorware model ensures that if you decide to use *mkdocstrings*, - you can be sure that bugs are fixed quickly and new features are added - regularly. +[^3]: Making an Open Source project sustainable is exceptionally hard: maintainers burn out, projects are abandoned. That's not great and very unpredictable. The sponsorware model ensures that if you decide to use *mkdocstrings*, you can be sure that bugs are fixed quickly and new features are added regularly. - +If you're unsure if you should sponsor this project, check out the list of [completed funding goals][goals completed] to learn whether you're already using features that were developed with the help of sponsorships. You're most likely using at least a handful of them, [thanks to our awesome sponsors][sponsors]! ## What's in it for me? @@ -97,50 +70,24 @@ else: ``` -Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management](https://pawamoy.github.io/backlog/). +Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management][backlog]. ## How to become a sponsor -Thanks for your interest in sponsoring! In order to become an eligible sponsor -with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], -and complete a sponsorship of **$10 a month or more**. -You can use your individual or organization GitHub account for sponsoring. +Thanks for your interest in sponsoring! In order to become an eligible sponsor with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. + +Sponsorships lower than $10 a month are also very much appreciated, and useful. They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. Every sponsorship helps us implementing new features and releasing them to the public. -Sponsorships lower than $10 a month are also very much appreciated, and useful. -They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. -*Every* sponsorship helps us implementing new features and releasing them to the public. +**Important:** By default, when you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, all the publicly visible members of the organization will be invited to join our private repositories. If you wish to only grant access to a subset of users, please send a short email to insiders@pawamoy.fr with the name of your organization and the GitHub accounts of the users that should be granted access. -**Important**: If you're sponsoring **[@pawamoy][github sponsor profile]** -through a GitHub organization, please send a short email -to insiders@pawamoy.fr with the name of your -organization and the GitHub account of the individual -that should be added as a collaborator.[^4] +**Tip:** to ensure that access is not tied to a particular individual GitHub account, you can create a bot account (i.e. a GitHub account that is not tied to a specific individual), and use this account for the sponsoring. After being granted access to our private repositories, the bot account can create private forks of our private repositories into your own organization, which all members of your organization will have access to. You can cancel your sponsorship anytime.[^5] - [^4]: - It's currently not possible to grant access to each member of an - organization, as GitHub only allows for adding users. Thus, after - sponsoring, please send an email to insiders@pawamoy.fr, stating which - account should become a collaborator of the Insiders repository. We're - working on a solution which will make access to organizations much simpler. - To ensure that access is not tied to a particular individual GitHub account, - create a bot account (i.e. a GitHub account that is not tied to a specific - individual), and use this account for the sponsoring. After being added to - the list of collaborators, the bot account can create a private fork of the - private Insiders GitHub repository, and grant access to all members of the - organizations. - - [^5]: - If you cancel your sponsorship, GitHub schedules a cancellation request - which will become effective at the end of the billing cycle. This means - that even though you cancel your sponsorship, you will keep your access to - Insiders as long as your cancellation isn't effective. All charges are - processed by GitHub through Stripe. As we don't receive any information - regarding your payment, and GitHub doesn't offer refunds, sponsorships are - non-refundable. - -[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors](https://github.com/sponsors/pawamoy){ .md-button .md-button--primary } +[^5]: If you cancel your sponsorship, GitHub schedules a cancellation request which will become effective at the end of the billing cycle. This means that even though you cancel your sponsorship, you will keep your access to Insiders as long as your cancellation isn't effective. All charges are processed by GitHub through Stripe. As we don't receive any information regarding your payment, and GitHub doesn't offer refunds, sponsorships are non-refundable. + + +[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors][github sponsor profile]{ .md-button .md-button--primary }
    @@ -153,23 +100,14 @@ You can cancel your sponsorship anytime.[^5]
    - If you sponsor publicly, you're automatically added here with a link to - your profile and avatar to show your support for *mkdocstrings*. - Alternatively, if you wish to keep your sponsorship private, you'll be a - silent +1. You can select visibility during checkout and change it - afterwards. + If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *mkdocstrings*. Alternatively, if you wish to keep your sponsorship private, you'll be a silent +1. You can select visibility during checkout and change it afterwards. ## Funding ### Goals -The following section lists all funding goals. Each goal contains a list of -features prefixed with a checkmark symbol, denoting whether a feature is -:octicons-check-circle-fill-24:{ style="color: #00e676" } already available or -:octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, -but not yet implemented. When the funding goal is hit, -the features are released for general availability. +The following section lists all funding goals. Each goal contains a list of features prefixed with a checkmark symbol, denoting whether a feature is :octicons-check-circle-fill-24:{ style="color: #00e676" } already available or :octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, but not yet implemented. When the funding goal is hit, the features are released for general availability. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): @@ -179,9 +117,7 @@ for goal in goals.values(): ### Goals completed -This section lists all funding goals that were previously completed, which means -that those features were part of Insiders, but are now generally available and -can be used by all users. +This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. ```python exec="1" session="insiders" idprefix="" for goal in goals.values(): @@ -193,47 +129,28 @@ for goal in goals.values(): ### Compatibility -> We're building an open source project and want to allow outside collaborators -to use *mkdocstrings* locally without having access to Insiders. -Is this still possible? +> We're building an open source project and want to allow outside collaborators to use *mkdocstrings* locally without having access to Insiders. Is this still possible? -Yes. Insiders is compatible with *mkdocstrings*. Almost all new features -and configuration options are either backward-compatible or implemented behind -feature flags. Most Insiders features enhance the overall experience, -though while these features add value for the users of your project, they -shouldn't be necessary for previewing when making changes to content. +Yes. Insiders is compatible with *mkdocstrings*. Almost all new features and configuration options are either backward-compatible or implemented behind feature flags. Most Insiders features enhance the overall experience, though while these features add value for the users of your project, they shouldn't be necessary for previewing when making changes to content. ### Payment > We don't want to pay for sponsorship every month. Are there any other options? -Yes. You can sponsor on a yearly basis by [switching your GitHub account to a -yearly billing cycle][billing cycle]. If for some reason you cannot do that, you -could also create a dedicated GitHub account with a yearly billing cycle, which -you only use for sponsoring (some sponsors already do that). +Yes. You can sponsor on a yearly basis by [switching your GitHub account to a yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). If you have any problems or further questions, please reach out to insiders@pawamoy.fr. ### Terms -> Are we allowed to use Insiders under the same terms and conditions as -*mkdocstrings*? - -Yes. Whether you're an individual or a company, you may use *mkdocstrings -Insiders* precisely under the same terms as *mkdocstrings*, which are given -by the [ISC License][license]. However, we kindly ask you to respect our -**fair use policy**: +> Are we allowed to use Insiders under the same terms and conditions as *mkdocstrings*? -- Please **don't distribute the source code** of Insiders. You may freely use - it for public, private or commercial projects, privately fork or mirror it, - but please don't make the source code public, as it would counteract the - sponsorware strategy. +Yes. Whether you're an individual or a company, you may use *mkdocstrings Insiders* precisely under the same terms as *mkdocstrings*, which are given by the [ISC license][license]. However, we kindly ask you to respect our **fair use policy**: -- If you cancel your subscription, you're automatically removed as a - collaborator and will miss out on all future updates of Insiders. However, you - may **use the latest version** that's available to you **as long as you like**. - Just remember that [GitHub deletes private forks][private forks]. +- Please **don't distribute the source code** of Insiders. You may freely use it for public, private or commercial projects, privately fork or mirror it, but please don't make the source code public, as it would counteract the sponsorware strategy. +- If you cancel your subscription, your access to the private repository is revoked, and you will miss out on all future updates of Insiders. However, you may **use the latest version** that's available to you **as long as you like**. Just remember that [GitHub deletes private forks][private forks]. +[backlog]: https://pawamoy.github.io/backlog/ [insiders]: #what-is-insiders [sponsorship]: #what-sponsorships-achieve [sponsors]: #how-to-become-a-sponsor diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md index 5671f0da..1df4608b 100644 --- a/docs/insiders/installation.md +++ b/docs/insiders/installation.md @@ -4,62 +4,38 @@ title: Getting started with Insiders # Getting started with Insiders -*mkdocstrings 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. - - [become an eligible sponsor]: index.md#how-to-become-a-sponsor +*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 PyPI Insiders - -[PyPI Insiders](https://pawamoy.github.io/pypi-insiders/) -is a tool that helps you keep up-to-date versions -of Insiders projects in the PyPI index of your choice -(self-hosted, Google registry, Artifactory, etc.). +### with the `insiders` tool -See [how to install it](https://pawamoy.github.io/pypi-insiders/#installation) -and [how to use it](https://pawamoy.github.io/pypi-insiders/#usage). +[`insiders`][insiders-tool] is a tool that helps you keep up-to-date versions of Insiders projects in the PyPI index of your choice (self-hosted, Google registry, Artifactory, etc.). -**We kindly ask that you do not upload the distributions to public registries, -as it is against our [Terms of use](index.md#terms).** +**We kindly ask that you do not upload the distributions to public registries, as it is against our [Terms of use][].** ### with pip (ssh/https) -*mkdocstrings Insiders* can be installed with `pip` [using SSH][using ssh]: +*mkdocstrings Insiders* can be installed with `pip` [using SSH][install-pip-ssh]: ```bash pip install git+ssh://git@github.com/pawamoy-insiders/mkdocstrings.git ``` - [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh - Or using HTTPS: ```bash pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/mkdocstrings.git ``` ->? NOTE: **How to get a GitHub personal access token** -> The `GH_TOKEN` environment variable is a GitHub token. -> It can be obtained by creating a [personal access token] for -> your GitHub account. It will give you access to the Insiders repository, -> programmatically, from the command line or GitHub Actions workflows: -> +>? NOTE: **How to get a GitHub personal access token?** The `GH_TOKEN` environment variable is a GitHub token. It can be obtained by creating a [personal access token][github-pat] for your GitHub account. It will give you access to the Insiders repository, programmatically, from the command line or GitHub Actions workflows: +> > 1. Go to https://github.com/settings/tokens -> 2. Click on [Generate a new token] +> 2. Click on [Generate a new token][github-pat-new] > 3. Enter a name and select the [`repo`][scopes] scope > 4. Generate the token and store it in a safe place -> -> [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token -> [Generate a new token]: https://github.com/settings/tokens/new -> [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes -> -> Note that the personal access -> token must be kept secret at all times, as it allows the owner to access your -> private repositories. +> +> Note that the personal access token must be kept secret at all times, as it allows the owner to access your private repositories. ### with Git @@ -77,12 +53,15 @@ pip install -e mkdocstrings ## 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`. +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. +If the major version increased, it's a good idea to consult the [changelog][] and go through the steps to ensure your configuration is up to date and all necessary changes have been made. - [changelog]: ./changelog.md +[become an eligible sponsor]: ./index.md#how-to-become-a-sponsor +[changelog]: ./changelog.md +[github-pat]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token +[github-pat-new]: https://github.com/settings/tokens/new +[insiders-tool]: https://pawamoy.github.io/insiders-project/ +[install-pip-ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh +[scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes +[terms of use]: ./index.md#terms diff --git a/docs/license.md b/docs/license.md index e81c0edf..5b25a00f 100644 --- a/docs/license.md +++ b/docs/license.md @@ -1,4 +1,5 @@ --- +title: License hide: - feedback --- diff --git a/duties.py b/duties.py index eae95cc1..6ee9b08d 100644 --- a/duties.py +++ b/duties.py @@ -3,11 +3,13 @@ from __future__ import annotations import os +import re import sys from contextlib import contextmanager +from functools import wraps from importlib.metadata import version as pkgversion from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable from duty import duty, tools @@ -26,15 +28,30 @@ MULTIRUN = os.environ.get("MULTIRUN", "0") == "1" -def pyprefix(title: str) -> str: # noqa: D103 +def pyprefix(title: str) -> str: if MULTIRUN: prefix = f"(python{sys.version_info.major}.{sys.version_info.minor})" return f"{prefix:14}{title}" return title +def not_from_insiders(func: Callable) -> Callable: + @wraps(func) + def wrapper(ctx: Context, *args: Any, **kwargs: Any) -> None: + origin = ctx.run("git config --get remote.origin.url", silent=True) + if "pawamoy-insiders/griffe" in origin: + ctx.run( + lambda: False, + title="Not running this task from insiders repository (do that from public repo instead!)", + ) + return + func(ctx, *args, **kwargs) + + return wrapper + + @contextmanager -def material_insiders() -> Iterator[bool]: # noqa: D103 +def material_insiders() -> Iterator[bool]: if "+insiders" in pkgversion("mkdocs-material"): os.environ["MATERIAL_INSIDERS"] = "true" try: @@ -45,6 +62,12 @@ def material_insiders() -> Iterator[bool]: # noqa: D103 yield False +def _get_changelog_version() -> str: + changelog_version_re = re.compile(r"^## \[(\d+\.\d+\.\d+)\].*$") + with Path(__file__).parent.joinpath("CHANGELOG.md").open("r", encoding="utf8") as file: + return next(filter(bool, map(changelog_version_re.match, file))).group(1) # type: ignore[union-attr] + + @duty def changelog(ctx: Context, bump: str = "") -> None: """Update the changelog in-place with latest commits. @@ -53,6 +76,7 @@ def changelog(ctx: Context, bump: str = "") -> None: bump: Bump option passed to git-changelog. """ ctx.run(tools.git_changelog(bump=bump or None), title="Updating changelog") + ctx.run(tools.yore.check(bump=bump or _get_changelog_version()), title="Checking legacy code") @duty(pre=["check-quality", "check-types", "check-docs", "check-api"]) @@ -85,6 +109,7 @@ def check_docs(ctx: Context) -> None: def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" os.environ["MYPYPATH"] = "src" + os.environ["FORCE_COLOR"] = "1" ctx.run( tools.mypy(*PY_SRC_LIST, config_file="config/mypy.ini"), title=pyprefix("Type-checking"), @@ -174,6 +199,7 @@ def build(ctx: Context) -> None: @duty +@not_from_insiders def publish(ctx: Context) -> None: """Publish source and wheel distributions to PyPI.""" if not Path("dist").exists(): @@ -187,18 +213,13 @@ def publish(ctx: Context) -> None: @duty(post=["build", "publish", "docs-deploy"]) +@not_from_insiders def release(ctx: Context, version: str = "") -> None: """Release a new Python package. Parameters: version: The new version number to use. """ - origin = ctx.run("git config --get remote.origin.url", silent=True) - if "pawamoy-insiders/mkdocstrings" in origin: - ctx.run( - lambda: False, - title="Not releasing from insiders repository (do that from public repo instead!)", - ) if not (version := (version or input("> Version to release: ")).strip()): ctx.run("false", title="A version must be provided") ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY) diff --git a/mkdocs.yml b/mkdocs.yml index 1a2a0a7e..eb280985 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -59,7 +59,8 @@ theme: - content.code.copy - content.tooltips - navigation.footer - - navigation.indexes + - navigation.instant.preview + - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky @@ -128,16 +129,17 @@ plugins: - search - autorefs - markdown-exec +- section-index - gen-files: scripts: - - scripts/gen_ref_nav.py + - scripts/gen_api_ref.py - literate-nav: - nav_file: SUMMARY.md + nav_file: SUMMARY.txt - coverage - mkdocstrings: handlers: python: - import: + 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 @@ -164,6 +166,17 @@ plugins: 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 diff --git a/pyproject.toml b/pyproject.toml index 45222bee..68a8669b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,8 @@ build-backend = "pdm.backend" name = "mkdocstrings" description = "Automatic documentation from sources, for MkDocs." authors = [{name = "Timothรฉe Mazzucotelli", email = "dev@pawamoy.fr"}] -license = {text = "ISC"} +license = "ISC" +license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"] @@ -87,15 +88,15 @@ data = [ ] [dependency-groups] -dev = [ - # maintenance +maintain = [ "build>=1.2", "git-changelog>=2.5", "twine>=5.1", - - # ci - "dirty-equals>=0.9.0", - "duty>=1.4", + "yore>=0.3.3", +] +ci = [ + "dirty-equals>=0.9", + "duty>=1.6", "ruff>=0.4", "pytest>=8.2", "pytest-cov>=5.0", @@ -104,8 +105,8 @@ dev = [ "mypy>=1.10", "types-markdown>=3.6", "types-pyyaml>=6.0", - - # docs +] + docs = [ "markdown-callouts>=0.4", "markdown-exec>=1.8", "mkdocs>=1.6", @@ -113,10 +114,15 @@ dev = [ "mkdocs-gen-files>=0.5", "mkdocs-git-revision-date-localized-plugin>=1.2", "mkdocs-literate-nav>=0.6", + "mkdocs-llmstxt>=0.1", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", + "mkdocs-section-index>=0.3", "mkdocstrings-python>=1.16.2", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] + +[tool.uv] +default-groups = ["maintain", "ci", "docs"] diff --git a/scripts/gen_ref_nav.py b/scripts/gen_api_ref.py similarity index 80% rename from scripts/gen_ref_nav.py rename to scripts/gen_api_ref.py index 676981b6..7f4cb961 100644 --- a/scripts/gen_ref_nav.py +++ b/scripts/gen_api_ref.py @@ -1,4 +1,4 @@ -"""Generate the code reference pages and navigation.""" +# Generate the API reference pages and navigation. from pathlib import Path @@ -12,7 +12,7 @@ for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") - doc_path = path.relative_to(src / "mkdocstrings").with_suffix(".md") + doc_path = path.relative_to(src).with_suffix(".md") full_doc_path = Path("reference", doc_path) parts = tuple(module_path.parts) @@ -21,7 +21,8 @@ parts = parts[:-1] doc_path = doc_path.with_name("index.md") full_doc_path = full_doc_path.with_name("index.md") - elif parts[-1].startswith("_"): + + if any(part.startswith("_") for part in parts): continue nav_parts = [f"{mod_symbol} {part}" for part in parts] @@ -33,5 +34,5 @@ mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: +with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index 721ac05d..b5499b7a 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -1,4 +1,4 @@ -"""Script to generate the project's credits.""" +# Script to generate the project's credits. from __future__ import annotations @@ -27,7 +27,7 @@ pyproject = tomllib.load(pyproject_file) project = pyproject["project"] project_name = project["name"] -devdeps = [dep for dep in pyproject["dependency-groups"]["dev"] if not dep.startswith("-e")] +devdeps = [dep for group in pyproject["dependency-groups"].values() for dep in group if not dep.startswith("-e")] PackageMetadata = dict[str, Union[str, Iterable[str]]] Metadata = dict[str, PackageMetadata] diff --git a/scripts/get_version.py b/scripts/get_version.py index f4a30a8c..6734e5b6 100644 --- a/scripts/get_version.py +++ b/scripts/get_version.py @@ -1,4 +1,4 @@ -"""Get current project version from Git tags or changelog.""" +# Get current project version from Git tags or changelog. import re from contextlib import suppress @@ -13,7 +13,6 @@ def get_version() -> str: - """Get current project version from Git tags or changelog.""" scm_version = get_version_from_scm(_root) or _default_scm_version if scm_version.version <= Version("0.1"): # Missing Git tags? with suppress(OSError, StopIteration): # noqa: SIM117 diff --git a/scripts/insiders.py b/scripts/insiders.py index a7da99bc..6535a31e 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -1,4 +1,4 @@ -"""Functions related to Insiders funding goals.""" +# Functions related to Insiders funding goals. from __future__ import annotations @@ -23,7 +23,7 @@ logger = logging.getLogger(f"mkdocs.logs.{__name__}") -def human_readable_amount(amount: int) -> str: # noqa: D103 +def human_readable_amount(amount: int) -> str: str_amount = str(amount) if len(str_amount) >= 4: # noqa: PLR2004 return f"{str_amount[: len(str_amount) - 3]},{str_amount[-3:]}" @@ -32,16 +32,12 @@ def human_readable_amount(amount: int) -> str: # noqa: D103 @dataclass class Project: - """Class representing an Insiders project.""" - name: str url: str @dataclass class Feature: - """Class representing an Insiders feature.""" - name: str ref: str | None since: date | None @@ -68,8 +64,6 @@ def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: @dataclass class Goal: - """Class representing an Insiders goal.""" - name: str amount: int features: list[Feature] @@ -94,16 +88,6 @@ def render(self, rel_base: str = "..") -> None: # noqa: D102 def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]: - """Load goals from JSON data. - - Parameters: - data: The JSON data. - funding: The current total funding, per month. - origin: The origin of the data (URL). - - Returns: - A dictionaries of goals, keys being their target monthly amount. - """ goals_data = yaml.safe_load(data)["goals"] return { amount: Goal( @@ -151,15 +135,6 @@ def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[in def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]: - """Load funding goals from a given data source. - - Parameters: - source: The data source (local file path or URL). - funding: The current total funding, per month. - - Returns: - A dictionaries of goals, keys being their target monthly amount. - """ if isinstance(source, str): return _load_goals_from_disk(source, funding) goals = {} @@ -174,18 +149,10 @@ def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = def feature_list(goals: Iterable[Goal]) -> list[Feature]: - """Extract feature list from funding goals. - - Parameters: - goals: A list of funding goals. - - Returns: - A list of features. - """ return list(chain.from_iterable(goal.features for goal in goals)) -def load_json(url: str) -> str | list | dict: # noqa: D103 +def load_json(url: str) -> str | list | dict: with urlopen(url) as response: # noqa: S310 return json.loads(response.read().decode()) diff --git a/scripts/make.py b/scripts/make.py index 3d427296..55679baa 100755 --- a/scripts/make.py +++ b/scripts/make.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -"""Management commands.""" - from __future__ import annotations import os diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py index 03550f9b..296931fa 100644 --- a/src/mkdocstrings/__init__.py +++ b/src/mkdocstrings/__init__.py @@ -2,3 +2,7 @@ Automatic documentation from sources, for MkDocs. """ + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/src/mkdocstrings/_internal/__init__.py b/src/mkdocstrings/_internal/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/mkdocstrings/debug.py b/src/mkdocstrings/_internal/debug.py similarity index 82% rename from src/mkdocstrings/debug.py rename to src/mkdocstrings/_internal/debug.py index b5da78f2..7b56409b 100644 --- a/src/mkdocstrings/debug.py +++ b/src/mkdocstrings/_internal/debug.py @@ -1,5 +1,3 @@ -"""Debugging utilities.""" - from __future__ import annotations import os @@ -10,7 +8,7 @@ @dataclass -class Variable: +class _Variable: """Dataclass describing an environment variable.""" name: str @@ -20,7 +18,7 @@ class Variable: @dataclass -class Package: +class _Package: """Dataclass describing a Python package.""" name: str @@ -30,7 +28,7 @@ class Package: @dataclass -class Environment: +class _Environment: """Dataclass to store environment information.""" interpreter_name: str @@ -41,9 +39,9 @@ class Environment: """Path to Python executable.""" platform: str """Operating System.""" - packages: list[Package] + packages: list[_Package] """Installed packages.""" - variables: list[Variable] + variables: list[_Variable] """Environment variables.""" @@ -58,7 +56,7 @@ def _interpreter_name_version() -> tuple[str, str]: return "", "0.0.0" -def get_version(dist: str = "mkdocstrings") -> str: +def _get_version(dist: str = "mkdocstrings") -> str: """Get version of the given distribution. Parameters: @@ -73,7 +71,7 @@ def get_version(dist: str = "mkdocstrings") -> str: return "0.0.0" -def get_debug_info() -> Environment: +def _get_debug_info() -> _Environment: """Get debug/environment information. Returns: @@ -82,19 +80,19 @@ def get_debug_info() -> Environment: py_name, py_version = _interpreter_name_version() packages = ["mkdocstrings"] variables = ["PYTHONPATH", *[var for var in os.environ if var.startswith("MKDOCSTRINGS")]] - return Environment( + return _Environment( interpreter_name=py_name, interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), - variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], - packages=[Package(pkg, get_version(pkg)) for pkg in packages], + variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], + packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) -def print_debug_info() -> None: +def _print_debug_info() -> None: """Print debug/environment information.""" - info = get_debug_info() + info = _get_debug_info() print(f"- __System__: {info.platform}") print(f"- __Python__: {info.interpreter_name} {info.interpreter_version} ({info.interpreter_path})") print("- __Environment variables__:") @@ -106,4 +104,4 @@ def print_debug_info() -> None: if __name__ == "__main__": - print_debug_info() + _print_debug_info() diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 00000000..da4a370a --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,177 @@ +"""Tests for our own API exposition.""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import TYPE_CHECKING + +import griffe +import pytest + +import mkdocstrings +from mkdocstrings.inventory import Inventory + +if TYPE_CHECKING: + from collections.abc import Iterator + + +@pytest.fixture(name="loader", scope="module") +def _fixture_loader() -> griffe.GriffeLoader: + loader = griffe.GriffeLoader() + loader.load("mkdocstrings") + loader.resolve_aliases() + return loader + + +@pytest.fixture(name="internal_api", scope="module") +def _fixture_internal_api(loader: griffe.GriffeLoader) -> griffe.Module: + return loader.modules_collection["mkdocstrings._internal"] + + +@pytest.fixture(name="public_api", scope="module") +def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: + return loader.modules_collection["mkdocstrings"] + + +def _yield_public_objects( + obj: griffe.Module | griffe.Class, + *, + modules: bool = False, + modulelevel: bool = True, + inherited: bool = False, + special: bool = False, +) -> Iterator[griffe.Object | griffe.Alias]: + for member in obj.all_members.values() if inherited else obj.members.values(): + try: + if member.is_module: + if member.is_alias or not member.is_public: + continue + if modules: + yield member + yield from _yield_public_objects( + member, # type: ignore[arg-type] + modules=modules, + modulelevel=modulelevel, + inherited=inherited, + special=special, + ) + elif member.is_public and (special or not member.is_special): + yield member + else: + continue + if member.is_class and not modulelevel: + yield from _yield_public_objects( + member, # type: ignore[arg-type] + modules=modules, + modulelevel=False, + inherited=inherited, + special=special, + ) + except (griffe.AliasResolutionError, griffe.CyclicAliasError): + continue + + +@pytest.fixture(name="modulelevel_internal_objects", scope="module") +def _fixture_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: + return list(_yield_public_objects(internal_api, modulelevel=True)) + + +@pytest.fixture(name="internal_objects", scope="module") +def _fixture_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: + return list(_yield_public_objects(internal_api, modulelevel=False, special=True)) + + +@pytest.fixture(name="public_objects", scope="module") +def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: + return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) + + +@pytest.fixture(name="inventory", scope="module") +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.") + with inventory_file.open("rb") as file: + return Inventory.parse_sphinx(file) + + +def test_exposed_objects(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: + """All public objects in the internal API are exposed under `mkdocstrings`.""" + not_exposed = [ + obj.path + for obj in modulelevel_internal_objects + if obj.name not in mkdocstrings.__all__ or not hasattr(mkdocstrings, obj.name) + ] + assert not not_exposed, "Objects not exposed:\n" + "\n".join(sorted(not_exposed)) + + +def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: + """All internal objects have unique names.""" + names_to_paths = defaultdict(list) + for obj in modulelevel_internal_objects: + names_to_paths[obj.name].append(obj.path) + non_unique = [paths for paths in names_to_paths.values() if len(paths) > 1] + assert not non_unique, "Non-unique names:\n" + "\n".join(str(paths) for paths in non_unique) + + +def test_single_locations(public_api: griffe.Module) -> None: + """All objects have a single public location.""" + + def _public_path(obj: griffe.Object | griffe.Alias) -> bool: + return obj.is_public and (obj.parent is None or _public_path(obj.parent)) + + multiple_locations = {} + for obj_name in mkdocstrings.__all__: + obj = public_api[obj_name] + if obj.aliases and ( + public_aliases := [path for path, alias in obj.aliases.items() if path != obj.path and _public_path(alias)] + ): + multiple_locations[obj.path] = public_aliases + assert not multiple_locations, "Multiple public locations:\n" + "\n".join( + f"{path}: {aliases}" for path, aliases in multiple_locations.items() + ) + + +def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias]) -> None: + """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 + ] + msg = "Objects not in the inventory (try running `make run mkdocs build`):\n{paths}" + assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) + + +def test_inventory_matches_api( + inventory: Inventory, + public_objects: list[griffe.Object | griffe.Alias], + loader: griffe.GriffeLoader, +) -> None: + """The inventory doesn't contain any additional Python object.""" + not_in_api = [] + 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: + obj = loader.modules_collection[item.name] + 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}" + assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api))) + + +def test_no_module_docstrings_in_internal_api(internal_api: griffe.Module) -> None: + """No module docstrings should be written in our internal API. + + The reasoning is that docstrings are addressed to users of the public API, + but internal modules are not exposed to users, so they should not have docstrings. + """ + + def _modules(obj: griffe.Module) -> Iterator[griffe.Module]: + for member in obj.modules.values(): + yield member + yield from _modules(member) + + for obj in _modules(internal_api): + assert not obj.docstring From caf2fff2d2bc83bbfd974f5e45a8a0c5d7c57f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 16:51:43 +0100 Subject: [PATCH 161/223] tests: Simplify the test that disables the plugin --- tests/test_plugin.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 3342e2aa..9cfda5b5 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -20,11 +20,20 @@ def test_disabling_plugin(tmp_path: Path) -> None: docs_dir.mkdir() site_dir.mkdir() docs_dir.joinpath("index.md").write_text("::: mkdocstrings") + config_file = tmp_path / "mkdocs.yml" + config_file.write_text( + """ + site_name: Test + theme: mkdocs + plugins: + - mkdocstrings: + enabled: false + """ + ) - mkdocs_config = load_config() + mkdocs_config = load_config(str(config_file)) mkdocs_config["docs_dir"] = str(docs_dir) mkdocs_config["site_dir"] = str(site_dir) - mkdocs_config["plugins"]["mkdocstrings"].config["enabled"] = False mkdocs_config["plugins"].run_event("startup", command="build", dirty=False) try: build(mkdocs_config) From 23fe23f11011d0470a6342ca85e060e5ac2b6bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 16:57:05 +0100 Subject: [PATCH 162/223] refactor: Move modules to internal folder --- .../{_download.py => _internal/download.py} | 2 +- src/mkdocstrings/{ => _internal}/extension.py | 2 +- .../{ => _internal}/handlers/__init__.py | 0 .../{ => _internal}/handlers/base.py | 6 +++--- .../{ => _internal}/handlers/rendering.py | 6 +++--- src/mkdocstrings/{ => _internal}/inventory.py | 0 src/mkdocstrings/{ => _internal}/loggers.py | 0 src/mkdocstrings/{ => _internal}/plugin.py | 4 ++-- tests/conftest.py | 2 +- tests/test_api.py | 2 +- tests/test_download.py | 16 ++++++++-------- tests/test_extension.py | 2 +- tests/test_handlers.py | 2 +- tests/test_inventory.py | 2 +- tests/test_loggers.py | 2 +- tests/test_plugin.py | 4 ++-- 16 files changed, 26 insertions(+), 26 deletions(-) rename src/mkdocstrings/{_download.py => _internal/download.py} (98%) rename src/mkdocstrings/{ => _internal}/extension.py (99%) rename src/mkdocstrings/{ => _internal}/handlers/__init__.py (100%) rename src/mkdocstrings/{ => _internal}/handlers/base.py (99%) rename src/mkdocstrings/{ => _internal}/handlers/rendering.py (97%) rename src/mkdocstrings/{ => _internal}/inventory.py (100%) rename src/mkdocstrings/{ => _internal}/loggers.py (100%) rename src/mkdocstrings/{ => _internal}/plugin.py (98%) diff --git a/src/mkdocstrings/_download.py b/src/mkdocstrings/_internal/download.py similarity index 98% rename from src/mkdocstrings/_download.py rename to src/mkdocstrings/_internal/download.py index b9af327d..999075fe 100644 --- a/src/mkdocstrings/_download.py +++ b/src/mkdocstrings/_internal/download.py @@ -7,7 +7,7 @@ from collections.abc import Mapping from typing import BinaryIO, Optional -from mkdocstrings.loggers import get_logger +from mkdocstrings._internal.loggers import get_logger log = get_logger(__name__) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/_internal/extension.py similarity index 99% rename from src/mkdocstrings/extension.py rename to src/mkdocstrings/_internal/extension.py index ea38b83f..ee359b93 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -35,8 +35,8 @@ from markdown.treeprocessors import Treeprocessor from mkdocs.exceptions import PluginError +from mkdocstrings._internal.loggers import get_logger from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers -from mkdocstrings.loggers import get_logger if TYPE_CHECKING: from collections.abc import MutableSequence diff --git a/src/mkdocstrings/handlers/__init__.py b/src/mkdocstrings/_internal/handlers/__init__.py similarity index 100% rename from src/mkdocstrings/handlers/__init__.py rename to src/mkdocstrings/_internal/handlers/__init__.py diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py similarity index 99% rename from src/mkdocstrings/handlers/base.py rename to src/mkdocstrings/_internal/handlers/base.py index e967af5f..8cf9b834 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -25,7 +25,9 @@ # TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. from mkdocs_get_deps.cache import download_and_cache_url -from mkdocstrings._download import download_url_with_gz +from mkdocstrings._internal.download import download_url_with_gz +from mkdocstrings._internal.inventory import Inventory +from mkdocstrings._internal.loggers import get_logger, get_template_logger from mkdocstrings.handlers.rendering import ( HeadingShiftingTreeprocessor, Highlighter, @@ -33,8 +35,6 @@ MkdocstringsInnerExtension, ParagraphStrippingTreeprocessor, ) -from mkdocstrings.inventory import Inventory -from mkdocstrings.loggers import get_logger, get_template_logger # TODO: remove once support for Python 3.9 is dropped if sys.version_info < (3, 10): diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/_internal/handlers/rendering.py similarity index 97% rename from src/mkdocstrings/handlers/rendering.py rename to src/mkdocstrings/_internal/handlers/rendering.py index 1db3c8f1..0fab5cc6 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/_internal/handlers/rendering.py @@ -148,7 +148,7 @@ def __init__(self, md: Markdown, id_prefix: str): super().__init__(md) self.id_prefix = id_prefix - def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) + def run(self, root: Element) -> None: if self.id_prefix: self._prefix_ids(root) @@ -205,7 +205,7 @@ def __init__(self, md: Markdown, shift_by: int): super().__init__(md) self.shift_by = shift_by - def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring) + def run(self, root: Element) -> None: if not self.shift_by: return for el in root.iter(): @@ -247,7 +247,7 @@ class ParagraphStrippingTreeprocessor(Treeprocessor): name = "mkdocstrings_strip_paragraph" strip = False - def run(self, root: Element) -> Element | None: # noqa: D102 (ignore missing docstring) + def run(self, root: Element) -> Element | None: if self.strip and len(root) == 1 and root[0].tag == "p": # Turn the single

    element into the root element and inherit its tag name (it's significant!) root[0].tag = root.tag diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/_internal/inventory.py similarity index 100% rename from src/mkdocstrings/inventory.py rename to src/mkdocstrings/_internal/inventory.py diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/_internal/loggers.py similarity index 100% rename from src/mkdocstrings/loggers.py rename to src/mkdocstrings/_internal/loggers.py diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/_internal/plugin.py similarity index 98% rename from src/mkdocstrings/plugin.py rename to src/mkdocstrings/_internal/plugin.py index 9cda9696..e037db1b 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -26,9 +26,9 @@ from mkdocs.utils import write_file from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin -from mkdocstrings.extension import MkdocstringsExtension +from mkdocstrings._internal.extension import MkdocstringsExtension +from mkdocstrings._internal.loggers import get_logger from mkdocstrings.handlers.base import BaseHandler, Handlers -from mkdocstrings.loggers import get_logger if sys.version_info < (3, 10): from typing_extensions import ParamSpec diff --git a/tests/conftest.py b/tests/conftest.py index 74688fe7..8a132e29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,7 @@ from mkdocs import config - from mkdocstrings.plugin import MkdocstringsPlugin + from mkdocstrings._internal.plugin import MkdocstringsPlugin @pytest.fixture(name="mkdocs_conf") diff --git a/tests/test_api.py b/tests/test_api.py index da4a370a..52e2303c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,7 +10,7 @@ import pytest import mkdocstrings -from mkdocstrings.inventory import Inventory +from mkdocstrings._internal.inventory import Inventory if TYPE_CHECKING: from collections.abc import Iterator diff --git a/tests/test_download.py b/tests/test_download.py index 95dc0233..47e5c479 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -7,7 +7,7 @@ import pytest -from mkdocstrings import _download +from mkdocstrings._internal import download if TYPE_CHECKING: from collections.abc import Mapping @@ -32,7 +32,7 @@ ) def test_expand_env_vars(credential: str, expected: str, env: Mapping[str, str]) -> None: """Test expanding environment variables.""" - assert _download._expand_env_vars(credential, url="https://test.example.com", env=env) == expected + assert download._expand_env_vars(credential, url="https://test.example.com", env=env) == expected def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) -> None: @@ -41,7 +41,7 @@ def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) credential = "${USER}" env: dict[str, str] = {} - assert _download._expand_env_vars(credential, url="https://test.example.com", env=env) == "${USER}" + assert download._expand_env_vars(credential, url="https://test.example.com", env=env) == "${USER}" output = caplog.records[0].getMessage() assert "'USER' is not set" in output @@ -61,20 +61,20 @@ def test_expand_env_vars_with_missing_env_var(caplog: pytest.LogCaptureFixture) ) def test_extract_auth_from_url(monkeypatch: pytest.MonkeyPatch, url: str, expected_url: str) -> None: """Test extracting the auth part from the URL.""" - monkeypatch.setattr(_download, "_create_auth_header", lambda *args, **kwargs: {}) - result_url, _result_auth_header = _download._extract_auth_from_url(url) + monkeypatch.setattr(download, "_create_auth_header", lambda *args, **kwargs: {}) + result_url, _result_auth_header = download._extract_auth_from_url(url) assert result_url == expected_url def test_create_auth_header_basic_auth() -> None: """Test creating the Authorization header for basic authentication.""" - auth_header = _download._create_auth_header(credential="testuser:testpass", url="https://test.example.com") + auth_header = download._create_auth_header(credential="testuser:testpass", url="https://test.example.com") assert auth_header == {"Authorization": "Basic dGVzdHVzZXI6dGVzdHBhc3M="} def test_create_auth_header_bearer_auth() -> None: """Test creating the Authorization header for bearer token authentication.""" - auth_header = _download._create_auth_header(credential="token123", url="https://test.example.com") + auth_header = download._create_auth_header(credential="token123", url="https://test.example.com") assert auth_header == {"Authorization": "Bearer token123"} @@ -96,7 +96,7 @@ def test_create_auth_header_bearer_auth() -> None: ) def test_env_var_pattern(var: str, match: str | None) -> None: """Test the environment variable regex pattern.""" - _match = _download.ENV_VAR_PATTERN.match(var) + _match = download.ENV_VAR_PATTERN.match(var) if _match is None: assert match is _match else: diff --git a/tests/test_extension.py b/tests/test_extension.py index 12723cf5..acfce0cd 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from markdown import Markdown - from mkdocstrings.plugin import MkdocstringsPlugin + from mkdocstrings._internal.plugin import MkdocstringsPlugin @pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"footnotes": {}}]}], indirect=["ext_markdown"]) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index cb39023d..7bc3086c 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from pathlib import Path - from mkdocstrings.plugin import MkdocstringsPlugin + from mkdocstrings._internal.plugin import MkdocstringsPlugin @pytest.mark.parametrize("extension_name", ["codehilite", "pymdownx.highlight"]) diff --git a/tests/test_inventory.py b/tests/test_inventory.py index ecbb3cd2..46e1c039 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -10,7 +10,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config -from mkdocstrings.inventory import Inventory, InventoryItem +from mkdocstrings._internal.inventory import Inventory, InventoryItem sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") diff --git a/tests/test_loggers.py b/tests/test_loggers.py index 1644c0f0..d2652049 100644 --- a/tests/test_loggers.py +++ b/tests/test_loggers.py @@ -4,7 +4,7 @@ import pytest -from mkdocstrings.loggers import get_logger, get_template_logger +from mkdocstrings._internal.loggers import get_logger, get_template_logger @pytest.mark.parametrize( diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 9cfda5b5..df568306 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -7,7 +7,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config -from mkdocstrings.plugin import MkdocstringsPlugin +from mkdocstrings._internal.plugin import MkdocstringsPlugin if TYPE_CHECKING: from pathlib import Path @@ -28,7 +28,7 @@ def test_disabling_plugin(tmp_path: Path) -> None: plugins: - mkdocstrings: enabled: false - """ + """, ) mkdocs_config = load_config(str(config_file)) From e66e08096d45f6790492d9a0b767d512e42f67a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 17:46:13 +0100 Subject: [PATCH 163/223] refactor: Re-expose public API in the top-level `mkdocstrings` module --- config/pytest.ini | 1 + docs/usage/handlers.md | 4 +- docs/usage/index.md | 2 +- docs/usage/theming.md | 4 +- pyproject.toml | 2 +- src/mkdocstrings/__init__.py | 66 ++++++++++++++++++++- src/mkdocstrings/_internal/extension.py | 10 ++-- src/mkdocstrings/_internal/handlers/base.py | 16 ++--- src/mkdocstrings/_internal/plugin.py | 16 ++--- src/mkdocstrings/extension.py | 15 +++++ src/mkdocstrings/handlers/__init__.py | 1 + src/mkdocstrings/handlers/base.py | 15 +++++ src/mkdocstrings/handlers/rendering.py | 15 +++++ src/mkdocstrings/inventory.py | 15 +++++ src/mkdocstrings/loggers.py | 15 +++++ src/mkdocstrings/plugin.py | 15 +++++ tests/test_api.py | 2 +- tests/test_extension.py | 2 +- tests/test_handlers.py | 4 +- tests/test_inventory.py | 2 +- tests/test_loggers.py | 2 +- tests/test_plugin.py | 2 +- 22 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 src/mkdocstrings/extension.py create mode 100644 src/mkdocstrings/handlers/__init__.py create mode 100644 src/mkdocstrings/handlers/base.py create mode 100644 src/mkdocstrings/handlers/rendering.py create mode 100644 src/mkdocstrings/inventory.py create mode 100644 src/mkdocstrings/loggers.py create mode 100644 src/mkdocstrings/plugin.py diff --git a/config/pytest.ini b/config/pytest.ini index 4c6549d9..288b6cff 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -11,3 +11,4 @@ testpaths = filterwarnings = error ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings + ignore:.*Importing from:DeprecationWarning:mkdocstrings_handlers diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index dcf4c5e3..b9a01f68 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -101,7 +101,7 @@ Since version 0.14, you can create and use custom handlers thanks to namespace packages. For more information about namespace packages, [see their documentation](https://packaging.python.org/guides/packaging-namespace-packages/). -TIP: **TL;DR - Project template for handlers.** +TIP: **TL;DR - Project template for handlers.** *mkdocstrings* provides a [Copier](https://github.com/copier-org/copier) template to kickstart new handlers: https://github.com/mkdocstrings/handler-template. To use it, install Copier (`pipx install copier`), then run `copier gh:mkdocstrings/handler-template my_handler` @@ -128,7 +128,7 @@ NOTE: **Note the absence of `__init__.py` module in `mkdocstrings_handlers`!** ### Code A handler is a subclass of the base handler provided by *mkdocstrings*. -See the documentation for the [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler]. +See the documentation for the [`BaseHandler`][mkdocstrings.BaseHandler]. Subclasses of the base handler must declare a `name` and `domain` as class attributes, as well as implement the following methods: diff --git a/docs/usage/index.md b/docs/usage/index.md index 133b1251..ea9716cc 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -249,7 +249,7 @@ In the example below you see the identifier to be linked is `foo.bar--tips`, bec The above tip about [Finding out the anchor](#finding-out-the-anchor) also applies the same way here. -You may also notice that such a heading does not get rendered as a `

    ` element directly, but rather the level gets shifted to fit the encompassing document structure. If you're curious about the implementation, check out [mkdocstrings.handlers.rendering.HeadingShiftingTreeprocessor][] and others. +You may also notice that such a heading does not get rendered as a `

    ` element directly, but rather the level gets shifted to fit the encompassing document structure. If you're curious about the implementation, check out [mkdocstrings.HeadingShiftingTreeprocessor][] and others. ### Cross-references to other projects / inventories diff --git a/docs/usage/theming.md b/docs/usage/theming.md index 09ee92fd..04d053f6 100644 --- a/docs/usage/theming.md +++ b/docs/usage/theming.md @@ -86,11 +86,11 @@ we cannot list them all here. See the documentation about CSS classes for: ### Syntax highlighting -Code blocks that occur in the docstring of an item inserted with *mkdocstrings*, as well as code blocks (such as *Source code*) that *mkdocstrings* inserts itself, are syntax-highlighted according to the same rules as other normal code blocks in your document. See more details in [mkdocstrings.handlers.rendering.Highlighter][]. +Code blocks that occur in the docstring of an item inserted with *mkdocstrings*, as well as code blocks (such as *Source code*) that *mkdocstrings* inserts itself, are syntax-highlighted according to the same rules as other normal code blocks in your document. See more details in [mkdocstrings.Highlighter][]. As for the CSS class used for code blocks -- it will also match the "normal" config, so the default (`.codehilite` or `.highlight`) will match your chosen Markdown extension for highlighting. -IMPORTANT: **Changed in version 0.15.** +IMPORTANT: **Changed in version 0.15.** The CSS class used to always be `.highlight`, but now it depends on the configuration. Long story short, you probably should add `pymdownx.highlight` to your `markdown_extensions`, and then use `.doc-contents .highlight` as the CSS selector in case you want to change something about *mkdocstrings'* code blocks specifically. diff --git a/pyproject.toml b/pyproject.toml index 68a8669b..35872a76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ Gitter = "https://gitter.im/mkdocstrings/community" Funding = "https://github.com/sponsors/pawamoy" [project.entry-points."mkdocs.plugins"] -mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin" +mkdocstrings = "mkdocstrings:MkdocstringsPlugin" [tool.pdm.version] source = "call" diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py index 296931fa..d6a01b2b 100644 --- a/src/mkdocstrings/__init__.py +++ b/src/mkdocstrings/__init__.py @@ -5,4 +5,68 @@ from __future__ import annotations -__all__: list[str] = [] +from mkdocstrings._internal.extension import AutoDocProcessor, MkdocstringsExtension +from mkdocstrings._internal.handlers.base import ( + BaseHandler, + CollectionError, + CollectorItem, + HandlerConfig, + HandlerOptions, + Handlers, + ThemeNotSupported, + do_any, +) +from mkdocstrings._internal.handlers.rendering import ( + HeadingShiftingTreeprocessor, + Highlighter, + IdPrependingTreeprocessor, + MkdocstringsInnerExtension, + ParagraphStrippingTreeprocessor, +) +from mkdocstrings._internal.inventory import Inventory, InventoryItem +from mkdocstrings._internal.loggers import ( + LoggerAdapter, + TemplateLogger, + get_logger, + get_template_logger, + get_template_logger_function, + get_template_path, +) +from mkdocstrings._internal.plugin import ( + InventoryImportType, + InventoryLoaderType, + MkdocstringsPlugin, + PluginConfig, + list_to_tuple, +) + +__all__: list[str] = [ + "AutoDocProcessor", + "BaseHandler", + "CollectionError", + "CollectorItem", + "HandlerConfig", + "HandlerOptions", + "Handlers", + "HeadingShiftingTreeprocessor", + "Highlighter", + "IdPrependingTreeprocessor", + "Inventory", + "InventoryImportType", + "InventoryItem", + "InventoryLoaderType", + "LoggerAdapter", + "MkdocstringsExtension", + "MkdocstringsInnerExtension", + "MkdocstringsPlugin", + "ParagraphStrippingTreeprocessor", + "PluginConfig", + "TemplateLogger", + "ThemeNotSupported", + "do_any", + "get_logger", + "get_template_logger", + "get_template_logger_function", + "get_template_path", + "list_to_tuple", +] diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index ee359b93..1c46f4a9 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -3,7 +3,7 @@ The extension is composed of a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockparser) that matches indented blocks starting with a line like `::: identifier`. -For each of these blocks, it uses a [handler][mkdocstrings.handlers.base.BaseHandler] to collect documentation about +For each of these blocks, it uses a [handler][mkdocstrings.BaseHandler] to collect documentation about the given identifier and render it with Jinja templates. Both the collection and rendering process can be configured by adding YAML configuration under the "autodoc" @@ -35,8 +35,8 @@ from markdown.treeprocessors import Treeprocessor from mkdocs.exceptions import PluginError +from mkdocstrings._internal.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers from mkdocstrings._internal.loggers import get_logger -from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers if TYPE_CHECKING: from collections.abc import MutableSequence @@ -51,8 +51,8 @@ class AutoDocProcessor(BlockProcessor): """Our "autodoc" Markdown block processor. - It has a [`test` method][mkdocstrings.extension.AutoDocProcessor.test] that tells if a block matches a criterion, - and a [`run` method][mkdocstrings.extension.AutoDocProcessor.run] that processes it. + It has a [`test` method][mkdocstrings.AutoDocProcessor.test] that tells if a block matches a criterion, + and a [`run` method][mkdocstrings.AutoDocProcessor.run] that processes it. It also has utility methods allowing to get handlers and their configuration easily, useful when processing a matched block. @@ -342,7 +342,7 @@ def __init__(self, handlers: Handlers, autorefs: AutorefsPlugin, **kwargs: Any) def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name) """Register the extension. - Add an instance of our [`AutoDocProcessor`][mkdocstrings.extension.AutoDocProcessor] to the Markdown parser. + Add an instance of our [`AutoDocProcessor`][mkdocstrings.AutoDocProcessor] to the Markdown parser. Arguments: md: A `markdown.Markdown` instance. diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 8cf9b834..385eed43 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -26,15 +26,15 @@ from mkdocs_get_deps.cache import download_and_cache_url from mkdocstrings._internal.download import download_url_with_gz -from mkdocstrings._internal.inventory import Inventory -from mkdocstrings._internal.loggers import get_logger, get_template_logger -from mkdocstrings.handlers.rendering import ( +from mkdocstrings._internal.handlers.rendering import ( HeadingShiftingTreeprocessor, Highlighter, IdPrependingTreeprocessor, MkdocstringsInnerExtension, ParagraphStrippingTreeprocessor, ) +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): @@ -547,8 +547,8 @@ def _update_env(self, md: Markdown, *, config: Any | None = None) -> None: class Handlers: """A collection of handlers. - Do not instantiate this directly. [The plugin][mkdocstrings.plugin.MkdocstringsPlugin] will keep one instance of - this for the purpose of caching. Use [mkdocstrings.plugin.MkdocstringsPlugin.get_handler][] for convenient access. + Do not instantiate this directly. [The plugin][mkdocstrings.MkdocstringsPlugin] will keep one instance of + this for the purpose of caching. Use [mkdocstrings.MkdocstringsPlugin.get_handler][] for convenient access. """ def __init__( @@ -595,7 +595,7 @@ 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.handlers.base.BaseHandler.collect] can accept). + 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. @@ -654,7 +654,7 @@ def get_handler(self, name: str, handler_config: dict | None = None) -> BaseHand handler_config: Configuration passed to the handler. Returns: - An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler], + An instance of a subclass of [`BaseHandler`][mkdocstrings.BaseHandler], as instantiated by the `get_handler` method of the handler's module. """ if name not in self._handlers: @@ -749,7 +749,7 @@ def seen_handlers(self) -> Iterable[BaseHandler]: """Get the handlers that were encountered so far throughout the build. Returns: - An iterable of instances of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler] + An iterable of instances of [`BaseHandler`][mkdocstrings.BaseHandler] (usable only to loop through it). """ return self._handlers.values() diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index e037db1b..37d77671 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -1,11 +1,11 @@ """This module contains the "mkdocstrings" plugin for MkDocs. -The plugin instantiates a Markdown extension ([`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension]), +The plugin instantiates a Markdown extension ([`MkdocstringsExtension`][mkdocstrings.MkdocstringsExtension]), and adds it to the list of Markdown extensions used by `mkdocs` during the [`on_config` event hook](https://www.mkdocs.org/user-guide/plugins/#on_config). Once the documentation is built, the [`on_post_build` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_build) -is triggered and calls the [`handlers.teardown()` method][mkdocstrings.handlers.base.Handlers.teardown]. This method is +is triggered and calls the [`handlers.teardown()` method][mkdocstrings.Handlers.teardown]. This method is used to teardown the handlers that were instantiated during documentation buildup. Finally, when serving the documentation, it can add directories to watch @@ -27,8 +27,8 @@ from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin from mkdocstrings._internal.extension import MkdocstringsExtension +from mkdocstrings._internal.handlers.base import BaseHandler, Handlers from mkdocstrings._internal.loggers import get_logger -from mkdocstrings.handlers.base import BaseHandler, Handlers if sys.version_info < (3, 10): from typing_extensions import ParamSpec @@ -120,13 +120,13 @@ def __init__(self) -> None: @property def handlers(self) -> Handlers: - """Get the instance of [mkdocstrings.handlers.base.Handlers][] for this plugin/build. + """Get the instance of [mkdocstrings.Handlers][] for this plugin/build. Raises: RuntimeError: If the plugin hasn't been initialized with a config. Returns: - An instance of [mkdocstrings.handlers.base.Handlers][] (the same throughout the build). + An instance of [mkdocstrings.Handlers][] (the same throughout the build). """ if not self._handlers: raise RuntimeError("The plugin hasn't been initialized with a config yet") @@ -136,7 +136,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: """Instantiate our Markdown extension. Hook for the [`on_config` event](https://www.mkdocs.org/user-guide/plugins/#on_config). - In this hook, we instantiate our [`MkdocstringsExtension`][mkdocstrings.extension.MkdocstringsExtension] + In this hook, we instantiate our [`MkdocstringsExtension`][mkdocstrings.MkdocstringsExtension] and add it to the list of Markdown extensions used by `mkdocs`. We pass this plugin's configuration dictionary to the extension when instantiating it (it will need it @@ -263,12 +263,12 @@ def on_post_build( self.handlers.teardown() def get_handler(self, handler_name: str) -> BaseHandler: - """Get a handler by its name. See [mkdocstrings.handlers.base.Handlers.get_handler][]. + """Get a handler by its name. See [mkdocstrings.Handlers.get_handler][]. Arguments: handler_name: The name of the handler. Returns: - An instance of a subclass of [`BaseHandler`][mkdocstrings.handlers.base.BaseHandler]. + An instance of a subclass of [`BaseHandler`][mkdocstrings.BaseHandler]. """ return self.handlers.get_handler(handler_name) diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py new file mode 100644 index 00000000..15a84cc8 --- /dev/null +++ b/src/mkdocstrings/extension.py @@ -0,0 +1,15 @@ +"""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 new file mode 100644 index 00000000..af032e98 --- /dev/null +++ b/src/mkdocstrings/handlers/__init__.py @@ -0,0 +1 @@ +"""Deprecated. Import from `mkdocstrings` directly.""" diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py new file mode 100644 index 00000000..82ee3edb --- /dev/null +++ b/src/mkdocstrings/handlers/base.py @@ -0,0 +1,15 @@ +"""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 new file mode 100644 index 00000000..940f3a9c --- /dev/null +++ b/src/mkdocstrings/handlers/rendering.py @@ -0,0 +1,15 @@ +"""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 new file mode 100644 index 00000000..b5c8adea --- /dev/null +++ b/src/mkdocstrings/inventory.py @@ -0,0 +1,15 @@ +"""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 new file mode 100644 index 00000000..ce805362 --- /dev/null +++ b/src/mkdocstrings/loggers.py @@ -0,0 +1,15 @@ +"""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 new file mode 100644 index 00000000..b4edf945 --- /dev/null +++ b/src/mkdocstrings/plugin.py @@ -0,0 +1,15 @@ +"""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/test_api.py b/tests/test_api.py index 52e2303c..7cf832d2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -10,7 +10,7 @@ import pytest import mkdocstrings -from mkdocstrings._internal.inventory import Inventory +from mkdocstrings import Inventory if TYPE_CHECKING: from collections.abc import Iterator diff --git a/tests/test_extension.py b/tests/test_extension.py index acfce0cd..b7c1c742 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from markdown import Markdown - from mkdocstrings._internal.plugin import MkdocstringsPlugin + from mkdocstrings import MkdocstringsPlugin @pytest.mark.parametrize("ext_markdown", [{"markdown_extensions": [{"footnotes": {}}]}], indirect=["ext_markdown"]) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 7bc3086c..30bdbdfc 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -10,12 +10,12 @@ from jinja2.exceptions import TemplateNotFound from markdown import Markdown -from mkdocstrings.handlers.base import Highlighter +from mkdocstrings import Highlighter if TYPE_CHECKING: from pathlib import Path - from mkdocstrings._internal.plugin import MkdocstringsPlugin + from mkdocstrings import MkdocstringsPlugin @pytest.mark.parametrize("extension_name", ["codehilite", "pymdownx.highlight"]) diff --git a/tests/test_inventory.py b/tests/test_inventory.py index 46e1c039..eb008661 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -10,7 +10,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config -from mkdocstrings._internal.inventory import Inventory, InventoryItem +from mkdocstrings import Inventory, InventoryItem sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") diff --git a/tests/test_loggers.py b/tests/test_loggers.py index d2652049..35e4dc86 100644 --- a/tests/test_loggers.py +++ b/tests/test_loggers.py @@ -4,7 +4,7 @@ import pytest -from mkdocstrings._internal.loggers import get_logger, get_template_logger +from mkdocstrings import get_logger, get_template_logger @pytest.mark.parametrize( diff --git a/tests/test_plugin.py b/tests/test_plugin.py index df568306..af8a5594 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -7,7 +7,7 @@ from mkdocs.commands.build import build from mkdocs.config import load_config -from mkdocstrings._internal.plugin import MkdocstringsPlugin +from mkdocstrings import MkdocstringsPlugin if TYPE_CHECKING: from pathlib import Path From 72e8baeb816e5c540a6d38e030ac20566ebb0d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 18:41:20 +0100 Subject: [PATCH 164/223] docs: Simplify API docs --- docs/css/mkdocstrings.css | 11 +++------- docs/reference/mkdocstrings.md | 9 ++++++++ mkdocs.yml | 16 +++++++------- pyproject.toml | 2 -- scripts/gen_api_ref.py | 38 ---------------------------------- 5 files changed, 21 insertions(+), 55 deletions(-) create mode 100644 docs/reference/mkdocstrings.md delete mode 100644 scripts/gen_api_ref.py diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 05f1088b..55321aa3 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -31,12 +31,7 @@ td code { word-break: normal !important; } -[data-md-color-scheme="default"] { - --doc-symbol-parameter-fg-color: #d3a81b; - --doc-symbol-parameter-bg-color: #d3a81b1a; -} - -[data-md-color-scheme="slate"] { - --doc-symbol-parameter-fg-color: #dfbe50; - --doc-symbol-parameter-bg-color: #dfbe501a; +/* Hide parameters in ToC. */ +li.md-nav__item:has(> a[href*="("]) { + display: none; } diff --git a/docs/reference/mkdocstrings.md b/docs/reference/mkdocstrings.md new file mode 100644 index 00000000..15af3021 --- /dev/null +++ b/docs/reference/mkdocstrings.md @@ -0,0 +1,9 @@ +--- +title: API reference +hide: +- navigation +--- + +# ::: mkdocstrings + options: + show_submodules: true diff --git a/mkdocs.yml b/mkdocs.yml index eb280985..828a81a4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,8 +34,7 @@ nav: - Guides: - Recipes: recipes.md - Troubleshooting: troubleshooting.md -# defer to gen-files + literate-nav -- API reference: reference/ +- API reference: reference/mkdocstrings.md - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md @@ -130,11 +129,6 @@ plugins: - autorefs - markdown-exec - section-index -- gen-files: - scripts: - - scripts/gen_api_ref.py -- literate-nav: - nav_file: SUMMARY.txt - coverage - mkdocstrings: handlers: @@ -185,6 +179,14 @@ plugins: 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: diff --git a/pyproject.toml b/pyproject.toml index 35872a76..07dfa630 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,9 +111,7 @@ ci = [ "markdown-exec>=1.8", "mkdocs>=1.6", "mkdocs-coverage>=1.0", - "mkdocs-gen-files>=0.5", "mkdocs-git-revision-date-localized-plugin>=1.2", - "mkdocs-literate-nav>=0.6", "mkdocs-llmstxt>=0.1", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", diff --git a/scripts/gen_api_ref.py b/scripts/gen_api_ref.py deleted file mode 100644 index 7f4cb961..00000000 --- a/scripts/gen_api_ref.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generate the API reference pages and navigation. - -from pathlib import Path - -import mkdocs_gen_files - -nav = mkdocs_gen_files.Nav() -mod_symbol = '' - -root = Path(__file__).parent.parent -src = root / "src" - -for path in sorted(src.rglob("*.py")): - module_path = path.relative_to(src).with_suffix("") - doc_path = path.relative_to(src).with_suffix(".md") - full_doc_path = Path("reference", doc_path) - - parts = tuple(module_path.parts) - - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - full_doc_path = full_doc_path.with_name("index.md") - - if any(part.startswith("_") for part in parts): - continue - - nav_parts = [f"{mod_symbol} {part}" for part in parts] - nav[tuple(nav_parts)] = doc_path.as_posix() - - with mkdocs_gen_files.open(full_doc_path, "w") as fd: - ident = ".".join(parts) - fd.write(f"---\ntitle: {ident}\n---\n\n::: {ident}") - - mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path.relative_to(root)) - -with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: - nav_file.writelines(nav.build_literate_nav()) From 0723fc25fdf5d45bc3b949f370712a706b85fbab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 18:42:23 +0100 Subject: [PATCH 165/223] refactor: Finish exposing/hiding public/internal objects --- src/mkdocstrings/__init__.py | 13 +--- src/mkdocstrings/_internal/download.py | 18 +++-- src/mkdocstrings/_internal/extension.py | 61 ++++++++--------- .../_internal/handlers/__init__.py | 1 - src/mkdocstrings/_internal/handlers/base.py | 29 +++++--- .../_internal/handlers/rendering.py | 31 ++++++--- src/mkdocstrings/_internal/inventory.py | 15 ++++- src/mkdocstrings/_internal/loggers.py | 21 ++++-- src/mkdocstrings/_internal/plugin.py | 67 +++++++------------ tests/test_api.py | 6 ++ tests/test_download.py | 2 +- 11 files changed, 144 insertions(+), 120 deletions(-) diff --git a/src/mkdocstrings/__init__.py b/src/mkdocstrings/__init__.py index d6a01b2b..71720f8a 100644 --- a/src/mkdocstrings/__init__.py +++ b/src/mkdocstrings/__init__.py @@ -25,6 +25,7 @@ ) from mkdocstrings._internal.inventory import Inventory, InventoryItem from mkdocstrings._internal.loggers import ( + TEMPLATES_DIRS, LoggerAdapter, TemplateLogger, get_logger, @@ -32,15 +33,10 @@ get_template_logger_function, get_template_path, ) -from mkdocstrings._internal.plugin import ( - InventoryImportType, - InventoryLoaderType, - MkdocstringsPlugin, - PluginConfig, - list_to_tuple, -) +from mkdocstrings._internal.plugin import MkdocstringsPlugin, PluginConfig __all__: list[str] = [ + "TEMPLATES_DIRS", "AutoDocProcessor", "BaseHandler", "CollectionError", @@ -52,9 +48,7 @@ "Highlighter", "IdPrependingTreeprocessor", "Inventory", - "InventoryImportType", "InventoryItem", - "InventoryLoaderType", "LoggerAdapter", "MkdocstringsExtension", "MkdocstringsInnerExtension", @@ -68,5 +62,4 @@ "get_template_logger", "get_template_logger_function", "get_template_path", - "list_to_tuple", ] diff --git a/src/mkdocstrings/_internal/download.py b/src/mkdocstrings/_internal/download.py index 999075fe..2beb053a 100644 --- a/src/mkdocstrings/_internal/download.py +++ b/src/mkdocstrings/_internal/download.py @@ -9,13 +9,13 @@ from mkdocstrings._internal.loggers import get_logger -log = get_logger(__name__) +_logger = get_logger(__name__) # Regex pattern for an environment variable in the form ${ENV_VAR}. -ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") +_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}") -def download_url_with_gz(url: str) -> bytes: +def _download_url_with_gz(url: str) -> bytes: url, auth_header = _extract_auth_from_url(url) req = urllib.request.Request( # noqa: S310 @@ -42,10 +42,14 @@ def replace_func(match: re.Match) -> str: try: return env[match.group(1)] except KeyError: - log.warning("Environment variable '%s' is not set, but is used in inventory URL %s", match.group(1), url) + _logger.warning( + "Environment variable '%s' is not set, but is used in inventory URL %s", + match.group(1), + url, + ) return match.group(0) - return re.sub(ENV_VAR_PATTERN, replace_func, credential) + return re.sub(_ENV_VAR_PATTERN, replace_func, credential) # Implementation adapted from PDM: https://github.com/pdm-project/pdm. @@ -67,11 +71,11 @@ def _create_auth_header(credential: str, url: str) -> dict[str, str]: """Create the Authorization header for basic or bearer authentication, depending on credential.""" if ":" not in credential: # We assume that the user is using a token. - log.debug("Using bearer token authentication for %s", url) + _logger.debug("Using bearer token authentication for %s", url) return {"Authorization": f"Bearer {credential}"} # Else, we assume that the user is using user:password. user, pwd = credential.split(":", 1) - log.debug("Using basic authentication for %s", url) + _logger.debug("Using basic authentication for %s", url) credentials = base64.encodebytes(f"{user}:{pwd}".encode()).decode().strip() return {"Authorization": f"Basic {credentials}"} diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 1c46f4a9..35ceca41 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -1,25 +1,24 @@ -"""This module holds the code of the Markdown extension responsible for matching "autodoc" instructions. - -The extension is composed of a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockparser) -that matches indented blocks starting with a line like `::: identifier`. - -For each of these blocks, it uses a [handler][mkdocstrings.BaseHandler] to collect documentation about -the given identifier and render it with Jinja templates. - -Both the collection and rendering process can be configured by adding YAML configuration under the "autodoc" -instruction: - -```yaml -::: some.identifier - handler: python - options: - option1: value1 - option2: - - value2a - - value2b - option_x: etc -``` -""" +# This module holds the code of the Markdown extension responsible for matching "autodoc" instructions. +# +# The extension is composed of a Markdown [block processor](https://python-markdown.github.io/extensions/api/#blockparser) +# that matches indented blocks starting with a line like `::: identifier`. +# +# For each of these blocks, it uses a [handler][mkdocstrings.BaseHandler] to collect documentation about +# the given identifier and render it with Jinja templates. +# +# Both the collection and rendering process can be configured by adding YAML configuration under the "autodoc" +# instruction: +# +# ```yaml +# ::: some.identifier +# handler: python +# options: +# option1: value1 +# option2: +# - value2a +# - value2b +# option_x: etc +# ``` from __future__ import annotations @@ -45,7 +44,7 @@ from mkdocs_autorefs import AutorefsPlugin -log = get_logger(__name__) +_logger = get_logger(__name__) class AutoDocProcessor(BlockProcessor): @@ -59,6 +58,7 @@ class AutoDocProcessor(BlockProcessor): """ regex = re.compile(r"^(?P#{1,6} *|)::: ?(?P.+?) *$", flags=re.MULTILINE) + """The regular expression to match our autodoc instructions.""" def __init__( self, @@ -76,6 +76,7 @@ def __init__( """ super().__init__(parser=md.parser) self.md = md + """The Markdown instance.""" self._handlers = handlers self._autorefs = autorefs self._updated_envs: set = set() @@ -120,7 +121,7 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None: if match: identifier = match["name"] heading_level = match["heading"].count("#") - log.debug("Matched '::: %s'", identifier) + _logger.debug("Matched '::: %s'", identifier) html, handler, data = self._process_block(identifier, block, heading_level) el = Element("div", {"class": "mkdocstrings"}) @@ -161,7 +162,7 @@ def _process_block( local_config = yaml.safe_load(yaml_block) or {} handler_name = self._handlers.get_handler_name(local_config) - log.debug("Using handler '%s'", handler_name) + _logger.debug("Using handler '%s'", handler_name) handler = self._handlers.get_handler(handler_name) local_options = local_config.get("options", {}) @@ -183,23 +184,23 @@ def _process_block( global_options = handler_config.get("options", {}) options = {**global_options, **local_options} - log.debug("Collecting data") + _logger.debug("Collecting data") try: data: CollectorItem = handler.collect(identifier, options) except CollectionError as exception: - log.error("%s", exception) # noqa: TRY400 + _logger.error("%s", exception) # noqa: TRY400 raise PluginError(f"Could not collect '{identifier}'") from exception if handler_name not in self._updated_envs: # We haven't seen this handler before on this document. - log.debug("Updating handler's rendering env") + _logger.debug("Updating handler's rendering env") handler._update_env(self.md, config=self._handlers._tool_config) self._updated_envs.add(handler_name) - log.debug("Rendering templates") + _logger.debug("Rendering templates") try: rendered = handler.render(data, options) except TemplateNotFound as exc: - log.error( # noqa: TRY400 + _logger.error( # noqa: TRY400 "Template '%s' not found for '%s' handler and theme '%s'.", exc.name, handler_name, diff --git a/src/mkdocstrings/_internal/handlers/__init__.py b/src/mkdocstrings/_internal/handlers/__init__.py index b9e2a29c..e69de29b 100644 --- a/src/mkdocstrings/_internal/handlers/__init__.py +++ b/src/mkdocstrings/_internal/handlers/__init__.py @@ -1 +0,0 @@ -"""Handlers module.""" diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 385eed43..32a4576e 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -1,7 +1,6 @@ -"""Base module for handlers. - -This module contains the base classes for implementing handlers. -""" +# Base module for handlers. +# +# This module contains the base classes for implementing handlers. from __future__ import annotations @@ -25,7 +24,7 @@ # TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. from mkdocs_get_deps.cache import download_and_cache_url -from mkdocstrings._internal.download import download_url_with_gz +from mkdocstrings._internal.download import _download_url_with_gz from mkdocstrings._internal.handlers.rendering import ( HeadingShiftingTreeprocessor, Highlighter, @@ -48,11 +47,14 @@ from markdown import Extension from mkdocs_autorefs import AutorefsHookInterface -log = get_logger(__name__) +_logger = get_logger(__name__) CollectorItem = Any +"""The type of the item returned by the `collect` method of a handler.""" HandlerConfig = Any +"""The type of the configuration of a handler.""" HandlerOptions = Any +"""The type of the options passed to a handler.""" # Autodoc instructions can appear in nested Markdown, @@ -201,9 +203,13 @@ def __init__( ) self.theme = theme + """The selected theme.""" self.custom_templates = custom_templates + """The path to custom templates.""" self.mdx = mdx + """The Markdown extensions to use.""" self.mdx_config = mdx_config + """The configuration for the Markdown extensions.""" self._md: Markdown | None = None self._headings: list[Element] = [] @@ -240,6 +246,8 @@ def __init__( loader=FileSystemLoader(paths), auto_reload=False, # Editing a template in the middle of a build is not useful. ) + """The Jinja environment.""" + self.env.filters["convert_markdown"] = self.do_convert_markdown self.env.filters["heading"] = self.do_heading self.env.filters["any"] = do_any @@ -587,6 +595,7 @@ def __init__( self._tool_config = tool_config self.inventory: Inventory = Inventory(project=inventory_project, version=inventory_version) + """The objects inventory.""" self._inv_futures: dict[futures.Future, tuple[BaseHandler, str, Any]] = {} @@ -722,26 +731,26 @@ def _download_inventories(self) -> None: if to_download: thread_pool = futures.ThreadPoolExecutor(4) for handler, url, conf in to_download: - log.debug("Downloading inventory from %s", url) + _logger.debug("Downloading inventory from %s", url) future = thread_pool.submit( download_and_cache_url, url, datetime.timedelta(days=1), - download=download_url_with_gz, + download=_download_url_with_gz, ) self._inv_futures[future] = (handler, url, conf) thread_pool.shutdown(wait=False) def _yield_inventory_items(self) -> Iterator[tuple[str, str]]: if self._inv_futures: - log.debug("Waiting for %s inventory download(s)", len(self._inv_futures)) + _logger.debug("Waiting for %s inventory download(s)", len(self._inv_futures)) futures.wait(self._inv_futures, timeout=30) # Reversed order so that pages from first futures take precedence: 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 - log.error("Couldn't load inventory %s through handler '%s': %s", url, handler.name, error) # noqa: TRY400 + _logger.error("Couldn't load inventory %s through handler '%s': %s", url, handler.name, error) # noqa: TRY400 self._inv_futures = {} @property diff --git a/src/mkdocstrings/_internal/handlers/rendering.py b/src/mkdocstrings/_internal/handlers/rendering.py index 0fab5cc6..23b444ec 100644 --- a/src/mkdocstrings/_internal/handlers/rendering.py +++ b/src/mkdocstrings/_internal/handlers/rendering.py @@ -1,4 +1,4 @@ -"""This module holds helpers responsible for augmentations to the Markdown sub-documents produced by handlers.""" +# This module holds helpers responsible for augmentations to the Markdown sub-documents produced by handlers. from __future__ import annotations @@ -133,7 +133,8 @@ def highlight( class IdPrependingTreeprocessor(Treeprocessor): """Prepend the configured prefix to IDs of all HTML elements.""" - name = "mkdocstrings_ids" + name: str = "mkdocstrings_ids" + """The name of the treeprocessor.""" id_prefix: str """The prefix to add to every ID. It is prepended without any separator; specify your own separator if needed.""" @@ -149,6 +150,7 @@ def __init__(self, md: Markdown, id_prefix: str): self.id_prefix = id_prefix def run(self, root: Element) -> None: + """Prepend the configured prefix to all IDs in the document.""" if self.id_prefix: self._prefix_ids(root) @@ -189,8 +191,11 @@ def _prefix_ids(self, root: Element) -> None: class HeadingShiftingTreeprocessor(Treeprocessor): """Shift levels of all Markdown headings according to the configured base level.""" - name = "mkdocstrings_headings" - regex = re.compile(r"([Hh])([1-6])") + name: str = "mkdocstrings_headings" + """The name of the treeprocessor.""" + + regex: re.Pattern = re.compile(r"([Hh])([1-6])") + """The regex to match heading tags.""" shift_by: int """The number of heading "levels" to add to every heading. `

    ` with `shift_by = 3` becomes `

    `.""" @@ -206,6 +211,7 @@ def __init__(self, md: Markdown, shift_by: int): self.shift_by = shift_by def run(self, root: Element) -> None: + """Shift the levels of all headings in the document.""" if not self.shift_by: return for el in root.iter(): @@ -219,8 +225,11 @@ def run(self, root: Element) -> None: class _HeadingReportingTreeprocessor(Treeprocessor): """Records the heading elements encountered in the document.""" - name = "mkdocstrings_headings_list" - regex = re.compile(r"[Hh][1-6]") + name: str = "mkdocstrings_headings_list" + """The name of the treeprocessor.""" + + regex: re.Pattern = re.compile(r"[Hh][1-6]") + """The regex to match heading tags.""" headings: list[Element] """The list (the one passed in the initializer) that is used to record the heading elements (by appending to it).""" @@ -230,6 +239,7 @@ def __init__(self, md: Markdown, headings: list[Element]): self.headings = headings 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] for el in root.iter(): if self.regex.fullmatch(el.tag): @@ -244,10 +254,14 @@ def run(self, root: Element) -> None: class ParagraphStrippingTreeprocessor(Treeprocessor): """Unwraps the

    element around the whole output.""" - name = "mkdocstrings_strip_paragraph" - strip = False + name: str = "mkdocstrings_strip_paragraph" + """The name of the treeprocessor.""" + + strip: bool = False + """Whether to strip `

    ` elements or not.""" def run(self, root: Element) -> Element | None: + """Unwrap the root element if it's a single `

    ` element.""" if self.strip and len(root) == 1 and root[0].tag == "p": # Turn the single

    element into the root element and inherit its tag name (it's significant!) root[0].tag = root.tag @@ -266,6 +280,7 @@ def __init__(self, headings: list[Element]): """ super().__init__() self.headings = headings + """The list that will be populated with all HTML heading elements encountered in the document.""" def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent method's name) """Register the extension. diff --git a/src/mkdocstrings/_internal/inventory.py b/src/mkdocstrings/_internal/inventory.py index fb2d0018..471e3633 100644 --- a/src/mkdocstrings/_internal/inventory.py +++ b/src/mkdocstrings/_internal/inventory.py @@ -1,7 +1,7 @@ -"""Module responsible for the objects inventory.""" - +# Module responsible for the objects inventory. +# # Credits to Brian Skinn and the sphobjinv project: -# https://github.com/bskinn/sphobjinv +# https://github.com/bskinn/sphobjinv. from __future__ import annotations @@ -37,11 +37,17 @@ def __init__( dispname: The item display name. """ self.name: str = name + """The item name.""" self.domain: str = domain + """The item domain.""" self.role: str = role + """The item role.""" self.uri: str = uri + """The item URI.""" self.priority: int = priority + """The item priority.""" self.dispname: str = dispname or name + """The item display name.""" def format_sphinx(self) -> str: """Format this item as a Sphinx inventory line. @@ -58,6 +64,7 @@ def format_sphinx(self) -> str: return f"{self.name} {self.domain}:{self.role} {self.priority} {uri} {dispname}" sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$") + """Regex to parse a Sphinx v2 inventory line.""" @classmethod def parse_sphinx(cls, line: str) -> InventoryItem: @@ -89,7 +96,9 @@ def __init__(self, items: list[InventoryItem] | None = None, project: str = "pro for item in items: self[item.name] = item self.project = project + """The project name.""" self.version = version + """The project version.""" def register( self, diff --git a/src/mkdocstrings/_internal/loggers.py b/src/mkdocstrings/_internal/loggers.py index 89f3d7f8..d56d09c3 100644 --- a/src/mkdocstrings/_internal/loggers.py +++ b/src/mkdocstrings/_internal/loggers.py @@ -1,4 +1,4 @@ -"""Logging functions.""" +# Logging functions. from __future__ import annotations @@ -12,18 +12,19 @@ except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped from jinja2 import contextfunction as pass_context # type: ignore[attr-defined,no-redef] +if TYPE_CHECKING: + from collections.abc import MutableMapping, Sequence + + from jinja2.runtime import Context + + try: import mkdocstrings_handlers except ImportError: TEMPLATES_DIRS: Sequence[Path] = () else: TEMPLATES_DIRS = tuple(mkdocstrings_handlers.__path__) - - -if TYPE_CHECKING: - from collections.abc import MutableMapping, Sequence - - from jinja2.runtime import Context + """The directories where the handler templates are located.""" class LoggerAdapter(logging.LoggerAdapter): @@ -56,6 +57,7 @@ def __init__(self, prefix: str, logger: logging.Logger): """ super().__init__(logger, {}) self.prefix = prefix + """The prefix to insert in front of every message.""" self._logged: set[tuple[LoggerAdapter, str]] = set() def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, Any]: @@ -110,10 +112,15 @@ def __init__(self, logger: LoggerAdapter): logger: A logger adapter. """ self.debug = get_template_logger_function(logger.debug) + """Log a DEBUG message.""" self.info = get_template_logger_function(logger.info) + """Log an INFO message.""" self.warning = get_template_logger_function(logger.warning) + """Log a WARNING message.""" self.error = get_template_logger_function(logger.error) + """Log an ERROR message.""" self.critical = get_template_logger_function(logger.critical) + """Log a CRITICAL message.""" class _Lazy: diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index 37d77671..7cb5b9ad 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -1,23 +1,21 @@ -"""This module contains the "mkdocstrings" plugin for MkDocs. - -The plugin instantiates a Markdown extension ([`MkdocstringsExtension`][mkdocstrings.MkdocstringsExtension]), -and adds it to the list of Markdown extensions used by `mkdocs` -during the [`on_config` event hook](https://www.mkdocs.org/user-guide/plugins/#on_config). - -Once the documentation is built, the [`on_post_build` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_build) -is triggered and calls the [`handlers.teardown()` method][mkdocstrings.Handlers.teardown]. This method is -used to teardown the handlers that were instantiated during documentation buildup. - -Finally, when serving the documentation, it can add directories to watch -during the [`on_serve` event hook](https://www.mkdocs.org/user-guide/plugins/#on_serve). -""" +# This module contains the "mkdocstrings" plugin for MkDocs. +# +# The plugin instantiates a Markdown extension ([`MkdocstringsExtension`][mkdocstrings.MkdocstringsExtension]), +# and adds it to the list of Markdown extensions used by `mkdocs` +# during the [`on_config` event hook](https://www.mkdocs.org/user-guide/plugins/#on_config). +# +# Once the documentation is built, the [`on_post_build` event hook](https://www.mkdocs.org/user-guide/plugins/#on_post_build) +# is triggered and calls the [`handlers.teardown()` method][mkdocstrings.Handlers.teardown]. This method is +# used to teardown the handlers that were instantiated during documentation buildup. +# +# Finally, when serving the documentation, it can add directories to watch +# during the [`on_serve` event hook](https://www.mkdocs.org/user-guide/plugins/#on_serve). from __future__ import annotations import os import sys -from collections.abc import Iterable, Mapping -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any from warnings import catch_warnings, simplefilter from mkdocs.config import Config @@ -31,34 +29,16 @@ from mkdocstrings._internal.loggers import get_logger if sys.version_info < (3, 10): - from typing_extensions import ParamSpec + pass else: - from typing import ParamSpec + pass if TYPE_CHECKING: from jinja2.environment import Environment from mkdocs.config.defaults import MkDocsConfig -log = get_logger(__name__) - -InventoryImportType = list[tuple[str, Mapping[str, Any]]] -InventoryLoaderType = Callable[..., Iterable[tuple[str, str]]] - -P = ParamSpec("P") -R = TypeVar("R") - - -def list_to_tuple(function: Callable[P, R]) -> Callable[P, R]: - """Decorater to convert lists to tuples in the arguments.""" - - def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: - safe_args = [tuple(item) if isinstance(item, list) else item for item in args] - if kwargs: - kwargs = {key: tuple(value) if isinstance(value, list) else value for key, value in kwargs.items()} # type: ignore[assignment] - return function(*safe_args, **kwargs) # type: ignore[arg-type] - - return wrapper +_logger = get_logger(__name__) class PluginConfig(Config): @@ -111,7 +91,8 @@ class MkdocstringsPlugin(BasePlugin[PluginConfig]): for more information about its plugin system. """ - css_filename = "assets/_mkdocstrings.css" + css_filename: str = "assets/_mkdocstrings.css" + """The path of the CSS file to write in the site directory.""" def __init__(self) -> None: """Initialize the object.""" @@ -149,9 +130,9 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: The modified config. """ if not self.plugin_enabled: - log.debug("Plugin is not enabled. Skipping.") + _logger.debug("Plugin is not enabled. Skipping.") return config - log.debug("Adding extension to the list") + _logger.debug("Adding extension to the list") handlers = Handlers( default=self.config.default_handler, @@ -171,14 +152,14 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: try: # If autorefs plugin is explicitly enabled, just use it. autorefs = config.plugins["autorefs"] # type: ignore[assignment] - log.debug("Picked up existing autorefs instance %r", autorefs) + _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.scan_toc = False config.plugins["autorefs"] = autorefs - log.debug("Added a subdued autorefs instance %r", autorefs) + _logger.debug("Added a subdued autorefs instance %r", autorefs) # YORE: Bump 1: Remove block. with catch_warnings(): simplefilter("ignore", category=DeprecationWarning) @@ -229,7 +210,7 @@ def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: A write_file(css_content.encode("utf-8"), os.path.join(config.site_dir, self.css_filename)) if self.inventory_enabled: - log.debug("Creating inventory file objects.inv") + _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")) @@ -259,7 +240,7 @@ def on_post_build( return if self._handlers: - log.debug("Tearing handlers down") + _logger.debug("Tearing handlers down") self.handlers.teardown() def get_handler(self, handler_name: str) -> BaseHandler: diff --git a/tests/test_api.py b/tests/test_api.py index 7cf832d2..4b13aac7 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -150,11 +150,17 @@ 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: 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}" diff --git a/tests/test_download.py b/tests/test_download.py index 47e5c479..4aa19fd7 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -96,7 +96,7 @@ def test_create_auth_header_bearer_auth() -> None: ) def test_env_var_pattern(var: str, match: str | None) -> None: """Test the environment variable regex pattern.""" - _match = download.ENV_VAR_PATTERN.match(var) + _match = download._ENV_VAR_PATTERN.match(var) if _match is None: assert match is _match else: From fd28fff07aa24e6e1381f67455980fc4364fb86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 8 Mar 2025 22:43:07 +0100 Subject: [PATCH 166/223] chore: Prepare release 0.28.3 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 455b6ae5..58e3659e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.28.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.3) - 2025-03-08 + +[Compare with 0.28.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.2...0.28.3) + +### Deprecations + +All public objects must now be imported from the top-level `mkdocstrings` module. Importing from submodules is deprecated, and will raise errors starting with v1. This should be the last deprecation before v1. + +### Build + +- Make `python` extra depend on latest mkdocstrings-python (1.16.2) ([ba9003e](https://github.com/mkdocstrings/mkdocstrings/commit/ba9003e96c8e5e01900743d5c464cbd228d732f4) by Timothรฉe Mazzucotelli). + +### Code Refactoring + +- Finish exposing/hiding public/internal objects ([0723fc2](https://github.com/mkdocstrings/mkdocstrings/commit/0723fc25fdf5d45bc3b949f370712a706b85fbab) by Timothรฉe Mazzucotelli). +- Re-expose public API in the top-level `mkdocstrings` module ([e66e080](https://github.com/mkdocstrings/mkdocstrings/commit/e66e08096d45f6790492d9a0b767d512e42f67a9) by Timothรฉe Mazzucotelli). +- Move modules to internal folder ([23fe23f](https://github.com/mkdocstrings/mkdocstrings/commit/23fe23f11011d0470a6342ca85e060e5ac2b6bd6) by Timothรฉe Mazzucotelli). + ## [0.28.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.2) - 2025-02-24 [Compare with 0.28.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.1...0.28.2) From 11bc400ab7089a47755f24a790c08f2f904c570b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:37:54 +0100 Subject: [PATCH 167/223] build: Depend on MkDocs 1.6 We need version 1.6 for combined events. --- pyproject.toml | 3 +-- src/mkdocstrings/_internal/handlers/base.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07dfa630..c3087f61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,9 +34,8 @@ dependencies = [ "Jinja2>=2.11.1", "Markdown>=3.6", "MarkupSafe>=1.1", - "mkdocs>=1.4", + "mkdocs>=1.6", "mkdocs-autorefs>=1.4", - "mkdocs-get-deps>=0.2", # TODO: Remove when we depend on mkdocs>=1.5. "pymdown-extensions>=6.3", "importlib-metadata>=4.6; python_version < '3.10'", "typing-extensions>=4.1; python_version < '3.10'", diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 32a4576e..5736676e 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -19,11 +19,9 @@ 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 -# TODO: Replace with `from mkdocs.utils.cache import download_and_cache_url` when we depend on mkdocs>=1.5. -from mkdocs_get_deps.cache import download_and_cache_url - from mkdocstrings._internal.download import _download_url_with_gz from mkdocstrings._internal.handlers.rendering import ( HeadingShiftingTreeprocessor, From 8d1dd754b4babd3c4f9e6c1d8856be57fe4ba9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:37:13 +0100 Subject: [PATCH 168/223] refactor: Use a combined event (each split with a different priority) for `on_env` This is required because we must run things in `on_env` *before* autorefs, and the backlinks feature will need to run things *after* autorefs. --- src/mkdocstrings/_internal/plugin.py | 49 ++++++++++++++-------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index 7cb5b9ad..1d3ba86f 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -14,13 +14,12 @@ from __future__ import annotations import os -import sys 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 -from mkdocs.plugins import BasePlugin +from mkdocs.plugins import BasePlugin, CombinedEvent, event_priority from mkdocs.utils import write_file from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin @@ -28,11 +27,6 @@ from mkdocstrings._internal.handlers.base import BaseHandler, Handlers from mkdocstrings._internal.loggers import get_logger -if sys.version_info < (3, 10): - pass -else: - pass - if TYPE_CHECKING: from jinja2.environment import Environment from mkdocs.config.defaults import MkDocsConfig @@ -194,29 +188,34 @@ def plugin_enabled(self) -> bool: """ return self.config.enabled - def on_env(self, env: Environment, config: MkDocsConfig, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 - """Extra actions that need to happen after all Markdown rendering and before HTML rendering. - - Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). - - - Write mkdocstrings' extra files into the site dir. - - Gather results from background inventory download tasks. - """ - if not self.plugin_enabled: - return + @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] + for identifier, url in self._handlers._yield_inventory_items(): + register(identifier, url) - if self._handlers: + @event_priority(-20) # Late, not important. + 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)) - if 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")) + @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")) - register = config.plugins["autorefs"].register_url # type: ignore[attr-defined] - for identifier, url in self._handlers._yield_inventory_items(): - register(identifier, url) + on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory) + """Extra actions that need to happen after all Markdown-to-HTML page rendering. + + Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). + + - Gather results from background inventory download tasks. + - Write mkdocstrings' extra files (CSS, inventory) into the site directory. + """ def on_post_build( self, From f49fb29582714795ca03febf1ee243aa2992917e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:40:27 +0100 Subject: [PATCH 169/223] refactor: Save and forward titles to autorefs This will be useful for the backlinks feature. Additionally, use autorefs `current_page` as a `Page` object, not a string (URL). --- src/mkdocstrings/_internal/extension.py | 94 +++++++++++---------- src/mkdocstrings/_internal/handlers/base.py | 2 + 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 35ceca41..182fc563 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -240,54 +240,56 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: # If we were in an inner handler layer, we wouldn't do any of this # and would just let headings bubble up to the outer handler layer. - page = self._autorefs.current_page - if page is not None: - for heading in headings: - rendered_id = heading.attrib["id"] - self._autorefs.register_anchor(page, rendered_id, 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) + if (page := self._autorefs.current_page) is None: + return + + for heading in headings: + rendered_id = heading.attrib["id"] + # 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_aliases(rendered_id) - + aliases = handler.get_anchors(data_object) + else: + aliases = handler.get_aliases(rendered_id) + + for alias in aliases: + if alias != rendered_id: + self._autorefs.register_anchor(page, alias, rendered_id, primary=False) + + if "data-role" in heading.attrib: + self._handlers.inventory.register( + name=rendered_id, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=1, # Register with standard priority. + uri=f"{page.url}#{rendered_id}", + ) for alias in aliases: - if alias != rendered_id: - self._autorefs.register_anchor(page, alias, rendered_id, primary=False) - - if "data-role" in heading.attrib: - self._handlers.inventory.register( - name=rendered_id, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=1, # Register with standard priority. - uri=f"{page}#{rendered_id}", - ) - for alias in aliases: - if alias not in self._handlers.inventory: - self._handlers.inventory.register( - name=alias, - domain=handler.domain, - role=heading.attrib["data-role"], - priority=2, # Register with lower priority. - uri=f"{page}#{rendered_id}", - ) + if alias not in self._handlers.inventory: + self._handlers.inventory.register( + name=alias, + domain=handler.domain, + role=heading.attrib["data-role"], + priority=2, # Register with lower priority. + uri=f"{page.url}#{rendered_id}", + ) class _HeadingsPostProcessor(Treeprocessor): diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 5736676e..61e71f48 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -473,6 +473,8 @@ def do_heading( el.set("data-toc-label", toc_label) if role: el.set("data-role", role) + if content: + el.text = str(content).strip() self._headings.append(el) if hidden: From d4c7b9c42f2de5df234c1ffefae0405a120e383c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 24 Feb 2025 20:44:51 +0100 Subject: [PATCH 170/223] feat: Support rendering backlinks through handlers Handlers must add `backlinks` HTML elements to their templates: ```html ``` mkdocstrings will run a regular expression substitution on each page's HTML, and call corresponding handlers' `render_backlinks` method with backlinks fetched from autorefs (using the specified identifier, and aliases obtained thanks to it through the same handler). Issue-723: https://github.com/mkdocstrings/mkdocstrings/issues/723 Issue-mkdocstrings-python-153: https://github.com/mkdocstrings/python/issues/153 PR-739: https://github.com/mkdocstrings/mkdocstrings/pull/739 --- src/mkdocstrings/_internal/handlers/base.py | 12 +++++-- src/mkdocstrings/_internal/plugin.py | 38 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 61e71f48..6059b886 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -20,7 +20,7 @@ from markdown.extensions.toc import TocTreeprocessor from markupsafe import Markup from mkdocs.utils.cache import download_and_cache_url -from mkdocs_autorefs import AutorefsInlineProcessor +from mkdocs_autorefs import AutorefsInlineProcessor, BacklinksTreeProcessor from mkdocstrings._internal.download import _download_url_with_gz from mkdocstrings._internal.handlers.rendering import ( @@ -43,7 +43,7 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from markdown import Extension - from mkdocs_autorefs import AutorefsHookInterface + from mkdocs_autorefs import AutorefsHookInterface, Backlink _logger = get_logger(__name__) @@ -331,6 +331,10 @@ def render(self, data: CollectorItem, options: HandlerOptions) -> str: """ raise NotImplementedError + def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: ARG002 + """Render backlinks.""" + return "" + def teardown(self) -> None: """Teardown the handler. @@ -420,6 +424,8 @@ def do_convert_markdown( 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] + 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] @@ -430,6 +436,8 @@ def do_convert_markdown( 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] + 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] self.md.reset() _markdown_conversion_layer -= 1 diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index 1d3ba86f..d7adf1c6 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -14,6 +14,8 @@ from __future__ import annotations import os +import re +from re import Match from typing import TYPE_CHECKING, Any from warnings import catch_warnings, simplefilter @@ -30,6 +32,7 @@ if TYPE_CHECKING: from jinja2.environment import Environment from mkdocs.config.defaults import MkDocsConfig + from mkdocs.structure.files import Files _logger = get_logger(__name__) @@ -142,6 +145,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: handlers._download_inventories() + AutorefsPlugin.record_backlinks = True autorefs: AutorefsPlugin try: # If autorefs plugin is explicitly enabled, just use it. @@ -164,6 +168,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: config.extra_css.insert(0, self.css_filename) # So that it has lower priority than user files. + self._autorefs = autorefs self._handlers = handlers return config @@ -208,13 +213,44 @@ def _on_env_write_inventory(self, env: Environment, config: MkDocsConfig, *args: inv_contents = self.handlers.inventory.format_sphinx() write_file(inv_contents, os.path.join(config.site_dir, "objects.inv")) - on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory) + @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 + regex = re.compile(r"") + + def repl(match: Match) -> str: + handler_name = match.group(2) + handler = self.handlers.get_handler(handler_name) + + # 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] + 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] + + # No backlinks, avoid calling the handler's method. + if not backlinks: + return "" + + return handler.render_backlinks(backlinks) + + for file in files: + if file.page and file.page.content: + _logger.debug("Applying backlinks in page %s", file.page.file.src_path) + file.page.content = regex.sub(repl, file.page.content) + + return env + + on_env = CombinedEvent(_on_env_load_inventories, _on_env_add_css, _on_env_write_inventory, _on_env_apply_backlinks) """Extra actions that need to happen after all Markdown-to-HTML page rendering. Hook for the [`on_env` event](https://www.mkdocs.org/user-guide/plugins/#on_env). - Gather results from background inventory download tasks. - Write mkdocstrings' extra files (CSS, inventory) into the site directory. + - Apply backlinks to the HTML output of each page. """ def on_post_build( From 3366f5be7012aada5d8582c85ed92d76ccb1aa70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 10 Mar 2025 13:23:09 +0100 Subject: [PATCH 171/223] docs: Escape `

    ` in docstrings --- src/mkdocstrings/_internal/handlers/base.py | 2 +- src/mkdocstrings/_internal/handlers/rendering.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 32a4576e..9f1b427d 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -411,7 +411,7 @@ def do_convert_markdown( text: The text to convert. heading_level: The base heading level to start all Markdown headings from. html_id: The HTML id of the element that's considered the parent of this element. - strip_paragraph: Whether to exclude the

    tag from around the whole output. + strip_paragraph: Whether to exclude the `

    ` tag from around the whole output. Returns: An HTML string. diff --git a/src/mkdocstrings/_internal/handlers/rendering.py b/src/mkdocstrings/_internal/handlers/rendering.py index 23b444ec..25db87e1 100644 --- a/src/mkdocstrings/_internal/handlers/rendering.py +++ b/src/mkdocstrings/_internal/handlers/rendering.py @@ -252,7 +252,7 @@ def run(self, root: Element) -> None: class ParagraphStrippingTreeprocessor(Treeprocessor): - """Unwraps the

    element around the whole output.""" + """Unwraps the `

    ` element around the whole output.""" name: str = "mkdocstrings_strip_paragraph" """The name of the treeprocessor.""" @@ -263,7 +263,7 @@ class ParagraphStrippingTreeprocessor(Treeprocessor): def run(self, root: Element) -> Element | None: """Unwrap the root element if it's a single `

    ` element.""" if self.strip and len(root) == 1 and root[0].tag == "p": - # Turn the single

    element into the root element and inherit its tag name (it's significant!) + # Turn the single `

    ` element into the root element and inherit its tag name (it's significant!) root[0].tag = root.tag return root[0] return None From 32e415d2ead0d524745ae90af78e1195c827f9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 10 Mar 2025 14:09:53 +0100 Subject: [PATCH 172/223] chore: Prepare release 0.29.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58e3659e..826873b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.29.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.29.0) - 2025-03-10 + +[Compare with 0.28.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.3...0.29.0) + +**This is the last version before v1!** + +### Build + +- Depend on MkDocs 1.6 ([11bc400](https://github.com/mkdocstrings/mkdocstrings/commit/11bc400ab7089a47755f24a790c08f2f904c570b) by Timothรฉe Mazzucotelli). + +### Features + +- Support rendering backlinks through handlers ([d4c7b9c](https://github.com/mkdocstrings/mkdocstrings/commit/d4c7b9c42f2de5df234c1ffefae0405a120e383c) by Timothรฉe Mazzucotelli). [Issue-723](https://github.com/mkdocstrings/mkdocstrings/issues/723), [Issue-mkdocstrings-python-153](https://github.com/mkdocstrings/python/issues/153), [PR-739](https://github.com/mkdocstrings/mkdocstrings/pull/739) + +### Code Refactoring + +- Save and forward titles to autorefs ([f49fb29](https://github.com/mkdocstrings/mkdocstrings/commit/f49fb29582714795ca03febf1ee243aa2992917e) by Timothรฉe Mazzucotelli). +- Use a combined event (each split with a different priority) for `on_env` ([8d1dd75](https://github.com/mkdocstrings/mkdocstrings/commit/8d1dd754b4babd3c4f9e6c1d8856be57fe4ba9ea) by Timothรฉe Mazzucotelli). + ## [0.28.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.28.3) - 2025-03-08 [Compare with 0.28.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.2...0.28.3) From ba98661b50e2cde19d8696d6c8ceecdbb49ce83f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 14 Mar 2025 14:39:54 +0100 Subject: [PATCH 173/223] deps: Remove unused typing-extensions dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c3087f61..1e104425 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ dependencies = [ "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] From 94645796ecbb48a4a4c654d219df2e588d59f7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 14 Mar 2025 14:40:23 +0100 Subject: [PATCH 174/223] tests: Remove old skip conditions --- tests/test_extension.py | 1 - tests/test_inventory.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/test_extension.py b/tests/test_extension.py index b7c1c742..aed0a1f1 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -60,7 +60,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") diff --git a/tests/test_inventory.py b/tests/test_inventory.py index eb008661..9589d528 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -36,7 +36,6 @@ 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.""" mkdocs_config = load_config() From 983b3cd8c4e1de249ef3ebec2678e295e6c6b966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 14 Mar 2025 14:40:45 +0100 Subject: [PATCH 175/223] chore: Mark legacy stuff with Yore comments --- pyproject.toml | 2 ++ src/mkdocstrings/_internal/handlers/base.py | 3 ++- src/mkdocstrings/_internal/loggers.py | 3 ++- src/mkdocstrings/extension.py | 2 ++ src/mkdocstrings/handlers/__init__.py | 2 ++ src/mkdocstrings/handlers/base.py | 2 ++ src/mkdocstrings/handlers/rendering.py | 2 ++ src/mkdocstrings/inventory.py | 2 ++ src/mkdocstrings/loggers.py | 2 ++ src/mkdocstrings/plugin.py | 2 ++ 10 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1e104425..29aff0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,12 +31,14 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ + # YORE: Bump 1: Replace `2.11.1` with `3.1` within line. "Jinja2>=2.11.1", "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.6", "mkdocs-autorefs>=1.4", "pymdown-extensions>=6.3", + # YORE: EOL 3.9: Remove line. "importlib-metadata>=4.6; python_version < '3.10'", ] diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 7784f007..f19d9094 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -33,7 +33,8 @@ 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 + +# YORE: EOL 3.9: Replace block with line 4. if sys.version_info < (3, 10): from importlib_metadata import entry_points else: diff --git a/src/mkdocstrings/_internal/loggers.py b/src/mkdocstrings/_internal/loggers.py index d56d09c3..6c6304c3 100644 --- a/src/mkdocstrings/_internal/loggers.py +++ b/src/mkdocstrings/_internal/loggers.py @@ -7,9 +7,10 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable +# YORE: Bump 1: Replace block with line 2. try: from jinja2 import pass_context -except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped +except ImportError: from jinja2 import contextfunction as pass_context # type: ignore[attr-defined,no-redef] if TYPE_CHECKING: diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py index 15a84cc8..c7943652 100644 --- a/src/mkdocstrings/extension.py +++ b/src/mkdocstrings/extension.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any diff --git a/src/mkdocstrings/handlers/__init__.py b/src/mkdocstrings/handlers/__init__.py index af032e98..b684324a 100644 --- a/src/mkdocstrings/handlers/__init__.py +++ b/src/mkdocstrings/handlers/__init__.py @@ -1 +1,3 @@ """Deprecated. Import from `mkdocstrings` directly.""" + +# YORE: Bump 1: Remove file. diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py index 82ee3edb..c55a50ba 100644 --- a/src/mkdocstrings/handlers/base.py +++ b/src/mkdocstrings/handlers/base.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any diff --git a/src/mkdocstrings/handlers/rendering.py b/src/mkdocstrings/handlers/rendering.py index 940f3a9c..f3f04eea 100644 --- a/src/mkdocstrings/handlers/rendering.py +++ b/src/mkdocstrings/handlers/rendering.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any diff --git a/src/mkdocstrings/inventory.py b/src/mkdocstrings/inventory.py index b5c8adea..7192acff 100644 --- a/src/mkdocstrings/inventory.py +++ b/src/mkdocstrings/inventory.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py index ce805362..25545ca5 100644 --- a/src/mkdocstrings/loggers.py +++ b/src/mkdocstrings/loggers.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py index b4edf945..dbb6abf9 100644 --- a/src/mkdocstrings/plugin.py +++ b/src/mkdocstrings/plugin.py @@ -1,5 +1,7 @@ """Deprecated. Import from `mkdocstrings` directly.""" +# YORE: Bump 1: Remove file. + import warnings from typing import Any From ccf65c1166103cf705f30b27b8c913863a372da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 28 Mar 2025 16:06:34 +0100 Subject: [PATCH 176/223] docs: Remove 'sponsors only' labels --- docs/usage/handlers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index b9a01f68..0d375a95 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -4,12 +4,12 @@ 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 } - [Python](https://mkdocstrings.github.io/python/){ .external } - [Python (Legacy)](https://mkdocstrings.github.io/python-legacy/){ .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 From 0bc4799b0e38bf7378d0e57428d52fdd1e7ac1ab Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 27 Mar 2025 17:27:17 +1100 Subject: [PATCH 177/223] style: Format and configure for Ruff >= 0.10.0 Ruff 0.10.0 introduces the S704 lint, which triggers when a non- literal string is passed to `markupsafe.Markup()`. This triggered 5 times in the codebase. Only one of these errors was trivially fixable, and the fix caused tests to fail because the "fix" introduced escapes to already correct markup. This commit therefore configures Ruff to ignore this lint and does not fix any code that triggers it. Other changes are due to other formatting and linting changes from recent releases of Ruff. Ruff 0.10.0: https://github.com/astral-sh/ruff/releases/tag/0.10.0 Lint S704: https://docs.astral.sh/ruff/rules/unsafe-markup-use/ --- config/ruff.toml | 1 + scripts/insiders.py | 2 +- src/mkdocstrings/_internal/handlers/base.py | 5 ++--- tests/test_extension.py | 1 - tests/test_inventory.py | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/ruff.toml b/config/ruff.toml index 655a158c..65416253 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -43,6 +43,7 @@ ignore = [ "PLR0913", # Too many arguments to function call "PLR0915", # Too many statements "SLF001", # Private member accessed + "S704", # Unsafe use of `markupsafe.Markup` "TRY003", # Avoid specifying long messages outside the exception class ] diff --git a/scripts/insiders.py b/scripts/insiders.py index 6535a31e..4cd438d4 100644 --- a/scripts/insiders.py +++ b/scripts/insiders.py @@ -168,6 +168,6 @@ def load_json(url: str) -> str | list | dict: ongoing_goals = [goal for goal in goals.values() if not goal.complete] unreleased_features = sorted( (ft for ft in feature_list(ongoing_goals) if ft.since), - key=lambda ft: cast(date, ft.since), + key=lambda ft: cast("date", ft.since), reverse=True, ) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index f19d9094..cb1fffb3 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -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,7 +32,6 @@ from mkdocstrings._internal.inventory import Inventory from mkdocstrings._internal.loggers import get_logger, get_template_logger - # YORE: EOL 3.9: Replace block with line 4. if sys.version_info < (3, 10): from importlib_metadata import entry_points @@ -44,6 +42,7 @@ 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__) @@ -494,7 +493,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: diff --git a/tests/test_extension.py b/tests/test_extension.py index aed0a1f1..dd3d7028 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 diff --git a/tests/test_inventory.py b/tests/test_inventory.py index 9589d528..8c2333d3 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -2,7 +2,6 @@ from __future__ import annotations -import sys from io import BytesIO from os.path import join From 81caff5ff76f1a6606da9d2980e81ae9d2e02246 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 27 Mar 2025 17:38:04 +1100 Subject: [PATCH 178/223] fix: Ignore invalid inventory lines Previously, inventory items whose `dispname` value contains multiple lines would prevent mkdocstrings from loading the whole inventory file. This change makes mkdocstrings ignore invalid lines in inventories so that the rest of the inventory can still be loaded. This continuation line behavior can be seen in the wild in the OpenEye toolkits inventory file and a few Open Force Field inventory files. These projects' inventories cannot be used with mkdocstrings because of the raised error. Note that in Sphinx too, these inventory files are read succesfully, and the continuation lines are discarded, truncating the display name. In theory, a continuation line that by chance did parse correctly would be interpreted by both packages as a new inventory item. OpenEye Toolkits inventory file: https://docs.eyesopen.com/toolkits/python/objects.inv Open Force Field Toolkit inventory file: https://docs.openforcefield.org/projects/toolkit/en/stable/objects.inv BespokeFit inventory file: https://docs.openforcefield.org/projects/bespokefit/en/stable/objects.inv --- src/mkdocstrings/_internal/inventory.py | 18 +++++++++++--- tests/test_inventory.py | 33 +++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 5 deletions(-) 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/tests/test_inventory.py b/tests/test_inventory.py index 8c2333d3..ab61e599 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -11,8 +11,6 @@ from mkdocstrings import Inventory, InventoryItem -sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") - @pytest.mark.parametrize( "our_inv", @@ -21,10 +19,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) @@ -37,6 +38,8 @@ def test_sphinx_load_inventory_file(our_inv: Inventory) -> None: 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: @@ -53,3 +56,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] From 1a980402c39728ce265d8998b396c34bf76a113d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 31 Mar 2025 10:30:52 +0200 Subject: [PATCH 179/223] refactor: Rename loggers to "mkdocstrings" --- src/mkdocstrings/_internal/download.py | 2 +- src/mkdocstrings/_internal/extension.py | 2 +- src/mkdocstrings/_internal/handlers/base.py | 2 +- src/mkdocstrings/_internal/plugin.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings/_internal/download.py b/src/mkdocstrings/_internal/download.py index 2beb053a..ffe25e6b 100644 --- a/src/mkdocstrings/_internal/download.py +++ b/src/mkdocstrings/_internal/download.py @@ -9,7 +9,7 @@ from mkdocstrings._internal.loggers import get_logger -_logger = get_logger(__name__) +_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_]*)\}") diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 182fc563..83421ff8 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -44,7 +44,7 @@ from mkdocs_autorefs import AutorefsPlugin -_logger = get_logger(__name__) +_logger = get_logger("mkdocstrings") class AutoDocProcessor(BlockProcessor): diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index cb1fffb3..3d5852c5 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -45,7 +45,7 @@ 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.""" diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index d7adf1c6..afc94490 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -35,7 +35,7 @@ from mkdocs.structure.files import Files -_logger = get_logger(__name__) +_logger = get_logger("mkdocstrings") class PluginConfig(Config): From df4e7c81bd5b2a8931c676adf2d916d06531a987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 31 Mar 2025 10:32:52 +0200 Subject: [PATCH 180/223] chore: Prepare release 0.29.1 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 826873b0..ed973133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.29.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.29.1) - 2025-03-31 + +[Compare with 0.29.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.29.0...0.29.1) + +### Dependencies + +- Remove unused typing-extensions dependency ([ba98661](https://github.com/mkdocstrings/mkdocstrings/commit/ba98661b50e2cde19d8696d6c8ceecdbb49ce83f) by Timothรฉe Mazzucotelli). + +### Bug Fixes + +- Ignore invalid inventory lines ([81caff5](https://github.com/mkdocstrings/mkdocstrings/commit/81caff5ff76f1a6606da9d2980e81ae9d2e02246) by Josh Mitchell). [PR-748](https://github.com/mkdocstrings/mkdocstrings/pull/748) + +### Code Refactoring + +- Rename loggers to "mkdocstrings" ([1a98040](https://github.com/mkdocstrings/mkdocstrings/commit/1a980402c39728ce265d8998b396c34bf76a113d) by Timothรฉe Mazzucotelli). + ## [0.29.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.29.0) - 2025-03-10 [Compare with 0.28.3](https://github.com/mkdocstrings/mkdocstrings/compare/0.28.3...0.29.0) From d5bf4e1ed0370853f968b210ad77913faf106eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Tue, 1 Apr 2025 14:05:31 +0200 Subject: [PATCH 181/223] docs: Update link to YAML idiosyncrasies --- docs/usage/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/index.md b/docs/usage/index.md index ea9716cc..d208b4b0 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. From b1da3d02c4f432f603cbb0004bb35099327706db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 30 Jun 2025 22:37:58 +0200 Subject: [PATCH 182/223] ci: Ignore Ruff warnings --- duties.py | 2 +- src/mkdocstrings/_internal/handlers/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/duties.py b/duties.py index 6ee9b08d..b75d8b55 100644 --- a/duties.py +++ b/duties.py @@ -238,7 +238,7 @@ def coverage(ctx: Context) -> None: @duty -def test(ctx: Context, *cli_args: str, match: str = "") -> None: +def test(ctx: Context, *cli_args: str, match: str = "") -> None: # noqa: PT028 """Run the test suite. Parameters: diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 3d5852c5..c87f473c 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -359,7 +359,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 From 51f217f38af9475415f758866697158d9010967d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 30 Jun 2025 22:47:11 +0200 Subject: [PATCH 183/223] chore: Template upgrade --- .copier-answers.yml | 2 +- docs/css/mkdocstrings.css | 45 ++++++++++++++++++++++ docs/reference/{mkdocstrings.md => api.md} | 0 mkdocs.yml | 29 +++++++------- pyproject.toml | 4 +- tests/test_api.py | 6 ++- tests/test_extension.py | 1 - 7 files changed, 69 insertions(+), 18 deletions(-) rename docs/reference/{mkdocstrings.md => api.md} (100%) diff --git a/.copier-answers.yml b/.copier-answers.yml index bda5cd0c..fa6c4f9c 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.7.1 +_commit: 1.8.4 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css index 55321aa3..3447549c 100644 --- a/docs/css/mkdocstrings.css +++ b/docs/css/mkdocstrings.css @@ -35,3 +35,48 @@ td code { li.md-nav__item:has(> a[href*="("]) { display: none; } + +/* Tree-like output for backlinks. */ +.doc-backlink-list { + --tree-clr: var(--md-default-fg-color); + --tree-font-size: 1rem; + --tree-item-height: 1; + --tree-offset: 1rem; + --tree-thickness: 1px; + --tree-style: solid; + display: grid; + list-style: none !important; +} + +.doc-backlink-list li > span:first-child { + text-indent: .3rem; +} +.doc-backlink-list li { + padding-inline-start: var(--tree-offset); + border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); + position: relative; + margin-left: 0 !important; + + &:last-child { + border-color: transparent; + } + &::before{ + content: ''; + position: absolute; + top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness)); + left: calc(var(--tree-thickness) * -1); + width: calc(var(--tree-offset) + var(--tree-thickness) * 2); + height: calc(var(--tree-item-height) * var(--tree-font-size)); + border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr); + border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr); + } + &::after{ + content: ''; + position: absolute; + border-radius: 50%; + background-color: var(--tree-clr); + top: calc(var(--tree-item-height) / 2 * 1rem); + left: var(--tree-offset) ; + translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1); + } +} diff --git a/docs/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/mkdocs.yml b/mkdocs.yml index 828a81a4..dc53327f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,7 +34,7 @@ nav: - Guides: - Recipes: recipes.md - Troubleshooting: troubleshooting.md -- API reference: reference/mkdocstrings.md +- API reference: reference/api.md - Development: - Contributing: contributing.md - Code of Conduct: code_of_conduct.md @@ -143,6 +143,7 @@ plugins: - https://markupsafe.palletsprojects.com/en/stable/objects.inv paths: [src] options: + backlinks: tree docstring_options: ignore_init_summary: true docstring_section_style: list @@ -161,16 +162,17 @@ plugins: signature_crossrefs: true summary: true - llmstxt: - files: - - output: llms-full.txt - inputs: + full_output: llms-full.txt + sections: + Usage: - index.md - usage/index.md - usage/handlers.md - usage/theming.md - recipes.md - troubleshooting.md - - reference/**.md + API: + - reference/api.md - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true @@ -179,14 +181,15 @@ plugins: 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 + reference/mkdocstrings.md: reference/api.md + reference/index.md: reference/api.md#mkdocstrings + reference/extension.md: reference/api.md#mkdocstrings.extension + reference/handlers/index.md: reference/api.md#mkdocstrings.handlers + reference/handlers/base.md: reference/api.md#mkdocstrings.handlers.base + reference/handlers/rendering.md: reference/api.md#mkdocstrings.handlers.rendering + reference/inventory.md: reference/api.md#mkdocstrings.inventory + reference/loggers.md: reference/api.md#mkdocstrings.loggers + reference/plugin.md: reference/api.md#mkdocstrings.plugin - minify: minify_html: !ENV [DEPLOY, false] - group: diff --git a/pyproject.toml b/pyproject.toml index 29aff0d9..4db21462 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ getter = "scripts.get_version:get_version" [tool.pdm.build] # Include as much as possible in the source distribution, to help redistributors. -excludes = ["**/.pytest_cache"] +excludes = ["**/.pytest_cache", "**/.mypy_cache"] source-includes = [ "config", "docs", @@ -112,7 +112,7 @@ ci = [ "mkdocs>=1.6", "mkdocs-coverage>=1.0", "mkdocs-git-revision-date-localized-plugin>=1.2", - "mkdocs-llmstxt>=0.1", + "mkdocs-llmstxt>=0.2", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", diff --git a/tests/test_api.py b/tests/test_api.py index 4b13aac7..57f0ce20 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -155,7 +155,11 @@ def test_inventory_matches_api( 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): diff --git a/tests/test_extension.py b/tests/test_extension.py index dd3d7028..c283f9c0 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -198,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: From 2b4ed541bc707e55d959092d950ebeecc4fbd136 Mon Sep 17 00:00:00 2001 From: Nyuan Zhang Date: Fri, 11 Jul 2025 00:19:09 +0800 Subject: [PATCH 184/223] feat: Add I18N support (translations) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-645: https://github.com/mkdocstrings/mkdocstrings/pull/645 Co-authored-by: Timothรฉe Mazzucotelli --- docs/schema.json | 6 ++++++ docs/usage/index.md | 12 ++++++++++++ src/mkdocstrings/_internal/extension.py | 8 +++++++- src/mkdocstrings/_internal/handlers/base.py | 18 +++++++++++++++--- src/mkdocstrings/_internal/plugin.py | 15 ++++++++++++++- tests/test_plugin.py | 2 ++ 6 files changed, 56 insertions(+), 5 deletions(-) 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/index.md b/docs/usage/index.md index d208b4b0..64588fdf 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -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/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 83421ff8..f67b5cfc 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -23,6 +23,8 @@ 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 @@ -197,8 +199,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 # type: ignore[assignment] 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'.", diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index c87f473c..b5641959 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -319,20 +319,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: @@ -578,6 +587,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 +601,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 +611,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) diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index afc94490..2af14a7a 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -15,6 +15,8 @@ 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 @@ -73,6 +75,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,6 +135,9 @@ 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, @@ -140,6 +147,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: 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, ) @@ -234,7 +242,12 @@ def repl(match: Match) -> str: 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 # type: ignore[assignment] + + return render_backlinks(backlinks) for file in files: if file.page and file.page.content: diff --git a/tests/test_plugin.py b/tests/test_plugin.py index af8a5594..acb9556c 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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, } From f856160b03b2c27e1d75fdf4f315c273cb9d9247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 21 Jul 2025 20:49:21 +0200 Subject: [PATCH 185/223] feat: Add `data-skip-inventory` boolean attribute for elements to skip registration in local inventory Issue-671: https://github.com/mkdocstrings/mkdocstrings/issues/671 PR-774: https://github.com/mkdocstrings/mkdocstrings/pull/774 --- src/mkdocstrings/_internal/extension.py | 9 +++++++++ src/mkdocstrings/_internal/handlers/base.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index f67b5cfc..00b112d3 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -251,6 +251,15 @@ 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) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index b5641959..60910436 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -459,6 +459,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. @@ -469,6 +470,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: @@ -488,6 +490,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: From 2be445f054c1191d308af7ee0cc881e359f5a4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 23 Jul 2025 01:48:33 +0200 Subject: [PATCH 186/223] chore: Prepare release 0.30.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed973133..658ae74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.30.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.30.0) - 2025-07-23 + +[Compare with 0.29.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.29.1...0.30.0) + +### Features + +- Add `data-skip-inventory` boolean attribute for elements to skip registration in local inventory ([f856160](https://github.com/mkdocstrings/mkdocstrings/commit/f856160b03b2c27e1d75fdf4f315c273cb9d9247) by Bartosz Sล‚awecki). [Issue-671](https://github.com/mkdocstrings/mkdocstrings/issues/671), [PR-774](https://github.com/mkdocstrings/mkdocstrings/pull/774) +- Add I18N support (translations) ([2b4ed54](https://github.com/mkdocstrings/mkdocstrings/commit/2b4ed541bc707e55d959092d950ebeecc4fbd136) by Nyuan Zhang). [PR-645](https://github.com/mkdocstrings/mkdocstrings/pull/645), Co-authored-by: Timothรฉe Mazzucotelli + ## [0.29.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.29.1) - 2025-03-31 [Compare with 0.29.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.29.0...0.29.1) From 85f4479690f9845a2d3d6d228176f2cc391106d8 Mon Sep 17 00:00:00 2001 From: Deven Mistry <31466137+deven367@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:16:18 -0400 Subject: [PATCH 187/223] docs: Fix broken NVidia link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d4b8bb0..d46f9bf7 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo [IBM](https://ds4sd.github.io/docling/api_reference/document_converter/), [Jitsi](https://jitsi.github.io/jiwer/reference/alignment/), [Microsoft](https://microsoft.github.io/presidio/api/analyzer_python/), -[NVIDIA](https://nvidia.github.io/bionemo-framework/API_reference/bionemo/core/api/), +[NVIDIA](https://nvidia.github.io/bionemo-framework/main/references/API_reference/bionemo/core/api/), [Prefect](https://docs.prefect.io/2.10.12/api-ref/prefect/agent/), [Pydantic](https://docs.pydantic.dev/dev-v2/api/main/), [Textual](https://textual.textualize.io/api/app/), From 572677173c2d31e5779f6313e48e90cf265b9c62 Mon Sep 17 00:00:00 2001 From: Mark Shui Hu Date: Sun, 24 Aug 2025 17:29:27 +0200 Subject: [PATCH 188/223] doc: Add links to MATLAB handler PR-789: https://github.com/mkdocstrings/mkdocstrings/pull/789 --- README.md | 1 + docs/usage/handlers.md | 1 + mkdocs.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index d46f9bf7..cb6887e6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo [C](https://mkdocstrings.github.io/c/), [Crystal](https://mkdocstrings.github.io/crystal/), [Python](https://mkdocstrings.github.io/python/), + [MATLAB](https://watermarkhu.nl/mkdocstrings-matlab/), [TypeScript](https://mkdocstrings.github.io/typescript/), and [VBA](https://pypi.org/project/mkdocstrings-vba/) languages, as well as for [shell scripts/libraries](https://mkdocstrings.github.io/shell/). diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index 0d375a95..c4bbda57 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -8,6 +8,7 @@ A handler is what makes it possible to collect and render documentation for a pa - [Crystal](https://mkdocstrings.github.io/crystal/){ .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 } - [VBA](https://pypi.org/project/mkdocstrings-vba/){ .external } diff --git a/mkdocs.yml b/mkdocs.yml index dc53327f..9163e91d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - Crystal: https://mkdocstrings.github.io/crystal/ - 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 From eec7fb4bab948ef6db594fc1d1688be0554c5780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87a=C4=9Flar=20Kutlu?= Date: Mon, 15 Sep 2025 12:14:26 +0200 Subject: [PATCH 189/223] fix: Create default SSL context in main thread before downloading inventories Issue-796: https://github.com/mkdocstrings/mkdocstrings/issue/796 PR-797: https://github.com/mkdocstrings/mkdocstrings/pull/797 --- src/mkdocstrings/_internal/handlers/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 60910436..c4e9950d 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -7,6 +7,7 @@ import datetime import importlib import inspect +import ssl import sys from concurrent import futures from io import BytesIO @@ -753,6 +754,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) From 18f650441629ca56a30befccb5fb0b3a361aff11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 15 Sep 2025 12:17:01 +0200 Subject: [PATCH 190/223] ci: Fix warning --- src/mkdocstrings/_internal/extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 00b112d3..2277775c 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -125,7 +125,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) From b550cdb6be37a65b487154b0edbd5cedc822b4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Fri, 19 Sep 2025 12:49:13 +0200 Subject: [PATCH 191/223] chore: Prepare release 0.30.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658ae74f..6346ccd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.30.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.30.1) - 2025-09-19 + +[Compare with 0.30.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.30.0...0.30.1) + +### Bug Fixes + +- Create default SSL context in main thread before downloading inventories ([eec7fb4](https://github.com/mkdocstrings/mkdocstrings/commit/eec7fb4bab948ef6db594fc1d1688be0554c5780) by ร‡aฤŸlar Kutlu). [Issue-796](https://github.com/mkdocstrings/mkdocstrings/issue/796), [PR-797](https://github.com/mkdocstrings/mkdocstrings/pull/797) + ## [0.30.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.30.0) - 2025-07-23 [Compare with 0.29.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.29.1...0.30.0) From 438be895b5edfe92121b2bd3a0654959ca343d4f Mon Sep 17 00:00:00 2001 From: Mark Shui Hu Date: Sun, 28 Sep 2025 18:00:21 +0200 Subject: [PATCH 192/223] docs: Add links to GitHub Actions handler PR-800: https://github.com/mkdocstrings/mkdocstrings/pull/800 --- README.md | 1 + docs/usage/handlers.md | 1 + mkdocs.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index cb6887e6..23d3246c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) for the [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/), [MATLAB](https://watermarkhu.nl/mkdocstrings-matlab/), [TypeScript](https://mkdocstrings.github.io/typescript/), and diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index c4bbda57..efdaccd1 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -6,6 +6,7 @@ A handler is what makes it possible to collect and render documentation for a pa - [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 } diff --git a/mkdocs.yml b/mkdocs.yml index 9163e91d..52721b3e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - 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/ From 75ff96877a7602a6ae02839b00ebc03e5b8fc91d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Oct 2025 13:15:07 +0200 Subject: [PATCH 193/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/pull_request_template.md | 15 ++++++++ .github/workflows/ci.yml | 29 ++++++++++++-- Makefile | 7 ++++ README.md | 2 +- config/pytest.ini | 4 ++ config/pytest_39.ini | 19 ++++++++++ docs/js/insiders.js | 13 ++++--- duties.py | 23 +++++++---- scripts/get_version.py | 7 +++- scripts/make.py | 59 +++++++++++++++++++++++------ src/mkdocstrings/_internal/debug.py | 2 +- tests/test_api.py | 6 ++- 13 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 config/pytest_39.ini diff --git a/.copier-answers.yml b/.copier-answers.yml index fa6c4f9c..e200114f 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.8.4 +_commit: 1.10.1 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..6f0f2faf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +### For reviewers + + +- [ ] I did not use AI +- [ ] I used AI and thoroughly reviewed every code/docs change + +### Description of the change + + +### Relevant resources + + +- +- +- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dd6da41..b721b7a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,16 @@ name: ci on: push: + - main + - test-me-* pull_request: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + defaults: run: shell: bash @@ -15,13 +21,30 @@ env: LC_ALL: en_US.utf-8 PYTHONIOENCODING: UTF-8 PYTHONPATH: docs + PYTHONWARNDEFAULTENCODING: "1" PYTHON_VERSIONS: "" jobs: quality: + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + python-version: + - "3.9" + - "3.13" + include: + - os: ubuntu-latest + python-version: "3.10" + - os: ubuntu-latest + python-version: "3.11" + - os: ubuntu-latest + python-version: "3.12" - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -33,7 +56,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Setup uv uses: astral-sh/setup-uv@v5 @@ -109,7 +132,7 @@ jobs: - lowest-direct exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.python-version == '3.14' }} + continue-on-error: true steps: - name: Checkout diff --git a/Makefile b/Makefile index 5e88121d..1b3391da 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,13 @@ # the `make` command will point at the `scripts/make` shell script. # This Makefile is just here to allow auto-completion in the terminal. +default: help + @echo + @echo 'Enable direnv in your shell to use the `make` command: `direnv allow`' + @echo 'Or use `python scripts/make ARGS` to run the commands/tasks directly.' + +.DEFAULT_GOAL: default + actions = \ allrun \ changelog \ diff --git a/README.md b/README.md index 23d3246c..af264b77 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/) [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) -[![gitter](https://badges.gitter.im/join%20chat.svg)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) +[![gitter](https://img.shields.io/badge/matrix-chat-4DB798.svg?style=flat)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) Automatic documentation from sources, for [MkDocs](https://www.mkdocs.org/). Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdocstrings/community). diff --git a/config/pytest.ini b/config/pytest.ini index 288b6cff..f65bd620 100644 --- a/config/pytest.ini +++ b/config/pytest.ini @@ -10,5 +10,9 @@ testpaths = # action:message_regex:warning_class:module_regex:line filterwarnings = error + default::EncodingWarning + error::EncodingWarning:mkdocstrings ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings ignore:.*Importing from:DeprecationWarning:mkdocstrings_handlers + # TODO: Remove once pytest-xdist 4 is released. + ignore:.*rsyncdir:DeprecationWarning:xdist diff --git a/config/pytest_39.ini b/config/pytest_39.ini new file mode 100644 index 00000000..27f45425 --- /dev/null +++ b/config/pytest_39.ini @@ -0,0 +1,19 @@ +# YORE: EOL 3.9: Remove file. +# This file is used on 3.9 due to forward compatibility issue with filterwarnings. +# See https://github.com/pytest-dev/pytest/issues/11101. +[pytest] +python_files = + test_*.py +addopts = + --cov + --cov-config config/coverage.ini +testpaths = + tests + +# action:message_regex:warning_class:module_regex:line +filterwarnings = + error + ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings + ignore:.*Importing from:DeprecationWarning:mkdocstrings_handlers + # TODO: Remove once pytest-xdist 4 is released. + ignore:.*rsyncdir:DeprecationWarning:xdist diff --git a/docs/js/insiders.js b/docs/js/insiders.js index 8bb68485..a86a0918 100644 --- a/docs/js/insiders.js +++ b/docs/js/insiders.js @@ -29,11 +29,14 @@ function updatePremiumSponsors(dataURL, rank) { let html = ''; html += `${capRank} sponsors

    ` sponsors.forEach(function (sponsor) { - html += ` - - ${sponsor.name} - - ` + html += `` + if (sponsor.image) { + html += `${sponsor.name}` + } else if (sponsor.imageLight && sponsor.imageDark) { + html += `${sponsor.name}` + html += `${sponsor.name}` + } + html += ''; }); html += '

    ' sponsorsDiv.innerHTML = html; diff --git a/duties.py b/duties.py index b75d8b55..ee20b497 100644 --- a/duties.py +++ b/duties.py @@ -26,6 +26,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 = "314" def pyprefix(title: str) -> str: @@ -84,7 +86,7 @@ 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( @@ -93,7 +95,7 @@ def check_quality(ctx: Context) -> None: ) -@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) @@ -105,7 +107,7 @@ def check_docs(ctx: Context) -> None: ) -@duty +@duty(nofail=PY_VERSION == PY_DEV) def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" os.environ["MYPYPATH"] = "src" @@ -116,7 +118,7 @@ def check_types(ctx: Context) -> None: ) -@duty +@duty(nofail=PY_VERSION == PY_DEV) def check_api(ctx: Context, *cli_args: str) -> None: """Check for API breaking changes.""" ctx.run( @@ -237,19 +239,24 @@ def coverage(ctx: Context) -> None: ctx.run(tools.coverage.html(rcfile="config/coverage.ini")) -@duty +@duty(nofail=PY_VERSION == PY_DEV) def test(ctx: Context, *cli_args: str, match: str = "") -> None: # noqa: PT028 """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}" + os.environ["COVERAGE_FILE"] = f".coverage.{PY_VERSION}" + os.environ["PYTHONWARNDEFAULTENCODING"] = "1" + config_file = "config/pytest.ini" + # YORE: EOL 3.9: Remove block. + if sys.version_info[:2] < (3, 10): + config_file = "config/pytest_39.ini" + ctx.run( tools.pytest( "tests", - config_file="config/pytest.ini", + config_file=config_file, select=match, color="yes", ).add_args("-n", "auto", *cli_args), diff --git a/scripts/get_version.py b/scripts/get_version.py index 6734e5b6..3c425a73 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" diff --git a/scripts/make.py b/scripts/make.py index 55679baa..1e697bcc 100755 --- a/scripts/make.py +++ b/scripts/make.py @@ -15,6 +15,7 @@ PYTHON_VERSIONS = os.getenv("PYTHON_VERSIONS", "3.9 3.10 3.11 3.12 3.13 3.14").split() +PYTHON_DEV = "3.14" 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: @@ -144,19 +160,31 @@ def main() -> int: cmd = args.pop(0) if cmd == "run": - run("default", *args) + if not args: + print("make: run: missing command", file=sys.stderr) + return 1 + run("default", *args) # ty: ignore[missing-argument] return 0 if cmd == "multirun": - multirun(*args) + if not args: + print("make: run: missing command", file=sys.stderr) + return 1 + multirun(*args) # ty: ignore[missing-argument] return 0 if cmd == "allrun": - allrun(*args) + if not args: + print("make: run: missing command", file=sys.stderr) + return 1 + allrun(*args) # ty: ignore[missing-argument] return 0 if cmd.startswith("3."): - run(cmd, *args) + if not args: + print("make: run: missing command", file=sys.stderr) + return 1 + run(cmd, *args) # ty: ignore[missing-argument] return 0 opts = [] @@ -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/_internal/debug.py b/src/mkdocstrings/_internal/debug.py index 7b56409b..f6b11600 100644 --- a/src/mkdocstrings/_internal/debug.py +++ b/src/mkdocstrings/_internal/debug.py @@ -85,7 +85,7 @@ def _get_debug_info() -> _Environment: interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), - variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], + variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], # ty: ignore[invalid-argument-type] packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) diff --git a/tests/test_api.py b/tests/test_api.py index 57f0ce20..ea672073 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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.") # ty: ignore[call-non-callable] with inventory_file.open("rb") as file: return Inventory.parse_sphinx(file) @@ -137,7 +137,9 @@ 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}" assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) From 5698963317af8e5d790a558fd945371f6ef3811a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 9 Oct 2025 17:52:31 +0200 Subject: [PATCH 194/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index e200114f..f459d439 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.10.1 +_commit: 1.10.2 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b721b7a5..253c08f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,9 @@ name: ci on: push: - - main - - test-me-* + branches: + - main + - test-me-* pull_request: branches: - main @@ -36,13 +37,13 @@ jobs: python-version: - "3.9" - "3.13" - include: - - os: ubuntu-latest - python-version: "3.10" - - os: ubuntu-latest - python-version: "3.11" - - os: ubuntu-latest - python-version: "3.12" + include: + - os: ubuntu-latest + python-version: "3.10" + - os: ubuntu-latest + python-version: "3.11" + - os: ubuntu-latest + python-version: "3.12" runs-on: ${{ matrix.os }} From b8f35c14f1b93408096cd2289782159beb0cdf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 10 Nov 2025 12:01:43 +0100 Subject: [PATCH 195/223] chore: Template upgrade --- .copier-answers.yml | 6 +- .github/workflows/ci.yml | 46 +++------ .github/workflows/release.yml | 21 +--- .github/workflows/sponsors.yml | 26 +++++ README.md | 5 + config/pytest_39.ini | 19 ---- docs/.overrides/main.html | 12 +-- docs/css/insiders.css | 124 ----------------------- docs/insiders/changelog.md | 3 - docs/insiders/goals.yml | 13 --- docs/insiders/index.md | 166 ------------------------------- docs/insiders/installation.md | 67 ------------- docs/js/insiders.js | 77 --------------- duties.py | 70 ++----------- mkdocs.yml | 8 +- pyproject.toml | 3 +- scripts/insiders.py | 173 --------------------------------- scripts/make.py | 4 +- 18 files changed, 65 insertions(+), 778 deletions(-) create mode 100644 .github/workflows/sponsors.yml delete mode 100644 config/pytest_39.ini delete mode 100644 docs/css/insiders.css delete mode 100644 docs/insiders/changelog.md delete mode 100644 docs/insiders/goals.yml delete mode 100644 docs/insiders/index.md delete mode 100644 docs/insiders/installation.md delete mode 100644 docs/js/insiders.js delete mode 100644 scripts/insiders.py diff --git a/.copier-answers.yml b/.copier-answers.yml index f459d439..6c5d650e 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.10.2 +_commit: 1.11.1 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli @@ -8,12 +8,8 @@ copyright_date: '2019' copyright_holder: Timothรฉe Mazzucotelli copyright_holder_email: dev@pawamoy.fr copyright_license: ISC -insiders: true -insiders_email: insiders@pawamoy.fr -insiders_repository_name: mkdocstrings project_description: Automatic documentation from sources, for MkDocs. project_name: mkdocstrings -public_release: true python_package_command_line_name: '' python_package_distribution_name: mkdocstrings python_package_import_name: mkdocstrings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 253c08f9..54dd741c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,15 +35,15 @@ jobs: - macos-latest - windows-latest python-version: - - "3.9" - - "3.13" + - "3.10" + - "3.14" include: - - os: ubuntu-latest - python-version: "3.10" - os: ubuntu-latest python-version: "3.11" - os: ubuntu-latest python-version: "3.12" + - os: ubuntu-latest + python-version: "3.13" runs-on: ${{ matrix.os }} @@ -55,7 +55,7 @@ jobs: fetch-tags: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -82,39 +82,15 @@ jobs: - name: Store objects inventory for tests uses: actions/upload-artifact@v4 + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13' }} with: name: objects.inv path: site/objects.inv - exclude-test-jobs: - runs-on: ubuntu-latest - outputs: - jobs: ${{ steps.exclude-jobs.outputs.jobs }} - steps: - - id: exclude-jobs - run: | - if ${{ github.repository_owner == 'pawamoy-insiders' }}; then - echo 'jobs=[ - {"os": "macos-latest"}, - {"os": "windows-latest"}, - {"python-version": "3.10"}, - {"python-version": "3.11"}, - {"python-version": "3.12"}, - {"python-version": "3.13"}, - {"python-version": "3.14"} - ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT - else - echo 'jobs=[ - {"os": "macos-latest", "resolution": "lowest-direct"}, - {"os": "windows-latest", "resolution": "lowest-direct"} - ]' | tr -d '[:space:]' >> $GITHUB_OUTPUT - fi - tests: needs: - quality - - exclude-test-jobs strategy: matrix: os: @@ -122,16 +98,20 @@ jobs: - macos-latest - windows-latest python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" + - "3.15" resolution: - highest - lowest-direct - exclude: ${{ fromJSON(needs.exclude-test-jobs.outputs.jobs) }} + exclude: + - os: macos-latest + resolution: lowest-direct + - os: windows-latest + resolution: lowest-direct runs-on: ${{ matrix.os }} continue-on-error: true @@ -143,7 +123,7 @@ jobs: fetch-tags: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73347dad..1c7cda36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,30 +15,15 @@ jobs: fetch-depth: 0 fetch-tags: true - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.13" - name: Setup uv uses: astral-sh/setup-uv@v5 - - name: Build dists - if: github.repository_owner == 'pawamoy-insiders' - run: uv tool run --from build pyproject-build - - name: Upload dists artifact - uses: actions/upload-artifact@v4 - if: github.repository_owner == 'pawamoy-insiders' - with: - name: mkdocstrings-insiders - path: ./dist/* - name: Prepare release notes - if: github.repository_owner != 'pawamoy-insiders' run: uv tool run git-changelog --release-notes > release-notes.md - - name: Create release with assets - uses: softprops/action-gh-release@v2 - if: github.repository_owner == 'pawamoy-insiders' - with: - files: ./dist/* - name: Create release uses: softprops/action-gh-release@v2 - if: github.repository_owner != 'pawamoy-insiders' with: body_path: release-notes.md + diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml new file mode 100644 index 00000000..8dd9150f --- /dev/null +++ b/.github/workflows/sponsors.yml @@ -0,0 +1,26 @@ +name: Update sponsors + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-readme: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Update README and create PR + uses: pawamoy/readme-insert@main + with: + markup-url: https://pawamoy.github.io/sponsors.txt + start-marker: '' + end-marker: '' + commit-message: 'chore: Update sponsors section in README' + pr-title: 'chore: Update sponsors section in README' + pr-body: 'This PR updates the sponsors section in the README file.' diff --git a/README.md b/README.md index af264b77..20f6fd39 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,8 @@ In one of your markdown files: ``` See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for more examples! + +## Sponsors + + + diff --git a/config/pytest_39.ini b/config/pytest_39.ini deleted file mode 100644 index 27f45425..00000000 --- a/config/pytest_39.ini +++ /dev/null @@ -1,19 +0,0 @@ -# YORE: EOL 3.9: Remove file. -# This file is used on 3.9 due to forward compatibility issue with filterwarnings. -# See https://github.com/pytest-dev/pytest/issues/11101. -[pytest] -python_files = - test_*.py -addopts = - --cov - --cov-config config/coverage.ini -testpaths = - tests - -# action:message_regex:warning_class:module_regex:line -filterwarnings = - error - ignore:.*`get_anchors` method:DeprecationWarning:mkdocstrings - ignore:.*Importing from:DeprecationWarning:mkdocstrings_handlers - # TODO: Remove once pytest-xdist 4 is released. - ignore:.*rsyncdir:DeprecationWarning:xdist diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index 1e956857..c702362f 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -1,13 +1,11 @@ {% extends "base.html" %} {% block announce %} - - Fund this project through - sponsorship - - {% include ".icons/octicons/heart-fill-16.svg" %} - — - + Fund this project through + sponsorship + + {% include ".icons/octicons/heart-fill-16.svg" %} + — Follow @pawamoy on diff --git a/docs/css/insiders.css b/docs/css/insiders.css deleted file mode 100644 index e7b9c74f..00000000 --- a/docs/css/insiders.css +++ /dev/null @@ -1,124 +0,0 @@ -@keyframes heart { - - 0%, - 40%, - 80%, - 100% { - transform: scale(1); - } - - 20%, - 60% { - transform: scale(1.15); - } -} - -@keyframes vibrate { - 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% { - -webkit-transform: translate3d(-2px, 0, 0); - transform: translate3d(-2px, 0, 0); - } - 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% { - -webkit-transform: translate3d(2px, 0, 0); - transform: translate3d(2px, 0, 0); - } - 20%, 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.heart { - color: #e91e63; -} - -.pulse { - animation: heart 1000ms infinite; -} - -.vibrate { - animation: vibrate 2000ms infinite; -} - -.new-feature svg { - fill: var(--md-accent-fg-color) !important; -} - -a.insiders { - color: #e91e63; -} - -.sponsorship-list { - width: 100%; -} - -.sponsorship-item { - border-radius: 100%; - display: inline-block; - height: 1.6rem; - margin: 0.1rem; - overflow: hidden; - width: 1.6rem; -} - -.sponsorship-item:focus, .sponsorship-item:hover { - transform: scale(1.1); -} - -.sponsorship-item img { - filter: grayscale(100%) opacity(75%); - height: auto; - width: 100%; -} - -.sponsorship-item:focus img, .sponsorship-item:hover img { - filter: grayscale(0); -} - -.sponsorship-item.private { - background: var(--md-default-fg-color--lightest); - color: var(--md-default-fg-color); - font-size: .6rem; - font-weight: 700; - line-height: 1.6rem; - text-align: center; -} - -.mastodon { - color: #897ff8; - border-radius: 100%; - box-shadow: inset 0 0 0 .05rem currentcolor; - display: inline-block; - height: 1.2rem !important; - padding: .25rem; - transition: all .25s; - vertical-align: bottom !important; - width: 1.2rem; -} - -.premium-sponsors { - text-align: center; -} - -#silver-sponsors img { - height: 140px; -} - -#bronze-sponsors img { - height: 140px; -} - -#bronze-sponsors p { - display: flex; - flex-wrap: wrap; - justify-content: center; -} - -#bronze-sponsors a { - display: block; - flex-shrink: 0; -} - -.sponsors-total { - font-weight: bold; -} \ No newline at end of file diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md deleted file mode 100644 index 0f438566..00000000 --- a/docs/insiders/changelog.md +++ /dev/null @@ -1,3 +0,0 @@ -# Changelog - -## mkdocstrings Insiders diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml deleted file mode 100644 index 0e27b997..00000000 --- a/docs/insiders/goals.yml +++ /dev/null @@ -1,13 +0,0 @@ -goals: - 500: - name: PlasmaVac User Guide - features: [] - 1000: - name: GraviFridge Fluid Renewal - features: [] - 1500: - name: HyperLamp Navigation Tips - features: [] - 2000: - name: FusionDrive Ejection Configuration - features: [] diff --git a/docs/insiders/index.md b/docs/insiders/index.md deleted file mode 100644 index ce59f6bb..00000000 --- a/docs/insiders/index.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Insiders ---- - -# Insiders - -*mkdocstrings* follows the **sponsorware** release strategy, which means that new features are first exclusively released to sponsors as part of [Insiders][]. Read on to learn [what sponsorships achieve][sponsorship], [how to become a sponsor][sponsors] to get access to Insiders, and [what's in it for you][features]! - -## What is Insiders? - -*mkdocstrings Insiders* is a private fork of *mkdocstrings*, hosted as a private GitHub repository. Almost[^1] [all new features][features] are developed as part of this fork, which means that they are immediately available to all eligible sponsors, as they are granted access to this private repository. - -[^1]: In general, every new feature is first exclusively released to sponsors, but sometimes upstream dependencies enhance existing features that must be supported by *mkdocstrings*. - -Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a funding goal is hit, the features that are tied to it are merged back into *mkdocstrings* and released for general availability, making them available to all users. Bugfixes are always released in tandem. - -Sponsorships start as low as [**$10 a month**][sponsors].[^2] - -[^2]: Note that $10 a month is the minimum amount to become eligible for Insiders. While GitHub Sponsors also allows to sponsor lower amounts or one-time amounts, those can't be granted access to Insiders due to technical reasons. Such contributions are still very much welcome as they help ensuring the project's sustainability. - -## What sponsorships achieve - -Sponsorships make this project sustainable, as they buy the maintainers of this project time โ€“ a very scarce resource โ€“ which is spent on the development of new features, bug fixing, stability improvement, issue triage and general support. The biggest bottleneck in Open Source is time.[^3] - -[^3]: Making an Open Source project sustainable is exceptionally hard: maintainers burn out, projects are abandoned. That's not great and very unpredictable. The sponsorware model ensures that if you decide to use *mkdocstrings*, you can be sure that bugs are fixed quickly and new features are added regularly. - -If you're unsure if you should sponsor this project, check out the list of [completed funding goals][goals completed] to learn whether you're already using features that were developed with the help of sponsorships. You're most likely using at least a handful of them, [thanks to our awesome sponsors][sponsors]! - -## What's in it for me? - -```python exec="1" session="insiders" -data_source = [ - "docs/insiders/goals.yml", - ("griffe-pydantic", "https://mkdocstrings.github.io/griffe-pydantic/", "insiders/goals.yml"), - ("griffe-typedoc", "https://mkdocstrings.github.io/griffe-typedoc/", "insiders/goals.yml"), - ("griffe-warnings-deprecated", "https://mkdocstrings.github.io/griffe-warnings-deprecated/", "insiders/goals.yml"), - ("mkdocstrings-c", "https://mkdocstrings.github.io/c/", "insiders/goals.yml"), - ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"), - ("mkdocstrings-shell", "https://mkdocstrings.github.io/shell/", "insiders/goals.yml"), - ("mkdocstrings-typescript", "https://mkdocstrings.github.io/typescript/", "insiders/goals.yml"), -] -``` - - -```python exec="1" session="insiders" idprefix="" ---8<-- "scripts/insiders.py" - -if unreleased_features: - print( - "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get **immediate " - f"access to {len(unreleased_features)} additional features** that you can start using right away, and " - "which are currently exclusively available to sponsors:\n" - ) - - for feature in unreleased_features: - feature.render(badge=True) - - print( - "\n\nThese are just the features related to this project. " - "[See the complete feature list on the author's main Insiders page](https://pawamoy.github.io/insiders/#whats-in-it-for-me)." - ) -else: - print( - "The moment you [become a sponsor](#how-to-become-a-sponsor), you'll get immediate " - "access to all released features that you can start using right away, and " - "which are exclusively available to sponsors. At this moment, there are no " - "Insiders features for this project, but checkout the [next funding goals](#goals) " - "to see what's coming, as well as **[the feature list for all Insiders projects](https://pawamoy.github.io/insiders/#whats-in-it-for-me).**" - ) -``` - - -Additionally, your sponsorship will give more weight to your upvotes on issues, helping us prioritize work items in our backlog. For more information on how we prioritize work, see this page: [Backlog management][backlog]. - -## How to become a sponsor - -Thanks for your interest in sponsoring! In order to become an eligible sponsor with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile], and complete a sponsorship of **$10 a month or more**. You can use your individual or organization GitHub account for sponsoring. - -Sponsorships lower than $10 a month are also very much appreciated, and useful. They won't grant you access to Insiders, but they will be counted towards reaching sponsorship goals. Every sponsorship helps us implementing new features and releasing them to the public. - -**Important:** By default, when you're sponsoring **[@pawamoy][github sponsor profile]** through a GitHub organization, all the publicly visible members of the organization will be invited to join our private repositories. If you wish to only grant access to a subset of users, please send a short email to insiders@pawamoy.fr with the name of your organization and the GitHub accounts of the users that should be granted access. - -**Tip:** to ensure that access is not tied to a particular individual GitHub account, you can create a bot account (i.e. a GitHub account that is not tied to a specific individual), and use this account for the sponsoring. After being granted access to our private repositories, the bot account can create private forks of our private repositories into your own organization, which all members of your organization will have access to. - -You can cancel your sponsorship anytime.[^5] - -[^5]: If you cancel your sponsorship, GitHub schedules a cancellation request which will become effective at the end of the billing cycle. This means that even though you cancel your sponsorship, you will keep your access to Insiders as long as your cancellation isn't effective. All charges are processed by GitHub through Stripe. As we don't receive any information regarding your payment, and GitHub doesn't offer refunds, sponsorships are non-refundable. - - -[:octicons-heart-fill-24:{ .pulse }   Join our awesome sponsors][github sponsor profile]{ .md-button .md-button--primary } - -
    -
    -
    -
    -
    -
    -
    - -
    - - - If you sponsor publicly, you're automatically added here with a link to your profile and avatar to show your support for *mkdocstrings*. Alternatively, if you wish to keep your sponsorship private, you'll be a silent +1. You can select visibility during checkout and change it afterwards. - - -## Funding - -### Goals - -The following section lists all funding goals. Each goal contains a list of features prefixed with a checkmark symbol, denoting whether a feature is :octicons-check-circle-fill-24:{ style="color: #00e676" } already available or :octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned, but not yet implemented. When the funding goal is hit, the features are released for general availability. - -```python exec="1" session="insiders" idprefix="" -for goal in goals.values(): - if not goal.complete: - goal.render() -``` - -### Goals completed - -This section lists all funding goals that were previously completed, which means that those features were part of Insiders, but are now generally available and can be used by all users. - -```python exec="1" session="insiders" idprefix="" -for goal in goals.values(): - if goal.complete: - goal.render() -``` - -## Frequently asked questions - -### Compatibility - -> We're building an open source project and want to allow outside collaborators to use *mkdocstrings* locally without having access to Insiders. Is this still possible? - -Yes. Insiders is compatible with *mkdocstrings*. Almost all new features and configuration options are either backward-compatible or implemented behind feature flags. Most Insiders features enhance the overall experience, though while these features add value for the users of your project, they shouldn't be necessary for previewing when making changes to content. - -### Payment - -> We don't want to pay for sponsorship every month. Are there any other options? - -Yes. You can sponsor on a yearly basis by [switching your GitHub account to a yearly billing cycle][billing cycle]. If for some reason you cannot do that, you could also create a dedicated GitHub account with a yearly billing cycle, which you only use for sponsoring (some sponsors already do that). - -If you have any problems or further questions, please reach out to insiders@pawamoy.fr. - -### Terms - -> Are we allowed to use Insiders under the same terms and conditions as *mkdocstrings*? - -Yes. Whether you're an individual or a company, you may use *mkdocstrings Insiders* precisely under the same terms as *mkdocstrings*, which are given by the [ISC license][license]. However, we kindly ask you to respect our **fair use policy**: - -- Please **don't distribute the source code** of Insiders. You may freely use it for public, private or commercial projects, privately fork or mirror it, but please don't make the source code public, as it would counteract the sponsorware strategy. -- If you cancel your subscription, your access to the private repository is revoked, and you will miss out on all future updates of Insiders. However, you may **use the latest version** that's available to you **as long as you like**. Just remember that [GitHub deletes private forks][private forks]. - -[backlog]: https://pawamoy.github.io/backlog/ -[insiders]: #what-is-insiders -[sponsorship]: #what-sponsorships-achieve -[sponsors]: #how-to-become-a-sponsor -[features]: #whats-in-it-for-me -[funding]: #funding -[goals completed]: #goals-completed -[github sponsor profile]: https://github.com/sponsors/pawamoy -[billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle -[license]: ../license.md -[private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository - - - 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 a86a0918..00000000 --- a/docs/js/insiders.js +++ /dev/null @@ -1,77 +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 += `` - if (sponsor.image) { - html += `${sponsor.name}` - } else if (sponsor.imageLight && sponsor.imageDark) { - html += `${sponsor.name}` - html += `${sponsor.name}` - } - 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 += ` - - +${privateSponsors} - - `; - } - }); - }); - updatePremiumSponsors(dataURL, "gold"); - updatePremiumSponsors(dataURL, "silver"); - updatePremiumSponsors(dataURL, "bronze"); -} diff --git a/duties.py b/duties.py index ee20b497..04357dbb 100644 --- a/duties.py +++ b/duties.py @@ -6,10 +6,9 @@ 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 @@ -37,21 +36,6 @@ 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"): @@ -145,39 +129,13 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000 @duty -def docs_deploy(ctx: Context, *, force: bool = False) -> None: - """Deploy the documentation to GitHub pages. - - Parameters: - force: Whether to force deployment, even from non-Insiders version. - """ +def docs_deploy(ctx: Context) -> None: + """Deploy the documentation to GitHub pages.""" 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.mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation") @duty @@ -201,7 +159,6 @@ def build(ctx: Context) -> None: @duty -@not_from_insiders def publish(ctx: Context) -> None: """Publish source and wheel distributions to PyPI.""" if not Path("dist").exists(): @@ -215,7 +172,6 @@ def publish(ctx: Context) -> None: @duty(post=["build", "publish", "docs-deploy"]) -@not_from_insiders def release(ctx: Context, version: str = "") -> None: """Release a new Python package. @@ -226,7 +182,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) @@ -240,24 +196,14 @@ def coverage(ctx: Context) -> None: @duty(nofail=PY_VERSION == PY_DEV) -def test(ctx: Context, *cli_args: str, match: str = "") -> None: # noqa: PT028 - """Run the test suite. - - Parameters: - match: A pytest expression to filter selected tests. - """ +def test(ctx: Context, *cli_args: str) -> None: + """Run the test suite.""" os.environ["COVERAGE_FILE"] = f".coverage.{PY_VERSION}" os.environ["PYTHONWARNDEFAULTENCODING"] = "1" - config_file = "config/pytest.ini" - # YORE: EOL 3.9: Remove block. - if sys.version_info[:2] < (3, 10): - config_file = "config/pytest_39.ini" - ctx.run( tools.pytest( "tests", - config_file=config_file, - select=match, + config_file="config/pytest.ini", color="yes", ).add_args("-n", "auto", *cli_args), title=pyprefix("Running tests"), diff --git a/mkdocs.yml b/mkdocs.yml index 52721b3e..cdfb5173 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,11 +41,6 @@ nav: - 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: @@ -93,7 +88,6 @@ extra_css: - css/style.css - css/material.css - css/mkdocstrings.css -- css/insiders.css extra_javascript: - js/feedback.js @@ -158,7 +152,7 @@ plugins: show_root_heading: true show_root_full_path: false show_signature_annotations: true - show_source: false + show_source: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true diff --git a/pyproject.toml b/pyproject.toml index 4db21462..14ba163b 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", diff --git a/scripts/insiders.py b/scripts/insiders.py deleted file mode 100644 index 4cd438d4..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 1e697bcc..b741a366 100755 --- a/scripts/make.py +++ b/scripts/make.py @@ -14,8 +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_DEV = "3.14" +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: From 39fbea1c2dd030f017ae6fd4a8653959f310a693 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Nov 2025 00:58:50 +0000 Subject: [PATCH 196/223] chore: Update sponsors section in README --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/README.md b/README.md index 20f6fd39..a486ed40 100644 --- a/README.md +++ b/README.md @@ -137,4 +137,62 @@ See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for mo ## Sponsors + +
    + +
    Silver sponsors

    +Material for MkDocs
    +FastAPI
    +Pydantic
    +

    + +
    Bronze sponsors

    +Nixtla
    +

    +
    + +--- + +

    +ofek +samuelcolvin +tlambert03 +ssbarnea +femtomc +cmarqu +kolenaIO +ramnes +machow +BenHammersley +trevorWieland +laenan8466 +MarcoGorelli +analog-cbarber +OdinManiac +rstudio-sponsorship +schlich +SuperCowPowers +butterlyn +livingbio +NemetschekAllplan +EricJayHartman +15r10nk +cdwilson +activeloopai +roboflow +wrath-codes +leodevian +cmclaughlin +blaisep +RapidataAI +rodolphebarbanneau +theSymbolSyndicate +blakeNaccarato +ChargeStorm +Alphadelta14 +

    + + +*And 8 more private sponsor(s).* + From 3076375ec7eb4c9c8b739e414a919a1be29df07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 19 Nov 2025 18:39:13 +0100 Subject: [PATCH 197/223] chore: Specify encoding when reading file in tests --- tests/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index acb9556c..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: From 6de266759b79eb72cddd300e6a0a8576085fae40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 19 Nov 2025 19:57:17 +0100 Subject: [PATCH 198/223] refactor: Expose the Markdown extension, to make mkdocstrings compatible with Zensical --- src/mkdocstrings/__init__.py | 3 +- src/mkdocstrings/_internal/extension.py | 79 ++++++++++++++++++++- src/mkdocstrings/_internal/handlers/base.py | 6 +- 3 files changed, 83 insertions(+), 5 deletions(-) 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/extension.py b/src/mkdocstrings/_internal/extension.py index 2277775c..c06a967b 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -35,6 +35,7 @@ from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor from mkdocs.exceptions import PluginError +from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin from mkdocstrings._internal.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers from mkdocstrings._internal.loggers import get_logger @@ -43,7 +44,6 @@ from collections.abc import MutableSequence from markdown import Markdown - from mkdocs_autorefs import AutorefsPlugin _logger = get_logger("mkdocstrings") @@ -380,3 +380,80 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me "mkdocstrings_post_toc_labels", priority=4, # Right after 'toc'. ) + + +# ----------------------------------------------------------------------------- +# 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]) -> tuple[list[str], dict[str, Any]]: + # Split markdown extensions and their configs from mkdocs.yml + mdx: list[str] = [] + mdx_config: dict[str, Any] = {} + for item in markdown_extensions: + if isinstance(item, str): + 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 + + +def makeExtension( # noqa: N802 + *args: Any, # noqa: ARG001 + **kwargs: Any, +) -> MkdocstringsExtension: + """Create the extension instance.""" + from zensical.config import _yaml_load # noqa: PLC0415 + + with open("mkdocs.yml", encoding="utf-8") as f: + mkdocs_config = _yaml_load(f) + + mkdocstrings_config = mkdocs_config.get("plugins", None) + if isinstance(mkdocstrings_config, dict): + mkdocstrings_config = mkdocstrings_config.get("mkdocstrings", {}) + elif isinstance(mkdocstrings_config, list): + for plugin in mkdocstrings_config: + if isinstance(plugin, dict) and "mkdocstrings" in plugin: + mkdocstrings_config = plugin["mkdocstrings"] + break + else: + mkdocstrings_config = _default_config + else: + mkdocstrings_config = _default_config + + mdx, mdx_config = _split_configs(mkdocs_config.get("markdown_extensions", [])) + + handlers = Handlers( + theme="material", + default=mkdocstrings_config.get("default_handler", _default_config["default_handler"]), + inventory_project=mkdocs_config.get("site_name", "Project"), + handlers_config=mkdocstrings_config.get("handlers", _default_config["handlers"]), + custom_templates=mkdocstrings_config.get("custom_templates", _default_config["custom_templates"]), + mdx=mdx, + mdx_config=mdx_config, + locale=mkdocstrings_config.get("locale", _default_config["locale"]), + tool_config=mkdocs_config, + ) + + handlers._download_inventories() + + autorefs = AutorefsPlugin() + autorefs.config = AutorefsConfig() + autorefs.scan_toc = False + + return MkdocstringsExtension(handlers=handlers, autorefs=autorefs, **kwargs) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index c4e9950d..af68a45c 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -436,8 +436,7 @@ def do_convert_markdown( treeprocessors[ParagraphStrippingTreeprocessor.name].strip = strip_paragraph # type: ignore[attr-defined] if BacklinksTreeProcessor.name in treeprocessors: treeprocessors[BacklinksTreeProcessor.name].initial_id = html_id # type: ignore[attr-defined] - - if autoref_hook: + if autoref_hook and AutorefsInlineProcessor.name in self.md.inlinePatterns: self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = autoref_hook # type: ignore[attr-defined] try: @@ -448,7 +447,8 @@ def do_convert_markdown( treeprocessors[ParagraphStrippingTreeprocessor.name].strip = False # type: ignore[attr-defined] 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] + if AutorefsInlineProcessor.name in self.md.inlinePatterns: + self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] self.md.reset() _markdown_conversion_layer -= 1 From fc4d588dc73b12c205a933a0a5808742119aa7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 14:47:00 +0100 Subject: [PATCH 199/223] docs: Announce maintenance mode --- docs/.overrides/main.html | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html index c702362f..3bfd4775 100644 --- a/docs/.overrides/main.html +++ b/docs/.overrides/main.html @@ -1,18 +1,7 @@ {% extends "base.html" %} {% block announce %} - Fund this project through - sponsorship - - {% include ".icons/octicons/heart-fill-16.svg" %} - — - Follow - @pawamoy on - - - {% include ".icons/fontawesome/brands/mastodon.svg" %} - - Fosstodon - - for updates + โš ๏ธ mkdocstrings is in maintenance mode! + blog post + {% endblock %} From bebbb88d3f09249b0129b05f98fdbd9f2eaa6818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 14:47:24 +0100 Subject: [PATCH 200/223] chore: Remove trailing space --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index cdfb5173..8250973b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,7 +26,7 @@ nav: - All handlers: - C: https://mkdocstrings.github.io/c/ - Crystal: https://mkdocstrings.github.io/crystal/ - - GitHub Actions: https://watermarkhu.nl/mkdocstrings-github/ + - 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/ From 6b73d5a2f455062ab6c68376c85adce6adc037a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 15:32:23 +0100 Subject: [PATCH 201/223] refactor: Expect Zensical to pass extension configuration instead of loading it again from YAML --- src/mkdocstrings/_internal/extension.py | 60 ++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index c06a967b..8ccea142 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -413,47 +413,47 @@ def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str], di return mdx, mdx_config +class _ToolConfig: + def __init__(self, config_file_path: str | None = None) -> None: + self.config_file_path = config_file_path + + def makeExtension( # noqa: N802 - *args: Any, # noqa: ARG001 - **kwargs: Any, + *, + 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] | None = None, + locale: str | None = None, + config_file_path: str | None = None, ) -> MkdocstringsExtension: - """Create the extension instance.""" - from zensical.config import _yaml_load # noqa: PLC0415 - - with open("mkdocs.yml", encoding="utf-8") as f: - mkdocs_config = _yaml_load(f) - - mkdocstrings_config = mkdocs_config.get("plugins", None) - if isinstance(mkdocstrings_config, dict): - mkdocstrings_config = mkdocstrings_config.get("mkdocstrings", {}) - elif isinstance(mkdocstrings_config, list): - for plugin in mkdocstrings_config: - if isinstance(plugin, dict) and "mkdocstrings" in plugin: - mkdocstrings_config = plugin["mkdocstrings"] - break - else: - mkdocstrings_config = _default_config - else: - mkdocstrings_config = _default_config + """Create the extension instance. - mdx, mdx_config = _split_configs(mkdocs_config.get("markdown_extensions", [])) + We only support this function being used by Zensical. + Consider this function private API. + """ + mdx, mdx_config = _split_configs(markdown_extensions or []) + tool_config = _ToolConfig(config_file_path=config_file_path) - handlers = Handlers( + handlers_instance = Handlers( theme="material", - default=mkdocstrings_config.get("default_handler", _default_config["default_handler"]), - inventory_project=mkdocs_config.get("site_name", "Project"), - handlers_config=mkdocstrings_config.get("handlers", _default_config["handlers"]), - custom_templates=mkdocstrings_config.get("custom_templates", _default_config["custom_templates"]), + 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=mkdocstrings_config.get("locale", _default_config["locale"]), - tool_config=mkdocs_config, + locale=locale or _default_config["locale"], + tool_config=tool_config, ) - handlers._download_inventories() + handlers_instance._download_inventories() autorefs = AutorefsPlugin() autorefs.config = AutorefsConfig() autorefs.scan_toc = False - return MkdocstringsExtension(handlers=handlers, autorefs=autorefs, **kwargs) + return MkdocstringsExtension(handlers=handlers_instance, autorefs=autorefs) From de34044a02b45250e215af0f969dca581dfb82c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 15:42:59 +0100 Subject: [PATCH 202/223] refactor: Remove deprecated code before v1 --- pyproject.toml | 5 +- src/mkdocstrings/_internal/extension.py | 33 +--- src/mkdocstrings/_internal/handlers/base.py | 174 ++------------------ src/mkdocstrings/_internal/loggers.py | 6 +- src/mkdocstrings/_internal/plugin.py | 5 - src/mkdocstrings/extension.py | 17 -- src/mkdocstrings/handlers/__init__.py | 3 - src/mkdocstrings/handlers/base.py | 17 -- src/mkdocstrings/handlers/rendering.py | 17 -- src/mkdocstrings/inventory.py | 17 -- src/mkdocstrings/loggers.py | 17 -- src/mkdocstrings/plugin.py | 17 -- tests/test_api.py | 5 - 13 files changed, 22 insertions(+), 311 deletions(-) delete mode 100644 src/mkdocstrings/extension.py delete mode 100644 src/mkdocstrings/handlers/__init__.py delete mode 100644 src/mkdocstrings/handlers/base.py delete mode 100644 src/mkdocstrings/handlers/rendering.py delete mode 100644 src/mkdocstrings/inventory.py delete mode 100644 src/mkdocstrings/loggers.py delete mode 100644 src/mkdocstrings/plugin.py diff --git a/pyproject.toml b/pyproject.toml index 14ba163b..2b3d6c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,15 +30,12 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - # YORE: Bump 1: Replace `2.11.1` with `3.1` within line. - "Jinja2>=2.11.1", + "Jinja2>=3.1", "Markdown>=3.6", "MarkupSafe>=1.1", "mkdocs>=1.6", "mkdocs-autorefs>=1.4", "pymdown-extensions>=6.3", - # YORE: EOL 3.9: Remove line. - "importlib-metadata>=4.6; python_version < '3.10'", ] [project.optional-dependencies] diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 8ccea142..3a6f4530 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -26,7 +26,6 @@ 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 @@ -172,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: @@ -266,23 +253,7 @@ def _process_headings(self, handler: BaseHandler, element: Element) -> None: # 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: diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index af68a45c..2eb9b3e6 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -6,10 +6,9 @@ import datetime import importlib -import inspect import ssl -import sys 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 @@ -33,12 +32,6 @@ from mkdocstrings._internal.inventory import Inventory from mkdocstrings._internal.loggers import get_logger, get_template_logger -# YORE: EOL 3.9: Replace block with line 4. -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 @@ -104,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.""" @@ -127,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. @@ -149,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 @@ -533,20 +463,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) @@ -561,17 +482,7 @@ def _update_env(self, md: Markdown, *, config: Any | None = None) -> None: 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: @@ -624,35 +535,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. @@ -696,34 +578,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: diff --git a/src/mkdocstrings/_internal/loggers.py b/src/mkdocstrings/_internal/loggers.py index 6c6304c3..c67a7f4e 100644 --- a/src/mkdocstrings/_internal/loggers.py +++ b/src/mkdocstrings/_internal/loggers.py @@ -7,11 +7,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Callable -# YORE: Bump 1: Replace block with line 2. -try: - from jinja2 import pass_context -except ImportError: - 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 diff --git a/src/mkdocstrings/_internal/plugin.py b/src/mkdocstrings/_internal/plugin.py index 2af14a7a..2c27a60a 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -19,7 +19,6 @@ 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 @@ -166,10 +165,6 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: 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] diff --git a/src/mkdocstrings/extension.py b/src/mkdocstrings/extension.py deleted file mode 100644 index c7943652..00000000 --- a/src/mkdocstrings/extension.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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 b684324a..00000000 --- a/src/mkdocstrings/handlers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py deleted file mode 100644 index c55a50ba..00000000 --- a/src/mkdocstrings/handlers/base.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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 f3f04eea..00000000 --- a/src/mkdocstrings/handlers/rendering.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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 7192acff..00000000 --- a/src/mkdocstrings/inventory.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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 25545ca5..00000000 --- a/src/mkdocstrings/loggers.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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 dbb6abf9..00000000 --- a/src/mkdocstrings/plugin.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Deprecated. Import from `mkdocstrings` directly.""" - -# YORE: Bump 1: Remove file. - -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/test_api.py b/tests/test_api.py index ea672073..9821e65c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -152,8 +152,6 @@ 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(): @@ -163,9 +161,6 @@ def test_inventory_matches_api( 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) From 68760a9ec55772c8b330b056c2d0896877324b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 16:39:22 +0100 Subject: [PATCH 203/223] chore: Prepare release 1.0.0 --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6346ccd3..9def4ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.0) - 2025-11-27 + +[Compare with 0.30.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.30.1...1.0.0) + +### Breaking Changes + +- `BaseHandler.name`: *Attribute value was changed*: `''` -> unset +- `BaseHandler.domain`: *Attribute value was changed*: `''` -> unset +- `BaseHandler.fallback_config`: *Public object was removed* +- `BaseHandler.__init__(args)`: *Parameter was removed* +- `BaseHandler.__init__(kwargs)`: *Parameter was removed* +- `BaseHandler.__init__(theme)`: *Parameter was added as required* +- `BaseHandler.__init__(custom_templates)`: *Parameter was added as required* +- `BaseHandler.__init__(mdx)`: *Parameter was added as required* +- `BaseHandler.__init__(mdx_config)`: *Parameter was added as required* +- `BaseHandler.update_env(args)`: *Parameter was removed* +- `BaseHandler.update_env(kwargs)`: *Parameter was removed* +- `BaseHandler.update_env(config)`: *Parameter was added as required* +- `Handlers.get_anchors`: *Public object was removed* (import from `mkdocstrings` directly) +- `mkdocstrings.plugin`: *Public module was removed* (import from `mkdocstrings` directly) +- `mkdocstrings.loggers`: *Public module was removed* (import from `mkdocstrings` directly) +- `mkdocstrings.inventory`: *Public module was removed* (import from `mkdocstrings` directly) +- `mkdocstrings.extension`: *Public module was removed* (import from `mkdocstrings` directly) +- `mkdocstrings.handlers`: *Public module was removed* (import from `mkdocstrings` directly) + +### Code Refactoring + +- Remove deprecated code before v1 ([de34044](https://github.com/mkdocstrings/mkdocstrings/commit/de34044a02b45250e215af0f969dca581dfb82c5) by Timothรฉe Mazzucotelli). +- Expect Zensical to pass extension configuration instead of loading it again from YAML ([6b73d5a](https://github.com/mkdocstrings/mkdocstrings/commit/6b73d5a2f455062ab6c68376c85adce6adc037a3) by Timothรฉe Mazzucotelli). +- Expose the Markdown extension, to make mkdocstrings compatible with Zensical ([6de2667](https://github.com/mkdocstrings/mkdocstrings/commit/6de266759b79eb72cddd300e6a0a8576085fae40) by Timothรฉe Mazzucotelli). + ## [0.30.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.30.1) - 2025-09-19 [Compare with 0.30.0](https://github.com/mkdocstrings/mkdocstrings/compare/0.30.0...0.30.1) From cc54d77f63ecb97c4bcc6fb1b447eddad6e842ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 27 Nov 2025 16:40:59 +0100 Subject: [PATCH 204/223] chore: Template upgrade --- .copier-answers.yml | 2 +- duties.py | 41 ++++++++++------------------------------- mkdocs.yml | 5 +---- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/.copier-answers.yml b/.copier-answers.yml index 6c5d650e..dafd61dc 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.11.1 +_commit: 1.11.2 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/duties.py b/duties.py index 04357dbb..653e2f0d 100644 --- a/duties.py +++ b/duties.py @@ -5,16 +5,12 @@ import os import re import sys -from contextlib import contextmanager -from importlib.metadata import version as pkgversion from pathlib import Path from typing import TYPE_CHECKING from duty import duty, tools if TYPE_CHECKING: - from collections.abc import Iterator - from duty.context import Context @@ -36,18 +32,6 @@ def pyprefix(title: str) -> str: return title -@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: @@ -84,11 +68,10 @@ 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.mkdocs.build(strict=True, verbose=True), + title=pyprefix("Building documentation"), + ) @duty(nofail=PY_VERSION == PY_DEV) @@ -120,22 +103,18 @@ 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.mkdocs.serve(dev_addr=f"{host}:{port}").add_args(*cli_args), + title="Serving documentation", + capture=False, + ) @duty def docs_deploy(ctx: Context) -> None: """Deploy the documentation to GitHub pages.""" 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!") - ctx.run(tools.mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation") + ctx.run(tools.mkdocs.gh_deploy(force=True), title="Deploying documentation") @duty diff --git a/mkdocs.yml b/mkdocs.yml index 8250973b..d2c5085e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -188,10 +188,7 @@ plugins: reference/plugin.md: reference/api.md#mkdocstrings.plugin - minify: minify_html: !ENV [DEPLOY, false] -- group: - enabled: !ENV [MATERIAL_INSIDERS, false] - plugins: - - typeset +- typeset extra: social: From afefc0fbccc09f7b6f0c92cf3446434d241f1658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sun, 30 Nov 2025 15:40:34 +0100 Subject: [PATCH 205/223] chore: Fix docs-deploy duty --- duties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/duties.py b/duties.py index 653e2f0d..0759f7a5 100644 --- a/duties.py +++ b/duties.py @@ -114,7 +114,7 @@ def docs(ctx: Context, *cli_args: str, host: str = "127.0.0.1", port: int = 8000 def docs_deploy(ctx: Context) -> None: """Deploy the documentation to GitHub pages.""" os.environ["DEPLOY"] = "true" - ctx.run(tools.mkdocs.gh_deploy(force=True), title="Deploying documentation") + ctx.run(tools.mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation") @duty From b6a33e23ba411159253ee44d1b1fa63b9a9a7b7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 01:02:44 +0000 Subject: [PATCH 206/223] chore: Update sponsors section in README --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a486ed40..e28c653b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,6 @@ See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for mo
    Silver sponsors

    -Material for MkDocs
    FastAPI
    Pydantic

    @@ -165,23 +164,18 @@ See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for mo machow BenHammersley trevorWieland -laenan8466 MarcoGorelli analog-cbarber OdinManiac rstudio-sponsorship schlich -SuperCowPowers butterlyn livingbio NemetschekAllplan EricJayHartman 15r10nk -cdwilson activeloopai roboflow -wrath-codes -leodevian cmclaughlin blaisep RapidataAI @@ -190,9 +184,10 @@ See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for mo blakeNaccarato ChargeStorm Alphadelta14 +Cusp-AI

    -*And 8 more private sponsor(s).* +*And 7 more private sponsor(s).* From f43f1ee2cd38a0dba64fc7d0db3c5ffb037bf7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 15 Jan 2026 15:06:16 +0100 Subject: [PATCH 207/223] refactor: Support cross-references in Zensical PR-812: https://github.com/mkdocstrings/mkdocstrings/pull/812 --- src/mkdocstrings/_internal/extension.py | 42 +++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 3a6f4530..da713a07 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -34,7 +34,7 @@ from markdown.extensions import Extension from markdown.treeprocessors import Treeprocessor from mkdocs.exceptions import PluginError -from mkdocs_autorefs import AutorefsConfig, AutorefsPlugin +from mkdocs_autorefs import AutorefsConfig, AutorefsExtension, AutorefsPlugin from mkdocstrings._internal.handlers.base import BaseHandler, CollectionError, CollectorItem, Handlers from mkdocstrings._internal.loggers import get_logger @@ -316,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. @@ -336,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"] # type: ignore[assignment] + md.parser.blockprocessors.register( AutoDocProcessor(md, handlers=self._handlers, autorefs=self._autorefs), "mkdocstrings", @@ -352,6 +367,9 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me 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 @@ -421,10 +439,22 @@ def makeExtension( # noqa: N802 tool_config=tool_config, ) - handlers_instance._download_inventories() - autorefs = AutorefsPlugin() autorefs.config = AutorefsConfig() - autorefs.scan_toc = False + 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 - return MkdocstringsExtension(handlers=handlers_instance, autorefs=autorefs) + handlers_instance._download_inventories() + register = autorefs.register_url + for identifier, url in handlers_instance._yield_inventory_items(): + register(identifier, url) + + return MkdocstringsExtension( + handlers=handlers_instance, + autorefs=autorefs, + autorefs_extension=True, + ) From 0edd18af00fa5907bb31e8309ecf5ad4309da552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 15 Jan 2026 15:06:54 +0100 Subject: [PATCH 208/223] chore: Clean up after v1 --- CHANGELOG.md | 5 +---- docs/usage/handlers.md | 2 +- mkdocs.yml | 7 ------- pyproject.toml | 2 +- src/mkdocstrings/_internal/handlers/base.py | 4 ++-- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9def4ba8..4fac770e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -952,8 +952,6 @@ See issue [#74](https://github.com/pawamoy/mkdocstrings/issues/74). ### Features - Prepare for new `pytkdocs` version ([336421a](https://github.com/pawamoy/mkdocstrings/commit/336421af95d752671276c2e88c5c173bff4093cc)). Add options `filters` and `members` to the Python collector to reflect the new `pytkdocs` options. - See [the default configuration of the Python collector](https://pawamoy.github.io/mkdocstrings/reference/handlers/python/#mkdocstrings.handlers.python.PythonCollector.DEFAULT_CONFIG). - ## [0.9.1](https://github.com/pawamoy/mkdocstrings/releases/tag/0.9.1) - 2020-03-21 @@ -979,8 +977,7 @@ No identified breaking changes for end-users. - **Better cross-references:** cross-references now not only work between documented objects (between all languages, given the objects' identifiers are unique), but also for every heading of your Markdown pages. - **Configuration options:** the rendering of Python documentation can now be configured, - (globally and locally thanks to the handlers system), - [check the docs!](https://pawamoy.github.io/mkdocstrings/reference/handlers/python/#mkdocstrings.handlers.python.PythonRenderer.DEFAULT_CONFIG) + (globally and locally thanks to the handlers system). Also see the [recommended CSS](https://pawamoy.github.io/mkdocstrings/handlers/python/#recommended-style). - **Proper logging messages:** `mkdocstrings` now logs debug, warning and error messages, useful when troubleshooting. diff --git a/docs/usage/handlers.md b/docs/usage/handlers.md index efdaccd1..6f326431 100644 --- a/docs/usage/handlers.md +++ b/docs/usage/handlers.md @@ -205,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/mkdocs.yml b/mkdocs.yml index d2c5085e..8423ce25 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -179,13 +179,6 @@ plugins: handlers/overview.md: usage/handlers.md reference/mkdocstrings.md: reference/api.md reference/index.md: reference/api.md#mkdocstrings - reference/extension.md: reference/api.md#mkdocstrings.extension - reference/handlers/index.md: reference/api.md#mkdocstrings.handlers - reference/handlers/base.md: reference/api.md#mkdocstrings.handlers.base - reference/handlers/rendering.md: reference/api.md#mkdocstrings.handlers.rendering - reference/inventory.md: reference/api.md#mkdocstrings.inventory - reference/loggers.md: reference/api.md#mkdocstrings.loggers - reference/plugin.md: reference/api.md#mkdocstrings.plugin - minify: minify_html: !ENV [DEPLOY, false] - typeset diff --git a/pyproject.toml b/pyproject.toml index 2b3d6c17..7f033477 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ ci = [ "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", "mkdocs-section-index>=0.3", - "mkdocstrings-python>=1.16.2", + "mkdocstrings-python>=2.0", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", ] diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 2eb9b3e6..05808ef9 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -241,7 +241,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. @@ -560,7 +560,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. From d37d9079e5381350b2e3ffc5f698e28a5b572d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 17 Jan 2026 19:20:23 +0100 Subject: [PATCH 209/223] refactor: Support manual cross-references in Zensical too --- src/mkdocstrings/_internal/extension.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index da713a07..b9174aec 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -387,7 +387,7 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me } -def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str], dict[str, Any]]: +def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str | Extension], dict[str, Any]]: # Split markdown extensions and their configs from mkdocs.yml mdx: list[str] = [] mdx_config: dict[str, Any] = {} @@ -399,7 +399,7 @@ def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str], di mdx.append(key) mdx_config[key] = value break # Only one item per dict - return mdx, mdx_config + return mdx, mdx_config # type: ignore[return-value] class _ToolConfig: @@ -426,6 +426,17 @@ def makeExtension( # noqa: N802 mdx, mdx_config = _split_configs(markdown_extensions or []) tool_config = _ToolConfig(config_file_path=config_file_path) + autorefs = AutorefsPlugin() + autorefs.config = AutorefsConfig() + 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 + + mdx.append(AutorefsExtension(autorefs)) + handlers_instance = Handlers( theme="material", default=default_handler or _default_config["default_handler"], @@ -439,15 +450,6 @@ def makeExtension( # noqa: N802 tool_config=tool_config, ) - autorefs = AutorefsPlugin() - autorefs.config = AutorefsConfig() - 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 - handlers_instance._download_inventories() register = autorefs.register_url for identifier, url in handlers_instance._yield_inventory_items(): From cb8a3c781674b8e23cf0048955d70358d7bab49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Mon, 19 Jan 2026 12:36:12 +0100 Subject: [PATCH 210/223] chore: Prepare release 1.0.1 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fac770e..5e8050e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.1) - 2026-01-19 + +[Compare with 1.0.0](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.0...1.0.1) + +### Code Refactoring + +- Support manual cross-references in Zensical too ([d37d907](https://github.com/mkdocstrings/mkdocstrings/commit/d37d9079e5381350b2e3ffc5f698e28a5b572d36) by Timothรฉe Mazzucotelli). +- Support cross-references in Zensical ([f43f1ee](https://github.com/mkdocstrings/mkdocstrings/commit/f43f1ee2cd38a0dba64fc7d0db3c5ffb037bf7f7) by Timothรฉe Mazzucotelli). [PR-812](https://github.com/mkdocstrings/mkdocstrings/pull/812) + ## [1.0.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.0) - 2025-11-27 [Compare with 0.30.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.30.1...1.0.0) From 9f79141d7eb35aba0c89a43795df0ee22a25a61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 24 Jan 2026 16:57:02 +0100 Subject: [PATCH 211/223] refactor: Use global instances for handlers and autorefs This allows to reduce redudant computations while running through Zensical. For example, we won't load (read from the disk) inventories on each page rendering, only once per build. --- src/mkdocstrings/_internal/extension.py | 93 ++++++++++++++++--------- 1 file changed, 61 insertions(+), 32 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index b9174aec..d20e57ac 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -407,6 +407,10 @@ 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, @@ -423,40 +427,65 @@ def makeExtension( # noqa: N802 We only support this function being used by Zensical. Consider this function private API. """ - mdx, mdx_config = _split_configs(markdown_extensions or []) - tool_config = _ToolConfig(config_file_path=config_file_path) - - autorefs = AutorefsPlugin() - autorefs.config = AutorefsConfig() - 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 - - mdx.append(AutorefsExtension(autorefs)) - - handlers_instance = 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, - ) + global _AUTOREFS # noqa: PLW0603 + if _AUTOREFS is None: + _AUTOREFS = AutorefsPlugin() + _AUTOREFS.config = AutorefsConfig() + _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_instance._download_inventories() - register = autorefs.register_url - for identifier, url in handlers_instance._yield_inventory_items(): - register(identifier, url) + _HANDLERS._download_inventories() + register = _AUTOREFS.register_url + for identifier, url in _HANDLERS._yield_inventory_items(): + register(identifier, url) return MkdocstringsExtension( - handlers=handlers_instance, - autorefs=autorefs, + 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"" From 4e66617fbe26636f86117c26b2482c21892166fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 24 Jan 2026 16:57:13 +0100 Subject: [PATCH 212/223] chore: Prepare release 1.0.2 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8050e6..d1c96386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.2) - 2026-01-24 + +[Compare with 1.0.1](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.1...1.0.2) + +### Code Refactoring + +- Use global instances for handlers and autorefs ([9f79141](https://github.com/mkdocstrings/mkdocstrings/commit/9f79141d7eb35aba0c89a43795df0ee22a25a61e) by Timothรฉe Mazzucotelli). + ## [1.0.1](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.1) - 2026-01-19 [Compare with 1.0.0](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.0...1.0.1) From dbf263dfdd2fdd769d66fa62bdd388e05988bc78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 5 Feb 2026 18:34:27 +0100 Subject: [PATCH 213/223] fix: Propagate Zensical's `zrelpath` processor --- src/mkdocstrings/_internal/handlers/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 05808ef9..4e0e7dfb 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -477,6 +477,10 @@ def _update_env(self, md: Markdown, *, config: Any | None = None) -> None: relpath = md.treeprocessors["relpath"] new_relpath = type(relpath)(relpath.file, relpath.files, relpath.config) # type: ignore[attr-defined,call-arg] 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) # type: ignore[attr-defined,call-arg] + new_md.treeprocessors.register(new_zrelpath, "zrelpath", priority=0) self._md = new_md From cc3d6a4d286668a9a249ac38dfaf8b45470f3314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 5 Feb 2026 18:37:33 +0100 Subject: [PATCH 214/223] chore: Template upgrade --- .copier-answers.yml | 2 +- .github/workflows/ci.yml | 6 +++++- .gitignore | 1 - config/mypy.ini | 5 ----- config/ruff.toml | 25 +++---------------------- config/vscode/settings.json | 3 --- duties.py | 9 ++++----- pyproject.toml | 4 ++-- scripts/gen_credits.py | 23 ++++++++++++----------- scripts/get_version.py | 2 +- scripts/make.py | 14 +++++++------- src/mkdocstrings/_internal/debug.py | 2 +- tests/test_api.py | 6 +++--- 13 files changed, 39 insertions(+), 63 deletions(-) delete mode 100644 config/mypy.ini diff --git a/.copier-answers.yml b/.copier-answers.yml index dafd61dc..3c48bac1 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.11.2 +_commit: 1.11.6 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54dd741c..a8d0a185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,8 +44,11 @@ jobs: python-version: "3.12" - os: ubuntu-latest python-version: "3.13" + - os: ubuntu-latest + python-version: "3.15-dev" runs-on: ${{ matrix.os }} + continue-on-error: true steps: - name: Checkout @@ -103,7 +106,7 @@ jobs: - "3.12" - "3.13" - "3.14" - - "3.15" + - "3.15-dev" resolution: - highest - lowest-direct @@ -112,6 +115,7 @@ jobs: resolution: lowest-direct - os: windows-latest resolution: lowest-direct + runs-on: ${{ matrix.os }} continue-on-error: true diff --git a/.gitignore b/.gitignore index 9fea0472..faeb06ae 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,5 @@ uv.lock # cache .cache/ .pytest_cache/ -.mypy_cache/ .ruff_cache/ __pycache__/ diff --git a/config/mypy.ini b/config/mypy.ini deleted file mode 100644 index 814e2ac8..00000000 --- a/config/mypy.ini +++ /dev/null @@ -1,5 +0,0 @@ -[mypy] -ignore_missing_imports = true -exclude = tests/fixtures/ -warn_unused_ignores = true -show_error_codes = true diff --git a/config/ruff.toml b/config/ruff.toml index 65416253..db162aba 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -5,30 +5,9 @@ line-length = 120 exclude = [ "tests/fixtures/*.py", ] -select = [ - "A", "ANN", "ARG", - "B", "BLE", - "C", "C4", - "COM", - "D", "DTZ", - "E", "ERA", "EXE", - "F", "FBT", - "G", - "I", "ICN", "INP", "ISC", - "N", - "PGH", "PIE", "PL", "PLC", "PLE", "PLR", "PLW", "PT", "PYI", - "Q", - "RUF", "RSE", "RET", - "S", "SIM", "SLF", - "T", "T10", "T20", "TCH", "TID", "TRY", - "UP", - "W", - "YTT", -] +select = ["ALL"] ignore = [ "A001", # Variable is shadowing a Python builtin - "ANN101", # Missing type annotation for self - "ANN102", # Missing type annotation for cls "ANN204", # Missing return type annotation for special method __str__ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ARG005", # Unused lambda argument @@ -36,6 +15,8 @@ ignore = [ "D105", # Missing docstring in magic method "D417", # Missing argument description in the docstring "E501", # Line too long + "EM101", # String literal when raising exception + "EM102", # f-string when raising exception "ERA001", # Commented out code "G004", # Logging statement uses f-string "PLR0911", # Too many return statements diff --git a/config/vscode/settings.json b/config/vscode/settings.json index 949856d1..87ecd639 100644 --- a/config/vscode/settings.json +++ b/config/vscode/settings.json @@ -4,9 +4,6 @@ "**/.venvs*/**": true, "**/venv*/**": true }, - "mypy-type-checker.args": [ - "--config-file=config/mypy.ini" - ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.pytestArgs": [ diff --git a/duties.py b/duties.py index 0759f7a5..3f09916a 100644 --- a/duties.py +++ b/duties.py @@ -22,7 +22,7 @@ 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 = "314" +PY_DEV = "315" def pyprefix(title: str) -> str: @@ -35,7 +35,7 @@ def pyprefix(title: str) -> str: 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] @duty @@ -77,10 +77,9 @@ def check_docs(ctx: Context) -> None: @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, color=True, error_on_warning=True, python_version=py), title=pyprefix("Type-checking"), ) diff --git a/pyproject.toml b/pyproject.toml index 7f033477..f661a98b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ getter = "scripts.get_version:get_version" [tool.pdm.build] # Include as much as possible in the source distribution, to help redistributors. -excludes = ["**/.pytest_cache", "**/.mypy_cache"] +excludes = ["**/.pytest_cache"] source-includes = [ "config", "docs", @@ -98,7 +98,7 @@ ci = [ "pytest-cov>=5.0", "pytest-randomly>=3.15", "pytest-xdist>=3.6", - "mypy>=1.10", + "ty>=0.0.14", "types-markdown>=3.6", "types-pyyaml>=6.0", ] diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py index b5499b7a..8e8a7bf8 100644 --- a/scripts/gen_credits.py +++ b/scripts/gen_credits.py @@ -47,7 +47,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 +63,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 +77,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 +91,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 +110,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 +122,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 3c425a73..d56f5858 100644 --- a/scripts/get_version.py +++ b/scripts/get_version.py @@ -22,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/make.py b/scripts/make.py index b741a366..7fa7b56d 100755 --- a/scripts/make.py +++ b/scripts/make.py @@ -117,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) @@ -151,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 @@ -163,28 +163,28 @@ def main() -> int: if not args: print("make: run: missing command", file=sys.stderr) return 1 - run("default", *args) # ty: ignore[missing-argument] + run("default", *args) return 0 if cmd == "multirun": if not args: print("make: run: missing command", file=sys.stderr) return 1 - multirun(*args) # ty: ignore[missing-argument] + multirun(*args) return 0 if cmd == "allrun": if not args: print("make: run: missing command", file=sys.stderr) return 1 - allrun(*args) # ty: ignore[missing-argument] + allrun(*args) return 0 if cmd.startswith("3."): if not args: print("make: run: missing command", file=sys.stderr) return 1 - run(cmd, *args) # ty: ignore[missing-argument] + run(cmd, *args) return 0 opts = [] diff --git a/src/mkdocstrings/_internal/debug.py b/src/mkdocstrings/_internal/debug.py index f6b11600..7b56409b 100644 --- a/src/mkdocstrings/_internal/debug.py +++ b/src/mkdocstrings/_internal/debug.py @@ -85,7 +85,7 @@ def _get_debug_info() -> _Environment: interpreter_version=py_version, interpreter_path=sys.executable, platform=platform.platform(), - variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], # ty: ignore[invalid-argument-type] + variables=[_Variable(var, val) for var in variables if (val := os.getenv(var))], packages=[_Package(pkg, _get_version(pkg)) for pkg in packages], ) diff --git a/tests/test_api.py b/tests/test_api.py index 9821e65c..0d714f0a 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(): - pytest.skip("The objects inventory is not available.") # ty: ignore[call-non-callable] + pytest.skip("The objects inventory is not available.") with inventory_file.open("rb") as file: return Inventory.parse_sphinx(file) From 1624e2c52fbdff2ee611142edbf52454427f8f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 5 Feb 2026 18:59:57 +0100 Subject: [PATCH 215/223] ci: Update lint/type-checking --- config/ruff.toml | 3 ++ config/ty.toml | 2 ++ duties.py | 8 ++++- src/mkdocstrings/_internal/download.py | 13 ++++--- src/mkdocstrings/_internal/extension.py | 10 +++--- src/mkdocstrings/_internal/handlers/base.py | 34 +++++++++---------- .../_internal/handlers/rendering.py | 6 ++-- src/mkdocstrings/_internal/loggers.py | 2 +- src/mkdocstrings/_internal/plugin.py | 18 +++++----- tests/conftest.py | 4 +-- tests/test_extension.py | 8 ++--- tests/test_handlers.py | 6 ++-- tests/test_inventory.py | 3 +- 13 files changed, 66 insertions(+), 51 deletions(-) create mode 100644 config/ty.toml diff --git a/config/ruff.toml b/config/ruff.toml index db162aba..95d92083 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -18,6 +18,7 @@ ignore = [ "EM101", # String literal when raising exception "EM102", # f-string when raising exception "ERA001", # Commented out code + "FIX", # TODO, FIXME, etc. "G004", # Logging statement uses f-string "PLR0911", # Too many return statements "PLR0912", # Too many branches @@ -25,6 +26,8 @@ ignore = [ "PLR0915", # Too many statements "SLF001", # Private member accessed "S704", # Unsafe use of `markupsafe.Markup` + "TD002", # Missing author in TODO + "TD003", # Missing issue link for TODO "TRY003", # Avoid specifying long messages outside the exception class ] diff --git a/config/ty.toml b/config/ty.toml new file mode 100644 index 00000000..545856a3 --- /dev/null +++ b/config/ty.toml @@ -0,0 +1,2 @@ +[src] +exclude = ["tests/fixtures"] diff --git a/duties.py b/duties.py index 3f09916a..974ed169 100644 --- a/duties.py +++ b/duties.py @@ -79,7 +79,13 @@ def check_types(ctx: Context) -> None: """Check that the code is correctly typed.""" py = f"{sys.version_info.major}.{sys.version_info.minor}" ctx.run( - tools.ty.check(*PY_SRC_LIST, color=True, error_on_warning=True, python_version=py), + tools.ty.check( + *PY_SRC_LIST, + config_file="config/ty.toml", + color=True, + error_on_warning=True, + python_version=py, + ), title=pyprefix("Type-checking"), ) diff --git a/src/mkdocstrings/_internal/download.py b/src/mkdocstrings/_internal/download.py index ffe25e6b..2d8b25f1 100644 --- a/src/mkdocstrings/_internal/download.py +++ b/src/mkdocstrings/_internal/download.py @@ -1,14 +1,19 @@ +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 +if TYPE_CHECKING: + from collections.abc import Mapping + + _logger = get_logger("mkdocstrings") # Regex pattern for an environment variable in the form ${ENV_VAR}. @@ -25,11 +30,11 @@ def _download_url_with_gz(url: str) -> bytes: with urllib.request.urlopen(req) 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 d20e57ac..8ba43aba 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -189,7 +189,7 @@ def _process_block( if "locale" in signature(handler.render).parameters: render = partial(handler.render, locale=self._handlers._locale) else: - render = handler.render # type: ignore[assignment] + render = handler.render try: rendered = render(data, options) except TemplateNotFound as exc: @@ -301,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: @@ -349,7 +349,7 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me # 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"] # type: ignore[assignment] + self._autorefs.current_page = md.preprocessors["zensical_current_page"] md.parser.blockprocessors.register( AutoDocProcessor(md, handlers=self._handlers, autorefs=self._autorefs), @@ -389,7 +389,7 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str | Extension], dict[str, Any]]: # Split markdown extensions and their configs from mkdocs.yml - mdx: list[str] = [] + mdx: list[str | Extension] = [] mdx_config: dict[str, Any] = {} for item in markdown_extensions: if isinstance(item, str): @@ -399,7 +399,7 @@ def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str | Ex mdx.append(key) mdx_config[key] = value break # Only one item per dict - return mdx, mdx_config # type: ignore[return-value] + return mdx, mdx_config class _ToolConfig: diff --git a/src/mkdocstrings/_internal/handlers/base.py b/src/mkdocstrings/_internal/handlers/base.py index 4e0e7dfb..41d89da9 100644 --- a/src/mkdocstrings/_internal/handlers/base.py +++ b/src/mkdocstrings/_internal/handlers/base.py @@ -150,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" @@ -361,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] + 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 # type: ignore[attr-defined] + 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] + treeprocessors[BacklinksTreeProcessor.name].initial_id = None if AutorefsInlineProcessor.name in self.md.inlinePatterns: - self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None # type: ignore[attr-defined] + self.md.inlinePatterns[AutorefsInlineProcessor.name].hook = None self.md.reset() _markdown_conversion_layer -= 1 @@ -475,11 +473,11 @@ 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) # type: ignore[attr-defined,call-arg] + 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 @@ -603,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. " @@ -645,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/loggers.py b/src/mkdocstrings/_internal/loggers.py index c67a7f4e..29f620a3 100644 --- a/src/mkdocstrings/_internal/loggers.py +++ b/src/mkdocstrings/_internal/loggers.py @@ -82,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 2c27a60a..2e7a8d39 100644 --- a/src/mkdocstrings/_internal/plugin.py +++ b/src/mkdocstrings/_internal/plugin.py @@ -140,7 +140,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: 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, @@ -156,7 +156,7 @@ 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`. @@ -167,7 +167,7 @@ def on_config(self, config: MkDocsConfig) -> MkDocsConfig | None: _logger.debug("Added a subdued autorefs instance %r", autorefs) 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. @@ -199,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[possibly-missing-attribute] for identifier, url in self._handlers._yield_inventory_items(): register(identifier, url) @@ -207,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 @@ -226,12 +226,12 @@ 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: @@ -240,7 +240,7 @@ def repl(match: Match) -> str: if "locale" in signature(handler.render_backlinks).parameters: render_backlinks = partial(handler.render_backlinks, locale=self.handlers._locale) else: - render_backlinks = handler.render_backlinks # type: ignore[assignment] + render_backlinks = handler.render_backlinks return render_backlinks(backlinks) diff --git a/tests/conftest.py b/tests/conftest.py index 8a132e29..0c9a89ad 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] conf.load_dict(conf_dict) assert conf.validate() == ([], []) diff --git a/tests/test_extension.py b/tests/test_extension.py index c283f9c0..6ba8adc1 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -99,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", @@ -154,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[possibly-missing-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" diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 30bdbdfc..fd9eb714 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[possibly-missing-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 ab61e599..858ac340 100644 --- a/tests/test_inventory.py +++ b/tests/test_inventory.py @@ -4,6 +4,7 @@ from io import BytesIO from os.path import join +from pathlib import Path import pytest from mkdocs.commands.build import build @@ -48,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) From 65b27ec8d1d671eddf021e48b0114cc3f8aca14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 5 Feb 2026 19:02:18 +0100 Subject: [PATCH 216/223] fix: Forward extension instances directly passed from Zensical --- src/mkdocstrings/_internal/extension.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/mkdocstrings/_internal/extension.py b/src/mkdocstrings/_internal/extension.py index 8ba43aba..4439d400 100644 --- a/src/mkdocstrings/_internal/extension.py +++ b/src/mkdocstrings/_internal/extension.py @@ -387,12 +387,14 @@ def extendMarkdown(self, md: Markdown) -> None: # noqa: N802 (casing: parent me } -def _split_configs(markdown_extensions: list[str | dict]) -> tuple[list[str | Extension], dict[str, Any]]: +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, Any] = {} + mdx_config: dict[str, dict[str, Any]] = {} for item in markdown_extensions: - if isinstance(item, str): + if isinstance(item, (str, Extension)): mdx.append(item) elif isinstance(item, dict): for key, value in item.items(): @@ -418,7 +420,7 @@ def makeExtension( # noqa: N802 inventory_version: str | None = None, handlers: dict[str, dict] | None = None, custom_templates: str | None = None, - markdown_extensions: list[str | dict] | None = None, + markdown_extensions: list[str | dict | Extension] | None = None, locale: str | None = None, config_file_path: str | None = None, ) -> MkdocstringsExtension: From 8bdff16af916b7285eae5a07eb085c21754be3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 7 Feb 2026 15:31:28 +0100 Subject: [PATCH 217/223] chore: Prepare release 1.0.3 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c96386..53065d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.0.3](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.3) - 2026-02-07 + +[Compare with 1.0.2](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.2...1.0.3) + +### Bug Fixes + +- Forward extension instances directly passed from Zensical ([65b27ec](https://github.com/mkdocstrings/mkdocstrings/commit/65b27ec8d1d671eddf021e48b0114cc3f8aca14a) by Timothรฉe Mazzucotelli). +- Propagate Zensical's `zrelpath` processor ([dbf263d](https://github.com/mkdocstrings/mkdocstrings/commit/dbf263dfdd2fdd769d66fa62bdd388e05988bc78) by Timothรฉe Mazzucotelli). + ## [1.0.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/1.0.2) - 2026-01-24 [Compare with 1.0.1](https://github.com/mkdocstrings/mkdocstrings/compare/1.0.1...1.0.2) From e500a2b416656ede76aac316304415e428dd0aa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Feb 2026 01:27:52 +0000 Subject: [PATCH 218/223] chore: Update sponsors section in README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e28c653b..3bfb8316 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,6 @@ See the [Usage](https://mkdocstrings.github.io/usage) section of the docs for mo
    Silver sponsors

    FastAPI
    -Pydantic

    Bronze sponsors

    From a0c47b9992416cf02b8dfc7a76a6c5503e98cd9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 11 Mar 2026 12:11:40 +0100 Subject: [PATCH 219/223] docs: Fix broken link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bfb8316..052690ba 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Come have a chat or ask questions on our [Gitter channel](https://gitter.im/mkdo - [**Language-agnostic:**](https://mkdocstrings.github.io/handlers/overview/) just like *MkDocs*, *mkdocstrings* is written in Python but is language-agnostic. It means you can use it with any programming language, as long as there is a - [**handler**](https://mkdocstrings.github.io/reference/handlers/base/) for it. + [**handler**](https://mkdocstrings.github.io/reference/api/#mkdocstrings.BaseHandler) for it. We currently have [handlers](https://mkdocstrings.github.io/handlers/overview/) for the [C](https://mkdocstrings.github.io/c/), [Crystal](https://mkdocstrings.github.io/crystal/), From 3d1969a279ea396792c682810d029503e48d8fcd Mon Sep 17 00:00:00 2001 From: Simon Lloyd Date: Wed, 15 Apr 2026 10:06:10 +0100 Subject: [PATCH 220/223] fix: Add timeout when downloading inventories (10 seconds) Issue-819: https://github.com/mkdocstrings/mkdocstrings/issues/819 --- src/mkdocstrings/_internal/download.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mkdocstrings/_internal/download.py b/src/mkdocstrings/_internal/download.py index 2d8b25f1..52bf42f5 100644 --- a/src/mkdocstrings/_internal/download.py +++ b/src/mkdocstrings/_internal/download.py @@ -19,6 +19,9 @@ # 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) @@ -27,7 +30,7 @@ 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) # ty: ignore[invalid-assignment] From 5f82a5822c50dc928ee4a133c1445543d3afa393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Wed, 15 Apr 2026 11:12:01 +0200 Subject: [PATCH 221/223] chore: Template upgrade --- .copier-answers.yml | 2 +- CONTRIBUTING.md | 5 +- LICENSE | 2 +- README.md | 2 +- config/ruff.toml | 3 +- config/ty.toml | 4 + config/vscode/launch.json | 3 +- config/vscode/settings.json | 1 + docs/.overrides/partials/comments.html | 2 +- docs/.overrides/partials/path-item.html | 22 --- docs/css/apidocs.css | 21 +++ docs/css/material.css | 4 - docs/css/mkdocstrings.css | 82 --------- duties.py | 34 ++-- mkdocs.yml | 209 ----------------------- pyproject.toml | 18 +- scripts/gen_credits.py | 6 +- tests/test_api.py | 4 +- zensical.toml | 214 ++++++++++++++++++++++++ 19 files changed, 283 insertions(+), 355 deletions(-) delete mode 100644 docs/.overrides/partials/path-item.html create mode 100644 docs/css/apidocs.css delete mode 100644 docs/css/material.css delete mode 100644 docs/css/mkdocstrings.css delete mode 100644 mkdocs.yml create mode 100644 zensical.toml diff --git a/.copier-answers.yml b/.copier-answers.yml index 3c48bac1..b709d322 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier. -_commit: 1.11.6 +_commit: 1.11.15 _src_path: gh:pawamoy/copier-uv author_email: dev@pawamoy.fr author_fullname: Timothรฉe Mazzucotelli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78db50f2..82526a81 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,8 @@ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. +**Please always create an issue before working on a new feature or a bug fix, so that we can discuss the implementation and make sure that your work will be merged.** + ## Environment setup Nothing easier! @@ -38,8 +40,7 @@ Run `make help` to see all the available actions! ## Tasks -The entry-point to run commands and tasks is the `make` Python script, located in the `scripts` directory. Try running `make` to show the available commands and tasks. The *commands* do not need the Python dependencies to be installed, -while the *tasks* do. The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). +The entry-point to run commands and tasks is the `make` Python script, located in the `scripts` directory. Try running `make` to show the available commands and tasks. The *commands* do not need the Python dependencies to be installed, while the *tasks* do. The cross-platform tasks are written in Python, thanks to [duty](https://github.com/pawamoy/duty). If you work in VSCode, we provide [an action to configure VSCode](https://pawamoy.github.io/copier-uv/work/#vscode-setup) for the project. diff --git a/LICENSE b/LICENSE index aa2449ff..6270ba3e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ ISC License -Copyright (c) 2019, Timothรฉe Mazzucotelli +Copyright (c) 2019, Timothรฉe Mazzucotelli and contributors Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/README.md b/README.md index 052690ba..8ddfc16b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mkdocstrings [![ci](https://github.com/mkdocstrings/mkdocstrings/workflows/ci/badge.svg)](https://github.com/mkdocstrings/mkdocstrings/actions?query=workflow%3Aci) -[![documentation](https://img.shields.io/badge/docs-mkdocs-708FCC.svg?style=flat)](https://mkdocstrings.github.io/) +[![documentation](https://img.shields.io/badge/docs-zensical-FF9100.svg?style=flat)](https://mkdocstrings.github.io/mkdocstrings/) [![pypi version](https://img.shields.io/pypi/v/mkdocstrings.svg)](https://pypi.org/project/mkdocstrings/) [![conda version](https://img.shields.io/conda/vn/conda-forge/mkdocstrings)](https://anaconda.org/conda-forge/mkdocstrings) [![gitter](https://img.shields.io/badge/matrix-chat-4DB798.svg?style=flat)](https://app.gitter.im/#/room/#mkdocstrings:gitter.im) diff --git a/config/ruff.toml b/config/ruff.toml index 95d92083..6cb05666 100644 --- a/config/ruff.toml +++ b/config/ruff.toml @@ -1,5 +1,6 @@ -target-version = "py39" +target-version = "py310" line-length = 120 +output-format = "concise" [lint] exclude = [ diff --git a/config/ty.toml b/config/ty.toml index 545856a3..97724fa8 100644 --- a/config/ty.toml +++ b/config/ty.toml @@ -1,2 +1,6 @@ [src] exclude = ["tests/fixtures"] + +[terminal] +error-on-warning = true +output-format = "concise" diff --git a/config/vscode/launch.json b/config/vscode/launch.json index 5f3742be..6571bd99 100644 --- a/config/vscode/launch.json +++ b/config/vscode/launch.json @@ -23,11 +23,10 @@ "name": "docs", "type": "debugpy", "request": "launch", - "module": "mkdocs", + "module": "zensical", "justMyCode": false, "args": [ "serve", - "-v" ] }, { diff --git a/config/vscode/settings.json b/config/vscode/settings.json index 87ecd639..51587578 100644 --- a/config/vscode/settings.json +++ b/config/vscode/settings.json @@ -16,6 +16,7 @@ "ruff.lint.args": [ "--config=config/ruff.toml" ], + "ty.configurationFile": "config/ty.toml", "yaml.schemas": { "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" }, diff --git a/docs/.overrides/partials/comments.html b/docs/.overrides/partials/comments.html index cc341ba9..793b075c 100644 --- a/docs/.overrides/partials/comments.html +++ b/docs/.overrides/partials/comments.html @@ -1,5 +1,5 @@ - +