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

pantsbuild / pants / 24145062409

08 Apr 2026 03:56PM UTC coverage: 52.369% (-40.5%) from 92.91%
24145062409

Pull #23233

github

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

7 of 10 new or added lines in 3 files covered. (70.0%)

23048 existing lines in 605 files now uncovered.

31656 of 60448 relevant lines covered (52.37%)

0.52 hits per line

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

64.65
/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
1✔
5

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

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

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

23

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

39

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

44
    valid_for_interpreter_constraints: InterpreterConstraints
1✔
45

46
    @staticmethod
1✔
47
    def new(
1✔
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

UNCOV
70
        return PythonLockfileMetadataV7(
×
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
1✔
86
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
1✔
87
        return f"{lockfile_location}.metadata"
1✔
88

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

98
    def is_valid_for(
1✔
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)
1✔
123
@dataclass(frozen=True)
1✔
124
class PythonLockfileMetadataV1(PythonLockfileMetadata):
1✔
125
    requirements_invalidation_digest: str
1✔
126

127
    @classmethod
1✔
128
    def _from_json_dict(
1✔
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
1✔
144
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
145
        instance = cast(PythonLockfileMetadataV1, instance)
×
146
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
147

148
    def is_valid_for(
1✔
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:
UNCOV
167
        failure_reasons: set[InvalidPythonLockfileReason] = set()
×
168

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

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

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

UNCOV
180
        return LockfileMetadataValidation(failure_reasons)
×
181

182

183
@_python_lockfile_metadata(2)
1✔
184
@dataclass(frozen=True)
1✔
185
class PythonLockfileMetadataV2(PythonLockfileMetadata):
1✔
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]
1✔
193

194
    @classmethod
1✔
195
    def _from_json_dict(
1✔
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)
1✔
202

203
        requirements = metadata(
1✔
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(
1✔
211
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
212
        )
213

214
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
1✔
215

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

223
    def is_valid_for(
1✔
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:
UNCOV
242
        failure_reasons = set()
×
UNCOV
243
        if not set(user_requirements).issubset(self.requirements):
×
UNCOV
244
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
×
245

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

UNCOV
251
        return LockfileMetadataValidation(failure_reasons)
×
252

253

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

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

264
    @classmethod
1✔
265
    def _from_json_dict(
1✔
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)
1✔
272
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
273
        manylinux = metadata("manylinux", str, lambda l: l)
1✔
274
        requirement_constraints = metadata(
1✔
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))
1✔
282
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
1✔
283

284
        return PythonLockfileMetadataV3(
1✔
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
1✔
294
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
295
        instance = cast(PythonLockfileMetadataV3, instance)
×
UNCOV
296
        return {
×
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(
1✔
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:
UNCOV
322
        failure_reasons = (
×
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

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

UNCOV
352
        return LockfileMetadataValidation(failure_reasons)
×
353

354

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

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

363
    @classmethod
1✔
364
    def _from_json_dict(
1✔
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)
1✔
371
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
372

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

376
        return PythonLockfileMetadataV4(
1✔
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
1✔
388
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
389
        instance = cast(PythonLockfileMetadataV4, instance)
×
UNCOV
390
        return {
×
391
            "excludes": sorted(instance.excludes),
392
            "overrides": sorted(instance.overrides),
393
        }
394

395
    def is_valid_for(
1✔
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:
UNCOV
414
        failure_reasons = (
×
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

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

UNCOV
440
        return LockfileMetadataValidation(failure_reasons)
×
441

442

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

448
    sources: set[str]
1✔
449

450
    @classmethod
1✔
451
    def _from_json_dict(
1✔
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(
1✔
458
            json_dict, lockfile_description, error_suffix
459
        )
460
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
461

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

464
        return PythonLockfileMetadataV5(
1✔
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
1✔
477
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
478
        instance = cast(PythonLockfileMetadataV5, instance)
×
UNCOV
479
        return {
×
480
            "sources": sorted(instance.sources),
481
        }
482

483
    def is_valid_for(
1✔
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:
UNCOV
501
        failure_reasons = (
×
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

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

UNCOV
525
        return LockfileMetadataValidation(failure_reasons)
×
526

527

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

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

536
    @classmethod
1✔
537
    def _from_json_dict(
1✔
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(
1✔
544
            json_dict, lockfile_description, error_suffix
545
        )
546
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
547

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

551
        return PythonLockfileMetadataV6(
1✔
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
1✔
566
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
567
        instance = cast(PythonLockfileMetadataV6, instance)
×
UNCOV
568
        return {
×
569
            "lock_style": instance.lock_style,
570
            "complete_platforms": sorted(instance.complete_platforms),
571
        }
572

573
    def is_valid_for(
1✔
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:
UNCOV
591
        failure_reasons = (
×
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

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

UNCOV
617
        return LockfileMetadataValidation(failure_reasons)
×
618

619

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

625
    uploaded_prior_to: str | None
1✔
626

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

UNCOV
639
        uploaded_prior_to: str | None = metadata("uploaded_prior_to", str, lambda l: l)
×
640

UNCOV
641
        return PythonLockfileMetadataV7(
×
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
1✔
657
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
658
        instance = cast(PythonLockfileMetadataV7, instance)
×
UNCOV
659
        return {
×
660
            "uploaded_prior_to": instance.uploaded_prior_to,
661
        }
662

663
    def is_valid_for(
1✔
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:
UNCOV
681
        failure_reasons = (
×
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

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

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