Skip to content

Commit b2e62cd

Browse files
authored
Respect user configuration (#428)
* extend/merge sphinx config instead of overwriting it
1 parent 7f51a58 commit b2e62cd

9 files changed

Lines changed: 121 additions & 60 deletions

File tree

src/extensions/score_draw_uml_funcs/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@
6060

6161

6262
def setup(app: Sphinx) -> dict[str, object]:
63-
app.config.needs_render_context = draw_uml_function_context
63+
for key, value in draw_uml_function_context.items():
64+
app.config.needs_render_context.setdefault(key, value)
6465
return {
6566
"version": "0.1",
6667
"parallel_read_safe": True,

src/extensions/score_layout/__init__.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import sphinx_options
1919
from sphinx.application import Sphinx
2020

21+
from src.helper_lib import config_setdefault
22+
2123
logger = logging.getLogger(__name__)
2224

2325

@@ -35,11 +37,24 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
3537
def update_config(app: Sphinx, _config: Any):
3638
logger.debug("score_layout update_config called")
3739

38-
app.config.needs_layouts = sphinx_options.needs_layouts
39-
app.config.needs_global_options = sphinx_options.needs_global_options
40-
app.config.html_theme = html_options.html_theme
41-
app.config.html_context = html_options.return_html_context(app)
42-
app.config.html_theme_options = html_options.return_html_theme_options(app)
40+
# Merge: user's entries take precedence over our defaults
41+
app.config.needs_layouts = {
42+
**sphinx_options.needs_layouts,
43+
**app.config.needs_layouts,
44+
}
45+
app.config.needs_global_options = {
46+
**sphinx_options.needs_global_options,
47+
**app.config.needs_global_options,
48+
}
49+
config_setdefault(app.config, "html_theme", html_options.html_theme)
50+
app.config.html_context = {
51+
**html_options.return_html_context(app),
52+
**app.config.html_context,
53+
}
54+
app.config.html_theme_options = {
55+
**html_options.return_html_theme_options(app),
56+
**app.config.html_theme_options,
57+
}
4358

4459
logger.debug(f"score_layout __file__: {__file__}")
4560

@@ -49,7 +64,7 @@ def update_config(app: Sphinx, _config: Any):
4964
app.config.html_static_path.append(str(score_layout_path / "assets"))
5065

5166
puml = score_layout_path / "assets" / "puml-theme-score.puml"
52-
app.config.needs_flow_configs = {"score_config": f"!include {puml}"}
67+
app.config.needs_flow_configs.setdefault("score_config", f"!include {puml}")
5368

5469
app.add_css_file("css/score.css", priority=500)
5570
app.add_css_file("css/score_needs.css", priority=500)

src/extensions/score_metamodel/__init__.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from src.extensions.score_metamodel.yaml_parser import (
3838
load_metamodel_data as load_metamodel_data,
3939
)
40+
from src.helper_lib import config_setdefault
4041

4142
logger = logging.get_logger(__name__)
4243

@@ -231,25 +232,25 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]):
231232

232233
def setup(app: Sphinx) -> dict[str, str | bool]:
233234
app.add_config_value("external_needs_source", "", rebuild="env")
234-
app.config.needs_id_required = True
235-
app.config.needs_id_regex = "^[A-Za-z0-9_-]{6,}"
235+
config_setdefault(app.config, "needs_id_required", True)
236+
config_setdefault(app.config, "needs_id_regex", "^[A-Za-z0-9_-]{6,}")
236237

237238
# load metamodel.yaml via ruamel.yaml
238239
metamodel = load_metamodel_data()
239240

240-
# Assign everything to Sphinx config
241-
app.config.needs_types = metamodel.needs_types
242-
app.config.needs_extra_links = metamodel.needs_extra_links
243-
app.config.needs_extra_options = metamodel.needs_extra_options
241+
# Extend sphinx-needs config rather than overwriting
242+
app.config.needs_types += metamodel.needs_types
243+
app.config.needs_extra_links += metamodel.needs_extra_links
244+
app.config.needs_extra_options += metamodel.needs_extra_options
244245
app.config.graph_checks = metamodel.needs_graph_check
245246
app.config.prohibited_words_checks = metamodel.prohibited_words_checks
246247

247248
# app.config.stop_words = metamodel["stop_words"]
248249
# app.config.weak_words = metamodel["weak_words"]
249250
# Ensure that 'needs.json' is always build.
250-
app.config.needs_build_json = True
251-
app.config.needs_reproducible_json = True
252-
app.config.needs_json_remove_defaults = True
251+
config_setdefault(app.config, "needs_build_json", True)
252+
config_setdefault(app.config, "needs_reproducible_json", True)
253+
config_setdefault(app.config, "needs_json_remove_defaults", True)
253254

254255
# sphinx-collections runs on default prio 500.
255256
# We need to populate the sphinx-collections config before that happens.

src/extensions/score_plantuml.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from sphinx.application import Sphinx
3030
from sphinx.util import logging
3131

32-
from src.helper_lib import get_runfiles_dir
32+
from src.helper_lib import config_setdefault, get_runfiles_dir
3333

3434
logger = logging.getLogger(__name__)
3535

@@ -53,10 +53,11 @@ def find_correct_path(runfiles: Path) -> Path:
5353

5454

5555
def setup(app: Sphinx):
56+
# we must overwrite the plantuml path due to Bazel
5657
app.config.plantuml = str(find_correct_path(get_runfiles_dir()))
57-
app.config.plantuml_output_format = "svg_obj"
58-
app.config.plantuml_syntax_error_image = True
59-
app.config.needs_build_needumls = "_plantuml_sources"
58+
config_setdefault(app.config, "plantuml_output_format", "svg_obj")
59+
config_setdefault(app.config, "plantuml_syntax_error_image", True)
60+
config_setdefault(app.config, "needs_build_needumls", "_plantuml_sources")
6061

6162
logger.debug(f"PlantUML binary found at {app.config.plantuml}")
6263

src/extensions/score_source_code_linker/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,15 @@ def setup_source_code_linker(app: Sphinx, ws_root: Path):
173173

174174
# Define need_string_links here to not have it in conf.py
175175
# source_code_link and testlinks have the same schema
176-
app.config.needs_string_links = {
177-
"source_code_linker": {
176+
app.config.needs_string_links.setdefault(
177+
"source_code_linker",
178+
{
178179
"regex": r"(?P<url>.+)<>(?P<name>.+)",
179180
"link_url": "{{url}}",
180181
"link_name": "{{name}}",
181182
"options": ["source_code_link", "testlink"],
182183
},
183-
}
184+
)
184185

185186
score_sourcelinks_json = os.environ.get("SCORE_SOURCELINKS")
186187
if score_sourcelinks_json:

src/extensions/score_sphinx_bundle/__init__.py

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# *******************************************************************************
1313
from sphinx.application import Sphinx
1414

15+
from src.helper_lib import config_setdefault
16+
1517
# Note: order matters!
1618
# Extensions are loaded in this order.
1719
# e.g. plantuml MUST be loaded before sphinx-needs
@@ -33,43 +35,46 @@
3335

3436

3537
def setup(app: Sphinx) -> dict[str, object]:
36-
app.config.html_copy_source = False
37-
app.config.html_show_sourcelink = False
38+
config_setdefault(app.config, "html_copy_source", False)
39+
config_setdefault(app.config, "html_show_sourcelink", False)
3840

3941
# Global settings
4042
# Note: the "sub-extensions" also set their own config values
4143

4244
# Same as current VS Code extension
43-
app.config.mermaid_version = "11.6.0"
44-
45-
# enable "..."-syntax in markdown
46-
app.config.myst_enable_extensions = ["colon_fence"]
45+
config_setdefault(app.config, "mermaid_version", "11.6.0")
4746

48-
app.config.exclude_patterns = [
49-
# The following entries are not required when building the documentation via
50-
# 'bazel build //:docs', as that command runs in a sandboxed environment.
51-
# However, when building the documentation via 'bazel run //:docs' or esbonio,
52-
# these entries are required to prevent the build from failing.
53-
"bazel-*",
54-
".venv*",
55-
]
47+
# The following entries are not required when building the documentation via
48+
# 'bazel build //:docs', as that command runs in a sandboxed environment.
49+
# However, when building the documentation via 'bazel run //:docs' or esbonio,
50+
# these entries are required to prevent the build from failing.
51+
app.config.exclude_patterns += ["bazel-*", ".venv*"]
5652

5753
# Enable markdown rendering
58-
app.config.source_suffix = {
59-
".rst": "restructuredtext",
60-
".md": "markdown",
61-
}
54+
app.config.source_suffix.setdefault(".rst", "restructuredtext")
55+
app.config.source_suffix.setdefault(".md", "markdown")
6256

63-
app.config.templates_path = ["templates"]
57+
if "templates" not in app.config.templates_path:
58+
app.config.templates_path += ["templates"]
6459

65-
app.config.numfig = True
66-
67-
app.config.author = "S-CORE"
60+
config_setdefault(app.config, "numfig", True)
61+
config_setdefault(app.config, "author", "S-CORE")
6862

6963
# Load the actual extensions list
7064
for e in score_extensions:
7165
app.setup_extension(e)
7266

67+
# enable "..."-syntax in markdown — must come after myst_parser is loaded above
68+
if isinstance(app.config.myst_enable_extensions, list):
69+
app.config.myst_enable_extensions.append("colon_fence")
70+
elif isinstance(app.config.myst_enable_extensions, set):
71+
app.config.myst_enable_extensions.add("colon_fence")
72+
else:
73+
print(
74+
"Unexpected type for myst_enable_extensions: %s",
75+
type(app.config.myst_enable_extensions),
76+
)
77+
7378
return {
7479
"version": "3.0.0",
7580
# Keep this in sync with the score_docs_as_code version in MODULE.bazel

src/extensions/score_sync_toml/__init__.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from sphinx.application import Sphinx
1616

17+
from src.helper_lib import config_setdefault
18+
1719

1820
def setup(app: Sphinx) -> dict[str, str | bool]:
1921
"""
@@ -22,35 +24,37 @@ def setup(app: Sphinx) -> dict[str, str | bool]:
2224
See https://needs-config-writer.useblocks.com
2325
"""
2426

25-
app.config.needscfg_outpath = "ubproject.toml"
27+
config_setdefault(app.config, "needscfg_outpath", "ubproject.toml")
2628
"""Write to the confdir directory."""
2729

28-
app.config.needscfg_overwrite = True
30+
config_setdefault(app.config, "needscfg_overwrite", True)
2931
"""Any changes to the shared/local configuration updates the generated config."""
3032

31-
app.config.needscfg_write_all = True
33+
config_setdefault(app.config, "needscfg_write_all", True)
3234
"""Write full config, so the final configuration is visible in one file."""
3335

34-
app.config.needscfg_exclude_defaults = True
36+
config_setdefault(app.config, "needscfg_exclude_defaults", True)
3537
"""Exclude default values from the generated configuration."""
3638

3739
# This is disabled for right now as it causes a lot of issues
3840
# While we are not using the generated file anywhere
39-
app.config.needscfg_warn_on_diff = False
41+
config_setdefault(app.config, "needscfg_warn_on_diff", False)
4042
"""Running Sphinx with -W will fail the CI for uncommitted TOML changes."""
4143

42-
app.config.needscfg_merge_toml_files = [
43-
str(Path(__file__).parent / "shared.toml"),
44-
]
44+
app.config.needscfg_merge_toml_files.append(
45+
str(Path(__file__).parent / "shared.toml")
46+
)
4547
"""Merge the static TOML file into the generated configuration."""
4648

47-
app.config.needscfg_relative_path_fields = [
48-
"needs_external_needs[*].json_path",
49-
{
50-
"field": "needs_flow_configs.score_config",
51-
"prefix": "!include ",
52-
},
53-
]
49+
app.config.needscfg_relative_path_fields.extend(
50+
[
51+
"needs_external_needs[*].json_path",
52+
{
53+
"field": "needs_flow_configs.score_config",
54+
"prefix": "!include ",
55+
},
56+
]
57+
)
5458
"""Relative paths to confdir for Bazel provided absolute paths."""
5559

5660
app.config.suppress_warnings += [

src/helper_lib/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,25 @@
1515
import subprocess
1616
import sys
1717
from pathlib import Path
18+
from typing import Any
1819

1920
from runfiles import Runfiles
21+
from sphinx.config import Config
2022
from sphinx_needs.logging import get_logger
2123

2224
LOGGER = get_logger(__name__)
2325

2426

27+
def config_setdefault(config: Config, name: str, value: Any) -> None:
28+
"""Set a Sphinx config value only if not explicitly set it in conf.py."""
29+
30+
# Sphinx has no public API for this check. We use ``_raw_config`` which is the
31+
# de-facto standard across the ecosystem (Furo, RTD-theme, etc.). If Sphinx
32+
# ever adds a public alternative, update this single function.
33+
if name not in config._raw_config: # pyright: ignore [reportPrivateUsage]
34+
setattr(config, name, value)
35+
36+
2537
def find_ws_root() -> Path | None:
2638
"""
2739
Find the current MODULE.bazel workspace root directory.

src/helper_lib/test_helper_lib.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,34 @@
1818
import pytest
1919

2020
from src.helper_lib import (
21+
config_setdefault,
2122
get_current_git_hash,
2223
get_github_repo_info,
2324
get_runfiles_dir,
2425
parse_remote_git_output,
2526
)
2627

2728

29+
class _FakeConfig:
30+
"""Minimal stand-in for sphinx.config.Config."""
31+
32+
def __init__(self, raw: dict):
33+
self._raw_config = raw
34+
35+
36+
def test_config_setdefault_sets_when_not_in_raw_config():
37+
cfg = _FakeConfig(raw={})
38+
config_setdefault(cfg, "html_copy_source", False) # pyright: ignore [reportArgumentType]
39+
assert cfg.html_copy_source is False # pyright: ignore [reportAttributeAccessIssue]
40+
41+
42+
def test_config_setdefault_does_not_overwrite_user_value():
43+
cfg = _FakeConfig(raw={"html_copy_source": True})
44+
cfg.html_copy_source = True # pyright: ignore[reportAttributeAccessIssue]
45+
config_setdefault(cfg, "html_copy_source", False) # pyright: ignore [reportArgumentType]
46+
assert cfg.html_copy_source is True # pyright: ignore[reportAttributeAccessIssue]
47+
48+
2849
@pytest.fixture
2950
def temp_dir():
3051
"""Create a temporary directory for tests."""

0 commit comments

Comments
 (0)