Skip to content

Commit f50af8f

Browse files
authored
Fix --elide-unused-requires-dist for inactive deps. (#2615)
The feature released broken in 2.25.0 for any locked requirement that had dependencies that would never be activated due to non-"extra" environment markers.
1 parent fa93760 commit f50af8f

File tree

5 files changed

+53
-149
lines changed

5 files changed

+53
-149
lines changed

CHANGES.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release Notes
22

3+
## 2.25.1
4+
5+
This is a hotfix release that fixes a bug in the implementation of the
6+
`--elide-unused-requires-dist` lock option introduced in Pex 2.25.0.
7+
8+
* Fix `--elide-unused-requires-dist` for unactivated deps. (#2615)
39
## 2.25.0
410

511
This release adds support for

pex/resolve/lockfile/model.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def extract_requirement(req):
121121
overridden=SortedTuple(overridden),
122122
locked_resolves=SortedTuple(
123123
(
124-
requires_dist.remove_unused_requires_dist(resolve_requirements, locked_resolve)
124+
requires_dist.remove_unused_requires_dist(locked_resolve)
125125
if elide_unused_requires_dist
126126
else locked_resolve
127127
)

pex/resolve/lockfile/requires_dist.py

+14-136
Original file line numberDiff line numberDiff line change
@@ -3,158 +3,36 @@
33

44
from __future__ import absolute_import
55

6-
import operator
7-
from collections import defaultdict, deque
8-
9-
from pex.dist_metadata import Requirement
10-
from pex.exceptions import production_assert
11-
from pex.orderedset import OrderedSet
12-
from pex.pep_503 import ProjectName
13-
from pex.resolve.locked_resolve import LockedRequirement, LockedResolve
6+
from pex.resolve.locked_resolve import LockedResolve
147
from pex.sorted_tuple import SortedTuple
15-
from pex.third_party.packaging.markers import Marker, Variable
16-
from pex.typing import TYPE_CHECKING, cast
8+
from pex.typing import TYPE_CHECKING
179

1810
if TYPE_CHECKING:
19-
from typing import Callable, DefaultDict, Dict, Iterable, List, Optional, Tuple, Union
20-
2111
import attr # vendor:skip
22-
23-
EvalExtra = Callable[[ProjectName], bool]
2412
else:
2513
from pex.third_party import attr
2614

2715

28-
_OPERATORS = {
29-
"in": lambda lhs, rhs: lhs in rhs,
30-
"not in": lambda lhs, rhs: lhs not in rhs,
31-
"<": operator.lt,
32-
"<=": operator.le,
33-
"==": operator.eq,
34-
"!=": operator.ne,
35-
">=": operator.ge,
36-
">": operator.gt,
37-
}
38-
39-
40-
class _Op(object):
41-
def __init__(self, lhs):
42-
self.lhs = lhs # type: EvalExtra
43-
self.rhs = None # type: Optional[EvalExtra]
44-
45-
46-
class _And(_Op):
47-
def __call__(self, extra):
48-
# type: (ProjectName) -> bool
49-
production_assert(self.rhs is not None)
50-
return self.lhs(extra) and cast("EvalExtra", self.rhs)(extra)
51-
52-
53-
class _Or(_Op):
54-
def __call__(self, extra):
55-
# type: (ProjectName) -> bool
56-
production_assert(self.rhs is not None)
57-
return self.lhs(extra) or cast("EvalExtra", self.rhs)(extra)
58-
59-
60-
def _parse_extra_item(
61-
stack, # type: List[EvalExtra]
62-
item, # type: Union[str, List, Tuple]
63-
marker, # type: Marker
64-
):
65-
# type: (...) -> None
66-
67-
if item == "and":
68-
stack.append(_And(stack.pop()))
69-
elif item == "or":
70-
stack.append(_Or(stack.pop()))
71-
elif isinstance(item, list):
72-
for element in item:
73-
_parse_extra_item(stack, element, marker)
74-
elif isinstance(item, tuple):
75-
lhs, op, rhs = item
76-
if isinstance(lhs, Variable) and "extra" == str(lhs):
77-
check = lambda extra: _OPERATORS[str(op)](extra, ProjectName(str(rhs)))
78-
elif isinstance(rhs, Variable) and "extra" == str(rhs):
79-
check = lambda extra: _OPERATORS[str(op)](extra, ProjectName(str(lhs)))
80-
else:
81-
# Any other condition could potentially be true.
82-
check = lambda _: True
83-
if stack:
84-
production_assert(isinstance(stack[-1], _Op))
85-
cast(_Op, stack[-1]).rhs = check
86-
else:
87-
stack.append(check)
88-
else:
89-
raise ValueError("Marker is invalid: {marker}".format(marker=marker))
16+
def remove_unused_requires_dist(locked_resolve):
17+
# type: (LockedResolve) -> LockedResolve
9018

91-
92-
def _parse_extra_check(marker):
93-
# type: (Marker) -> EvalExtra
94-
checks = [] # type: List[EvalExtra]
95-
for item in marker._markers:
96-
_parse_extra_item(checks, item, marker)
97-
production_assert(len(checks) == 1)
98-
return checks[0]
99-
100-
101-
_EXTRA_CHECKS = {} # type: Dict[str, EvalExtra]
102-
103-
104-
def _parse_marker_for_extra_check(marker):
105-
# type: (Marker) -> EvalExtra
106-
maker_str = str(marker)
107-
eval_extra = _EXTRA_CHECKS.get(maker_str)
108-
if not eval_extra:
109-
eval_extra = _parse_extra_check(marker)
110-
_EXTRA_CHECKS[maker_str] = eval_extra
111-
return eval_extra
112-
113-
114-
def _evaluate_for_extras(
115-
marker, # type: Optional[Marker]
116-
extras, # type: Iterable[str]
117-
):
118-
# type: (...) -> bool
119-
if not marker:
120-
return True
121-
eval_extra = _parse_marker_for_extra_check(marker)
122-
return any(eval_extra(ProjectName(extra)) for extra in (extras or [""]))
123-
124-
125-
def remove_unused_requires_dist(
126-
resolve_requirements, # type: Iterable[Requirement]
127-
locked_resolve, # type: LockedResolve
128-
):
129-
# type: (...) -> LockedResolve
130-
131-
locked_req_by_project_name = {
132-
locked_req.pin.project_name: locked_req for locked_req in locked_resolve.locked_requirements
19+
locked_projects = {
20+
locked_req.pin.project_name for locked_req in locked_resolve.locked_requirements
13321
}
134-
requires_dist_by_locked_req = defaultdict(
135-
OrderedSet
136-
) # type: DefaultDict[LockedRequirement, OrderedSet[Requirement]]
137-
seen = set()
138-
requirements = deque(resolve_requirements)
139-
while requirements:
140-
requirement = requirements.popleft()
141-
if requirement in seen:
142-
continue
143-
144-
seen.add(requirement)
145-
locked_req = locked_req_by_project_name[requirement.project_name]
146-
for dep in locked_req.requires_dists:
147-
if _evaluate_for_extras(dep.marker, requirement.extras):
148-
requires_dist_by_locked_req[locked_req].add(dep)
149-
requirements.append(dep)
150-
15122
return attr.evolve(
15223
locked_resolve,
15324
locked_requirements=SortedTuple(
15425
attr.evolve(
15526
locked_requirement,
15627
requires_dists=SortedTuple(
157-
requires_dist_by_locked_req[locked_requirement], key=str
28+
(
29+
requires_dist
30+
for requires_dist in locked_requirement.requires_dists
31+
# Otherwise, the requirement markers were never selected in the resolve
32+
# process; so the requirement was not locked.
33+
if requires_dist.project_name in locked_projects
34+
),
35+
key=str,
15836
),
15937
)
16038
for locked_requirement in locked_resolve.locked_requirements

pex/version.py

+1-1
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.25.0"
4+
__version__ = "2.25.1"

tests/resolve/lockfile/test_requires_dist.py

+31-11
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_remove_unused_requires_dist_noop():
4646
locked_req("baz", "1.0"),
4747
)
4848
assert locked_resolve_with_no_extras == requires_dist.remove_unused_requires_dist(
49-
resolve_requirements=[req("foo")], locked_resolve=locked_resolve_with_no_extras
49+
locked_resolve_with_no_extras
5050
)
5151

5252

@@ -58,8 +58,7 @@ def test_remove_unused_requires_dist_simple():
5858
locked_req("bar", "1.0"),
5959
locked_req("spam", "1.0"),
6060
) == requires_dist.remove_unused_requires_dist(
61-
resolve_requirements=[req("foo")],
62-
locked_resolve=locked_resolve(
61+
locked_resolve(
6362
locked_req("foo", "1.0", "bar", "baz; extra == 'tests'", "spam"),
6463
locked_req("bar", "1.0"),
6564
locked_req("spam", "1.0"),
@@ -75,8 +74,7 @@ def test_remove_unused_requires_dist_mixed_extras():
7574
locked_req("bar", "1.0"),
7675
locked_req("spam", "1.0"),
7776
) == requires_dist.remove_unused_requires_dist(
78-
resolve_requirements=[req("foo[extra1]")],
79-
locked_resolve=locked_resolve(
77+
locked_resolve(
8078
locked_req("foo", "1.0", "bar; extra == 'extra1'", "baz; extra == 'tests'", "spam"),
8179
locked_req("bar", "1.0"),
8280
locked_req("spam", "1.0"),
@@ -99,8 +97,7 @@ def test_remove_unused_requires_dist_mixed_markers():
9997
locked_req("baz", "1.0"),
10098
locked_req("spam", "1.0"),
10199
) == requires_dist.remove_unused_requires_dist(
102-
resolve_requirements=[req("foo[extra1]")],
103-
locked_resolve=locked_resolve(
100+
locked_resolve(
104101
locked_req(
105102
"foo",
106103
"1.0",
@@ -127,8 +124,7 @@ def test_remove_unused_requires_dist_mixed_markers():
127124
locked_req("bar", "1.0"),
128125
locked_req("spam", "1.0"),
129126
) == requires_dist.remove_unused_requires_dist(
130-
resolve_requirements=[req("foo[extra1]")],
131-
locked_resolve=locked_resolve(
127+
locked_resolve(
132128
locked_req(
133129
"foo",
134130
"1.0",
@@ -155,8 +151,7 @@ def test_remove_unused_requires_dist_complex_markers():
155151
locked_req("bar", "1.0"),
156152
locked_req("spam", "1.0"),
157153
) == requires_dist.remove_unused_requires_dist(
158-
resolve_requirements=[req("foo")],
159-
locked_resolve=locked_resolve(
154+
locked_resolve(
160155
locked_req(
161156
"foo",
162157
"1.0",
@@ -168,3 +163,28 @@ def test_remove_unused_requires_dist_complex_markers():
168163
locked_req("spam", "1.0"),
169164
),
170165
)
166+
167+
168+
def test_remove_unused_requires_dist_not_present_due_to_other_markers():
169+
# type: () -> None
170+
171+
assert locked_resolve(
172+
locked_req("foo", "1.0", "bar", "spam"),
173+
locked_req("bar", "1.0"),
174+
locked_req("spam", "1.0"),
175+
) == requires_dist.remove_unused_requires_dist(
176+
locked_resolve(
177+
locked_req(
178+
"foo",
179+
"1.0",
180+
"bar",
181+
"baz; python_version < '3'",
182+
"spam",
183+
),
184+
locked_req("bar", "1.0"),
185+
locked_req("spam", "1.0"),
186+
),
187+
), (
188+
"Here we simulate a lock where baz is not present in the lock since the lock was for "
189+
"Python 3. We expect the lack of a locked baz to not trip up the elision process."
190+
)

0 commit comments

Comments
 (0)