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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

52.29
/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

36

37
@dataclass(frozen=True)
1✔
38
class PythonLockfileMetadata(LockfileMetadata):
1✔
39
    scope = LockfileScope.PYTHON
1✔
40

41
    valid_for_interpreter_constraints: InterpreterConstraints
1✔
42

43
    @staticmethod
1✔
44
    def new(
1✔
45
        *,
46
        valid_for_interpreter_constraints: InterpreterConstraints,
47
        requirements: set[PipRequirement],
48
        manylinux: str | None,
49
        requirement_constraints: set[PipRequirement],
50
        only_binary: set[str],
51
        no_binary: set[str],
52
        excludes: set[str],
53
        overrides: set[str],
54
        sources: set[str],
55
    ) -> PythonLockfileMetadata:
56
        """Call the most recent version of the `LockfileMetadata` class to construct a concrete
57
        instance.
58

59
        This static method should be used in place of the `LockfileMetadata` constructor. This gives
60
        calling sites a predictable method to call to construct a new `LockfileMetadata` for
61
        writing, while still allowing us to support _reading_ older, deprecated metadata versions.
62
        """
63

UNCOV
64
        return PythonLockfileMetadataV5(
×
65
            valid_for_interpreter_constraints,
66
            requirements,
67
            manylinux=manylinux,
68
            requirement_constraints=requirement_constraints,
69
            only_binary=only_binary,
70
            no_binary=no_binary,
71
            excludes=excludes,
72
            overrides=overrides,
73
            sources=sources,
74
        )
75

76
    @staticmethod
1✔
77
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
1✔
78
        return f"{lockfile_location}.metadata"
×
79

80
    @classmethod
1✔
81
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
82
        instance = cast(PythonLockfileMetadata, instance)
×
UNCOV
83
        return {
×
84
            "valid_for_interpreter_constraints": [
85
                str(ic) for ic in instance.valid_for_interpreter_constraints
86
            ]
87
        }
88

89
    def is_valid_for(
1✔
90
        self,
91
        *,
92
        expected_invalidation_digest: str | None,
93
        user_interpreter_constraints: InterpreterConstraints,
94
        interpreter_universe: Iterable[str],
95
        user_requirements: Iterable[PipRequirement],
96
        manylinux: str | None,
97
        requirement_constraints: Iterable[PipRequirement],
98
        only_binary: Iterable[str],
99
        no_binary: Iterable[str],
100
        excludes: Iterable[str],
101
        overrides: Iterable[str],
102
        sources: Iterable[str],
103
    ) -> LockfileMetadataValidation:
104
        """Returns Truthy if this `PythonLockfileMetadata` can be used in the current execution
105
        context."""
106

107
        raise NotImplementedError("call `is_valid_for` on subclasses only")
×
108

109

110
@_python_lockfile_metadata(1)
1✔
111
@dataclass(frozen=True)
1✔
112
class PythonLockfileMetadataV1(PythonLockfileMetadata):
1✔
113
    requirements_invalidation_digest: str
1✔
114

115
    @classmethod
1✔
116
    def _from_json_dict(
1✔
117
        cls: type[PythonLockfileMetadataV1],
118
        json_dict: dict[Any, Any],
119
        lockfile_description: str,
120
        error_suffix: str,
121
    ) -> PythonLockfileMetadataV1:
UNCOV
122
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
123

UNCOV
124
        interpreter_constraints = metadata(
×
125
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
126
        )
UNCOV
127
        requirements_digest = metadata("requirements_invalidation_digest", str, None)
×
128

UNCOV
129
        return PythonLockfileMetadataV1(interpreter_constraints, requirements_digest)
×
130

131
    @classmethod
1✔
132
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
133
        instance = cast(PythonLockfileMetadataV1, instance)
×
134
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
135

136
    def is_valid_for(
1✔
137
        self,
138
        *,
139
        expected_invalidation_digest: str | None,
140
        user_interpreter_constraints: InterpreterConstraints,
141
        interpreter_universe: Iterable[str],
142
        # Everything below is not used by v1.
143
        user_requirements: Iterable[PipRequirement],
144
        manylinux: str | None,
145
        requirement_constraints: Iterable[PipRequirement],
146
        only_binary: Iterable[str],
147
        no_binary: Iterable[str],
148
        excludes: Iterable[str],
149
        overrides: Iterable[str],
150
        sources: Iterable[str],
151
    ) -> LockfileMetadataValidation:
UNCOV
152
        failure_reasons: set[InvalidPythonLockfileReason] = set()
×
153

UNCOV
154
        if expected_invalidation_digest is None:
×
155
            return LockfileMetadataValidation(failure_reasons)
×
156

UNCOV
157
        if self.requirements_invalidation_digest != expected_invalidation_digest:
×
UNCOV
158
            failure_reasons.add(InvalidPythonLockfileReason.INVALIDATION_DIGEST_MISMATCH)
×
159

UNCOV
160
        if not self.valid_for_interpreter_constraints.contains(
×
161
            user_interpreter_constraints, interpreter_universe
162
        ):
UNCOV
163
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
×
164

UNCOV
165
        return LockfileMetadataValidation(failure_reasons)
×
166

167

168
@_python_lockfile_metadata(2)
1✔
169
@dataclass(frozen=True)
1✔
170
class PythonLockfileMetadataV2(PythonLockfileMetadata):
1✔
171
    """Lockfile version that permits specifying a requirements as a set rather than a digest.
172

173
    Validity is tested by the set of requirements strings being the same in the user requirements as
174
    those in the stored requirements.
175
    """
176

177
    requirements: set[PipRequirement]
1✔
178

179
    @classmethod
1✔
180
    def _from_json_dict(
1✔
181
        cls: type[PythonLockfileMetadataV2],
182
        json_dict: dict[Any, Any],
183
        lockfile_description: str,
184
        error_suffix: str,
185
    ) -> PythonLockfileMetadataV2:
UNCOV
186
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
187

UNCOV
188
        requirements = metadata(
×
189
            "generated_with_requirements",
190
            set[PipRequirement],
191
            lambda l: {
192
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
193
            },
194
        )
UNCOV
195
        interpreter_constraints = metadata(
×
196
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
197
        )
198

UNCOV
199
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
×
200

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

208
    def is_valid_for(
1✔
209
        self,
210
        *,
211
        expected_invalidation_digest: str | None,  # Not used by V2.
212
        user_interpreter_constraints: InterpreterConstraints,
213
        interpreter_universe: Iterable[str],
214
        user_requirements: Iterable[PipRequirement],
215
        # Everything below is not used by V2.
216
        manylinux: str | None,
217
        requirement_constraints: Iterable[PipRequirement],
218
        only_binary: Iterable[str],
219
        no_binary: Iterable[str],
220
        excludes: Iterable[str],
221
        overrides: Iterable[str],
222
        sources: Iterable[str],
223
    ) -> LockfileMetadataValidation:
UNCOV
224
        failure_reasons = set()
×
UNCOV
225
        if not set(user_requirements).issubset(self.requirements):
×
UNCOV
226
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
×
227

UNCOV
228
        if not self.valid_for_interpreter_constraints.contains(
×
229
            user_interpreter_constraints, interpreter_universe
230
        ):
UNCOV
231
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
×
232

UNCOV
233
        return LockfileMetadataValidation(failure_reasons)
×
234

235

236
@_python_lockfile_metadata(3)
1✔
237
@dataclass(frozen=True)
1✔
238
class PythonLockfileMetadataV3(PythonLockfileMetadataV2):
1✔
239
    """Lockfile version that considers constraints files."""
240

241
    manylinux: str | None
1✔
242
    requirement_constraints: set[PipRequirement]
1✔
243
    only_binary: set[str]
1✔
244
    no_binary: set[str]
1✔
245

246
    @classmethod
1✔
247
    def _from_json_dict(
1✔
248
        cls: type[PythonLockfileMetadataV3],
249
        json_dict: dict[Any, Any],
250
        lockfile_description: str,
251
        error_suffix: str,
252
    ) -> PythonLockfileMetadataV3:
UNCOV
253
        v2_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
×
UNCOV
254
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
UNCOV
255
        manylinux = metadata("manylinux", str, lambda l: l)
×
UNCOV
256
        requirement_constraints = metadata(
×
257
            "requirement_constraints",
258
            set[PipRequirement],
259
            lambda l: {
260
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
261
            },
262
        )
UNCOV
263
        only_binary = metadata("only_binary", set[str], lambda l: set(l))
×
UNCOV
264
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
×
265

UNCOV
266
        return PythonLockfileMetadataV3(
×
267
            valid_for_interpreter_constraints=v2_metadata.valid_for_interpreter_constraints,
268
            requirements=v2_metadata.requirements,
269
            manylinux=manylinux,
270
            requirement_constraints=requirement_constraints,
271
            only_binary=only_binary,
272
            no_binary=no_binary,
273
        )
274

275
    @classmethod
1✔
276
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
277
        instance = cast(PythonLockfileMetadataV3, instance)
×
UNCOV
278
        return {
×
279
            "manylinux": instance.manylinux,
280
            "requirement_constraints": sorted(str(i) for i in instance.requirement_constraints),
281
            "only_binary": sorted(instance.only_binary),
282
            "no_binary": sorted(instance.no_binary),
283
        }
284

285
    def is_valid_for(
1✔
286
        self,
287
        *,
288
        expected_invalidation_digest: str | None,  # Validation digests are not used by V2.
289
        user_interpreter_constraints: InterpreterConstraints,
290
        interpreter_universe: Iterable[str],
291
        user_requirements: Iterable[PipRequirement],
292
        manylinux: str | None,
293
        requirement_constraints: Iterable[PipRequirement],
294
        only_binary: Iterable[str],
295
        no_binary: Iterable[str],
296
        # not used for V3
297
        excludes: Iterable[str],
298
        overrides: Iterable[str],
299
        sources: Iterable[str],
300
    ) -> LockfileMetadataValidation:
UNCOV
301
        failure_reasons = (
×
302
            super()
303
            .is_valid_for(
304
                expected_invalidation_digest=expected_invalidation_digest,
305
                user_interpreter_constraints=user_interpreter_constraints,
306
                interpreter_universe=interpreter_universe,
307
                user_requirements=user_requirements,
308
                manylinux=manylinux,
309
                requirement_constraints=requirement_constraints,
310
                only_binary=only_binary,
311
                no_binary=no_binary,
312
                excludes=excludes,
313
                overrides=overrides,
314
                sources=sources,
315
            )
316
            .failure_reasons
317
        )
318

UNCOV
319
        if self.manylinux != manylinux:
×
UNCOV
320
            failure_reasons.add(InvalidPythonLockfileReason.MANYLINUX_MISMATCH)
×
UNCOV
321
        if self.requirement_constraints != set(requirement_constraints):
×
UNCOV
322
            failure_reasons.add(InvalidPythonLockfileReason.CONSTRAINTS_FILE_MISMATCH)
×
UNCOV
323
        if self.only_binary != set(only_binary):
×
UNCOV
324
            failure_reasons.add(InvalidPythonLockfileReason.ONLY_BINARY_MISMATCH)
×
UNCOV
325
        if self.no_binary != set(no_binary):
×
UNCOV
326
            failure_reasons.add(InvalidPythonLockfileReason.NO_BINARY_MISMATCH)
×
327

UNCOV
328
        return LockfileMetadataValidation(failure_reasons)
×
329

330

331
@_python_lockfile_metadata(4)
1✔
332
@dataclass(frozen=True)
1✔
333
class PythonLockfileMetadataV4(PythonLockfileMetadataV3):
1✔
334
    """Lockfile version with excludes/overrides."""
335

336
    excludes: set[str]
1✔
337
    overrides: set[str]
1✔
338

339
    @classmethod
1✔
340
    def _from_json_dict(
1✔
341
        cls: type[PythonLockfileMetadataV4],
342
        json_dict: dict[Any, Any],
343
        lockfile_description: str,
344
        error_suffix: str,
345
    ) -> PythonLockfileMetadataV4:
UNCOV
346
        v3_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
×
UNCOV
347
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
348

UNCOV
349
        excludes = metadata("excludes", set[str], lambda l: set(l))
×
UNCOV
350
        overrides = metadata("overrides", set[str], lambda l: set(l))
×
351

UNCOV
352
        return PythonLockfileMetadataV4(
×
353
            valid_for_interpreter_constraints=v3_metadata.valid_for_interpreter_constraints,
354
            requirements=v3_metadata.requirements,
355
            manylinux=v3_metadata.manylinux,
356
            requirement_constraints=v3_metadata.requirement_constraints,
357
            only_binary=v3_metadata.only_binary,
358
            no_binary=v3_metadata.no_binary,
359
            excludes=excludes,
360
            overrides=overrides,
361
        )
362

363
    @classmethod
1✔
364
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
365
        instance = cast(PythonLockfileMetadataV4, instance)
×
UNCOV
366
        return {
×
367
            "excludes": sorted(instance.excludes),
368
            "overrides": sorted(instance.overrides),
369
        }
370

371
    def is_valid_for(
1✔
372
        self,
373
        *,
374
        expected_invalidation_digest: str | None,
375
        user_interpreter_constraints: InterpreterConstraints,
376
        interpreter_universe: Iterable[str],
377
        user_requirements: Iterable[PipRequirement],
378
        manylinux: str | None,
379
        requirement_constraints: Iterable[PipRequirement],
380
        only_binary: Iterable[str],
381
        no_binary: Iterable[str],
382
        excludes: Iterable[str],
383
        overrides: Iterable[str],
384
        # not used for V4
385
        sources: Iterable[str],
386
    ) -> LockfileMetadataValidation:
UNCOV
387
        failure_reasons = (
×
388
            super()
389
            .is_valid_for(
390
                expected_invalidation_digest=expected_invalidation_digest,
391
                user_interpreter_constraints=user_interpreter_constraints,
392
                interpreter_universe=interpreter_universe,
393
                user_requirements=user_requirements,
394
                manylinux=manylinux,
395
                requirement_constraints=requirement_constraints,
396
                only_binary=only_binary,
397
                no_binary=no_binary,
398
                excludes=excludes,
399
                overrides=overrides,
400
                sources=sources,
401
            )
402
            .failure_reasons
403
        )
404

UNCOV
405
        if self.excludes != set(excludes):
×
UNCOV
406
            failure_reasons.add(InvalidPythonLockfileReason.EXCLUDES_MISMATCH)
×
UNCOV
407
        if self.overrides != set(overrides):
×
UNCOV
408
            failure_reasons.add(InvalidPythonLockfileReason.OVERRIDES_MISMATCH)
×
409

UNCOV
410
        return LockfileMetadataValidation(failure_reasons)
×
411

412

413
@_python_lockfile_metadata(5)
1✔
414
@dataclass(frozen=True)
1✔
415
class PythonLockfileMetadataV5(PythonLockfileMetadataV4):
1✔
416
    """Lockfile version with sources."""
417

418
    sources: set[str]
1✔
419

420
    @classmethod
1✔
421
    def _from_json_dict(
1✔
422
        cls: type[PythonLockfileMetadataV5],
423
        json_dict: dict[Any, Any],
424
        lockfile_description: str,
425
        error_suffix: str,
426
    ) -> PythonLockfileMetadataV5:
UNCOV
427
        v4_metadata = PythonLockfileMetadataV4._from_json_dict(
×
428
            json_dict, lockfile_description, error_suffix
429
        )
UNCOV
430
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
431

UNCOV
432
        sources = metadata("sources", set[str], lambda l: set(l))
×
433

UNCOV
434
        return PythonLockfileMetadataV5(
×
435
            valid_for_interpreter_constraints=v4_metadata.valid_for_interpreter_constraints,
436
            requirements=v4_metadata.requirements,
437
            manylinux=v4_metadata.manylinux,
438
            requirement_constraints=v4_metadata.requirement_constraints,
439
            only_binary=v4_metadata.only_binary,
440
            no_binary=v4_metadata.no_binary,
441
            excludes=v4_metadata.excludes,
442
            overrides=v4_metadata.overrides,
443
            sources=sources,
444
        )
445

446
    @classmethod
1✔
447
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
1✔
UNCOV
448
        instance = cast(PythonLockfileMetadataV5, instance)
×
UNCOV
449
        return {
×
450
            "sources": sorted(instance.sources),
451
        }
452

453
    def is_valid_for(
1✔
454
        self,
455
        *,
456
        expected_invalidation_digest: str | None,
457
        user_interpreter_constraints: InterpreterConstraints,
458
        interpreter_universe: Iterable[str],
459
        user_requirements: Iterable[PipRequirement],
460
        manylinux: str | None,
461
        requirement_constraints: Iterable[PipRequirement],
462
        only_binary: Iterable[str],
463
        no_binary: Iterable[str],
464
        excludes: Iterable[str],
465
        overrides: Iterable[str],
466
        sources: Iterable[str],
467
    ) -> LockfileMetadataValidation:
UNCOV
468
        failure_reasons = (
×
469
            super()
470
            .is_valid_for(
471
                expected_invalidation_digest=expected_invalidation_digest,
472
                user_interpreter_constraints=user_interpreter_constraints,
473
                interpreter_universe=interpreter_universe,
474
                user_requirements=user_requirements,
475
                manylinux=manylinux,
476
                requirement_constraints=requirement_constraints,
477
                only_binary=only_binary,
478
                no_binary=no_binary,
479
                excludes=excludes,
480
                overrides=overrides,
481
                sources=sources,
482
            )
483
            .failure_reasons
484
        )
485

UNCOV
486
        if self.sources != set(sources):
×
UNCOV
487
            failure_reasons.add(InvalidPythonLockfileReason.SOURCES_MISMATCH)
×
488

UNCOV
489
        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

© 2025 Coveralls, Inc