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

pantsbuild / pants / 25443604553

06 May 2026 03:05PM UTC coverage: 92.879% (-0.04%) from 92.915%
25443604553

push

github

web-flow
[pants_ng] Scaffolding for a pants_ng mode. (#23319)

In this mode the command line is parsed as an
NG invocation, and dispatched appropriately.

Of course at the moment there are no
implementations to dispatch to. That will follow.

This does expose a new option, `pants_ng` to users. 
There is a big warning not to set it, but we're not trying
to hide that we're working on a new thing, so I am
comfortable with this.

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

1294 existing lines in 76 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

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✔
157
        instance = cast(PythonLockfileMetadataV1, instance)
×
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✔
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✔
625
            failure_reasons.add(InvalidPythonLockfileReason.LOCK_STYLE_MISMATCH)
×
626
        if self.complete_platforms != complete_platforms:
2✔
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✔
UNCOV
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