From db6634e52d9b85ea5df941a3758863caac442c7c Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Wed, 28 Oct 2026 16:51:40 +0000 Subject: [PATCH] fix(pypi): allow disabling Simple API downloader with index_url = "" In rules_python 2.0.0, the Simple API downloader was enabled by default by falling back to `https://pypi.org/simple` in `build_config`. While `hub_builder.bzl` retains the fallback to `pip` when `default_index_url` is empty, `pip.default(index_url = "")` could not be used to trigger it because `_configure` ignores falsy attribute values (and `attr.string()` defaults to `""` when omitted). Set the default of `pip.default.index_url` to `"https://pypi.org/simple"` on the tag attribute and record any non-default value (including `""`) in `build_config`. Also apply `envsubst` to `self._config.index_url` in `hub_builder.bzl` so an environment variable that expands to `""` disables the Simple API downloader in the same way as `pip.parse.experimental_index_url`. --- news/pip-default-empty-index-url.fixed.md | 2 + python/private/pypi/extension.bzl | 19 +++++++- python/private/pypi/hub_builder.bzl | 6 ++- tests/pypi/extension/extension_tests.bzl | 55 ++++++++++++++++++++++- 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 news/pip-default-empty-index-url.fixed.md diff --git a/news/pip-default-empty-index-url.fixed.md b/news/pip-default-empty-index-url.fixed.md new file mode 100644 index 0000000000..ef2341d615 --- /dev/null +++ b/news/pip-default-empty-index-url.fixed.md @@ -0,0 +1,2 @@ +(pypi) Fixed {obj}`pip.default` so setting `index_url = ""` disables the Simple +API downloader and falls back to `pip` instead of being ignored. diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl index 1b827806f2..9bfbac1311 100644 --- a/python/private/pypi/extension.bzl +++ b/python/private/pypi/extension.bzl @@ -33,6 +33,8 @@ load(":simpleapi_download.bzl", "simpleapi_download") load(":unified_hub_repo.bzl", "unified_hub_repo") load(":whl_library.bzl", "whl_library") +_DEFAULT_INDEX_URL = "https://pypi.org/simple" + def _whl_mods_impl(whl_mods_dict): """Implementation of the pip.whl_mods tag class. @@ -249,10 +251,14 @@ def build_config( if platform and not (tag.arch_name or tag.config_settings or tag.env or tag.os_name or tag.whl_abi_tags or tag.whl_platform_tags or tag.marker): defaults["platforms"].pop(platform) + if tag.index_url != _DEFAULT_INDEX_URL and ( + mod.is_root or "index_url" not in defaults + ): + defaults["index_url"] = tag.index_url + _configure( defaults, override = mod.is_root, - index_url = tag.index_url, # extra values that we just add auth_patterns = tag.auth_patterns, netrc = tag.netrc, @@ -264,7 +270,7 @@ def build_config( return struct( auth_patterns = defaults.get("auth_patterns", {}), default_hub = default_hub, - index_url = defaults.get("index_url", "https://pypi.org/simple").rstrip("/"), + index_url = defaults.get("index_url", _DEFAULT_INDEX_URL).rstrip("/"), netrc = defaults.get("netrc", None), python_version = defaults.get("python_version", None), platforms = { @@ -687,6 +693,7 @@ Supported keys: """, ), "index_url": attr.string( + default = _DEFAULT_INDEX_URL, doc = """\ The index URL to use as a default when downloading packages from PyPI. This is used if nothing is specified via `--index-url` or `--extra-index-url` parameters in the `requirements.txt` file or via @@ -701,6 +708,9 @@ The indexes must support Simple API as described here: Index metadata will be used to get the hash digest values for packages even if the `--hash` values are not present in the requirements.txt lock file. +Set to `""` to disable the Simple API downloader and fall back to using `pip` +to download wheels. + Defaults to `https://pypi.org/simple`. :::{versionadded} 2.0.0 @@ -708,6 +718,11 @@ This has been added as a replacement for {obj}`pip.parse.experimental_index_url` and {obj}`pip.parse.experimental_extra_index_urls`. ::: + +:::{versionchanged} VERSION_NEXT_PATCH +Setting `index_url = ""` now disables the Simple API downloader instead of +being ignored. +::: """, ), "marker": attr.string( diff --git a/python/private/pypi/hub_builder.bzl b/python/private/pypi/hub_builder.bzl index cbf4877d65..0eab936701 100644 --- a/python/private/pypi/hub_builder.bzl +++ b/python/private/pypi/hub_builder.bzl @@ -377,7 +377,11 @@ def _set_get_index_urls(self, mctx, pip_attr): pip_attr.experimental_index_url, pip_attr.envsubst, mctx.getenv, - ) or self._config.index_url + ) or envsubst( + self._config.index_url, + pip_attr.envsubst, + mctx.getenv, + ) default_extra_index_urls = pip_attr.experimental_extra_index_urls or [] if not default_index_url: diff --git a/tests/pypi/extension/extension_tests.bzl b/tests/pypi/extension/extension_tests.bzl index b3a2ebf975..64993afda5 100644 --- a/tests/pypi/extension/extension_tests.bzl +++ b/tests/pypi/extension/extension_tests.bzl @@ -46,7 +46,7 @@ def _default( config_settings = None, default_hub = "", env = None, - index_url = None, + index_url = "https://pypi.org/simple", marker = None, netrc = None, os_name = None, @@ -61,7 +61,7 @@ def _default( config_settings = config_settings, default_hub = default_hub, env = env or {}, - index_url = index_url or "", + index_url = index_url, marker = marker or "", netrc = netrc, os_name = os_name, @@ -761,6 +761,57 @@ simple==0.0.1 \ _tests.append(_test_default_index_setting) +def _test_default_index_url_empty_disables_simpleapi(env): + def fail_simpleapi_download(*_args, **_kwargs): + fail("simpleapi_download should not be called when index_url is empty") + + for index_url, envsubst, want_whl_library in [ + ("", [], {}), + ( + "$RULES_PYTHON_PIP_INDEX_URL", + ["RULES_PYTHON_PIP_INDEX_URL"], + {"envsubst": ["RULES_PYTHON_PIP_INDEX_URL"]}, + ), + ]: + pypi = _parse_modules( + env, + module_ctx = _pypi_mock_mctx( + _mod( + name = "my_module", + default = _default_tags_default + [ + _default(index_url = index_url), + _default(default_hub = "pypi_a"), + ], + parse = [ + _parse( + hub_name = "pypi_a", + python_version = "3.15", + envsubst = envsubst, + requirements_lock = "requirements.txt", + ), + ], + ), + os_name = "linux", + arch_name = "x86_64", + ), + available_interpreters = { + "python_3_15_host": "unit_test_interpreter_target", + }, + minor_mapping = {"3.15": "3.15.19"}, + simpleapi_download = fail_simpleapi_download, + ) + + pypi.whl_libraries().contains_exactly({ + "pypi_a_315_simple": { + "config_load": "@pypi_a//:config.bzl", + "dep_template": "@pypi_a//{name}:{target}", + "python_interpreter_target": "unit_test_interpreter_target", + "requirement": "simple==0.0.1 --hash=sha256:deadbeef --hash=sha256:deadbaaf", + } | want_whl_library, + }) + +_tests.append(_test_default_index_url_empty_disables_simpleapi) + def extension_test_suite(name): """Create the test suite.