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

pantsbuild / pants / 23710389144

29 Mar 2026 01:42PM UTC coverage: 92.849% (-0.07%) from 92.917%
23710389144

Pull #23200

github

web-flow
Merge 7a0639d44 into da60c6486
Pull Request #23200: perf: Port FrozenOrderedSet to rust

22 of 26 new or added lines in 6 files covered. (84.62%)

77 existing lines in 13 files now uncovered.

91400 of 98439 relevant lines covered (92.85%)

4.04 hits per line

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

99.31
/src/python/pants/backend/python/util_rules/interpreter_constraints_test.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

9
import pytest
1✔
10
from packaging.requirements import InvalidRequirement, Requirement
1✔
11

12
from pants.backend.python.subsystems.setup import PythonSetup
1✔
13
from pants.backend.python.target_types import InterpreterConstraintsField, PythonResolveField
1✔
14
from pants.backend.python.util_rules.interpreter_constraints import (
1✔
15
    _PATCH_VERSION_UPPER_BOUND,
16
    InterpreterConstraints,
17
    parse_constraint,
18
)
19
from pants.build_graph.address import Address
1✔
20
from pants.engine.target import FieldSet
1✔
21
from pants.testutil.option_util import create_subsystem
1✔
22
from pants.util.frozendict import FrozenDict
1✔
23
from pants.util.ordered_set import FrozenOrderedSet
1✔
24
from pants.util.strutil import softwrap
1✔
25

26

27
@dataclass(frozen=True)
1✔
28
class MockFieldSet(FieldSet):
1✔
29
    interpreter_constraints: InterpreterConstraintsField
1✔
30
    resolve: PythonResolveField
1✔
31

32
    @classmethod
1✔
33
    def create_for_test(
1✔
34
        cls, address: Address, compat: str | None, resolve: str = "python-default"
35
    ) -> MockFieldSet:
36
        return cls(
1✔
37
            address=address,
38
            interpreter_constraints=InterpreterConstraintsField(
39
                [compat] if compat else None, address=address
40
            ),
41
            resolve=PythonResolveField(resolve, address=address),
42
        )
43

44

45
def test_merge_interpreter_constraints() -> None:
1✔
46
    def assert_merged(*, inp: list[list[str]], expected: list[str]) -> None:
1✔
47
        result = sorted(str(req) for req in InterpreterConstraints.merge_constraint_sets(inp))
1✔
48
        # Requirement.parse() sorts specs differently than we'd like, so we convert each str to a
49
        # Requirement.
50
        normalized_expected = sorted(str(Requirement(v)) for v in expected)
1✔
51
        assert result == normalized_expected
1✔
52

53
    # Multiple constraint sets get merged so that they are ANDed.
54
    # A & B => A & B
55
    assert_merged(inp=[["CPython==2.7.*"], ["CPython==3.6.*"]], expected=["CPython==2.7.*,==3.6.*"])
1✔
56

57
    # Multiple constraints within a single constraint set are kept separate so that they are ORed.
58
    # A | B => A | B
59
    assert_merged(
1✔
60
        inp=[["CPython==2.7.*", "CPython==3.6.*"]], expected=["CPython==2.7.*", "CPython==3.6.*"]
61
    )
62

63
    # Input constraints already were ANDed.
64
    # A => A
65
    assert_merged(inp=[["CPython>=2.7,<3"]], expected=["CPython>=2.7,<3"])
1✔
66

67
    # Both AND and OR.
68
    # (A | B) & C => (A & B) | (B & C)
69
    assert_merged(
1✔
70
        inp=[["CPython>=2.7,<3", "CPython>=3.5"], ["CPython==3.6.*"]],
71
        expected=["CPython>=2.7,<3,==3.6.*", "CPython>=3.5,==3.6.*"],
72
    )
73
    # A & B & (C | D) => (A & B & C) | (A & B & D)
74
    assert_merged(
1✔
75
        inp=[["CPython==2.7.*"], ["CPython==3.6.*"], ["CPython==3.7.*", "CPython==3.8.*"]],
76
        expected=["CPython==2.7.*,==3.6.*,==3.7.*", "CPython==2.7.*,==3.6.*,==3.8.*"],
77
    )
78
    # (A | B) & (C | D) => (A & C) | (A & D) | (B & C) | (B & D)
79
    assert_merged(
1✔
80
        inp=[["CPython>=2.7,<3", "CPython>=3.5"], ["CPython==3.6.*", "CPython==3.7.*"]],
81
        expected=[
82
            "CPython>=2.7,<3,==3.6.*",
83
            "CPython>=2.7,<3,==3.7.*",
84
            "CPython>=3.5,==3.6.*",
85
            "CPython>=3.5,==3.7.*",
86
        ],
87
    )
88
    # A & (B | C | D) & (E | F) & G =>
89
    # (A & B & E & G) | (A & B & F & G) | (A & C & E & G) | (A & C & F & G) | (A & D & E & G) | (A & D & F & G)
90
    assert_merged(
1✔
91
        inp=[
92
            ["CPython==3.6.5"],
93
            ["CPython==2.7.14", "CPython==2.7.15", "CPython==2.7.16"],
94
            ["CPython>=3.6", "CPython==3.5.10"],
95
            ["CPython>3.8"],
96
        ],
97
        expected=[
98
            "CPython==2.7.14,==3.5.10,==3.6.5,>3.8",
99
            "CPython==2.7.14,>=3.6,==3.6.5,>3.8",
100
            "CPython==2.7.15,==3.5.10,==3.6.5,>3.8",
101
            "CPython==2.7.15,>=3.6,==3.6.5,>3.8",
102
            "CPython==2.7.16,==3.5.10,==3.6.5,>3.8",
103
            "CPython==2.7.16,>=3.6,==3.6.5,>3.8",
104
        ],
105
    )
106

107
    # Deduplicate between constraint_sets
108
    # (A | B) & (A | B) => A | B. Naively, this should actually resolve as follows:
109
    #   (A | B) & (A | B) => (A & A) | (A & B) | (B & B) => A | (A & B) | B.
110
    # But, we first deduplicate each constraint_set.  (A | B) & (A | B) can be rewritten as
111
    # X & X => X.
112
    assert_merged(
1✔
113
        inp=[["CPython==2.7.*", "CPython==3.6.*"], ["CPython==2.7.*", "CPython==3.6.*"]],
114
        expected=["CPython==2.7.*", "CPython==3.6.*"],
115
    )
116
    # (A | B) & C & (A | B) => (A & C) | (B & C). Alternatively, this can be rewritten as
117
    # X & Y & X => X & Y.
118
    assert_merged(
1✔
119
        inp=[
120
            ["CPython>=2.7,<3", "CPython>=3.5"],
121
            ["CPython==3.6.*"],
122
            ["CPython>=3.5", "CPython>=2.7,<3"],
123
        ],
124
        expected=["CPython>=2.7,<3,==3.6.*", "CPython>=3.5,==3.6.*"],
125
    )
126

127
    # No specifiers
128
    assert_merged(inp=[["CPython"]], expected=["CPython"])
1✔
129
    assert_merged(inp=[["CPython"], ["CPython==3.7.*"]], expected=["CPython==3.7.*"])
1✔
130

131
    # No interpreter is shorthand for CPython, which is how Pex behaves
132
    assert_merged(inp=[[">=3.5"], ["CPython==3.7.*"]], expected=["CPython>=3.5,==3.7.*"])
1✔
133

134
    # Handle empty constraints correctly.
135
    assert_merged(inp=[[], []], expected=[])
1✔
136
    assert_merged(inp=[[], ["==3.8.*"]], expected=["CPython==3.8.*"])
1✔
137
    assert_merged(inp=[[">=3.8.2"], [], ["==3.8.*"]], expected=["CPython>=3.8.2,==3.8.*"])
1✔
138
    assert_merged(inp=[], expected=[])
1✔
139

140
    # Handle mixed types correctly when there is a solution.
141
    assert_merged(inp=[["CPython==3.7.*", "PyPy==43.0"]], expected=["CPython==3.7.*", "PyPy==43.0"])
1✔
142
    assert_merged(
1✔
143
        inp=[["CPython==3.7.*", "PyPy>=43.0"], ["PyPy<44.0"]], expected=["PyPy>=43.0,<44.0"]
144
    )
145
    assert_merged(
1✔
146
        inp=[
147
            ["CPython==3.7.*", "Jython", "PyPy>=43.0"],
148
            ["PyPy<44.0", "Jython>=1.2"],
149
            ["Jython<1.3", "PyPy<44.0"],
150
        ],
151
        expected=["PyPy>=43.0,<44.0", "Jython>=1.2,<1.3"],
152
    )
153

154
    # Ensure we error when there is no solution.
155
    def assert_impossible(constraints, expected_msg):
1✔
156
        with pytest.raises(ValueError) as excinfo:
1✔
157
            print(InterpreterConstraints.merge_constraint_sets(constraints))
1✔
158
        assert str(excinfo.value) == softwrap(
1✔
159
            f"""
160
            These interpreter constraints cannot be merged, as they require conflicting
161
            interpreter types: {expected_msg}
162
            """
163
        )
164

165
    assert_impossible([["CPython==3.7.*"], ["PyPy==43.0"]], "(CPython==3.7.*) AND (PyPy==43.0)")
1✔
166
    assert_impossible(
1✔
167
        [["CPython==3.7.*", "Jython>=1.2"], ["PyPy==43.0", "Stackless<3.7"]],
168
        "(CPython==3.7.* OR Jython>=1.2) AND (PyPy==43.0 OR Stackless<3.7)",
169
    )
170

171

172
@pytest.mark.parametrize(
1✔
173
    "constraints",
174
    [
175
        ["CPython>=2.7,<3"],
176
        ["CPython>=2.7,<3", "CPython>=3.6"],
177
        ["CPython>=2.7.13"],
178
        ["CPython>=2.7.13,<2.7.16"],
179
        ["CPython>=2.7.13,!=2.7.16"],
180
        ["PyPy>=2.7,<3"],
181
        ["CPython"],
182
    ],
183
)
184
def test_interpreter_constraints_includes_python2(constraints) -> None:
1✔
185
    assert InterpreterConstraints(constraints).includes_python2() is True
1✔
186

187

188
@pytest.mark.parametrize(
1✔
189
    "constraints",
190
    [
191
        ["CPython>=3.6"],
192
        ["CPython>=3.7"],
193
        ["CPython>=3.6", "CPython>=3.8"],
194
        ["CPython!=2.7.*"],
195
        ["PyPy>=3.6"],
196
        [],
197
    ],
198
)
199
def test_interpreter_constraints_do_not_include_python2(constraints):
1✔
200
    assert InterpreterConstraints(constraints).includes_python2() is False
1✔
201

202

203
@pytest.mark.parametrize(
1✔
204
    "constraints,expected",
205
    [
206
        (["CPython>=2.7"], "2.7"),
207
        (["CPython>=3.5"], "3.5"),
208
        (["CPython>=3.6"], "3.6"),
209
        (["CPython>=3.7"], "3.7"),
210
        (["CPython>=3.8"], "3.8"),
211
        (["CPython>=3.9"], "3.9"),
212
        (["CPython>=3.10"], "3.10"),
213
        (["CPython==2.7.10"], "2.7"),
214
        (["CPython==3.5.*", "CPython>=3.6"], "3.5"),
215
        (["CPython==2.6.*"], None),
216
        (["CPython"], "2.7"),
217
        ([], None),
218
    ],
219
)
220
def test_interpreter_constraints_minimum_python_version(
1✔
221
    constraints: list[str], expected: str
222
) -> None:
223
    universe = ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"]
1✔
224
    ics = InterpreterConstraints(constraints)
1✔
225
    assert ics.minimum_python_version(universe) == expected
1✔
226
    assert ics.minimum_python_version(reversed(universe)) == expected
1✔
227
    assert ics.minimum_python_version(sorted(universe)) == expected
1✔
228

229

230
@pytest.mark.parametrize(
1✔
231
    "constraints,expected",
232
    [
233
        (["CPython>=2.7"], "CPython==2.7.*"),
234
        (["CPython>=2.7,!=2.7.2"], "CPython==2.7.*,!=2.7.2"),
235
        (["CPython>=3.7"], "CPython==3.7.*"),
236
        (["CPython>=3.8.3,!=3.8.5,!=3.9.1"], "CPython==3.8.*,!=3.8.0,!=3.8.1,!=3.8.2,!=3.8.5"),
237
        (["CPython==3.5.*", "CPython>=3.6"], "CPython==3.5.*"),
238
        (["CPython==3.7.*", "PyPy==3.6.*"], "PyPy==3.6.*"),
239
        (["CPython"], "CPython==2.7.*"),
240
        (["CPython==3.7.*,<3.6"], None),
241
        ([], None),
242
    ],
243
)
244
def test_snap_to_minimum(constraints, expected) -> None:
1✔
245
    universe = ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"]
1✔
246
    ics = InterpreterConstraints(constraints)
1✔
247
    snapped = ics.snap_to_minimum(universe)
1✔
248
    if expected is None:
1✔
249
        assert snapped is None
1✔
250
    else:
251
        assert snapped == InterpreterConstraints([expected])
1✔
252

253

254
@pytest.mark.parametrize(
1✔
255
    "constraints",
256
    [
257
        ["CPython==3.8.*"],
258
        ["CPython==3.8.1"],
259
        ["CPython==3.9.1"],
260
        ["CPython>=3.8"],
261
        ["CPython>=3.9"],
262
        ["CPython>=3.10"],
263
        ["CPython==3.8.*", "CPython==3.9.*"],
264
        ["PyPy>=3.8"],
265
    ],
266
)
267
def test_interpreter_constraints_require_python38(constraints) -> None:
1✔
268
    ics = InterpreterConstraints(constraints)
1✔
269
    universe = ("2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11")
1✔
270
    assert ics.requires_python38_or_newer(universe) is True
1✔
271
    assert ics.requires_python38_or_newer(reversed(universe)) is True
1✔
272
    assert ics.requires_python38_or_newer(sorted(universe)) is True
1✔
273

274

275
@pytest.mark.parametrize(
1✔
276
    "constraints",
277
    [
278
        ["CPython==3.5.*"],
279
        ["CPython==3.6.*"],
280
        ["CPython==3.7.*"],
281
        ["CPython==3.7.3"],
282
        ["CPython>=3.7"],
283
        ["CPython==3.7.*", "CPython==3.8.*"],
284
        ["CPython==3.5.3", "CPython==3.8.3"],
285
        ["PyPy>=3.7"],
286
        ["CPython"],
287
        [],
288
    ],
289
)
290
def test_interpreter_constraints_do_not_require_python38(constraints):
1✔
291
    ics = InterpreterConstraints(constraints)
1✔
292
    universe = ("2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11")
1✔
293
    assert ics.requires_python38_or_newer(universe) is False
1✔
294
    assert ics.requires_python38_or_newer(reversed(universe)) is False
1✔
295
    assert ics.requires_python38_or_newer(sorted(universe)) is False
1✔
296

297

298
def test_group_field_sets_by_constraints() -> None:
1✔
299
    py2_fs = MockFieldSet.create_for_test(Address("", target_name="py2"), ">=2.7,<3")
1✔
300
    py3_fs = [
1✔
301
        MockFieldSet.create_for_test(Address("", target_name="py3"), "==3.6.*"),
302
        MockFieldSet.create_for_test(Address("", target_name="py3_second"), "==3.6.*"),
303
    ]
304
    assert InterpreterConstraints.group_field_sets_by_constraints(
1✔
305
        [py2_fs, *py3_fs],
306
        python_setup=create_subsystem(
307
            PythonSetup,
308
            interpreter_constraints=[],
309
            warn_on_python2_usage=False,
310
            enable_resolves=False,
311
        ),
312
    ) == FrozenDict(
313
        {
314
            InterpreterConstraints(["CPython>=2.7,<3"]): (py2_fs,),
315
            InterpreterConstraints(["CPython==3.6.*"]): tuple(py3_fs),
316
        }
317
    )
318

319

320
def test_group_field_sets_by_constraints_with_unsorted_inputs() -> None:
1✔
321
    py3_fs = [
1✔
322
        MockFieldSet.create_for_test(
323
            Address("src/python/a_dir/path.py", target_name="test"), "==3.6.*"
324
        ),
325
        MockFieldSet.create_for_test(
326
            Address("src/python/b_dir/path.py", target_name="test"), ">2.7,<3"
327
        ),
328
        MockFieldSet.create_for_test(
329
            Address("src/python/c_dir/path.py", target_name="test"), "==3.6.*"
330
        ),
331
    ]
332

333
    ic_36 = InterpreterConstraints([Requirement("CPython==3.6.*")])
1✔
334

335
    output = InterpreterConstraints.group_field_sets_by_constraints(
1✔
336
        py3_fs,
337
        python_setup=create_subsystem(
338
            PythonSetup,
339
            interpreter_constraints=[],
340
            warn_on_python2_usage=False,
341
            enable_resolves=False,
342
        ),
343
    )
344

UNCOV
345
    assert output[ic_36] == (
×
346
        MockFieldSet.create_for_test(
347
            Address("src/python/a_dir/path.py", target_name="test"), "==3.6.*"
348
        ),
349
        MockFieldSet.create_for_test(
350
            Address("src/python/c_dir/path.py", target_name="test"), "==3.6.*"
351
        ),
352
    )
353

354

355
_SKIPPED_PY3 = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
1✔
356

357

358
@pytest.mark.parametrize(
1✔
359
    "constraints,expected",
360
    (
361
        (["==2.7.*"], "==2.7.*"),
362
        (["==2.7.*", ">=3.6,!=3.6.1"], "!=3.6.1,>=3.6 || ==2.7.*"),
363
        ([], "*"),
364
        # If any of the constraints are unconstrained (e.g. `CPython`), use a wildcard.
365
        (["==2.7", ""], "*"),
366
    ),
367
)
368
def test_to_poetry_constraint(constraints: list[str], expected: str) -> None:
1✔
369
    assert InterpreterConstraints(constraints).to_poetry_constraint() == expected
1✔
370

371

372
_ALL_PATCHES = list(range(_PATCH_VERSION_UPPER_BOUND + 1))
1✔
373

374

375
def patches(
1✔
376
    major: int, minor: int, unqualified_patches: Iterable[int]
377
) -> list[tuple[int, int, int]]:
378
    return [(major, minor, patch) for patch in unqualified_patches]
1✔
379

380

381
@pytest.mark.parametrize(
1✔
382
    "constraints,expected",
383
    (
384
        (["==2.7.15"], [(2, 7, 15)]),
385
        (["==2.7.*"], patches(2, 7, _ALL_PATCHES)),
386
        (["==3.6.15", "==3.7.15"], [(3, 6, 15), (3, 7, 15)]),
387
        (["==3.6.*", "==3.7.*"], patches(3, 6, _ALL_PATCHES) + patches(3, 7, _ALL_PATCHES)),
388
        (
389
            ["==2.7.1", ">=3.6.15"],
390
            (
391
                [(2, 7, 1)]
392
                + patches(3, 6, range(15, _PATCH_VERSION_UPPER_BOUND + 1))
393
                + patches(3, 7, _ALL_PATCHES)
394
                + patches(3, 8, _ALL_PATCHES)
395
                + patches(3, 9, _ALL_PATCHES)
396
            ),
397
        ),
398
        ([], []),
399
    ),
400
)
401
def test_enumerate_python_versions(
1✔
402
    constraints: list[str], expected: list[tuple[int, int, int]]
403
) -> None:
404
    assert InterpreterConstraints(constraints).enumerate_python_versions(
1✔
405
        ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"]
406
    ) == FrozenOrderedSet(expected)
407

408

409
def test_enumerate_python_versions_none_matching() -> None:
1✔
410
    with pytest.raises(ValueError):
1✔
411
        InterpreterConstraints(["==3.6.*"]).enumerate_python_versions(interpreter_universe=["2.7"])
1✔
412

413

414
@pytest.mark.parametrize("version", ["2.6", "2.8", "4.1", "1.0"])
1✔
415
def test_enumerate_python_versions_invalid_universe(version: str) -> None:
1✔
416
    with pytest.raises(AssertionError):
1✔
417
        InterpreterConstraints(["==2.7.*", "==3.5.*"]).enumerate_python_versions([version])
1✔
418

419

420
@pytest.mark.parametrize(
1✔
421
    "candidate,target,matches",
422
    (
423
        ([">=3.5,<=3.6"], [">=3.5.5"], False),  # Target ICs contain versions in the 3.6 range
424
        ([">=3.5,<=3.6"], [">=3.5.5,<=3.5.10"], True),
425
        (
426
            [">=3.5", "<=3.6"],
427
            [">=3.5.5,<=3.5.10"],
428
            True,
429
        ),  # Target ICs match each of the actual ICs individually
430
        (
431
            [">=3.5", "<=3.5.4"],
432
            [">=3.5.5,<=3.5.10"],
433
            True,
434
        ),  # Target ICs do not match any candidate ICs
435
        ([">=3.5,<=3.6"], ["==3.5.*,!=3.5.10"], True),
436
        (
437
            [">=3.5,<=3.6, !=3.5.10"],
438
            ["==3.5.*"],
439
            False,
440
        ),  # Excluded IC from candidate range is valid for target ICs
441
        ([">=3.5"], [">=3.5,<=3.6", ">= 3.8"], True),
442
        (
443
            [">=3.5,!=3.7.10"],
444
            [">=3.5,<=3.6", ">= 3.8"],
445
            True,
446
        ),  # Excluded version from candidate ICs is not in a range specified by target ICs
447
        (
448
            [">=3.5,<=3.6", ">= 3.8"],
449
            [">=3.9"],
450
            True,
451
        ),  # matches only one of the candidate specifications
452
        (
453
            ["<3.6", ">=3.6"],
454
            [">=3.5"],
455
            True,
456
        ),  # target matches a weirdly specified non-disjoint IC list
457
    ),
458
)
459
def test_contains(candidate, target, matches) -> None:
1✔
460
    assert (
1✔
461
        InterpreterConstraints(candidate).contains(
462
            InterpreterConstraints(target), ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"]
463
        )
464
        == matches
465
    )
466

467

468
def test_constraints_are_correctly_sorted_at_construction() -> None:
1✔
469
    # #12578: This list itself is out of order, and `CPython>=3.6,<4,!=3.7.*` is specified with
470
    # out-of-order component requirements. This test verifies that the list is fully sorted after
471
    # the first call to `InterpreterConstraints()`
472
    inputs = ["CPython==2.7.*", "PyPy", "CPython>=3.6,<4,!=3.7.*"]
1✔
473
    a = InterpreterConstraints(inputs)
1✔
474
    a_str = [str(i) for i in a]
1✔
475
    b = InterpreterConstraints(a_str)
1✔
476
    assert a == b
1✔
477

478

479
@pytest.mark.parametrize(
1✔
480
    "constraints,expected",
481
    (
482
        (["==2.7.*"], ["2.7"]),
483
        ([">=3.7"], ["3.7", "3.8", "3.9", "3.10"]),
484
        (["==2.7", "==3.6.5"], ["2.7", "3.6"]),
485
    ),
486
)
487
def test_partition_into_major_minor_versions(constraints: list[str], expected: list[str]) -> None:
1✔
488
    assert InterpreterConstraints(constraints).partition_into_major_minor_versions(
1✔
489
        ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"]
490
    ) == tuple(expected)
491

492

493
@pytest.mark.parametrize(
1✔
494
    ("constraints", "expected"),
495
    [
496
        # Valid
497
        (["==2.7.*"], (2, 7)),
498
        (["CPython==2.7.*"], (2, 7)),
499
        (["==3.0.*"], (3, 0)),
500
        (["==3.45.*"], (3, 45)),
501
        ([">=3.45,<3.46"], (3, 45)),
502
        (["CPython>=3.45,<3.46"], (3, 45)),
503
        (["<3.46,>=3.45"], (3, 45)),
504
        # Invalid/too hard
505
        # equality, but with patch versions involved
506
        (["==3.45"], None),
507
        (["==3.45.6"], None),
508
        (["==3.45,!=3.45.6"], None),
509
        (["==3.45,!=3.67"], None),
510
        (["==3.45.*,!=3.45.6"], None),
511
        # comparisons, with patch versions
512
        ([">=3.45,<3.45.10"], None),
513
        ([">=3.45.67,<3.46"], None),
514
        # comparisons, with too-wide constraints
515
        ([">=2.7,<3.8"], None),
516
        ([">=3.45,<3.47"], None),
517
        ([">=3,<4"], None),
518
        # (even excluding the extra version isn't enough)
519
        ([">=3.45,<3.47,!=3.46"], None),
520
        # other operators
521
        (["~=3.45"], None),
522
        ([">3.45,<=3.46"], None),
523
        ([">3.45,<3.47"], None),
524
        (["===3.45"], None),
525
        # wrong number of elements
526
        ([], None),
527
        (["==3.45.*", "==3.46.*"], None),
528
        (["==3.45.*", ">=3.45,<3.46"], None),
529
    ],
530
    ids=str,
531
)
532
def test_major_minor_version_when_single_and_entire(
1✔
533
    constraints: list[str], expected: None | tuple[int, int]
534
) -> None:
535
    ics = InterpreterConstraints(constraints)
1✔
536
    computed = ics.major_minor_version_when_single_and_entire()
1✔
537
    assert computed == expected
1✔
538

539
    if expected is not None:
1✔
540
        # if we infer a specific version, let's confirm the full enumeration includes exactly all
541
        # the patch versions of that major/minor
542
        universe = ["2.7", *(f"3.{minor}" for minor in range(100))]
1✔
543
        all_versions = ics.enumerate_python_versions(universe)
1✔
544
        assert set(all_versions) == {
1✔
545
            (*expected, patch) for patch in range(_PATCH_VERSION_UPPER_BOUND + 1)
546
        }
547

548

549
@pytest.mark.parametrize(
1✔
550
    ("input_ic", "expected"),
551
    [
552
        ("CPython==3.9.*", "CPython==3.9.*"),
553
        ("==3.9.*", "CPython==3.9.*"),
554
        ("PyPy==3.9.*", "PyPy==3.9.*"),
555
    ],
556
)
557
def test_parse_python_interpreter_constraint_when_valid(input_ic: str, expected: str) -> None:
1✔
558
    assert parse_constraint(input_ic) == Requirement(expected)
1✔
559

560

561
@pytest.mark.parametrize(
1✔
562
    ("input_ic", "expected_error"),
563
    [
564
        ("some-invalid-constraint-3.9.*", "Failed to parse Python interpreter constraint"),
565
        ("3.9.*", "Failed to parse Python interpreter constraint"),
566
    ],
567
)
568
def test_parse_python_interpreter_constraint_when_invalid(
1✔
569
    input_ic: str, expected_error: str
570
) -> None:
571
    with pytest.raises(InvalidRequirement, match=expected_error):
1✔
572
        parse_constraint(input_ic)
1✔
573

574

575
@pytest.mark.parametrize(
1✔
576
    argnames=("compat", "enable_resolves", "expected"),
577
    argvalues=[
578
        ("==3.11.*", False, InterpreterConstraints.for_fixed_python_version("3.11.*")),
579
        (None, False, InterpreterConstraints.for_fixed_python_version("3.10.*")),
580
        (None, True, InterpreterConstraints.for_fixed_python_version("3.12.*")),
581
    ],
582
)
583
def test_default_to_resolve_interpreter_constraints(
1✔
584
    compat: str | None, enable_resolves: bool, expected: InterpreterConstraints
585
) -> None:
586
    field_set = MockFieldSet.create_for_test(
1✔
587
        Address("src/python/path.py", target_name="test"), compat
588
    )
589
    subsystem = create_subsystem(
1✔
590
        PythonSetup,
591
        enable_resolves=enable_resolves,
592
        interpreter_constraints=["==3.10.*"],
593
        default_resolve="python-default",
594
        resolves={"python-default": "default.lock"},
595
        default_to_resolve_interpreter_constraints=True,
596
        resolves_to_interpreter_constraints={"python-default": ["==3.12.*"]},
597
        warn_on_python2_usage=False,
598
    )
599
    assert InterpreterConstraints.create_from_field_sets([field_set], subsystem) == expected
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

© 2026 Coveralls, Inc