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

pantsbuild / pants / 18517631058

15 Oct 2025 04:18AM UTC coverage: 69.207% (-11.1%) from 80.267%
18517631058

Pull #22745

github

web-flow
Merge 642a76ca1 into 99919310e
Pull Request #22745: [windows] Add windows support in the stdio crate.

53815 of 77759 relevant lines covered (69.21%)

2.42 hits per line

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

93.23
/src/python/pants/backend/python/util_rules/lockfile_metadata.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
7✔
5

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

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

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

23

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

35

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

40
    valid_for_interpreter_constraints: InterpreterConstraints
7✔
41

42
    @staticmethod
7✔
43
    def new(
7✔
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(
2✔
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
7✔
74
    def metadata_location_for_lockfile(lockfile_location: str) -> str:
7✔
75
        return f"{lockfile_location}.metadata"
×
76

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

86
    def is_valid_for(
7✔
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)
7✔
107
@dataclass(frozen=True)
7✔
108
class PythonLockfileMetadataV1(PythonLockfileMetadata):
7✔
109
    requirements_invalidation_digest: str
7✔
110

111
    @classmethod
7✔
112
    def _from_json_dict(
7✔
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
7✔
128
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
129
        instance = cast(PythonLockfileMetadataV1, instance)
×
130
        return {"requirements_invalidation_digest": instance.requirements_invalidation_digest}
×
131

132
    def is_valid_for(
7✔
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()
1✔
148

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

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

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

160
        return LockfileMetadataValidation(failure_reasons)
1✔
161

162

163
@_python_lockfile_metadata(2)
7✔
164
@dataclass(frozen=True)
7✔
165
class PythonLockfileMetadataV2(PythonLockfileMetadata):
7✔
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]
7✔
173

174
    @classmethod
7✔
175
    def _from_json_dict(
7✔
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)
1✔
182

183
        requirements = metadata(
1✔
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(
1✔
191
            "valid_for_interpreter_constraints", InterpreterConstraints, InterpreterConstraints
192
        )
193

194
        return PythonLockfileMetadataV2(interpreter_constraints, requirements)
1✔
195

196
    @classmethod
7✔
197
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
198
        instance = cast(PythonLockfileMetadataV2, instance)
2✔
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))}
2✔
202

203
    def is_valid_for(
7✔
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()
1✔
219
        if not set(user_requirements).issubset(self.requirements):
1✔
220
            failure_reasons.add(InvalidPythonLockfileReason.REQUIREMENTS_MISMATCH)
1✔
221

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

227
        return LockfileMetadataValidation(failure_reasons)
1✔
228

229

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

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

240
    @classmethod
7✔
241
    def _from_json_dict(
7✔
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)
1✔
248
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
249
        manylinux = metadata("manylinux", str, lambda l: l)
1✔
250
        requirement_constraints = metadata(
1✔
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))
1✔
258
        no_binary = metadata("no_binary", set[str], lambda l: set(l))
1✔
259

260
        return PythonLockfileMetadataV3(
1✔
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
7✔
270
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
271
        instance = cast(PythonLockfileMetadataV3, instance)
2✔
272
        return {
2✔
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(
7✔
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 = (
1✔
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:
1✔
312
            failure_reasons.add(InvalidPythonLockfileReason.MANYLINUX_MISMATCH)
1✔
313
        if self.requirement_constraints != set(requirement_constraints):
1✔
314
            failure_reasons.add(InvalidPythonLockfileReason.CONSTRAINTS_FILE_MISMATCH)
1✔
315
        if self.only_binary != set(only_binary):
1✔
316
            failure_reasons.add(InvalidPythonLockfileReason.ONLY_BINARY_MISMATCH)
1✔
317
        if self.no_binary != set(no_binary):
1✔
318
            failure_reasons.add(InvalidPythonLockfileReason.NO_BINARY_MISMATCH)
1✔
319

320
        return LockfileMetadataValidation(failure_reasons)
1✔
321

322

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

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

331
    @classmethod
7✔
332
    def _from_json_dict(
7✔
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)
1✔
339
        metadata = _get_metadata(json_dict, lockfile_description, error_suffix)
1✔
340

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

344
        return PythonLockfileMetadataV4(
1✔
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
7✔
356
    def additional_header_attrs(cls, instance: LockfileMetadata) -> dict[Any, Any]:
7✔
357
        instance = cast(PythonLockfileMetadataV4, instance)
2✔
358
        return {
2✔
359
            "excludes": sorted(instance.excludes),
360
            "overrides": sorted(instance.overrides),
361
        }
362

363
    def is_valid_for(
7✔
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 = (
1✔
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):
1✔
395
            failure_reasons.add(InvalidPythonLockfileReason.EXCLUDES_MISMATCH)
1✔
396
        if self.overrides != set(overrides):
1✔
397
            failure_reasons.add(InvalidPythonLockfileReason.OVERRIDES_MISMATCH)
1✔
398

399
        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