mirror of
https://github.com/zensical/zensical.git
synced 2026-10-08 05:41:24 +00:00
feature: support mkdocstrings' enable_inventory setting
Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
8 files changed
+445
-26
No files matched your search
+130
@@ -26,9 +26,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
from io import BytesIO
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from mkdocstrings import Inventory
|
||||
from mkdocstrings_handlers.python import PythonHandler
|
||||
from yaml import safe_dump
|
||||
|
||||
import zensical
|
||||
@@ -107,11 +110,13 @@ def test_autorefs_without_backlinks_does_not_load_handlers(
|
||||
|
||||
@pytest.mark.parametrize("backlinks", [None, False, "flat", "tree"])
|
||||
@pytest.mark.parametrize("blog", [False, True])
|
||||
@pytest.mark.parametrize("enable_inventory", [False, True, None])
|
||||
def test_backlinks_across_cold_cached_and_changed_builds(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
backlinks: str | bool | None,
|
||||
blog: bool,
|
||||
enable_inventory: bool | None,
|
||||
) -> None:
|
||||
"""Backlinks are opt-in and refresh when only a referring page changes."""
|
||||
pytest.importorskip("mkdocstrings_handlers.python")
|
||||
@@ -159,6 +164,7 @@ def test_backlinks_across_cold_cached_and_changed_builds(
|
||||
handler_options["backlinks"] = backlinks
|
||||
plugins: dict[str, dict[str, Any]] = {
|
||||
"mkdocstrings": {
|
||||
"enable_inventory": enable_inventory,
|
||||
"handlers": {
|
||||
"python": {"paths": ["."], "options": handler_options}
|
||||
},
|
||||
@@ -197,6 +203,8 @@ def test_backlinks_across_cold_cached_and_changed_builds(
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
|
||||
api = tmp_path / "site" / "api" / "index.html"
|
||||
inventory = tmp_path / "site" / "objects.inv"
|
||||
assert inventory.exists() is (enable_inventory is not False)
|
||||
first = api.read_text(encoding="utf-8")
|
||||
assert "<backlinks" not in first
|
||||
assert "<autoref" not in first
|
||||
@@ -215,6 +223,7 @@ def test_backlinks_across_cold_cached_and_changed_builds(
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
|
||||
assert api.read_text(encoding="utf-8") == first
|
||||
assert inventory.exists() is (enable_inventory is not False)
|
||||
|
||||
# Keep the API page cached while changing the backlink title and anchor.
|
||||
guide.write_text(
|
||||
@@ -224,6 +233,7 @@ def test_backlinks_across_cold_cached_and_changed_builds(
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
|
||||
updated = api.read_text(encoding="utf-8")
|
||||
assert inventory.exists() is (enable_inventory is not False)
|
||||
if backlinks:
|
||||
assert f"{guide_url}#updated" in updated
|
||||
assert f"{guide_url}#example" not in updated
|
||||
@@ -311,3 +321,123 @@ def test_backlinks_inside_autoref_titles(
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
|
||||
assert api.read_text(encoding="utf-8") == output
|
||||
|
||||
|
||||
def _write_project(root: Path, options: dict[str, Any]) -> Path:
|
||||
docs = root / "docs"
|
||||
docs.mkdir(exist_ok=True)
|
||||
if not (docs / "index.md").exists():
|
||||
(docs / "index.md").write_text(
|
||||
"# API\n\n::: sample_api.greet\n\n"
|
||||
"[Greeting function][sample_api.greet]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(docs / "other.md").write_text("# Other\n", encoding="utf-8")
|
||||
(root / "sample_api.py").write_text(
|
||||
"def greet(name: str) -> str:\n"
|
||||
' """Return a greeting."""\n'
|
||||
' return f"Hello {name}"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = root / "mkdocs.yml"
|
||||
config.write_text(
|
||||
safe_dump(
|
||||
{
|
||||
"site_name": "Inventory",
|
||||
"plugins": {
|
||||
"mkdocstrings": {
|
||||
"handlers": {"python": {"paths": [str(root)]}},
|
||||
**options,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("setting", [True, False, None])
|
||||
def test_inventory_setting_keeps_api_rendering_and_cross_references(
|
||||
tmp_path: Path, setting: bool | None
|
||||
) -> None:
|
||||
config = _write_project(tmp_path, {"enable_inventory": setting})
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
|
||||
html = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
|
||||
assert "Return a greeting." in html
|
||||
assert 'class="autorefs autorefs-internal"' in html
|
||||
assert 'href="#sample_api.greet"' in html
|
||||
exported = tmp_path / "site" / "objects.inv"
|
||||
assert exported.exists() is (setting is not False)
|
||||
data = (tmp_path / ".cache" / "objects.inv").read_bytes()
|
||||
assert "sample_api.greet" in Inventory.parse_sphinx(BytesIO(data))
|
||||
|
||||
|
||||
def test_inventory_can_be_disabled_and_reenabled_between_builds(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
for enabled in [True, False, True]:
|
||||
config = _write_project(tmp_path, {"enable_inventory": enabled})
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
exported = tmp_path / "site" / "objects.inv"
|
||||
assert exported.exists() is enabled
|
||||
if enabled:
|
||||
assert (
|
||||
exported.read_bytes()
|
||||
== (tmp_path / ".cache" / "objects.inv").read_bytes()
|
||||
)
|
||||
|
||||
|
||||
def test_automatic_inventory_survives_cached_api_pages(tmp_path: Path) -> None:
|
||||
config = _write_project(tmp_path, {})
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
expected = (tmp_path / "site" / "objects.inv").read_bytes()
|
||||
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert mkdocstrings.HANDLERS is None
|
||||
assert (tmp_path / "site" / "objects.inv").read_bytes() == expected
|
||||
|
||||
(tmp_path / "docs" / "other.md").write_text(
|
||||
"# Other\n\nChanged\n", encoding="utf-8"
|
||||
)
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert mkdocstrings.HANDLERS is not None
|
||||
assert not mkdocstrings.HANDLERS.inventory
|
||||
assert (tmp_path / "site" / "objects.inv").read_bytes() == expected
|
||||
|
||||
|
||||
def test_automatic_inventory_resets_handler_preference_with_configuration(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
config = _write_project(tmp_path, {})
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert (tmp_path / "site" / "objects.inv").exists()
|
||||
|
||||
monkeypatch.setattr(PythonHandler, "enable_inventory", False)
|
||||
# Changing the config makes the next build render pages again and check
|
||||
# whether the handlers enable `objects.inv`.
|
||||
config = _write_project(tmp_path, {"enable_inventory": None})
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert not (tmp_path / "site" / "objects.inv").exists()
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert mkdocstrings.HANDLERS is None
|
||||
assert not (tmp_path / "site" / "objects.inv").exists()
|
||||
|
||||
|
||||
def test_disabled_plugin_preserves_static_inventory(tmp_path: Path) -> None:
|
||||
docs = tmp_path / "docs"
|
||||
docs.mkdir()
|
||||
(docs / "index.md").write_text("# Home\n", encoding="utf-8")
|
||||
(docs / "objects.inv").write_bytes(b"user-provided inventory")
|
||||
config = tmp_path / "zensical.toml"
|
||||
config.write_text(
|
||||
'[project]\nsite_name = "Inventory"\n'
|
||||
"[project.plugins.mkdocstrings]\n"
|
||||
"enabled = false\nenable_inventory = false\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
zensical.build(str(config), _BUILD_OPTIONS)
|
||||
assert (
|
||||
tmp_path / "site" / "objects.inv"
|
||||
).read_bytes() == b"user-provided inventory"
|
||||
Vendored
+23
-1
@@ -291,7 +291,7 @@ class TestPluginShimming:
|
||||
"callouts": {"aliases": False, "breakless_lists": False},
|
||||
"glightbox": {"slide_effect": "fade"},
|
||||
"mike": {"javascript_dir": "scripts"},
|
||||
"mkdocstrings": {"enable_inventory": False, "watch": ["src"]},
|
||||
"mkdocstrings": {"watch": ["src"]},
|
||||
"search": {"lang": ["en", "fr"]},
|
||||
"material/tags": {"tags_file": "tags.md", "export_only": True},
|
||||
}.items():
|
||||
@@ -848,6 +848,28 @@ class TestPluginShimming:
|
||||
autorefs = config["mdx_configs"][AutorefsExtension.name]
|
||||
assert autorefs["record_backlinks"] is (backlinks is not False)
|
||||
|
||||
@pytest.mark.parametrize("value", [True, False, None])
|
||||
def test_mkdocstrings_inventory_setting_forwarded_and_hashed(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
value: bool | None,
|
||||
) -> None:
|
||||
monkeypatch.setattr("zensical.config.find_spec", lambda _name: True)
|
||||
baseline = self._parse_yaml(tmp_path, plugins={"mkdocstrings": {}})
|
||||
config_file = tmp_path / "mkdocs.yml"
|
||||
config_file.write_text(
|
||||
_minimal_yaml(plugins={"mkdocstrings": {"enable_inventory": value}})
|
||||
)
|
||||
configured = parse_config(str(config_file))
|
||||
assert (
|
||||
configured["mdx_configs"][MkdocstringsExtension.name][
|
||||
"enable_inventory"
|
||||
]
|
||||
is value
|
||||
)
|
||||
assert configured["plugins_hash"] != baseline["plugins_hash"]
|
||||
|
||||
def test_mkdocstrings_not_installed_raises(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2025-2026 Zensical and contributors
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
# All contributions are certified under the DCO
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
# IN THE SOFTWARE.
|
||||
|
||||
"""Test when mkdocstrings writes `objects.inv`, using fake handlers."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
import zensical.config as config_module
|
||||
from zensical.compat import mkdocstrings
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("setting", "handlers", "cached", "expected", "automatic"),
|
||||
[
|
||||
(None, None, False, False, False),
|
||||
(None, None, True, True, True),
|
||||
(None, [], False, False, False),
|
||||
(None, [False], False, False, False),
|
||||
(None, [False, True], False, True, True),
|
||||
(None, [False], True, True, True),
|
||||
(True, [False], False, True, False),
|
||||
(True, None, False, True, False),
|
||||
(False, [True], False, False, True),
|
||||
(False, None, True, False, True),
|
||||
],
|
||||
)
|
||||
def test_inventory_policy_combines_settings_with_cached_handlers(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
setting: bool | None,
|
||||
handlers: list[bool] | None,
|
||||
cached: bool,
|
||||
expected: bool,
|
||||
automatic: bool,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
config_module,
|
||||
"_CONFIG",
|
||||
{
|
||||
"plugins": {
|
||||
"mkdocstrings": {"config": {"enable_inventory": setting}}
|
||||
}
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(mkdocstrings, "_ENABLE_INVENTORY", setting)
|
||||
monkeypatch.setattr(
|
||||
mkdocstrings,
|
||||
"HANDLERS",
|
||||
(
|
||||
None
|
||||
if handlers is None
|
||||
else SimpleNamespace(
|
||||
seen_handlers=[
|
||||
SimpleNamespace(enable_inventory=value)
|
||||
for value in handlers
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
assert mkdocstrings.get_inventory_policy(cached) == (expected, automatic)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("as_extension", [False, True])
|
||||
def test_disabled_mkdocstrings_does_not_export_cached_inventory(
|
||||
monkeypatch: pytest.MonkeyPatch, as_extension: bool
|
||||
) -> None:
|
||||
options = {"enabled": False, "enable_inventory": True}
|
||||
config = (
|
||||
{"mdx_configs": {"zensical.extensions.mkdocstrings": options}}
|
||||
if as_extension
|
||||
else {"plugins": {"mkdocstrings": {"config": options}}}
|
||||
)
|
||||
monkeypatch.setattr(config_module, "_CONFIG", config)
|
||||
monkeypatch.setattr(mkdocstrings, "HANDLERS", None)
|
||||
assert mkdocstrings.get_inventory_policy(True) == (False, True)
|
||||
|
||||
|
||||
def test_cached_inventory_uses_explicit_extension_options(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
config_module,
|
||||
"_CONFIG",
|
||||
{
|
||||
"plugins": {"mkdocstrings": {"config": {"enable_inventory": True}}},
|
||||
"mdx_configs": {
|
||||
"zensical.extensions.mkdocstrings": {"enable_inventory": False}
|
||||
},
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(mkdocstrings, "HANDLERS", None)
|
||||
assert mkdocstrings.get_inventory_policy(True) == (False, True)
|
||||
+19
-1
@@ -289,7 +289,6 @@ def test_rejects_invalid_blog_configuration(name: str, data: Any) -> None:
|
||||
("glightbox", "shadow"),
|
||||
("mike", "css_dir"),
|
||||
("mike", "javascript_dir"),
|
||||
("mkdocstrings", "enable_inventory"),
|
||||
("mkdocstrings", "watch"),
|
||||
("search", "fields"),
|
||||
("search", "indexing"),
|
||||
@@ -416,6 +415,7 @@ def test_normalizes_null_shim_configuration(name: str) -> None:
|
||||
"mkdocstrings",
|
||||
{
|
||||
"enabled": False,
|
||||
"enable_inventory": False,
|
||||
"handlers": {"python": {"options": {}}},
|
||||
"custom_templates": None,
|
||||
"default_handler": "python",
|
||||
@@ -480,6 +480,24 @@ def test_accepts_supported_shim_options(
|
||||
assert plugins[name]["config"] == config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", ["mkdocstrings", "material/mkdocstrings"])
|
||||
@pytest.mark.parametrize("value", [True, False, None])
|
||||
def test_preserves_mkdocstrings_inventory_setting(
|
||||
plugin: str, value: bool | None
|
||||
) -> None:
|
||||
data = {"enable_inventory": value}
|
||||
assert _convert_plugins({plugin: data})["mkdocstrings"]["config"] == data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [0, 1, "true", "auto", [], {}])
|
||||
def test_rejects_invalid_mkdocstrings_inventory_setting(value: Any) -> None:
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match="mkdocstrings enable_inventory must be a boolean or null",
|
||||
):
|
||||
_convert_plugins({"mkdocstrings": {"enable_inventory": value}})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", ["macros", "material/macros"])
|
||||
@pytest.mark.parametrize(
|
||||
("option", "value"),
|
||||
|
||||
Reference in new issue
Block a user