From db9731160f934daaa8aa56086b2058144abb4b5d Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:00:21 -0400 Subject: [PATCH] Fix config lookup: use 'is None' instead of 'or default', add get_config_flex and html_extra_path support - Add get_config_flex() to accept both hyphens and underscores in config keys - Replace 'or default' pattern with 'is None' check in configuration_args loop to avoid overwriting intentional falsy values (False, [], 0, etc.) - Switch use_autoapi, autoapi_ignore, custom_css, custom_js lookups to get_config_flex for flexible key matching - Add html_extra_path support in build.py and conf.py.j2, with relative-to- absolute path conversion - Add tests for get_config_flex and html_extra_path --- docs/examples/example.html | 11 +++ pyproject.toml | 1 + yardang/build.py | 24 +++-- yardang/conf.py.j2 | 1 + yardang/tests/test_all.py | 186 +++++++++++++++++++++++++++++++++++++ yardang/utils.py | 25 ++++- 6 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 docs/examples/example.html diff --git a/docs/examples/example.html b/docs/examples/example.html new file mode 100644 index 0000000..e6f4a7e --- /dev/null +++ b/docs/examples/example.html @@ -0,0 +1,11 @@ + + + + +Example Extra Page + + +

Example Extra Page

+

This standalone HTML file is served via html_extra_path.

+ + diff --git a/pyproject.toml b/pyproject.toml index 1fda771..0b4dfd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/yardang/build.py b/yardang/build.py index 14e40c4..be3ca1a 100644 --- a/yardang/build.py +++ b/yardang/build.py @@ -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") @@ -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 @@ -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": [], @@ -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" @@ -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"]: diff --git a/yardang/conf.py.j2 b/yardang/conf.py.j2 index 322a418..6b6cfd6 100644 --- a/yardang/conf.py.j2 +++ b/yardang/conf.py.j2 @@ -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}}, diff --git a/yardang/tests/test_all.py b/yardang/tests/test_all.py index 0b3edce..5660a31 100644 --- a/yardang/tests/test_all.py +++ b/yardang/tests/test_all.py @@ -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(): @@ -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("test") + + 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("test") + + 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) diff --git a/yardang/utils.py b/yardang/utils.py index 94ca6f8..e6dd8d3 100644 --- a/yardang/utils.py +++ b/yardang/utils.py @@ -3,7 +3,7 @@ import toml -__all__ = ("get_config",) +__all__ = ("get_config", "get_config_flex") def get_pyproject_toml(): @@ -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