Skip to content

Commit 6c35b49

Browse files
authored
Fix pex3 lock sync for multiple requirements for the same project. (#3191)
Issue discovered in pantsbuild/pants#23407.
1 parent 85b8ae0 commit 6c35b49

6 files changed

Lines changed: 930 additions & 46 deletions

File tree

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 2.96.1
4+
5+
This release fixes `pex3 lock sync` to handle multiple input requirements for the same project
6+
name.
7+
8+
* Fix `pex3 lock sync` for multiple requirements for the same project.
9+
310
## 2.96.0
411

512
This release adds support for `--pip-version 26.1.2`.

pex/cli/commands/lock.py

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,15 +1894,27 @@ def _process_lock_update(
18941894
):
18951895
# type: (...) -> Result
18961896

1897-
original_requirements_by_project_name = OrderedDict(
1898-
(requirement.project_name, requirement) for requirement in lock_file.requirements
1899-
)
1900-
requirements_by_project_name = OrderedDict(
1901-
(requirement.project_name, requirement) for requirement in lock_update.requirements
1902-
)
1897+
original_requirements_by_project_name = (
1898+
OrderedDict()
1899+
) # type: OrderedDict[ProjectName, OrderedSet[Requirement]]
1900+
for requirement in lock_file.requirements:
1901+
original_requirements_by_project_name.setdefault(
1902+
requirement.project_name, OrderedSet()
1903+
).add(requirement)
19031904

1905+
requirements_by_project_name = (
1906+
OrderedDict()
1907+
) # type: OrderedDict[ProjectName, OrderedSet[Requirement]]
1908+
for requirement in lock_update.requirements:
1909+
requirements_by_project_name.setdefault(requirement.project_name, OrderedSet()).add(
1910+
requirement
1911+
)
1912+
1913+
# TODO(John Sirois): Support multiple constraints for the same project name distinguished
1914+
# by markers.
19041915
original_constraints_by_project_name = OrderedDict(
1905-
(constraint.project_name, constraint) for constraint in lock_file.constraints
1916+
(constraint.project_name, OrderedSet([constraint]))
1917+
for constraint in lock_file.constraints
19061918
)
19071919
constraints_by_project_name = original_constraints_by_project_name.copy()
19081920

@@ -1951,7 +1963,14 @@ def _process_lock_update(
19511963
"earlier. Found deleted project {project_name} in updated requirements:\n"
19521964
"{requirements}",
19531965
project_name=project_name,
1954-
requirements="\n".join(map(str, requirements_by_project_name.values())),
1966+
requirements="\n".join(
1967+
map(
1968+
str,
1969+
itertools.chain.from_iterable(
1970+
requirements_by_project_name.values()
1971+
),
1972+
)
1973+
),
19551974
)
19561975
constraints_by_project_name.pop(project_name, None)
19571976
elif isinstance(update, VersionUpdate):
@@ -1972,7 +1991,9 @@ def _process_lock_update(
19721991
# grab the latest version in the range already constrained by an existing
19731992
# requirement or constraint.
19741993
if update_req and str(update_req) != update_req.name:
1975-
constraints_by_project_name[project_name] = update_req.as_constraint()
1994+
constraints_by_project_name[project_name] = OrderedSet(
1995+
[update_req.as_constraint()]
1996+
)
19761997
else:
19771998
print(
19781999
" {lead_in} {project_name} {updated_version}".format(
@@ -1983,7 +2004,9 @@ def _process_lock_update(
19832004
file=output,
19842005
)
19852006
if update_req:
1986-
requirements_by_project_name[project_name] = update_req
2007+
requirements_by_project_name.setdefault(project_name, OrderedSet()).add(
2008+
update_req
2009+
)
19872010
elif isinstance(update, ArtifactsUpdate):
19882011
message_lines = [
19892012
" {lead_in} {project_name} {version} artifacts:".format(
@@ -2053,50 +2076,50 @@ def _process_lock_update(
20532076
)
20542077

20552078
def process_req_edit(
2056-
original, # type: Optional[Constraint]
2057-
final, # type: Optional[Constraint]
2079+
original, # type: Iterable[Constraint]
2080+
final, # type: Iterable[Constraint]
20582081
):
20592082
# type: (...) -> None
20602083
if not original:
20612084
print(
20622085
" {lead_in} {requirement!r}".format(
20632086
lead_in="Would add" if dry_run else "Added",
2064-
requirement=str(final),
2087+
requirement=", ".join(map(str, final)),
20652088
),
20662089
file=output,
20672090
)
20682091
elif not final:
20692092
print(
20702093
" {lead_in} {requirement!r}".format(
20712094
lead_in="Would delete" if dry_run else "Deleted",
2072-
requirement=str(original),
2095+
requirement=", ".join(map(str, original)),
20732096
),
20742097
file=output,
20752098
)
20762099
else:
20772100
print(
20782101
" {lead_in} {original!r} to {final!r}".format(
20792102
lead_in="Would update" if dry_run else "Updated",
2080-
original=str(original),
2081-
final=str(final),
2103+
original=", ".join(map(str, original)),
2104+
final=", ".join(map(str, final)),
20822105
),
20832106
file=output,
20842107
)
20852108

20862109
def process_req_edits(
20872110
requirement_type, # type: str
2088-
original, # type: Mapping[ProjectName, Constraint]
2089-
final, # type: Mapping[ProjectName, Constraint]
2111+
original, # type: Mapping[ProjectName, Iterable[Constraint]]
2112+
final, # type: Mapping[ProjectName, Iterable[Constraint]]
20902113
):
2091-
# type: (...) -> Tuple[Tuple[Optional[Constraint], Optional[Constraint]], ...]
2092-
edits = [] # type: List[Tuple[Optional[Constraint], Optional[Constraint]]]
2093-
for name, original_req in original.items():
2094-
final_req = final.get(name)
2095-
if final_req != original_req:
2096-
edits.append((original_req, final_req))
2097-
for name, final_req in final.items():
2114+
# type: (...) -> Tuple[Tuple[Iterable[Constraint], Iterable[Constraint]], ...]
2115+
edits = [] # type: List[Tuple[Iterable[Constraint], Iterable[Constraint]]]
2116+
for name, original_reqs in original.items():
2117+
final_reqs = final.get(name, OrderedSet())
2118+
if final_reqs != original_reqs:
2119+
edits.append((original_reqs, final_reqs))
2120+
for name, final_reqs in final.items():
20982121
if name not in original:
2099-
edits.append((None, final_req))
2122+
edits.append((OrderedSet(), final_reqs))
21002123
if not edits:
21012124
return ()
21022125

@@ -2138,8 +2161,13 @@ def process_req_edits(
21382161
lock_file=attr.evolve(
21392162
lock_file,
21402163
pex_version=__version__,
2141-
requirements=SortedTuple(requirements_by_project_name.values(), key=str),
2142-
constraints=SortedTuple(constraints_by_project_name.values(), key=str),
2164+
requirements=SortedTuple(
2165+
itertools.chain.from_iterable(requirements_by_project_name.values()),
2166+
key=str,
2167+
),
2168+
constraints=SortedTuple(
2169+
itertools.chain.from_iterable(constraints_by_project_name.values()), key=str
2170+
),
21432171
locked_resolves=SortedTuple(
21442172
resolve_update.updated_resolve for resolve_update in lock_update.resolves
21452173
),

pex/resolve/lockfile/updater.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -256,28 +256,39 @@ def derived(
256256
):
257257
# type: (...) -> Union[ResolveUpdater, Error]
258258

259-
original_requirements_by_project_name = OrderedDict(
260-
(requirement.project_name, requirement) for requirement in lock_file.requirements
261-
) # type: OrderedDict[ProjectName, Requirement]
259+
original_requirements_by_project_name = (
260+
OrderedDict()
261+
) # type: OrderedDict[ProjectName, OrderedSet[Requirement]]
262+
for requirement in lock_file.requirements:
263+
original_requirements_by_project_name.setdefault(
264+
requirement.project_name, OrderedSet()
265+
).add(requirement)
262266

263-
replace_requirements = [] # type: List[Requirement]
267+
parsed_requirements_by_project_name = (
268+
OrderedDict()
269+
) # type: OrderedDict[ProjectName, OrderedSet[Requirement]]
264270
for parsed_requirement in parsed_requirements:
265271
if isinstance(parsed_requirement, (PyPIRequirement, URLRequirement, VCSRequirement)):
266272
requirement = parsed_requirement.requirement
267-
original_requirement = original_requirements_by_project_name.pop(
268-
requirement.project_name, None
269-
)
270-
if requirement != original_requirement:
271-
replace_requirements.append(requirement)
273+
parsed_requirements_by_project_name.setdefault(
274+
requirement.project_name, OrderedSet()
275+
).add(requirement)
272276
else:
273277
return Error(
274278
"Cannot update a bare local project directory requirement on {path}.\n"
275279
"Try re-phrasing as a PEP-508 direct reference with a file:// URL.\n"
276280
"See: https://peps.python.org/pep-0508/".format(path=parsed_requirement.path)
277281
)
282+
replace_requirements = OrderedSet() # type: OrderedSet[Requirement]
283+
for project_name, parsed_requirements in parsed_requirements_by_project_name.items():
284+
original_reqs = original_requirements_by_project_name.pop(project_name, None)
285+
if not original_reqs or parsed_requirements != original_reqs:
286+
replace_requirements.update(parsed_requirements)
278287

279288
deletes = tuple(original_requirements_by_project_name) if parsed_requirements else ()
280289

290+
# TODO(John Sirois): Support multiple constraints for the same project name distinguished
291+
# by markers.
281292
original_constraints_by_project_name = OrderedDict(
282293
(constraint.project_name, constraint) for constraint in lock_file.constraints
283294
) # type: OrderedDict[ProjectName, Constraint]
@@ -296,15 +307,24 @@ def derived(
296307
)
297308
update_constraints_by_project_name[project_name] = requirement.as_constraint()
298309

299-
original_requirements = OrderedDict(
300-
(requirement.project_name, requirement) for requirement in lock_file.requirements
301-
) # type: OrderedDict[ProjectName, Requirement]
302-
original_requirements.update(
303-
(replacement.project_name, replacement) for replacement in replace_requirements
304-
)
310+
original_requirements = (
311+
OrderedDict()
312+
) # type: OrderedDict[ProjectName, OrderedSet[Requirement]]
313+
for requirement in lock_file.requirements:
314+
original_requirements.setdefault(requirement.project_name, OrderedSet()).add(
315+
requirement
316+
)
317+
for replacement in replace_requirements:
318+
original_requirements.pop(replacement.project_name, None)
319+
for replacement in replace_requirements:
320+
original_requirements.setdefault(replacement.project_name, OrderedSet()).add(
321+
replacement
322+
)
305323
return cls(
306324
requirement_configuration=requirement_configuration,
307-
original_requirements=tuple(original_requirements.values()),
325+
original_requirements=tuple(
326+
itertools.chain.from_iterable(original_requirements.values())
327+
),
308328
update_constraints_by_project_name=update_constraints_by_project_name,
309329
deletes=frozenset(deletes),
310330
pure_delete=bool(deletes and not replace_requirements),

pex/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2015 Pex project contributors.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "2.96.0"
4+
__version__ = "2.96.1"

0 commit comments

Comments
 (0)