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) 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/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 diff --git a/pyproject.toml b/pyproject.toml index c3087f61..29aff0d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,14 +31,15 @@ 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'", - "typing-extensions>=4.1; python_version < '3.10'", ] [project.optional-dependencies] 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/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 7784f007..3d5852c5 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,7 @@ 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: @@ -43,9 +42,10 @@ from collections.abc import Iterable, Iterator, Mapping, Sequence from markdown import Extension + from markdown.extensions.toc import TocTreeprocessor from mkdocs_autorefs import AutorefsHookInterface, Backlink -_logger = get_logger(__name__) +_logger = get_logger("mkdocstrings") CollectorItem = Any """The type of the item returned by the `collect` method of a handler.""" @@ -493,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/src/mkdocstrings/_internal/inventory.py b/src/mkdocstrings/_internal/inventory.py index 471e3633..241bbb12 100644 --- a/src/mkdocstrings/_internal/inventory.py +++ b/src/mkdocstrings/_internal/inventory.py @@ -8,7 +8,7 @@ import re import zlib from textwrap import dedent -from typing import TYPE_CHECKING, BinaryIO +from typing import TYPE_CHECKING, BinaryIO, Literal, overload if TYPE_CHECKING: from collections.abc import Collection @@ -66,11 +66,21 @@ def format_sphinx(self) -> str: sphinx_item_regex = re.compile(r"^(.+?)\s+(\S+):(\S+)\s+(-?\d+)\s+(\S+)\s*(.*)$") """Regex to parse a Sphinx v2 inventory line.""" + @overload @classmethod - def parse_sphinx(cls, line: str) -> InventoryItem: + def parse_sphinx(cls, line: str, *, return_none: Literal[False]) -> InventoryItem: ... + + @overload + @classmethod + def parse_sphinx(cls, line: str, *, return_none: Literal[True]) -> InventoryItem | None: ... + + @classmethod + def parse_sphinx(cls, line: str, *, return_none: bool = False) -> InventoryItem | None: """Parse a line from a Sphinx v2 inventory file and return an `InventoryItem` from it.""" match = cls.sphinx_item_regex.search(line) if not match: + if return_none: + return None raise ValueError(line) name, domain, role, priority, uri, dispname = match.groups() if uri.endswith("$"): @@ -167,7 +177,9 @@ def parse_sphinx(cls, in_file: BinaryIO, *, domain_filter: Collection[str] = ()) for _ in range(4): in_file.readline() lines = zlib.decompress(in_file.read()).splitlines() - items = [InventoryItem.parse_sphinx(line.decode("utf8")) for line in lines] + items: list[InventoryItem] = [ + item for line in lines if (item := InventoryItem.parse_sphinx(line.decode("utf8"), return_none=True)) + ] if domain_filter: items = [item for item in items if item.domain in domain_filter] return cls(items) diff --git a/src/mkdocstrings/_internal/loggers.py b/src/mkdocstrings/_internal/loggers.py index d56d09c3..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/_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): 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 diff --git a/tests/test_extension.py b/tests/test_extension.py index b7c1c742..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 @@ -60,7 +59,6 @@ def test_reference_inside_autodoc(ext_markdown: Markdown) -> None: assert re.search(r"Link to <.*something\.Else.*>something\.Else<.*>\.", output) -@pytest.mark.skipif(sys.version_info < (3, 8), reason="typing.Literal requires Python 3.8") def test_quote_inside_annotation(ext_markdown: Markdown) -> None: """Assert that inline highlighting doesn't double-escape HTML.""" output = ext_markdown.convert("::: tests.fixtures.string_annotation.Foo") diff --git a/tests/test_inventory.py b/tests/test_inventory.py index eb008661..ab61e599 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 @@ -12,8 +11,6 @@ from mkdocstrings import Inventory, InventoryItem -sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") - @pytest.mark.parametrize( "our_inv", @@ -22,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) @@ -36,9 +36,10 @@ def test_sphinx_load_inventory_file(our_inv: Inventory) -> None: assert item.name in sphinx_inv[f"{item.domain}:{item.role}"] -@pytest.mark.skipif(sys.version_info < (3, 7), reason="using plugins that require Python 3.7") def test_sphinx_load_mkdocstrings_inventory_file() -> None: """Perform the 'live' inventory load test on mkdocstrings own inventory.""" + sphinx = pytest.importorskip("sphinx.util.inventory", reason="Sphinx is not installed") + mkdocs_config = load_config() mkdocs_config["plugins"].run_event("startup", command="build", dirty=False) try: @@ -55,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]