Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider specifiers for equality operator to pin a dependency in make_install_requirement #1323

Merged
merged 11 commits into from Feb 25, 2021
4 changes: 1 addition & 3 deletions piptools/repositories/local.py
Expand Up @@ -63,9 +63,7 @@ def find_best_match(self, ireq, prereleases=None):
existing_pin = self.existing_pins.get(key)
if existing_pin and ireq_satisfied_by_existing_pin(ireq, existing_pin):
project, version, _ = as_tuple(existing_pin)
return make_install_requirement(
project, version, ireq.extras, constraint=ireq.constraint
)
return make_install_requirement(project, version, ireq)
else:
return self.repository.find_best_match(ireq, prereleases)

Expand Down
3 changes: 1 addition & 2 deletions piptools/repositories/pypi.py
Expand Up @@ -159,8 +159,7 @@ def find_best_match(self, ireq, prereleases=None):
return make_install_requirement(
best_candidate.name,
best_candidate.version,
ireq.extras,
constraint=ireq.constraint,
ireq,
)

def resolve_reqs(self, download_dir, ireq, wheel_cache):
Expand Down
14 changes: 12 additions & 2 deletions piptools/utils.py
Expand Up @@ -21,6 +21,7 @@
from pip._internal.vcs import is_url
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.version import Version

_KT = TypeVar("_KT")
_VT = TypeVar("_VT")
Expand Down Expand Up @@ -66,16 +67,25 @@ def comment(text: str) -> str:


def make_install_requirement(
name: str, version: str, extras: Iterable[str], constraint: bool = False
name: str, version: Union[str, Version], ireq: InstallRequirement
) -> InstallRequirement:
# If no extras are specified, the extras string is blank
extras_string = ""
extras = ireq.extras
if extras:
# Sort extras for stability
extras_string = f"[{','.join(sorted(extras))}]"

version_pin_operator = "=="
version_as_str = str(version)
for specifier in ireq.specifier:
if specifier.operator == "===" and specifier.version == version_as_str:
version_pin_operator = "==="
break

return install_req_from_line(
str(f"{name}{extras_string}=={version}"), constraint=constraint
str(f"{name}{extras_string}{version_pin_operator}{version}"),
constraint=ireq.constraint,
)


Expand Down
4 changes: 1 addition & 3 deletions tests/conftest.py
Expand Up @@ -68,9 +68,7 @@ def find_best_match(self, ireq, prereleases=False):
]
raise NoCandidateFound(ireq, tried_versions, ["https://fake.url.foo"])
best_version = max(versions, key=Version)
return make_install_requirement(
key_from_ireq(ireq), best_version, ireq.extras, constraint=ireq.constraint
)
return make_install_requirement(key_from_ireq(ireq), best_version, ireq)

def get_dependencies(self, ireq):
if ireq.editable or is_url_requirement(ireq):
Expand Down
71 changes: 71 additions & 0 deletions tests/test_cli_compile.py
Expand Up @@ -1555,3 +1555,74 @@ def test_duplicate_reqs_combined(
assert out.exit_code == 0, out
assert str(test_package_2) in out.stderr
assert "test-package-1==0.1" in out.stderr


@pytest.mark.parametrize(
("pkg2_install_requires", "req_in_content", "out_expected_content"),
(
pytest.param(
"",
["test-package-1===0.1.0\n"],
["test-package-1===0.1.0"],
id="pin package with ===",
),
pytest.param(
"",
["test-package-1==0.1.0\n"],
["test-package-1==0.1.0"],
id="pin package with ==",
),
pytest.param(
"test-package-1==0.1.0",
["test-package-1===0.1.0\n", "test-package-2==0.1.0\n"],
["test-package-1===0.1.0", "test-package-2==0.1.0"],
id="dep === pin preferred over == pin, main package == pin",
),
pytest.param(
"test-package-1==0.1.0",
["test-package-1===0.1.0\n", "test-package-2===0.1.0\n"],
["test-package-1===0.1.0", "test-package-2===0.1.0"],
id="dep === pin preferred over == pin, main package === pin",
),
pytest.param(
"test-package-1==0.1.0",
["test-package-2===0.1.0\n"],
["test-package-1==0.1.0", "test-package-2===0.1.0"],
id="dep == pin conserved, main package === pin",
),
),
)
def test_triple_equal_pinned_dependency_is_used(
runner,
make_package,
make_wheel,
tmpdir,
pkg2_install_requires,
req_in_content,
out_expected_content,
):
"""
Test that pip-compile properly emits the pinned requirement with ===
torchvision 0.8.2 requires torch==1.7.1 which can resolve to versions with
patches (e.g. torch 1.7.1+cu110), we want torch===1.7.1 without patches
"""

dists_dir = tmpdir / "dists"

test_package_1 = make_package("test_package_1", version="0.1.0")
make_wheel(test_package_1, dists_dir)

test_package_2 = make_package(
"test_package_2", version="0.1.0", install_requires=[pkg2_install_requires]
)
make_wheel(test_package_2, dists_dir)

with open("requirements.in", "w") as reqs_in:
for line in req_in_content:
reqs_in.write(line)

out = runner.invoke(cli, ["--find-links", str(dists_dir)])

assert out.exit_code == 0, out
for line in out_expected_content:
assert line in out.stderr