• 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.49
/src/python/pants/backend/python/util_rules/interpreter_constraints.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
import itertools
11✔
7
import logging
11✔
8
import re
11✔
9
from collections import defaultdict
11✔
10
from collections.abc import Iterable, Iterator, Sequence
11✔
11
from typing import Protocol, TypeVar
11✔
12

13
from packaging.requirements import InvalidRequirement, Requirement
11✔
14

15
from pants.backend.python.subsystems.setup import PythonSetup
11✔
16
from pants.backend.python.target_types import InterpreterConstraintsField, PythonResolveField
11✔
17
from pants.build_graph.address import Address
11✔
18
from pants.engine.engine_aware import EngineAwareParameter
11✔
19
from pants.engine.target import Target
11✔
20
from pants.util.docutil import bin_name
11✔
21
from pants.util.frozendict import FrozenDict
11✔
22
from pants.util.memo import memoized
11✔
23
from pants.util.ordered_set import FrozenOrderedSet, OrderedSet
11✔
24
from pants.util.strutil import softwrap
11✔
25

26
logger = logging.getLogger(__name__)
11✔
27

28

29
# This protocol allows us to work with any arbitrary FieldSet. See
30
# https://mypy.readthedocs.io/en/stable/protocols.html.
31
class FieldSetWithInterpreterConstraints(Protocol):
11✔
32
    @property
33
    def address(self) -> Address: ...
34

35
    @property
36
    def interpreter_constraints(self) -> InterpreterConstraintsField: ...
37

38
    @property
39
    def resolve(self) -> PythonResolveField: ...
40

41

42
_FS = TypeVar("_FS", bound=FieldSetWithInterpreterConstraints)
11✔
43

44

45
RawConstraints = tuple[str, ...]
11✔
46

47

48
# The current maxes are 2.7.18 and 3.6.15.  We go much higher, for safety.
49
_PATCH_VERSION_UPPER_BOUND = 30
11✔
50

51

52
@memoized
11✔
53
def interpreter_constraints_contains(
11✔
54
    a: RawConstraints, b: RawConstraints, interpreter_universe: tuple[str, ...]
55
) -> bool:
56
    """A memoized version of `InterpreterConstraints.contains`.
57

58
    This is a function in order to keep the memoization cache on the module rather than on an
59
    instance. It can't go on `PythonSetup`, since that would cause a cycle with this module.
60
    """
61
    return InterpreterConstraints(a).contains(InterpreterConstraints(b), interpreter_universe)
×
62

63

64
@memoized
11✔
65
def parse_constraint(constraint: str) -> Requirement:
11✔
66
    """Parse an interpreter constraint, e.g., CPython>=2.7,<3.
67

68
    We allow shorthand such as `>=3.7`, which gets expanded to `CPython>=3.7`. See Pex's
69
    interpreter.py's `parse_requirement()`.
70
    """
71
    try:
11✔
72
        parsed_requirement = Requirement(constraint)
11✔
73
    except InvalidRequirement as err:
6✔
74
        try:
6✔
75
            parsed_requirement = Requirement(f"CPython{constraint}")
6✔
76
        except InvalidRequirement:
1✔
77
            raise InvalidRequirement(
1✔
78
                f"Failed to parse Python interpreter constraint `{constraint}`: {err}"
79
            )
80

81
    return parsed_requirement
11✔
82

83

84
# Normally we would subclass `DeduplicatedCollection`, but we want a custom constructor.
85
class InterpreterConstraints(FrozenOrderedSet[Requirement], EngineAwareParameter):
11✔
86
    @classmethod
11✔
87
    def for_fixed_python_version(
11✔
88
        cls, python_version_str: str, interpreter_type: str = "CPython"
89
    ) -> InterpreterConstraints:
90
        return cls([f"{interpreter_type}=={python_version_str}"])
1✔
91

92
    def __init__(self, constraints: Iterable[str | Requirement] = ()) -> None:
11✔
93
        # #12578 `parse_constraint` will sort the requirement's component constraints into a stable form.
94
        # We need to sort the component constraints for each requirement _before_ sorting the entire list
95
        # for the ordering to be correct.
96
        parsed_constraints = (
11✔
97
            i if isinstance(i, Requirement) else parse_constraint(i) for i in constraints
98
        )
99
        super().__init__(sorted(parsed_constraints, key=lambda c: str(c)))
11✔
100

101
    def __str__(self) -> str:
11✔
102
        return " OR ".join(str(constraint) for constraint in self)
1✔
103

104
    def debug_hint(self) -> str:
11✔
105
        return str(self)
×
106

107
    @property
11✔
108
    def description(self) -> str:
11✔
UNCOV
109
        return str(sorted(str(c) for c in self))
×
110

111
    @classmethod
11✔
112
    def merge(cls, ics: Iterable[InterpreterConstraints]) -> InterpreterConstraints:
11✔
113
        return InterpreterConstraints(
×
114
            cls.merge_constraint_sets(tuple(str(requirement) for requirement in ic) for ic in ics)
115
        )
116

117
    @classmethod
11✔
118
    def merge_constraint_sets(
11✔
119
        cls, constraint_sets: Iterable[Iterable[str]]
120
    ) -> frozenset[Requirement]:
121
        """Given a collection of constraints sets, merge by ORing within each individual constraint
122
        set and ANDing across each distinct constraint set.
123

124
        For example, given `[["CPython>=2.7", "CPython<=3"], ["CPython==3.6.*"]]`, return
125
        `["CPython>=2.7,==3.6.*", "CPython<=3,==3.6.*"]`.
126
        """
127
        # A sentinel to indicate a requirement that is impossible to satisfy (i.e., one that
128
        # requires two different interpreter types).
129
        impossible = parse_constraint("IMPOSSIBLE")
1✔
130

131
        # Each element (a Set[ParsedConstraint]) will get ANDed. We use sets to deduplicate
132
        # identical top-level parsed constraint sets.
133

134
        # First filter out any empty constraint_sets, as those represent "no constraints", i.e.,
135
        # any interpreters are allowed, so omitting them has the logical effect of ANDing them with
136
        # the others, without having to deal with the vacuous case below.
137
        constraint_sets = [cs for cs in constraint_sets if cs]
1✔
138
        if not constraint_sets:
1✔
139
            return frozenset()
1✔
140

141
        parsed_constraint_sets: set[frozenset[Requirement]] = set()
1✔
142
        for constraint_set in constraint_sets:
1✔
143
            # Each element (a ParsedConstraint) will get ORed.
144
            parsed_constraint_set = frozenset(
1✔
145
                parse_constraint(constraint) for constraint in constraint_set
146
            )
147
            parsed_constraint_sets.add(parsed_constraint_set)
1✔
148

149
        if len(parsed_constraint_sets) == 1:
1✔
150
            return next(iter(parsed_constraint_sets))
1✔
151

152
        def and_constraints(parsed_requirements: Sequence[Requirement]) -> Requirement:
1✔
153
            assert len(parsed_requirements) > 0, "At least one `Requirement` must be supplied."
1✔
154
            expected_name = parsed_requirements[0].name
1✔
155
            current_requirement_specifier = parsed_requirements[0].specifier
1✔
156
            for requirement in parsed_requirements[1:]:
1✔
157
                if requirement.name != expected_name:
1✔
158
                    return impossible
1✔
159
                current_requirement_specifier &= requirement.specifier
1✔
160
            return Requirement(f"{expected_name}{current_requirement_specifier}")
1✔
161

162
        ored_constraints = (
1✔
163
            and_constraints(constraints_product)
164
            for constraints_product in itertools.product(*parsed_constraint_sets)
165
        )
166
        ret = frozenset(cs for cs in ored_constraints if cs != impossible)
1✔
167
        if not ret:
1✔
168
            # There are no possible combinations.
169
            attempted_str = " AND ".join(f"({' OR '.join(cs)})" for cs in constraint_sets)
1✔
170
            raise ValueError(
1✔
171
                softwrap(
172
                    f"""
173
                    These interpreter constraints cannot be merged, as they require
174
                    conflicting interpreter types: {attempted_str}
175
                    """
176
                )
177
            )
178
        return ret
1✔
179

180
    @classmethod
11✔
181
    def create_from_targets(
11✔
182
        cls, targets: Iterable[Target], python_setup: PythonSetup
183
    ) -> InterpreterConstraints | None:
184
        """Returns merged InterpreterConstraints for the given Targets.
185

186
        If none of the given Targets have InterpreterConstraintsField, returns None.
187

188
        NB: Because Python targets validate that they have ICs which are a subset of their
189
        dependencies, merging constraints like this is only necessary when you are _mixing_ code
190
        which might not have any interdependencies, such as when you're merging unrelated roots.
191
        """
192
        fields = [
×
193
            (
194
                tgt[InterpreterConstraintsField],
195
                tgt[PythonResolveField] if tgt.has_field(PythonResolveField) else None,
196
            )
197
            for tgt in targets
198
            if tgt.has_field(InterpreterConstraintsField)
199
        ]
200
        if not fields:
×
201
            return None
×
202
        return cls.create_from_compatibility_fields(fields, python_setup)
×
203

204
    @classmethod
11✔
205
    def create_from_field_sets(
11✔
206
        cls, fs: Iterable[_FS], python_setup: PythonSetup
207
    ) -> InterpreterConstraints:
208
        return cls.create_from_compatibility_fields(
1✔
209
            [(field_set.interpreter_constraints, field_set.resolve) for field_set in fs],
210
            python_setup,
211
        )
212

213
    @classmethod
11✔
214
    def create_from_compatibility_fields(
11✔
215
        cls,
216
        fields: Iterable[tuple[InterpreterConstraintsField, PythonResolveField | None]],
217
        python_setup: PythonSetup,
218
    ) -> InterpreterConstraints:
219
        """Returns merged InterpreterConstraints for the given `InterpreterConstraintsField`s.
220

221
        NB: Because Python targets validate that they have ICs which are a subset of their
222
        dependencies, merging constraints like this is only necessary when you are _mixing_ code
223
        which might not have any inter-dependencies, such as when you're merging un-related roots.
224
        """
225
        constraint_sets = {
1✔
226
            ics.value_or_configured_default(python_setup, resolve) for ics, resolve in fields
227
        }
228
        # This will OR within each field and AND across fields.
229
        merged_constraints = cls.merge_constraint_sets(constraint_sets)
1✔
230
        return InterpreterConstraints(merged_constraints)
1✔
231

232
    @classmethod
11✔
233
    def group_field_sets_by_constraints(
11✔
234
        cls, field_sets: Iterable[_FS], python_setup: PythonSetup
235
    ) -> FrozenDict[InterpreterConstraints, tuple[_FS, ...]]:
236
        results = defaultdict(set)
1✔
237
        for fs in field_sets:
1✔
238
            constraints = cls.create_from_compatibility_fields(
1✔
239
                [(fs.interpreter_constraints, fs.resolve)], python_setup
240
            )
241
            results[constraints].add(fs)
1✔
242
        return FrozenDict(
1✔
243
            {
244
                constraints: tuple(sorted(field_sets, key=lambda fs: fs.address))
245
                for constraints, field_sets in sorted(results.items())
246
            }
247
        )
248

249
    def generate_pex_arg_list(self) -> list[str]:
11✔
250
        args = []
1✔
251
        for constraint in self:
1✔
252
            args.extend(["--interpreter-constraint", str(constraint)])
1✔
253
        return args
1✔
254

255
    def _valid_patch_versions(self, major: int, minor: int) -> Iterator[int]:
11✔
256
        for p in range(0, _PATCH_VERSION_UPPER_BOUND + 1):
11✔
257
            for req in self:
11✔
258
                if req.specifier.contains(f"{major}.{minor}.{p}"):
11✔
259
                    yield p
11✔
260

261
    def _includes_version(self, major: int, minor: int) -> bool:
11✔
262
        return any(True for _ in self._valid_patch_versions(major, minor))
2✔
263

264
    def includes_python2(self) -> bool:
11✔
265
        """Checks if any of the constraints include Python 2.
266

267
        This will return True even if the code works with Python 3 too, so long as at least one of
268
        the constraints works with Python 2.
269
        """
270
        return self._includes_version(2, 7)
1✔
271

272
    def minimum_python_version(self, interpreter_universe: Iterable[str]) -> str | None:
11✔
273
        """Find the lowest major.minor Python version that will work with these constraints.
274

275
        The constraints may also be compatible with later versions; this is the lowest version that
276
        still works.
277
        """
278
        for major, minor in sorted(_major_minor_to_int(s) for s in interpreter_universe):
2✔
279
            if self._includes_version(major, minor):
2✔
280
                return f"{major}.{minor}"
2✔
281
        return None
1✔
282

283
    def snap_to_minimum(self, interpreter_universe: Iterable[str]) -> InterpreterConstraints | None:
11✔
284
        """Snap to the lowest Python major.minor version that works with these constraints.
285

286
        Will exclude patch versions that are expressly incompatible.
287
        """
288
        for major, minor in sorted(_major_minor_to_int(s) for s in interpreter_universe):
1✔
289
            for p in range(0, _PATCH_VERSION_UPPER_BOUND + 1):
1✔
290
                for req in self:
1✔
291
                    if req.specifier.contains(f"{major}.{minor}.{p}"):
1✔
292
                        # We've found the minimum major.minor that is compatible.
293
                        req_strs = [f"{req.name}=={major}.{minor}.*"]
1✔
294
                        # Now find any patches within that major.minor that we must exclude.
295
                        invalid_patches = sorted(
1✔
296
                            set(range(0, _PATCH_VERSION_UPPER_BOUND + 1))
297
                            - set(self._valid_patch_versions(major, minor))
298
                        )
299
                        req_strs.extend(f"!={major}.{minor}.{p}" for p in invalid_patches)
1✔
300
                        req_str = ",".join(req_strs)
1✔
301
                        snapped = parse_constraint(req_str)
1✔
302
                        return InterpreterConstraints([snapped])
1✔
303
        return None
1✔
304

305
    def _requires_python3_version_or_newer(
11✔
306
        self, *, allowed_versions: Iterable[str], prior_version: str
307
    ) -> bool:
308
        if not self:
1✔
309
            return False
1✔
310
        patch_versions = list(reversed(range(0, _PATCH_VERSION_UPPER_BOUND)))
1✔
311
        # We only look at the prior Python release. For example, consider Python 3.8+
312
        # looking at 3.7. If using something like `>=3.5`, Py37 will be included.
313
        # `==3.6.*,!=3.7.*,==3.8.*` is unlikely, and even that will work correctly as
314
        # it's an invalid constraint so setuptools returns False always. `['==2.7.*', '==3.8.*']`
315
        # will fail because not every single constraint is exclusively 3.8.
316
        prior_versions = [f"{prior_version}.{p}" for p in patch_versions]
1✔
317
        allowed_versions = [
1✔
318
            f"{major_minor}.{p}" for major_minor in allowed_versions for p in patch_versions
319
        ]
320

321
        def valid_constraint(constraint: Requirement) -> bool:
1✔
322
            if any(constraint.specifier.contains(prior) for prior in prior_versions):
1✔
323
                return False
1✔
324
            if not any(constraint.specifier.contains(allowed) for allowed in allowed_versions):
1✔
325
                return False
1✔
326
            return True
1✔
327

328
        return all(valid_constraint(c) for c in self)
1✔
329

330
    def requires_python38_or_newer(self, interpreter_universe: Iterable[str]) -> bool:
11✔
331
        """Checks if the constraints are all for Python 3.8+.
332

333
        This will return False if Python 3.8 is allowed, but prior versions like 3.7 are also
334
        allowed.
335
        """
336
        py38_and_later = [
1✔
337
            interp for interp in interpreter_universe if _major_minor_to_int(interp) >= (3, 8)
338
        ]
339
        return self._requires_python3_version_or_newer(
1✔
340
            allowed_versions=py38_and_later, prior_version="3.7"
341
        )
342

343
    def to_poetry_constraint(self) -> str:
11✔
344
        specifiers = []
1✔
345
        wildcard_encountered = False
1✔
346
        for constraint in self:
1✔
347
            specifier = str(constraint.specifier)
1✔
348
            if specifier:
1✔
349
                specifiers.append(specifier)
1✔
350
            else:
351
                wildcard_encountered = True
1✔
352
        if not specifiers or wildcard_encountered:
1✔
353
            return "*"
1✔
354
        return " || ".join(specifiers)
1✔
355

356
    def enumerate_python_versions(
11✔
357
        self, interpreter_universe: Iterable[str]
358
    ) -> FrozenOrderedSet[tuple[int, int, int]]:
359
        """Return a set of all plausible (major, minor, patch) tuples for all Python 2.7/3.x in the
360
        specified interpreter universe that matches this set of interpreter constraints.
361

362
        This also validates our assumptions around the `interpreter_universe`:
363

364
        - Python 2.7 is the only Python 2 version in the universe, if at all.
365
        - Python 3 is the last major release of Python, which the core devs have committed to in
366
          public several times.
367
        """
368
        if not self:
11✔
369
            return FrozenOrderedSet()
1✔
370

371
        minors = []
11✔
372
        for major_minor in interpreter_universe:
11✔
373
            major, minor = _major_minor_to_int(major_minor)
11✔
374
            if major == 2:
11✔
375
                if minor != 7:
11✔
376
                    raise AssertionError(
1✔
377
                        softwrap(
378
                            f"""
379
                            Unexpected value in `[python].interpreter_versions_universe`:
380
                            {major_minor}. Expected the only Python 2 value to be '2.7', given that
381
                            all other versions are unmaintained or do not exist.
382
                            """
383
                        )
384
                    )
385
                minors.append((2, minor))
11✔
386
            elif major == 3:
11✔
387
                minors.append((3, minor))
11✔
388
            else:
389
                raise AssertionError(
1✔
390
                    softwrap(
391
                        f"""
392
                        Unexpected value in `[python].interpreter_versions_universe`:
393
                        {major_minor}. Expected to only include '2.7' and/or Python 3 versions,
394
                        given that Python 3 will be the last major Python version. Please open an
395
                        issue at https://github.com/pantsbuild/pants/issues/new if this is no longer
396
                        true.
397
                        """
398
                    )
399
                )
400

401
        valid_patches = FrozenOrderedSet(
11✔
402
            (major, minor, patch)
403
            for (major, minor) in sorted(minors)
404
            for patch in self._valid_patch_versions(major, minor)
405
        )
406

407
        if not valid_patches:
11✔
408
            raise ValueError(
1✔
409
                softwrap(
410
                    f"""
411
                    The interpreter constraints `{self}` are not compatible with any of the
412
                    interpreter versions from `[python].interpreter_versions_universe`.
413

414
                    Please either change these interpreter constraints or update the
415
                    `interpreter_versions_universe` to include the interpreters set in these
416
                    constraints. Run `{bin_name()} help-advanced python` for more information on the
417
                    `interpreter_versions_universe` option.
418
                    """
419
                )
420
            )
421

422
        return valid_patches
11✔
423

424
    def contains(self, other: InterpreterConstraints, interpreter_universe: Iterable[str]) -> bool:
11✔
425
        """Returns True if the `InterpreterConstraints` specified in `other` is a subset of these
426
        `InterpreterConstraints`.
427

428
        This is restricted to the set of minor Python versions specified in `universe`.
429
        """
430
        if self == other:
2✔
431
            return True
1✔
432
        this = self.enumerate_python_versions(interpreter_universe)
2✔
433
        that = other.enumerate_python_versions(interpreter_universe)
2✔
434
        return this.issuperset(that)
2✔
435

436
    def partition_into_major_minor_versions(
11✔
437
        self, interpreter_universe: Iterable[str]
438
    ) -> tuple[str, ...]:
439
        """Return all the valid major.minor versions, e.g. `('2.7', '3.6')`."""
440
        result: OrderedSet[str] = OrderedSet()
10✔
441
        for major, minor, _ in self.enumerate_python_versions(interpreter_universe):
10✔
442
            result.add(f"{major}.{minor}")
10✔
443
        return tuple(result)
10✔
444

445
    def major_minor_version_when_single_and_entire(self) -> None | tuple[int, int]:
11✔
446
        """Returns the (major, minor) version that these constraints cover, if they cover all of
447
        exactly one major minor version, without rules about patch versions.
448

449
        This is a best effort function, e.g. for using during inference that can be overridden.
450

451
        Examples:
452

453
        All of these return (3, 9): `==3.9.*`, `CPython==3.9.*`, `>=3.9,<3.10`, `<3.10,>=3.9`
454

455
        All of these return None:
456

457
        - `==3.9.10`: restricted to a single patch version
458
        - `==3.9`: restricted to a single patch version (0, implicitly)
459
        - `==3.9.*,!=3.9.2`: excludes a patch
460
        - `>=3.9,<3.11`: more than one major version
461
        - `>=3.9,<3.11,!=3.10`: too complicated to understand it only includes 3.9
462
        - more than one requirement in the list: too complicated
463
        """
464

465
        try:
1✔
466
            return _major_minor_version_when_single_and_entire(self)
1✔
467
        except _NonSimpleMajorMinor:
1✔
468
            return None
1✔
469

470

471
def _major_minor_to_int(major_minor: str) -> tuple[int, int]:
11✔
472
    return tuple(int(x) for x in major_minor.split(".", maxsplit=1))  # type: ignore[return-value]
11✔
473

474

475
class _NonSimpleMajorMinor(Exception):
11✔
476
    pass
11✔
477

478

479
_ANY_PATCH_VERSION = re.compile(r"^(?P<major>\d+)\.(?P<minor>\d+)(?P<any_patch>\.\*)?$")
11✔
480

481

482
def _parse_simple_version(version: str, require_any_patch: bool) -> tuple[int, int]:
11✔
483
    match = _ANY_PATCH_VERSION.fullmatch(version)
1✔
484
    if match is None or (require_any_patch and match.group("any_patch") is None):
1✔
485
        raise _NonSimpleMajorMinor()
1✔
486

487
    return int(match.group("major")), int(match.group("minor"))
1✔
488

489

490
def _major_minor_version_when_single_and_entire(ics: InterpreterConstraints) -> tuple[int, int]:
11✔
491
    if len(ics) != 1:
1✔
492
        raise _NonSimpleMajorMinor()
1✔
493

494
    req = next(iter(ics))
1✔
495

496
    just_cpython = req.name == "CPython" and not req.extras and not req.marker
1✔
497
    if not just_cpython:
1✔
498
        raise _NonSimpleMajorMinor()
×
499

500
    # ==major.minor or ==major.minor.*
501
    if len(req.specifier) == 1:
1✔
502
        specifier = next(iter(req.specifier))
1✔
503
        if specifier.operator != "==":
1✔
504
            raise _NonSimpleMajorMinor()
1✔
505

506
        return _parse_simple_version(specifier.version, require_any_patch=True)
1✔
507

508
    # >=major.minor,<major.(minor+1)
509
    if len(req.specifier) == 2:
1✔
510
        specifiers = sorted(req.specifier, key=lambda s: s.version)
1✔
511
        operator_lo, version_lo = (specifiers[0].operator, specifiers[0].version)
1✔
512
        operator_hi, version_hi = (specifiers[1].operator, specifiers[1].version)
1✔
513

514
        if operator_lo != ">=":
1✔
515
            # if the lo operator isn't >=, they might be in the wrong order (or, if not, the check
516
            # below will catch them)
517
            operator_lo, operator_hi = operator_hi, operator_lo
1✔
518
            version_lo, version_hi = version_hi, version_lo
1✔
519

520
        if operator_lo != ">=" and operator_hi != "<":
1✔
521
            raise _NonSimpleMajorMinor()
1✔
522

523
        major_lo, minor_lo = _parse_simple_version(version_lo, require_any_patch=False)
1✔
524
        major_hi, minor_hi = _parse_simple_version(version_hi, require_any_patch=False)
1✔
525

526
        if major_lo == major_hi and minor_lo + 1 == minor_hi:
1✔
527
            return major_lo, minor_lo
1✔
528

529
        raise _NonSimpleMajorMinor()
1✔
530

531
    # anything else we don't understand
532
    raise _NonSimpleMajorMinor()
1✔
533

534

535
@memoized
11✔
536
def _warn_on_python2_usage_in_interpreter_constraints(
11✔
537
    interpreter_constraints: tuple[str, ...], *, description_of_origin: str
538
) -> None:
539
    ics = InterpreterConstraints(interpreter_constraints)
×
540
    if ics.includes_python2():
×
541
        logger.warning(
×
542
            f"The Python interpreter constraints from {description_of_origin} includes Python 2.x as a selected Python version. "
543
            "Please note that Pants will no longer be proactively tested with Python 2.x starting with Pants v2.24.x because "
544
            "Python 2 support ended on 1 January 2020. Please consider upgrading to Python 3.x for your code."
545
        )
546

547

548
def warn_on_python2_usage_in_interpreter_constraints(
11✔
549
    interpreter_constraints: Iterable[str], *, description_of_origin: str
550
) -> None:
551
    _warn_on_python2_usage_in_interpreter_constraints(
×
552
        tuple(interpreter_constraints), description_of_origin=description_of_origin
553
    )
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