Skip to content

Commit 6b162ac

Browse files
authored
Fix a bug where pip-sync would unexpectedly uninstall some packages (#1919)
1 parent 9636428 commit 6b162ac

File tree

3 files changed

+30
-6
lines changed

3 files changed

+30
-6
lines changed

piptools/sync.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
direct_url_as_pep440_direct_reference,
1616
direct_url_from_link,
1717
)
18+
from pip._vendor.packaging.utils import canonicalize_name
1819

1920
from ._compat import Distribution, get_dev_pkgs
2021
from .exceptions import IncompatibleRequirements
@@ -59,7 +60,7 @@ def dependency_tree(
5960

6061
while queue:
6162
v = queue.popleft()
62-
key = v.key
63+
key = str(canonicalize_name(v.key))
6364
if key in dependencies:
6465
continue
6566

@@ -85,7 +86,7 @@ def get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:
8586
locally, click should also be installed/uninstalled depending on the given
8687
requirements.
8788
"""
88-
installed_keys = {r.key: r for r in installed}
89+
installed_keys = {str(canonicalize_name(r.key)): r for r in installed}
8990
return list(
9091
flat_map(lambda req: dependency_tree(installed_keys, req), PACKAGES_TO_IGNORE)
9192
)
@@ -143,7 +144,7 @@ def diff_key_from_ireq(ireq: InstallRequirement) -> str:
143144

144145
def diff_key_from_req(req: Distribution) -> str:
145146
"""Get a unique key for the requirement."""
146-
key = req.key
147+
key = str(canonicalize_name(req.key))
147148
if (
148149
req.direct_url
149150
and isinstance(req.direct_url.info, ArchiveInfo)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def _fake_dist(line, deps=None):
139139
if deps is None:
140140
deps = []
141141
req = Requirement.parse(line)
142-
key = req.key
142+
key = req.name
143143
if "==" in line:
144144
version = line.split("==")[1]
145145
else:

tests/test_sync.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,16 +226,18 @@ def test_diff_leave_packaging_packages_alone(fake_dist, from_line):
226226
assert to_uninstall == {"first"}
227227

228228

229-
def test_diff_leave_piptools_alone(fake_dist, from_line):
229+
def test_diff_leave_piptools_and_its_dependencies_alone(fake_dist, from_line):
230230
# Suppose an env contains Django, and pip-tools itself (including all of
231231
# its dependencies)
232232
installed = [
233233
fake_dist("django==1.7"),
234234
fake_dist("first==2.0.1"),
235-
fake_dist("pip-tools==1.1.1", ["click>=4", "first", "six"]),
235+
fake_dist("pip-tools==1.1.1", ["click>=4", "first", "six", "build"]),
236236
fake_dist("six==1.9.0"),
237237
fake_dist("click==4.1"),
238238
fake_dist("foobar==0.3.6"),
239+
fake_dist("build==0.10.0", ["pyproject_hooks"]),
240+
fake_dist("pyproject_hooks==1.0.0"),
239241
]
240242

241243
# Then this Django-only requirement should keep pip around (i.e. NOT
@@ -274,6 +276,27 @@ def test_diff_with_matching_url_hash(fake_dist, from_line):
274276
assert to_uninstall == set()
275277

276278

279+
@pytest.mark.parametrize(
280+
("installed_dist", "compiled_req"),
281+
(
282+
pytest.param("Django==1.7", "django==1.7", id="case insensitive"),
283+
pytest.param(
284+
"jaraco.classes==3.2",
285+
"jaraco-classes==3.2",
286+
id="different namespace notation",
287+
),
288+
),
289+
)
290+
def test_diff_respects_canonical_package_names(
291+
fake_dist, from_line, installed_dist, compiled_req
292+
):
293+
installed = [fake_dist(installed_dist)]
294+
reqs = [from_line(compiled_req)]
295+
to_install, to_uninstall = diff(reqs, installed)
296+
assert to_install == set()
297+
assert to_uninstall == set()
298+
299+
277300
def test_diff_with_no_url_hash(fake_dist, from_line):
278301
# if URL hash is not provided, assume the contents have
279302
# changed and reinstall

0 commit comments

Comments
 (0)