From 92aa2d99bf7c8d71c23056b183cb4e6e14f7299e Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Mon, 5 Oct 2026 23:25:41 +0000 Subject: [PATCH 1/5] feat(pypi): generate requirements.bzl in the unified @pypi hub The unified `@pypi` hub generates no `requirements.bzl`. A repo whose hub was named `pypi` has to rename it now that the name is reserved, and once it does, every `load("@pypi//:requirements.bzl", "requirement")` stops resolving at the same moment, so the rename can't be split into smaller changes. Flipping `RULES_PYTHON_PYPI_HUB_RESERVED` on by default would cause the same breakage for every such repo. Generate a `requirements.bzl` in the unified hub from the same template as a concrete hub's. `requirement()`, `whl_requirement()`, `data_requirement()` and `dist_info_requirement()` return labels in the unified hub, so they route through `--@rules_python//python/config_settings:venv` like `@pypi//`. The `all_*` lists are fixed at loading time, before the venv flag is known, so they list the default hub's packages. Co-Authored-By: Claude Opus 5.5 --- docs/pypi/download.md | 7 ++++ news/unified-hub-requirements-bzl.added.md | 8 ++++ python/private/pypi/unified_hub_repo.bzl | 40 +++++++++++++++++++ tests/integration/unified_pypi/BUILD.bazel | 27 +++++++++++++ .../unified_pypi/test_all_requirements.py | 5 +++ tests/integration/unified_pypi_test.py | 9 +++++ 6 files changed, 96 insertions(+) create mode 100644 news/unified-hub-requirements-bzl.added.md create mode 100644 tests/integration/unified_pypi/test_all_requirements.py diff --git a/docs/pypi/download.md b/docs/pypi/download.md index e4ab38714d..e509a78bd3 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -121,6 +121,13 @@ Shared library targets can simply depend on the unified hub (e.g., `@pypi//numpy`), and the dependency will automatically resolve to the correct wheel version from the active hub during the build. +The unified hub also generates a `requirements.bzl` with the same macros as a +concrete hub's, so `load("@pypi//:requirements.bzl", "requirement")` keeps +working after a hub that used to be named `pypi` is renamed, and +`requirement("numpy")` routes through the active hub like `@pypi//numpy`. Its +`all_requirements` and the other `all_*` lists are fixed at loading time, so +they name the packages of the `default_hub`. + ### Declaring Abstract Dependencies (pip.dep) :::{versionadded} 2.2.0 diff --git a/news/unified-hub-requirements-bzl.added.md b/news/unified-hub-requirements-bzl.added.md new file mode 100644 index 0000000000..cd3c5ad463 --- /dev/null +++ b/news/unified-hub-requirements-bzl.added.md @@ -0,0 +1,8 @@ +(pypi) The unified `@pypi` hub now generates a `requirements.bzl` with the same +API as a concrete hub's (`requirement`, `whl_requirement`, `data_requirement`, +`dist_info_requirement` and the `all_*` lists). A repo that renames its hub +away from the reserved `pypi` name keeps its +`load("@pypi//:requirements.bzl", "requirement")` users working, and the +returned labels route through +{obj}`--@rules_python//python/config_settings:venv`. The `all_*` lists name the +packages of the default hub. diff --git a/python/private/pypi/unified_hub_repo.bzl b/python/private/pypi/unified_hub_repo.bzl index f88db75f19..cc58c32ab4 100644 --- a/python/private/pypi/unified_hub_repo.bzl +++ b/python/private/pypi/unified_hub_repo.bzl @@ -8,6 +8,9 @@ load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_venv_fl package(default_visibility = ["//visibility:public"]) +# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it +exports_files(["requirements.bzl"]) + define_venv_flag_config_settings( name = "venv_config_settings", hubs = {hubs}, @@ -60,6 +63,40 @@ def _unified_hub_repo_impl(rctx): ), ) + # 4. Generate requirements.bzl with the same API as a concrete hub's, so a + # hub renamed away from the reserved "pypi" name keeps its + # `load("@pypi//:requirements.bzl", "requirement")` users working. The + # macros return labels in this repo, so they route through the venv flag + # like any other `@pypi//` label. The `all_*` lists are fixed at + # loading time, before the venv flag is known, so they name the default + # hub's packages. + # + # NOTE: we are using the canonical name with the double '@' in order to + # always uniquely identify a repository, as the labels are being passed as + # a string and the resolution of the label happens at the call-site of the + # `requirement`, et al. macros. + macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) + default_packages = sorted([ + pkg_name + for pkg_name, pkg_hubs in rctx.attr.packages.items() + if default_hub in pkg_hubs + ]) + rctx.template("requirements.bzl", rctx.attr._requirements_bzl_template, substitutions = { + "%%ALL_DATA_REQUIREMENTS%%": render.list([ + macro_tmpl.format(p, "data") + for p in default_packages + ]), + "%%ALL_REQUIREMENTS%%": render.list([ + macro_tmpl.format(p, "pkg") + for p in default_packages + ]), + "%%ALL_WHL_REQUIREMENTS_BY_PACKAGE%%": render.dict({ + p: macro_tmpl.format(p, "whl") + for p in default_packages + }), + "%%MACRO_TMPL%%": macro_tmpl, + }) + unified_hub_repo = repository_rule( implementation = _unified_hub_repo_impl, attrs = { @@ -77,6 +114,9 @@ unified_hub_repo = repository_rule( mandatory = True, doc = "Dictionary mapping package names to a list of hubs that contain them.", ), + "_requirements_bzl_template": attr.label( + default = ":requirements.bzl.tmpl.bzlmod", + ), }, doc = "Private repository rule creating the automatic Unified PyPI Hub.", ) diff --git a/tests/integration/unified_pypi/BUILD.bazel b/tests/integration/unified_pypi/BUILD.bazel index c0b9905fa6..0106fdebc1 100644 --- a/tests/integration/unified_pypi/BUILD.bazel +++ b/tests/integration/unified_pypi/BUILD.bazel @@ -1,8 +1,35 @@ +load("@pypi//:requirements.bzl", "all_requirements", "requirement") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") package(default_visibility = ["//visibility:public"]) +# `requirement()` from the unified hub normalizes the name and resolves +# through the venv flag like a plain `@pypi//` label. +py_test( + name = "test_requirement_default", + srcs = ["test_default.py"], + main = "test_default.py", + deps = [requirement("Colorama")], +) + +py_test( + name = "test_requirement_a", + srcs = ["test_a.py"], + config_settings = { + "@rules_python//python/config_settings:venv": "pypi_a", + }, + main = "test_a.py", + deps = [requirement("colorama")], +) + +# `all_requirements` names the default hub's (pypi_b's) packages. +py_test( + name = "test_all_requirements", + srcs = ["test_all_requirements.py"], + deps = all_requirements, +) + py_test( name = "test_default", srcs = ["test_default.py"], diff --git a/tests/integration/unified_pypi/test_all_requirements.py b/tests/integration/unified_pypi/test_all_requirements.py new file mode 100644 index 0000000000..db9fa0d1f0 --- /dev/null +++ b/tests/integration/unified_pypi/test_all_requirements.py @@ -0,0 +1,5 @@ +import colorama +import six + +assert colorama.__version__ == "0.4.5" +assert six.__version__ == "1.17.0" diff --git a/tests/integration/unified_pypi_test.py b/tests/integration/unified_pypi_test.py index a19a9fcfbe..3b2b93088a 100644 --- a/tests/integration/unified_pypi_test.py +++ b/tests/integration/unified_pypi_test.py @@ -13,6 +13,15 @@ def test_default_fallback_hub(self): def test_transitioned_hub(self): self.run_bazel("test", "//:test_a") + def test_requirement_macro_default_hub(self): + self.run_bazel("test", "//:test_requirement_default") + + def test_requirement_macro_transitioned_hub(self): + self.run_bazel("test", "//:test_requirement_a") + + def test_all_requirements_lists_default_hub(self): + self.run_bazel("test", "//:test_all_requirements") + def test_cli_override(self): self.run_bazel( "run", From 65da5f98de949d915c488a5958861acac4a03a6c Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Mon, 5 Oct 2026 23:26:04 +0000 Subject: [PATCH 2/5] news: name the entry after #4223 --- news/{unified-hub-requirements-bzl.added.md => 4223.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{unified-hub-requirements-bzl.added.md => 4223.added.md} (100%) diff --git a/news/unified-hub-requirements-bzl.added.md b/news/4223.added.md similarity index 100% rename from news/unified-hub-requirements-bzl.added.md rename to news/4223.added.md From dae5f8742876922fb3e1f4325584fb624c12f442 Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Tue, 6 Oct 2026 00:47:52 +0000 Subject: [PATCH 3/5] fix(pypi): leave the all_* lists out of the unified requirements.bzl They are fixed at loading time, before the venv flag is known, so they could only ever list one hub's packages and would not follow the flag like the rest of the unified hub. Load them from a concrete hub. Co-Authored-By: Claude Opus 5.5 --- docs/pypi/download.md | 13 ++-- news/4223.added.md | 11 ++-- python/private/pypi/unified_hub_repo.bzl | 59 ++++++++++--------- tests/integration/unified_pypi/BUILD.bazel | 9 +-- .../unified_pypi/test_all_requirements.py | 5 -- tests/integration/unified_pypi_test.py | 3 - 6 files changed, 45 insertions(+), 55 deletions(-) delete mode 100644 tests/integration/unified_pypi/test_all_requirements.py diff --git a/docs/pypi/download.md b/docs/pypi/download.md index e509a78bd3..935c8f850c 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -121,12 +121,13 @@ Shared library targets can simply depend on the unified hub (e.g., `@pypi//numpy`), and the dependency will automatically resolve to the correct wheel version from the active hub during the build. -The unified hub also generates a `requirements.bzl` with the same macros as a -concrete hub's, so `load("@pypi//:requirements.bzl", "requirement")` keeps -working after a hub that used to be named `pypi` is renamed, and -`requirement("numpy")` routes through the active hub like `@pypi//numpy`. Its -`all_requirements` and the other `all_*` lists are fixed at loading time, so -they name the packages of the `default_hub`. +The unified hub also generates a `requirements.bzl` with the per-package macros +of a concrete hub's (`requirement`, `whl_requirement`, `data_requirement` and +`dist_info_requirement`), so `load("@pypi//:requirements.bzl", "requirement")` +keeps working after a hub that used to be named `pypi` is renamed, and +`requirement("numpy")` routes through the active hub like `@pypi//numpy`. It has +no `all_requirements` or other `all_*` lists: those are fixed at loading time, +before the active hub is known, so load them from a concrete hub instead. ### Declaring Abstract Dependencies (pip.dep) diff --git a/news/4223.added.md b/news/4223.added.md index cd3c5ad463..0f5eac6b6c 100644 --- a/news/4223.added.md +++ b/news/4223.added.md @@ -1,8 +1,9 @@ -(pypi) The unified `@pypi` hub now generates a `requirements.bzl` with the same -API as a concrete hub's (`requirement`, `whl_requirement`, `data_requirement`, -`dist_info_requirement` and the `all_*` lists). A repo that renames its hub +(pypi) The unified `@pypi` hub now generates a `requirements.bzl` with the +per-package macros of a concrete hub's (`requirement`, `whl_requirement`, +`data_requirement` and `dist_info_requirement`). A repo that renames its hub away from the reserved `pypi` name keeps its `load("@pypi//:requirements.bzl", "requirement")` users working, and the returned labels route through -{obj}`--@rules_python//python/config_settings:venv`. The `all_*` lists name the -packages of the default hub. +{obj}`--@rules_python//python/config_settings:venv`. The `all_*` lists are not +provided, since they could only list one hub's packages; load them from a +concrete hub. diff --git a/python/private/pypi/unified_hub_repo.bzl b/python/private/pypi/unified_hub_repo.bzl index cc58c32ab4..2a20267d6a 100644 --- a/python/private/pypi/unified_hub_repo.bzl +++ b/python/private/pypi/unified_hub_repo.bzl @@ -31,6 +31,30 @@ define_pypi_package_targets( ) """ +# The per-package macros of a concrete hub's requirements.bzl. The `all_*` +# lists are left out on purpose: they are fixed at loading time, before the +# venv flag is known, so they could only ever list one hub's packages. +_REQUIREMENTS_BZL_TMPL = '''\ +"""Starlark representation of the unified PyPI hub's packages. + +@generated by rules_python pip.parse bzlmod extension. +""" + +load("@rules_python//python:pip.bzl", "pip_utils") + +def requirement(name): + return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "pkg") + +def whl_requirement(name): + return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "whl") + +def data_requirement(name): + return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "data") + +def dist_info_requirement(name): + return "%%MACRO_TMPL%%".format(pip_utils.normalize_name(name), "dist_info") +''' + def _unified_hub_repo_impl(rctx): hubs = rctx.attr.hubs default_hub = rctx.attr.default_hub or None @@ -63,39 +87,21 @@ def _unified_hub_repo_impl(rctx): ), ) - # 4. Generate requirements.bzl with the same API as a concrete hub's, so a - # hub renamed away from the reserved "pypi" name keeps its + # 4. Generate requirements.bzl with a concrete hub's per-package macros, so + # a hub renamed away from the reserved "pypi" name keeps its # `load("@pypi//:requirements.bzl", "requirement")` users working. The # macros return labels in this repo, so they route through the venv flag - # like any other `@pypi//` label. The `all_*` lists are fixed at - # loading time, before the venv flag is known, so they name the default - # hub's packages. + # like any other `@pypi//` label. # # NOTE: we are using the canonical name with the double '@' in order to # always uniquely identify a repository, as the labels are being passed as # a string and the resolution of the label happens at the call-site of the # `requirement`, et al. macros. macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) - default_packages = sorted([ - pkg_name - for pkg_name, pkg_hubs in rctx.attr.packages.items() - if default_hub in pkg_hubs - ]) - rctx.template("requirements.bzl", rctx.attr._requirements_bzl_template, substitutions = { - "%%ALL_DATA_REQUIREMENTS%%": render.list([ - macro_tmpl.format(p, "data") - for p in default_packages - ]), - "%%ALL_REQUIREMENTS%%": render.list([ - macro_tmpl.format(p, "pkg") - for p in default_packages - ]), - "%%ALL_WHL_REQUIREMENTS_BY_PACKAGE%%": render.dict({ - p: macro_tmpl.format(p, "whl") - for p in default_packages - }), - "%%MACRO_TMPL%%": macro_tmpl, - }) + rctx.file( + "requirements.bzl", + _REQUIREMENTS_BZL_TMPL.replace("%%MACRO_TMPL%%", macro_tmpl), + ) unified_hub_repo = repository_rule( implementation = _unified_hub_repo_impl, @@ -114,9 +120,6 @@ unified_hub_repo = repository_rule( mandatory = True, doc = "Dictionary mapping package names to a list of hubs that contain them.", ), - "_requirements_bzl_template": attr.label( - default = ":requirements.bzl.tmpl.bzlmod", - ), }, doc = "Private repository rule creating the automatic Unified PyPI Hub.", ) diff --git a/tests/integration/unified_pypi/BUILD.bazel b/tests/integration/unified_pypi/BUILD.bazel index 0106fdebc1..c0aee2de5d 100644 --- a/tests/integration/unified_pypi/BUILD.bazel +++ b/tests/integration/unified_pypi/BUILD.bazel @@ -1,4 +1,4 @@ -load("@pypi//:requirements.bzl", "all_requirements", "requirement") +load("@pypi//:requirements.bzl", "requirement") load("@rules_python//python:py_binary.bzl", "py_binary") load("@rules_python//python:py_test.bzl", "py_test") @@ -23,13 +23,6 @@ py_test( deps = [requirement("colorama")], ) -# `all_requirements` names the default hub's (pypi_b's) packages. -py_test( - name = "test_all_requirements", - srcs = ["test_all_requirements.py"], - deps = all_requirements, -) - py_test( name = "test_default", srcs = ["test_default.py"], diff --git a/tests/integration/unified_pypi/test_all_requirements.py b/tests/integration/unified_pypi/test_all_requirements.py deleted file mode 100644 index db9fa0d1f0..0000000000 --- a/tests/integration/unified_pypi/test_all_requirements.py +++ /dev/null @@ -1,5 +0,0 @@ -import colorama -import six - -assert colorama.__version__ == "0.4.5" -assert six.__version__ == "1.17.0" diff --git a/tests/integration/unified_pypi_test.py b/tests/integration/unified_pypi_test.py index 3b2b93088a..bb3c22d45d 100644 --- a/tests/integration/unified_pypi_test.py +++ b/tests/integration/unified_pypi_test.py @@ -19,9 +19,6 @@ def test_requirement_macro_default_hub(self): def test_requirement_macro_transitioned_hub(self): self.run_bazel("test", "//:test_requirement_a") - def test_all_requirements_lists_default_hub(self): - self.run_bazel("test", "//:test_all_requirements") - def test_cli_override(self): self.run_bazel( "run", From 8ec91076a8e36dbe746551a1dd91d7431df637fb Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Tue, 6 Oct 2026 01:14:20 +0000 Subject: [PATCH 4/5] feat(pypi): make the unified requirements.bzl opt-in Generate the unified hub's requirements.bzl only when the root module sets `pip.default(unified_hub_requirements_bzl = True)`. The docs steer users to `@pypi//` labels over the requirement() helper, so keep the helper on the unified hub a deliberate choice for repos migrating a hub that used to be named `pypi`, not a default surface. Co-Authored-By: Claude Opus 5.5 --- docs/pypi/download.md | 17 +++--- news/4223.added.md | 12 ++-- python/private/pypi/extension.bzl | 26 +++++++++ python/private/pypi/unified_hub_repo.bzl | 39 ++++++++----- tests/integration/unified_pypi/MODULE.bazel | 2 +- tests/integration/unified_pypi_test.py | 19 +++++++ tests/pypi/extension/extension_tests.bzl | 63 +++++++++++++++++++++ 7 files changed, 151 insertions(+), 27 deletions(-) diff --git a/docs/pypi/download.md b/docs/pypi/download.md index 935c8f850c..b9904d4071 100644 --- a/docs/pypi/download.md +++ b/docs/pypi/download.md @@ -121,13 +121,16 @@ Shared library targets can simply depend on the unified hub (e.g., `@pypi//numpy`), and the dependency will automatically resolve to the correct wheel version from the active hub during the build. -The unified hub also generates a `requirements.bzl` with the per-package macros -of a concrete hub's (`requirement`, `whl_requirement`, `data_requirement` and -`dist_info_requirement`), so `load("@pypi//:requirements.bzl", "requirement")` -keeps working after a hub that used to be named `pypi` is renamed, and -`requirement("numpy")` routes through the active hub like `@pypi//numpy`. It has -no `all_requirements` or other `all_*` lists: those are fixed at loading time, -before the active hub is known, so load them from a concrete hub instead. +To migrate a hub that used to be named `pypi`, the root module can opt into a +`requirements.bzl` in the unified hub with +`pip.default(unified_hub_requirements_bzl = True)`. It has the per-package +macros of a concrete hub's (`requirement`, `whl_requirement`, `data_requirement` +and `dist_info_requirement`), so existing +`load("@pypi//:requirements.bzl", "requirement")` calls keep working after the +rename, and `requirement("numpy")` routes through the active hub like +`@pypi//numpy`. It has no `all_requirements` or other `all_*` lists: those are +fixed at loading time, before the active hub is known, so load them from a +concrete hub instead. Prefer `@pypi//` labels in new code. ### Declaring Abstract Dependencies (pip.dep) diff --git a/news/4223.added.md b/news/4223.added.md index 0f5eac6b6c..de37e0e0e3 100644 --- a/news/4223.added.md +++ b/news/4223.added.md @@ -1,9 +1,9 @@ -(pypi) The unified `@pypi` hub now generates a `requirements.bzl` with the -per-package macros of a concrete hub's (`requirement`, `whl_requirement`, -`data_requirement` and `dist_info_requirement`). A repo that renames its hub -away from the reserved `pypi` name keeps its -`load("@pypi//:requirements.bzl", "requirement")` users working, and the -returned labels route through +(pypi) Added `pip.default(unified_hub_requirements_bzl = True)`, which makes the +unified `@pypi` hub generate a `requirements.bzl` with the per-package macros of +a concrete hub's (`requirement`, `whl_requirement`, `data_requirement` and +`dist_info_requirement`). A repo that renames its hub away from the reserved +`pypi` name keeps its `load("@pypi//:requirements.bzl", "requirement")` users +working, and the returned labels route through {obj}`--@rules_python//python/config_settings:venv`. The `all_*` lists are not provided, since they could only list one hub's packages; load them from a concrete hub. diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index cafe245eb2..1b827806f2 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -207,6 +207,7 @@ def build_config( A struct with the configuration. """ default_hub = None + unified_hub_requirements_bzl = False defaults = { "platforms": default_platforms(), "python_version": None, @@ -221,6 +222,8 @@ def build_config( if default_hub: fail("Duplicate pip.default tag: only one explicit default PyPI hub is allowed.") default_hub = tag.default_hub + if tag.unified_hub_requirements_bzl and mod.is_root: + unified_hub_requirements_bzl = True pyproject_toml = tag.pyproject_toml if pyproject_toml: pyproject = read_pyproject(module_ctx, pyproject_toml) @@ -270,6 +273,7 @@ def build_config( }, enable_pipstar_extract = enable_pipstar_extract, toml_decode = toml.decode, + unified_hub_requirements_bzl = unified_hub_requirements_bzl, ) def parse_modules( @@ -482,6 +486,7 @@ You cannot use both the additive_build_content and additive_build_content_file a facts = simpleapi_cache.get_facts(), hub_group_map = hub_group_map, hub_whl_map = hub_whl_map, + unified_hub_requirements_bzl = config.unified_hub_requirements_bzl, whl_libraries = whl_libraries, whl_mods = whl_mods, platform_config_settings = { @@ -535,6 +540,7 @@ def _create_unified_hub_repo(mods): extra_aliases = extra_aliases, hubs = hubs, packages = packages, + requirements_bzl = mods.unified_hub_requirements_bzl, ) def _pip_impl(module_ctx): @@ -753,6 +759,26 @@ This is designed to work with dependency management tools like Renovate. :::{versionadded} 2.3.0 ::: +""", + ), + "unified_hub_requirements_bzl": attr.bool( + default = False, + doc = """\ +Generate a `requirements.bzl` in the [Unified @pypi Hub](unified-pypi-hub) with +the per-package macros of a concrete hub's: `requirement`, `whl_requirement`, +`data_requirement` and `dist_info_requirement`. The labels they return are in +the unified hub, so they follow {flag}`--venv` like `@pypi//`. + +This is a migration aid for a repository whose hub used to be named `pypi`: +after renaming it, its existing `load("@pypi//:requirements.bzl", "requirement")` +calls keep working and can move to `@pypi//` labels one at a time. The +`all_*` lists are not generated, since they are fixed at loading time and could +only list one hub's packages; load those from a concrete hub. + +Only the root module's value is used. + +:::{versionadded} VERSION_NEXT_FEATURE +::: """, ), "whl_abi_tags": attr.string_list( diff --git a/python/private/pypi/unified_hub_repo.bzl b/python/private/pypi/unified_hub_repo.bzl index 2a20267d6a..ddd15c0d86 100644 --- a/python/private/pypi/unified_hub_repo.bzl +++ b/python/private/pypi/unified_hub_repo.bzl @@ -7,10 +7,7 @@ _ROOT_BUILD_TMPL = """\ load("@rules_python//python/private/pypi:unified_hub_setup.bzl", "define_venv_flag_config_settings") package(default_visibility = ["//visibility:public"]) - -# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it -exports_files(["requirements.bzl"]) - +{exports_requirements_bzl} define_venv_flag_config_settings( name = "venv_config_settings", hubs = {hubs}, @@ -31,6 +28,11 @@ define_pypi_package_targets( ) """ +_EXPORTS_REQUIREMENTS_BZL = """ +# Ensure the `requirements.bzl` source can be accessed by stardoc, since users load() from it +exports_files(["requirements.bzl"]) +""" + # The per-package macros of a concrete hub's requirements.bzl. The `all_*` # lists are left out on purpose: they are fixed at loading time, before the # venv flag is known, so they could only ever list one hub's packages. @@ -62,7 +64,10 @@ def _unified_hub_repo_impl(rctx): # 1. Generate Root BUILD.bazel with shared config settings rctx.file( "BUILD.bazel", - _ROOT_BUILD_TMPL.format(hubs = hubs), + _ROOT_BUILD_TMPL.format( + exports_requirements_bzl = _EXPORTS_REQUIREMENTS_BZL if rctx.attr.requirements_bzl else "", + hubs = hubs, + ), ) # 2. Organize extra aliases by package @@ -87,8 +92,8 @@ def _unified_hub_repo_impl(rctx): ), ) - # 4. Generate requirements.bzl with a concrete hub's per-package macros, so - # a hub renamed away from the reserved "pypi" name keeps its + # 4. Optionally generate requirements.bzl with a concrete hub's per-package + # macros, so a hub renamed away from the reserved "pypi" name keeps its # `load("@pypi//:requirements.bzl", "requirement")` users working. The # macros return labels in this repo, so they route through the venv flag # like any other `@pypi//` label. @@ -97,11 +102,12 @@ def _unified_hub_repo_impl(rctx): # always uniquely identify a repository, as the labels are being passed as # a string and the resolution of the label happens at the call-site of the # `requirement`, et al. macros. - macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) - rctx.file( - "requirements.bzl", - _REQUIREMENTS_BZL_TMPL.replace("%%MACRO_TMPL%%", macro_tmpl), - ) + if rctx.attr.requirements_bzl: + macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) + rctx.file( + "requirements.bzl", + _REQUIREMENTS_BZL_TMPL.replace("%%MACRO_TMPL%%", macro_tmpl), + ) unified_hub_repo = repository_rule( implementation = _unified_hub_repo_impl, @@ -120,11 +126,15 @@ unified_hub_repo = repository_rule( mandatory = True, doc = "Dictionary mapping package names to a list of hubs that contain them.", ), + "requirements_bzl": attr.bool( + default = False, + doc = "Whether to generate a requirements.bzl with the per-package `requirement` macros.", + ), }, doc = "Private repository rule creating the automatic Unified PyPI Hub.", ) -def unified_workspace_hub_repo(name, hubs, default_hub = None, extra_aliases = {}): +def unified_workspace_hub_repo(name, hubs, default_hub = None, extra_aliases = {}, requirements_bzl = False): """Creates a Unified PyPI Hub repository for WORKSPACE mode by loading requirements from hubs. Args: @@ -133,6 +143,8 @@ def unified_workspace_hub_repo(name, hubs, default_hub = None, extra_aliases = { e.g. {"dev_pip": dev_pip_requirements, "pypi_alpha": pypi_alpha_requirements} default_hub: Optional default hub name. extra_aliases: Dictionary mapping 'package:alias' to a list of hubs that support it. + requirements_bzl: Whether to generate a requirements.bzl with the + per-package `requirement` macros. """ packages = {} for hub_name, req_map in hubs.items(): @@ -151,4 +163,5 @@ def unified_workspace_hub_repo(name, hubs, default_hub = None, extra_aliases = { extra_aliases = extra_aliases, hubs = sorted(hubs.keys()), packages = packages, + requirements_bzl = requirements_bzl, ) diff --git a/tests/integration/unified_pypi/MODULE.bazel b/tests/integration/unified_pypi/MODULE.bazel index 6a4b87e015..52cc331f37 100644 --- a/tests/integration/unified_pypi/MODULE.bazel +++ b/tests/integration/unified_pypi/MODULE.bazel @@ -43,8 +43,8 @@ pip.parse( requirements_lock = "//:requirements_b.txt", ) use_repo(pip, "pypi_b") - pip.default(default_hub = "pypi_b") +pip.default(unified_hub_requirements_bzl = True) pip.dep( name = "declared-only-pkg", extra_targets = ["declared-only-alias"], diff --git a/tests/integration/unified_pypi_test.py b/tests/integration/unified_pypi_test.py index bb3c22d45d..08f7b029c1 100644 --- a/tests/integration/unified_pypi_test.py +++ b/tests/integration/unified_pypi_test.py @@ -19,6 +19,25 @@ def test_requirement_macro_default_hub(self): def test_requirement_macro_transitioned_hub(self): self.run_bazel("test", "//:test_requirement_a") + def test_requirements_bzl_requires_opt_in(self): + module_bazel = self.repo_root / "MODULE.bazel" + without_opt_in = module_bazel.read_text().replace( + "pip.default(unified_hub_requirements_bzl = True)\n", + "", + ) + with self._temp_modify_file(module_bazel, without_opt_in): + result = self.run_bazel( + "cquery", "//:test_requirement_default", check=False + ) + self.assertNotEqual( + result.exit_code, + 0, + "Expected loading @pypi//:requirements.bzl to fail without the opt-in", + ) + self.assert_result_matches( + result, r"cannot load '[^']*//:requirements\.bzl': no such file" + ) + def test_cli_override(self): self.run_bazel( "run", diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index 97849e882d..6d7e971132 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -52,6 +52,7 @@ def _default( os_name = None, platform = None, pyproject_toml = None, + unified_hub_requirements_bzl = False, whl_platform_tags = None, whl_abi_tags = None): return struct( @@ -66,6 +67,7 @@ def _default( os_name = os_name, platform = platform, pyproject_toml = pyproject_toml, + unified_hub_requirements_bzl = unified_hub_requirements_bzl, whl_abi_tags = whl_abi_tags or [], whl_platform_tags = whl_platform_tags or [], ) @@ -120,6 +122,7 @@ def _parse_modules(env, **kwargs): exposed_packages = subjects.dict, hub_group_map = subjects.dict, hub_whl_map = subjects.dict, + unified_hub_requirements_bzl = subjects.bool, whl_libraries = subjects.dict, whl_mods = subjects.dict, ), @@ -497,6 +500,66 @@ def _test_default_hub_precedence(env): _tests.append(_test_default_hub_precedence) +def _parse_unified_hub_requirements_bzl(env, *modules): + return _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + os_name = "linux", + arch_name = "x86_64", + *modules + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + ) + +def _unified_hub_requirements_bzl_mod(name, is_root, unified_hub_requirements_bzl): + return _mod( + name = name, + is_root = is_root, + default = _default_tags_default + [ + _default(unified_hub_requirements_bzl = unified_hub_requirements_bzl), + ], + parse = [ + _parse( + hub_name = name + "_pypi", + python_version = "3.15", + simpleapi_skip = ["simple"], + requirements_lock = "requirements.txt", + ), + ], + ) + +def _test_unified_hub_requirements_bzl_off_by_default(env): + pypi = _parse_unified_hub_requirements_bzl( + env, + _unified_hub_requirements_bzl_mod("root", is_root = True, unified_hub_requirements_bzl = False), + ) + pypi.unified_hub_requirements_bzl().equals(False) + +_tests.append(_test_unified_hub_requirements_bzl_off_by_default) + +def _test_unified_hub_requirements_bzl_root_opt_in(env): + pypi = _parse_unified_hub_requirements_bzl( + env, + _unified_hub_requirements_bzl_mod("root", is_root = True, unified_hub_requirements_bzl = True), + ) + pypi.unified_hub_requirements_bzl().equals(True) + +_tests.append(_test_unified_hub_requirements_bzl_root_opt_in) + +def _test_unified_hub_requirements_bzl_ignored_outside_root(env): + """Only the root module decides whether the unified hub gets a requirements.bzl.""" + pypi = _parse_unified_hub_requirements_bzl( + env, + _unified_hub_requirements_bzl_mod("root", is_root = True, unified_hub_requirements_bzl = False), + _unified_hub_requirements_bzl_mod("rules_python", is_root = False, unified_hub_requirements_bzl = True), + ) + pypi.unified_hub_requirements_bzl().equals(False) + +_tests.append(_test_unified_hub_requirements_bzl_ignored_outside_root) + def _test_extension_dep(env): pypi = _parse_modules( env, From 371c28c2f5ebca3aa46555092f75ff366ed2b2c2 Mon Sep 17 00:00:00 2001 From: Ted Kaplan Date: Tue, 6 Oct 2026 20:24:09 +0000 Subject: [PATCH 5/5] style: restore the blank line buildifier 8.2.1 expects in the unified_pypi MODULE.bazel Co-Authored-By: Claude Opus 5.5 --- tests/integration/unified_pypi/MODULE.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/unified_pypi/MODULE.bazel b/tests/integration/unified_pypi/MODULE.bazel index 52cc331f37..6dd2e25dd5 100644 --- a/tests/integration/unified_pypi/MODULE.bazel +++ b/tests/integration/unified_pypi/MODULE.bazel @@ -43,6 +43,7 @@ pip.parse( requirements_lock = "//:requirements_b.txt", ) use_repo(pip, "pypi_b") + pip.default(default_hub = "pypi_b") pip.default(unified_hub_requirements_bzl = True) pip.dep(