Skip to content

Commit 0e63d0e

Browse files
authored
Merge pull request #629 from sfinkens/fix-pip-hashmode
Add support for pip hash checking
2 parents 75c525e + 96c1326 commit 0e63d0e

File tree

6 files changed

+129
-14
lines changed

6 files changed

+129
-14
lines changed

conda_lock/interfaces/vendored_poetry.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
)
66
from conda_lock._vendor.poetry.core.packages import URLDependency as PoetryURLDependency
77
from conda_lock._vendor.poetry.core.packages import VCSDependency as PoetryVCSDependency
8+
from conda_lock._vendor.poetry.core.packages.utils.link import Link
89
from conda_lock._vendor.poetry.factory import Factory
910
from conda_lock._vendor.poetry.installation.chooser import Chooser
1011
from conda_lock._vendor.poetry.installation.operations.uninstall import Uninstall
@@ -21,6 +22,7 @@
2122
"Chooser",
2223
"Env",
2324
"Factory",
25+
"Link",
2426
"PoetryDependency",
2527
"PoetryPackage",
2628
"PoetryProjectPackage",

conda_lock/models/lock_spec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class VersionedDependency(_BaseDependency):
2929
version: str
3030
build: Optional[str] = None
3131
conda_channel: Optional[str] = None
32+
hash: Optional[str] = None
3233

3334

3435
class URLDependency(_BaseDependency):

conda_lock/pypi_solver.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44

55
from pathlib import Path
66
from posixpath import expandvars
7-
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple
7+
from typing import TYPE_CHECKING, Dict, FrozenSet, List, Literal, Optional, Tuple, Union
88
from urllib.parse import urldefrag, urlsplit, urlunsplit
99

1010
from clikit.api.io.flags import VERY_VERBOSE
1111
from clikit.io import ConsoleIO, NullIO
1212
from packaging.tags import compatible_tags, cpython_tags, mac_platforms
1313
from packaging.version import Version
1414

15+
from conda_lock._vendor.poetry.core.semver import VersionConstraint
1516
from conda_lock.interfaces.vendored_poetry import (
1617
Chooser,
1718
Env,
1819
Factory,
20+
Link,
1921
PoetryDependency,
2022
PoetryPackage,
2123
PoetryProjectPackage,
@@ -278,12 +280,51 @@ def parse_pip_requirement(requirement: str) -> Optional[Dict[str, str]]:
278280
return match.groupdict()
279281

280282

283+
class PoetryDependencyWithHash(PoetryDependency):
284+
def __init__(
285+
self,
286+
name, # type: str
287+
constraint, # type: Union[str, VersionConstraint]
288+
optional=False, # type: bool
289+
category="main", # type: str
290+
allows_prereleases=False, # type: bool
291+
extras=None, # type: Optional[Union[List[str], FrozenSet[str]]]
292+
source_type=None, # type: Optional[str]
293+
source_url=None, # type: Optional[str]
294+
source_reference=None, # type: Optional[str]
295+
source_resolved_reference=None, # type: Optional[str]
296+
hash: Optional[str] = None,
297+
) -> None:
298+
super().__init__(
299+
name,
300+
constraint,
301+
optional=optional,
302+
category=category,
303+
allows_prereleases=allows_prereleases,
304+
extras=extras, # type: ignore # upstream type hint is wrong
305+
source_type=source_type,
306+
source_url=source_url,
307+
source_reference=source_reference,
308+
source_resolved_reference=source_resolved_reference,
309+
)
310+
self.hash = hash
311+
312+
def get_hash_model(self) -> Optional[HashModel]:
313+
if self.hash:
314+
algo, value = self.hash.split(":")
315+
return HashModel(**{algo: value})
316+
return None
317+
318+
281319
def get_dependency(dep: lock_spec.Dependency) -> PoetryDependency:
282320
# FIXME: how do deal with extras?
283321
extras: List[str] = []
284322
if isinstance(dep, lock_spec.VersionedDependency):
285-
return PoetryDependency(
286-
name=dep.name, constraint=dep.version or "*", extras=dep.extras
323+
return PoetryDependencyWithHash(
324+
name=dep.name,
325+
constraint=dep.version or "*",
326+
extras=dep.extras,
327+
hash=dep.hash,
287328
)
288329
elif isinstance(dep, lock_spec.URLDependency):
289330
return PoetryURLDependency(
@@ -359,14 +400,9 @@ def get_requirements(
359400
# https://github.com/conda/conda-lock/blob/ac31f5ddf2951ed4819295238ccf062fb2beb33c/conda_lock/_vendor/poetry/installation/executor.py#L557
360401
else:
361402
link = chooser.choose_for(op.package)
362-
parsed_url = urlsplit(link.url)
363-
link.url = link.url.replace(parsed_url.netloc, str(parsed_url.hostname))
364-
url = link.url_without_fragment
365-
hashes: Dict[str, str] = {}
366-
if link.hash_name is not None and link.hash is not None:
367-
hashes[link.hash_name] = link.hash
368-
hash = HashModel.parse_obj(hashes)
369-
403+
url = _get_url(link)
404+
hash_chooser = _HashChooser(link, op.package.dependency)
405+
hash = hash_chooser.get_hash()
370406
if source_repository:
371407
url = source_repository.normalize_solver_url(url)
372408

@@ -387,6 +423,34 @@ def get_requirements(
387423
return requirements
388424

389425

426+
def _get_url(link: Link) -> str:
427+
parsed_url = urlsplit(link.url)
428+
link.url = link.url.replace(parsed_url.netloc, str(parsed_url.hostname))
429+
return link.url_without_fragment
430+
431+
432+
class _HashChooser:
433+
def __init__(
434+
self, link: Link, dependency: Union[PoetryDependency, PoetryDependencyWithHash]
435+
):
436+
self.link = link
437+
self.dependency = dependency
438+
439+
def get_hash(self) -> HashModel:
440+
return self._get_hash_from_dependency() or self._get_hash_from_link()
441+
442+
def _get_hash_from_dependency(self) -> Optional[HashModel]:
443+
if isinstance(self.dependency, PoetryDependencyWithHash):
444+
return self.dependency.get_hash_model()
445+
return None
446+
447+
def _get_hash_from_link(self) -> HashModel:
448+
hashes: Dict[str, str] = {}
449+
if self.link.hash_name is not None and self.link.hash is not None:
450+
hashes[self.link.hash_name] = self.link.hash
451+
return HashModel.parse_obj(hashes)
452+
453+
390454
def solve_pypi(
391455
pip_specs: Dict[str, lock_spec.Dependency],
392456
use_latest: List[str],

conda_lock/src_parser/pyproject_toml.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,25 @@ def to_match_spec(conda_dep_name: str, conda_version: Optional[str]) -> str:
377377
return spec
378378

379379

380+
class RequirementWithHash(Requirement):
381+
"""Requirement with support for pip hash checking.
382+
383+
Pip offers hash checking where the requirement string is
384+
my_package == 1.23 --hash=sha256:1234...
385+
"""
386+
387+
def __init__(self, requirement_string: str) -> None:
388+
try:
389+
requirement_string, hash = requirement_string.split(" --hash=")
390+
except ValueError:
391+
hash = None
392+
self.hash: Optional[str] = hash
393+
super().__init__(requirement_string)
394+
395+
380396
def parse_requirement_specifier(
381397
requirement: str,
382-
) -> Requirement:
398+
) -> RequirementWithHash:
383399
"""Parse a url requirement to a conda spec"""
384400
if (
385401
requirement.startswith("git+")
@@ -392,9 +408,9 @@ def parse_requirement_specifier(
392408
if repo_name.endswith(".git"):
393409
repo_name = repo_name[:-4]
394410
# Use the repo name as a placeholder for the package name
395-
return Requirement(f"{repo_name} @ {requirement}")
411+
return RequirementWithHash(f"{repo_name} @ {requirement}")
396412
else:
397-
return Requirement(requirement)
413+
return RequirementWithHash(requirement)
398414

399415

400416
def unpack_git_url(url: str) -> Tuple[str, Optional[str]]:
@@ -460,6 +476,7 @@ def parse_python_requirement(
460476
manager=manager,
461477
category=category,
462478
extras=extras,
479+
hash=parsed_req.hash,
463480
)
464481

465482

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# environment.yml
2+
channels:
3+
- conda-forge
4+
5+
dependencies:
6+
- pip
7+
- pip:
8+
- flit-core === 3.9.0 --hash=sha256:1234

tests/test_conda_lock.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ def pip_environment_different_names_same_deps(tmp_path: Path):
156156
)
157157

158158

159+
@pytest.fixture
160+
def pip_hash_checking_environment(tmp_path: Path):
161+
return clone_test_dir("test-pip-hash-checking", tmp_path).joinpath(
162+
"environment.yml"
163+
)
164+
165+
159166
@pytest.fixture
160167
def pip_local_package_environment(tmp_path: Path):
161168
return clone_test_dir("test-local-pip", tmp_path).joinpath("environment.yml")
@@ -1539,6 +1546,22 @@ def test_run_lock_with_pip_environment_different_names_same_deps(
15391546
run_lock([pip_environment_different_names_same_deps], conda_exe=conda_exe)
15401547

15411548

1549+
def test_run_lock_with_pip_hash_checking(
1550+
monkeypatch: "pytest.MonkeyPatch",
1551+
pip_hash_checking_environment: Path,
1552+
conda_exe: str,
1553+
):
1554+
work_dir = pip_hash_checking_environment.parent
1555+
monkeypatch.chdir(work_dir)
1556+
if is_micromamba(conda_exe):
1557+
monkeypatch.setenv("CONDA_FLAGS", "-v")
1558+
run_lock([pip_hash_checking_environment], conda_exe=conda_exe)
1559+
1560+
lockfile = parse_conda_lock_file(work_dir / DEFAULT_LOCKFILE_NAME)
1561+
hashes = {package.name: package.hash for package in lockfile.package}
1562+
assert hashes["flit-core"].sha256 == "1234"
1563+
1564+
15421565
def test_run_lock_uppercase_pip(
15431566
monkeypatch: "pytest.MonkeyPatch",
15441567
env_with_uppercase_pip: Path,

0 commit comments

Comments
 (0)