• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 25405422172

05 May 2026 10:18PM UTC coverage: 92.879% (-0.07%) from 92.944%
25405422172

Pull #23319

github

web-flow
Merge c82d0f333 into e8b784f89
Pull Request #23319: [pants_ng] Scaffolding for a pants_ng mode.

25 of 76 new or added lines in 9 files covered. (32.89%)

209 existing lines in 15 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.27
/src/python/pants/backend/python/util_rules/lockfile_metadata.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
12✔
5

6
from collections.abc import Iterable
12✔
7
from dataclasses import dataclass
12✔
8
from enum import Enum, StrEnum, auto
12✔
9
from typing import Any, cast
12✔
10

11
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
12✔
12
from pants.core.util_rules.lockfile_metadata import (
12✔
13
    LockfileMetadata,
14
    LockfileMetadataValidation,
15
    LockfileScope,
16
    _get_metadata,
17
    lockfile_metadata_registrar,
18
)
19
from pants.util.pip_requirement import PipRequirement
12✔
20

21
_python_lockfile_metadata = lockfile_metadata_registrar(LockfileScope.PYTHON)
12✔
22

23

24
class LockfileFormat(StrEnum):
12✔
25
    PEX = auto()
12✔
26
    UV = auto()
12✔
27
    # The very old, deprecated constraints-based "lockfile" that should
28
    # be removed entirely.
29
    CONSTRAINTS_DEPRECATED = auto()
12✔
30

31

32
class InvalidPythonLockfileReason(Enum):
12✔
33
    INVALIDATION_DIGEST_MISMATCH = "invalidation_digest_mismatch"
12✔
34
    INTERPRETER_CONSTRAINTS_MISMATCH = "interpreter_constraints_mismatch"
12✔
35
    REQUIREMENTS_MISMATCH = "requirements_mismatch"
12✔
36
    MANYLINUX_MISMATCH = "manylinux_mismatch"
12✔
37
    CONSTRAINTS_FILE_MISMATCH = "constraints_file_mismatch"
12✔
38
    ONLY_BINARY_MISMATCH = "only_binary_mismatch"
12✔
39
    NO_BINARY_MISMATCH = "no_binary_mismatch"
12✔
40
    EXCLUDES_MISMATCH = "excludes_mismatch"
12✔
41
    OVERRIDES_MISMATCH = "overrides_mismatch"
12✔
42
    SOURCES_MISMATCH = "sources_mismatch"
12✔
43
    LOCK_STYLE_MISMATCH = "lock_style_mismatch"
12✔
44
    COMPLETE_PLATFORMS_MISMATCH = "complete_platforms_mismatch"
12✔
45
    UPLOADED_PRIOR_TO_MISMATCH = "uploaded_prior_to_mismatch"
12✔
46

47

48
@dataclass(frozen=True)
12✔
49
class PythonLockfileMetadata(LockfileMetadata):
12✔
50
    scope = LockfileScope.PYTHON
12✔
51

52
    valid_for_interpreter_constraints: InterpreterConstraints
12✔
53

54
    @staticmethod
12✔
55
    def new(
12✔
56
        *,
57
        valid_for_interpreter_constraints: InterpreterConstraints,
58
        requirements: set[PipRequirement],
59
        manylinux: str | None,
60
        requirement_constraints: set[PipRequirement],
61
        only_binary: set[str],
62
        no_binary: set[str],
63
        excludes: set[str],
64
        overrides: set[str],
65
        sources: set[str],
66
        lock_style: str,
67
        complete_platforms: tuple[str, ...],
68
        uploaded_prior_to: str | None,
69
        lockfile_format: LockfileFormat,
70
        resolve: str,
71
    ) -> PythonLockfileMetadata:
72
        """Call the most recent version of the `LockfileMetadata` class to construct a concrete
73
        instance.
74

75
        This static method should be used in place of the `LockfileMetadata` constructor. This gives
76
        calling sites a predictable method to call to construct a new `LockfileMetadata` for
77
        writing, while still allowing us to support _reading_ older, deprecated metadata versions.
78
        """
79

80
        return PythonLockfileMetadataV8(
3✔
81
            valid_for_interpreter_constraints,
82
            requirements,
83
            manylinux=manylinux,
84
            requirement_constraints=requirement_constraints,
85
            only_binary=only_binary,
86
            no_binary=no_binary,
87
            excludes=excludes,
88
            overrides=overrides,
89
            sources=sources,
90
            lock_style=lock_style,
91
            complete_platforms=complete_platforms,
92
            uploaded_prior_to=uploaded_prior_to,
93
            lockfile_format=lockfile_format,
94
            resolve=resolve,
95
        )
96

97
    @staticmethod
12✔
98
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
12✔
99
        return f"{lockfile_location}.metadata"
12✔
100

101
    @classmethod
12✔
102
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
103
        instance = cast(PythonLockfileMetadata, instance)
3✔
104
        return {
3✔
105
            "valid_for_interpreter_constraints": [
106
                str(ic) for ic in instance.valid_for_interpreter_constraints
107
            ]
108
        }
109

110
    def is_valid_for(
12✔
111
        self,
112
        *,
113
        expected_invalidation_digest: str | None,
114
        user_interpreter_constraints: InterpreterConstraints,
115
        interpreter_universe: Iterable[str],
116
        user_requirements: Iterable[PipRequirement],
117
        manylinux: str | None,
118
        requirement_constraints: Iterable[PipRequirement],
119
        only_binary: Iterable[str],
120
        no_binary: Iterable[str],
121
        excludes: Iterable[str],
122
        overrides: Iterable[str],
123
        sources: Iterable[str],
124
        lock_style: str | None,
125
        complete_platforms: Iterable[str],
126
        uploaded_prior_to: str | None,
127
    ) -> LockfileMetadataValidation:
128
        """Returns Truthy if this `PythonLockfileMetadata` can be used in the current execution
129
        context."""
130

UNCOV
131
        raise NotImplementedError("call `is_valid_for` on subclasses only")
×
132

133

134
@_python_lockfile_metadata(1)
12✔
135
@dataclass(frozen=True)
12✔
136
class PythonLockfileMetadataV1(PythonLockfileMetadata):
12✔
137
    requirements_invalidation_digest: str
12✔
138

139
    @classmethod
12✔
140
    def _from_json_dict(
12✔
141
        cls: type[PythonLockfileMetadataV1],
142
        json_dict: dict[Any, Any],
143
        lockfile_description: str,
144
        error_suffix: str,
145
    ) -> PythonLockfileMetadataV1:
146
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
147

148
        interpreter_constraints = metadata(
1✔
149
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
150
        )
151
        requirements_digest = metadata("requirements_invalidation_digest", str, None)
1✔
152

153
        return PythonLockfileMetadataV1(interpreter_constraints, requirements_digest)
1✔
154

155
    @classmethod
12✔
156
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
UNCOV
157
        instance = cast(PythonLockfileMetadataV1, instance)
×
UNCOV
158
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
159

160
    def is_valid_for(
12✔
161
        self,
162
        *,
163
        expected_invalidation_digest: str | None,
164
        user_interpreter_constraints: InterpreterConstraints,
165
        interpreter_universe: Iterable[str],
166
        # Everything below is not used by v1.
167
        user_requirements: Iterable[PipRequirement],
168
        manylinux: str | None,
169
        requirement_constraints: Iterable[PipRequirement],
170
        only_binary: Iterable[str],
171
        no_binary: Iterable[str],
172
        excludes: Iterable[str],
173
        overrides: Iterable[str],
174
        sources: Iterable[str],
175
        lock_style: str | None,
176
        complete_platforms: Iterable[str],
177
        uploaded_prior_to: str | None,
178
    ) -> LockfileMetadataValidation:
179
        failure_reasons: set[InvalidPythonLockfileReason] = set()
1✔
180

181
        if expected_invalidation_digest is None:
1✔
UNCOV
182
            return LockfileMetadataValidation(failure_reasons)
×
183

184
        if self.requirements_invalidation_digest != expected_invalidation_digest:
1✔
185
            failure_reasons.add(InvalidPythonLockfileReason.INVALIDATION_DIGEST_MISMATCH)
1✔
186

187
        if not self.valid_for_interpreter_constraints.contains(
1✔
188
            user_interpreter_constraints, interpreter_universe
189
        ):
190
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
1✔
191

192
        return LockfileMetadataValidation(failure_reasons)
1✔
193

194

195
@_python_lockfile_metadata(2)
12✔
196
@dataclass(frozen=True)
12✔
197
class PythonLockfileMetadataV2(PythonLockfileMetadata):
12✔
198
    """Lockfile version that permits specifying a requirements as a set rather than a digest.
199

200
    Validity is tested by the set of requirements strings being the same in the user requirements as
201
    those in the stored requirements.
202
    """
203

204
    requirements: set[PipRequirement]
12✔
205

206
    @classmethod
12✔
207
    def _from_json_dict(
12✔
208
        cls: type[PythonLockfileMetadataV2],
209
        json_dict: dict[Any, Any],
210
        lockfile_description: str,
211
        error_suffix: str,
212
    ) -> PythonLockfileMetadataV2:
213
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
12✔
214

215
        requirements = metadata(
12✔
216
            "generated_with_requirements",
217
            set[PipRequirement],
218
            lambda l: {
219
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
220
            },
221
        )
222
        interpreter_constraints = metadata(
12✔
223
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
224
        )
225

226
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
12✔
227

228
    @classmethod
12✔
229
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
230
        instance = cast(PythonLockfileMetadataV2, instance)
3✔
231
        # Requirements need to be stringified then sorted so that tests are deterministic. Sorting
232
        # followed by stringifying does not produce a meaningful result.
233
        return {"generated_with_requirements": (sorted(str(i) for i in instance.requirements))}
3✔
234

235
    def is_valid_for(
12✔
236
        self,
237
        *,
238
        expected_invalidation_digest: str | None,  # Not used by V2.
239
        user_interpreter_constraints: InterpreterConstraints,
240
        interpreter_universe: Iterable[str],
241
        user_requirements: Iterable[PipRequirement],
242
        # Everything below is not used by V2.
243
        manylinux: str | None,
244
        requirement_constraints: Iterable[PipRequirement],
245
        only_binary: Iterable[str],
246
        no_binary: Iterable[str],
247
        excludes: Iterable[str],
248
        overrides: Iterable[str],
249
        sources: Iterable[str],
250
        lock_style: str | None,
251
        complete_platforms: Iterable[str],
252
        uploaded_prior_to: str | None,
253
    ) -> LockfileMetadataValidation:
254
        failure_reasons = set()
3✔
255
        if not set(user_requirements).issubset(self.requirements):
3✔
256
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
3✔
257

258
        if not self.valid_for_interpreter_constraints.contains(
3✔
259
            user_interpreter_constraints, interpreter_universe
260
        ):
261
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
3✔
262

263
        return LockfileMetadataValidation(failure_reasons)
3✔
264

265

266
@_python_lockfile_metadata(3)
12✔
267
@dataclass(frozen=True)
12✔
268
class PythonLockfileMetadataV3(PythonLockfileMetadataV2):
12✔
269
    """Lockfile version that considers constraints files."""
270

271
    manylinux: str | None
12✔
272
    requirement_constraints: set[PipRequirement]
12✔
273
    only_binary: set[str]
12✔
274
    no_binary: set[str]
12✔
275

276
    @classmethod
12✔
277
    def _from_json_dict(
12✔
278
        cls: type[PythonLockfileMetadataV3],
279
        json_dict: dict[Any, Any],
280
        lockfile_description: str,
281
        error_suffix: str,
282
    ) -> PythonLockfileMetadataV3:
283
        v2_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
12✔
284
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
12✔
285
        manylinux = metadata("manylinux", str, lambda l: l)
12✔
286
        requirement_constraints = metadata(
12✔
287
            "requirement_constraints",
288
            set[PipRequirement],
289
            lambda l: {
290
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
291
            },
292
        )
293
        only_binary = metadata("only_binary", set[str], lambda l: set(l))
12✔
294
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
12✔
295

296
        return PythonLockfileMetadataV3(
12✔
297
            valid_for_interpreter_constraints=v2_metadata.valid_for_interpreter_constraints,
298
            requirements=v2_metadata.requirements,
299
            manylinux=manylinux,
300
            requirement_constraints=requirement_constraints,
301
            only_binary=only_binary,
302
            no_binary=no_binary,
303
        )
304

305
    @classmethod
12✔
306
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
307
        instance = cast(PythonLockfileMetadataV3, instance)
3✔
308
        return {
3✔
309
            "manylinux": instance.manylinux,
310
            "requirement_constraints": sorted(str(i) for i in instance.requirement_constraints),
311
            "only_binary": sorted(instance.only_binary),
312
            "no_binary": sorted(instance.no_binary),
313
        }
314

315
    def is_valid_for(
12✔
316
        self,
317
        *,
318
        expected_invalidation_digest: str | None,  # Validation digests are not used by V2.
319
        user_interpreter_constraints: InterpreterConstraints,
320
        interpreter_universe: Iterable[str],
321
        user_requirements: Iterable[PipRequirement],
322
        manylinux: str | None,
323
        requirement_constraints: Iterable[PipRequirement],
324
        only_binary: Iterable[str],
325
        no_binary: Iterable[str],
326
        # not used for V3
327
        excludes: Iterable[str],
328
        overrides: Iterable[str],
329
        sources: Iterable[str],
330
        lock_style: str | None,
331
        complete_platforms: Iterable[str],
332
        uploaded_prior_to: str | None,
333
    ) -> LockfileMetadataValidation:
334
        failure_reasons = (
3✔
335
            super()
336
            .is_valid_for(
337
                expected_invalidation_digest=expected_invalidation_digest,
338
                user_interpreter_constraints=user_interpreter_constraints,
339
                interpreter_universe=interpreter_universe,
340
                user_requirements=user_requirements,
341
                manylinux=manylinux,
342
                requirement_constraints=requirement_constraints,
343
                only_binary=only_binary,
344
                no_binary=no_binary,
345
                excludes=excludes,
346
                overrides=overrides,
347
                sources=sources,
348
                lock_style=lock_style,
349
                complete_platforms=complete_platforms,
350
                uploaded_prior_to=uploaded_prior_to,
351
            )
352
            .failure_reasons
353
        )
354

355
        if self.manylinux != manylinux:
3✔
356
            failure_reasons.add(InvalidPythonLockfileReason.MANYLINUX_MISMATCH)
3✔
357
        if self.requirement_constraints != set(requirement_constraints):
3✔
358
            failure_reasons.add(InvalidPythonLockfileReason.CONSTRAINTS_FILE_MISMATCH)
2✔
359
        if self.only_binary != set(only_binary):
3✔
360
            failure_reasons.add(InvalidPythonLockfileReason.ONLY_BINARY_MISMATCH)
2✔
361
        if self.no_binary != set(no_binary):
3✔
362
            failure_reasons.add(InvalidPythonLockfileReason.NO_BINARY_MISMATCH)
2✔
363

364
        return LockfileMetadataValidation(failure_reasons)
3✔
365

366

367
@_python_lockfile_metadata(4)
12✔
368
@dataclass(frozen=True)
12✔
369
class PythonLockfileMetadataV4(PythonLockfileMetadataV3):
12✔
370
    """Lockfile version with excludes/overrides."""
371

372
    excludes: set[str]
12✔
373
    overrides: set[str]
12✔
374

375
    @classmethod
12✔
376
    def _from_json_dict(
12✔
377
        cls: type[PythonLockfileMetadataV4],
378
        json_dict: dict[Any, Any],
379
        lockfile_description: str,
380
        error_suffix: str,
381
    ) -> PythonLockfileMetadataV4:
382
        v3_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
12✔
383
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
12✔
384

385
        excludes = metadata("excludes", set[str], lambda l: set(l))
12✔
386
        overrides = metadata("overrides", set[str], lambda l: set(l))
12✔
387

388
        return PythonLockfileMetadataV4(
12✔
389
            valid_for_interpreter_constraints=v3_metadata.valid_for_interpreter_constraints,
390
            requirements=v3_metadata.requirements,
391
            manylinux=v3_metadata.manylinux,
392
            requirement_constraints=v3_metadata.requirement_constraints,
393
            only_binary=v3_metadata.only_binary,
394
            no_binary=v3_metadata.no_binary,
395
            excludes=excludes,
396
            overrides=overrides,
397
        )
398

399
    @classmethod
12✔
400
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
401
        instance = cast(PythonLockfileMetadataV4, instance)
3✔
402
        return {
3✔
403
            "excludes": sorted(instance.excludes),
404
            "overrides": sorted(instance.overrides),
405
        }
406

407
    def is_valid_for(
12✔
408
        self,
409
        *,
410
        expected_invalidation_digest: str | None,
411
        user_interpreter_constraints: InterpreterConstraints,
412
        interpreter_universe: Iterable[str],
413
        user_requirements: Iterable[PipRequirement],
414
        manylinux: str | None,
415
        requirement_constraints: Iterable[PipRequirement],
416
        only_binary: Iterable[str],
417
        no_binary: Iterable[str],
418
        excludes: Iterable[str],
419
        overrides: Iterable[str],
420
        # not used for V4
421
        sources: Iterable[str],
422
        lock_style: str | None,
423
        complete_platforms: Iterable[str],
424
        uploaded_prior_to: str | None,
425
    ) -> LockfileMetadataValidation:
426
        failure_reasons = (
2✔
427
            super()
428
            .is_valid_for(
429
                expected_invalidation_digest=expected_invalidation_digest,
430
                user_interpreter_constraints=user_interpreter_constraints,
431
                interpreter_universe=interpreter_universe,
432
                user_requirements=user_requirements,
433
                manylinux=manylinux,
434
                requirement_constraints=requirement_constraints,
435
                only_binary=only_binary,
436
                no_binary=no_binary,
437
                excludes=excludes,
438
                overrides=overrides,
439
                sources=sources,
440
                lock_style=lock_style,
441
                complete_platforms=complete_platforms,
442
                uploaded_prior_to=uploaded_prior_to,
443
            )
444
            .failure_reasons
445
        )
446

447
        if self.excludes != set(excludes):
2✔
448
            failure_reasons.add(InvalidPythonLockfileReason.EXCLUDES_MISMATCH)
1✔
449
        if self.overrides != set(overrides):
2✔
450
            failure_reasons.add(InvalidPythonLockfileReason.OVERRIDES_MISMATCH)
1✔
451

452
        return LockfileMetadataValidation(failure_reasons)
2✔
453

454

455
@_python_lockfile_metadata(5)
12✔
456
@dataclass(frozen=True)
12✔
457
class PythonLockfileMetadataV5(PythonLockfileMetadataV4):
12✔
458
    """Lockfile version with sources."""
459

460
    sources: set[str]
12✔
461

462
    @classmethod
12✔
463
    def _from_json_dict(
12✔
464
        cls: type[PythonLockfileMetadataV5],
465
        json_dict: dict[Any, Any],
466
        lockfile_description: str,
467
        error_suffix: str,
468
    ) -> PythonLockfileMetadataV5:
469
        v4_metadata = PythonLockfileMetadataV4._from_json_dict(
12✔
470
            json_dict, lockfile_description, error_suffix
471
        )
472
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
12✔
473

474
        sources = metadata("sources", set[str], lambda l: set(l))
12✔
475

476
        return PythonLockfileMetadataV5(
12✔
477
            valid_for_interpreter_constraints=v4_metadata.valid_for_interpreter_constraints,
478
            requirements=v4_metadata.requirements,
479
            manylinux=v4_metadata.manylinux,
480
            requirement_constraints=v4_metadata.requirement_constraints,
481
            only_binary=v4_metadata.only_binary,
482
            no_binary=v4_metadata.no_binary,
483
            excludes=v4_metadata.excludes,
484
            overrides=v4_metadata.overrides,
485
            sources=sources,
486
        )
487

488
    @classmethod
12✔
489
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
490
        instance = cast(PythonLockfileMetadataV5, instance)
3✔
491
        return {
3✔
492
            "sources": sorted(instance.sources),
493
        }
494

495
    def is_valid_for(
12✔
496
        self,
497
        *,
498
        expected_invalidation_digest: str | None,
499
        user_interpreter_constraints: InterpreterConstraints,
500
        interpreter_universe: Iterable[str],
501
        user_requirements: Iterable[PipRequirement],
502
        manylinux: str | None,
503
        requirement_constraints: Iterable[PipRequirement],
504
        only_binary: Iterable[str],
505
        no_binary: Iterable[str],
506
        excludes: Iterable[str],
507
        overrides: Iterable[str],
508
        sources: Iterable[str],
509
        lock_style: str | None,
510
        complete_platforms: Iterable[str],
511
        uploaded_prior_to: str | None,
512
    ) -> LockfileMetadataValidation:
513
        failure_reasons = (
2✔
514
            super()
515
            .is_valid_for(
516
                expected_invalidation_digest=expected_invalidation_digest,
517
                user_interpreter_constraints=user_interpreter_constraints,
518
                interpreter_universe=interpreter_universe,
519
                user_requirements=user_requirements,
520
                manylinux=manylinux,
521
                requirement_constraints=requirement_constraints,
522
                only_binary=only_binary,
523
                no_binary=no_binary,
524
                excludes=excludes,
525
                overrides=overrides,
526
                sources=sources,
527
                lock_style=lock_style,
528
                complete_platforms=complete_platforms,
529
                uploaded_prior_to=uploaded_prior_to,
530
            )
531
            .failure_reasons
532
        )
533

534
        if self.sources != set(sources):
2✔
535
            failure_reasons.add(InvalidPythonLockfileReason.SOURCES_MISMATCH)
1✔
536

537
        return LockfileMetadataValidation(failure_reasons)
2✔
538

539

540
@_python_lockfile_metadata(6)
12✔
541
@dataclass(frozen=True)
12✔
542
class PythonLockfileMetadataV6(PythonLockfileMetadataV5):
12✔
543
    """Lockfile version with lock_style and complete_platforms."""
544

545
    lock_style: str | None
12✔
546
    complete_platforms: Iterable[str]
12✔
547

548
    @classmethod
12✔
549
    def _from_json_dict(
12✔
550
        cls: type[PythonLockfileMetadataV6],
551
        json_dict: dict[Any, Any],
552
        lockfile_description: str,
553
        error_suffix: str,
554
    ) -> PythonLockfileMetadataV6:
555
        v5_metadata = PythonLockfileMetadataV5._from_json_dict(
5✔
556
            json_dict, lockfile_description, error_suffix
557
        )
558
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
5✔
559

560
        lock_style = metadata("lock_style", str, lambda l: l)
5✔
561
        complete_platforms = metadata("complete_platforms", tuple[str, ...], lambda l: tuple(l))
5✔
562

563
        return PythonLockfileMetadataV6(
5✔
564
            valid_for_interpreter_constraints=v5_metadata.valid_for_interpreter_constraints,
565
            requirements=v5_metadata.requirements,
566
            manylinux=v5_metadata.manylinux,
567
            requirement_constraints=v5_metadata.requirement_constraints,
568
            only_binary=v5_metadata.only_binary,
569
            no_binary=v5_metadata.no_binary,
570
            excludes=v5_metadata.excludes,
571
            overrides=v5_metadata.overrides,
572
            sources=v5_metadata.sources,
573
            lock_style=lock_style,
574
            complete_platforms=complete_platforms,
575
        )
576

577
    @classmethod
12✔
578
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
579
        instance = cast(PythonLockfileMetadataV6, instance)
3✔
580
        return {
3✔
581
            "lock_style": instance.lock_style,
582
            "complete_platforms": sorted(instance.complete_platforms),
583
        }
584

585
    def is_valid_for(
12✔
586
        self,
587
        *,
588
        expected_invalidation_digest: str | None,
589
        user_interpreter_constraints: InterpreterConstraints,
590
        interpreter_universe: Iterable[str],
591
        user_requirements: Iterable[PipRequirement],
592
        manylinux: str | None,
593
        requirement_constraints: Iterable[PipRequirement],
594
        only_binary: Iterable[str],
595
        no_binary: Iterable[str],
596
        excludes: Iterable[str],
597
        overrides: Iterable[str],
598
        sources: Iterable[str],
599
        lock_style: str | None,
600
        complete_platforms: Iterable[str],
601
        uploaded_prior_to: str | None,
602
    ) -> LockfileMetadataValidation:
603
        failure_reasons = (
2✔
604
            super()
605
            .is_valid_for(
606
                expected_invalidation_digest=expected_invalidation_digest,
607
                user_interpreter_constraints=user_interpreter_constraints,
608
                interpreter_universe=interpreter_universe,
609
                user_requirements=user_requirements,
610
                manylinux=manylinux,
611
                requirement_constraints=requirement_constraints,
612
                only_binary=only_binary,
613
                no_binary=no_binary,
614
                excludes=excludes,
615
                overrides=overrides,
616
                sources=sources,
617
                lock_style=lock_style,
618
                complete_platforms=complete_platforms,
619
                uploaded_prior_to=uploaded_prior_to,
620
            )
621
            .failure_reasons
622
        )
623

624
        if self.lock_style != lock_style:
2✔
UNCOV
625
            failure_reasons.add(InvalidPythonLockfileReason.LOCK_STYLE_MISMATCH)
×
626
        if self.complete_platforms != complete_platforms:
2✔
UNCOV
627
            failure_reasons.add(InvalidPythonLockfileReason.COMPLETE_PLATFORMS_MISMATCH)
×
628

629
        return LockfileMetadataValidation(failure_reasons)
2✔
630

631

632
@_python_lockfile_metadata(7)
12✔
633
@dataclass(frozen=True)
12✔
634
class PythonLockfileMetadataV7(PythonLockfileMetadataV6):
12✔
635
    """Lockfile version with uploaded_prior_to."""
636

637
    uploaded_prior_to: str | None
12✔
638

639
    @classmethod
12✔
640
    def _from_json_dict(
12✔
641
        cls: type[PythonLockfileMetadataV7],
642
        json_dict: dict[Any, Any],
643
        lockfile_description: str,
644
        error_suffix: str,
645
    ) -> PythonLockfileMetadataV7:
646
        v6_metadata = PythonLockfileMetadataV6._from_json_dict(
2✔
647
            json_dict, lockfile_description, error_suffix
648
        )
649
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
2✔
650

651
        uploaded_prior_to: str | None = metadata("uploaded_prior_to", str, lambda l: l)
2✔
652

653
        return PythonLockfileMetadataV7(
2✔
654
            valid_for_interpreter_constraints=v6_metadata.valid_for_interpreter_constraints,
655
            requirements=v6_metadata.requirements,
656
            manylinux=v6_metadata.manylinux,
657
            requirement_constraints=v6_metadata.requirement_constraints,
658
            only_binary=v6_metadata.only_binary,
659
            no_binary=v6_metadata.no_binary,
660
            excludes=v6_metadata.excludes,
661
            overrides=v6_metadata.overrides,
662
            sources=v6_metadata.sources,
663
            lock_style=v6_metadata.lock_style,
664
            complete_platforms=v6_metadata.complete_platforms,
665
            uploaded_prior_to=uploaded_prior_to,
666
        )
667

668
    @classmethod
12✔
669
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
670
        instance = cast(PythonLockfileMetadataV7, instance)
3✔
671
        return {
3✔
672
            "uploaded_prior_to": instance.uploaded_prior_to,
673
        }
674

675
    def is_valid_for(
12✔
676
        self,
677
        *,
678
        expected_invalidation_digest: str | None,
679
        user_interpreter_constraints: InterpreterConstraints,
680
        interpreter_universe: Iterable[str],
681
        user_requirements: Iterable[PipRequirement],
682
        manylinux: str | None,
683
        requirement_constraints: Iterable[PipRequirement],
684
        only_binary: Iterable[str],
685
        no_binary: Iterable[str],
686
        excludes: Iterable[str],
687
        overrides: Iterable[str],
688
        sources: Iterable[str],
689
        lock_style: str | None,
690
        complete_platforms: Iterable[str],
691
        uploaded_prior_to: str | None,
692
    ) -> LockfileMetadataValidation:
693
        failure_reasons = (
2✔
694
            super()
695
            .is_valid_for(
696
                expected_invalidation_digest=expected_invalidation_digest,
697
                user_interpreter_constraints=user_interpreter_constraints,
698
                interpreter_universe=interpreter_universe,
699
                user_requirements=user_requirements,
700
                manylinux=manylinux,
701
                requirement_constraints=requirement_constraints,
702
                only_binary=only_binary,
703
                no_binary=no_binary,
704
                excludes=excludes,
705
                overrides=overrides,
706
                sources=sources,
707
                lock_style=lock_style,
708
                complete_platforms=complete_platforms,
709
                uploaded_prior_to=uploaded_prior_to,
710
            )
711
            .failure_reasons
712
        )
713

714
        if self.uploaded_prior_to != uploaded_prior_to:
2✔
715
            failure_reasons.add(InvalidPythonLockfileReason.UPLOADED_PRIOR_TO_MISMATCH)
1✔
716

717
        return LockfileMetadataValidation(failure_reasons)
2✔
718

719

720
@_python_lockfile_metadata(8)
12✔
721
@dataclass(frozen=True)
12✔
722
class PythonLockfileMetadataV8(PythonLockfileMetadataV7):
12✔
723
    """Lockfile version that records the lockfile format (pex, uv, etc.) and the resolve name."""
724

725
    lockfile_format: LockfileFormat
12✔
726
    resolve: str
12✔
727

728
    @classmethod
12✔
729
    def _from_json_dict(
12✔
730
        cls: type[PythonLockfileMetadataV8],
731
        json_dict: dict[Any, Any],
732
        lockfile_description: str,
733
        error_suffix: str,
734
    ) -> PythonLockfileMetadataV8:
735
        v7_metadata = PythonLockfileMetadataV7._from_json_dict(
2✔
736
            json_dict, lockfile_description, error_suffix
737
        )
738
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
2✔
739

740
        lockfile_format = metadata("lockfile_format", LockfileFormat, LockfileFormat)
2✔
741
        resolve = metadata("resolve", str, str)
2✔
742

743
        return PythonLockfileMetadataV8(
2✔
744
            valid_for_interpreter_constraints=v7_metadata.valid_for_interpreter_constraints,
745
            requirements=v7_metadata.requirements,
746
            manylinux=v7_metadata.manylinux,
747
            requirement_constraints=v7_metadata.requirement_constraints,
748
            only_binary=v7_metadata.only_binary,
749
            no_binary=v7_metadata.no_binary,
750
            excludes=v7_metadata.excludes,
751
            overrides=v7_metadata.overrides,
752
            sources=v7_metadata.sources,
753
            lock_style=v7_metadata.lock_style,
754
            complete_platforms=v7_metadata.complete_platforms,
755
            uploaded_prior_to=v7_metadata.uploaded_prior_to,
756
            lockfile_format=lockfile_format,
757
            resolve=resolve,
758
        )
759

760
    @classmethod
12✔
761
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
12✔
762
        instance = cast(PythonLockfileMetadataV8, instance)
3✔
763
        return {
3✔
764
            "lockfile_format": instance.lockfile_format,
765
            "resolve": instance.resolve,
766
        }
767

768
    def is_valid_for(self, **kwargs) -> LockfileMetadataValidation:
12✔
769
        return super().is_valid_for(**kwargs)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc