nab-python 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. nab_python/__init__.py +1 -0
  2. nab_python/_build/__init__.py +1 -0
  3. nab_python/_build/env.py +364 -0
  4. nab_python/_build/errors.py +17 -0
  5. nab_python/_build/runner.py +254 -0
  6. nab_python/_lockfile/__init__.py +1 -0
  7. nab_python/_lockfile/builder.py +339 -0
  8. nab_python/_lockfile/disjointness.py +207 -0
  9. nab_python/_lockfile/pylock.py +323 -0
  10. nab_python/_lockfile/requirements.py +121 -0
  11. nab_python/_packaging_provider.py +98 -0
  12. nab_python/_provider/__init__.py +1 -0
  13. nab_python/_provider/build_remote.py +95 -0
  14. nab_python/_provider/extras.py +231 -0
  15. nab_python/_provider/listing.py +442 -0
  16. nab_python/_provider/lookahead.py +156 -0
  17. nab_python/_provider/metadata_resolver.py +450 -0
  18. nab_python/_provider/priority.py +174 -0
  19. nab_python/_provider/sources.py +215 -0
  20. nab_python/_testing/__init__.py +1 -0
  21. nab_python/_testing/coordinator_fake.py +240 -0
  22. nab_python/_vcs_admission.py +209 -0
  23. nab_python/_vendor/__init__.py +6 -0
  24. nab_python/_vendor/packaging/LICENSE +3 -0
  25. nab_python/_vendor/packaging/LICENSE.APACHE +177 -0
  26. nab_python/_vendor/packaging/LICENSE.BSD +23 -0
  27. nab_python/_vendor/packaging/PROVENANCE.md +73 -0
  28. nab_python/_vendor/packaging/__init__.py +15 -0
  29. nab_python/_vendor/packaging/_elffile.py +108 -0
  30. nab_python/_vendor/packaging/_manylinux.py +265 -0
  31. nab_python/_vendor/packaging/_musllinux.py +88 -0
  32. nab_python/_vendor/packaging/_parser.py +394 -0
  33. nab_python/_vendor/packaging/_structures.py +33 -0
  34. nab_python/_vendor/packaging/_tokenizer.py +196 -0
  35. nab_python/_vendor/packaging/dependency_groups.py +302 -0
  36. nab_python/_vendor/packaging/direct_url.py +325 -0
  37. nab_python/_vendor/packaging/errors.py +94 -0
  38. nab_python/_vendor/packaging/licenses/__init__.py +186 -0
  39. nab_python/_vendor/packaging/licenses/_spdx.py +799 -0
  40. nab_python/_vendor/packaging/markers.py +506 -0
  41. nab_python/_vendor/packaging/metadata.py +964 -0
  42. nab_python/_vendor/packaging/py.typed +0 -0
  43. nab_python/_vendor/packaging/pylock.py +910 -0
  44. nab_python/_vendor/packaging/ranges.py +1803 -0
  45. nab_python/_vendor/packaging/requirements.py +132 -0
  46. nab_python/_vendor/packaging/specifiers.py +1141 -0
  47. nab_python/_vendor/packaging/tags.py +929 -0
  48. nab_python/_vendor/packaging/utils.py +296 -0
  49. nab_python/_vendor/packaging/version.py +1230 -0
  50. nab_python/build_backend.py +184 -0
  51. nab_python/config.py +805 -0
  52. nab_python/download.py +170 -0
  53. nab_python/fetch.py +827 -0
  54. nab_python/lockfile.py +238 -0
  55. nab_python/metadata.py +145 -0
  56. nab_python/provider.py +1235 -0
  57. nab_python/py.typed +0 -0
  58. nab_python/requirements_file.py +180 -0
  59. nab_python/resolve.py +497 -0
  60. nab_python/universal/__init__.py +1 -0
  61. nab_python/universal/matrix.py +235 -0
  62. nab_python/universal/provider.py +214 -0
  63. nab_python/universal/reresolve.py +310 -0
  64. nab_python/universal/resolve.py +508 -0
  65. nab_python/universal/validate.py +439 -0
  66. nab_python/universal/wheel_selection.py +327 -0
  67. nab_python/workspace.py +214 -0
  68. nab_python-0.0.1.dist-info/METADATA +49 -0
  69. nab_python-0.0.1.dist-info/RECORD +71 -0
  70. nab_python-0.0.1.dist-info/WHEEL +4 -0
  71. nab_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,910 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import logging
5
+ import re
6
+ from collections.abc import Mapping, Sequence
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Callable,
13
+ Protocol,
14
+ TypeVar,
15
+ cast,
16
+ )
17
+ from urllib.parse import urlparse
18
+
19
+ from .markers import (
20
+ Environment,
21
+ Marker,
22
+ _pep440_python_full_version,
23
+ default_environment,
24
+ )
25
+ from .specifiers import SpecifierSet
26
+ from .tags import create_compatible_tags_selector, sys_tags
27
+ from .utils import (
28
+ NormalizedName,
29
+ is_normalized_name,
30
+ parse_sdist_filename,
31
+ parse_wheel_filename,
32
+ )
33
+ from .version import Version
34
+
35
+ if TYPE_CHECKING: # pragma: no cover
36
+ from collections.abc import Collection, Iterator
37
+ from pathlib import Path
38
+
39
+ from typing_extensions import Self
40
+
41
+ from .tags import Tag
42
+
43
+ _logger = logging.getLogger(__name__)
44
+
45
+ __all__ = [
46
+ "Package",
47
+ "PackageArchive",
48
+ "PackageDirectory",
49
+ "PackageSdist",
50
+ "PackageVcs",
51
+ "PackageWheel",
52
+ "Pylock",
53
+ "PylockUnsupportedVersionError",
54
+ "PylockValidationError",
55
+ "is_valid_pylock_path",
56
+ ]
57
+
58
+
59
+ def __dir__() -> list[str]:
60
+ return __all__
61
+
62
+
63
+ _T = TypeVar("_T")
64
+ _T2 = TypeVar("_T2")
65
+
66
+
67
+ class _FromMappingProtocol(Protocol): # pragma: no cover
68
+ @classmethod
69
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self: ...
70
+
71
+
72
+ _FromMappingProtocolT = TypeVar("_FromMappingProtocolT", bound=_FromMappingProtocol)
73
+
74
+
75
+ _PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
76
+
77
+
78
+ def is_valid_pylock_path(path: Path) -> bool:
79
+ """Check if the given path is a valid pylock file path."""
80
+ return path.name == "pylock.toml" or bool(_PYLOCK_FILE_NAME_RE.match(path.name))
81
+
82
+
83
+ def _toml_key(key: str) -> str:
84
+ return key.replace("_", "-")
85
+
86
+
87
+ def _toml_value(key: str, value: Any) -> Any: # noqa: ANN401
88
+ if isinstance(value, (Version, Marker, SpecifierSet)):
89
+ return str(value)
90
+ if isinstance(value, Sequence) and key == "environments":
91
+ return [str(v) for v in value]
92
+ return value
93
+
94
+
95
+ def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
96
+ return {
97
+ _toml_key(key): _toml_value(key, value)
98
+ for key, value in data
99
+ if value is not None
100
+ }
101
+
102
+
103
+ def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
104
+ """Get a value from the dictionary and verify it's the expected type."""
105
+ if (value := d.get(key)) is None:
106
+ return None
107
+ if not isinstance(value, expected_type):
108
+ raise PylockValidationError(
109
+ f"Unexpected type {type(value).__name__} "
110
+ f"(expected {expected_type.__name__})",
111
+ context=key,
112
+ )
113
+ return value
114
+
115
+
116
+ def _get_required(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T:
117
+ """Get a required value from the dictionary and verify it's the expected type."""
118
+ if (value := _get(d, expected_type, key)) is None:
119
+ raise _PylockRequiredKeyError(key)
120
+ return value
121
+
122
+
123
+ def _get_sequence(
124
+ d: Mapping[str, Any], expected_item_type: type[_T], key: str
125
+ ) -> Sequence[_T] | None:
126
+ """Get a list value from the dictionary and verify it's the expected items type."""
127
+ if (value := _get(d, Sequence, key)) is None: # type: ignore[type-abstract]
128
+ return None
129
+ if isinstance(value, (str, bytes)):
130
+ # special case: str and bytes are Sequences, but we want to reject it
131
+ raise PylockValidationError(
132
+ f"Unexpected type {type(value).__name__} (expected Sequence)",
133
+ context=key,
134
+ )
135
+ for i, item in enumerate(value):
136
+ if not isinstance(item, expected_item_type):
137
+ raise PylockValidationError(
138
+ f"Unexpected type {type(item).__name__} "
139
+ f"(expected {expected_item_type.__name__})",
140
+ context=f"{key}[{i}]",
141
+ )
142
+ return value
143
+
144
+
145
+ def _get_as(
146
+ d: Mapping[str, Any],
147
+ expected_type: type[_T],
148
+ target_type: Callable[[_T], _T2],
149
+ key: str,
150
+ ) -> _T2 | None:
151
+ """Get a value from the dictionary, verify it's the expected type,
152
+ and convert to the target type.
153
+
154
+ This assumes the target_type constructor accepts the value.
155
+ """
156
+ if (value := _get(d, expected_type, key)) is None:
157
+ return None
158
+ try:
159
+ return target_type(value)
160
+ except Exception as e:
161
+ raise PylockValidationError(e, context=key) from e
162
+
163
+
164
+ def _get_required_as(
165
+ d: Mapping[str, Any],
166
+ expected_type: type[_T],
167
+ target_type: Callable[[_T], _T2],
168
+ key: str,
169
+ ) -> _T2:
170
+ """Get a required value from the dict, verify it's the expected type,
171
+ and convert to the target type."""
172
+ if (value := _get_as(d, expected_type, target_type, key)) is None:
173
+ raise _PylockRequiredKeyError(key)
174
+ return value
175
+
176
+
177
+ def _get_sequence_as(
178
+ d: Mapping[str, Any],
179
+ expected_item_type: type[_T],
180
+ target_item_type: Callable[[_T], _T2],
181
+ key: str,
182
+ ) -> list[_T2] | None:
183
+ """Get list value from dictionary and verify expected items type."""
184
+ if (value := _get_sequence(d, expected_item_type, key)) is None:
185
+ return None
186
+ result = []
187
+ try:
188
+ for item in value:
189
+ typed_item = target_item_type(item)
190
+ result.append(typed_item)
191
+ except Exception as e:
192
+ raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
193
+ return result
194
+
195
+
196
+ def _get_object(
197
+ d: Mapping[str, Any], target_type: type[_FromMappingProtocolT], key: str
198
+ ) -> _FromMappingProtocolT | None:
199
+ """Get a dictionary value from the dictionary and convert it to a dataclass."""
200
+ if (value := _get(d, Mapping, key)) is None: # type: ignore[type-abstract]
201
+ return None
202
+ try:
203
+ return target_type._from_dict(value)
204
+ except Exception as e:
205
+ raise PylockValidationError(e, context=key) from e
206
+
207
+
208
+ def _get_sequence_of_objects(
209
+ d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
210
+ ) -> list[_FromMappingProtocolT] | None:
211
+ """Get a list value from the dictionary and convert its items to a dataclass."""
212
+ if (value := _get_sequence(d, Mapping, key)) is None: # type: ignore[type-abstract]
213
+ return None
214
+ result: list[_FromMappingProtocolT] = []
215
+ try:
216
+ for item in value:
217
+ typed_item = target_item_type._from_dict(item)
218
+ result.append(typed_item)
219
+ except Exception as e:
220
+ raise PylockValidationError(e, context=f"{key}[{len(result)}]") from e
221
+ return result
222
+
223
+
224
+ def _get_required_sequence_of_objects(
225
+ d: Mapping[str, Any], target_item_type: type[_FromMappingProtocolT], key: str
226
+ ) -> Sequence[_FromMappingProtocolT]:
227
+ """Get a required list value from the dictionary and convert its items to a
228
+ dataclass."""
229
+ if (result := _get_sequence_of_objects(d, target_item_type, key)) is None:
230
+ raise _PylockRequiredKeyError(key)
231
+ return result
232
+
233
+
234
+ def _validate_normalized_name(name: str) -> NormalizedName:
235
+ """Validate that a string is a NormalizedName."""
236
+ if not is_normalized_name(name):
237
+ raise PylockValidationError(f"Name {name!r} is not normalized")
238
+ return NormalizedName(name)
239
+
240
+
241
+ def _validate_path_url(path: str | None, url: str | None) -> None:
242
+ if not path and not url:
243
+ raise PylockValidationError("path or url must be provided")
244
+
245
+
246
+ def _path_name(path: str | None) -> str | None:
247
+ if not path:
248
+ return None
249
+ # If the path is relative it MAY use POSIX-style path separators explicitly
250
+ # for portability
251
+ if "/" in path:
252
+ return path.rsplit("/", 1)[-1]
253
+ elif "\\" in path:
254
+ return path.rsplit("\\", 1)[-1]
255
+ else:
256
+ return path
257
+
258
+
259
+ def _url_name(url: str | None) -> str | None:
260
+ if not url:
261
+ return None
262
+ url_path = urlparse(url).path
263
+ return url_path.rsplit("/", 1)[-1]
264
+
265
+
266
+ def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
267
+ if not hashes:
268
+ raise PylockValidationError("At least one hash must be provided")
269
+ if not all(isinstance(hash_val, str) for hash_val in hashes.values()):
270
+ raise PylockValidationError("Hash values must be strings")
271
+ return hashes
272
+
273
+
274
+ class PylockValidationError(Exception):
275
+ """Raised when when input data is not spec-compliant."""
276
+
277
+ context: str | None = None
278
+ message: str
279
+
280
+ def __init__(
281
+ self,
282
+ cause: str | Exception,
283
+ *,
284
+ context: str | None = None,
285
+ ) -> None:
286
+ if isinstance(cause, PylockValidationError):
287
+ if cause.context:
288
+ self.context = (
289
+ f"{context}.{cause.context}" if context else cause.context
290
+ )
291
+ else:
292
+ self.context = context
293
+ self.message = cause.message
294
+ else:
295
+ self.context = context
296
+ self.message = str(cause)
297
+
298
+ def __str__(self) -> str:
299
+ if self.context:
300
+ return f"{self.message} in {self.context!r}"
301
+ return self.message
302
+
303
+
304
+ class _PylockRequiredKeyError(PylockValidationError):
305
+ def __init__(self, key: str) -> None:
306
+ super().__init__("Missing required value", context=key)
307
+
308
+
309
+ class PylockUnsupportedVersionError(PylockValidationError):
310
+ """Raised when encountering an unsupported `lock_version`."""
311
+
312
+
313
+ class PylockSelectError(Exception):
314
+ """Base exception for errors raised by :meth:`Pylock.select`."""
315
+
316
+
317
+ @dataclass(frozen=True, init=False)
318
+ class PackageVcs:
319
+ type: str
320
+ url: str | None = None
321
+ path: str | None = None
322
+ requested_revision: str | None = None
323
+ commit_id: str # type: ignore[misc]
324
+ subdirectory: str | None = None
325
+
326
+ def __init__(
327
+ self,
328
+ *,
329
+ type: str,
330
+ url: str | None = None,
331
+ path: str | None = None,
332
+ requested_revision: str | None = None,
333
+ commit_id: str,
334
+ subdirectory: str | None = None,
335
+ ) -> None:
336
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
337
+ object.__setattr__(self, "type", type)
338
+ object.__setattr__(self, "url", url)
339
+ object.__setattr__(self, "path", path)
340
+ object.__setattr__(self, "requested_revision", requested_revision)
341
+ object.__setattr__(self, "commit_id", commit_id)
342
+ object.__setattr__(self, "subdirectory", subdirectory)
343
+
344
+ @classmethod
345
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
346
+ package_vcs = cls(
347
+ type=_get_required(d, str, "type"),
348
+ url=_get(d, str, "url"),
349
+ path=_get(d, str, "path"),
350
+ requested_revision=_get(d, str, "requested-revision"),
351
+ commit_id=_get_required(d, str, "commit-id"),
352
+ subdirectory=_get(d, str, "subdirectory"),
353
+ )
354
+ _validate_path_url(package_vcs.path, package_vcs.url)
355
+ return package_vcs
356
+
357
+
358
+ @dataclass(frozen=True, init=False)
359
+ class PackageDirectory:
360
+ path: str
361
+ editable: bool | None = None
362
+ subdirectory: str | None = None
363
+
364
+ def __init__(
365
+ self,
366
+ *,
367
+ path: str,
368
+ editable: bool | None = None,
369
+ subdirectory: str | None = None,
370
+ ) -> None:
371
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
372
+ object.__setattr__(self, "path", path)
373
+ object.__setattr__(self, "editable", editable)
374
+ object.__setattr__(self, "subdirectory", subdirectory)
375
+
376
+ @classmethod
377
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
378
+ return cls(
379
+ path=_get_required(d, str, "path"),
380
+ editable=_get(d, bool, "editable"),
381
+ subdirectory=_get(d, str, "subdirectory"),
382
+ )
383
+
384
+
385
+ @dataclass(frozen=True, init=False)
386
+ class PackageArchive:
387
+ url: str | None = None
388
+ path: str | None = None
389
+ size: int | None = None
390
+ upload_time: datetime | None = None
391
+ hashes: Mapping[str, str] # type: ignore[misc]
392
+ subdirectory: str | None = None
393
+
394
+ def __init__(
395
+ self,
396
+ *,
397
+ url: str | None = None,
398
+ path: str | None = None,
399
+ size: int | None = None,
400
+ upload_time: datetime | None = None,
401
+ hashes: Mapping[str, str],
402
+ subdirectory: str | None = None,
403
+ ) -> None:
404
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
405
+ object.__setattr__(self, "url", url)
406
+ object.__setattr__(self, "path", path)
407
+ object.__setattr__(self, "size", size)
408
+ object.__setattr__(self, "upload_time", upload_time)
409
+ object.__setattr__(self, "hashes", hashes)
410
+ object.__setattr__(self, "subdirectory", subdirectory)
411
+
412
+ @classmethod
413
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
414
+ package_archive = cls(
415
+ url=_get(d, str, "url"),
416
+ path=_get(d, str, "path"),
417
+ size=_get(d, int, "size"),
418
+ upload_time=_get(d, datetime, "upload-time"),
419
+ hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
420
+ subdirectory=_get(d, str, "subdirectory"),
421
+ )
422
+ _validate_path_url(package_archive.path, package_archive.url)
423
+ return package_archive
424
+
425
+
426
+ @dataclass(frozen=True, init=False)
427
+ class PackageSdist:
428
+ name: str | None = None
429
+ upload_time: datetime | None = None
430
+ url: str | None = None
431
+ path: str | None = None
432
+ size: int | None = None
433
+ hashes: Mapping[str, str] # type: ignore[misc]
434
+
435
+ def __init__(
436
+ self,
437
+ *,
438
+ name: str | None = None,
439
+ upload_time: datetime | None = None,
440
+ url: str | None = None,
441
+ path: str | None = None,
442
+ size: int | None = None,
443
+ hashes: Mapping[str, str],
444
+ ) -> None:
445
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
446
+ object.__setattr__(self, "name", name)
447
+ object.__setattr__(self, "upload_time", upload_time)
448
+ object.__setattr__(self, "url", url)
449
+ object.__setattr__(self, "path", path)
450
+ object.__setattr__(self, "size", size)
451
+ object.__setattr__(self, "hashes", hashes)
452
+
453
+ @classmethod
454
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
455
+ package_sdist = cls(
456
+ name=_get(d, str, "name"),
457
+ upload_time=_get(d, datetime, "upload-time"),
458
+ url=_get(d, str, "url"),
459
+ path=_get(d, str, "path"),
460
+ size=_get(d, int, "size"),
461
+ hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
462
+ )
463
+ _validate_path_url(package_sdist.path, package_sdist.url)
464
+ return package_sdist
465
+
466
+ @property
467
+ def filename(self) -> str:
468
+ """Get the filename of the sdist."""
469
+ filename = self.name or _path_name(self.path) or _url_name(self.url)
470
+ if not filename:
471
+ raise PylockValidationError("Cannot determine sdist filename")
472
+ return filename
473
+
474
+
475
+ @dataclass(frozen=True, init=False)
476
+ class PackageWheel:
477
+ name: str | None = None
478
+ upload_time: datetime | None = None
479
+ url: str | None = None
480
+ path: str | None = None
481
+ size: int | None = None
482
+ hashes: Mapping[str, str] # type: ignore[misc]
483
+
484
+ def __init__(
485
+ self,
486
+ *,
487
+ name: str | None = None,
488
+ upload_time: datetime | None = None,
489
+ url: str | None = None,
490
+ path: str | None = None,
491
+ size: int | None = None,
492
+ hashes: Mapping[str, str],
493
+ ) -> None:
494
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
495
+ object.__setattr__(self, "name", name)
496
+ object.__setattr__(self, "upload_time", upload_time)
497
+ object.__setattr__(self, "url", url)
498
+ object.__setattr__(self, "path", path)
499
+ object.__setattr__(self, "size", size)
500
+ object.__setattr__(self, "hashes", hashes)
501
+
502
+ @classmethod
503
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
504
+ package_wheel = cls(
505
+ name=_get(d, str, "name"),
506
+ upload_time=_get(d, datetime, "upload-time"),
507
+ url=_get(d, str, "url"),
508
+ path=_get(d, str, "path"),
509
+ size=_get(d, int, "size"),
510
+ hashes=_get_required_as(d, Mapping, _validate_hashes, "hashes"), # type: ignore[type-abstract]
511
+ )
512
+ _validate_path_url(package_wheel.path, package_wheel.url)
513
+ return package_wheel
514
+
515
+ @property
516
+ def filename(self) -> str:
517
+ """Get the filename of the wheel."""
518
+ filename = self.name or _path_name(self.path) or _url_name(self.url)
519
+ if not filename:
520
+ raise PylockValidationError("Cannot determine wheel filename")
521
+ return filename
522
+
523
+
524
+ @dataclass(frozen=True, init=False)
525
+ class Package:
526
+ name: NormalizedName
527
+ version: Version | None = None
528
+ marker: Marker | None = None
529
+ requires_python: SpecifierSet | None = None
530
+ dependencies: Sequence[Mapping[str, Any]] | None = None
531
+ vcs: PackageVcs | None = None
532
+ directory: PackageDirectory | None = None
533
+ archive: PackageArchive | None = None
534
+ index: str | None = None
535
+ sdist: PackageSdist | None = None
536
+ wheels: Sequence[PackageWheel] | None = None
537
+ attestation_identities: Sequence[Mapping[str, Any]] | None = None
538
+ tool: Mapping[str, Any] | None = None
539
+
540
+ def __init__(
541
+ self,
542
+ *,
543
+ name: NormalizedName,
544
+ version: Version | None = None,
545
+ marker: Marker | None = None,
546
+ requires_python: SpecifierSet | None = None,
547
+ dependencies: Sequence[Mapping[str, Any]] | None = None,
548
+ vcs: PackageVcs | None = None,
549
+ directory: PackageDirectory | None = None,
550
+ archive: PackageArchive | None = None,
551
+ index: str | None = None,
552
+ sdist: PackageSdist | None = None,
553
+ wheels: Sequence[PackageWheel] | None = None,
554
+ attestation_identities: Sequence[Mapping[str, Any]] | None = None,
555
+ tool: Mapping[str, Any] | None = None,
556
+ ) -> None:
557
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
558
+ object.__setattr__(self, "name", name)
559
+ object.__setattr__(self, "version", version)
560
+ object.__setattr__(self, "marker", marker)
561
+ object.__setattr__(self, "requires_python", requires_python)
562
+ object.__setattr__(self, "dependencies", dependencies)
563
+ object.__setattr__(self, "vcs", vcs)
564
+ object.__setattr__(self, "directory", directory)
565
+ object.__setattr__(self, "archive", archive)
566
+ object.__setattr__(self, "index", index)
567
+ object.__setattr__(self, "sdist", sdist)
568
+ object.__setattr__(self, "wheels", wheels)
569
+ object.__setattr__(self, "attestation_identities", attestation_identities)
570
+ object.__setattr__(self, "tool", tool)
571
+
572
+ @classmethod
573
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
574
+ package = cls(
575
+ name=_get_required_as(d, str, _validate_normalized_name, "name"),
576
+ version=_get_as(d, str, Version, "version"),
577
+ requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
578
+ dependencies=_get_sequence(d, Mapping, "dependencies"), # type: ignore[type-abstract]
579
+ marker=_get_as(d, str, Marker, "marker"),
580
+ vcs=_get_object(d, PackageVcs, "vcs"),
581
+ directory=_get_object(d, PackageDirectory, "directory"),
582
+ archive=_get_object(d, PackageArchive, "archive"),
583
+ index=_get(d, str, "index"),
584
+ sdist=_get_object(d, PackageSdist, "sdist"),
585
+ wheels=_get_sequence_of_objects(d, PackageWheel, "wheels"),
586
+ attestation_identities=_get_sequence(d, Mapping, "attestation-identities"), # type: ignore[type-abstract]
587
+ tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
588
+ )
589
+ distributions = bool(package.sdist) + len(package.wheels or [])
590
+ direct_urls = (
591
+ bool(package.vcs) + bool(package.directory) + bool(package.archive)
592
+ )
593
+ if distributions > 0 and direct_urls > 0:
594
+ raise PylockValidationError(
595
+ "None of vcs, directory, archive must be set if sdist or wheels are set"
596
+ )
597
+ if distributions == 0 and direct_urls != 1:
598
+ raise PylockValidationError(
599
+ "Exactly one of vcs, directory, archive must be set "
600
+ "if sdist and wheels are not set"
601
+ )
602
+ for i, wheel in enumerate(package.wheels or []):
603
+ try:
604
+ (name, version, _, _) = parse_wheel_filename(wheel.filename)
605
+ except Exception as e:
606
+ raise PylockValidationError(
607
+ f"Invalid wheel filename {wheel.filename!r}",
608
+ context=f"wheels[{i}]",
609
+ ) from e
610
+ if name != package.name:
611
+ raise PylockValidationError(
612
+ f"Name in {wheel.filename!r} is not consistent with "
613
+ f"package name {package.name!r}",
614
+ context=f"wheels[{i}]",
615
+ )
616
+ if package.version and version != package.version:
617
+ raise PylockValidationError(
618
+ f"Version in {wheel.filename!r} is not consistent with "
619
+ f"package version {str(package.version)!r}",
620
+ context=f"wheels[{i}]",
621
+ )
622
+ if package.sdist:
623
+ try:
624
+ name, version = parse_sdist_filename(package.sdist.filename)
625
+ except Exception as e:
626
+ raise PylockValidationError(
627
+ f"Invalid sdist filename {package.sdist.filename!r}",
628
+ context="sdist",
629
+ ) from e
630
+ if name != package.name:
631
+ raise PylockValidationError(
632
+ f"Name in {package.sdist.filename!r} is not consistent with "
633
+ f"package name {package.name!r}",
634
+ context="sdist",
635
+ )
636
+ if package.version and version != package.version:
637
+ raise PylockValidationError(
638
+ f"Version in {package.sdist.filename!r} is not consistent with "
639
+ f"package version {str(package.version)!r}",
640
+ context="sdist",
641
+ )
642
+ try:
643
+ for i, attestation_identity in enumerate( # noqa: B007
644
+ package.attestation_identities or []
645
+ ):
646
+ _get_required(attestation_identity, str, "kind")
647
+ except Exception as e:
648
+ raise PylockValidationError(
649
+ e, context=f"attestation-identities[{i}]"
650
+ ) from e
651
+ return package
652
+
653
+ @property
654
+ def is_direct(self) -> bool:
655
+ return not (self.sdist or self.wheels)
656
+
657
+
658
+ @dataclass(frozen=True, init=False)
659
+ class Pylock:
660
+ """A class representing a pylock file."""
661
+
662
+ lock_version: Version
663
+ environments: Sequence[Marker] | None = None
664
+ requires_python: SpecifierSet | None = None
665
+ extras: Sequence[NormalizedName] | None = None
666
+ dependency_groups: Sequence[str] | None = None
667
+ default_groups: Sequence[str] | None = None
668
+ created_by: str # type: ignore[misc]
669
+ packages: Sequence[Package] # type: ignore[misc]
670
+ tool: Mapping[str, Any] | None = None
671
+
672
+ def __init__(
673
+ self,
674
+ *,
675
+ lock_version: Version,
676
+ environments: Sequence[Marker] | None = None,
677
+ requires_python: SpecifierSet | None = None,
678
+ extras: Sequence[NormalizedName] | None = None,
679
+ dependency_groups: Sequence[str] | None = None,
680
+ default_groups: Sequence[str] | None = None,
681
+ created_by: str,
682
+ packages: Sequence[Package],
683
+ tool: Mapping[str, Any] | None = None,
684
+ ) -> None:
685
+ # In Python 3.10+ make dataclass kw_only=True and remove __init__
686
+ object.__setattr__(self, "lock_version", lock_version)
687
+ object.__setattr__(self, "environments", environments)
688
+ object.__setattr__(self, "requires_python", requires_python)
689
+ object.__setattr__(self, "extras", extras)
690
+ object.__setattr__(self, "dependency_groups", dependency_groups)
691
+ object.__setattr__(self, "default_groups", default_groups)
692
+ object.__setattr__(self, "created_by", created_by)
693
+ object.__setattr__(self, "packages", packages)
694
+ object.__setattr__(self, "tool", tool)
695
+
696
+ @classmethod
697
+ def _from_dict(cls, d: Mapping[str, Any]) -> Self:
698
+ pylock = cls(
699
+ lock_version=_get_required_as(d, str, Version, "lock-version"),
700
+ environments=_get_sequence_as(d, str, Marker, "environments"),
701
+ extras=_get_sequence_as(d, str, _validate_normalized_name, "extras"),
702
+ dependency_groups=_get_sequence(d, str, "dependency-groups"),
703
+ default_groups=_get_sequence(d, str, "default-groups"),
704
+ created_by=_get_required(d, str, "created-by"),
705
+ requires_python=_get_as(d, str, SpecifierSet, "requires-python"),
706
+ packages=_get_required_sequence_of_objects(d, Package, "packages"),
707
+ tool=_get(d, Mapping, "tool"), # type: ignore[type-abstract]
708
+ )
709
+ if not Version("1") <= pylock.lock_version < Version("2"):
710
+ raise PylockUnsupportedVersionError(
711
+ f"pylock version {pylock.lock_version} is not supported"
712
+ )
713
+ if pylock.lock_version > Version("1.0"):
714
+ _logger.warning(
715
+ "pylock minor version %s is not supported", pylock.lock_version
716
+ )
717
+ return pylock
718
+
719
+ @classmethod
720
+ def from_dict(cls, d: Mapping[str, Any], /) -> Self:
721
+ """Create and validate a Pylock instance from a TOML dictionary.
722
+
723
+ Raises :class:`PylockValidationError` if the input data is not
724
+ spec-compliant.
725
+ """
726
+ return cls._from_dict(d)
727
+
728
+ def to_dict(self) -> Mapping[str, Any]:
729
+ """Convert the Pylock instance to a TOML dictionary."""
730
+ return dataclasses.asdict(self, dict_factory=_toml_dict_factory)
731
+
732
+ def validate(self) -> None:
733
+ """Validate the Pylock instance against the specification.
734
+
735
+ Raises :class:`PylockValidationError` otherwise."""
736
+ self.from_dict(self.to_dict())
737
+
738
+ def select(
739
+ self,
740
+ *,
741
+ environment: Environment | None = None,
742
+ tags: Sequence[Tag] | None = None,
743
+ extras: Collection[str] | None = None,
744
+ dependency_groups: Collection[str] | None = None,
745
+ ) -> Iterator[
746
+ tuple[
747
+ Package,
748
+ PackageVcs
749
+ | PackageDirectory
750
+ | PackageArchive
751
+ | PackageWheel
752
+ | PackageSdist,
753
+ ]
754
+ ]:
755
+ """Select what to install from the lock file.
756
+
757
+ The *environment* and *tags* parameters represent the environment being
758
+ selected for. If unspecified, ``packaging.markers.default_environment()`` and
759
+ ``packaging.tags.sys_tags()`` are used.
760
+
761
+ The *extras* parameter represents the extras to install.
762
+
763
+ The *dependency_groups* parameter represents the groups to install. If
764
+ unspecified, the default groups are used.
765
+
766
+ This method must be used on valid Pylock instances (i.e. one obtained
767
+ from :meth:`Pylock.from_dict` or if constructed manually, after calling
768
+ :meth:`Pylock.validate`).
769
+ """
770
+ compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())
771
+
772
+ # #. Gather the extras and dependency groups to install and set ``extras`` and
773
+ # ``dependency_groups`` for marker evaluation, respectively.
774
+ #
775
+ # #. ``extras`` SHOULD be set to the empty set by default.
776
+ # #. ``dependency_groups`` SHOULD be the set created from
777
+ # :ref:`pylock-default-groups` by default.
778
+ env = cast(
779
+ "dict[str, str | frozenset[str]]",
780
+ dict(
781
+ environment or {}, # Marker.evaluate will fill-up
782
+ extras=frozenset(extras or []),
783
+ dependency_groups=frozenset(
784
+ (self.default_groups or [])
785
+ if dependency_groups is None # to allow selecting no group
786
+ else dependency_groups
787
+ ),
788
+ ),
789
+ )
790
+ env_python_full_version = _pep440_python_full_version(
791
+ environment["python_full_version"]
792
+ if environment
793
+ else default_environment()["python_full_version"]
794
+ )
795
+
796
+ # #. Check if the metadata version specified by :ref:`pylock-lock-version` is
797
+ # supported; an error or warning MUST be raised as appropriate.
798
+ # Covered by lock.validate() which is a precondition for this method.
799
+
800
+ # #. If :ref:`pylock-requires-python` is specified, check that the environment
801
+ # being installed for meets the requirement; an error MUST be raised if it is
802
+ # not met.
803
+ if self.requires_python and not self.requires_python.contains(
804
+ env_python_full_version,
805
+ ):
806
+ raise PylockSelectError(
807
+ f"python_full_version {env_python_full_version!r} "
808
+ f"in provided environment does not satisfy the Python version "
809
+ f"requirement {str(self.requires_python)!r}"
810
+ )
811
+
812
+ # #. If :ref:`pylock-environments` is specified, check that at least one of the
813
+ # environment marker expressions is satisfied; an error MUST be raised if no
814
+ # expression is satisfied.
815
+ if self.environments:
816
+ for env_marker in self.environments:
817
+ if env_marker.evaluate(
818
+ cast("dict[str, str]", environment or {}), context="requirement"
819
+ ):
820
+ break
821
+ else:
822
+ raise PylockSelectError(
823
+ "Provided environment does not satisfy any of the "
824
+ "environments specified in the lock file"
825
+ )
826
+
827
+ # #. For each package listed in :ref:`pylock-packages`:
828
+ selected_packages_by_name: dict[str, tuple[int, Package]] = {}
829
+ for package_index, package in enumerate(self.packages):
830
+ # #. If :ref:`pylock-packages-marker` is specified, check if it is
831
+ # satisfied;if it isn't, skip to the next package.
832
+ if package.marker and not package.marker.evaluate(env, context="lock_file"):
833
+ continue
834
+
835
+ # #. If :ref:`pylock-packages-requires-python` is specified, check if it is
836
+ # satisfied; an error MUST be raised if it isn't.
837
+ if package.requires_python and not package.requires_python.contains(
838
+ env_python_full_version,
839
+ ):
840
+ raise PylockSelectError(
841
+ f"python_full_version {env_python_full_version!r} "
842
+ f"in provided environment does not satisfy the Python version "
843
+ f"requirement {str(package.requires_python)!r} for package "
844
+ f"{package.name!r} at packages[{package_index}]"
845
+ )
846
+
847
+ # #. Check that no other conflicting instance of the package has been slated
848
+ # to be installed; an error about the ambiguity MUST be raised otherwise.
849
+ if package.name in selected_packages_by_name:
850
+ raise PylockSelectError(
851
+ f"Multiple packages with the name {package.name!r} are "
852
+ f"selected at packages[{package_index}] and "
853
+ f"packages[{selected_packages_by_name[package.name][0]}]"
854
+ )
855
+
856
+ # #. Check that the source of the package is specified appropriately (i.e.
857
+ # there are no conflicting sources in the package entry);
858
+ # an error MUST be raised if any issues are found.
859
+ # Covered by lock.validate() which is a precondition for this method.
860
+
861
+ # #. Add the package to the set of packages to install.
862
+ selected_packages_by_name[package.name] = (package_index, package)
863
+
864
+ # #. For each package to be installed:
865
+ for package_index, package in selected_packages_by_name.values():
866
+ # - If :ref:`pylock-packages-vcs` is set:
867
+ if package.vcs is not None:
868
+ yield package, package.vcs
869
+
870
+ # - Else if :ref:`pylock-packages-directory` is set:
871
+ elif package.directory is not None:
872
+ yield package, package.directory
873
+
874
+ # - Else if :ref:`pylock-packages-archive` is set:
875
+ elif package.archive is not None:
876
+ yield package, package.archive
877
+
878
+ # - Else if there are entries for :ref:`pylock-packages-wheels`:
879
+ elif package.wheels:
880
+ # #. Look for the appropriate wheel file based on
881
+ # :ref:`pylock-packages-wheels-name`; if one is not found then move
882
+ # on to :ref:`pylock-packages-sdist` or an error MUST be raised about
883
+ # a lack of source for the project.
884
+ best_wheel = next(
885
+ compatible_tags_selector(
886
+ (wheel, parse_wheel_filename(wheel.filename)[-1])
887
+ for wheel in package.wheels
888
+ ),
889
+ None,
890
+ )
891
+ if best_wheel:
892
+ yield package, best_wheel
893
+ elif package.sdist is not None:
894
+ yield package, package.sdist
895
+ else:
896
+ raise PylockSelectError(
897
+ f"No wheel found matching the provided tags "
898
+ f"for package {package.name!r} "
899
+ f"at packages[{package_index}], "
900
+ f"and no sdist available as a fallback"
901
+ )
902
+
903
+ # - Else if no :ref:`pylock-packages-wheels` file is found or
904
+ # :ref:`pylock-packages-sdist` is solely set:
905
+ elif package.sdist is not None:
906
+ yield package, package.sdist
907
+
908
+ else:
909
+ # Covered by lock.validate() which is a precondition for this method.
910
+ raise NotImplementedError # pragma: no cover