mirror of
https://github.com/zensical/zensical.git
synced 2026-10-08 05:41:24 +00:00
680 lines
23 KiB
Python
Vendored
680 lines
23 KiB
Python
Vendored
# 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
from bs4 import BeautifulSoup
|
|
|
|
import zensical
|
|
|
|
|
|
def project(
|
|
root: Path,
|
|
plugin: str,
|
|
options: dict,
|
|
nav: list | None = None,
|
|
extra_plugins: list | None = None,
|
|
) -> Path:
|
|
docs = root / "docs"
|
|
docs.mkdir(exist_ok=True)
|
|
(docs / "index.md").write_text("# Home\n")
|
|
package = root / "src" / "sample"
|
|
package.mkdir(parents=True, exist_ok=True)
|
|
for name, content in {
|
|
"__init__.py": '"""Sample package."""\n',
|
|
"public.py": '"""Public module documentation."""\n',
|
|
"_private.py": '"""Private module documentation."""\n',
|
|
"index.py": '"""Index module documentation."""\n',
|
|
}.items():
|
|
(package / name).write_text(content)
|
|
config = {
|
|
"site_name": "API test",
|
|
"theme": {"features": ["content.action.edit"]},
|
|
"repo_url": "https://example.com/repository",
|
|
"edit_uri": "edit/main/docs",
|
|
"plugins": [
|
|
{plugin: options},
|
|
{"mkdocstrings": {"handlers": {"python": {"paths": ["src"]}}}},
|
|
*(extra_plugins or []),
|
|
],
|
|
}
|
|
if nav is not None:
|
|
config["nav"] = nav
|
|
path = root / "mkdocs.yml"
|
|
path.write_text(yaml.safe_dump(config, sort_keys=False))
|
|
return path
|
|
|
|
|
|
def build(path: Path, *, strict: bool = True) -> None:
|
|
zensical.build(str(path), {"clean": False, "strict": strict})
|
|
|
|
|
|
def navigation_template(config: Path) -> None:
|
|
"""Render navigation entries with their depth, title, and URL."""
|
|
overrides = config.parent / "overrides"
|
|
overrides.mkdir()
|
|
(overrides / "main.html").write_text(
|
|
"""\
|
|
{% macro render(items, depth) %}
|
|
{% for item in items %}
|
|
<item depth="{{ depth }}" title="{{ (item.title or '') | e }}"
|
|
url="{{ (item.url or '') | e }}" />
|
|
{{ render(item.children, depth + 1) }}
|
|
{% endfor %}
|
|
{% endmacro %}
|
|
{{ render(nav.items, 0) }}
|
|
"""
|
|
)
|
|
data = yaml.safe_load(config.read_text())
|
|
data["theme"]["custom_dir"] = "overrides"
|
|
config.write_text(yaml.safe_dump(data, sort_keys=False))
|
|
|
|
|
|
def navigation_items(root: Path) -> list[tuple[int, str, str]]:
|
|
soup = BeautifulSoup((root / "site/index.html").read_text(), "html.parser")
|
|
return [
|
|
(int(str(item["depth"])), str(item["title"]), str(item["url"]))
|
|
for item in soup.find_all("item")
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin", "options", "prefix"),
|
|
[
|
|
("mkdocs-autoapi", {"autoapi_dir": "src"}, "autoapi"),
|
|
("api-autonav", {"modules": ["src/sample"]}, "reference"),
|
|
],
|
|
)
|
|
def test_generates_api_pages_and_navigation(
|
|
tmp_path: Path, plugin: str, options: dict, prefix: str
|
|
) -> None:
|
|
config = project(tmp_path, plugin, options)
|
|
if plugin == "mkdocs-autoapi":
|
|
# Upstream AutoAPI uses index.md for both index.py and __init__.py.
|
|
(tmp_path / "src/sample/index.py").unlink()
|
|
build(config)
|
|
home = (tmp_path / "site/index.html").read_text()
|
|
page = (tmp_path / f"site/{prefix}/sample/public/index.html").read_text()
|
|
assert "Public module documentation" in page
|
|
assert "API Reference" in home
|
|
assert f"{prefix}/sample/public/" in home
|
|
assert not (tmp_path / f"docs/{prefix}").exists()
|
|
assert (
|
|
tmp_path / f"site/{prefix}/sample/_private/index.html"
|
|
).exists() == (plugin == "mkdocs-autoapi")
|
|
search = json.loads((tmp_path / "site/search.json").read_text())
|
|
assert any(
|
|
"Public module documentation" in doc["text"] for doc in search["items"]
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("plugin", "options", "prefix"),
|
|
[
|
|
("mkdocs-autoapi", {"autoapi_dir": "src"}, "autoapi"),
|
|
("api-autonav", {"modules": ["src/sample"]}, "reference"),
|
|
],
|
|
)
|
|
def test_exclude_filters_generated_api_pages(
|
|
tmp_path: Path, plugin: str, options: dict, prefix: str
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
plugin,
|
|
options,
|
|
extra_plugins=[{"exclude": {"glob": f"{prefix}/sample/public.md"}}],
|
|
)
|
|
|
|
build(config, strict=False)
|
|
|
|
# Generated Markdown follows the same exclusion rules as physical pages.
|
|
assert not (tmp_path / f"site/{prefix}/sample/public/index.html").exists()
|
|
assert (tmp_path / f"site/{prefix}/sample/index.html").exists()
|
|
search = (tmp_path / "site/search.json").read_text()
|
|
assert "Public module documentation" not in search
|
|
|
|
|
|
def test_autoapi_patterns_stubs_keep_and_manual_navigation(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"mkdocs-autoapi",
|
|
{
|
|
"autoapi_dir": "src/sample",
|
|
"autoapi_file_patterns": ["*.pyi", "*.py"],
|
|
"autoapi_ignore": ["index.py", "_private.py"],
|
|
"autoapi_root": "api",
|
|
"autoapi_add_nav_entry": False,
|
|
"autoapi_keep_files": True,
|
|
},
|
|
nav=[{"Home": "index.md"}, {"Manual API": "api/"}],
|
|
)
|
|
(tmp_path / "src/sample/public.pyi").write_text('"""Stub docs."""\n')
|
|
build(config)
|
|
assert (
|
|
tmp_path / "docs/api/sample/public.md"
|
|
).read_text() == "::: sample.public\n"
|
|
summary = (tmp_path / "docs/api/summary.md").read_text()
|
|
assert "[public](sample/public.md)" in summary
|
|
assert "_private" not in summary
|
|
assert not (tmp_path / "site/api/summary/index.html").exists()
|
|
home = (tmp_path / "site/index.html").read_text()
|
|
assert "Manual API" in home
|
|
assert "API Reference" not in home
|
|
|
|
|
|
@pytest.mark.parametrize("preferred", ["py", "pyi"])
|
|
@pytest.mark.parametrize("source_root", ["src", "src/sample"])
|
|
def test_autoapi_excludes_preferred_source_without_falling_back(
|
|
tmp_path: Path, preferred: str, source_root: str
|
|
) -> None:
|
|
fallback = "pyi" if preferred == "py" else "py"
|
|
ignored = (
|
|
f"**/public.{preferred}"
|
|
if source_root == "src"
|
|
else f"public.{preferred}"
|
|
)
|
|
config = project(
|
|
tmp_path,
|
|
"mkdocs-autoapi",
|
|
{
|
|
"autoapi_dir": source_root,
|
|
"autoapi_file_patterns": [f"*.{preferred}", f"*.{fallback}"],
|
|
"autoapi_ignore": [ignored, "**/index.py"],
|
|
"autoapi_keep_files": True,
|
|
},
|
|
)
|
|
(tmp_path / "src/sample/public.pyi").write_text(
|
|
'"""Stub documentation."""\n'
|
|
)
|
|
|
|
build(config)
|
|
|
|
# Excluding the selected extension must exclude the module altogether.
|
|
assert (tmp_path / "docs/autoapi/sample/index.md").exists()
|
|
assert not (tmp_path / "docs/autoapi/sample/public.md").exists()
|
|
assert not (tmp_path / "site/autoapi/sample/public/index.html").exists()
|
|
assert "public" not in (tmp_path / "docs/autoapi/summary.md").read_text()
|
|
|
|
|
|
@pytest.mark.parametrize("api_root", ["reference", "api/reference"])
|
|
def test_autonav_preserves_handwritten_pages_in_automatic_navigation(
|
|
tmp_path: Path, api_root: str
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/sample"],
|
|
"api_root_uri": api_root,
|
|
"nav_item_prefix": "MOD ",
|
|
"show_full_namespace": True,
|
|
},
|
|
)
|
|
directory = tmp_path / "docs" / api_root
|
|
(directory / "sample/topics").mkdir(parents=True)
|
|
(directory / "overview.md").write_text("# API overview\n")
|
|
(directory / "sample/guide.md").write_text("# Package guide\n")
|
|
(directory / "sample/topics/start.md").write_text("# Getting started\n")
|
|
navigation_template(config)
|
|
|
|
build(config)
|
|
|
|
# Automatic navigation includes every page, with API section titles applied.
|
|
assert navigation_items(tmp_path) == [
|
|
(0, "Home", ""),
|
|
(0, "API Reference", ""),
|
|
(1, "API overview", f"{api_root}/overview/"),
|
|
(1, "MOD sample", ""),
|
|
(2, "sample", f"{api_root}/sample/"),
|
|
(2, "Package guide", f"{api_root}/sample/guide/"),
|
|
(2, "sample.index", f"{api_root}/sample/index_py/"),
|
|
(2, "sample.public", f"{api_root}/sample/public/"),
|
|
(2, "MOD sample.topics", ""),
|
|
(3, "Getting started", f"{api_root}/sample/topics/start/"),
|
|
]
|
|
|
|
|
|
def test_autonav_preserves_directory_links_with_a_different_title(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{"modules": ["src/sample"], "nav_item_prefix": ""},
|
|
nav=[{"Home": "index.md"}, {"Manual docs": "reference/"}],
|
|
)
|
|
navigation_template(config)
|
|
|
|
build(config)
|
|
|
|
# Only the configured API section title is an Autonav placeholder.
|
|
items = navigation_items(tmp_path)
|
|
assert items[:3] == [
|
|
(0, "Home", ""),
|
|
(0, "Manual docs", "reference/"),
|
|
(0, "API Reference", ""),
|
|
]
|
|
assert (2, "public", "reference/sample/public/") in items
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"nav",
|
|
[
|
|
["API Reference", {"Home": "index.md"}],
|
|
[{"API Reference": "api/"}, {"Home": "index.md"}],
|
|
[{"API Reference": [{"Intro": "intro.md"}]}, {"Home": "index.md"}],
|
|
[{"Nested": [{"API Reference": "api"}]}, {"Home": "index.md"}],
|
|
],
|
|
)
|
|
def test_autonav_navigation_and_module_options(
|
|
tmp_path: Path, nav: list
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/sample"],
|
|
"api_root_uri": "api",
|
|
"nav_item_prefix": "MOD ",
|
|
"show_full_namespace": True,
|
|
"exclude_private": False,
|
|
"exclude": ["sample.index", r"re:sample\._private$"],
|
|
"module_options": {
|
|
".*": {"heading_level": 1},
|
|
r"sample\.public$": {
|
|
"heading_level": 2,
|
|
"show_root_heading": True,
|
|
},
|
|
},
|
|
},
|
|
nav=nav,
|
|
)
|
|
(tmp_path / "docs/intro.md").write_text("# Introduction\n")
|
|
build(config)
|
|
page = (tmp_path / "site/api/sample/public/index.html").read_text()
|
|
assert "MOD sample.public" in page
|
|
assert '<h1 id="samplepublic"' in page
|
|
assert not (tmp_path / "site/api/sample/index_py").exists()
|
|
assert not (tmp_path / "site/api/sample/_private").exists()
|
|
|
|
|
|
@pytest.mark.parametrize("policy", ["raise", "warn", "skip"])
|
|
def test_namespace_policy(tmp_path: Path, policy: str) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/sample"],
|
|
"on_implicit_namespace_package": policy,
|
|
},
|
|
)
|
|
namespace = tmp_path / "src/sample/implicit"
|
|
namespace.mkdir()
|
|
(namespace / "child.py").write_text('"""Child."""\n')
|
|
if policy == "raise":
|
|
with pytest.raises(RuntimeError, match="implicit namespace package"):
|
|
build(config)
|
|
else:
|
|
build(config, strict=False)
|
|
assert not (tmp_path / "site/reference/sample/implicit").exists()
|
|
assert (tmp_path / "site/reference/sample/public/index.html").exists()
|
|
|
|
|
|
@pytest.mark.parametrize("full_namespace", [False, True])
|
|
def test_autonav_with_awesome_nav(tmp_path: Path, full_namespace: bool) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/sample"],
|
|
"show_full_namespace": full_namespace,
|
|
"nav_item_prefix": "MOD ",
|
|
},
|
|
extra_plugins=["awesome-nav"],
|
|
)
|
|
package = tmp_path / "src/sample/sub_package"
|
|
package.mkdir()
|
|
(package / "__init__.py").write_text('"""Subpackage documentation."""\n')
|
|
(package / "child.py").write_text('"""Child module documentation."""\n')
|
|
navigation_template(config)
|
|
|
|
build(config)
|
|
|
|
# Awesome-nav uses its own root title and the generated package titles.
|
|
items = navigation_items(tmp_path)
|
|
assert items[:3] == [
|
|
(0, "Home", ""),
|
|
(0, "Reference", ""),
|
|
(1, "sample", ""),
|
|
]
|
|
assert (
|
|
2,
|
|
"sample.public" if full_namespace else "public",
|
|
"reference/sample/public/",
|
|
) in items
|
|
assert (
|
|
2,
|
|
"sample.sub_package" if full_namespace else "sub_package",
|
|
"",
|
|
) in items
|
|
assert not (tmp_path / "docs/reference").exists()
|
|
assert not list((tmp_path / "site").rglob(".nav.yml"))
|
|
|
|
|
|
@pytest.mark.parametrize("plugin", ["mkdocs-autoapi", "api-autonav"])
|
|
@pytest.mark.parametrize("control", ["hide: true\n", "nav:\n - overview.md\n"])
|
|
def test_awesome_nav_controls_generated_api_pages(
|
|
tmp_path: Path, plugin: str, control: str
|
|
) -> None:
|
|
options = (
|
|
{
|
|
"autoapi_dir": "src",
|
|
"autoapi_root": "reference",
|
|
"autoapi_ignore": ["**/index.py"],
|
|
}
|
|
if plugin == "mkdocs-autoapi"
|
|
else {"modules": ["src/sample"]}
|
|
)
|
|
config = project(tmp_path, plugin, options, extra_plugins=["awesome-nav"])
|
|
directory = tmp_path / "docs/reference"
|
|
directory.mkdir()
|
|
(directory / ".nav.yml").write_text(control)
|
|
(directory / "overview.md").write_text("# API overview\n")
|
|
navigation_template(config)
|
|
|
|
build(config)
|
|
|
|
# API pages still build, but awesome-nav controls their navigation entries.
|
|
assert (tmp_path / "site/reference/sample/public/index.html").exists()
|
|
expected = [(0, "Home", "")]
|
|
if control.startswith("nav:"):
|
|
expected.extend(
|
|
[
|
|
(0, "Reference", ""),
|
|
(1, "API overview", "reference/overview/"),
|
|
]
|
|
)
|
|
assert navigation_items(tmp_path) == expected
|
|
|
|
|
|
def test_autonav_index_module_and_source_rebuild(tmp_path: Path) -> None:
|
|
config = project(tmp_path, "api-autonav", {"modules": ["src/sample"]})
|
|
build(config)
|
|
assert (tmp_path / "site/reference/sample/index_py/index.html").exists()
|
|
(tmp_path / "src/sample/public.py").write_text(
|
|
'"""Updated module documentation."""\n'
|
|
)
|
|
build(config)
|
|
page = (tmp_path / "site/reference/sample/public/index.html").read_text()
|
|
assert "Updated module documentation" in page
|
|
(tmp_path / "src/sample/public.py").unlink()
|
|
build(config)
|
|
assert not (tmp_path / "site/reference/sample/public/index.html").exists()
|
|
|
|
|
|
@pytest.mark.parametrize("plugin", ["mkdocs-autoapi", "api-autonav"])
|
|
def test_serve_discovers_added_renamed_and_removed_modules(
|
|
tmp_path: Path, plugin: str
|
|
) -> None:
|
|
options = (
|
|
{"autoapi_dir": "src", "autoapi_ignore": ["**/index.py"]}
|
|
if plugin == "mkdocs-autoapi"
|
|
else {"modules": ["src/sample"]}
|
|
)
|
|
config = project(tmp_path, plugin, options)
|
|
with config.open("a") as file:
|
|
file.write("dev_addr: 127.0.0.1:0\n")
|
|
prefix = "autoapi" if plugin == "mkdocs-autoapi" else "reference"
|
|
output = tmp_path / f"site/{prefix}/sample/public/index.html"
|
|
added = tmp_path / "src/sample/added.py"
|
|
added_output = tmp_path / f"site/{prefix}/sample/added/index.html"
|
|
renamed = tmp_path / "src/sample/renamed.py"
|
|
renamed_output = tmp_path / f"site/{prefix}/sample/renamed/index.html"
|
|
with (tmp_path / "serve.log").open("w+") as log:
|
|
process = subprocess.Popen( # noqa: S603
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"zensical",
|
|
"serve",
|
|
"--config-file",
|
|
str(config),
|
|
],
|
|
cwd=tmp_path,
|
|
stdout=log,
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
|
|
def wait_for(condition: Callable[[], bool]) -> None:
|
|
deadline = time.monotonic() + 15
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
if condition():
|
|
return
|
|
except FileNotFoundError:
|
|
# A rebuild can remove output between checking and reading.
|
|
pass
|
|
if process.poll() is not None:
|
|
break
|
|
time.sleep(0.02)
|
|
log.seek(0)
|
|
raise AssertionError(log.read())
|
|
|
|
try:
|
|
wait_for(output.is_file)
|
|
added.write_text('"""New module."""\n')
|
|
wait_for(added_output.is_file)
|
|
added.rename(renamed)
|
|
wait_for(
|
|
lambda: renamed_output.is_file() and not added_output.exists()
|
|
)
|
|
renamed.write_text('"""Changed module."""\n')
|
|
wait_for(
|
|
lambda: (
|
|
renamed_output.exists()
|
|
and "Changed module" in renamed_output.read_text()
|
|
)
|
|
)
|
|
renamed.unlink()
|
|
wait_for(lambda: output.is_file() and not renamed_output.exists())
|
|
finally:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=5)
|
|
|
|
|
|
def test_generated_pages_work_with_redirects_and_edit_links(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"mkdocs-autoapi",
|
|
{
|
|
"autoapi_dir": "src",
|
|
"autoapi_ignore": ["**/index.py"],
|
|
},
|
|
extra_plugins=[
|
|
{
|
|
"redirects": {
|
|
"redirect_maps": {
|
|
"old-api.md": "autoapi/sample/public.md",
|
|
"old-home.md": "index.md",
|
|
}
|
|
}
|
|
}
|
|
],
|
|
)
|
|
build(config)
|
|
page = (tmp_path / "site/autoapi/sample/public/index.html").read_text()
|
|
assert (
|
|
"https://example.com/repository/edit/main/src/sample/public.py" in page
|
|
)
|
|
assert (tmp_path / "site/old-api/index.html").exists()
|
|
assert (tmp_path / "site/old-home/index.html").exists()
|
|
|
|
|
|
def test_autoapi_can_link_retained_docs_without_generating(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"mkdocs-autoapi",
|
|
{
|
|
"autoapi_generate_api_docs": False,
|
|
"autoapi_add_nav_entry": "Saved API",
|
|
},
|
|
)
|
|
directory = tmp_path / "docs/autoapi"
|
|
directory.mkdir()
|
|
(directory / "topic.md").write_text("# Saved topic\n")
|
|
(directory / "summary.md").write_text("- [Saved topic](topic.md)\n")
|
|
build(config)
|
|
home = (tmp_path / "site/index.html").read_text()
|
|
assert "Saved API" in home
|
|
assert "autoapi/topic/" in home
|
|
assert not (tmp_path / "site/autoapi/sample").exists()
|
|
|
|
|
|
def test_autonav_single_file_private_modules_and_flat_urls(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/sample"],
|
|
"exclude_private": False,
|
|
"nav_item_prefix": "",
|
|
},
|
|
)
|
|
with config.open("a") as file:
|
|
file.write("use_directory_urls: false\n")
|
|
build(config)
|
|
assert (tmp_path / "site/reference/sample/_private.html").exists()
|
|
page = (tmp_path / "site/reference/sample/public.html").read_text()
|
|
assert "repository/edit/main/docs/reference" not in page
|
|
data = yaml.safe_load(config.read_text())
|
|
data["plugins"][0]["api-autonav"]["modules"] = ["src/standalone.py"]
|
|
(tmp_path / "src/standalone.py").write_text('"""Standalone module."""\n')
|
|
config.write_text(yaml.safe_dump(data))
|
|
build(config)
|
|
assert (tmp_path / "site/reference/standalone.html").exists()
|
|
|
|
|
|
def test_autoapi_uses_vba_identifiers_for_custom_file_patterns(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"mkdocs-autoapi",
|
|
{
|
|
"autoapi_dir": "src",
|
|
"autoapi_file_patterns": ["*.bas"],
|
|
"autoapi_keep_files": True,
|
|
},
|
|
)
|
|
(tmp_path / "src/sample/code.bas").write_text("Sub Example()\nEnd Sub\n")
|
|
data = yaml.safe_load(config.read_text())
|
|
data["plugins"][1]["mkdocstrings"].update(
|
|
enabled=False, default_handler="vba"
|
|
)
|
|
config.write_text(yaml.safe_dump(data))
|
|
build(config)
|
|
assert (
|
|
tmp_path / "docs/autoapi/sample/code.md"
|
|
).read_text() == "::: sample/code.bas\n"
|
|
|
|
|
|
def test_autonav_preserves_root_order_and_uses_last_matching_options(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{
|
|
"modules": ["src/zeta", "src/alpha"],
|
|
"nav_item_prefix": "",
|
|
"module_options": {
|
|
"zeta": {"heading_level": 3},
|
|
".*": {"heading_level": 2, "show_root_heading": True},
|
|
},
|
|
},
|
|
nav=["API Reference"],
|
|
)
|
|
for name in ("zeta", "alpha"):
|
|
directory = tmp_path / "src" / name
|
|
directory.mkdir()
|
|
(directory / "__init__.py").write_text(f'"""{name} docs."""\n')
|
|
build(config)
|
|
home = (tmp_path / "site/index.html").read_text()
|
|
assert home.index('reference/zeta/" class="md-nav__link"') < home.index(
|
|
'reference/alpha/" class="md-nav__link"'
|
|
)
|
|
page = (tmp_path / "site/reference/zeta/index.html").read_text()
|
|
assert '<h2 id="zeta"' in page
|
|
|
|
|
|
def test_generators_replace_physical_collisions_and_inherit_metadata(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
config = project(
|
|
tmp_path,
|
|
"api-autonav",
|
|
{"modules": ["src/sample"]},
|
|
extra_plugins=["meta"],
|
|
)
|
|
directory = tmp_path / "docs/reference/sample"
|
|
directory.mkdir(parents=True)
|
|
(directory / "public.md").write_text("# Physical collision\n")
|
|
(directory / ".meta.yml").write_text(
|
|
"description: Inherited API description\n"
|
|
)
|
|
build(config)
|
|
page = (tmp_path / "site/reference/sample/public/index.html").read_text()
|
|
assert "Public module documentation" in page
|
|
assert "Physical collision" not in page
|
|
assert "Inherited API description" in page
|
|
|
|
|
|
def test_disabled_api_generators_leave_docs_unchanged(tmp_path: Path) -> None:
|
|
config = project(tmp_path, "mkdocs-autoapi", {"enabled": False})
|
|
build(config)
|
|
assert not (tmp_path / "site/autoapi").exists()
|