Skip to content

Commit 369ca91

Browse files
authored
refactor(pypi): return a list from parse_requirements (#2931)
The modeling of the data structures returned by the `parse_requirements` function was not optimal and this was because historically there was more logic in the `extension.bzl` and more things were decided there. With the recent refactors it is possible to have a harder to misuse data structure from the `parse_requirements`. For each `package` we will return a struct which will have a `srcs` field that will contain easy to consume values. With this in place we can do the fix that is outlined in the referenced issue. Work towards #2648
1 parent c0415c6 commit 369ca91

File tree

4 files changed

+424
-331
lines changed

4 files changed

+424
-331
lines changed

python/private/pypi/extension.bzl

Lines changed: 78 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,12 @@ def _create_whl_repos(
202202
logger = logger,
203203
)
204204

205-
for whl_name, requirements in requirements_by_platform.items():
206-
group_name = whl_group_mapping.get(whl_name)
205+
exposed_packages = {}
206+
for whl in requirements_by_platform:
207+
if whl.is_exposed:
208+
exposed_packages[whl.name] = None
209+
210+
group_name = whl_group_mapping.get(whl.name)
207211
group_deps = requirement_cycles.get(group_name, [])
208212

209213
# Construct args separately so that the lock file can be smaller and does not include unused
@@ -214,7 +218,7 @@ def _create_whl_repos(
214218
maybe_args = dict(
215219
# The following values are safe to omit if they have false like values
216220
add_libdir_to_library_search_path = pip_attr.add_libdir_to_library_search_path,
217-
annotation = whl_modifications.get(whl_name),
221+
annotation = whl_modifications.get(whl.name),
218222
download_only = pip_attr.download_only,
219223
enable_implicit_namespace_pkgs = pip_attr.enable_implicit_namespace_pkgs,
220224
environment = pip_attr.environment,
@@ -226,7 +230,7 @@ def _create_whl_repos(
226230
python_interpreter_target = python_interpreter_target,
227231
whl_patches = {
228232
p: json.encode(args)
229-
for p, args in whl_overrides.get(whl_name, {}).items()
233+
for p, args in whl_overrides.get(whl.name, {}).items()
230234
},
231235
)
232236
if not enable_pipstar:
@@ -245,119 +249,99 @@ def _create_whl_repos(
245249
if v != default
246250
})
247251

248-
for requirement in requirements:
249-
for repo_name, (args, config_setting) in _whl_repos(
250-
requirement = requirement,
252+
for src in whl.srcs:
253+
repo = _whl_repo(
254+
src = src,
251255
whl_library_args = whl_library_args,
252256
download_only = pip_attr.download_only,
253257
netrc = pip_attr.netrc,
254258
auth_patterns = pip_attr.auth_patterns,
255259
python_version = major_minor,
256-
multiple_requirements_for_whl = len(requirements) > 1.,
260+
is_multiple_versions = whl.is_multiple_versions,
257261
enable_pipstar = enable_pipstar,
258-
).items():
259-
repo_name = "{}_{}".format(pip_name, repo_name)
260-
if repo_name in whl_libraries:
261-
fail("Attempting to creating a duplicate library {} for {}".format(
262-
repo_name,
263-
whl_name,
264-
))
262+
)
265263

266-
whl_libraries[repo_name] = args
267-
whl_map.setdefault(whl_name, {})[config_setting] = repo_name
264+
repo_name = "{}_{}".format(pip_name, repo.repo_name)
265+
if repo_name in whl_libraries:
266+
fail("Attempting to creating a duplicate library {} for {}".format(
267+
repo_name,
268+
whl.name,
269+
))
270+
271+
whl_libraries[repo_name] = repo.args
272+
whl_map.setdefault(whl.name, {})[repo.config_setting] = repo_name
268273

269274
return struct(
270275
whl_map = whl_map,
271-
exposed_packages = {
272-
whl_name: None
273-
for whl_name, requirements in requirements_by_platform.items()
274-
if len([r for r in requirements if r.is_exposed]) > 0
275-
},
276+
exposed_packages = exposed_packages,
276277
extra_aliases = extra_aliases,
277278
whl_libraries = whl_libraries,
278279
)
279280

280-
def _whl_repos(*, requirement, whl_library_args, download_only, netrc, auth_patterns, multiple_requirements_for_whl = False, python_version, enable_pipstar = False):
281-
ret = {}
282-
283-
dists = requirement.whls
284-
if not download_only and requirement.sdist:
285-
dists = dists + [requirement.sdist]
286-
287-
for distribution in dists:
288-
args = dict(whl_library_args)
289-
if netrc:
290-
args["netrc"] = netrc
291-
if auth_patterns:
292-
args["auth_patterns"] = auth_patterns
293-
294-
if not distribution.filename.endswith(".whl"):
295-
# pip is not used to download wheels and the python
296-
# `whl_library` helpers are only extracting things, however
297-
# for sdists, they will be built by `pip`, so we still
298-
# need to pass the extra args there.
299-
args["extra_pip_args"] = requirement.extra_pip_args
300-
301-
# This is no-op because pip is not used to download the wheel.
302-
args.pop("download_only", None)
303-
304-
args["requirement"] = requirement.line
305-
args["urls"] = [distribution.url]
306-
args["sha256"] = distribution.sha256
307-
args["filename"] = distribution.filename
308-
if not enable_pipstar:
309-
args["experimental_target_platforms"] = [
310-
# Get rid of the version fot the target platforms because we are
311-
# passing the interpreter any way. Ideally we should search of ways
312-
# how to pass the target platforms through the hub repo.
313-
p.partition("_")[2]
314-
for p in requirement.target_platforms
315-
]
316-
317-
# Pure python wheels or sdists may need to have a platform here
318-
target_platforms = None
319-
if distribution.filename.endswith(".whl") and not distribution.filename.endswith("-any.whl"):
320-
pass
321-
elif multiple_requirements_for_whl:
322-
target_platforms = requirement.target_platforms
323-
324-
repo_name = whl_repo_name(
325-
distribution.filename,
326-
distribution.sha256,
327-
)
328-
ret[repo_name] = (
329-
args,
330-
whl_config_setting(
281+
def _whl_repo(*, src, whl_library_args, is_multiple_versions, download_only, netrc, auth_patterns, python_version, enable_pipstar = False):
282+
args = dict(whl_library_args)
283+
args["requirement"] = src.requirement_line
284+
is_whl = src.filename.endswith(".whl")
285+
286+
if src.extra_pip_args and not is_whl:
287+
# pip is not used to download wheels and the python
288+
# `whl_library` helpers are only extracting things, however
289+
# for sdists, they will be built by `pip`, so we still
290+
# need to pass the extra args there, so only pop this for whls
291+
args["extra_pip_args"] = src.extra_pip_args
292+
293+
if not src.url or (not is_whl and download_only):
294+
# Fallback to a pip-installed wheel
295+
target_platforms = src.target_platforms if is_multiple_versions else []
296+
return struct(
297+
repo_name = pypi_repo_name(
298+
normalize_name(src.distribution),
299+
*target_platforms
300+
),
301+
args = args,
302+
config_setting = whl_config_setting(
331303
version = python_version,
332-
filename = distribution.filename,
333-
target_platforms = target_platforms,
304+
target_platforms = target_platforms or None,
334305
),
335306
)
336307

337-
if ret:
338-
return ret
339-
340-
# Fallback to a pip-installed wheel
341-
args = dict(whl_library_args) # make a copy
342-
args["requirement"] = requirement.line
343-
if requirement.extra_pip_args:
344-
args["extra_pip_args"] = requirement.extra_pip_args
308+
# This is no-op because pip is not used to download the wheel.
309+
args.pop("download_only", None)
310+
311+
if netrc:
312+
args["netrc"] = netrc
313+
if auth_patterns:
314+
args["auth_patterns"] = auth_patterns
315+
316+
args["urls"] = [src.url]
317+
args["sha256"] = src.sha256
318+
args["filename"] = src.filename
319+
if not enable_pipstar:
320+
args["experimental_target_platforms"] = [
321+
# Get rid of the version fot the target platforms because we are
322+
# passing the interpreter any way. Ideally we should search of ways
323+
# how to pass the target platforms through the hub repo.
324+
p.partition("_")[2]
325+
for p in src.target_platforms
326+
]
327+
328+
# Pure python wheels or sdists may need to have a platform here
329+
target_platforms = None
330+
if is_whl and not src.filename.endswith("-any.whl"):
331+
pass
332+
elif is_multiple_versions:
333+
target_platforms = src.target_platforms
345334

346-
target_platforms = requirement.target_platforms if multiple_requirements_for_whl else []
347-
repo_name = pypi_repo_name(
348-
normalize_name(requirement.distribution),
349-
*target_platforms
350-
)
351-
ret[repo_name] = (
352-
args,
353-
whl_config_setting(
335+
return struct(
336+
repo_name = whl_repo_name(src.filename, src.sha256),
337+
args = args,
338+
config_setting = whl_config_setting(
354339
version = python_version,
355-
target_platforms = target_platforms or None,
340+
filename = src.filename,
341+
target_platforms = target_platforms,
356342
),
357343
)
358344

359-
return ret
360-
361345
def parse_modules(
362346
module_ctx,
363347
_fail = fail,

python/private/pypi/parse_requirements.bzl

Lines changed: 67 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -179,49 +179,91 @@ def parse_requirements(
179179
}),
180180
)
181181

182-
ret = {}
183-
for whl_name, reqs in sorted(requirements_by_platform.items()):
182+
ret = []
183+
for name, reqs in sorted(requirements_by_platform.items()):
184184
requirement_target_platforms = {}
185185
for r in reqs.values():
186186
target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms)
187187
for p in target_platforms:
188188
requirement_target_platforms[p] = None
189189

190-
is_exposed = len(requirement_target_platforms) == len(requirements)
191-
if not is_exposed and logger:
190+
item = struct(
191+
# Return normalized names
192+
name = normalize_name(name),
193+
is_exposed = len(requirement_target_platforms) == len(requirements),
194+
is_multiple_versions = len(reqs.values()) > 1,
195+
srcs = _package_srcs(
196+
name = name,
197+
reqs = reqs,
198+
index_urls = index_urls,
199+
env_marker_target_platforms = env_marker_target_platforms,
200+
extract_url_srcs = extract_url_srcs,
201+
logger = logger,
202+
),
203+
)
204+
ret.append(item)
205+
if not item.is_exposed and logger:
192206
logger.debug(lambda: "Package '{}' will not be exposed because it is only present on a subset of platforms: {} out of {}".format(
193-
whl_name,
207+
name,
194208
sorted(requirement_target_platforms),
195209
sorted(requirements),
196210
))
197211

198-
# Return normalized names
199-
ret_requirements = ret.setdefault(normalize_name(whl_name), [])
212+
if logger:
213+
logger.debug(lambda: "Will configure whl repos: {}".format([w.name for w in ret]))
200214

201-
for r in sorted(reqs.values(), key = lambda r: r.requirement_line):
202-
whls, sdist = _add_dists(
203-
requirement = r,
204-
index_urls = index_urls.get(whl_name),
205-
logger = logger,
206-
)
215+
return ret
207216

208-
target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms)
209-
ret_requirements.append(
217+
def _package_srcs(
218+
*,
219+
name,
220+
reqs,
221+
index_urls,
222+
logger,
223+
env_marker_target_platforms,
224+
extract_url_srcs):
225+
"""A function to return sources for a particular package."""
226+
srcs = []
227+
for r in sorted(reqs.values(), key = lambda r: r.requirement_line):
228+
whls, sdist = _add_dists(
229+
requirement = r,
230+
index_urls = index_urls.get(name),
231+
logger = logger,
232+
)
233+
234+
target_platforms = env_marker_target_platforms.get(r.requirement_line, r.target_platforms)
235+
target_platforms = sorted(target_platforms)
236+
237+
all_dists = [] + whls
238+
if sdist:
239+
all_dists.append(sdist)
240+
241+
if extract_url_srcs and all_dists:
242+
req_line = r.srcs.requirement
243+
else:
244+
all_dists = [struct(
245+
url = "",
246+
filename = "",
247+
sha256 = "",
248+
yanked = False,
249+
)]
250+
req_line = r.srcs.requirement_line
251+
252+
for dist in all_dists:
253+
srcs.append(
210254
struct(
211-
distribution = r.distribution,
212-
line = r.srcs.requirement if extract_url_srcs and (whls or sdist) else r.srcs.requirement_line,
213-
target_platforms = sorted(target_platforms),
255+
distribution = name,
214256
extra_pip_args = r.extra_pip_args,
215-
whls = whls,
216-
sdist = sdist,
217-
is_exposed = is_exposed,
257+
requirement_line = req_line,
258+
target_platforms = target_platforms,
259+
filename = dist.filename,
260+
sha256 = dist.sha256,
261+
url = dist.url,
262+
yanked = dist.yanked,
218263
),
219264
)
220265

221-
if logger:
222-
logger.debug(lambda: "Will configure whl repos: {}".format(ret.keys()))
223-
224-
return ret
266+
return srcs
225267

226268
def select_requirement(requirements, *, platform):
227269
"""A simple function to get a requirement for a particular platform.

python/private/pypi/pip_repository.bzl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,15 @@ def _pip_repository_impl(rctx):
9494
selected_requirements = {}
9595
options = None
9696
repository_platform = host_platform(rctx)
97-
for name, requirements in requirements_by_platform.items():
97+
for whl in requirements_by_platform:
9898
requirement = select_requirement(
99-
requirements,
99+
whl.srcs,
100100
platform = None if rctx.attr.download_only else repository_platform,
101101
)
102102
if not requirement:
103103
continue
104104
options = options or requirement.extra_pip_args
105-
selected_requirements[name] = requirement.line
105+
selected_requirements[whl.name] = requirement.requirement_line
106106

107107
bzl_packages = sorted(selected_requirements.keys())
108108

0 commit comments

Comments
 (0)