Skip to content

Commit 0c90423

Browse files
committed
Allow to configure a different metamodel
Fixes #415
1 parent 21640ab commit 0c90423

8 files changed

Lines changed: 143 additions & 22 deletions

File tree

docs.bzl

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def _missing_requirements(deps):
120120
fail(msg)
121121
fail("This case should be unreachable?!")
122122

123-
def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
123+
def docs(source_dir = "docs", data = [], deps = [], scan_code = [], metamodel = None):
124124
"""Creates all targets related to documentation.
125125
126126
By using this function, you'll get any and all updates for documentation targets in one place.
@@ -130,13 +130,23 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
130130
data: Additional data files to include in the documentation build.
131131
deps: Additional dependencies for the documentation build.
132132
scan_code: List of code targets to scan for source code links.
133+
metamodel: Optional label to a metamodel.yaml file. When set, the extension loads this
134+
file instead of the default metamodel shipped with score_metamodel.
133135
"""
134136

135137
call_path = native.package_name()
136138

137139
if call_path != "":
138140
fail("docs() must be called from the root package. Current package: " + call_path)
139141

142+
metamodel_data = []
143+
metamodel_env = {}
144+
metamodel_opts = []
145+
if metamodel != None:
146+
metamodel_data = [metamodel]
147+
metamodel_env = {"SCORE_METAMODEL_YAML": "$(location " + str(metamodel) + ")"}
148+
metamodel_opts = ["--define=score_metamodel_yaml=$(location " + str(metamodel) + ")"]
149+
140150
module_deps = deps
141151
deps = deps + _missing_requirements(deps)
142152
deps = deps + [
@@ -147,7 +157,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
147157
sphinx_build_binary(
148158
name = "sphinx_build",
149159
visibility = ["//visibility:private"],
150-
data = data,
160+
data = data + metamodel_data,
151161
deps = deps,
152162
)
153163

@@ -181,28 +191,28 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
181191
name = "docs",
182192
tags = ["cli_help=Build documentation:\nbazel run //:docs"],
183193
srcs = ["@score_docs_as_code//src:incremental.py"],
184-
data = data + [":sourcelinks_json"],
194+
data = data + [":sourcelinks_json"] + metamodel_data,
185195
deps = deps,
186196
env = {
187197
"SOURCE_DIRECTORY": source_dir,
188198
"DATA": str(data),
189199
"ACTION": "incremental",
190200
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
191-
},
201+
} | metamodel_env,
192202
)
193203

194204
py_binary(
195205
name = "docs_combo",
196206
tags = ["cli_help=Build full documentation with all dependencies:\nbazel run //:docs_combo"],
197207
srcs = ["@score_docs_as_code//src:incremental.py"],
198-
data = data_with_docs_sources + [":merged_sourcelinks"],
208+
data = data_with_docs_sources + [":merged_sourcelinks"] + metamodel_data,
199209
deps = deps,
200210
env = {
201211
"SOURCE_DIRECTORY": source_dir,
202212
"DATA": str(data_with_docs_sources),
203213
"ACTION": "incremental",
204214
"SCORE_SOURCELINKS": "$(location :merged_sourcelinks)",
205-
},
215+
} | metamodel_env,
206216
)
207217

208218
native.alias(
@@ -215,55 +225,55 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
215225
name = "docs_link_check",
216226
tags = ["cli_help=Verify Links inside Documentation:\nbazel run //:link_check\n (Note: this could take a long time)"],
217227
srcs = ["@score_docs_as_code//src:incremental.py"],
218-
data = data,
228+
data = data + metamodel_data,
219229
deps = deps,
220230
env = {
221231
"SOURCE_DIRECTORY": source_dir,
222232
"DATA": str(data),
223233
"ACTION": "linkcheck",
224-
},
234+
} | metamodel_env,
225235
)
226236

227237
py_binary(
228238
name = "docs_check",
229239
tags = ["cli_help=Verify documentation:\nbazel run //:docs_check"],
230240
srcs = ["@score_docs_as_code//src:incremental.py"],
231-
data = data + [":sourcelinks_json"],
241+
data = data + [":sourcelinks_json"] + metamodel_data,
232242
deps = deps,
233243
env = {
234244
"SOURCE_DIRECTORY": source_dir,
235245
"DATA": str(data),
236246
"ACTION": "check",
237247
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
238-
},
248+
} | metamodel_env,
239249
)
240250

241251
py_binary(
242252
name = "live_preview",
243253
tags = ["cli_help=Live preview documentation in the browser:\nbazel run //:live_preview"],
244254
srcs = ["@score_docs_as_code//src:incremental.py"],
245-
data = data + [":sourcelinks_json"],
255+
data = data + [":sourcelinks_json"] + metamodel_data,
246256
deps = deps,
247257
env = {
248258
"SOURCE_DIRECTORY": source_dir,
249259
"DATA": str(data),
250260
"ACTION": "live_preview",
251261
"SCORE_SOURCELINKS": "$(location :sourcelinks_json)",
252-
},
262+
} | metamodel_env,
253263
)
254264

255265
py_binary(
256266
name = "live_preview_combo_experimental",
257267
tags = ["cli_help=Live preview full documentation with all dependencies in the browser:\nbazel run //:live_preview_combo_experimental"],
258268
srcs = ["@score_docs_as_code//src:incremental.py"],
259-
data = data_with_docs_sources + [":merged_sourcelinks"],
269+
data = data_with_docs_sources + [":merged_sourcelinks"] + metamodel_data,
260270
deps = deps,
261271
env = {
262272
"SOURCE_DIRECTORY": source_dir,
263273
"DATA": str(data_with_docs_sources),
264274
"ACTION": "live_preview",
265275
"SCORE_SOURCELINKS": "$(location :merged_sourcelinks)",
266-
},
276+
} | metamodel_env,
267277
)
268278

269279
score_virtualenv(
@@ -272,7 +282,7 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
272282
venv_name = ".venv_docs",
273283
reqs = deps,
274284
# Add dependencies to ide_support, so esbonio has access to them.
275-
data = data,
285+
data = data + metamodel_data,
276286
)
277287

278288
sphinx_docs(
@@ -286,10 +296,10 @@ def docs(source_dir = "docs", data = [], deps = [], scan_code = []):
286296
"--jobs",
287297
"auto",
288298
"--define=external_needs_source=" + str(data),
289-
],
299+
] + metamodel_opts,
290300
formats = ["needs"],
291301
sphinx = ":sphinx_build",
292-
tools = data,
302+
tools = data + metamodel_data,
293303
visibility = ["//visibility:public"],
294304
)
295305

docs/how-to/setup.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ The `docs()` macro accepts the following arguments:
6868
| Parameter | Description | Required |
6969
|-----------|-------------|----------|
7070
| `source_dir` | Directory of documentation source files (RST, MD) | Yes |
71-
| `data` | List of `needs_json` targets that should be included in the documentation| No |
72-
71+
| `data` | List of `needs_json` targets that should be included in the documentation | No |
72+
| `deps` | Additional Bazel Python dependencies | No |
73+
| `scan_code` | Source code targets to scan for traceability tags | No |
74+
| `metamodel` | Label to a custom `metamodel.yaml` that replaces the default metamodel | No |
7375

7476
### 4. Copy conf.py
7577

docs/reference/bazel_macros.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ Minimal example (root ``BUILD``)
4747
If you don't provide the necessary Sphinx packages,
4848
this function adds its own (but checks for conflicts).
4949

50+
- ``scan_code`` (list of bazel labels)
51+
Source code targets to scan for traceability tags (``req-Id:`` annotations).
52+
Used to generate the source-code-link JSON that maps tags back to source files.
53+
54+
- ``metamodel`` (bazel label, optional)
55+
Path to a custom ``metamodel.yaml`` file.
56+
When set, the ``score_metamodel`` extension loads **this file instead of** the default metamodel.
57+
The label is automatically added to the ``data`` and ``tools`` of every generated target
58+
so the file is available in the Bazel sandbox at build time.
59+
60+
Example:
61+
62+
.. code-block:: python
63+
64+
docs(
65+
source_dir = "docs",
66+
metamodel = "//:my_metamodel.yaml",
67+
)
68+
69+
The custom ``metamodel.yaml`` must follow the same schema as the default one
70+
(see :doc:`score_metamodel </internals/extensions/metamodel>`).
71+
When ``metamodel`` is omitted the default metamodel is used unchanged.
72+
5073
Edge cases
5174
----------
5275

src/extensions/score_metamodel/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,14 @@ def postprocess_need_links(needs_types_list: list[ScoreNeedType]):
232232

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

238239
# load metamodel.yaml via ruamel.yaml
239-
metamodel = load_metamodel_data()
240+
raw_metamodel_path = app.config.score_metamodel_yaml
241+
override_path = Path(raw_metamodel_path) if raw_metamodel_path else None
242+
metamodel = load_metamodel_data(override_path)
240243

241244
# Extend sphinx-needs config rather than overwriting
242245
app.config.needs_types += metamodel.needs_types

src/extensions/score_metamodel/tests/test_metamodel__init__.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
#
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
13+
import sys
14+
from pathlib import Path
15+
from unittest.mock import MagicMock, patch
16+
1317
import pytest
1418
from attribute_plugin import add_test_properties # type: ignore[import-untyped]
1519
from sphinx.application import Sphinx
@@ -21,9 +25,15 @@
2125
graph_checks,
2226
local_checks,
2327
parse_checks_filter,
28+
setup,
2429
)
2530
from src.extensions.score_metamodel.tests import need
2631

32+
# The module where setup() looks up load_metamodel_data at runtime.
33+
# Using __globals__ avoids breakage when the same file is imported under
34+
# two different names (e.g. "score_metamodel" and "src.extensions.score_metamodel").
35+
_setup_module = sys.modules[setup.__module__]
36+
2737

2838
def dummy_local_check(app: Sphinx, need: NeedItem, log: CheckLogger) -> None:
2939
pass
@@ -176,3 +186,58 @@ def test_combined_core_links_and_extras(self):
176186
assert n.get("output", []) == ["output_wp"]
177187
# Extra fields
178188
assert n.get("custom_attr") == "custom_value"
189+
190+
191+
# =============================================================================
192+
# Tests for setup() — score_metamodel_yaml config value wiring
193+
# =============================================================================
194+
195+
196+
def _make_mock_app(metamodel_yaml_value: str = "") -> MagicMock:
197+
"""Return a minimal mock Sphinx app suitable for calling setup()."""
198+
app = MagicMock()
199+
app.config.score_metamodel_yaml = metamodel_yaml_value
200+
app.config.needs_types = []
201+
app.config.needs_extra_links = []
202+
app.config.needs_extra_options = []
203+
return app
204+
205+
206+
def _mock_metamodel_return():
207+
return MagicMock(
208+
needs_types=[],
209+
needs_extra_links=[],
210+
needs_extra_options=[],
211+
needs_graph_check={},
212+
prohibited_words_checks=[],
213+
)
214+
215+
216+
def test_setup_uses_default_path_when_config_empty():
217+
"""setup() calls load_metamodel_data(None) when score_metamodel_yaml is empty."""
218+
app = _make_mock_app(metamodel_yaml_value="")
219+
220+
with patch.object(
221+
_setup_module,
222+
"load_metamodel_data",
223+
return_value=_mock_metamodel_return(),
224+
) as mock_load:
225+
setup(app)
226+
227+
mock_load.assert_called_once_with(None)
228+
229+
230+
def test_setup_passes_path_when_config_set(tmp_path):
231+
"""setup() calls load_metamodel_data(Path(...)) when score_metamodel_yaml is set."""
232+
yaml_file = tmp_path / "custom_metamodel.yaml"
233+
yaml_file.write_text("needs_types: {}\n")
234+
app = _make_mock_app(metamodel_yaml_value=str(yaml_file))
235+
236+
with patch.object(
237+
_setup_module,
238+
"load_metamodel_data",
239+
return_value=_mock_metamodel_return(),
240+
) as mock_load:
241+
setup(app)
242+
243+
mock_load.assert_called_once_with(Path(str(yaml_file)))

src/extensions/score_metamodel/tests/test_metamodel_load.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ def load_model_data(model_file: str) -> str:
2525
return f.read()
2626

2727

28+
def test_load_metamodel_data_explicit_path():
29+
"""When an explicit path is given, load_metamodel_data reads that file."""
30+
explicit_path = MODEL_DIR / "simple_model.yaml"
31+
result = load_metamodel_data(yaml_path=explicit_path)
32+
33+
assert len(result.needs_types) == 1
34+
assert result.needs_types[0]["directive"] == "type1"
35+
36+
2837
def test_load_metamodel_data():
2938
model_data: str = load_model_data("simple_model.yaml")
3039

src/extensions/score_metamodel/yaml_parser.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,16 @@ def _collect_all_custom_options(
182182
return sorted(all_options - defaults)
183183

184184

185-
def load_metamodel_data() -> MetaModelData:
185+
def load_metamodel_data(yaml_path: Path | None = None) -> MetaModelData:
186186
"""
187187
Load metamodel.yaml and prepare data fields as needed for sphinx-needs.
188+
189+
Args:
190+
yaml_path: Path to the metamodel YAML file. When None, the default
191+
metamodel shipped with this extension is used.
188192
"""
189-
yaml_path = Path(__file__).resolve().parent / "metamodel.yaml"
193+
if yaml_path is None:
194+
yaml_path = Path(__file__).resolve().parent / "metamodel.yaml"
190195

191196
with open(yaml_path, encoding="utf-8") as f:
192197
data = cast(dict[str, Any], YAML().load(f))

src/incremental.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ def get_env(name: str) -> str:
8484
f"--define=external_needs_source={get_env('DATA')}",
8585
]
8686

87+
metamodel_yaml = os.environ.get("SCORE_METAMODEL_YAML", "")
88+
if metamodel_yaml:
89+
base_arguments.append(f"--define=score_metamodel_yaml={metamodel_yaml}")
90+
8791
# configure sphinx build with GitHub user and repo from CLI
8892
if args.github_user and args.github_repo:
8993
base_arguments.append(f"-A=github_user={args.github_user}")

0 commit comments

Comments
 (0)