From 1b23c8d776f445f4a2cb6407660a45a7e600a9b1 Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 21 Aug 2026 16:41:08 +0200 Subject: [PATCH 1/2] feat: derive required_in_id for generated conf --- BUILD | 1 + default_conf.py.tpl | 5 ++ docs.bzl | 38 +++++++++--- docs/reference/bazel_macros.rst | 7 ++- scripts_bazel/BUILD | 7 +++ scripts_bazel/generate_conf.py | 74 +++++++++++++++++++++++ scripts_bazel/tests/BUILD | 9 +++ scripts_bazel/tests/generate_conf_test.py | 47 ++++++++++++++ src/tests/docs_bzl/test_basic_docs.py | 7 ++- 9 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 scripts_bazel/generate_conf.py create mode 100644 scripts_bazel/tests/generate_conf_test.py diff --git a/BUILD b/BUILD index 34d7a5e70..344fb7268 100644 --- a/BUILD +++ b/BUILD @@ -15,6 +15,7 @@ load("//:docs.bzl", "docs") package(default_visibility = ["//visibility:public"]) exports_files([ + "MODULE.bazel", "default_conf.py.tpl", "pyproject.toml", ]) diff --git a/default_conf.py.tpl b/default_conf.py.tpl index 3c2426f27..d7455cbb2 100644 --- a/default_conf.py.tpl +++ b/default_conf.py.tpl @@ -18,4 +18,9 @@ project = {PROJECT} project_url = {PROJECT_URL} version = "0.0.0" +# Allow feature IDs that use the Bazel module name without its first +# underscore-separated prefix (for example, ``score_docs_as_code`` becomes +# ``docs_as_code``). A user-provided conf.py remains authoritative. +required_in_id = {REQUIRED_IN_ID} + extensions = ["score_sphinx_bundle"] diff --git a/docs.bzl b/docs.bzl index cdc3a735c..fc9f1aa8f 100644 --- a/docs.bzl +++ b/docs.bzl @@ -61,15 +61,32 @@ load( "create_mounts_manifest", ) +def _module_file_label(): + """Return the MODULE.bazel label belonging to the calling repository.""" + repository_name = native.repository_name() + if native.package_name() == "": + # MODULE.bazel is a source file outside the normal BUILD package + # declarations. Export it when docs() is called from a repository root + # so the generator can consume the caller's module metadata. + native.exports_files(["MODULE.bazel"]) + if repository_name == "@": + return "@//:MODULE.bazel" + return repository_name + "//:MODULE.bazel" + def _generated_conf_impl(ctx): output = ctx.actions.declare_file(ctx.attr.output_path) - ctx.actions.expand_template( - template = ctx.file.template, - output = output, - substitutions = { - "{PROJECT}": repr(ctx.attr.project), - "{PROJECT_URL}": repr(ctx.attr.project_url), - }, + arguments = ctx.actions.args() + arguments.add("--template", ctx.file.template.path) + arguments.add("--module-file", ctx.file.module_file.path) + arguments.add("--project", ctx.attr.project) + arguments.add("--project-url", ctx.attr.project_url) + arguments.add("--output", output.path) + ctx.actions.run( + executable = ctx.executable._generate_conf, + arguments = [arguments], + inputs = [ctx.file.template, ctx.file.module_file], + outputs = [output], + mnemonic = "GenerateSphinxConf", ) return [DefaultInfo(files = depset([output]))] @@ -78,11 +95,17 @@ _generated_conf = rule( attrs = { "project": attr.string(mandatory = True), "project_url": attr.string(mandatory = True), + "module_file": attr.label(allow_single_file = True, mandatory = True), "output_path": attr.string(mandatory = True), "template": attr.label( allow_single_file = True, default = Label("@score_docs_as_code//:default_conf.py.tpl"), ), + "_generate_conf": attr.label( + cfg = "exec", + default = Label("@score_docs_as_code//scripts_bazel:generate_conf"), + executable = True, + ), }, ) @@ -238,6 +261,7 @@ def docs( name = "_docs_generated_config", project = project, project_url = project_url, + module_file = _module_file_label(), output_path = config_file_path, ) sphinx_config = ":_docs_generated_config" diff --git a/docs/reference/bazel_macros.rst b/docs/reference/bazel_macros.rst index f188c93f5..0a28f6075 100644 --- a/docs/reference/bazel_macros.rst +++ b/docs/reference/bazel_macros.rst @@ -56,8 +56,11 @@ Minimal example (root ``BUILD``) - ``project`` and ``project_url`` (strings, optional) Project name and canonical project URL. They are required when ``source_dir`` has no ``conf.py``; in that case ``docs()`` generates the Sphinx configuration - and supplies the Docs-as-Code baseline version and extensions. If a ``conf.py`` - exists, it remains authoritative and these values are not used. + and supplies the Docs-as-Code baseline version, extensions, and a + ``required_in_id`` entry derived from the Bazel module name. The first + underscore-separated prefix is removed (for example, + ``score_docs_as_code`` becomes ``docs_as_code``). If a ``conf.py`` exists, + it remains authoritative and these values are not used. - ``data`` (list of bazel labels) Extra runfiles / data targets that should be made available to the documentation targets. diff --git a/scripts_bazel/BUILD b/scripts_bazel/BUILD index e2d0402d2..ba91059c7 100644 --- a/scripts_bazel/BUILD +++ b/scripts_bazel/BUILD @@ -30,6 +30,13 @@ py_binary( ] + all_requirements, ) +py_binary( + name = "generate_conf", + srcs = ["generate_conf.py"], + main = "generate_conf.py", + visibility = ["//visibility:public"], +) + py_binary( name = "merge_sourcelinks", srcs = ["merge_sourcelinks.py"], diff --git a/scripts_bazel/generate_conf.py b/scripts_bazel/generate_conf.py new file mode 100644 index 000000000..5ad2fe302 --- /dev/null +++ b/scripts_bazel/generate_conf.py @@ -0,0 +1,74 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Generate the default Sphinx configuration for a Bazel module.""" + +import argparse +import json +import re +from pathlib import Path + + +def module_name_without_prefix(module_file: Path) -> str: + """Read a module name and remove its first underscore-separated prefix.""" + module_contents = module_file.read_text(encoding="utf-8") + match = re.search( + r"\bmodule\s*\(.*?\bname\s*=\s*[\"']([^\"']+)[\"']", + module_contents, + flags=re.DOTALL, + ) + if match is None: + raise ValueError(f"could not find module(name = ...) in {module_file}") + + module_name = match.group(1) + return module_name.split("_", maxsplit=1)[-1] + + +def generate_conf( + template: Path, + module_file: Path, + project: str, + project_url: str, + output: Path, +) -> None: + """Expand the default configuration template.""" + substitutions = { + "{PROJECT}": json.dumps(project), + "{PROJECT_URL}": json.dumps(project_url), + "{REQUIRED_IN_ID}": json.dumps([module_name_without_prefix(module_file)]), + } + contents = template.read_text(encoding="utf-8") + for placeholder, value in substitutions.items(): + contents = contents.replace(placeholder, value) + output.write_text(contents, encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--template", type=Path, required=True) + parser.add_argument("--module-file", type=Path, required=True) + parser.add_argument("--project", required=True) + parser.add_argument("--project-url", required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + generate_conf( + template=args.template, + module_file=args.module_file, + project=args.project, + project_url=args.project_url, + output=args.output, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts_bazel/tests/BUILD b/scripts_bazel/tests/BUILD index e6fab5f1c..69a6c2f79 100644 --- a/scripts_bazel/tests/BUILD +++ b/scripts_bazel/tests/BUILD @@ -14,6 +14,15 @@ load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements") load("//:score_pytest.bzl", "score_pytest") +score_pytest( + name = "generate_conf_test", + srcs = ["generate_conf_test.py"], + deps = [ + "//scripts_bazel:generate_conf", + ] + all_requirements, + pytest_config = "//:pyproject.toml", +) + score_pytest( name = "generate_sourcelinks_cli_test", srcs = ["generate_sourcelinks_cli_test.py"], diff --git a/scripts_bazel/tests/generate_conf_test.py b/scripts_bazel/tests/generate_conf_test.py new file mode 100644 index 000000000..ec2b9e248 --- /dev/null +++ b/scripts_bazel/tests/generate_conf_test.py @@ -0,0 +1,47 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Tests for the default Sphinx configuration generator.""" + +from pathlib import Path + +from scripts_bazel.generate_conf import generate_conf + + +def test_generate_conf_uses_module_name_without_first_prefix(tmp_path: Path) -> None: + module_file = tmp_path / "MODULE.bazel" + module_file.write_text( + 'module(\n name = "score_docs_as_code",\n version = "1.0.0"\n)\n', + encoding="utf-8", + ) + template = tmp_path / "default_conf.py.tpl" + template.write_text( + "project = {PROJECT}\n" + "project_url = {PROJECT_URL}\n" + "required_in_id = {REQUIRED_IN_ID}\n", + encoding="utf-8", + ) + output = tmp_path / "conf.py" + + generate_conf( + template=template, + module_file=module_file, + project="Docs-as-Code", + project_url="https://example.invalid/docs", + output=output, + ) + + assert output.read_text(encoding="utf-8") == ( + 'project = "Docs-as-Code"\n' + 'project_url = "https://example.invalid/docs"\n' + 'required_in_id = ["docs_as_code"]\n' + ) diff --git a/src/tests/docs_bzl/test_basic_docs.py b/src/tests/docs_bzl/test_basic_docs.py index cffda3ba0..ff92ffb1e 100644 --- a/src/tests/docs_bzl/test_basic_docs.py +++ b/src/tests/docs_bzl/test_basic_docs.py @@ -13,7 +13,7 @@ """Public docs() smoke scenario.""" -from src.tests.docs_bzl.helpers import load_needs_json, run_scenario +from src.tests.docs_bzl.helpers import built_output, load_needs_json, run_scenario def test_basic_docs_builds_html(): @@ -33,3 +33,8 @@ def test_basic_docs_builds_needs_without_conf_py(): assert result.artifacts is not None, f"expected artifacts: {result}" data = load_needs_json(result.artifacts["needs.json"]) assert data["current_version"], "current_version must be non-empty" + + generated_conf = built_output("scenarios/basic_docs", "docs/conf.py") + assert 'required_in_id = ["docs_as_code"]' in generated_conf.read_text( + encoding="utf-8" + ) From d8ed8912c1b278c6a71355aab2434141d33eb2de Mon Sep 17 00:00:00 2001 From: Alexander Lanin Date: Fri, 21 Aug 2026 16:56:57 +0200 Subject: [PATCH 2/2] refactor: use Bazel module name directly --- BUILD | 1 - docs.bzl | 46 +++++--------- scripts_bazel/BUILD | 7 --- scripts_bazel/generate_conf.py | 74 ----------------------- scripts_bazel/tests/BUILD | 9 --- scripts_bazel/tests/generate_conf_test.py | 47 -------------- 6 files changed, 16 insertions(+), 168 deletions(-) delete mode 100644 scripts_bazel/generate_conf.py delete mode 100644 scripts_bazel/tests/generate_conf_test.py diff --git a/BUILD b/BUILD index 344fb7268..34d7a5e70 100644 --- a/BUILD +++ b/BUILD @@ -15,7 +15,6 @@ load("//:docs.bzl", "docs") package(default_visibility = ["//visibility:public"]) exports_files([ - "MODULE.bazel", "default_conf.py.tpl", "pyproject.toml", ]) diff --git a/docs.bzl b/docs.bzl index fc9f1aa8f..b41864f83 100644 --- a/docs.bzl +++ b/docs.bzl @@ -61,32 +61,23 @@ load( "create_mounts_manifest", ) -def _module_file_label(): - """Return the MODULE.bazel label belonging to the calling repository.""" - repository_name = native.repository_name() - if native.package_name() == "": - # MODULE.bazel is a source file outside the normal BUILD package - # declarations. Export it when docs() is called from a repository root - # so the generator can consume the caller's module metadata. - native.exports_files(["MODULE.bazel"]) - if repository_name == "@": - return "@//:MODULE.bazel" - return repository_name + "//:MODULE.bazel" +def _module_name_without_prefix(): + """Return the current Bazel module name without its first prefix.""" + module_name = native.module_name() + if not module_name: + return "" + return module_name.split("_", 1)[-1] def _generated_conf_impl(ctx): output = ctx.actions.declare_file(ctx.attr.output_path) - arguments = ctx.actions.args() - arguments.add("--template", ctx.file.template.path) - arguments.add("--module-file", ctx.file.module_file.path) - arguments.add("--project", ctx.attr.project) - arguments.add("--project-url", ctx.attr.project_url) - arguments.add("--output", output.path) - ctx.actions.run( - executable = ctx.executable._generate_conf, - arguments = [arguments], - inputs = [ctx.file.template, ctx.file.module_file], - outputs = [output], - mnemonic = "GenerateSphinxConf", + ctx.actions.expand_template( + template = ctx.file.template, + output = output, + substitutions = { + "{PROJECT}": repr(ctx.attr.project), + "{PROJECT_URL}": repr(ctx.attr.project_url), + "{REQUIRED_IN_ID}": repr([ctx.attr.required_in_id]) if ctx.attr.required_in_id else "[]", + }, ) return [DefaultInfo(files = depset([output]))] @@ -95,17 +86,12 @@ _generated_conf = rule( attrs = { "project": attr.string(mandatory = True), "project_url": attr.string(mandatory = True), - "module_file": attr.label(allow_single_file = True, mandatory = True), + "required_in_id": attr.string(mandatory = True), "output_path": attr.string(mandatory = True), "template": attr.label( allow_single_file = True, default = Label("@score_docs_as_code//:default_conf.py.tpl"), ), - "_generate_conf": attr.label( - cfg = "exec", - default = Label("@score_docs_as_code//scripts_bazel:generate_conf"), - executable = True, - ), }, ) @@ -261,7 +247,7 @@ def docs( name = "_docs_generated_config", project = project, project_url = project_url, - module_file = _module_file_label(), + required_in_id = _module_name_without_prefix(), output_path = config_file_path, ) sphinx_config = ":_docs_generated_config" diff --git a/scripts_bazel/BUILD b/scripts_bazel/BUILD index ba91059c7..e2d0402d2 100644 --- a/scripts_bazel/BUILD +++ b/scripts_bazel/BUILD @@ -30,13 +30,6 @@ py_binary( ] + all_requirements, ) -py_binary( - name = "generate_conf", - srcs = ["generate_conf.py"], - main = "generate_conf.py", - visibility = ["//visibility:public"], -) - py_binary( name = "merge_sourcelinks", srcs = ["merge_sourcelinks.py"], diff --git a/scripts_bazel/generate_conf.py b/scripts_bazel/generate_conf.py deleted file mode 100644 index 5ad2fe302..000000000 --- a/scripts_bazel/generate_conf.py +++ /dev/null @@ -1,74 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License, Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""Generate the default Sphinx configuration for a Bazel module.""" - -import argparse -import json -import re -from pathlib import Path - - -def module_name_without_prefix(module_file: Path) -> str: - """Read a module name and remove its first underscore-separated prefix.""" - module_contents = module_file.read_text(encoding="utf-8") - match = re.search( - r"\bmodule\s*\(.*?\bname\s*=\s*[\"']([^\"']+)[\"']", - module_contents, - flags=re.DOTALL, - ) - if match is None: - raise ValueError(f"could not find module(name = ...) in {module_file}") - - module_name = match.group(1) - return module_name.split("_", maxsplit=1)[-1] - - -def generate_conf( - template: Path, - module_file: Path, - project: str, - project_url: str, - output: Path, -) -> None: - """Expand the default configuration template.""" - substitutions = { - "{PROJECT}": json.dumps(project), - "{PROJECT_URL}": json.dumps(project_url), - "{REQUIRED_IN_ID}": json.dumps([module_name_without_prefix(module_file)]), - } - contents = template.read_text(encoding="utf-8") - for placeholder, value in substitutions.items(): - contents = contents.replace(placeholder, value) - output.write_text(contents, encoding="utf-8") - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--template", type=Path, required=True) - parser.add_argument("--module-file", type=Path, required=True) - parser.add_argument("--project", required=True) - parser.add_argument("--project-url", required=True) - parser.add_argument("--output", type=Path, required=True) - args = parser.parse_args() - generate_conf( - template=args.template, - module_file=args.module_file, - project=args.project, - project_url=args.project_url, - output=args.output, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts_bazel/tests/BUILD b/scripts_bazel/tests/BUILD index 69a6c2f79..e6fab5f1c 100644 --- a/scripts_bazel/tests/BUILD +++ b/scripts_bazel/tests/BUILD @@ -14,15 +14,6 @@ load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements") load("//:score_pytest.bzl", "score_pytest") -score_pytest( - name = "generate_conf_test", - srcs = ["generate_conf_test.py"], - deps = [ - "//scripts_bazel:generate_conf", - ] + all_requirements, - pytest_config = "//:pyproject.toml", -) - score_pytest( name = "generate_sourcelinks_cli_test", srcs = ["generate_sourcelinks_cli_test.py"], diff --git a/scripts_bazel/tests/generate_conf_test.py b/scripts_bazel/tests/generate_conf_test.py deleted file mode 100644 index ec2b9e248..000000000 --- a/scripts_bazel/tests/generate_conf_test.py +++ /dev/null @@ -1,47 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -"""Tests for the default Sphinx configuration generator.""" - -from pathlib import Path - -from scripts_bazel.generate_conf import generate_conf - - -def test_generate_conf_uses_module_name_without_first_prefix(tmp_path: Path) -> None: - module_file = tmp_path / "MODULE.bazel" - module_file.write_text( - 'module(\n name = "score_docs_as_code",\n version = "1.0.0"\n)\n', - encoding="utf-8", - ) - template = tmp_path / "default_conf.py.tpl" - template.write_text( - "project = {PROJECT}\n" - "project_url = {PROJECT_URL}\n" - "required_in_id = {REQUIRED_IN_ID}\n", - encoding="utf-8", - ) - output = tmp_path / "conf.py" - - generate_conf( - template=template, - module_file=module_file, - project="Docs-as-Code", - project_url="https://example.invalid/docs", - output=output, - ) - - assert output.read_text(encoding="utf-8") == ( - 'project = "Docs-as-Code"\n' - 'project_url = "https://example.invalid/docs"\n' - 'required_in_id = ["docs_as_code"]\n' - )