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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

94.12
/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
11✔
5

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

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

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

23

24
class InvalidPythonLockfileReason(Enum):
11✔
25
    INVALIDATION_DIGEST_MISMATCH = "invalidation_digest_mismatch"
11✔
26
    INTERPRETER_CONSTRAINTS_MISMATCH = "interpreter_constraints_mismatch"
11✔
27
    REQUIREMENTS_MISMATCH = "requirements_mismatch"
11✔
28
    MANYLINUX_MISMATCH = "manylinux_mismatch"
11✔
29
    CONSTRAINTS_FILE_MISMATCH = "constraints_file_mismatch"
11✔
30
    ONLY_BINARY_MISMATCH = "only_binary_mismatch"
11✔
31
    NO_BINARY_MISMATCH = "no_binary_mismatch"
11✔
32
    EXCLUDES_MISMATCH = "excludes_mismatch"
11✔
33
    OVERRIDES_MISMATCH = "overrides_mismatch"
11✔
34
    SOURCES_MISMATCH = "sources_mismatch"
11✔
35

36

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

41
    valid_for_interpreter_constraints: InterpreterConstraints
11✔
42

43
    @staticmethod
11✔
44
    def new(
11✔
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

64
        return PythonLockfileMetadataV5(
2✔
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
11✔
77
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
11✔
78
        return f"{lockfile_location}.metadata"
×
79

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

89
    def is_valid_for(
11✔
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)
11✔
111
@dataclass(frozen=True)
11✔
112
class PythonLockfileMetadataV1(PythonLockfileMetadata):
11✔
113
    requirements_invalidation_digest: str
11✔
114

115
    @classmethod
11✔
116
    def _from_json_dict(
11✔
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
11✔
132
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
11✔
133
        instance = cast(PythonLockfileMetadataV1, instance)
×
134
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
135

136
    def is_valid_for(
11✔
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:
152
        failure_reasons: set[InvalidPythonLockfileReason] = set()
1✔
153

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

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

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

165
        return LockfileMetadataValidation(failure_reasons)
1✔
166

167

168
@_python_lockfile_metadata(2)
11✔
169
@dataclass(frozen=True)
11✔
170
class PythonLockfileMetadataV2(PythonLockfileMetadata):
11✔
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]
11✔
178

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

188
        requirements = metadata(
1✔
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
        )
195
        interpreter_constraints = metadata(
1✔
196
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
197
        )
198

199
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
1✔
200

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

208
    def is_valid_for(
11✔
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:
224
        failure_reasons = set()
1✔
225
        if not set(user_requirements).issubset(self.requirements):
1✔
226
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
1✔
227

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

233
        return LockfileMetadataValidation(failure_reasons)
1✔
234

235

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

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

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

266
        return PythonLockfileMetadataV3(
1✔
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
11✔
276
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
11✔
277
        instance = cast(PythonLockfileMetadataV3, instance)
2✔
278
        return {
2✔
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(
11✔
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:
301
        failure_reasons = (
1✔
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

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

328
        return LockfileMetadataValidation(failure_reasons)
1✔
329

330

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

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

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

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

352
        return PythonLockfileMetadataV4(
1✔
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
11✔
364
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
11✔
365
        instance = cast(PythonLockfileMetadataV4, instance)
2✔
366
        return {
2✔
367
            "excludes": sorted(instance.excludes),
368
            "overrides": sorted(instance.overrides),
369
        }
370

371
    def is_valid_for(
11✔
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:
387
        failure_reasons = (
1✔
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

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

410
        return LockfileMetadataValidation(failure_reasons)
1✔
411

412

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

418
    sources: set[str]
11✔
419

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

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

434
        return PythonLockfileMetadataV5(
1✔
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
11✔
447
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
11✔
448
        instance = cast(PythonLockfileMetadataV5, instance)
2✔
449
        return {
2✔
450
            "sources": sorted(instance.sources),
451
        }
452

453
    def is_valid_for(
11✔
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:
468
        failure_reasons = (
1✔
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

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

489
        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

© 2025 Coveralls, Inc