Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/examples/example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example Extra Page</title>
</head>
<body>
<h1>Example Extra Page</h1>
<p>This standalone HTML file is served via <code>html_extra_path</code>.</p>
</body>
</html>
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ pages = [
"docs/src/api.md",
]
use-autoapi = false
html_extra_path = ["docs/examples"]

[tool.yardang.breathe]
projects = { calculator = "examples/cpp/xml" }
Expand Down
24 changes: 18 additions & 6 deletions yardang/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from jinja2 import Environment, FileSystemLoader

from .utils import get_config
from .utils import get_config, get_config_flex

__all__ = ("generate_docs_configuration", "run_doxygen_if_needed", "generate_wiki_configuration")

Expand Down Expand Up @@ -239,16 +239,18 @@ def customize(args):
root = root if root is not None else get_config(section="root", base=config_base)
cname = cname if cname is not None else get_config(section="cname", base=config_base)
pages = pages if pages is not None else get_config(section="pages", base=config_base) or []
use_autoapi = use_autoapi if use_autoapi is not None else get_config(section="use-autoapi", base=config_base)
autoapi_ignore = autoapi_ignore if autoapi_ignore is not None else get_config(section="docs.autoapi-ignore")
use_autoapi = use_autoapi if use_autoapi is not None else get_config_flex(section="use-autoapi", base=config_base)
autoapi_ignore = autoapi_ignore if autoapi_ignore is not None else get_config_flex(section="autoapi-ignore", base=config_base)

custom_css = (
custom_css
if custom_css is not None
else Path(get_config(section="custom-css", base=config_base) or (Path(__file__).parent / "custom.css"))
else Path(get_config_flex(section="custom-css", base=config_base) or (Path(__file__).parent / "custom.css"))
)
custom_js = (
custom_js if custom_js is not None else Path(get_config(section="custom-js", base=config_base) or (Path(__file__).parent / "custom.js"))
custom_js
if custom_js is not None
else Path(get_config_flex(section="custom-js", base=config_base) or (Path(__file__).parent / "custom.js"))
)

# if custom_css and custom_js are strings and they exist as paths, read them as Paths
Expand Down Expand Up @@ -280,6 +282,7 @@ def customize(args):
# sphinx generic
"html_theme_options": {},
"html_static_path": [],
"html_extra_path": [],
"html_css_files": [],
"html_js_files": [],
"source_suffix": [],
Expand Down Expand Up @@ -357,7 +360,9 @@ def customize(args):
# sphinx-reredirects
"redirects": {},
}.items():
configuration_args[config_option] = get_config(section=config_option, base=config_base) or default
configuration_args[config_option] = get_config_flex(section=config_option, base=config_base)
if configuration_args[config_option] is None:
configuration_args[config_option] = default

# Load breathe/doxygen configuration from tool.yardang.breathe
breathe_config_base = f"{config_base}.breathe"
Expand Down Expand Up @@ -397,6 +402,13 @@ def customize(args):
if use_breathe and auto_run_doxygen and breathe_args["breathe_projects"]:
run_doxygen_if_needed(breathe_args["breathe_projects"])

# Convert relative paths in html_extra_path to absolute paths
# This is needed because the conf.py is generated in a temp directory
if configuration_args["html_extra_path"]:
configuration_args["html_extra_path"] = [
str(Path(path).resolve()) if not Path(path).is_absolute() else path for path in configuration_args["html_extra_path"]
]

# Convert relative paths in breathe_projects to absolute paths
# This is needed because the conf.py is generated in a temp directory
if breathe_args["breathe_projects"]:
Expand Down
1 change: 1 addition & 0 deletions yardang/conf.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ os.environ["SPHINX_BUILDING"] = "1"
html_theme = "{{theme}}"
html_theme_options = {{html_theme_options}}
html_static_path = {{html_static_path}}
html_extra_path = {{html_extra_path}}
html_css_files = [
"styles/custom.css",
*{{html_css_files}},
Expand Down
186 changes: 186 additions & 0 deletions yardang/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from yardang.build import generate_docs_configuration
from yardang.cli import build, debug
from yardang.utils import get_config_flex


def test_build():
Expand Down Expand Up @@ -82,3 +83,188 @@ def test_use_autoapi_none_falls_back_to_config(self, tmp_path):
assert "use_autoapi = True" in conf_content
finally:
os.chdir(original_cwd)


class TestGetConfigFlex:
"""Tests for get_config_flex accepting both hyphens and underscores."""

def test_hyphen_key_found(self, tmp_path):
"""Test that hyphenated keys are found."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
html-extra-path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = get_config_flex(section="html-extra-path", base="tool.yardang")
assert result == ["docs/extra"]
finally:
os.chdir(original_cwd)

def test_underscore_key_found(self, tmp_path):
"""Test that underscored keys are found."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
html_extra_path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = get_config_flex(section="html_extra_path", base="tool.yardang")
assert result == ["docs/extra"]
finally:
os.chdir(original_cwd)

def test_hyphen_key_searched_when_underscore_queried(self, tmp_path):
"""Test that querying with underscores finds hyphenated keys."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
html-extra-path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
# Query with underscores, but TOML uses hyphens
result = get_config_flex(section="html_extra_path", base="tool.yardang")
assert result == ["docs/extra"]
finally:
os.chdir(original_cwd)

def test_underscore_key_searched_when_hyphen_queried(self, tmp_path):
"""Test that querying with hyphens finds underscored keys."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
html_extra_path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
# Query with hyphens, but TOML uses underscores
result = get_config_flex(section="html-extra-path", base="tool.yardang")
assert result == ["docs/extra"]
finally:
os.chdir(original_cwd)

def test_hyphen_takes_precedence(self, tmp_path):
"""Test that hyphenated key takes precedence when both exist."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
html-extra-path = ["from-hyphens"]
html_extra_path = ["from-underscores"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = get_config_flex(section="html_extra_path", base="tool.yardang")
assert result == ["from-hyphens"]
finally:
os.chdir(original_cwd)

def test_missing_key_returns_none(self, tmp_path):
"""Test that missing keys return None."""
pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
title = "Test"
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
result = get_config_flex(section="html_extra_path", base="tool.yardang")
assert result is None
finally:
os.chdir(original_cwd)


class TestHtmlExtraPath:
"""Tests for html_extra_path in generated conf.py."""

def test_html_extra_path_with_hyphens(self, tmp_path):
"""Test that html-extra-path (hyphens) is picked up in generated conf.py."""
extra_dir = tmp_path / "docs" / "extra"
extra_dir.mkdir(parents=True)
(extra_dir / "page.html").write_text("<html><body>test</body></html>")

pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
title = "Test Project"
root = "README.md"
use-autoapi = false
html-extra-path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
(tmp_path / "README.md").write_text("# Test\n\nContent.")

original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
with generate_docs_configuration() as conf_dir:
conf_content = (Path(conf_dir) / "conf.py").read_text()
assert "html_extra_path" in conf_content
assert "docs/extra" in conf_content or "docs\\\\extra" in conf_content or str(extra_dir) in conf_content
finally:
os.chdir(original_cwd)

def test_html_extra_path_with_underscores(self, tmp_path):
"""Test that html_extra_path (underscores) is also accepted."""
extra_dir = tmp_path / "docs" / "extra"
extra_dir.mkdir(parents=True)
(extra_dir / "page.html").write_text("<html><body>test</body></html>")

pyproject_content = """
[project]
name = "test-project"
version = "1.0.0"

[tool.yardang]
title = "Test Project"
root = "README.md"
use-autoapi = false
html_extra_path = ["docs/extra"]
"""
(tmp_path / "pyproject.toml").write_text(pyproject_content)
(tmp_path / "README.md").write_text("# Test\n\nContent.")

original_cwd = os.getcwd()
try:
os.chdir(tmp_path)
with generate_docs_configuration() as conf_dir:
conf_content = (Path(conf_dir) / "conf.py").read_text()
assert "html_extra_path" in conf_content
assert "docs/extra" in conf_content or "docs\\\\extra" in conf_content or str(extra_dir) in conf_content
finally:
os.chdir(original_cwd)
25 changes: 24 additions & 1 deletion yardang/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import toml

__all__ = ("get_config",)
__all__ = ("get_config", "get_config_flex")


def get_pyproject_toml():
Expand All @@ -22,3 +22,26 @@ def get_config(section="", base="tool.yardang"):
if config is None:
return None
return config


def get_config_flex(section="", base="tool.yardang"):
"""Look up a config key, accepting both hyphens and underscores.

Tries the hyphenated form first (TOML convention), then the
underscored form (Sphinx convention). For example, looking up
``html_extra_path`` will try ``html-extra-path`` first, then
``html_extra_path``.
"""
hyphen_key = section.replace("_", "-")
underscore_key = section.replace("-", "_")

# Prefer hyphens (TOML convention)
result = get_config(section=hyphen_key, base=base)
if result is not None:
return result

# Fall back to underscores (Sphinx convention)
if underscore_key != hyphen_key:
return get_config(section=underscore_key, base=base)

return None
Loading