mirror of
https://github.com/zensical/zensical.git
synced 2026-10-08 13:51:22 +00:00
feature: support more macros settings
Signed-off-by: Timothée Mazzucotelli <dev@pawamoy.fr>
This commit is contained in:
8 files changed
+425
-19
No files matched your search
+176
@@ -0,0 +1,176 @@
|
||||
# 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.
|
||||
|
||||
"""Integration coverage for macros page selection and verbose output."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
import zensical
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _write_config(
|
||||
root: Path, options: dict[str, str | bool], *, toml: bool = False
|
||||
) -> Path:
|
||||
(root / "content").mkdir(exist_ok=True)
|
||||
overrides = root / "overrides"
|
||||
overrides.mkdir(exist_ok=True)
|
||||
(overrides / "main.html").write_text("{{ page.content }}", encoding="utf-8")
|
||||
if toml:
|
||||
config = root / "zensical.toml"
|
||||
content = (
|
||||
'[project]\nsite_name = "Macros"\ndocs_dir = "content"\n'
|
||||
'[project.theme]\ncustom_dir = "overrides"\n'
|
||||
"[project.plugins.macros]\n"
|
||||
)
|
||||
content += "\n".join(
|
||||
f"{name} = {json.dumps(value)}" for name, value in options.items()
|
||||
)
|
||||
else:
|
||||
config = root / "mkdocs.yml"
|
||||
content = yaml.safe_dump(
|
||||
{
|
||||
"site_name": "Macros",
|
||||
"docs_dir": "content",
|
||||
"theme": {"custom_dir": "overrides"},
|
||||
"plugins": {"macros": options},
|
||||
}
|
||||
)
|
||||
config.write_text(content, encoding="utf-8")
|
||||
return config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("toml", [False, True], ids=["yaml", "toml"])
|
||||
def test_force_render_paths_uses_docs_paths_and_page_metadata(
|
||||
tmp_path: Path, toml: bool
|
||||
) -> None:
|
||||
config = _write_config(
|
||||
tmp_path,
|
||||
{
|
||||
"render_by_default": False,
|
||||
"force_render_paths": (
|
||||
"/guides/\n!guides/drafts/\nguides/drafts/keep.md"
|
||||
),
|
||||
},
|
||||
toml=toml,
|
||||
)
|
||||
pages = {
|
||||
"index.md": (None, False),
|
||||
"guides/index.md": (None, True),
|
||||
"guides/disabled.md": (False, False),
|
||||
"guides/drafts/page.md": (None, False),
|
||||
"guides/drafts/keep.md": (None, True),
|
||||
"outside.md": (True, True),
|
||||
"nested/guides/page.md": (None, False),
|
||||
}
|
||||
for path, (override, _) in pages.items():
|
||||
page = tmp_path / "content" / path
|
||||
page.parent.mkdir(parents=True, exist_ok=True)
|
||||
header = (
|
||||
""
|
||||
if override is None
|
||||
else f"---\nrender_macros: {str(override).lower()}\n---\n"
|
||||
)
|
||||
page.write_text(header + "Value: {{ 1 + 1 }}\n", encoding="utf-8")
|
||||
|
||||
zensical.build(str(config), {"clean": True, "strict": False})
|
||||
|
||||
for path, (_, rendered) in pages.items():
|
||||
output = tmp_path / "site" / path.removesuffix(".md")
|
||||
output = (
|
||||
output.with_suffix(".html")
|
||||
if output.name == "index"
|
||||
else output / "index.html"
|
||||
)
|
||||
html = output.read_text(encoding="utf-8")
|
||||
assert ("Value: 2" if rendered else "Value: {{ 1 + 1 }}") in html, path
|
||||
|
||||
|
||||
def test_force_render_paths_changes_between_builds(tmp_path: Path) -> None:
|
||||
for index, (pattern, rendered) in enumerate(
|
||||
[("", False), ("/index.md", True), ("!index.md", False)]
|
||||
):
|
||||
config = _write_config(
|
||||
tmp_path,
|
||||
{
|
||||
"render_by_default": False,
|
||||
"force_render_paths": pattern,
|
||||
},
|
||||
)
|
||||
if index == 0:
|
||||
(tmp_path / "content" / "index.md").write_text(
|
||||
"Value: {{ 1 + 1 }}\n", encoding="utf-8"
|
||||
)
|
||||
zensical.build(str(config), {"clean": index == 0, "strict": False})
|
||||
html = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
|
||||
assert ("Value: 2" if rendered else "Value: {{ 1 + 1 }}") in html
|
||||
|
||||
|
||||
def test_verbose_chatter_reaches_cli_and_can_be_disabled(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
for index, verbose in enumerate([False, True, False]):
|
||||
config = _write_config(
|
||||
tmp_path, {"verbose": verbose, "on_error_fail": True}
|
||||
)
|
||||
if index == 0:
|
||||
(tmp_path / "content" / "index.md").write_text(
|
||||
'{{ greet("World") }}\n', encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "main.py").write_text(
|
||||
"def define_env(env):\n"
|
||||
' chatter = env.start_chatting("Example", color="cyan")\n'
|
||||
' chatter("Registered macros")\n'
|
||||
" @env.macro\n"
|
||||
" def greet(name):\n"
|
||||
' chatter("Greeting:", name)\n'
|
||||
' return "Hello " + name\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-m", "zensical.main", "build", "-f", str(config)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
html = (tmp_path / "site" / "index.html").read_text(encoding="utf-8")
|
||||
assert "Hello World" in html
|
||||
if verbose:
|
||||
assert "[macros - Example] - Registered macros" in result.stderr
|
||||
assert "[macros - Example] - Greeting: World" in result.stderr
|
||||
assert "Rendering page: index.md" in result.stderr
|
||||
assert "Loading local module:" in result.stderr
|
||||
else:
|
||||
assert "[macros -" not in result.stderr
|
||||
assert "[macros -" not in result.stdout
|
||||
assert "[macros -" not in html
|
||||
+129
-2
@@ -23,6 +23,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from io import StringIO
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -34,11 +35,13 @@ from jinja2.exceptions import (
|
||||
TemplateSyntaxError,
|
||||
UndefinedError,
|
||||
)
|
||||
from markdown import Markdown
|
||||
|
||||
from tests.unit.extensions.conftest import soup
|
||||
from zensical.extensions.context import ContextPreprocessor
|
||||
from zensical.extensions.macros import (
|
||||
MacroEnv,
|
||||
MacrosExtension,
|
||||
_fix_url,
|
||||
_load_module,
|
||||
_load_one_yaml,
|
||||
@@ -55,7 +58,6 @@ from zensical.extensions.table_reader import (
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from markdown import Markdown
|
||||
from pandas import DataFrame
|
||||
|
||||
|
||||
@@ -126,6 +128,18 @@ class TestFilters:
|
||||
|
||||
|
||||
class TestMacroEnv:
|
||||
@pytest.mark.parametrize("verbose", [False, True])
|
||||
def test_chatter_respects_verbose(
|
||||
self, caplog: pytest.LogCaptureFixture, verbose: bool
|
||||
) -> None:
|
||||
env = MacroEnv(verbose=verbose)
|
||||
chatter = env.start_chatting("Example", color="cyan")
|
||||
with caplog.at_level(logging.INFO, logger="zensical.extensions.macros"):
|
||||
chatter("Count:", 2)
|
||||
assert caplog.messages == (
|
||||
["[macros - Example] - Count: 2"] if verbose else []
|
||||
)
|
||||
|
||||
def test_conf_is_stored(self) -> None:
|
||||
conf = {"site_name": "My Site", "docs_dir": "/docs"}
|
||||
env = MacroEnv(conf=conf)
|
||||
@@ -268,6 +282,118 @@ class TestLoadModule:
|
||||
|
||||
|
||||
class TestPreprocessor:
|
||||
@pytest.mark.parametrize(
|
||||
"md",
|
||||
[
|
||||
{
|
||||
"config": {
|
||||
"markdown_extensions": {
|
||||
"zensical.extensions.macros": {
|
||||
"render_by_default": False,
|
||||
"force_render_paths": (
|
||||
"# Selected pages\n\n"
|
||||
"guides/\n!guides/drafts/\nguides/drafts/keep.md\n"
|
||||
"/only.md\nrender_*.md\n\\#literal.md\n\\!literal.md"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("path", "override", "rendered"),
|
||||
[
|
||||
("guides/index.md", None, True),
|
||||
(os.path.join("guides", "nested", "page.md"), None, True),
|
||||
("guides/café.md", None, True),
|
||||
("guides/drafts/page.md", None, False),
|
||||
("guides/drafts/keep.md", None, True),
|
||||
("guides/disabled.md", False, False),
|
||||
("guides/drafts/keep.md", False, False),
|
||||
("outside.md", True, True),
|
||||
("outside.md", None, False),
|
||||
("guides.md", None, False),
|
||||
("only.md", None, True),
|
||||
("nested/only.md", None, False),
|
||||
("nested/render_example.md", None, True),
|
||||
("#literal.md", None, True),
|
||||
("!literal.md", None, True),
|
||||
],
|
||||
)
|
||||
def test_force_render_paths_respects_page_overrides(
|
||||
self, md: Markdown, path: str, override: bool | None, rendered: bool
|
||||
) -> None:
|
||||
context = ContextPreprocessor.from_markdown(md)
|
||||
assert context is not None
|
||||
context.page.path = path
|
||||
if override is not None:
|
||||
context.page.meta["render_macros"] = override
|
||||
|
||||
html = soup(md.convert("Value: {{ 1 + 1 }}"))
|
||||
assert html.get_text() == (
|
||||
"Value: 2" if rendered else "Value: {{ 1 + 1 }}"
|
||||
)
|
||||
|
||||
def test_force_render_paths_does_not_require_page_context(self) -> None:
|
||||
md = Markdown(
|
||||
extensions=[
|
||||
MacrosExtension(
|
||||
render_by_default=False, force_render_paths="**"
|
||||
)
|
||||
]
|
||||
)
|
||||
assert md.convert("Value: {{ 1 + 1 }}") == "<p>Value: {{ 1 + 1 }}</p>"
|
||||
|
||||
@pytest.mark.parametrize("verbose", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
"md",
|
||||
[
|
||||
{
|
||||
"config": {
|
||||
"markdown_extensions": {
|
||||
"zensical.extensions.macros": {"on_error_fail": True}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_verbose_traces_module_loading_and_rendering(
|
||||
self,
|
||||
md: Markdown,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
verbose: bool,
|
||||
) -> None:
|
||||
md.preprocessors["macros"].config.verbose = verbose
|
||||
(tmp_path / "main.py").write_text(
|
||||
"def define_env(env):\n"
|
||||
' chatter = env.start_chatting("Example")\n'
|
||||
' chatter("Registered macros")\n'
|
||||
" @env.macro\n"
|
||||
" def greet(name):\n"
|
||||
' chatter("Greeting:", name)\n'
|
||||
' return "Hello " + name\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
with caplog.at_level(logging.INFO, logger="zensical.extensions.macros"):
|
||||
html = md.convert('{{ greet("World") }}')
|
||||
assert "Hello World" in html
|
||||
if verbose:
|
||||
assert (
|
||||
"[macros - render] - Rendering page: index.md"
|
||||
in caplog.messages
|
||||
)
|
||||
assert any(
|
||||
"Loading local module:" in line for line in caplog.messages
|
||||
)
|
||||
assert "[macros - Example] - Registered macros" in caplog.messages
|
||||
assert "[macros - Example] - Greeting: World" in caplog.messages
|
||||
else:
|
||||
assert caplog.messages == []
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("md", "expected_text"),
|
||||
[
|
||||
@@ -289,7 +415,8 @@ class TestPreprocessor:
|
||||
"config": {
|
||||
"markdown_extensions": {
|
||||
"zensical.extensions.macros": {
|
||||
"render_by_default": True
|
||||
"render_by_default": True,
|
||||
"force_render_paths": "!**",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Vendored
+17
-2
@@ -281,7 +281,6 @@ class TestPluginShimming:
|
||||
plugins: dict[str, dict[str, Any]] = {
|
||||
"callouts": {},
|
||||
"glightbox": {"auto": False},
|
||||
"macros": {"render_by_default": False},
|
||||
"mike": {"version_selector": False},
|
||||
"mkdocstrings": {"enabled": False},
|
||||
"search": {"separator": r"\s+"},
|
||||
@@ -291,7 +290,6 @@ class TestPluginShimming:
|
||||
for name, options in {
|
||||
"callouts": {"aliases": False, "breakless_lists": False},
|
||||
"glightbox": {"slide_effect": "fade"},
|
||||
"macros": {"force_render_paths": "guides/**"},
|
||||
"mike": {"javascript_dir": "scripts"},
|
||||
"mkdocstrings": {"enable_inventory": False, "watch": ["src"]},
|
||||
"search": {"lang": ["en", "fr"]},
|
||||
@@ -634,6 +632,23 @@ class TestPluginShimming:
|
||||
config = self._parse_yaml(tmp_path, plugins={"macros": {}})
|
||||
assert MacrosExtension.name in config["markdown_extensions"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("option", "value"),
|
||||
[("force_render_paths", "guides/\n!guides/drafts/"), ("verbose", True)],
|
||||
)
|
||||
def test_macros_settings_forwarded_and_hashed(
|
||||
self, tmp_path: Path, option: str, value: str | bool
|
||||
) -> None:
|
||||
baseline = self._parse_yaml(tmp_path, plugins={"macros": {}})
|
||||
config_file = tmp_path / "mkdocs.yml"
|
||||
config_file.write_text(
|
||||
_minimal_yaml(plugins={"macros": {option: value}})
|
||||
)
|
||||
configured = parse_config(str(config_file))
|
||||
|
||||
assert configured["mdx_configs"][MacrosExtension.name][option] == value
|
||||
assert configured["plugins_hash"] != baseline["plugins_hash"]
|
||||
|
||||
def test_table_reader_plugin_shimmed(self, tmp_path: Path) -> None:
|
||||
config = self._parse_yaml(tmp_path, plugins={"table-reader": {}})
|
||||
assert TableReaderExtension.name in config["markdown_extensions"]
|
||||
|
||||
+40
-2
@@ -287,8 +287,6 @@ def test_rejects_invalid_blog_configuration(name: str, data: Any) -> None:
|
||||
("glightbox", "draggable"),
|
||||
("glightbox", "background"),
|
||||
("glightbox", "shadow"),
|
||||
("macros", "force_render_paths"),
|
||||
("macros", "verbose"),
|
||||
("mike", "css_dir"),
|
||||
("mike", "javascript_dir"),
|
||||
("mkdocstrings", "enable_inventory"),
|
||||
@@ -449,6 +447,8 @@ def test_normalizes_null_shim_configuration(name: str) -> None:
|
||||
"include_yaml": {"data": "data.yml"},
|
||||
"include_dir": "includes",
|
||||
"render_by_default": False,
|
||||
"force_render_paths": "guides/\n!guides/drafts/",
|
||||
"verbose": True,
|
||||
"on_error_fail": True,
|
||||
"on_undefined": "strict",
|
||||
"j2_block_start_string": "<%",
|
||||
@@ -480,6 +480,44 @@ def test_accepts_supported_shim_options(
|
||||
assert plugins[name]["config"] == config
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", ["macros", "material/macros"])
|
||||
@pytest.mark.parametrize(
|
||||
("option", "value"),
|
||||
[
|
||||
("force_render_paths", ""),
|
||||
("force_render_paths", "# Pages to render\nguides/\n!guides/drafts/"),
|
||||
("verbose", True),
|
||||
("verbose", False),
|
||||
],
|
||||
)
|
||||
def test_preserves_macros_settings(
|
||||
plugin: str, option: str, value: Any
|
||||
) -> None:
|
||||
data = {option: value}
|
||||
plugins = _convert_plugins({plugin: data})
|
||||
assert plugins["macros"]["config"] == data
|
||||
assert data == {option: value}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("option", "value"),
|
||||
[
|
||||
("force_render_paths", True),
|
||||
("force_render_paths", ["guides/"]),
|
||||
("force_render_paths", {}),
|
||||
("force_render_paths", 1),
|
||||
("force_render_paths", None),
|
||||
("verbose", "true"),
|
||||
("verbose", 1),
|
||||
("verbose", []),
|
||||
("verbose", None),
|
||||
],
|
||||
)
|
||||
def test_rejects_invalid_macros_settings(option: str, value: Any) -> None:
|
||||
with pytest.raises(ConfigurationError, match=f"macros {option} must be"):
|
||||
_convert_plugins({"macros": {option: value}})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plugin", ["autorefs", "material/autorefs"])
|
||||
@pytest.mark.parametrize(
|
||||
("option", "value"),
|
||||
|
||||
Reference in new issue
Block a user