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

pantsbuild / pants / 24145945949

08 Apr 2026 04:14PM UTC coverage: 82.077% (-10.8%) from 92.91%
24145945949

Pull #23233

github

web-flow
Merge 089d98e3c into 9036734c9
Pull Request #23233: Introduce a LockfileFormat enum.

8 of 11 new or added lines in 4 files covered. (72.73%)

7635 existing lines in 306 files now uncovered.

63732 of 77649 relevant lines covered (82.08%)

2.96 hits per line

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

94.95
/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
7✔
5

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

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

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

23

24
class InvalidPythonLockfileReason(Enum):
7✔
25
    INVALIDATION_DIGEST_MISMATCH = "invalidation_digest_mismatch"
7✔
26
    INTERPRETER_CONSTRAINTS_MISMATCH = "interpreter_constraints_mismatch"
7✔
27
    REQUIREMENTS_MISMATCH = "requirements_mismatch"
7✔
28
    MANYLINUX_MISMATCH = "manylinux_mismatch"
7✔
29
    CONSTRAINTS_FILE_MISMATCH = "constraints_file_mismatch"
7✔
30
    ONLY_BINARY_MISMATCH = "only_binary_mismatch"
7✔
31
    NO_BINARY_MISMATCH = "no_binary_mismatch"
7✔
32
    EXCLUDES_MISMATCH = "excludes_mismatch"
7✔
33
    OVERRIDES_MISMATCH = "overrides_mismatch"
7✔
34
    SOURCES_MISMATCH = "sources_mismatch"
7✔
35
    LOCK_STYLE_MISMATCH = "lock_style_mismatch"
7✔
36
    COMPLETE_PLATFORMS_MISMATCH = "complete_platforms_mismatch"
7✔
37
    UPLOADED_PRIOR_TO_MISMATCH = "uploaded_prior_to_mismatch"
7✔
38

39

40
@dataclass(frozen=True)
7✔
41
class PythonLockfileMetadata(LockfileMetadata):
7✔
42
    scope = LockfileScope.PYTHON
7✔
43

44
    valid_for_interpreter_constraints: InterpreterConstraints
7✔
45

46
    @staticmethod
7✔
47
    def new(
7✔
48
        *,
49
        valid_for_interpreter_constraints: InterpreterConstraints,
50
        requirements: set[PipRequirement],
51
        manylinux: str | None,
52
        requirement_constraints: set[PipRequirement],
53
        only_binary: set[str],
54
        no_binary: set[str],
55
        excludes: set[str],
56
        overrides: set[str],
57
        sources: set[str],
58
        lock_style: str,
59
        complete_platforms: tuple[str, ...],
60
        uploaded_prior_to: str | None,
61
    ) -> PythonLockfileMetadata:
62
        """Call the most recent version of the `LockfileMetadata` class to construct a concrete
63
        instance.
64

65
        This static method should be used in place of the `LockfileMetadata` constructor. This gives
66
        calling sites a predictable method to call to construct a new `LockfileMetadata` for
67
        writing, while still allowing us to support _reading_ older, deprecated metadata versions.
68
        """
69

70
        return PythonLockfileMetadataV7(
1✔
71
            valid_for_interpreter_constraints,
72
            requirements,
73
            manylinux=manylinux,
74
            requirement_constraints=requirement_constraints,
75
            only_binary=only_binary,
76
            no_binary=no_binary,
77
            excludes=excludes,
78
            overrides=overrides,
79
            sources=sources,
80
            lock_style=lock_style,
81
            complete_platforms=complete_platforms,
82
            uploaded_prior_to=uploaded_prior_to,
83
        )
84

85
    @staticmethod
7✔
86
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
7✔
87
        return f"{lockfile_location}.metadata"
7✔
88

89
    @classmethod
7✔
90
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
91
        instance = cast(PythonLockfileMetadata, instance)
1✔
92
        return {
1✔
93
            "valid_for_interpreter_constraints": [
94
                str(ic) for ic in instance.valid_for_interpreter_constraints
95
            ]
96
        }
97

98
    def is_valid_for(
7✔
99
        self,
100
        *,
101
        expected_invalidation_digest: str | None,
102
        user_interpreter_constraints: InterpreterConstraints,
103
        interpreter_universe: Iterable[str],
104
        user_requirements: Iterable[PipRequirement],
105
        manylinux: str | None,
106
        requirement_constraints: Iterable[PipRequirement],
107
        only_binary: Iterable[str],
108
        no_binary: Iterable[str],
109
        excludes: Iterable[str],
110
        overrides: Iterable[str],
111
        sources: Iterable[str],
112
        lock_style: str | None,
113
        complete_platforms: Iterable[str],
114
        uploaded_prior_to: str | None,
115
    ) -> LockfileMetadataValidation:
116
        """Returns Truthy if this `PythonLockfileMetadata` can be used in the current execution
117
        context."""
118

119
        raise NotImplementedError("call `is_valid_for` on subclasses only")
×
120

121

122
@_python_lockfile_metadata(1)
7✔
123
@dataclass(frozen=True)
7✔
124
class PythonLockfileMetadataV1(PythonLockfileMetadata):
7✔
125
    requirements_invalidation_digest: str
7✔
126

127
    @classmethod
7✔
128
    def _from_json_dict(
7✔
129
        cls: type[PythonLockfileMetadataV1],
130
        json_dict: dict[Any, Any],
131
        lockfile_description: str,
132
        error_suffix: str,
133
    ) -> PythonLockfileMetadataV1:
UNCOV
134
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
135

UNCOV
136
        interpreter_constraints = metadata(
×
137
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
138
        )
UNCOV
139
        requirements_digest = metadata("requirements_invalidation_digest", str, None)
×
140

UNCOV
141
        return PythonLockfileMetadataV1(interpreter_constraints, requirements_digest)
×
142

143
    @classmethod
7✔
144
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
145
        instance = cast(PythonLockfileMetadataV1, instance)
×
146
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
147

148
    def is_valid_for(
7✔
149
        self,
150
        *,
151
        expected_invalidation_digest: str | None,
152
        user_interpreter_constraints: InterpreterConstraints,
153
        interpreter_universe: Iterable[str],
154
        # Everything below is not used by v1.
155
        user_requirements: Iterable[PipRequirement],
156
        manylinux: str | None,
157
        requirement_constraints: Iterable[PipRequirement],
158
        only_binary: Iterable[str],
159
        no_binary: Iterable[str],
160
        excludes: Iterable[str],
161
        overrides: Iterable[str],
162
        sources: Iterable[str],
163
        lock_style: str | None,
164
        complete_platforms: Iterable[str],
165
        uploaded_prior_to: str | None,
166
    ) -> LockfileMetadataValidation:
167
        failure_reasons: set[InvalidPythonLockfileReason] = set()
1✔
168

169
        if expected_invalidation_digest is None:
1✔
170
            return LockfileMetadataValidation(failure_reasons)
×
171

172
        if self.requirements_invalidation_digest != expected_invalidation_digest:
1✔
173
            failure_reasons.add(InvalidPythonLockfileReason.INVALIDATION_DIGEST_MISMATCH)
1✔
174

175
        if not self.valid_for_interpreter_constraints.contains(
1✔
176
            user_interpreter_constraints, interpreter_universe
177
        ):
178
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
1✔
179

180
        return LockfileMetadataValidation(failure_reasons)
1✔
181

182

183
@_python_lockfile_metadata(2)
7✔
184
@dataclass(frozen=True)
7✔
185
class PythonLockfileMetadataV2(PythonLockfileMetadata):
7✔
186
    """Lockfile version that permits specifying a requirements as a set rather than a digest.
187

188
    Validity is tested by the set of requirements strings being the same in the user requirements as
189
    those in the stored requirements.
190
    """
191

192
    requirements: set[PipRequirement]
7✔
193

194
    @classmethod
7✔
195
    def _from_json_dict(
7✔
196
        cls: type[PythonLockfileMetadataV2],
197
        json_dict: dict[Any, Any],
198
        lockfile_description: str,
199
        error_suffix: str,
200
    ) -> PythonLockfileMetadataV2:
201
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
7✔
202

203
        requirements = metadata(
7✔
204
            "generated_with_requirements",
205
            set[PipRequirement],
206
            lambda l: {
207
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
208
            },
209
        )
210
        interpreter_constraints = metadata(
7✔
211
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
212
        )
213

214
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
7✔
215

216
    @classmethod
7✔
217
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
218
        instance = cast(PythonLockfileMetadataV2, instance)
1✔
219
        # Requirements need to be stringified then sorted so that tests are deterministic. Sorting
220
        # followed by stringifying does not produce a meaningful result.
221
        return {"generated_with_requirements": (sorted(str(i) for i in instance.requirements))}
1✔
222

223
    def is_valid_for(
7✔
224
        self,
225
        *,
226
        expected_invalidation_digest: str | None,  # Not used by V2.
227
        user_interpreter_constraints: InterpreterConstraints,
228
        interpreter_universe: Iterable[str],
229
        user_requirements: Iterable[PipRequirement],
230
        # Everything below is not used by V2.
231
        manylinux: str | None,
232
        requirement_constraints: Iterable[PipRequirement],
233
        only_binary: Iterable[str],
234
        no_binary: Iterable[str],
235
        excludes: Iterable[str],
236
        overrides: Iterable[str],
237
        sources: Iterable[str],
238
        lock_style: str | None,
239
        complete_platforms: Iterable[str],
240
        uploaded_prior_to: str | None,
241
    ) -> LockfileMetadataValidation:
242
        failure_reasons = set()
1✔
243
        if not set(user_requirements).issubset(self.requirements):
1✔
244
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
1✔
245

246
        if not self.valid_for_interpreter_constraints.contains(
1✔
247
            user_interpreter_constraints, interpreter_universe
248
        ):
249
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
1✔
250

251
        return LockfileMetadataValidation(failure_reasons)
1✔
252

253

254
@_python_lockfile_metadata(3)
7✔
255
@dataclass(frozen=True)
7✔
256
class PythonLockfileMetadataV3(PythonLockfileMetadataV2):
7✔
257
    """Lockfile version that considers constraints files."""
258

259
    manylinux: str | None
7✔
260
    requirement_constraints: set[PipRequirement]
7✔
261
    only_binary: set[str]
7✔
262
    no_binary: set[str]
7✔
263

264
    @classmethod
7✔
265
    def _from_json_dict(
7✔
266
        cls: type[PythonLockfileMetadataV3],
267
        json_dict: dict[Any, Any],
268
        lockfile_description: str,
269
        error_suffix: str,
270
    ) -> PythonLockfileMetadataV3:
271
        v2_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
7✔
272
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
7✔
273
        manylinux = metadata("manylinux", str, lambda l: l)
7✔
274
        requirement_constraints = metadata(
7✔
275
            "requirement_constraints",
276
            set[PipRequirement],
277
            lambda l: {
278
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
279
            },
280
        )
281
        only_binary = metadata("only_binary", set[str], lambda l: set(l))
7✔
282
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
7✔
283

284
        return PythonLockfileMetadataV3(
7✔
285
            valid_for_interpreter_constraints=v2_metadata.valid_for_interpreter_constraints,
286
            requirements=v2_metadata.requirements,
287
            manylinux=manylinux,
288
            requirement_constraints=requirement_constraints,
289
            only_binary=only_binary,
290
            no_binary=no_binary,
291
        )
292

293
    @classmethod
7✔
294
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
295
        instance = cast(PythonLockfileMetadataV3, instance)
1✔
296
        return {
1✔
297
            "manylinux": instance.manylinux,
298
            "requirement_constraints": sorted(str(i) for i in instance.requirement_constraints),
299
            "only_binary": sorted(instance.only_binary),
300
            "no_binary": sorted(instance.no_binary),
301
        }
302

303
    def is_valid_for(
7✔
304
        self,
305
        *,
306
        expected_invalidation_digest: str | None,  # Validation digests are not used by V2.
307
        user_interpreter_constraints: InterpreterConstraints,
308
        interpreter_universe: Iterable[str],
309
        user_requirements: Iterable[PipRequirement],
310
        manylinux: str | None,
311
        requirement_constraints: Iterable[PipRequirement],
312
        only_binary: Iterable[str],
313
        no_binary: Iterable[str],
314
        # not used for V3
315
        excludes: Iterable[str],
316
        overrides: Iterable[str],
317
        sources: Iterable[str],
318
        lock_style: str | None,
319
        complete_platforms: Iterable[str],
320
        uploaded_prior_to: str | None,
321
    ) -> LockfileMetadataValidation:
322
        failure_reasons = (
1✔
323
            super()
324
            .is_valid_for(
325
                expected_invalidation_digest=expected_invalidation_digest,
326
                user_interpreter_constraints=user_interpreter_constraints,
327
                interpreter_universe=interpreter_universe,
328
                user_requirements=user_requirements,
329
                manylinux=manylinux,
330
                requirement_constraints=requirement_constraints,
331
                only_binary=only_binary,
332
                no_binary=no_binary,
333
                excludes=excludes,
334
                overrides=overrides,
335
                sources=sources,
336
                lock_style=lock_style,
337
                complete_platforms=complete_platforms,
338
                uploaded_prior_to=uploaded_prior_to,
339
            )
340
            .failure_reasons
341
        )
342

343
        if self.manylinux != manylinux:
1✔
344
            failure_reasons.add(InvalidPythonLockfileReason.MANYLINUX_MISMATCH)
1✔
345
        if self.requirement_constraints != set(requirement_constraints):
1✔
346
            failure_reasons.add(InvalidPythonLockfileReason.CONSTRAINTS_FILE_MISMATCH)
1✔
347
        if self.only_binary != set(only_binary):
1✔
348
            failure_reasons.add(InvalidPythonLockfileReason.ONLY_BINARY_MISMATCH)
1✔
349
        if self.no_binary != set(no_binary):
1✔
350
            failure_reasons.add(InvalidPythonLockfileReason.NO_BINARY_MISMATCH)
1✔
351

352
        return LockfileMetadataValidation(failure_reasons)
1✔
353

354

355
@_python_lockfile_metadata(4)
7✔
356
@dataclass(frozen=True)
7✔
357
class PythonLockfileMetadataV4(PythonLockfileMetadataV3):
7✔
358
    """Lockfile version with excludes/overrides."""
359

360
    excludes: set[str]
7✔
361
    overrides: set[str]
7✔
362

363
    @classmethod
7✔
364
    def _from_json_dict(
7✔
365
        cls: type[PythonLockfileMetadataV4],
366
        json_dict: dict[Any, Any],
367
        lockfile_description: str,
368
        error_suffix: str,
369
    ) -> PythonLockfileMetadataV4:
370
        v3_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
7✔
371
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
7✔
372

373
        excludes = metadata("excludes", set[str], lambda l: set(l))
7✔
374
        overrides = metadata("overrides", set[str], lambda l: set(l))
7✔
375

376
        return PythonLockfileMetadataV4(
7✔
377
            valid_for_interpreter_constraints=v3_metadata.valid_for_interpreter_constraints,
378
            requirements=v3_metadata.requirements,
379
            manylinux=v3_metadata.manylinux,
380
            requirement_constraints=v3_metadata.requirement_constraints,
381
            only_binary=v3_metadata.only_binary,
382
            no_binary=v3_metadata.no_binary,
383
            excludes=excludes,
384
            overrides=overrides,
385
        )
386

387
    @classmethod
7✔
388
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
389
        instance = cast(PythonLockfileMetadataV4, instance)
1✔
390
        return {
1✔
391
            "excludes": sorted(instance.excludes),
392
            "overrides": sorted(instance.overrides),
393
        }
394

395
    def is_valid_for(
7✔
396
        self,
397
        *,
398
        expected_invalidation_digest: str | None,
399
        user_interpreter_constraints: InterpreterConstraints,
400
        interpreter_universe: Iterable[str],
401
        user_requirements: Iterable[PipRequirement],
402
        manylinux: str | None,
403
        requirement_constraints: Iterable[PipRequirement],
404
        only_binary: Iterable[str],
405
        no_binary: Iterable[str],
406
        excludes: Iterable[str],
407
        overrides: Iterable[str],
408
        # not used for V4
409
        sources: Iterable[str],
410
        lock_style: str | None,
411
        complete_platforms: Iterable[str],
412
        uploaded_prior_to: str | None,
413
    ) -> LockfileMetadataValidation:
414
        failure_reasons = (
1✔
415
            super()
416
            .is_valid_for(
417
                expected_invalidation_digest=expected_invalidation_digest,
418
                user_interpreter_constraints=user_interpreter_constraints,
419
                interpreter_universe=interpreter_universe,
420
                user_requirements=user_requirements,
421
                manylinux=manylinux,
422
                requirement_constraints=requirement_constraints,
423
                only_binary=only_binary,
424
                no_binary=no_binary,
425
                excludes=excludes,
426
                overrides=overrides,
427
                sources=sources,
428
                lock_style=lock_style,
429
                complete_platforms=complete_platforms,
430
                uploaded_prior_to=uploaded_prior_to,
431
            )
432
            .failure_reasons
433
        )
434

435
        if self.excludes != set(excludes):
1✔
436
            failure_reasons.add(InvalidPythonLockfileReason.EXCLUDES_MISMATCH)
1✔
437
        if self.overrides != set(overrides):
1✔
438
            failure_reasons.add(InvalidPythonLockfileReason.OVERRIDES_MISMATCH)
1✔
439

440
        return LockfileMetadataValidation(failure_reasons)
1✔
441

442

443
@_python_lockfile_metadata(5)
7✔
444
@dataclass(frozen=True)
7✔
445
class PythonLockfileMetadataV5(PythonLockfileMetadataV4):
7✔
446
    """Lockfile version with sources."""
447

448
    sources: set[str]
7✔
449

450
    @classmethod
7✔
451
    def _from_json_dict(
7✔
452
        cls: type[PythonLockfileMetadataV5],
453
        json_dict: dict[Any, Any],
454
        lockfile_description: str,
455
        error_suffix: str,
456
    ) -> PythonLockfileMetadataV5:
457
        v4_metadata = PythonLockfileMetadataV4._from_json_dict(
7✔
458
            json_dict, lockfile_description, error_suffix
459
        )
460
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
7✔
461

462
        sources = metadata("sources", set[str], lambda l: set(l))
7✔
463

464
        return PythonLockfileMetadataV5(
7✔
465
            valid_for_interpreter_constraints=v4_metadata.valid_for_interpreter_constraints,
466
            requirements=v4_metadata.requirements,
467
            manylinux=v4_metadata.manylinux,
468
            requirement_constraints=v4_metadata.requirement_constraints,
469
            only_binary=v4_metadata.only_binary,
470
            no_binary=v4_metadata.no_binary,
471
            excludes=v4_metadata.excludes,
472
            overrides=v4_metadata.overrides,
473
            sources=sources,
474
        )
475

476
    @classmethod
7✔
477
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
478
        instance = cast(PythonLockfileMetadataV5, instance)
1✔
479
        return {
1✔
480
            "sources": sorted(instance.sources),
481
        }
482

483
    def is_valid_for(
7✔
484
        self,
485
        *,
486
        expected_invalidation_digest: str | None,
487
        user_interpreter_constraints: InterpreterConstraints,
488
        interpreter_universe: Iterable[str],
489
        user_requirements: Iterable[PipRequirement],
490
        manylinux: str | None,
491
        requirement_constraints: Iterable[PipRequirement],
492
        only_binary: Iterable[str],
493
        no_binary: Iterable[str],
494
        excludes: Iterable[str],
495
        overrides: Iterable[str],
496
        sources: Iterable[str],
497
        lock_style: str | None,
498
        complete_platforms: Iterable[str],
499
        uploaded_prior_to: str | None,
500
    ) -> LockfileMetadataValidation:
501
        failure_reasons = (
1✔
502
            super()
503
            .is_valid_for(
504
                expected_invalidation_digest=expected_invalidation_digest,
505
                user_interpreter_constraints=user_interpreter_constraints,
506
                interpreter_universe=interpreter_universe,
507
                user_requirements=user_requirements,
508
                manylinux=manylinux,
509
                requirement_constraints=requirement_constraints,
510
                only_binary=only_binary,
511
                no_binary=no_binary,
512
                excludes=excludes,
513
                overrides=overrides,
514
                sources=sources,
515
                lock_style=lock_style,
516
                complete_platforms=complete_platforms,
517
                uploaded_prior_to=uploaded_prior_to,
518
            )
519
            .failure_reasons
520
        )
521

522
        if self.sources != set(sources):
1✔
523
            failure_reasons.add(InvalidPythonLockfileReason.SOURCES_MISMATCH)
1✔
524

525
        return LockfileMetadataValidation(failure_reasons)
1✔
526

527

528
@_python_lockfile_metadata(6)
7✔
529
@dataclass(frozen=True)
7✔
530
class PythonLockfileMetadataV6(PythonLockfileMetadataV5):
7✔
531
    """Lockfile version with lock_style and complete_platforms."""
532

533
    lock_style: str | None
7✔
534
    complete_platforms: Iterable[str]
7✔
535

536
    @classmethod
7✔
537
    def _from_json_dict(
7✔
538
        cls: type[PythonLockfileMetadataV6],
539
        json_dict: dict[Any, Any],
540
        lockfile_description: str,
541
        error_suffix: str,
542
    ) -> PythonLockfileMetadataV6:
543
        v5_metadata = PythonLockfileMetadataV5._from_json_dict(
3✔
544
            json_dict, lockfile_description, error_suffix
545
        )
546
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
3✔
547

548
        lock_style = metadata("lock_style", str, lambda l: l)
3✔
549
        complete_platforms = metadata("complete_platforms", tuple[str, ...], lambda l: tuple(l))
3✔
550

551
        return PythonLockfileMetadataV6(
3✔
552
            valid_for_interpreter_constraints=v5_metadata.valid_for_interpreter_constraints,
553
            requirements=v5_metadata.requirements,
554
            manylinux=v5_metadata.manylinux,
555
            requirement_constraints=v5_metadata.requirement_constraints,
556
            only_binary=v5_metadata.only_binary,
557
            no_binary=v5_metadata.no_binary,
558
            excludes=v5_metadata.excludes,
559
            overrides=v5_metadata.overrides,
560
            sources=v5_metadata.sources,
561
            lock_style=lock_style,
562
            complete_platforms=complete_platforms,
563
        )
564

565
    @classmethod
7✔
566
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
567
        instance = cast(PythonLockfileMetadataV6, instance)
1✔
568
        return {
1✔
569
            "lock_style": instance.lock_style,
570
            "complete_platforms": sorted(instance.complete_platforms),
571
        }
572

573
    def is_valid_for(
7✔
574
        self,
575
        *,
576
        expected_invalidation_digest: str | None,
577
        user_interpreter_constraints: InterpreterConstraints,
578
        interpreter_universe: Iterable[str],
579
        user_requirements: Iterable[PipRequirement],
580
        manylinux: str | None,
581
        requirement_constraints: Iterable[PipRequirement],
582
        only_binary: Iterable[str],
583
        no_binary: Iterable[str],
584
        excludes: Iterable[str],
585
        overrides: Iterable[str],
586
        sources: Iterable[str],
587
        lock_style: str | None,
588
        complete_platforms: Iterable[str],
589
        uploaded_prior_to: str | None,
590
    ) -> LockfileMetadataValidation:
591
        failure_reasons = (
1✔
592
            super()
593
            .is_valid_for(
594
                expected_invalidation_digest=expected_invalidation_digest,
595
                user_interpreter_constraints=user_interpreter_constraints,
596
                interpreter_universe=interpreter_universe,
597
                user_requirements=user_requirements,
598
                manylinux=manylinux,
599
                requirement_constraints=requirement_constraints,
600
                only_binary=only_binary,
601
                no_binary=no_binary,
602
                excludes=excludes,
603
                overrides=overrides,
604
                sources=sources,
605
                lock_style=lock_style,
606
                complete_platforms=complete_platforms,
607
                uploaded_prior_to=uploaded_prior_to,
608
            )
609
            .failure_reasons
610
        )
611

612
        if self.lock_style != lock_style:
1✔
613
            failure_reasons.add(InvalidPythonLockfileReason.LOCK_STYLE_MISMATCH)
×
614
        if self.complete_platforms != complete_platforms:
1✔
615
            failure_reasons.add(InvalidPythonLockfileReason.COMPLETE_PLATFORMS_MISMATCH)
×
616

617
        return LockfileMetadataValidation(failure_reasons)
1✔
618

619

620
@_python_lockfile_metadata(7)
7✔
621
@dataclass(frozen=True)
7✔
622
class PythonLockfileMetadataV7(PythonLockfileMetadataV6):
7✔
623
    """Lockfile version with uploaded_prior_to."""
624

625
    uploaded_prior_to: str | None
7✔
626

627
    @classmethod
7✔
628
    def _from_json_dict(
7✔
629
        cls: type[PythonLockfileMetadataV7],
630
        json_dict: dict[Any, Any],
631
        lockfile_description: str,
632
        error_suffix: str,
633
    ) -> PythonLockfileMetadataV7:
634
        v6_metadata = PythonLockfileMetadataV6._from_json_dict(
1✔
635
            json_dict, lockfile_description, error_suffix
636
        )
637
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
638

639
        uploaded_prior_to: str | None = metadata("uploaded_prior_to", str, lambda l: l)
1✔
640

641
        return PythonLockfileMetadataV7(
1✔
642
            valid_for_interpreter_constraints=v6_metadata.valid_for_interpreter_constraints,
643
            requirements=v6_metadata.requirements,
644
            manylinux=v6_metadata.manylinux,
645
            requirement_constraints=v6_metadata.requirement_constraints,
646
            only_binary=v6_metadata.only_binary,
647
            no_binary=v6_metadata.no_binary,
648
            excludes=v6_metadata.excludes,
649
            overrides=v6_metadata.overrides,
650
            sources=v6_metadata.sources,
651
            lock_style=v6_metadata.lock_style,
652
            complete_platforms=v6_metadata.complete_platforms,
653
            uploaded_prior_to=uploaded_prior_to,
654
        )
655

656
    @classmethod
7✔
657
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
658
        instance = cast(PythonLockfileMetadataV7, instance)
1✔
659
        return {
1✔
660
            "uploaded_prior_to": instance.uploaded_prior_to,
661
        }
662

663
    def is_valid_for(
7✔
664
        self,
665
        *,
666
        expected_invalidation_digest: str | None,
667
        user_interpreter_constraints: InterpreterConstraints,
668
        interpreter_universe: Iterable[str],
669
        user_requirements: Iterable[PipRequirement],
670
        manylinux: str | None,
671
        requirement_constraints: Iterable[PipRequirement],
672
        only_binary: Iterable[str],
673
        no_binary: Iterable[str],
674
        excludes: Iterable[str],
675
        overrides: Iterable[str],
676
        sources: Iterable[str],
677
        lock_style: str | None,
678
        complete_platforms: Iterable[str],
679
        uploaded_prior_to: str | None,
680
    ) -> LockfileMetadataValidation:
681
        failure_reasons = (
1✔
682
            super()
683
            .is_valid_for(
684
                expected_invalidation_digest=expected_invalidation_digest,
685
                user_interpreter_constraints=user_interpreter_constraints,
686
                interpreter_universe=interpreter_universe,
687
                user_requirements=user_requirements,
688
                manylinux=manylinux,
689
                requirement_constraints=requirement_constraints,
690
                only_binary=only_binary,
691
                no_binary=no_binary,
692
                excludes=excludes,
693
                overrides=overrides,
694
                sources=sources,
695
                lock_style=lock_style,
696
                complete_platforms=complete_platforms,
697
                uploaded_prior_to=uploaded_prior_to,
698
            )
699
            .failure_reasons
700
        )
701

702
        if self.uploaded_prior_to != uploaded_prior_to:
1✔
703
            failure_reasons.add(InvalidPythonLockfileReason.UPLOADED_PRIOR_TO_MISMATCH)
1✔
704

705
        return LockfileMetadataValidation(failure_reasons)
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