Skip to content

Commit 9a6079c

Browse files
committed
update CHANGELOG and the implementation
1 parent b0db732 commit 9a6079c

File tree

6 files changed

+184
-97
lines changed

6 files changed

+184
-97
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ Unreleased changes template.
6161

6262
{#v0-0-0-added}
6363
### Added
64-
* (pypi) Added version-based fetching in `_add_dists` when SHA256 hashes are unavailable, with unit tests in `//tests/pypi/parse_requirements`[#2023](https://github.com/bazel-contrib/rules_python/issues/2023).
64+
* (pypi) From now on `sha256` values in the `requirements.txt` is no longer
65+
mandatory when enabling {attr}`pip.parse.experimental_index_url` feature.
66+
This means that `rules_python` will attempt to fetch metadata for all
67+
packages through SimpleAPI unless they are pulled through direct URL
68+
references. Fixes [#2023](https://github.com/bazel-contrib/rules_python/issues/2023).
6569

6670
{#v0-0-0-removed}
6771
### Removed

docs/pypi-dependencies.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,13 @@ This does not mean that `rules_python` is fetching the wheels eagerly, but it
386386
rather means that it is calling the PyPI server to get the Simple API response
387387
to get the list of all available source and wheel distributions. Once it has
388388
got all of the available distributions, it will select the right ones depending
389-
on the `sha256` values in your `requirements_lock.txt` file. The compatible
390-
distribution URLs will be then written to the `MODULE.bazel.lock` file. Currently
391-
users wishing to use the lock file with `rules_python` with this feature have
392-
to set an environment variable `RULES_PYTHON_OS_ARCH_LOCK_FILE=0` which will
393-
become default in the next release.
389+
on the `sha256` values in your `requirements_lock.txt` file. If `sha256` hashes
390+
are not present in the requirements file, we will fallback to matching by version
391+
specified in the lock file. The compatible distribution URLs will be then
392+
written to the `MODULE.bazel.lock` file. Currently users wishing to use the
393+
lock file with `rules_python` with this feature have to set an environment
394+
variable `RULES_PYTHON_OS_ARCH_LOCK_FILE=0` which will become default in the
395+
next release.
394396

395397
Fetching the distribution information from the PyPI allows `rules_python` to
396398
know which `whl` should be used on which target platform and it will determine

python/private/pypi/parse_requirements.bzl

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def parse_requirements(
184184
req.distribution: None
185185
for reqs in requirements_by_platform.values()
186186
for req in reqs.values()
187-
if req.srcs.shas
187+
if not req.srcs.url
188188
}),
189189
)
190190

@@ -316,45 +316,27 @@ def _add_dists(*, requirement, index_urls, logger = None):
316316
sdist = None
317317

318318
# First try to find distributions by SHA256 if provided
319-
if requirement.srcs.shas:
320-
for sha256 in requirement.srcs.shas:
321-
# For now if the artifact is marked as yanked we just ignore it.
322-
#
323-
# See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
324-
325-
maybe_whl = index_urls.whls.get(sha256)
326-
if maybe_whl and not maybe_whl.yanked:
327-
whls.append(maybe_whl)
328-
continue
329-
330-
maybe_sdist = index_urls.sdists.get(sha256)
331-
if maybe_sdist and not maybe_sdist.yanked:
332-
sdist = maybe_sdist
333-
continue
334-
335-
if logger:
336-
logger.warn(lambda: "Could not find a whl or an sdist with sha256={}".format(sha256))
337-
else:
338-
# If no SHA256s provided, try to find distributions by version
339-
version = requirement.srcs.version
340-
if version:
341-
# Look for wheels matching the version
342-
for whl in index_urls.whls.values():
343-
# Extract package name from wheel filename (format: package_name-version-python_tag-abi_tag-platform_tag.whl)
344-
whl_name = whl.filename.split("-")[0]
345-
if whl_name == requirement.distribution and whl.version == version and not whl.yanked:
346-
whls.append(whl)
347-
348-
# Look for source distributions matching the version
349-
for sdist_dist in index_urls.sdists.values():
350-
# Extract package name from sdist filename (format: package_name-version.tar.gz or package_name-version.zip)
351-
sdist_name = sdist_dist.filename.split("-")[0]
352-
if sdist_name == requirement.distribution and sdist_dist.version == version and not sdist_dist.yanked:
353-
sdist = sdist_dist
354-
break
355-
356-
if not whls and not sdist and logger:
357-
logger.warn(lambda: "Could not find any distributions for version={}".format(version))
319+
shas_to_use = requirement.srcs.shas
320+
if not shas_to_use:
321+
shas_to_use = index_urls.sha256s_by_version.get(requirement.srcs.version, [])
322+
323+
for sha256 in shas_to_use:
324+
# For now if the artifact is marked as yanked we just ignore it.
325+
#
326+
# See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
327+
328+
maybe_whl = index_urls.whls.get(sha256)
329+
if maybe_whl and not maybe_whl.yanked:
330+
whls.append(maybe_whl)
331+
continue
332+
333+
maybe_sdist = index_urls.sdists.get(sha256)
334+
if maybe_sdist and not maybe_sdist.yanked:
335+
sdist = maybe_sdist
336+
continue
337+
338+
if logger:
339+
logger.warn(lambda: "Could not find a whl or an sdist with sha256={}".format(sha256))
358340

359341
yanked = {}
360342
for dist in whls + [sdist]:

python/private/pypi/parse_simpleapi_html.bzl

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def parse_simpleapi_html(*, url, content):
2626
Returns:
2727
A list of structs with:
2828
* filename: The filename of the artifact.
29+
* version: The version of the artifact.
2930
* url: The URL to download the artifact.
3031
* sha256: The sha256 of the artifact.
3132
* metadata_sha256: The whl METADATA sha256 if we can download it. If this is
@@ -51,15 +52,20 @@ def parse_simpleapi_html(*, url, content):
5152

5253
# Each line follows the following pattern
5354
# <a href="https://...#sha256=..." attribute1="foo" ... attributeN="bar">filename</a><br />
55+
sha256_by_version = {}
5456
for line in lines[1:]:
5557
dist_url, _, tail = line.partition("#sha256=")
58+
dist_url = _absolute_url(url, dist_url)
59+
5660
sha256, _, tail = tail.partition("\"")
5761

5862
# See https://packaging.python.org/en/latest/specifications/simple-repository-api/#adding-yank-support-to-the-simple-api
5963
yanked = "data-yanked" in line
6064

6165
head, _, _ = tail.rpartition("</a>")
6266
maybe_metadata, _, filename = head.rpartition(">")
67+
version = _version(filename)
68+
sha256_by_version.setdefault(version, []).append(sha256)
6369

6470
metadata_sha256 = ""
6571
metadata_url = ""
@@ -75,7 +81,8 @@ def parse_simpleapi_html(*, url, content):
7581
if filename.endswith(".whl"):
7682
whls[sha256] = struct(
7783
filename = filename,
78-
url = _absolute_url(url, dist_url),
84+
version = version,
85+
url = dist_url,
7986
sha256 = sha256,
8087
metadata_sha256 = metadata_sha256,
8188
metadata_url = _absolute_url(url, metadata_url) if metadata_url else "",
@@ -84,7 +91,8 @@ def parse_simpleapi_html(*, url, content):
8491
else:
8592
sdists[sha256] = struct(
8693
filename = filename,
87-
url = _absolute_url(url, dist_url),
94+
version = version,
95+
url = dist_url,
8896
sha256 = sha256,
8997
metadata_sha256 = "",
9098
metadata_url = "",
@@ -94,8 +102,31 @@ def parse_simpleapi_html(*, url, content):
94102
return struct(
95103
sdists = sdists,
96104
whls = whls,
105+
sha256_by_version = sha256_by_version,
97106
)
98107

108+
_SDIST_EXTS = [
109+
".tar", # handles any compression
110+
".zip",
111+
]
112+
113+
def _version(filename):
114+
# See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#binary-distribution-format
115+
116+
_, _, tail = filename.partition("-")
117+
version, _, _ = tail.partition("-")
118+
if version != tail:
119+
# The format is {name}-{version}-{whl_specifiers}.whl
120+
return version
121+
122+
# NOTE @aignas 2025-03-29: most of the files are wheels, so this is not the common path
123+
124+
# {name}-{version}.{ext}
125+
for ext in _SDIST_EXTS:
126+
version, _, _ = version.partition(ext) # build or name
127+
128+
return version
129+
99130
def _get_root_directory(url):
100131
scheme_end = url.find("://")
101132
if scheme_end == -1:

tests/pypi/extension/extension_tests.bzl

Lines changed: 94 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,21 @@ def _test_simple_get_index(env):
461461
),
462462
},
463463
),
464+
"some_other_pkg": struct(
465+
whls = {
466+
"deadb33f": struct(
467+
yanked = False,
468+
filename = "some-other-pkg-0.0.1-py3-none-any.whl",
469+
sha256 = "deadb33f",
470+
url = "example2.org/index/some_other_pkg/",
471+
),
472+
},
473+
sdists = {},
474+
sha256s_by_version = {
475+
"0.0.1": ["deadb33f"],
476+
"0.0.3": ["deadbeef"],
477+
},
478+
),
464479
}
465480

466481
pypi = _parse_modules(
@@ -485,7 +500,10 @@ def _test_simple_get_index(env):
485500
simple==0.0.1 \
486501
--hash=sha256:deadbeef \
487502
--hash=sha256:deadb00f
488-
some_pkg==0.0.1
503+
some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl \
504+
--hash=sha256:deadbaaf
505+
some_other_pkg==0.0.1
506+
pip_fallback==0.0.1
489507
""",
490508
}[x],
491509
),
@@ -496,42 +514,71 @@ some_pkg==0.0.1
496514
)
497515

498516
pypi.is_reproducible().equals(False)
499-
pypi.exposed_packages().contains_exactly({"pypi": ["simple", "some_pkg"]})
517+
pypi.exposed_packages().contains_exactly({"pypi": ["pip_fallback", "simple", "some_other_pkg", "some_pkg"]})
500518
pypi.hub_group_map().contains_exactly({"pypi": {}})
501519
pypi.hub_whl_map().contains_exactly({
502520
"pypi": {
521+
"pip_fallback": {
522+
"pypi_315_pip_fallback": [
523+
struct(
524+
config_setting = None,
525+
filename = None,
526+
target_platforms = None,
527+
version = "3.15",
528+
),
529+
],
530+
},
503531
"simple": {
504532
"pypi_315_simple_py3_none_any_deadb00f": [
505-
whl_config_setting(
533+
struct(
534+
config_setting = None,
506535
filename = "simple-0.0.1-py3-none-any.whl",
536+
target_platforms = None,
507537
version = "3.15",
508538
),
509539
],
510540
"pypi_315_simple_sdist_deadbeef": [
511-
whl_config_setting(
541+
struct(
542+
config_setting = None,
512543
filename = "simple-0.0.1.tar.gz",
544+
target_platforms = None,
545+
version = "3.15",
546+
),
547+
],
548+
},
549+
"some_other_pkg": {
550+
"pypi_315_some_py3_none_any_deadb33f": [
551+
struct(
552+
config_setting = None,
553+
filename = "some-other-pkg-0.0.1-py3-none-any.whl",
554+
target_platforms = None,
513555
version = "3.15",
514556
),
515557
],
516558
},
517559
"some_pkg": {
518-
"pypi_315_some_pkg": [whl_config_setting(version = "3.15")],
560+
"pypi_315_some_pkg_py3_none_any_deadbaaf": [
561+
struct(
562+
config_setting = None,
563+
filename = "some_pkg-0.0.1-py3-none-any.whl",
564+
target_platforms = None,
565+
version = "3.15",
566+
),
567+
],
519568
},
520569
},
521570
})
522571
pypi.whl_libraries().contains_exactly({
572+
"pypi_315_pip_fallback": {
573+
"dep_template": "@pypi//{name}:{target}",
574+
"extra_pip_args": ["--extra-args-for-sdist-building"],
575+
"python_interpreter_target": "unit_test_interpreter_target",
576+
"repo": "pypi_315",
577+
"requirement": "pip_fallback==0.0.1",
578+
},
523579
"pypi_315_simple_py3_none_any_deadb00f": {
524580
"dep_template": "@pypi//{name}:{target}",
525-
"experimental_target_platforms": [
526-
"cp315_linux_aarch64",
527-
"cp315_linux_arm",
528-
"cp315_linux_ppc",
529-
"cp315_linux_s390x",
530-
"cp315_linux_x86_64",
531-
"cp315_osx_aarch64",
532-
"cp315_osx_x86_64",
533-
"cp315_windows_x86_64",
534-
],
581+
"experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"],
535582
"filename": "simple-0.0.1-py3-none-any.whl",
536583
"python_interpreter_target": "unit_test_interpreter_target",
537584
"repo": "pypi_315",
@@ -541,16 +588,7 @@ some_pkg==0.0.1
541588
},
542589
"pypi_315_simple_sdist_deadbeef": {
543590
"dep_template": "@pypi//{name}:{target}",
544-
"experimental_target_platforms": [
545-
"cp315_linux_aarch64",
546-
"cp315_linux_arm",
547-
"cp315_linux_ppc",
548-
"cp315_linux_s390x",
549-
"cp315_linux_x86_64",
550-
"cp315_osx_aarch64",
551-
"cp315_osx_x86_64",
552-
"cp315_windows_x86_64",
553-
],
591+
"experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"],
554592
"extra_pip_args": ["--extra-args-for-sdist-building"],
555593
"filename": "simple-0.0.1.tar.gz",
556594
"python_interpreter_target": "unit_test_interpreter_target",
@@ -559,29 +597,43 @@ some_pkg==0.0.1
559597
"sha256": "deadbeef",
560598
"urls": ["example.org"],
561599
},
562-
# We are falling back to regular `pip`
563-
"pypi_315_some_pkg": {
600+
"pypi_315_some_pkg_py3_none_any_deadbaaf": {
564601
"dep_template": "@pypi//{name}:{target}",
565-
"extra_pip_args": ["--extra-args-for-sdist-building"],
602+
"experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"],
603+
"filename": "some_pkg-0.0.1-py3-none-any.whl",
566604
"python_interpreter_target": "unit_test_interpreter_target",
567605
"repo": "pypi_315",
568-
"requirement": "some_pkg==0.0.1",
606+
"requirement": "some_pkg==0.0.1 @ example-direct.org/some_pkg-0.0.1-py3-none-any.whl --hash=sha256:deadbaaf",
607+
"sha256": "deadbaaf",
608+
"urls": ["example-direct.org/some_pkg-0.0.1-py3-none-any.whl"],
609+
},
610+
"pypi_315_some_py3_none_any_deadb33f": {
611+
"dep_template": "@pypi//{name}:{target}",
612+
"experimental_target_platforms": ["cp315_linux_aarch64", "cp315_linux_arm", "cp315_linux_ppc", "cp315_linux_s390x", "cp315_linux_x86_64", "cp315_osx_aarch64", "cp315_osx_x86_64", "cp315_windows_x86_64"],
613+
"filename": "some-other-pkg-0.0.1-py3-none-any.whl",
614+
"python_interpreter_target": "unit_test_interpreter_target",
615+
"repo": "pypi_315",
616+
"requirement": "some_other_pkg==0.0.1",
617+
"sha256": "deadb33f",
618+
"urls": ["example2.org/index/some_other_pkg/"],
569619
},
570620
})
571621
pypi.whl_mods().contains_exactly({})
572-
env.expect.that_dict(got_simpleapi_download_kwargs).contains_exactly({
573-
"attr": struct(
574-
auth_patterns = {},
575-
envsubst = {},
576-
extra_index_urls = [],
577-
index_url = "pypi.org",
578-
index_url_overrides = {},
579-
netrc = None,
580-
sources = ["simple"],
581-
),
582-
"cache": {},
583-
"parallel_download": False,
584-
})
622+
env.expect.that_dict(got_simpleapi_download_kwargs).contains_exactly(
623+
{
624+
"attr": struct(
625+
auth_patterns = {},
626+
envsubst = {},
627+
extra_index_urls = [],
628+
index_url = "pypi.org",
629+
index_url_overrides = {},
630+
netrc = None,
631+
sources = ["simple", "pip_fallback", "some_other_pkg"],
632+
),
633+
"cache": {},
634+
"parallel_download": False,
635+
},
636+
)
585637

586638
_tests.append(_test_simple_get_index)
587639

0 commit comments

Comments
 (0)