Skip to content
78 changes: 46 additions & 32 deletions python/private/pypi/parse_requirements.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def parse_requirements(
evaluate_markers = evaluate_markers or (lambda _ctx, _requirements: {})
options = {}
requirements = {}
reqs_with_env_markers = {}
for file, plats in requirements_by_platform.items():
logger.trace(lambda: "Using {} for {}".format(file, plats))
contents = ctx.read(file)
Expand All @@ -96,16 +97,47 @@ def parse_requirements(
# needed for the whl_library declarations later.
parse_result = parse_requirements_txt(contents)

tokenized_options = []
for opt in parse_result.options:
for p in opt.split(" "):
tokenized_options.append(p)

pip_args = tokenized_options + extra_pip_args
for plat in plats:
requirements[plat] = parse_result.requirements
for entry in parse_result.requirements:
requirement_line = entry[1]

# output all of the requirement lines that have a marker
if ";" in requirement_line:
reqs_with_env_markers.setdefault(requirement_line, []).append(plat)
options[plat] = pip_args

# This may call to Python, so execute it early (before calling to the
# internet below) and ensure that we call it only once.
#
# NOTE @aignas 2024-07-13: in the future, if this is something that we want
# to do, we could use Python to parse the requirement lines and infer the
# URL of the files to download things from. This should be important for
# VCS package references.
env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers)
logger.trace(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format(
reqs_with_env_markers,
env_marker_target_platforms,
))

requirements_by_platform = {}
for target_platform, reqs_ in requirements.items():
# Replicate a surprising behavior that WORKSPACE builds allowed:
# Defining a repo with the same name multiple times, but only the last
# definition is respected.
# The requirement lines might have duplicate names because lines for extras
# are returned as just the base package name. e.g., `foo[bar]` results
# in an entry like `("foo", "foo[bar] == 1.0 ...")`.
# Lines with different markers are not condidered duplicates.
# Lines with different markers are not considered duplicates.
requirements_dict = {}
for entry in sorted(
parse_result.requirements,
reqs_,
# Get the longest match and fallback to original WORKSPACE sorting,
# which should get us the entry with most extras.
#
Expand All @@ -114,33 +146,28 @@ def parse_requirements(
# should do this now.
key = lambda x: (len(x[1].partition("==")[0]), x),
):
req = requirement(entry[1])
requirements_dict[(req.name, req.version, req.marker)] = entry
req_line = entry[1]
req = requirement(req_line)

tokenized_options = []
for opt in parse_result.options:
for p in opt.split(" "):
tokenized_options.append(p)
if req.marker:
plats = env_marker_target_platforms.get(req_line, [])
if not plats:
continue
elif target_platform not in plats:
continue

pip_args = tokenized_options + extra_pip_args
for plat in plats:
requirements[plat] = requirements_dict.values()
options[plat] = pip_args
# FIXME @aignas 2025-11-01: I don't think the `req.version` should be here
# because WORKSPACE would create files based on the `req.name`.
requirements_dict[(req.name, req.version)] = entry

requirements_by_platform = {}
reqs_with_env_markers = {}
for target_platform, reqs_ in requirements.items():
extra_pip_args = options[target_platform]

for distribution, requirement_line in reqs_:
for distribution, requirement_line in requirements_dict.values():
for_whl = requirements_by_platform.setdefault(
normalize_name(distribution),
{},
)

if ";" in requirement_line:
reqs_with_env_markers.setdefault(requirement_line, []).append(target_platform)

for_req = for_whl.setdefault(
(requirement_line, ",".join(extra_pip_args)),
struct(
Expand All @@ -153,19 +180,6 @@ def parse_requirements(
)
for_req.target_platforms.append(target_platform)

# This may call to Python, so execute it early (before calling to the
# internet below) and ensure that we call it only once.
#
# NOTE @aignas 2024-07-13: in the future, if this is something that we want
# to do, we could use Python to parse the requirement lines and infer the
# URL of the files to download things from. This should be important for
# VCS package references.
env_marker_target_platforms = evaluate_markers(ctx, reqs_with_env_markers)
logger.trace(lambda: "Evaluated env markers from:\n{}\n\nTo:\n{}".format(
reqs_with_env_markers,
env_marker_target_platforms,
))

index_urls = {}
if get_index_urls:
index_urls = get_index_urls(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ def _test_get_index_urls_single_py_version(env):
env.expect.that_collection(got).contains_exactly([
struct(
is_exposed = True,
is_multiple_versions = True,
is_multiple_versions = False,
name = "foo",
srcs = [
struct(
Expand Down