Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_vendor/packaging/pylock.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

384 statements  

1from __future__ import annotations 

2 

3import dataclasses 

4import logging 

5import re 

6from collections.abc import Mapping, Sequence 

7from dataclasses import dataclass 

8from datetime import datetime 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 Callable, 

13 Protocol, 

14 TypeVar, 

15 cast, 

16) 

17from urllib.parse import urlparse 

18 

19from .markers import Environment, Marker, default_environment 

20from .specifiers import SpecifierSet 

21from .tags import create_compatible_tags_selector, sys_tags 

22from .utils import ( 

23 NormalizedName, 

24 is_normalized_name, 

25 parse_sdist_filename, 

26 parse_wheel_filename, 

27) 

28from .version import Version 

29 

30if TYPE_CHECKING: # pragma: no cover 

31 from collections.abc import Collection, Iterator 

32 from pathlib import Path 

33 

34 from typing_extensions import Self 

35 

36 from .tags import Tag 

37 

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 cover 

63 @classmethod 

64 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: ANN401 

83 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 value 

88 

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 data 

94 if value is not None 

95 } 

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 None 

102 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 value 

109 

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 value 

116 

117 

118def _get_sequence( 

119 d: Mapping[str, Any], expected_item_type: type[_T], key: str 

120) -> 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 None 

124 if isinstance(value, (str, bytes)): 

125 # special case: str and bytes are Sequences, but we want to reject it 

126 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 value 

138 

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 None 

153 try: 

154 return target_type(value) 

155 except Exception as e: 

156 raise PylockValidationError(e, context=key) from e 

157 

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 value 

170 

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 None 

181 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 e 

188 return result 

189 

190 

191def _get_object( 

192 d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str 

193) -> _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 None 

197 try: 

198 return target_type._from_dict(value) 

199 except Exception as e: 

200 raise PylockValidationError(e, context=key) from e 

201 

202 

203def _get_sequence_of_objects( 

204 d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str 

205) -> 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 None 

209 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 e 

216 return result 

217 

218 

219def _get_required_sequence_of_objects( 

220 d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str 

221) -> Sequence[_FromMappingProtocolT]: 

222 """Get a required list value from the dictionary and convert its items to a 

223 dataclass.""" 

224 if (result := _get_sequence_of_objects(d, target_item_type, key)) is None: 

225 raise _PylockRequiredKeyError(key) 

226 return result 

227 

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 None 

244 # If the path is relative it MAY use POSIX-style path separators explicitly 

245 # for portability 

246 if "/" in path: 

247 return path.rsplit("/", 1)[-1] 

248 elif "\\" in path: 

249 return path.rsplit("\\", 1)[-1] 

250 else: 

251 return path 

252 

253 

254def _url_name(url: str | None) -> str | None: 

255 if not url: 

256 return None 

257 url_path = urlparse(url).path 

258 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 hashes 

267 

268 

269class PylockValidationError(Exception): 

270 """Raised when when input data is not spec-compliant.""" 

271 

272 context: str | None = None 

273 message: str 

274 

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.context 

285 ) 

286 else: 

287 self.context = context 

288 self.message = cause.message 

289 else: 

290 self.context = context 

291 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.message 

297 

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: str 

315 url: str | None = None 

316 path: str | None = None 

317 requested_revision: str | None = None 

318 commit_id: str # type: ignore[misc] 

319 subdirectory: str | None = None 

320 

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 @classmethod 

340 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_vcs 

351 

352 

353@dataclass(frozen=True, init=False) 

354class PackageDirectory: 

355 path: str 

356 editable: bool | None = None 

357 subdirectory: str | None = None 

358 

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 @classmethod 

372 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 = None 

383 path: str | None = None 

384 size: int | None = None 

385 upload_time: datetime | None = None 

386 hashes: Mapping[str, str] # type: ignore[misc] 

387 subdirectory: str | None = None 

388 

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 @classmethod 

408 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_archive 

419 

420 

421@dataclass(frozen=True, init=False) 

422class PackageSdist: 

423 name: str | None = None 

424 upload_time: datetime | None = None 

425 url: str | None = None 

426 path: str | None = None 

427 size: int | None = None 

428 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 @classmethod 

449 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_sdist 

460 

461 @property 

462 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 filename 

468 

469 

470@dataclass(frozen=True, init=False) 

471class PackageWheel: 

472 name: str | None = None 

473 upload_time: datetime | None = None 

474 url: str | None = None 

475 path: str | None = None 

476 size: int | None = None 

477 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 @classmethod 

498 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_wheel 

509 

510 @property 

511 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 filename 

517 

518 

519@dataclass(frozen=True, init=False) 

520class Package: 

521 name: NormalizedName 

522 version: Version | None = None 

523 marker: Marker | None = None 

524 requires_python: SpecifierSet | None = None 

525 dependencies: Sequence[Mapping[str, Any]] | None = None 

526 vcs: PackageVcs | None = None 

527 directory: PackageDirectory | None = None 

528 archive: PackageArchive | None = None 

529 index: str | None = None 

530 sdist: PackageSdist | None = None 

531 wheels: Sequence[PackageWheel] | None = None 

532 attestation_identities: Sequence[Mapping[str, Any]] | None = None 

533 tool: Mapping[str, Any] | None = None 

534 

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 @classmethod 

568 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 e 

605 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 e 

625 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: B007 

639 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 e 

646 return package 

647 

648 @property 

649 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: Version 

658 environments: Sequence[Marker] | None = None 

659 requires_python: SpecifierSet | None = None 

660 extras: Sequence[NormalizedName] | None = None 

661 dependency_groups: Sequence[str] | None = None 

662 default_groups: Sequence[str] | None = None 

663 created_by: str # type: ignore[misc] 

664 packages: Sequence[Package] # type: ignore[misc] 

665 tool: Mapping[str, Any] | None = None 

666 

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 @classmethod 

692 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_version 

711 ) 

712 return pylock 

713 

714 @classmethod 

715 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 not 

719 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 PackageVcs 

744 | PackageDirectory 

745 | PackageArchive 

746 | PackageWheel 

747 | PackageSdist, 

748 ] 

749 ]: 

750 """Select what to install from the lock file. 

751 

752 The *environment* and *tags* parameters represent the environment being 

753 selected for. If unspecified, ``packaging.markers.default_environment()`` and 

754 ``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. If 

759 unspecified, the default groups are used. 

760 

761 This method must be used on valid Pylock instances (i.e. one obtained 

762 from :meth:`Pylock.from_dict` or if constructed manually, after calling 

763 :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`` and 

768 # ``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 from 

772 # :ref:`pylock-default-groups` by default. 

773 env = cast( 

774 "dict[str, str | frozenset[str]]", 

775 dict( 

776 environment or {}, # Marker.evaluate will fill-up 

777 extras=frozenset(extras or []), 

778 dependency_groups=frozenset( 

779 (self.default_groups or []) 

780 if dependency_groups is None # to allow selecting no group 

781 else dependency_groups 

782 ), 

783 ), 

784 ) 

785 env_python_full_version = ( 

786 environment["python_full_version"] 

787 if environment 

788 else default_environment()["python_full_version"] 

789 ) 

790 

791 # #. Check if the metadata version specified by :ref:`pylock-lock-version` is 

792 # 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 environment 

796 # being installed for meets the requirement; an error MUST be raised if it is 

797 # 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 the 

808 # environment marker expressions is satisfied; an error MUST be raised if no 

809 # 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 break 

816 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 is 

826 # satisfied;if it isn't, skip to the next package. 

827 if package.marker and not package.marker.evaluate(env, context="lock_file"): 

828 continue 

829 

830 # #. If :ref:`pylock-packages-requires-python` is specified, check if it is 

831 # 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 slated 

843 # 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.vcs 

864 

865 # - Else if :ref:`pylock-packages-directory` is set: 

866 elif package.directory is not None: 

867 yield package, package.directory 

868 

869 # - Else if :ref:`pylock-packages-archive` is set: 

870 elif package.archive is not None: 

871 yield package, package.archive 

872 

873 # - Else if there are entries for :ref:`pylock-packages-wheels`: 

874 elif package.wheels: 

875 # #. Look for the appropriate wheel file based on 

876 # :ref:`pylock-packages-wheels-name`; if one is not found then move 

877 # on to :ref:`pylock-packages-sdist` or an error MUST be raised about 

878 # 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.wheels 

883 ), 

884 None, 

885 ) 

886 if best_wheel: 

887 yield package, best_wheel 

888 elif package.sdist is not None: 

889 yield package, package.sdist 

890 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 or 

899 # :ref:`pylock-packages-sdist` is solely set: 

900 elif package.sdist is not None: 

901 yield package, package.sdist 

902 

903 else: 

904 # Covered by lock.validate() which is a precondition for this method. 

905 raise NotImplementedError # pragma: no cover