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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

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

52.63
/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
3✔
5

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

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

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

23

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

35

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

40
    valid_for_interpreter_constraints: InterpreterConstraints
3✔
41

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

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

62
        return PythonLockfileMetadataV4(
×
63
            valid_for_interpreter_constraints,
64
            requirements,
65
            manylinux=manylinux,
66
            requirement_constraints=requirement_constraints,
67
            only_binary=only_binary,
68
            no_binary=no_binary,
69
            excludes=excludes,
70
            overrides=overrides,
71
        )
72

73
    @staticmethod
3✔
74
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
3✔
75
        return f"{lockfile_location}.metadata"
×
76

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

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

103
        raise NotImplementedError("call `is_valid_for` on subclasses only")
×
104

105

106
@_python_lockfile_metadata(1)
3✔
107
@dataclass(frozen=True)
3✔
108
class PythonLockfileMetadataV1(PythonLockfileMetadata):
3✔
109
    requirements_invalidation_digest: str
3✔
110

111
    @classmethod
3✔
112
    def _from_json_dict(
3✔
113
        cls: type[PythonLockfileMetadataV1],
114
        json_dict: dict[Any, Any],
115
        lockfile_description: str,
116
        error_suffix: str,
117
    ) -> PythonLockfileMetadataV1:
118
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
119

120
        interpreter_constraints = metadata(
×
121
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
122
        )
123
        requirements_digest = metadata("requirements_invalidation_digest", str, None)
×
124

125
        return PythonLockfileMetadataV1(interpreter_constraints, requirements_digest)
×
126

127
    @classmethod
3✔
128
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
3✔
129
        instance = cast(PythonLockfileMetadataV1, instance)
×
130
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
131

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

149
        if expected_invalidation_digest is None:
×
150
            return LockfileMetadataValidation(failure_reasons)
×
151

152
        if self.requirements_invalidation_digest != expected_invalidation_digest:
×
153
            failure_reasons.add(InvalidPythonLockfileReason.INVALIDATION_DIGEST_MISMATCH)
×
154

155
        if not self.valid_for_interpreter_constraints.contains(
×
156
            user_interpreter_constraints, interpreter_universe
157
        ):
158
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
×
159

160
        return LockfileMetadataValidation(failure_reasons)
×
161

162

163
@_python_lockfile_metadata(2)
3✔
164
@dataclass(frozen=True)
3✔
165
class PythonLockfileMetadataV2(PythonLockfileMetadata):
3✔
166
    """Lockfile version that permits specifying a requirements as a set rather than a digest.
167

168
    Validity is tested by the set of requirements strings being the same in the user requirements as
169
    those in the stored requirements.
170
    """
171

172
    requirements: set[PipRequirement]
3✔
173

174
    @classmethod
3✔
175
    def _from_json_dict(
3✔
176
        cls: type[PythonLockfileMetadataV2],
177
        json_dict: dict[Any, Any],
178
        lockfile_description: str,
179
        error_suffix: str,
180
    ) -> PythonLockfileMetadataV2:
181
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
182

183
        requirements = metadata(
×
184
            "generated_with_requirements",
185
            set[PipRequirement],
186
            lambda l: {
187
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
188
            },
189
        )
190
        interpreter_constraints = metadata(
×
191
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
192
        )
193

194
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
×
195

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

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

222
        if not self.valid_for_interpreter_constraints.contains(
×
223
            user_interpreter_constraints, interpreter_universe
224
        ):
225
            failure_reasons.add(InvalidPythonLockfileReason.INTERPRETER_CONSTRAINTS_MISMATCH)
×
226

227
        return LockfileMetadataValidation(failure_reasons)
×
228

229

230
@_python_lockfile_metadata(3)
3✔
231
@dataclass(frozen=True)
3✔
232
class PythonLockfileMetadataV3(PythonLockfileMetadataV2):
3✔
233
    """Lockfile version that considers constraints files."""
234

235
    manylinux: str | None
3✔
236
    requirement_constraints: set[PipRequirement]
3✔
237
    only_binary: set[str]
3✔
238
    no_binary: set[str]
3✔
239

240
    @classmethod
3✔
241
    def _from_json_dict(
3✔
242
        cls: type[PythonLockfileMetadataV3],
243
        json_dict: dict[Any, Any],
244
        lockfile_description: str,
245
        error_suffix: str,
246
    ) -> PythonLockfileMetadataV3:
247
        v2_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
×
248
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
249
        manylinux = metadata("manylinux", str, lambda l: l)
×
250
        requirement_constraints = metadata(
×
251
            "requirement_constraints",
252
            set[PipRequirement],
253
            lambda l: {
254
                PipRequirement.parse(i, description_of_origin=lockfile_description) for i in l
255
            },
256
        )
257
        only_binary = metadata("only_binary", set[str], lambda l: set(l))
×
258
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
×
259

260
        return PythonLockfileMetadataV3(
×
261
            valid_for_interpreter_constraints=v2_metadata.valid_for_interpreter_constraints,
262
            requirements=v2_metadata.requirements,
263
            manylinux=manylinux,
264
            requirement_constraints=requirement_constraints,
265
            only_binary=only_binary,
266
            no_binary=no_binary,
267
        )
268

269
    @classmethod
3✔
270
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
3✔
271
        instance = cast(PythonLockfileMetadataV3, instance)
×
272
        return {
×
273
            "manylinux": instance.manylinux,
274
            "requirement_constraints": sorted(str(i) for i in instance.requirement_constraints),
275
            "only_binary": sorted(instance.only_binary),
276
            "no_binary": sorted(instance.no_binary),
277
        }
278

279
    def is_valid_for(
3✔
280
        self,
281
        *,
282
        expected_invalidation_digest: str | None,  # Validation digests are not used by V2.
283
        user_interpreter_constraints: InterpreterConstraints,
284
        interpreter_universe: Iterable[str],
285
        user_requirements: Iterable[PipRequirement],
286
        manylinux: str | None,
287
        requirement_constraints: Iterable[PipRequirement],
288
        only_binary: Iterable[str],
289
        no_binary: Iterable[str],
290
        # not used for V3
291
        excludes: Iterable[str],
292
        overrides: Iterable[str],
293
    ) -> LockfileMetadataValidation:
294
        failure_reasons = (
×
295
            super()
296
            .is_valid_for(
297
                expected_invalidation_digest=expected_invalidation_digest,
298
                user_interpreter_constraints=user_interpreter_constraints,
299
                interpreter_universe=interpreter_universe,
300
                user_requirements=user_requirements,
301
                manylinux=manylinux,
302
                requirement_constraints=requirement_constraints,
303
                only_binary=only_binary,
304
                no_binary=no_binary,
305
                excludes=excludes,
306
                overrides=overrides,
307
            )
308
            .failure_reasons
309
        )
310

311
        if self.manylinux != manylinux:
×
312
            failure_reasons.add(InvalidPythonLockfileReason.MANYLINUX_MISMATCH)
×
313
        if self.requirement_constraints != set(requirement_constraints):
×
314
            failure_reasons.add(InvalidPythonLockfileReason.CONSTRAINTS_FILE_MISMATCH)
×
315
        if self.only_binary != set(only_binary):
×
316
            failure_reasons.add(InvalidPythonLockfileReason.ONLY_BINARY_MISMATCH)
×
317
        if self.no_binary != set(no_binary):
×
318
            failure_reasons.add(InvalidPythonLockfileReason.NO_BINARY_MISMATCH)
×
319

320
        return LockfileMetadataValidation(failure_reasons)
×
321

322

323
@_python_lockfile_metadata(4)
3✔
324
@dataclass(frozen=True)
3✔
325
class PythonLockfileMetadataV4(PythonLockfileMetadataV3):
3✔
326
    """Lockfile version with excludes/overrides."""
327

328
    excludes: set[str]
3✔
329
    overrides: set[str]
3✔
330

331
    @classmethod
3✔
332
    def _from_json_dict(
3✔
333
        cls: type[PythonLockfileMetadataV4],
334
        json_dict: dict[Any, Any],
335
        lockfile_description: str,
336
        error_suffix: str,
337
    ) -> PythonLockfileMetadataV4:
338
        v3_metadata = super()._from_json_dict(json_dict, lockfile_description, error_suffix)
×
339
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
×
340

341
        excludes = metadata("excludes", set[str], lambda l: set(l))
×
342
        overrides = metadata("overrides", set[str], lambda l: set(l))
×
343

344
        return PythonLockfileMetadataV4(
×
345
            valid_for_interpreter_constraints=v3_metadata.valid_for_interpreter_constraints,
346
            requirements=v3_metadata.requirements,
347
            manylinux=v3_metadata.manylinux,
348
            requirement_constraints=v3_metadata.requirement_constraints,
349
            only_binary=v3_metadata.only_binary,
350
            no_binary=v3_metadata.no_binary,
351
            excludes=excludes,
352
            overrides=overrides,
353
        )
354

355
    @classmethod
3✔
356
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
3✔
357
        instance = cast(PythonLockfileMetadataV4, instance)
×
358
        return {
×
359
            "excludes": sorted(instance.excludes),
360
            "overrides": sorted(instance.overrides),
361
        }
362

363
    def is_valid_for(
3✔
364
        self,
365
        *,
366
        expected_invalidation_digest: str | None,
367
        user_interpreter_constraints: InterpreterConstraints,
368
        interpreter_universe: Iterable[str],
369
        user_requirements: Iterable[PipRequirement],
370
        manylinux: str | None,
371
        requirement_constraints: Iterable[PipRequirement],
372
        only_binary: Iterable[str],
373
        no_binary: Iterable[str],
374
        excludes: Iterable[str],
375
        overrides: Iterable[str],
376
    ) -> LockfileMetadataValidation:
377
        failure_reasons = (
×
378
            super()
379
            .is_valid_for(
380
                expected_invalidation_digest=expected_invalidation_digest,
381
                user_interpreter_constraints=user_interpreter_constraints,
382
                interpreter_universe=interpreter_universe,
383
                user_requirements=user_requirements,
384
                manylinux=manylinux,
385
                requirement_constraints=requirement_constraints,
386
                only_binary=only_binary,
387
                no_binary=no_binary,
388
                excludes=excludes,
389
                overrides=overrides,
390
            )
391
            .failure_reasons
392
        )
393

394
        if self.excludes != set(excludes):
×
395
            failure_reasons.add(InvalidPythonLockfileReason.EXCLUDES_MISMATCH)
×
396
        if self.overrides != set(overrides):
×
397
            failure_reasons.add(InvalidPythonLockfileReason.OVERRIDES_MISMATCH)
×
398

399
        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