Skip to content

Commit b64111a

Browse files
committed
Fix pip-sync --python-executable evaluating markers for the wrong environment
1 parent 5330964 commit b64111a

File tree

2 files changed

+68
-6
lines changed

2 files changed

+68
-6
lines changed

piptools/scripts/sync.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
import itertools
4+
import json
45
import os
56
import shlex
67
import shutil
78
import sys
89
from pathlib import Path
10+
from subprocess import run # nosec
911
from typing import cast
1012

1113
import click
@@ -100,6 +102,21 @@ def cli(
100102

101103
if python_executable:
102104
_validate_python_executable(python_executable)
105+
environment = json.loads(
106+
run( # nosec
107+
[
108+
python_executable,
109+
"-m",
110+
"pip",
111+
"inspect",
112+
],
113+
check=True,
114+
capture_output=True,
115+
text=True,
116+
).stdout
117+
)["environment"]
118+
else:
119+
environment = None
103120

104121
install_command = cast(InstallCommand, create_command("install"))
105122
options, _ = install_command.parse_args([])
@@ -113,7 +130,9 @@ def cli(
113130
)
114131

115132
try:
116-
merged_requirements = sync.merge(requirements, ignore_conflicts=force)
133+
merged_requirements = sync.merge(
134+
requirements, ignore_conflicts=force, environment=environment
135+
)
117136
except PipToolsError as e:
118137
log.error(str(e))
119138
sys.exit(2)
@@ -128,7 +147,9 @@ def cli(
128147
local_only=python_executable is None,
129148
paths=paths,
130149
)
131-
to_install, to_uninstall = sync.diff(merged_requirements, installed_dists)
150+
to_install, to_uninstall = sync.diff(
151+
merged_requirements, installed_dists, environment
152+
)
132153

133154
install_flags = _compose_install_flags(
134155
finder,

piptools/sync.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import sys
66
import tempfile
7+
from functools import wraps
78
from subprocess import run # nosec
89
from typing import Deque, Iterable, Mapping, ValuesView
910

@@ -40,6 +41,41 @@
4041
]
4142

4243

44+
def patch_match_markers() -> None:
45+
"""
46+
Monkey patches pip._internal.req.InstallRequirement.match_markers to allow
47+
us to pass environment other than "extra".
48+
"""
49+
50+
@wraps(InstallRequirement.match_markers)
51+
def match_markers(
52+
self: InstallRequirement,
53+
extras_requested: Iterable[str] | None = None,
54+
environment: dict[str, str] | None = None,
55+
) -> bool:
56+
if environment is None:
57+
environment = {}
58+
59+
assert "extra" not in environment
60+
61+
if not extras_requested:
62+
# Provide an extra to safely evaluate the markers
63+
# without matching any extra
64+
extras_requested = ("",)
65+
if self.markers is not None:
66+
return any(
67+
self.markers.evaluate({"extra": extra, **environment})
68+
for extra in extras_requested
69+
)
70+
else:
71+
return True
72+
73+
InstallRequirement.match_markers = match_markers
74+
75+
76+
patch_match_markers()
77+
78+
4379
def dependency_tree(
4480
installed_keys: Mapping[str, Distribution], root_key: str
4581
) -> set[str]:
@@ -93,15 +129,17 @@ def get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:
93129

94130

95131
def merge(
96-
requirements: Iterable[InstallRequirement], ignore_conflicts: bool
132+
requirements: Iterable[InstallRequirement],
133+
ignore_conflicts: bool,
134+
environment: dict[str, str] | None = None,
97135
) -> ValuesView[InstallRequirement]:
98136
by_key: dict[str, InstallRequirement] = {}
99137

100138
for ireq in requirements:
101139
# Limitation: URL requirements are merged by precise string match, so
102140
# "file:///example.zip#egg=example", "file:///example.zip", and
103141
# "example==1.0" will not merge with each other
104-
if ireq.match_markers():
142+
if ireq.match_markers(environment=environment):
105143
key = key_from_ireq(ireq)
106144

107145
if not ignore_conflicts:
@@ -158,6 +196,7 @@ def diff_key_from_req(req: Distribution) -> str:
158196
def diff(
159197
compiled_requirements: Iterable[InstallRequirement],
160198
installed_dists: Iterable[Distribution],
199+
environment: dict[str, str] | None = None,
161200
) -> tuple[set[InstallRequirement], set[str]]:
162201
"""
163202
Calculate which packages should be installed or uninstalled, given a set
@@ -172,13 +211,15 @@ def diff(
172211
pkgs_to_ignore = get_dists_to_ignore(installed_dists)
173212
for dist in installed_dists:
174213
key = diff_key_from_req(dist)
175-
if key not in requirements_lut or not requirements_lut[key].match_markers():
214+
if key not in requirements_lut or not requirements_lut[key].match_markers(
215+
environment=environment
216+
):
176217
to_uninstall.add(key)
177218
elif requirements_lut[key].specifier.contains(dist.version):
178219
satisfied.add(key)
179220

180221
for key, requirement in requirements_lut.items():
181-
if key not in satisfied and requirement.match_markers():
222+
if key not in satisfied and requirement.match_markers(environment=environment):
182223
to_install.add(requirement)
183224

184225
# Make sure to not uninstall any packages that should be ignored

0 commit comments

Comments
 (0)