codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import dataclasses4import logging5import re6from collections.abc import Mapping, Sequence7from dataclasses import dataclass8from datetime import datetime9from typing import (10 TYPE_CHECKING,11 Any,12 Callable,13 Protocol,14 TypeVar,15 cast,16)17from urllib.parse import urlparse18 19from .markers import Environment, Marker, default_environment20from .specifiers import SpecifierSet21from .tags import create_compatible_tags_selector, sys_tags22from .utils import (23 NormalizedName,24 is_normalized_name,25 parse_sdist_filename,26 parse_wheel_filename,27)28from .version import Version29 30if TYPE_CHECKING: # pragma: no cover31 from collections.abc import Collection, Iterator32 from pathlib import Path33 34 from typing_extensions import Self35 36 from .tags import Tag37 38_logger = logging.getLogger(__name__)39 40__all__ = [41 "Package",42 "PackageArchive",43 "PackageDirectory",44 "PackageSdist",45 "PackageVcs",46 "PackageWheel",47 "Pylock",48 "PylockUnsupportedVersionError",49 "PylockValidationError",50 "is_valid_pylock_path",51]52 53 54def __dir__() -> list[str]:55 return __all__56 57 58_T = TypeVar("_T")59_T2 = TypeVar("_T2")60 61 62class _FromMappingProtocol(Protocol): # pragma: no cover63 @classmethod64 def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...65 66 67_FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)68 69 70_PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")71 72 73def is_valid_pylock_path(path: Path) -> bool:74 """Check if the given path is a valid pylock file path."""75 return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))76 77 78def _toml_key(key: str) -> str:79 return key.replace("_", "-")80 81 82def _toml_value(key: str, value: Any) -> Any: # noqa: ANN40183 if isinstance(value, (Version, Marker, SpecifierSet)):84 return str(value)85 if isinstance(value, Sequence) and key == "environments":86 return [str(v) for v in value]87 return value88 89 90def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:91 return {92 _toml_key(key): _toml_value(key, value)93 for key, value in data94 if value is not None95 }96 97 98def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:99 """Get a value from the dictionary and verify it's the expected type."""100 if (value := d.get(key)) is None:101 return None102 if not isinstance(value, expected_type):103 raise PylockValidationError(104 f"Unexpected type {type(value).__name__} "105 f"(expected {expected_type.__name__})",106 context=key,107 )108 return value109 110 111def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:112 """Get a required value from the dictionary and verify it's the expected type."""113 if (value := _get(d, expected_type, key)) is None:114 raise _PylockRequiredKeyError(key)115 return value116 117 118def _get_sequence(119 d: Mapping[str, Any], expected_item_type: type[_T], key: str120) -> Sequence[_T] | None:121 """Get a list value from the dictionary and verify it's the expected items type."""122 if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]123 return None124 if isinstance(value, (str, bytes)):125 # special case: str and bytes are Sequences, but we want to reject it126 raise PylockValidationError(127 f"Unexpected type {type(value).__name__} (expected Sequence)",128 context=key,129 )130 for i, item in enumerate(value):131 if not isinstance(item, expected_item_type):132 raise PylockValidationError(133 f"Unexpected type {type(item).__name__} "134 f"(expected {expected_item_type.__name__})",135 context=f"{key}[{i}]",136 )137 return value138 139 140def _get_as(141 d: Mapping[str, Any],142 expected_type: type[_T],143 target_type: Callable[[_T], _T2],144 key: str,145) -> _T2 | None:146 """Get a value from the dictionary, verify it's the expected type,147 and convert to the target type.148 149 This assumes the target_type constructor accepts the value.150 """151 if (value := _get(d, expected_type, key)) is None:152 return None153 try:154 return target_type(value)155 except Exception as e:156 raise PylockValidationError(e, context=key) from e157 158 159def _get_required_as(160 d: Mapping[str, Any],161 expected_type: type[_T],162 target_type: Callable[[_T], _T2],163 key: str,164) -> _T2:165 """Get a required value from the dict, verify it's the expected type,166 and convert to the target type."""167 if (value := _get_as(d, expected_type, target_type, key)) is None:168 raise _PylockRequiredKeyError(key)169 return value170 171 172def _get_sequence_as(173 d: Mapping[str, Any],174 expected_item_type: type[_T],175 target_item_type: Callable[[_T], _T2],176 key: str,177) -> list[_T2] | None:178 """Get list value from dictionary and verify expected items type."""179 if (value := _get_sequence(d, expected_item_type, key)) is None:180 return None181 result = []182 try:183 for item in value:184 typed_item = target_item_type(item)185 result.append(typed_item)186 except Exception as e:187 raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e188 return result189 190 191def _get_object(192 d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str193) -> _FromMappingProtocolT | None:194 """Get a dictionary value from the dictionary and convert it to a dataclass."""195 if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]196 return None197 try:198 return target_type._from_dict(value)199 except Exception as e:200 raise PylockValidationError(e, context=key) from e201 202 203def _get_sequence_of_objects(204 d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str205) -> list[_FromMappingProtocolT] | None:206 """Get a list value from the dictionary and convert its items to a dataclass."""207 if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]208 return None209 result: list[_FromMappingProtocolT] = []210 try:211 for item in value:212 typed_item = target_item_type._from_dict(item)213 result.append(typed_item)214 except Exception as e:215 raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e216 return result217 218 219def _get_required_sequence_of_objects(220 d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str221) -> Sequence[_FromMappingProtocolT]:222 """Get a required list value from the dictionary and convert its items to a223 dataclass."""224 if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:225 raise _PylockRequiredKeyError(key)226 return result227 228 229def _validate_normalized_name(name: str) -> NormalizedName:230 """Validate that a string is a NormalizedName."""231 if not is_normalized_name(name):232 raise PylockValidationError(f"Name {name!r} is not normalized")233 return NormalizedName(name)234 235 236def _validate_path_url(path: str | None, url: str | None) -> None:237 if not path and not url:238 raise PylockValidationError("path or url must be provided")239 240 241def _path_name(path: str | None) -> str | None:242 if not path:243 return None244 # If the path is relative it MAY use POSIX-style path separators explicitly245 # for portability246 if "/" in path:247 return path.rsplit("/", 1)[-1]248 elif "\\" in path:249 return path.rsplit("\\", 1)[-1]250 else:251 return path252 253 254def _url_name(url: str | None) -> str | None:255 if not url:256 return None257 url_path = urlparse(url).path258 return url_path.rsplit("/", 1)[-1]259 260 261def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:262 if not hashes:263 raise PylockValidationError("At least one hash must be provided")264 if not all(isinstance(hash_val, str) for hash_val in hashes.values()):265 raise PylockValidationError("Hash values must be strings")266 return hashes267 268 269class PylockValidationError(Exception):270 """Raised when when input data is not spec-compliant."""271 272 context: str | None = None273 message: str274 275 def __init__(276 self,277 cause: str | Exception,278 *,279 context: str | None = None,280 ) -> None:281 if isinstance(cause, PylockValidationError):282 if cause.context:283 self.context = (284 f"{context}.{cause.context}" if context else cause.context285 )286 else:287 self.context = context288 self.message = cause.message289 else:290 self.context = context291 self.message = str(cause)292 293 def __str__(self) -> str:294 if self.context:295 return f"{self.message} in {self.context!r}"296 return self.message297 298 299class _PylockRequiredKeyError(PylockValidationError):300 def __init__(self, key: str) -> None:301 super().__init__("Missing required value", context=key)302 303 304class PylockUnsupportedVersionError(PylockValidationError):305 """Raised when encountering an unsupported `lock_version`."""306 307 308class PylockSelectError(Exception):309 """Base exception for errors raised by :meth:`Pylock.select`."""310 311 312@dataclass(frozen=True, init=False)313class PackageVcs:314 type: str315 url: str | None = None316 path: str | None = None317 requested_revision: str | None = None318 commit_id: str # type: ignore[misc]319 subdirectory: str | None = None320 321 def __init__(322 self,323 *,324 type: str,325 url: str | None = None,326 path: str | None = None,327 requested_revision: str | None = None,328 commit_id: str,329 subdirectory: str | None = None,330 ) -> None:331 # In Python 3.10+ make dataclass kw_only=True and remove __init__332 object.__setattr__(self, "type", type)333 object.__setattr__(self, "url", url)334 object.__setattr__(self, "path", path)335 object.__setattr__(self, "requested_revision", requested_revision)336 object.__setattr__(self, "commit_id", commit_id)337 object.__setattr__(self, "subdirectory", subdirectory)338 339 @classmethod340 def _from_dict(cls, d: Mapping[str, Any]) -> Self:341 package_vcs = cls(342 type=_get_required(d, str, "type"),343 url=_get(d, str, "url"),344 path=_get(d, str, "path"),345 requested_revision=_get(d, str, "requested-revision"),346 commit_id=_get_required(d, str, "commit-id"),347 subdirectory=_get(d, str, "subdirectory"),348 )349 _validate_path_url(package_vcs.path, package_vcs.url)350 return package_vcs351 352 353@dataclass(frozen=True, init=False)354class PackageDirectory:355 path: str356 editable: bool | None = None357 subdirectory: str | None = None358 359 def __init__(360 self,361 *,362 path: str,363 editable: bool | None = None,364 subdirectory: str | None = None,365 ) -> None:366 # In Python 3.10+ make dataclass kw_only=True and remove __init__367 object.__setattr__(self, "path", path)368 object.__setattr__(self, "editable", editable)369 object.__setattr__(self, "subdirectory", subdirectory)370 371 @classmethod372 def _from_dict(cls, d: Mapping[str, Any]) -> Self:373 return cls(374 path=_get_required(d, str, "path"),375 editable=_get(d, bool, "editable"),376 subdirectory=_get(d, str, "subdirectory"),377 )378 379 380@dataclass(frozen=True, init=False)381class PackageArchive:382 url: str | None = None383 path: str | None = None384 size: int | None = None385 upload_time: datetime | None = None386 hashes: Mapping[str, str] # type: ignore[misc]387 subdirectory: str | None = None388 389 def __init__(390 self,391 *,392 url: str | None = None,393 path: str | None = None,394 size: int | None = None,395 upload_time: datetime | None = None,396 hashes: Mapping[str, str],397 subdirectory: str | None = None,398 ) -> None:399 # In Python 3.10+ make dataclass kw_only=True and remove __init__400 object.__setattr__(self, "url", url)401 object.__setattr__(self, "path", path)402 object.__setattr__(self, "size", size)403 object.__setattr__(self, "upload_time", upload_time)404 object.__setattr__(self, "hashes", hashes)405 object.__setattr__(self, "subdirectory", subdirectory)406 407 @classmethod408 def _from_dict(cls, d: Mapping[str, Any]) -> Self:409 package_archive = cls(410 url=_get(d, str, "url"),411 path=_get(d, str, "path"),412 size=_get(d, int, "size"),413 upload_time=_get(d, datetime, "upload-time"),414 hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]415 subdirectory=_get(d, str, "subdirectory"),416 )417 _validate_path_url(package_archive.path, package_archive.url)418 return package_archive419 420 421@dataclass(frozen=True, init=False)422class PackageSdist:423 name: str | None = None424 upload_time: datetime | None = None425 url: str | None = None426 path: str | None = None427 size: int | None = None428 hashes: Mapping[str, str] # type: ignore[misc]429 430 def __init__(431 self,432 *,433 name: str | None = None,434 upload_time: datetime | None = None,435 url: str | None = None,436 path: str | None = None,437 size: int | None = None,438 hashes: Mapping[str, str],439 ) -> None:440 # In Python 3.10+ make dataclass kw_only=True and remove __init__441 object.__setattr__(self, "name", name)442 object.__setattr__(self, "upload_time", upload_time)443 object.__setattr__(self, "url", url)444 object.__setattr__(self, "path", path)445 object.__setattr__(self, "size", size)446 object.__setattr__(self, "hashes", hashes)447 448 @classmethod449 def _from_dict(cls, d: Mapping[str, Any]) -> Self:450 package_sdist = cls(451 name=_get(d, str, "name"),452 upload_time=_get(d, datetime, "upload-time"),453 url=_get(d, str, "url"),454 path=_get(d, str, "path"),455 size=_get(d, int, "size"),456 hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]457 )458 _validate_path_url(package_sdist.path, package_sdist.url)459 return package_sdist460 461 @property462 def filename(self) -> str:463 """Get the filename of the sdist."""464 filename = self.name or _path_name(self.path) or _url_name(self.url)465 if not filename:466 raise PylockValidationError("Cannot determine sdist filename")467 return filename468 469 470@dataclass(frozen=True, init=False)471class PackageWheel:472 name: str | None = None473 upload_time: datetime | None = None474 url: str | None = None475 path: str | None = None476 size: int | None = None477 hashes: Mapping[str, str] # type: ignore[misc]478 479 def __init__(480 self,481 *,482 name: str | None = None,483 upload_time: datetime | None = None,484 url: str | None = None,485 path: str | None = None,486 size: int | None = None,487 hashes: Mapping[str, str],488 ) -> None:489 # In Python 3.10+ make dataclass kw_only=True and remove __init__490 object.__setattr__(self, "name", name)491 object.__setattr__(self, "upload_time", upload_time)492 object.__setattr__(self, "url", url)493 object.__setattr__(self, "path", path)494 object.__setattr__(self, "size", size)495 object.__setattr__(self, "hashes", hashes)496 497 @classmethod498 def _from_dict(cls, d: Mapping[str, Any]) -> Self:499 package_wheel = cls(500 name=_get(d, str, "name"),501 upload_time=_get(d, datetime, "upload-time"),502 url=_get(d, str, "url"),503 path=_get(d, str, "path"),504 size=_get(d, int, "size"),505 hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]506 )507 _validate_path_url(package_wheel.path, package_wheel.url)508 return package_wheel509 510 @property511 def filename(self) -> str:512 """Get the filename of the wheel."""513 filename = self.name or _path_name(self.path) or _url_name(self.url)514 if not filename:515 raise PylockValidationError("Cannot determine wheel filename")516 return filename517 518 519@dataclass(frozen=True, init=False)520class Package:521 name: NormalizedName522 version: Version | None = None523 marker: Marker | None = None524 requires_python: SpecifierSet | None = None525 dependencies: Sequence[Mapping[str, Any]] | None = None526 vcs: PackageVcs | None = None527 directory: PackageDirectory | None = None528 archive: PackageArchive | None = None529 index: str | None = None530 sdist: PackageSdist | None = None531 wheels: Sequence[PackageWheel] | None = None532 attestation_identities: Sequence[Mapping[str, Any]] | None = None533 tool: Mapping[str, Any] | None = None534 535 def __init__(536 self,537 *,538 name: NormalizedName,539 version: Version | None = None,540 marker: Marker | None = None,541 requires_python: SpecifierSet | None = None,542 dependencies: Sequence[Mapping[str, Any]] | None = None,543 vcs: PackageVcs | None = None,544 directory: PackageDirectory | None = None,545 archive: PackageArchive | None = None,546 index: str | None = None,547 sdist: PackageSdist | None = None,548 wheels: Sequence[PackageWheel] | None = None,549 attestation_identities: Sequence[Mapping[str, Any]] | None = None,550 tool: Mapping[str, Any] | None = None,551 ) -> None:552 # In Python 3.10+ make dataclass kw_only=True and remove __init__553 object.__setattr__(self, "name", name)554 object.__setattr__(self, "version", version)555 object.__setattr__(self, "marker", marker)556 object.__setattr__(self, "requires_python", requires_python)557 object.__setattr__(self, "dependencies", dependencies)558 object.__setattr__(self, "vcs", vcs)559 object.__setattr__(self, "directory", directory)560 object.__setattr__(self, "archive", archive)561 object.__setattr__(self, "index", index)562 object.__setattr__(self, "sdist", sdist)563 object.__setattr__(self, "wheels", wheels)564 object.__setattr__(self, "attestation_identities", attestation_identities)565 object.__setattr__(self, "tool", tool)566 567 @classmethod568 def _from_dict(cls, d: Mapping[str, Any]) -> Self:569 package = cls(570 name=_get_required_as(d, str, _validate_normalized_name, "name"),571 version=_get_as(d, str, Version, "version"),572 requires_python=_get_as(d, str, SpecifierSet, "requires-python"),573 dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]574 marker=_get_as(d, str, Marker, "marker"),575 vcs=_get_object(d, PackageVcs, "vcs"),576 directory=_get_object(d, PackageDirectory, "directory"),577 archive=_get_object(d, PackageArchive, "archive"),578 index=_get(d, str, "index"),579 sdist=_get_object(d, PackageSdist, "sdist"),580 wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),581 attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]582 tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]583 )584 distributions = bool(package.sdist) + len(package.wheels or [])585 direct_urls = (586 bool(package.vcs) + bool(package.directory) + bool(package.archive)587 )588 if distributions > 0 and direct_urls > 0:589 raise PylockValidationError(590 "None of vcs, directory, archive must be set if sdist or wheels are set"591 )592 if distributions == 0 and direct_urls != 1:593 raise PylockValidationError(594 "Exactly one of vcs, directory, archive must be set "595 "if sdist and wheels are not set"596 )597 for i, wheel in enumerate(package.wheels or []):598 try:599 (name, version, _, _) = parse_wheel_filename(wheel.filename)600 except Exception as e:601 raise PylockValidationError(602 f"Invalid wheel filename {wheel.filename!r}",603 context=f"wheels[{i}]",604 ) from e605 if name != package.name:606 raise PylockValidationError(607 f"Name in {wheel.filename!r} is not consistent with "608 f"package name {package.name!r}",609 context=f"wheels[{i}]",610 )611 if package.version and version != package.version:612 raise PylockValidationError(613 f"Version in {wheel.filename!r} is not consistent with "614 f"package version {str(package.version)!r}",615 context=f"wheels[{i}]",616 )617 if package.sdist:618 try:619 name, version = parse_sdist_filename(package.sdist.filename)620 except Exception as e:621 raise PylockValidationError(622 f"Invalid sdist filename {package.sdist.filename!r}",623 context="sdist",624 ) from e625 if name != package.name:626 raise PylockValidationError(627 f"Name in {package.sdist.filename!r} is not consistent with "628 f"package name {package.name!r}",629 context="sdist",630 )631 if package.version and version != package.version:632 raise PylockValidationError(633 f"Version in {package.sdist.filename!r} is not consistent with "634 f"package version {str(package.version)!r}",635 context="sdist",636 )637 try:638 for i, attestation_identity in enumerate( # noqa: B007639 package.attestation_identities or []640 ):641 _get_required(attestation_identity, str, "kind")642 except Exception as e:643 raise PylockValidationError(644 e, context=f"attestation-identities[{i}]"645 ) from e646 return package647 648 @property649 def is_direct(self) -> bool:650 return not (self.sdist or self.wheels)651 652 653@dataclass(frozen=True, init=False)654class Pylock:655 """A class representing a pylock file."""656 657 lock_version: Version658 environments: Sequence[Marker] | None = None659 requires_python: SpecifierSet | None = None660 extras: Sequence[NormalizedName] | None = None661 dependency_groups: Sequence[str] | None = None662 default_groups: Sequence[str] | None = None663 created_by: str # type: ignore[misc]664 packages: Sequence[Package] # type: ignore[misc]665 tool: Mapping[str, Any] | None = None666 667 def __init__(668 self,669 *,670 lock_version: Version,671 environments: Sequence[Marker] | None = None,672 requires_python: SpecifierSet | None = None,673 extras: Sequence[NormalizedName] | None = None,674 dependency_groups: Sequence[str] | None = None,675 default_groups: Sequence[str] | None = None,676 created_by: str,677 packages: Sequence[Package],678 tool: Mapping[str, Any] | None = None,679 ) -> None:680 # In Python 3.10+ make dataclass kw_only=True and remove __init__681 object.__setattr__(self, "lock_version", lock_version)682 object.__setattr__(self, "environments", environments)683 object.__setattr__(self, "requires_python", requires_python)684 object.__setattr__(self, "extras", extras)685 object.__setattr__(self, "dependency_groups", dependency_groups)686 object.__setattr__(self, "default_groups", default_groups)687 object.__setattr__(self, "created_by", created_by)688 object.__setattr__(self, "packages", packages)689 object.__setattr__(self, "tool", tool)690 691 @classmethod692 def _from_dict(cls, d: Mapping[str, Any]) -> Self:693 pylock = cls(694 lock_version=_get_required_as(d, str, Version, "lock-version"),695 environments=_get_sequence_as(d, str, Marker, "environments"),696 extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),697 dependency_groups=_get_sequence(d, str, "dependency-groups"),698 default_groups=_get_sequence(d, str, "default-groups"),699 created_by=_get_required(d, str, "created-by"),700 requires_python=_get_as(d, str, SpecifierSet, "requires-python"),701 packages=_get_required_sequence_of_objects(d, Package, "packages"),702 tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]703 )704 if not Version("1") <= pylock.lock_version < Version("2"):705 raise PylockUnsupportedVersionError(706 f"pylock version {pylock.lock_version} is not supported"707 )708 if pylock.lock_version > Version("1.0"):709 _logger.warning(710 "pylock minor version %s is not supported", pylock.lock_version711 )712 return pylock713 714 @classmethod715 def from_dict(cls, d: Mapping[str, Any], /) -> Self:716 """Create and validate a Pylock instance from a TOML dictionary.717 718 Raises :class:`PylockValidationError` if the input data is not719 spec-compliant.720 """721 return cls._from_dict(d)722 723 def to_dict(self) -> Mapping[str, Any]:724 """Convert the Pylock instance to a TOML dictionary."""725 return dataclasses.asdict(self, dict_factory=_toml_dict_factory)726 727 def validate(self) -> None:728 """Validate the Pylock instance against the specification.729 730 Raises :class:`PylockValidationError` otherwise."""731 self.from_dict(self.to_dict())732 733 def select(734 self,735 *,736 environment: Environment | None = None,737 tags: Sequence[Tag] | None = None,738 extras: Collection[str] | None = None,739 dependency_groups: Collection[str] | None = None,740 ) -> Iterator[741 tuple[742 Package,743 PackageVcs744 | PackageDirectory745 | PackageArchive746 | PackageWheel747 | PackageSdist,748 ]749 ]:750 """Select what to install from the lock file.751 752 The *environment* and *tags* parameters represent the environment being753 selected for. If unspecified, ``packaging.markers.default_environment()`` and754 ``packaging.tags.sys_tags()`` are used.755 756 The *extras* parameter represents the extras to install.757 758 The *dependency_groups* parameter represents the groups to install. If759 unspecified, the default groups are used.760 761 This method must be used on valid Pylock instances (i.e. one obtained762 from :meth:`Pylock.from_dict` or if constructed manually, after calling763 :meth:`Pylock.validate`).764 """765 compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())766 767 # #. Gather the extras and dependency groups to install and set ``extras`` and768 # ``dependency_groups`` for marker evaluation, respectively.769 #770 # #. ``extras`` SHOULD be set to the empty set by default.771 # #. ``dependency_groups`` SHOULD be the set created from772 # :ref:`pylock-default-groups` by default.773 env = cast(774 "dict[str, str | frozenset[str]]",775 dict(776 environment or {}, # Marker.evaluate will fill-up777 extras=frozenset(extras or []),778 dependency_groups=frozenset(779 (self.default_groups or [])780 if dependency_groups is None # to allow selecting no group781 else dependency_groups782 ),783 ),784 )785 env_python_full_version = (786 environment["python_full_version"]787 if environment788 else default_environment()["python_full_version"]789 )790 791 # #. Check if the metadata version specified by :ref:`pylock-lock-version` is792 # supported; an error or warning MUST be raised as appropriate.793 # Covered by lock.validate() which is a precondition for this method.794 795 # #. If :ref:`pylock-requires-python` is specified, check that the environment796 # being installed for meets the requirement; an error MUST be raised if it is797 # not met.798 if self.requires_python and not self.requires_python.contains(799 env_python_full_version,800 ):801 raise PylockSelectError(802 f"python_full_version {env_python_full_version!r} "803 f"in provided environment does not satisfy the Python version "804 f"requirement {str(self.requires_python)!r}"805 )806 807 # #. If :ref:`pylock-environments` is specified, check that at least one of the808 # environment marker expressions is satisfied; an error MUST be raised if no809 # expression is satisfied.810 if self.environments:811 for env_marker in self.environments:812 if env_marker.evaluate(813 cast("dict[str, str]", environment or {}), context="requirement"814 ):815 break816 else:817 raise PylockSelectError(818 "Provided environment does not satisfy any of the "819 "environments specified in the lock file"820 )821 822 # #. For each package listed in :ref:`pylock-packages`:823 selected_packages_by_name: dict[str, tuple[int, Package]] = {}824 for package_index, package in enumerate(self.packages):825 # #. If :ref:`pylock-packages-marker` is specified, check if it is826 # satisfied;if it isn't, skip to the next package.827 if package.marker and not package.marker.evaluate(env, context="lock_file"):828 continue829 830 # #. If :ref:`pylock-packages-requires-python` is specified, check if it is831 # satisfied; an error MUST be raised if it isn't.832 if package.requires_python and not package.requires_python.contains(833 env_python_full_version,834 ):835 raise PylockSelectError(836 f"python_full_version {env_python_full_version!r} "837 f"in provided environment does not satisfy the Python version "838 f"requirement {str(package.requires_python)!r} for package "839 f"{package.name!r} at packages[{package_index}]"840 )841 842 # #. Check that no other conflicting instance of the package has been slated843 # to be installed; an error about the ambiguity MUST be raised otherwise.844 if package.name in selected_packages_by_name:845 raise PylockSelectError(846 f"Multiple packages with the name {package.name!r} are "847 f"selected at packages[{package_index}] and "848 f"packages[{selected_packages_by_name[package.name][0]}]"849 )850 851 # #. Check that the source of the package is specified appropriately (i.e.852 # there are no conflicting sources in the package entry);853 # an error MUST be raised if any issues are found.854 # Covered by lock.validate() which is a precondition for this method.855 856 # #. Add the package to the set of packages to install.857 selected_packages_by_name[package.name] = (package_index, package)858 859 # #. For each package to be installed:860 for package_index, package in selected_packages_by_name.values():861 # - If :ref:`pylock-packages-vcs` is set:862 if package.vcs is not None:863 yield package, package.vcs864 865 # - Else if :ref:`pylock-packages-directory` is set:866 elif package.directory is not None:867 yield package, package.directory868 869 # - Else if :ref:`pylock-packages-archive` is set:870 elif package.archive is not None:871 yield package, package.archive872 873 # - Else if there are entries for :ref:`pylock-packages-wheels`:874 elif package.wheels:875 # #. Look for the appropriate wheel file based on876 # :ref:`pylock-packages-wheels-name`; if one is not found then move877 # on to :ref:`pylock-packages-sdist` or an error MUST be raised about878 # a lack of source for the project.879 best_wheel = next(880 compatible_tags_selector(881 (wheel, parse_wheel_filename(wheel.filename)[-1])882 for wheel in package.wheels883 ),884 None,885 )886 if best_wheel:887 yield package, best_wheel888 elif package.sdist is not None:889 yield package, package.sdist890 else:891 raise PylockSelectError(892 f"No wheel found matching the provided tags "893 f"for package {package.name!r} "894 f"at packages[{package_index}], "895 f"and no sdist available as a fallback"896 )897 898 # - Else if no :ref:`pylock-packages-wheels` file is found or899 # :ref:`pylock-packages-sdist` is solely set:900 elif package.sdist is not None:901 yield package, package.sdist902 903 else:904 # Covered by lock.validate() which is a precondition for this method.905 raise NotImplementedError # pragma: no cover906 