From 7f1eb29f4821da55a9a692e165cdf29a92e79753 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Fri, 27 Feb 2026 17:06:30 +0100 Subject: [PATCH 1/5] extend/merge sphinx config instead of overwriting it fixes #416 --- .../score_draw_uml_funcs/__init__.py | 3 +- src/extensions/score_layout/__init__.py | 20 ++++++--- src/extensions/score_metamodel/__init__.py | 23 ++++++---- src/extensions/score_plantuml.py | 12 +++-- .../score_source_code_linker/__init__.py | 7 +-- .../score_sphinx_bundle/__init__.py | 44 ++++++++++--------- src/extensions/score_sync_toml/__init__.py | 25 +++++++---- 7 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/extensions/score_draw_uml_funcs/__init__.py b/src/extensions/score_draw_uml_funcs/__init__.py index 8276b80f..d4a95f39 100644 --- a/src/extensions/score_draw_uml_funcs/__init__.py +++ b/src/extensions/score_draw_uml_funcs/__init__.py @@ -60,7 +60,8 @@ def setup(app: Sphinx) -> dict[str, object]: - app.config.needs_render_context = draw_uml_function_context + for key, value in draw_uml_function_context.items(): + app.config.needs_render_context.setdefault(key, value) return { "version": "0.1", "parallel_read_safe": True, diff --git a/src/extensions/score_layout/__init__.py b/src/extensions/score_layout/__init__.py index 6d54f0b7..d9224cfc 100644 --- a/src/extensions/score_layout/__init__.py +++ b/src/extensions/score_layout/__init__.py @@ -35,11 +35,19 @@ def setup(app: Sphinx) -> dict[str, str | bool]: def update_config(app: Sphinx, _config: Any): logger.debug("score_layout update_config called") - app.config.needs_layouts = sphinx_options.needs_layouts - app.config.needs_global_options = sphinx_options.needs_global_options - app.config.html_theme = html_options.html_theme - app.config.html_context = html_options.return_html_context(app) - app.config.html_theme_options = html_options.return_html_theme_options(app) + # Merge: user's entries take precedence over our defaults + app.config.needs_layouts = {**sphinx_options.needs_layouts, **app.config.needs_layouts} + app.config.needs_global_options = { + **sphinx_options.needs_global_options, + **app.config.needs_global_options, + } + if "html_theme" not in app.config._raw_config: + app.config.html_theme = html_options.html_theme + app.config.html_context = {**html_options.return_html_context(app), **app.config.html_context} + app.config.html_theme_options = { + **html_options.return_html_theme_options(app), + **app.config.html_theme_options, + } logger.debug(f"score_layout __file__: {__file__}") @@ -49,7 +57,7 @@ def update_config(app: Sphinx, _config: Any): app.config.html_static_path.append(str(score_layout_path / "assets")) puml = score_layout_path / "assets" / "puml-theme-score.puml" - app.config.needs_flow_configs = {"score_config": f"!include {puml}"} + app.config.needs_flow_configs.setdefault("score_config", f"!include {puml}") app.add_css_file("css/score.css", priority=500) app.add_css_file("css/score_needs.css", priority=500) diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 0a6c4dae..161d4947 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -231,25 +231,30 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]): def setup(app: Sphinx) -> dict[str, str | bool]: app.add_config_value("external_needs_source", "", rebuild="env") - app.config.needs_id_required = True - app.config.needs_id_regex = "^[A-Za-z0-9_-]{6,}" + if "needs_id_required" not in app.config._raw_config: + app.config.needs_id_required = True + if "needs_id_regex" not in app.config._raw_config: + app.config.needs_id_regex = "^[A-Za-z0-9_-]{6,}" # load metamodel.yaml via ruamel.yaml metamodel = load_metamodel_data() - # Assign everything to Sphinx config - app.config.needs_types = metamodel.needs_types - app.config.needs_extra_links = metamodel.needs_extra_links - app.config.needs_extra_options = metamodel.needs_extra_options + # Extend sphinx-needs config rather than overwriting + app.config.needs_types += metamodel.needs_types + app.config.needs_extra_links += metamodel.needs_extra_links + app.config.needs_extra_options += metamodel.needs_extra_options app.config.graph_checks = metamodel.needs_graph_check app.config.prohibited_words_checks = metamodel.prohibited_words_checks # app.config.stop_words = metamodel["stop_words"] # app.config.weak_words = metamodel["weak_words"] # Ensure that 'needs.json' is always build. - app.config.needs_build_json = True - app.config.needs_reproducible_json = True - app.config.needs_json_remove_defaults = True + if "needs_build_json" not in app.config._raw_config: + app.config.needs_build_json = True + if "needs_reproducible_json" not in app.config._raw_config: + app.config.needs_reproducible_json = True + if "needs_json_remove_defaults" not in app.config._raw_config: + app.config.needs_json_remove_defaults = True # sphinx-collections runs on default prio 500. # We need to populate the sphinx-collections config before that happens. diff --git a/src/extensions/score_plantuml.py b/src/extensions/score_plantuml.py index 3dbbd138..9d045710 100644 --- a/src/extensions/score_plantuml.py +++ b/src/extensions/score_plantuml.py @@ -53,10 +53,14 @@ def find_correct_path(runfiles: Path) -> Path: def setup(app: Sphinx): - app.config.plantuml = str(find_correct_path(get_runfiles_dir())) - app.config.plantuml_output_format = "svg_obj" - app.config.plantuml_syntax_error_image = True - app.config.needs_build_needumls = "_plantuml_sources" + if not app.config.plantuml: + app.config.plantuml = str(find_correct_path(get_runfiles_dir())) + if "plantuml_output_format" not in app.config._raw_config: + app.config.plantuml_output_format = "svg_obj" + if "plantuml_syntax_error_image" not in app.config._raw_config: + app.config.plantuml_syntax_error_image = True + if "needs_build_needumls" not in app.config._raw_config: + app.config.needs_build_needumls = "_plantuml_sources" logger.debug(f"PlantUML binary found at {app.config.plantuml}") diff --git a/src/extensions/score_source_code_linker/__init__.py b/src/extensions/score_source_code_linker/__init__.py index 094ebf4a..cf9843dc 100644 --- a/src/extensions/score_source_code_linker/__init__.py +++ b/src/extensions/score_source_code_linker/__init__.py @@ -173,14 +173,15 @@ def setup_source_code_linker(app: Sphinx, ws_root: Path): # Define need_string_links here to not have it in conf.py # source_code_link and testlinks have the same schema - app.config.needs_string_links = { - "source_code_linker": { + app.config.needs_string_links.setdefault( + "source_code_linker", + { "regex": r"(?P.+)<>(?P.+)", "link_url": "{{url}}", "link_name": "{{name}}", "options": ["source_code_link", "testlink"], }, - } + ) score_sourcelinks_json = os.environ.get("SCORE_SOURCELINKS") if score_sourcelinks_json: diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index d30df6c3..7f4d3e63 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -33,43 +33,45 @@ def setup(app: Sphinx) -> dict[str, object]: - app.config.html_copy_source = False - app.config.html_show_sourcelink = False + if "html_copy_source" not in app.config._raw_config: + app.config.html_copy_source = False + if "html_show_sourcelink" not in app.config._raw_config: + app.config.html_show_sourcelink = False # Global settings # Note: the "sub-extensions" also set their own config values # Same as current VS Code extension - app.config.mermaid_version = "11.6.0" + if "mermaid_version" not in app.config._raw_config: + app.config.mermaid_version = "11.6.0" - # enable "..."-syntax in markdown - app.config.myst_enable_extensions = ["colon_fence"] - - app.config.exclude_patterns = [ - # The following entries are not required when building the documentation via - # 'bazel build //:docs', as that command runs in a sandboxed environment. - # However, when building the documentation via 'bazel run //:docs' or esbonio, - # these entries are required to prevent the build from failing. - "bazel-*", - ".venv*", - ] + # The following entries are not required when building the documentation via + # 'bazel build //:docs', as that command runs in a sandboxed environment. + # However, when building the documentation via 'bazel run //:docs' or esbonio, + # these entries are required to prevent the build from failing. + app.config.exclude_patterns += ["bazel-*", ".venv*"] # Enable markdown rendering - app.config.source_suffix = { - ".rst": "restructuredtext", - ".md": "markdown", - } + app.config.source_suffix.setdefault(".rst", "restructuredtext") + app.config.source_suffix.setdefault(".md", "markdown") - app.config.templates_path = ["templates"] + if "templates" not in app.config.templates_path: + app.config.templates_path += ["templates"] - app.config.numfig = True + if "numfig" not in app.config._raw_config: + app.config.numfig = True - app.config.author = "S-CORE" + if not app.config.author: + app.config.author = "S-CORE" # Load the actual extensions list for e in score_extensions: app.setup_extension(e) + # enable "..."-syntax in markdown — must come after myst_parser is loaded above + if "colon_fence" not in app.config.myst_enable_extensions: + app.config.myst_enable_extensions = set(app.config.myst_enable_extensions) | {"colon_fence"} + return { "version": "3.0.0", # Keep this in sync with the score_docs_as_code version in MODULE.bazel diff --git a/src/extensions/score_sync_toml/__init__.py b/src/extensions/score_sync_toml/__init__.py index 79ebfb7a..3f72b96f 100644 --- a/src/extensions/score_sync_toml/__init__.py +++ b/src/extensions/score_sync_toml/__init__.py @@ -22,29 +22,36 @@ def setup(app: Sphinx) -> dict[str, str | bool]: See https://needs-config-writer.useblocks.com """ - app.config.needscfg_outpath = "ubproject.toml" + if "needscfg_outpath" not in app.config._raw_config: + app.config.needscfg_outpath = "ubproject.toml" """Write to the confdir directory.""" - app.config.needscfg_overwrite = True + if "needscfg_overwrite" not in app.config._raw_config: + app.config.needscfg_overwrite = True """Any changes to the shared/local configuration updates the generated config.""" - app.config.needscfg_write_all = True + if "needscfg_write_all" not in app.config._raw_config: + app.config.needscfg_write_all = True """Write full config, so the final configuration is visible in one file.""" - app.config.needscfg_exclude_defaults = True + if "needscfg_exclude_defaults" not in app.config._raw_config: + app.config.needscfg_exclude_defaults = True """Exclude default values from the generated configuration.""" # This is disabled for right now as it causes a lot of issues # While we are not using the generated file anywhere - app.config.needscfg_warn_on_diff = False + if "needscfg_warn_on_diff" not in app.config._raw_config: + app.config.needscfg_warn_on_diff = False """Running Sphinx with -W will fail the CI for uncommitted TOML changes.""" - app.config.needscfg_merge_toml_files = [ - str(Path(__file__).parent / "shared.toml"), - ] + app.config.needscfg_merge_toml_files = ( + app.config.needscfg_merge_toml_files or [] + ) + [str(Path(__file__).parent / "shared.toml")] """Merge the static TOML file into the generated configuration.""" - app.config.needscfg_relative_path_fields = [ + app.config.needscfg_relative_path_fields = ( + app.config.needscfg_relative_path_fields or [] + ) + [ "needs_external_needs[*].json_path", { "field": "needs_flow_configs.score_config", From 52261292a55287ff2232ec1c2b423084c274f33f Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Fri, 27 Feb 2026 17:14:32 +0100 Subject: [PATCH 2/5] when playing dirty, at least only do it in one place --- src/extensions/score_layout/__init__.py | 5 +++-- src/extensions/score_metamodel/__init__.py | 17 +++++++---------- src/extensions/score_plantuml.py | 11 ++++------- src/extensions/score_sphinx_bundle/__init__.py | 14 ++++++-------- src/extensions/score_sync_toml/__init__.py | 17 +++++++---------- src/helper_lib/__init__.py | 12 ++++++++++++ 6 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/extensions/score_layout/__init__.py b/src/extensions/score_layout/__init__.py index d9224cfc..62ec548f 100644 --- a/src/extensions/score_layout/__init__.py +++ b/src/extensions/score_layout/__init__.py @@ -18,6 +18,8 @@ import sphinx_options from sphinx.application import Sphinx +from src.helper_lib import config_setdefault + logger = logging.getLogger(__name__) @@ -41,8 +43,7 @@ def update_config(app: Sphinx, _config: Any): **sphinx_options.needs_global_options, **app.config.needs_global_options, } - if "html_theme" not in app.config._raw_config: - app.config.html_theme = html_options.html_theme + config_setdefault(app.config, "html_theme", html_options.html_theme) app.config.html_context = {**html_options.return_html_context(app), **app.config.html_context} app.config.html_theme_options = { **html_options.return_html_theme_options(app), diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index 161d4947..fddff21f 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -18,6 +18,8 @@ from sphinx.application import Sphinx from sphinx_needs import logging + +from src.helper_lib import config_setdefault from sphinx_needs.data import NeedsView, SphinxNeedsData from sphinx_needs.need_item import NeedItem @@ -231,10 +233,8 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]): def setup(app: Sphinx) -> dict[str, str | bool]: app.add_config_value("external_needs_source", "", rebuild="env") - if "needs_id_required" not in app.config._raw_config: - app.config.needs_id_required = True - if "needs_id_regex" not in app.config._raw_config: - app.config.needs_id_regex = "^[A-Za-z0-9_-]{6,}" + config_setdefault(app.config, "needs_id_required", True) + config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}") # load metamodel.yaml via ruamel.yaml metamodel = load_metamodel_data() @@ -249,12 +249,9 @@ def setup(app: Sphinx) -> dict[str, str | bool]: # app.config.stop_words = metamodel["stop_words"] # app.config.weak_words = metamodel["weak_words"] # Ensure that 'needs.json' is always build. - if "needs_build_json" not in app.config._raw_config: - app.config.needs_build_json = True - if "needs_reproducible_json" not in app.config._raw_config: - app.config.needs_reproducible_json = True - if "needs_json_remove_defaults" not in app.config._raw_config: - app.config.needs_json_remove_defaults = True + config_setdefault(app.config, "needs_build_json", True) + config_setdefault(app.config, "needs_reproducible_json", True) + config_setdefault(app.config, "needs_json_remove_defaults", True) # sphinx-collections runs on default prio 500. # We need to populate the sphinx-collections config before that happens. diff --git a/src/extensions/score_plantuml.py b/src/extensions/score_plantuml.py index 9d045710..b46b51aa 100644 --- a/src/extensions/score_plantuml.py +++ b/src/extensions/score_plantuml.py @@ -29,7 +29,7 @@ from sphinx.application import Sphinx from sphinx.util import logging -from src.helper_lib import get_runfiles_dir +from src.helper_lib import config_setdefault, get_runfiles_dir logger = logging.getLogger(__name__) @@ -55,12 +55,9 @@ def find_correct_path(runfiles: Path) -> Path: def setup(app: Sphinx): if not app.config.plantuml: app.config.plantuml = str(find_correct_path(get_runfiles_dir())) - if "plantuml_output_format" not in app.config._raw_config: - app.config.plantuml_output_format = "svg_obj" - if "plantuml_syntax_error_image" not in app.config._raw_config: - app.config.plantuml_syntax_error_image = True - if "needs_build_needumls" not in app.config._raw_config: - app.config.needs_build_needumls = "_plantuml_sources" + config_setdefault(app.config, "plantuml_output_format", "svg_obj") + config_setdefault(app.config, "plantuml_syntax_error_image", True) + config_setdefault(app.config, "needs_build_needumls", "_plantuml_sources") logger.debug(f"PlantUML binary found at {app.config.plantuml}") diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index 7f4d3e63..9dfd8680 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -12,6 +12,8 @@ # ******************************************************************************* from sphinx.application import Sphinx +from src.helper_lib import config_setdefault + # Note: order matters! # Extensions are loaded in this order. # e.g. plantuml MUST be loaded before sphinx-needs @@ -33,17 +35,14 @@ def setup(app: Sphinx) -> dict[str, object]: - if "html_copy_source" not in app.config._raw_config: - app.config.html_copy_source = False - if "html_show_sourcelink" not in app.config._raw_config: - app.config.html_show_sourcelink = False + config_setdefault(app.config, "html_copy_source", False) + config_setdefault(app.config, "html_show_sourcelink", False) # Global settings # Note: the "sub-extensions" also set their own config values # Same as current VS Code extension - if "mermaid_version" not in app.config._raw_config: - app.config.mermaid_version = "11.6.0" + config_setdefault(app.config, "mermaid_version", "11.6.0") # The following entries are not required when building the documentation via # 'bazel build //:docs', as that command runs in a sandboxed environment. @@ -58,8 +57,7 @@ def setup(app: Sphinx) -> dict[str, object]: if "templates" not in app.config.templates_path: app.config.templates_path += ["templates"] - if "numfig" not in app.config._raw_config: - app.config.numfig = True + config_setdefault(app.config, "numfig", True) if not app.config.author: app.config.author = "S-CORE" diff --git a/src/extensions/score_sync_toml/__init__.py b/src/extensions/score_sync_toml/__init__.py index 3f72b96f..069e017c 100644 --- a/src/extensions/score_sync_toml/__init__.py +++ b/src/extensions/score_sync_toml/__init__.py @@ -14,6 +14,8 @@ from sphinx.application import Sphinx +from src.helper_lib import config_setdefault + def setup(app: Sphinx) -> dict[str, str | bool]: """ @@ -22,26 +24,21 @@ def setup(app: Sphinx) -> dict[str, str | bool]: See https://needs-config-writer.useblocks.com """ - if "needscfg_outpath" not in app.config._raw_config: - app.config.needscfg_outpath = "ubproject.toml" + config_setdefault(app.config, "needscfg_outpath", "ubproject.toml") """Write to the confdir directory.""" - if "needscfg_overwrite" not in app.config._raw_config: - app.config.needscfg_overwrite = True + config_setdefault(app.config, "needscfg_overwrite", True) """Any changes to the shared/local configuration updates the generated config.""" - if "needscfg_write_all" not in app.config._raw_config: - app.config.needscfg_write_all = True + config_setdefault(app.config, "needscfg_write_all", True) """Write full config, so the final configuration is visible in one file.""" - if "needscfg_exclude_defaults" not in app.config._raw_config: - app.config.needscfg_exclude_defaults = True + config_setdefault(app.config, "needscfg_exclude_defaults", True) """Exclude default values from the generated configuration.""" # This is disabled for right now as it causes a lot of issues # While we are not using the generated file anywhere - if "needscfg_warn_on_diff" not in app.config._raw_config: - app.config.needscfg_warn_on_diff = False + config_setdefault(app.config, "needscfg_warn_on_diff", False) """Running Sphinx with -W will fail the CI for uncommitted TOML changes.""" app.config.needscfg_merge_toml_files = ( diff --git a/src/helper_lib/__init__.py b/src/helper_lib/__init__.py index 156a50fc..b48536c5 100644 --- a/src/helper_lib/__init__.py +++ b/src/helper_lib/__init__.py @@ -15,13 +15,25 @@ import subprocess import sys from pathlib import Path +from typing import Any from runfiles import Runfiles +from sphinx.config import Config from sphinx_needs.logging import get_logger LOGGER = get_logger(__name__) +def config_setdefault(config: Config, name: str, value: Any) -> None: + """Set a Sphinx config value only if the user hasn't explicitly set it in conf.py.""" + + # Sphinx has no public API for this check. We use ``_raw_config`` which is the + # de-facto standard across the ecosystem (Furo, RTD-theme, etc.). If Sphinx + # ever adds a public alternative, update this single function. + if name not in config._raw_config: + setattr(config, name, value) + + def find_ws_root() -> Path | None: """ Find the current MODULE.bazel workspace root directory. From 1e602f447360932e0d4bdbdb6b5bcee7226bdc7e Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Fri, 27 Feb 2026 17:21:27 +0100 Subject: [PATCH 3/5] add unit tests --- src/helper_lib/test_helper_lib.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/helper_lib/test_helper_lib.py b/src/helper_lib/test_helper_lib.py index 83bbc30e..9878bdbd 100644 --- a/src/helper_lib/test_helper_lib.py +++ b/src/helper_lib/test_helper_lib.py @@ -18,6 +18,7 @@ import pytest from src.helper_lib import ( + config_setdefault, get_current_git_hash, get_github_repo_info, get_runfiles_dir, @@ -25,6 +26,26 @@ ) +class _FakeConfig: + """Minimal stand-in for sphinx.config.Config sufficient to test config_setdefault.""" + + def __init__(self, raw: dict): + self._raw_config = raw + + +def test_config_setdefault_sets_when_not_in_raw_config(): + cfg = _FakeConfig(raw={}) + config_setdefault(cfg, "html_copy_source", False) + assert cfg.html_copy_source is False + + +def test_config_setdefault_does_not_overwrite_user_value(): + cfg = _FakeConfig(raw={"html_copy_source": True}) + cfg.html_copy_source = True + config_setdefault(cfg, "html_copy_source", False) + assert cfg.html_copy_source is True + + @pytest.fixture def temp_dir(): """Create a temporary directory for tests.""" From d92cfb0544f68ff0bee22929ca3f2b3da87dade6 Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Mon, 2 Mar 2026 09:08:03 +0100 Subject: [PATCH 4/5] Fix linter warnings --- src/extensions/score_layout/__init__.py | 6 ++++-- src/extensions/score_metamodel/__init__.py | 3 +-- src/extensions/score_sphinx_bundle/__init__.py | 2 +- src/helper_lib/__init__.py | 4 ++-- src/helper_lib/test_helper_lib.py | 12 ++++++------ 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/extensions/score_layout/__init__.py b/src/extensions/score_layout/__init__.py index 62ec548f..1e8c2d6e 100644 --- a/src/extensions/score_layout/__init__.py +++ b/src/extensions/score_layout/__init__.py @@ -38,13 +38,15 @@ def update_config(app: Sphinx, _config: Any): logger.debug("score_layout update_config called") # Merge: user's entries take precedence over our defaults - app.config.needs_layouts = {**sphinx_options.needs_layouts, **app.config.needs_layouts} + app.config.needs_layouts = {**sphinx_options.needs_layouts, + **app.config.needs_layouts} app.config.needs_global_options = { **sphinx_options.needs_global_options, **app.config.needs_global_options, } config_setdefault(app.config, "html_theme", html_options.html_theme) - app.config.html_context = {**html_options.return_html_context(app), **app.config.html_context} + app.config.html_context = {**html_options.return_html_context(app), + **app.config.html_context} app.config.html_theme_options = { **html_options.return_html_theme_options(app), **app.config.html_theme_options, diff --git a/src/extensions/score_metamodel/__init__.py b/src/extensions/score_metamodel/__init__.py index fddff21f..220cb767 100644 --- a/src/extensions/score_metamodel/__init__.py +++ b/src/extensions/score_metamodel/__init__.py @@ -18,8 +18,6 @@ from sphinx.application import Sphinx from sphinx_needs import logging - -from src.helper_lib import config_setdefault from sphinx_needs.data import NeedsView, SphinxNeedsData from sphinx_needs.need_item import NeedItem @@ -39,6 +37,7 @@ from src.extensions.score_metamodel.yaml_parser import ( load_metamodel_data as load_metamodel_data, ) +from src.helper_lib import config_setdefault logger = logging.get_logger(__name__) diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index 9dfd8680..46732935 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -68,7 +68,7 @@ def setup(app: Sphinx) -> dict[str, object]: # enable "..."-syntax in markdown — must come after myst_parser is loaded above if "colon_fence" not in app.config.myst_enable_extensions: - app.config.myst_enable_extensions = set(app.config.myst_enable_extensions) | {"colon_fence"} + app.config.myst_enable_extensions |= {"colon_fence"} return { "version": "3.0.0", diff --git a/src/helper_lib/__init__.py b/src/helper_lib/__init__.py index b48536c5..5699e478 100644 --- a/src/helper_lib/__init__.py +++ b/src/helper_lib/__init__.py @@ -25,12 +25,12 @@ def config_setdefault(config: Config, name: str, value: Any) -> None: - """Set a Sphinx config value only if the user hasn't explicitly set it in conf.py.""" + """Set a Sphinx config value only if not explicitly set it in conf.py.""" # Sphinx has no public API for this check. We use ``_raw_config`` which is the # de-facto standard across the ecosystem (Furo, RTD-theme, etc.). If Sphinx # ever adds a public alternative, update this single function. - if name not in config._raw_config: + if name not in config._raw_config: # pyright: ignore [reportPrivateUsage] setattr(config, name, value) diff --git a/src/helper_lib/test_helper_lib.py b/src/helper_lib/test_helper_lib.py index 9878bdbd..0486821a 100644 --- a/src/helper_lib/test_helper_lib.py +++ b/src/helper_lib/test_helper_lib.py @@ -27,7 +27,7 @@ class _FakeConfig: - """Minimal stand-in for sphinx.config.Config sufficient to test config_setdefault.""" + """Minimal stand-in for sphinx.config.Config.""" def __init__(self, raw: dict): self._raw_config = raw @@ -35,15 +35,15 @@ def __init__(self, raw: dict): def test_config_setdefault_sets_when_not_in_raw_config(): cfg = _FakeConfig(raw={}) - config_setdefault(cfg, "html_copy_source", False) - assert cfg.html_copy_source is False + config_setdefault(cfg, "html_copy_source", False) # pyright: ignore [reportArgumentType] + assert cfg.html_copy_source is False # pyright: ignore [reportAttributeAccessIssue] def test_config_setdefault_does_not_overwrite_user_value(): cfg = _FakeConfig(raw={"html_copy_source": True}) - cfg.html_copy_source = True - config_setdefault(cfg, "html_copy_source", False) - assert cfg.html_copy_source is True + cfg.html_copy_source = True # pyright: ignore[reportAttributeAccessIssue] + config_setdefault(cfg, "html_copy_source", False) # pyright: ignore [reportArgumentType] + assert cfg.html_copy_source is True # pyright: ignore[reportAttributeAccessIssue] @pytest.fixture From e5569ecf87a102a4097cbf3821cb738875c694bc Mon Sep 17 00:00:00 2001 From: Andreas Zwinkau Date: Mon, 2 Mar 2026 16:44:23 +0100 Subject: [PATCH 5/5] Fix review feedback from Copilot --- src/extensions/score_layout/__init__.py | 12 ++++++---- src/extensions/score_plantuml.py | 4 ++-- .../score_sphinx_bundle/__init__.py | 15 ++++++++---- src/extensions/score_sync_toml/__init__.py | 24 +++++++++---------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/extensions/score_layout/__init__.py b/src/extensions/score_layout/__init__.py index 1e8c2d6e..ce7b128c 100644 --- a/src/extensions/score_layout/__init__.py +++ b/src/extensions/score_layout/__init__.py @@ -38,15 +38,19 @@ def update_config(app: Sphinx, _config: Any): logger.debug("score_layout update_config called") # Merge: user's entries take precedence over our defaults - app.config.needs_layouts = {**sphinx_options.needs_layouts, - **app.config.needs_layouts} + app.config.needs_layouts = { + **sphinx_options.needs_layouts, + **app.config.needs_layouts, + } app.config.needs_global_options = { **sphinx_options.needs_global_options, **app.config.needs_global_options, } config_setdefault(app.config, "html_theme", html_options.html_theme) - app.config.html_context = {**html_options.return_html_context(app), - **app.config.html_context} + app.config.html_context = { + **html_options.return_html_context(app), + **app.config.html_context, + } app.config.html_theme_options = { **html_options.return_html_theme_options(app), **app.config.html_theme_options, diff --git a/src/extensions/score_plantuml.py b/src/extensions/score_plantuml.py index b46b51aa..d3d2854c 100644 --- a/src/extensions/score_plantuml.py +++ b/src/extensions/score_plantuml.py @@ -53,8 +53,8 @@ def find_correct_path(runfiles: Path) -> Path: def setup(app: Sphinx): - if not app.config.plantuml: - app.config.plantuml = str(find_correct_path(get_runfiles_dir())) + # we must overwrite the plantuml path due to Bazel + app.config.plantuml = str(find_correct_path(get_runfiles_dir())) config_setdefault(app.config, "plantuml_output_format", "svg_obj") config_setdefault(app.config, "plantuml_syntax_error_image", True) config_setdefault(app.config, "needs_build_needumls", "_plantuml_sources") diff --git a/src/extensions/score_sphinx_bundle/__init__.py b/src/extensions/score_sphinx_bundle/__init__.py index 46732935..6ae04008 100644 --- a/src/extensions/score_sphinx_bundle/__init__.py +++ b/src/extensions/score_sphinx_bundle/__init__.py @@ -58,17 +58,22 @@ def setup(app: Sphinx) -> dict[str, object]: app.config.templates_path += ["templates"] config_setdefault(app.config, "numfig", True) - - if not app.config.author: - app.config.author = "S-CORE" + config_setdefault(app.config, "author", "S-CORE") # Load the actual extensions list for e in score_extensions: app.setup_extension(e) # enable "..."-syntax in markdown — must come after myst_parser is loaded above - if "colon_fence" not in app.config.myst_enable_extensions: - app.config.myst_enable_extensions |= {"colon_fence"} + if isinstance(app.config.myst_enable_extensions, list): + app.config.myst_enable_extensions.append("colon_fence") + elif isinstance(app.config.myst_enable_extensions, set): + app.config.myst_enable_extensions.add("colon_fence") + else: + print( + "Unexpected type for myst_enable_extensions: %s", + type(app.config.myst_enable_extensions), + ) return { "version": "3.0.0", diff --git a/src/extensions/score_sync_toml/__init__.py b/src/extensions/score_sync_toml/__init__.py index 069e017c..42709b21 100644 --- a/src/extensions/score_sync_toml/__init__.py +++ b/src/extensions/score_sync_toml/__init__.py @@ -41,20 +41,20 @@ def setup(app: Sphinx) -> dict[str, str | bool]: config_setdefault(app.config, "needscfg_warn_on_diff", False) """Running Sphinx with -W will fail the CI for uncommitted TOML changes.""" - app.config.needscfg_merge_toml_files = ( - app.config.needscfg_merge_toml_files or [] - ) + [str(Path(__file__).parent / "shared.toml")] + app.config.needscfg_merge_toml_files.append( + str(Path(__file__).parent / "shared.toml") + ) """Merge the static TOML file into the generated configuration.""" - app.config.needscfg_relative_path_fields = ( - app.config.needscfg_relative_path_fields or [] - ) + [ - "needs_external_needs[*].json_path", - { - "field": "needs_flow_configs.score_config", - "prefix": "!include ", - }, - ] + app.config.needscfg_relative_path_fields.extend( + [ + "needs_external_needs[*].json_path", + { + "field": "needs_flow_configs.score_config", + "prefix": "!include ", + }, + ] + ) """Relative paths to confdir for Bazel provided absolute paths.""" app.config.suppress_warnings += [