From af5aeb7cdd5aa3be60760830d07ff812560ed3c2 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sat, 8 Mar 2025 19:59:29 +0800 Subject: [PATCH 01/13] extract out url and filename todo: check later if filename is necessary. Maybe not. --- python/private/pypi/index_sources.bzl | 16 ++++++++++- .../index_sources/index_sources_tests.bzl | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 8b3c300946..13fa15df87 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -32,6 +32,8 @@ def index_sources(line): * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can be given to `pip` to install a package. + * `url` - str; URL if the requirement specifies a direct URL. + * `filename` - str; filename if URL is present, extracted from the URL. """ line = line.replace("\\", " ") head, _, maybe_hashes = line.partition(";") @@ -55,9 +57,19 @@ def index_sources(line): requirement, " ".join(["--hash=sha256:{}".format(sha) for sha in shas]), ).strip() + + # Extract URL if present + url = "" + filename = "" if "@" in head: requirement = requirement_line - shas = [] + # Extract URL from direct URL format + url = requirement.split("@")[1].split("#")[0].strip() + # Extract filename from URL + if url: + filename = url.rpartition("/")[2] + if not filename: + filename = url.rpartition("/")[0].rpartition("/")[2] return struct( requirement = requirement, @@ -65,4 +77,6 @@ def index_sources(line): version = version, shas = sorted(shas), marker = marker, + url = url, + filename = filename, ) diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 440957e2f0..504e5c2ad9 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -24,27 +24,45 @@ def _test_no_simple_api_sources(env): "foo==0.0.1": struct( requirement = "foo==0.0.1", marker = "", + url = "", + filename = "", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", marker = "", + url = "https://someurl.org", + filename = "someurl.org", ), - "foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef": struct( - requirement = "foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef", + "foo==0.0.1 @ https://someurl.org/package.whl": struct( + requirement = "foo==0.0.1 @ https://someurl.org/package.whl", marker = "", + url = "https://someurl.org/package.whl", + filename = "package.whl", ), - "foo==0.0.1 @ https://someurl.org; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( - requirement = "foo==0.0.1 @ https://someurl.org --hash=sha256:deadbeef", + "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct( + requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", + marker = "", + url = "https://someurl.org/package.whl", + filename = "package.whl", + shas = ["deadbeef"], + ), + "foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( + requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", + url = "https://someurl.org/package.whl", + filename = "package.whl", + shas = ["deadbeef"], ), } for input, want in inputs.items(): got = index_sources(input) - env.expect.that_collection(got.shas).contains_exactly([]) + env.expect.that_collection(got.shas).contains_exactly(want.shas if hasattr(want, "shas") else []) env.expect.that_str(got.version).equals("0.0.1") env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement) env.expect.that_str(got.marker).equals(want.marker) + env.expect.that_str(got.url).equals(want.url) + env.expect.that_str(got.filename).equals(want.filename) _tests.append(_test_no_simple_api_sources) From 8a3480de0e6afbe8178fc26a6e615b5fecf67ed2 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 13:51:14 +0800 Subject: [PATCH 02/13] simplify logic to reduce test failures --- python/private/pypi/index_sources.bzl | 16 +++++++++++----- tests/pypi/index_sources/index_sources_tests.bzl | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 13fa15df87..b1dba5e50f 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -59,24 +59,30 @@ def index_sources(line): ).strip() # Extract URL if present - url = "" - filename = "" if "@" in head: requirement = requirement_line # Extract URL from direct URL format - url = requirement.split("@")[1].split("#")[0].strip() + url = requirement.partition("@")[2].strip().partition(" ")[0].strip() # Extract filename from URL if url: filename = url.rpartition("/")[2] if not filename: filename = url.rpartition("/")[0].rpartition("/")[2] + return struct( + requirement = requirement, + requirement_line = requirement_line, + version = version, + shas = sorted(shas), + marker = marker, + url = url, + filename = filename, + ) + return struct( requirement = requirement, requirement_line = requirement_line, version = version, shas = sorted(shas), marker = marker, - url = url, - filename = filename, ) diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 504e5c2ad9..45c94d8256 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -24,8 +24,6 @@ def _test_no_simple_api_sources(env): "foo==0.0.1": struct( requirement = "foo==0.0.1", marker = "", - url = "", - filename = "", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", @@ -61,8 +59,10 @@ def _test_no_simple_api_sources(env): env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement) env.expect.that_str(got.marker).equals(want.marker) - env.expect.that_str(got.url).equals(want.url) - env.expect.that_str(got.filename).equals(want.filename) + if hasattr(want, "url"): + env.expect.that_str(got.url).equals(want.url) + if hasattr(want, "filename"): + env.expect.that_str(got.filename).equals(want.filename) _tests.append(_test_no_simple_api_sources) From 2128d54cd4697aebb4a42a98e8457911acc3cd3a Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 13:52:56 +0800 Subject: [PATCH 03/13] return direct url in parse_requirements --- python/private/pypi/parse_requirements.bzl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 2bca8d8621..0c4bad1f40 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -292,6 +292,18 @@ def _add_dists(*, requirement, index_urls, logger = None): index_urls: The result of simpleapi_download. logger: A logger for printing diagnostic info. """ + # Handle direct URLs in requirements + if hasattr(requirement.srcs, "url"): + # Create a struct that matches the expected format for direct URLs + direct_url_dist = struct( + url = requirement.srcs.url, + filename = requirement.srcs.filename, + # TODO: if more than one hash is present, we should use all of them + sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", # Use hash if provided + yanked = False, + ) + return [direct_url_dist], None + if not index_urls: return [], None From 6f50f42935418b4469e4aeb90b110272aca6cb66 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 13:56:39 +0800 Subject: [PATCH 04/13] add test for parse_requirements_tests --- .../parse_requirements_tests.bzl | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 77e22b825a..86861d69e0 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -26,7 +26,9 @@ foo==0.0.1 \ --hash=sha256:deadb00f """, "requirements_direct": """\ -foo[extra] @ https://some-url +foo[extra] @ https://some-url/package.whl +bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef +baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f """, "requirements_extra_args": """\ --index-url=example.org @@ -124,6 +126,90 @@ def _test_simple(env): _tests.append(_test_simple) +def _test_direct_urls(env): + got = parse_requirements( + ctx = _mock_ctx(), + requirements_by_platform = { + "requirements_direct": ["linux_x86_64"], + }, + ) + env.expect.that_dict(got).contains_exactly({ + "foo": [ + struct( + distribution = "foo", + extra_pip_args = [], + sdist = None, + is_exposed = True, + srcs = struct( + marker = "", + requirement = "foo[extra] @ https://some-url/package.whl", + requirement_line = "foo[extra] @ https://some-url/package.whl", + shas = [], + version = "", + url = "https://some-url/package.whl", + filename = "package.whl", + ), + target_platforms = ["linux_x86_64"], + whls = [struct( + url = "https://some-url/package.whl", + filename = "package.whl", + sha256 = "", + yanked = False, + )], + ), + ], + "bar": [ + struct( + distribution = "bar", + extra_pip_args = [], + sdist = None, + is_exposed = True, + srcs = struct( + marker = "", + requirement = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef", + requirement_line = "bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef", + shas = ["deadbeef"], + version = "", + url = "https://example.org/bar-1.0.whl", + filename = "bar-1.0.whl", + ), + target_platforms = ["linux_x86_64"], + whls = [struct( + url = "https://example.org/bar-1.0.whl", + filename = "bar-1.0.whl", + sha256 = "deadbeef", + yanked = False, + )], + ), + ], + "baz": [ + struct( + distribution = "baz", + extra_pip_args = [], + sdist = None, + is_exposed = True, + srcs = struct( + marker = "python_version < \"3.8\"", + requirement = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f", + requirement_line = "baz @ https://test.com/baz-2.0.whl --hash=sha256:deadb00f", + shas = ["deadb00f"], + version = "", + url = "https://test.com/baz-2.0.whl", + filename = "baz-2.0.whl", + ), + target_platforms = ["linux_x86_64"], + whls = [struct( + url = "https://test.com/baz-2.0.whl", + filename = "baz-2.0.whl", + sha256 = "deadb00f", + yanked = False, + )], + ), + ], + }) + +_tests.append(_test_direct_urls) + def _test_extra_pip_args(env): got = parse_requirements( ctx = _mock_ctx(), From 908b5e582a1867b00cabcd86333dc025515e231f Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 14:05:36 +0800 Subject: [PATCH 05/13] improve readability --- python/private/pypi/index_sources.bzl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index b1dba5e50f..76401d6b00 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -62,7 +62,8 @@ def index_sources(line): if "@" in head: requirement = requirement_line # Extract URL from direct URL format - url = requirement.partition("@")[2].strip().partition(" ")[0].strip() + _, _, url_and_rest = requirement.partition("@") + url = url_and_rest.strip().partition(" ")[0].strip() # Extract filename from URL if url: filename = url.rpartition("/")[2] From 05dcc4d025ec931ca2f7edcb3fe056f5173f8eb1 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 14:13:19 +0800 Subject: [PATCH 06/13] lint --- python/private/pypi/index_sources.bzl | 2 ++ python/private/pypi/parse_requirements.bzl | 1 + 2 files changed, 3 insertions(+) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 76401d6b00..a5ba7c5bce 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -61,9 +61,11 @@ def index_sources(line): # Extract URL if present if "@" in head: requirement = requirement_line + # Extract URL from direct URL format _, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() + # Extract filename from URL if url: filename = url.rpartition("/")[2] diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 0c4bad1f40..9c9c24fcd2 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -292,6 +292,7 @@ def _add_dists(*, requirement, index_urls, logger = None): index_urls: The result of simpleapi_download. logger: A logger for printing diagnostic info. """ + # Handle direct URLs in requirements if hasattr(requirement.srcs, "url"): # Create a struct that matches the expected format for direct URLs From bc10178937308993447752f3544199beaf105764 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 14:21:52 +0800 Subject: [PATCH 07/13] fix lint --- .../parse_requirements_tests.bzl | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 86861d69e0..76e850253a 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -134,30 +134,6 @@ def _test_direct_urls(env): }, ) env.expect.that_dict(got).contains_exactly({ - "foo": [ - struct( - distribution = "foo", - extra_pip_args = [], - sdist = None, - is_exposed = True, - srcs = struct( - marker = "", - requirement = "foo[extra] @ https://some-url/package.whl", - requirement_line = "foo[extra] @ https://some-url/package.whl", - shas = [], - version = "", - url = "https://some-url/package.whl", - filename = "package.whl", - ), - target_platforms = ["linux_x86_64"], - whls = [struct( - url = "https://some-url/package.whl", - filename = "package.whl", - sha256 = "", - yanked = False, - )], - ), - ], "bar": [ struct( distribution = "bar", @@ -206,7 +182,30 @@ def _test_direct_urls(env): )], ), ], - }) + "foo": [ + struct( + distribution = "foo", + extra_pip_args = [], + sdist = None, + is_exposed = True, + srcs = struct( + marker = "", + requirement = "foo[extra] @ https://some-url/package.whl", + requirement_line = "foo[extra] @ https://some-url/package.whl", + shas = [], + version = "", + url = "https://some-url/package.whl", + filename = "package.whl", + ), + target_platforms = ["linux_x86_64"], + whls = [struct( + url = "https://some-url/package.whl", + filename = "package.whl", + sha256 = "", + yanked = False, + )], + ), + ], }) _tests.append(_test_direct_urls) From c42b6dbec0f3b6284397f5447d3648c632490a73 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 14:30:42 +0800 Subject: [PATCH 08/13] lint --- tests/pypi/parse_requirements/parse_requirements_tests.bzl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 76e850253a..8a2c953fe3 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -182,7 +182,7 @@ def _test_direct_urls(env): )], ), ], - "foo": [ + "foo": [ struct( distribution = "foo", extra_pip_args = [], @@ -205,7 +205,8 @@ def _test_direct_urls(env): yanked = False, )], ), - ], }) + ], + }) _tests.append(_test_direct_urls) From 00ece5bbc76bc5de1c8514782b476eed35a56469 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 9 Mar 2025 20:49:38 +0800 Subject: [PATCH 09/13] simplify: remove filename as it's redundant --- python/private/pypi/index_sources.bzl | 12 ++---------- python/private/pypi/parse_requirements.bzl | 12 +++++++----- tests/pypi/index_sources/index_sources_tests.bzl | 6 ------ .../parse_requirements/parse_requirements_tests.bzl | 3 --- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index a5ba7c5bce..194ed4f364 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -32,8 +32,7 @@ def index_sources(line): * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can be given to `pip` to install a package. - * `url` - str; URL if the requirement specifies a direct URL. - * `filename` - str; filename if URL is present, extracted from the URL. + * `url` - str, optional; URL if the requirement specifies a direct URL. """ line = line.replace("\\", " ") head, _, maybe_hashes = line.partition(";") @@ -58,20 +57,14 @@ def index_sources(line): " ".join(["--hash=sha256:{}".format(sha) for sha in shas]), ).strip() - # Extract URL if present if "@" in head: requirement = requirement_line - # Extract URL from direct URL format + _, _, url_and_rest = requirement.partition("@") _, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() - # Extract filename from URL if url: - filename = url.rpartition("/")[2] - if not filename: - filename = url.rpartition("/")[0].rpartition("/")[2] - return struct( requirement = requirement, requirement_line = requirement_line, @@ -79,7 +72,6 @@ def index_sources(line): shas = sorted(shas), marker = marker, url = url, - filename = filename, ) return struct( diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 9c9c24fcd2..ee3f9286f7 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -295,14 +295,16 @@ def _add_dists(*, requirement, index_urls, logger = None): # Handle direct URLs in requirements if hasattr(requirement.srcs, "url"): - # Create a struct that matches the expected format for direct URLs + url = requirement.srcs.url + _, _, filename = url.rpartition("/") direct_url_dist = struct( - url = requirement.srcs.url, - filename = requirement.srcs.filename, - # TODO: if more than one hash is present, we should use all of them - sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", # Use hash if provided + url = url, + filename = filename, + sha256 = requirement.srcs.shas[0] if requirement.srcs.shas else "", yanked = False, ) + + # TODO should be able to handle sdist by checking the filename extension return [direct_url_dist], None if not index_urls: diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 45c94d8256..8317b40c0a 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -29,26 +29,22 @@ def _test_no_simple_api_sources(env): requirement = "foo==0.0.1 @ https://someurl.org", marker = "", url = "https://someurl.org", - filename = "someurl.org", ), "foo==0.0.1 @ https://someurl.org/package.whl": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl", marker = "", url = "https://someurl.org/package.whl", - filename = "package.whl", ), "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "", url = "https://someurl.org/package.whl", - filename = "package.whl", shas = ["deadbeef"], ), "foo==0.0.1 @ https://someurl.org/package.whl; python_version < \"2.7\"\\ --hash=sha256:deadbeef": struct( requirement = "foo==0.0.1 @ https://someurl.org/package.whl --hash=sha256:deadbeef", marker = "python_version < \"2.7\"", url = "https://someurl.org/package.whl", - filename = "package.whl", shas = ["deadbeef"], ), } @@ -61,8 +57,6 @@ def _test_no_simple_api_sources(env): env.expect.that_str(got.marker).equals(want.marker) if hasattr(want, "url"): env.expect.that_str(got.url).equals(want.url) - if hasattr(want, "filename"): - env.expect.that_str(got.filename).equals(want.filename) _tests.append(_test_no_simple_api_sources) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 8a2c953fe3..b25d66aff3 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -147,7 +147,6 @@ def _test_direct_urls(env): shas = ["deadbeef"], version = "", url = "https://example.org/bar-1.0.whl", - filename = "bar-1.0.whl", ), target_platforms = ["linux_x86_64"], whls = [struct( @@ -171,7 +170,6 @@ def _test_direct_urls(env): shas = ["deadb00f"], version = "", url = "https://test.com/baz-2.0.whl", - filename = "baz-2.0.whl", ), target_platforms = ["linux_x86_64"], whls = [struct( @@ -195,7 +193,6 @@ def _test_direct_urls(env): shas = [], version = "", url = "https://some-url/package.whl", - filename = "package.whl", ), target_platforms = ["linux_x86_64"], whls = [struct( From 9a793325a63e2506920b375c5e617ffedaeb9f4d Mon Sep 17 00:00:00 2001 From: chua Date: Tue, 11 Mar 2025 08:30:45 +0000 Subject: [PATCH 10/13] always emit the url attribute --- python/private/pypi/index_sources.bzl | 16 +++------------- python/private/pypi/parse_requirements.bzl | 2 +- tests/pypi/index_sources/index_sources_tests.bzl | 7 +++++-- .../parse_requirements_tests.bzl | 13 +++++++++++++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/python/private/pypi/index_sources.bzl b/python/private/pypi/index_sources.bzl index 194ed4f364..e3762d2a48 100644 --- a/python/private/pypi/index_sources.bzl +++ b/python/private/pypi/index_sources.bzl @@ -32,7 +32,7 @@ def index_sources(line): * `marker` - str; the marker expression, as per PEP508 spec. * `requirement` - str; a requirement line without the marker. This can be given to `pip` to install a package. - * `url` - str, optional; URL if the requirement specifies a direct URL. + * `url` - str; URL if the requirement specifies a direct URL, empty string otherwise. """ line = line.replace("\\", " ") head, _, maybe_hashes = line.partition(";") @@ -57,27 +57,17 @@ def index_sources(line): " ".join(["--hash=sha256:{}".format(sha) for sha in shas]), ).strip() + url = "" if "@" in head: requirement = requirement_line - - _, _, url_and_rest = requirement.partition("@") _, _, url_and_rest = requirement.partition("@") url = url_and_rest.strip().partition(" ")[0].strip() - if url: - return struct( - requirement = requirement, - requirement_line = requirement_line, - version = version, - shas = sorted(shas), - marker = marker, - url = url, - ) - return struct( requirement = requirement, requirement_line = requirement_line, version = version, shas = sorted(shas), marker = marker, + url = url, ) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index ee3f9286f7..c52ae1666f 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -294,7 +294,7 @@ def _add_dists(*, requirement, index_urls, logger = None): """ # Handle direct URLs in requirements - if hasattr(requirement.srcs, "url"): + if requirement.srcs.url: url = requirement.srcs.url _, _, filename = url.rpartition("/") direct_url_dist = struct( diff --git a/tests/pypi/index_sources/index_sources_tests.bzl b/tests/pypi/index_sources/index_sources_tests.bzl index 8317b40c0a..ffeed87a7b 100644 --- a/tests/pypi/index_sources/index_sources_tests.bzl +++ b/tests/pypi/index_sources/index_sources_tests.bzl @@ -24,6 +24,7 @@ def _test_no_simple_api_sources(env): "foo==0.0.1": struct( requirement = "foo==0.0.1", marker = "", + url = "", ), "foo==0.0.1 @ https://someurl.org": struct( requirement = "foo==0.0.1 @ https://someurl.org", @@ -55,8 +56,7 @@ def _test_no_simple_api_sources(env): env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(got.requirement) env.expect.that_str(got.marker).equals(want.marker) - if hasattr(want, "url"): - env.expect.that_str(got.url).equals(want.url) + env.expect.that_str(got.url).equals(want.url) _tests.append(_test_no_simple_api_sources) @@ -70,6 +70,7 @@ def _test_simple_api_sources(env): marker = "", requirement = "foo==0.0.2", requirement_line = "foo==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef", + url = "", ), "foo[extra]==0.0.2; (python_version < 2.7 or extra == \"@\") --hash=sha256:deafbeef --hash=sha256:deadbeef": struct( shas = [ @@ -79,6 +80,7 @@ def _test_simple_api_sources(env): marker = "(python_version < 2.7 or extra == \"@\")", requirement = "foo[extra]==0.0.2", requirement_line = "foo[extra]==0.0.2 --hash=sha256:deafbeef --hash=sha256:deadbeef", + url = "", ), } for input, want in tests.items(): @@ -88,6 +90,7 @@ def _test_simple_api_sources(env): env.expect.that_str(got.requirement).equals(want.requirement) env.expect.that_str(got.requirement_line).equals(want.requirement_line) env.expect.that_str(got.marker).equals(want.marker) + env.expect.that_str(got.url).equals(want.url) _tests.append(_test_simple_api_sources) diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index b25d66aff3..3eca9e4cce 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -108,6 +108,7 @@ def _test_simple(env): requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = [ "linux_x86_64", @@ -228,6 +229,7 @@ def _test_extra_pip_args(env): requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = [ "linux_x86_64", @@ -265,6 +267,7 @@ def _test_dupe_requirements(env): requirement_line = "foo[extra,extra_2]==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = ["linux_x86_64"], whls = [], @@ -294,6 +297,7 @@ def _test_multi_os(env): requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", shas = ["deadb00f"], version = "0.0.1", + url = "", ), target_platforms = ["windows_x86_64"], whls = [], @@ -311,6 +315,7 @@ def _test_multi_os(env): requirement_line = "foo==0.0.3 --hash=sha256:deadbaaf", shas = ["deadbaaf"], version = "0.0.3", + url = "", ), target_platforms = ["linux_x86_64"], whls = [], @@ -326,6 +331,7 @@ def _test_multi_os(env): requirement_line = "foo[extra]==0.0.2 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.2", + url = "", ), target_platforms = ["windows_x86_64"], whls = [], @@ -365,6 +371,7 @@ def _test_multi_os_legacy(env): requirement_line = "bar==0.0.1 --hash=sha256:deadb00f", shas = ["deadb00f"], version = "0.0.1", + url = "", ), target_platforms = ["cp39_linux_x86_64"], whls = [], @@ -382,6 +389,7 @@ def _test_multi_os_legacy(env): requirement_line = "foo==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = ["cp39_linux_x86_64"], whls = [], @@ -397,6 +405,7 @@ def _test_multi_os_legacy(env): requirement = "foo==0.0.3", shas = ["deadbaaf"], version = "0.0.3", + url = "", ), target_platforms = ["cp39_osx_aarch64"], whls = [], @@ -450,6 +459,7 @@ def _test_env_marker_resolution(env): requirement_line = "bar==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = ["cp311_linux_super_exotic", "cp311_windows_x86_64"], whls = [], @@ -467,6 +477,7 @@ def _test_env_marker_resolution(env): requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1", + url = "", ), target_platforms = ["cp311_windows_x86_64"], whls = [], @@ -502,6 +513,7 @@ def _test_different_package_version(env): requirement_line = "foo==0.0.1 --hash=sha256:deadb00f", shas = ["deadb00f"], version = "0.0.1", + url = "", ), target_platforms = ["linux_x86_64"], whls = [], @@ -517,6 +529,7 @@ def _test_different_package_version(env): requirement_line = "foo==0.0.1+local --hash=sha256:deadbeef", shas = ["deadbeef"], version = "0.0.1+local", + url = "", ), target_platforms = ["linux_x86_64"], whls = [], From 900dabb94d897410f351fc9792d6abb767ce02db Mon Sep 17 00:00:00 2001 From: chua Date: Tue, 11 Mar 2025 08:31:56 +0000 Subject: [PATCH 11/13] handle sdist --- python/private/pypi/parse_requirements.bzl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index c52ae1666f..2b3de27996 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -304,8 +304,10 @@ def _add_dists(*, requirement, index_urls, logger = None): yanked = False, ) - # TODO should be able to handle sdist by checking the filename extension - return [direct_url_dist], None + if filename.endswith(".whl"): + return [direct_url_dist], None + else: + return None, direct_url_dist if not index_urls: return [], None From 544cd2158ab59de6034cb07c9c143fc75546cb6c Mon Sep 17 00:00:00 2001 From: chua Date: Tue, 11 Mar 2025 08:39:11 +0000 Subject: [PATCH 12/13] add tests for sdist handling --- python/private/pypi/parse_requirements.bzl | 2 +- .../parse_requirements_tests.bzl | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl index 2b3de27996..dbff44ecb3 100644 --- a/python/private/pypi/parse_requirements.bzl +++ b/python/private/pypi/parse_requirements.bzl @@ -307,7 +307,7 @@ def _add_dists(*, requirement, index_urls, logger = None): if filename.endswith(".whl"): return [direct_url_dist], None else: - return None, direct_url_dist + return [], direct_url_dist if not index_urls: return [], None diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl index 3eca9e4cce..8edc2689bf 100644 --- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl +++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl @@ -29,6 +29,7 @@ foo==0.0.1 \ foo[extra] @ https://some-url/package.whl bar @ https://example.org/bar-1.0.whl --hash=sha256:deadbeef baz @ https://test.com/baz-2.0.whl; python_version < "3.8" --hash=sha256:deadb00f +qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f """, "requirements_extra_args": """\ --index-url=example.org @@ -204,6 +205,29 @@ def _test_direct_urls(env): )], ), ], + "qux": [ + struct( + distribution = "qux", + extra_pip_args = [], + sdist = struct( + url = "https://example.org/qux-1.0.tar.gz", + filename = "qux-1.0.tar.gz", + sha256 = "deadbe0f", + yanked = False, + ), + is_exposed = True, + srcs = struct( + marker = "", + requirement = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f", + requirement_line = "qux @ https://example.org/qux-1.0.tar.gz --hash=sha256:deadbe0f", + shas = ["deadbe0f"], + version = "", + url = "https://example.org/qux-1.0.tar.gz", + ), + target_platforms = ["linux_x86_64"], + whls = [], + ), + ], }) _tests.append(_test_direct_urls) From 176a1dd3ce40fc6096a500c0d3ac9b80094afe56 Mon Sep 17 00:00:00 2001 From: chua Date: Wed, 12 Mar 2025 11:17:57 +0000 Subject: [PATCH 13/13] add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59d225189..947d5fcd32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,9 @@ Unreleased changes template. binary whose interpreter to use. * (pypi) An extra argument to add the interpreter lib dir to `LDFLAGS` when building wheels from `sdist`. +* (pypi) Direct HTTP urls for wheels and sdists are now supported when using + {obj}`experimental_index_url` (bazel downloader). + Partially fixes [#2363](https://github.com/bazelbuild/rules_python/issues/2363). {#v0-0-0-removed} ### Removed