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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 hits per line

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

94.19
/src/python/pants/backend/python/subsystems/setup.py
1
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
from __future__ import annotations
4✔
5

6
import enum
4✔
7
import logging
4✔
8
import os
4✔
9
from collections.abc import Iterable
4✔
10
from typing import TypeVar, cast
4✔
11

12
from packaging.utils import canonicalize_name
4✔
13

14
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
4✔
15
from pants.option.errors import OptionsError
4✔
16
from pants.option.option_types import (
4✔
17
    BoolOption,
18
    DictOption,
19
    EnumOption,
20
    FileOption,
21
    StrListOption,
22
    StrOption,
23
)
24
from pants.option.subsystem import Subsystem
4✔
25
from pants.util.docutil import bin_name, doc_url
4✔
26
from pants.util.memo import memoized_method, memoized_property
4✔
27
from pants.util.strutil import softwrap
4✔
28

29
logger = logging.getLogger(__name__)
4✔
30

31

32
@enum.unique
4✔
33
class InvalidLockfileBehavior(enum.Enum):
4✔
34
    error = "error"
4✔
35
    ignore = "ignore"
4✔
36
    warn = "warn"
4✔
37

38

39
@enum.unique
4✔
40
class LockfileGenerator(enum.Enum):
4✔
41
    PEX = "pex"
4✔
42
    POETRY = "poetry"
4✔
43

44

45
RESOLVE_OPTION_KEY__DEFAULT = "__default__"
4✔
46

47
_T = TypeVar("_T")
4✔
48

49

50
DEFAULT_TEST_FILE_GLOBS = ("test_*.py", "*_test.py", "tests.py")
4✔
51
DEFAULT_TESTUTIL_FILE_GLOBS = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
4✔
52

53

54
class PythonSetup(Subsystem):
4✔
55
    options_scope = "python"
4✔
56
    help = "Options for Pants's Python backend."
4✔
57

58
    default_interpreter_universe = [
4✔
59
        "2.7",
60
        "3.5",
61
        "3.6",
62
        "3.7",
63
        "3.8",
64
        "3.9",
65
        "3.10",
66
        "3.11",
67
        "3.12",
68
        "3.13",
69
        "3.14",
70
    ]
71

72
    _interpreter_constraints = StrListOption(
4✔
73
        default=None,
74
        help=softwrap(
75
            """
76
            The Python interpreters your codebase is compatible with.
77

78
            These constraints are used as the default value for the `interpreter_constraints`
79
            field of Python targets.
80

81
            Specify with requirement syntax, e.g. `'CPython>=2.7,<3'` (A CPython interpreter with
82
            version >=2.7 AND version <3) or `'PyPy'` (A pypy interpreter of any version). Multiple
83
            constraint strings will be ORed together.
84
            """
85
        ),
86
        metavar="<requirement>",
87
    )
88

89
    warn_on_python2_usage = BoolOption(
4✔
90
        default=True,
91
        advanced=True,
92
        help=softwrap(
93
            """\
94
            True if Pants should generate a deprecation warning when Python 2.x is used in interpreter constraints.
95

96
            As of Pants v2.24.x and later, Pants will no longer be tested regularly with Python 2.7.x. As such, going
97
            forward, Pants may or may not work with Python 2.7. This option allows disabling the deprecation
98
            warning announcing this policy change.
99
            """
100
        ),
101
    )
102

103
    @memoized_property
4✔
104
    def interpreter_constraints(self) -> tuple[str, ...]:
4✔
105
        if not self._interpreter_constraints:
4✔
106
            # TODO: This is a hacky affordance for Pants's own tests, dozens of which were
107
            #  written when Pants provided default ICs, and implicitly rely on that assumption.
108
            #  We'll probably want to find and modify all those tests to set an explicit IC, but
109
            #  that will take time.
110
            if "PYTEST_CURRENT_TEST" in os.environ:
4✔
111
                return (">=3.9,<3.15",)
4✔
112
            raise OptionsError(
×
113
                softwrap(
114
                    f"""\
115
                    You must explicitly specify the default Python interpreter versions your code
116
                    is intended to run against.
117

118
                    You specify these interpreter constraints using the `interpreter_constraints`
119
                    option in the `[python]` section of pants.toml.
120

121
                    We recommend constraining to a single interpreter minor version if you can,
122
                    e.g., `interpreter_constraints = ['==3.11.*']`, or at least a small number of
123
                    interpreter minor versions, e.g., `interpreter_constraints = ['>=3.10,<3.12']`.
124

125
                    Individual targets can override these default interpreter constraints,
126
                    if different parts of your codebase run against different python interpreter
127
                    versions in a single repo.
128

129
                    See {doc_url("docs/python/overview/interpreter-compatibility")} for details.
130
                    """
131
                ),
132
            )
133

134
        # Warn if Python 2.x is still in use. This warning should only be displayed once since this
135
        # function is memoized.
136
        if self.warn_on_python2_usage:
4✔
137
            # Side-step import cycle.
138
            from pants.backend.python.util_rules.interpreter_constraints import (
4✔
139
                warn_on_python2_usage_in_interpreter_constraints,
140
            )
141

142
            warn_on_python2_usage_in_interpreter_constraints(
4✔
143
                self._interpreter_constraints,
144
                description_of_origin="the `[python].interpreter_constraints` option",
145
            )
146

147
        return self._interpreter_constraints
4✔
148

149
    interpreter_versions_universe = StrListOption(
4✔
150
        default=default_interpreter_universe,
151
        help=softwrap(
152
            f"""
153
            All known Python major/minor interpreter versions that may be used by either
154
            your code or tools used by your code.
155

156
            This is used by Pants to robustly handle interpreter constraints, such as knowing
157
            when generating lockfiles which Python versions to check if your code is using.
158

159
            This does not control which interpreter your code will use. Instead, to set your
160
            interpreter constraints, update `[python].interpreter_constraints`, the
161
            `interpreter_constraints` field, and relevant tool options like
162
            `[isort].interpreter_constraints` to tell Pants which interpreters your code
163
            actually uses. See {doc_url("docs/python/overview/interpreter-compatibility")}.
164

165
            All elements must be the minor and major Python version, e.g. `'2.7'` or `'3.10'`. Do
166
            not include the patch version.
167
            """
168
        ),
169
        advanced=True,
170
    )
171
    enable_resolves = BoolOption(
4✔
172
        default=False,
173
        help=softwrap(
174
            """
175
            Set to true to enable lockfiles for user code. See `[python].resolves` for an
176
            explanation of this feature.
177

178
            This option is mutually exclusive with `[python].requirement_constraints`. We strongly
179
            recommend using this option because it:
180

181
              1. Uses `--hash` to validate that all downloaded files are expected, which reduces\
182
                the risk of supply chain attacks.
183
              2. Enforces that all transitive dependencies are in the lockfile, whereas\
184
                constraints allow you to leave off dependencies. This ensures your build is more\
185
                stable and reduces the risk of supply chain attacks.
186
              3. Allows you to have multiple lockfiles in your repository.
187
            """
188
        ),
189
        advanced=True,
190
        mutually_exclusive_group="lockfile",
191
    )
192
    resolves = DictOption[str](
4✔
193
        default={"python-default": "3rdparty/python/default.lock"},
194
        help=softwrap(
195
            f"""
196
            A mapping of logical names to lockfile paths used in your project.
197

198
            Many organizations only need a single resolve for their whole project, which is
199
            a good default and often the simplest thing to do. However, you may need multiple
200
            resolves, such as if you use two conflicting versions of a requirement in
201
            your repository.
202

203
            If you only need a single resolve, run `{bin_name()} generate-lockfiles` to
204
            generate the lockfile.
205

206
            If you need multiple resolves:
207

208
              1. Via this option, define multiple resolve names and their lockfile paths.\
209
                The names should be meaningful to your repository, such as `data-science` or\
210
                `pants-plugins`.
211
              2. Set the default with `[python].default_resolve`.
212
              3. Update your `python_requirement` targets with the `resolve` field to declare which\
213
                resolve they should be available in. They default to `[python].default_resolve`,\
214
                so you only need to update targets that you want in non-default resolves.\
215
                (Often you'll set this via the `python_requirements` or `poetry_requirements`\
216
                target generators)
217
              4. Run `{bin_name()} generate-lockfiles` to generate the lockfiles. If the results\
218
                aren't what you'd expect, adjust the prior step.
219
              5. Update any targets like `python_source` / `python_sources`,\
220
                `python_test` / `python_tests`, and `pex_binary` which need to set a non-default\
221
                resolve with the `resolve` field.
222

223
            If a target can work with multiple resolves, you can either use the `parametrize`
224
            mechanism or manually create a distinct target per resolve. See {doc_url("docs/using-pants/key-concepts/targets-and-build-files")}
225
            for information about `parametrize`.
226

227
            For example:
228

229
                python_sources(
230
                    resolve=parametrize("data-science", "web-app"),
231
                )
232

233
            You can name the lockfile paths what you would like; Pants does not expect a
234
            certain file extension or location.
235

236
            Only applies if `[python].enable_resolves` is true.
237
            """
238
        ),
239
        advanced=True,
240
    )
241
    default_resolve = StrOption(
4✔
242
        default="python-default",
243
        help=softwrap(
244
            """
245
            The default value used for the `resolve` field.
246

247
            The name must be defined as a resolve in `[python].resolves`.
248
            """
249
        ),
250
        advanced=True,
251
    )
252

253
    _default_to_resolve_interpreter_constraints = BoolOption(
4✔
254
        default=False,
255
        help=softwrap(
256
            """
257
            For Python targets with both `resolve` and `interpreter_constraints` fields, default to using the `interpreter_constraints` field of the resolve if `interpreter_constraints` is not set on the target itself.
258

259
            `[python].enable_resolves` must be `True` for this option to also be enabled. This will become True by default in a future version of Pants and eventually be deprecated and then removed.
260
            """
261
        ),
262
        advanced=True,
263
    )
264

265
    @memoized_property
4✔
266
    def default_to_resolve_interpreter_constraints(self) -> bool:
4✔
267
        if self._default_to_resolve_interpreter_constraints and not self.enable_resolves:
1✔
268
            raise OptionsError(
×
269
                softwrap(
270
                    """
271
                You cannot set `[python].default_to_resolve_interpreter_constraints = true` without setting `[python].enable_resolves = true`.
272

273
                Please either enable resolves or set `[python].default_to_resolve_interpreter_constraints = false` (the default setting).
274
                """
275
                )
276
            )
277
        return self._default_to_resolve_interpreter_constraints
1✔
278

279
    separate_lockfile_metadata_file = BoolOption(
4✔
280
        advanced=True,
281
        default=False,
282
        help=softwrap(
283
            """
284
            If set, lockfile metadata will be written to a separate sibling file, rather than
285
            prepended as a header to the lockfile (which has various disadvantages).
286
            This will soon become True by default and eventually the header option will be
287
            deprecated and then removed.
288
            """
289
        ),
290
    )
291
    default_run_goal_use_sandbox = BoolOption(
4✔
292
        default=True,
293
        help=softwrap(
294
            """
295
            The default value used for the `run_goal_use_sandbox` field of Python targets. See the
296
            relevant field for more details.
297
            """
298
        ),
299
    )
300
    pip_version = StrOption(
4✔
301
        default="24.2",
302
        help=softwrap(
303
            f"""
304
            Use this version of Pip for resolving requirements and generating lockfiles.
305

306
            The value used here must be one of the Pip versions supported by the underlying PEX
307
            version. See {doc_url("docs/python/overview/pex")} for details.
308

309
            N.B.: The `latest` value selects the latest of the choices listed by PEX which is not
310
            necessarily the latest Pip version released on PyPI.
311
            """
312
        ),
313
        advanced=True,
314
    )
315
    _resolves_to_interpreter_constraints = DictOption[list[str]](
4✔
316
        help=softwrap(
317
            """
318
            Override the interpreter constraints to use when generating a resolve's lockfile
319
            with the `generate-lockfiles` goal.
320

321
            By default, each resolve from `[python].resolves` will use your
322
            global interpreter constraints set in `[python].interpreter_constraints`. With
323
            this option, you can override each resolve to use certain interpreter
324
            constraints, such as `{'data-science': ['==3.8.*']}`.
325

326
            Warning: this does NOT impact the interpreter constraints used by targets within the
327
            resolve, which is instead set by the option `[python].interpreter_constraints` and the
328
            `interpreter_constraints` field. It only impacts how the lockfile is generated.
329

330
            Pants will validate that the interpreter constraints of your code using a
331
            resolve are compatible with that resolve's own constraints. For example, if your
332
            code is set to use `['==3.9.*']` via the `interpreter_constraints` field, but it's
333
            using a resolve whose interpreter constraints are set to `['==3.7.*']`, then
334
            Pants will error explaining the incompatibility.
335

336
            The keys must be defined as resolves in `[python].resolves`.
337
            """
338
        ),
339
        advanced=True,
340
    )
341
    _resolves_to_constraints_file = DictOption[str](
4✔
342
        help=softwrap(
343
            f"""
344
            When generating a resolve's lockfile, use a constraints file to pin the version of
345
            certain requirements. This is particularly useful to pin the versions of transitive
346
            dependencies of your direct requirements.
347

348
            See https://pip.pypa.io/en/stable/user_guide/#constraints-files for more information on
349
            the format of constraint files and how constraints are applied in Pex and pip.
350

351
            Expects a dictionary of resolve names from `[python].resolves` and Python tools (e.g.
352
            `black` and `pytest`) to file paths for
353
            constraints files. For example,
354
            `{{'data-science': '3rdparty/data-science-constraints.txt'}}`.
355
            If a resolve is not set in the dictionary, it will not use a constraints file.
356

357
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
358
            resolves.
359
            """
360
        ),
361
        advanced=True,
362
    )
363
    _resolves_to_no_binary = DictOption[list[str]](
4✔
364
        help=softwrap(
365
            f"""
366
            When generating a resolve's lockfile, do not use binary packages (i.e. wheels) for
367
            these 3rdparty project names.
368

369
            Expects a dictionary of resolve names from `[python].resolves` and Python tools (e.g.
370
            `black` and `pytest`) to lists of project names. For example,
371
            `{{'data-science': ['requests', 'numpy']}}`. If a resolve is not set in the dictionary,
372
            it will have no restrictions on binary packages.
373

374
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
375
            resolves.
376

377
            For each resolve, you can also use the value `:all:` to disable all binary packages:
378
            `{{'data-science': [':all:']}}`.
379

380
            Note that some packages are tricky to compile and may fail to install when this option
381
            is used on them. See https://pip.pypa.io/en/stable/cli/pip_install/#install-no-binary
382
            for details.
383
            """
384
        ),
385
        advanced=True,
386
    )
387
    _resolves_to_only_binary = DictOption[list[str]](
4✔
388
        help=softwrap(
389
            f"""
390
            When generating a resolve's lockfile, do not use source packages (i.e. sdists) for
391
            these 3rdparty project names, e.g `['django', 'requests']`.
392

393
            Expects a dictionary of resolve names from `[python].resolves` and Python tools (e.g.
394
            `black` and `pytest`) to lists of project names. For example,
395
            `{{'data-science': ['requests', 'numpy']}}`. If a resolve is not set in the dictionary,
396
            it will have no restrictions on source packages.
397

398
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
399
            resolves.
400

401
            For each resolve you can use the value `:all:` to disable all source packages:
402
            `{{'data-science': [':all:']}}`.
403

404
            Packages without binary distributions will fail to install when this option is used on
405
            them. See https://pip.pypa.io/en/stable/cli/pip_install/#install-only-binary for
406
            details.
407
            """
408
        ),
409
        advanced=True,
410
    )
411
    _resolves_to_excludes = DictOption[list[str]](
4✔
412
        help=softwrap(
413
            """ Specifies requirements to exclude from a resolve and its
414
            lockfile.  Any distribution included in the PEX's resolve that
415
            matches the requirement is excluded from the built PEX along with
416
            all of its transitive dependencies that are not also required by
417
            other non-excluded distributions.  At runtime, the PEX will boot
418
            without checking the excluded dependencies are available.
419
            """
420
        ),
421
        advanced=True,
422
    )
423
    _resolves_to_overrides = DictOption[list[str]](
4✔
424
        help=softwrap(
425
            """ Specifies a transitive requirement to override in a resolve
426
            and its lockfile.  Overrides can either modify an existing
427
            dependency on a project name by changing extras, version
428
            constraints or markers or else they can completely swap out the
429
            dependency for a dependency on another project altogether. For the
430
            former, simply supply the requirement you wish. For example,
431
            specifying `--override cowsay==5.0` will override any transitive
432
            dependency on cowsay that has any combination of extras, version
433
            constraints or markers with the requirement `cowsay==5.0`. To
434
            completely replace cowsay with another library altogether, you can
435
            specify an override like `--override cowsay=my-cowsay>2`. This
436
            will replace any transitive dependency on cowsay that has any
437
            combination of extras, version constraints or markers with the
438
            requirement `my-cowsay>2`."""
439
        ),
440
        advanced=True,
441
    )
442

443
    _resolves_to_sources = DictOption[list[str]](
4✔
444
        help=softwrap(""" Defines a limited scope to use a named find links repo or
445
            index for specific dependencies in a resolve and its lockfile.
446
            Sources take the form `<name>=<scope>` where the name must match
447
            a find links repo or index defined via `[python-repos].indexes` or
448
            `[python-repos].find_links`. The scope can be a project name
449
            (e.g., `internal=torch` to resolve the `torch` project from the
450
            `internal` repo), a project name with a marker (e.g.,
451
            `internal=torch; sys_platform != 'darwin'` to resolve `torch` from
452
            the `internal` repo except on macOS), or just a marker (e.g.,
453
            `piwheels=platform_machine == 'armv7l'` to resolve from the
454
            `piwheels` repo when targeting 32bit ARM machines)."""),
455
        advanced=True,
456
    )
457

458
    _resolves_to_lock_style = DictOption[str](
4✔
459
        help=softwrap(
460
            f"""
461
            The style of lock to generate. Valid values are 'strict', 'sources', or 'universal'
462
            (additional styles may be supported in future PEX versions).
463

464
            The 'strict' style generates a lock file that contains exactly the
465
            distributions that would be used in a local PEX build. If an sdist would be used, the sdist is included, but if a
466
            wheel would be used, an accompanying sdist will not be included. The 'sources' style includes locks containing both
467
            wheels and the associated sdists when available. The 'universal' style generates a universal lock for all possible
468
            target interpreters and platforms, although the scope can be constrained via `[python].resolves_to_interpreter_constraints`. Of
469
            the three lock styles, only 'strict' can give you full confidence in the lock since it includes exactly the artifacts
470
            that are included in the local PEX you'll build to test the lock result with before checking in the lock. With the
471
            other two styles you lock un-vetted artifacts in addition to the 'strict' ones; so, even though you can be sure to
472
            reproducibly resolve those same un-vetted artifacts in the future, they're still un-vetted and could be innocently or
473
            maliciously different from the 'strict' artifacts you can locally vet before committing the lock to version control.
474
            The effects of the differences could range from failing a resolve using the lock when the un-vetted artifacts have
475
            different dependencies from their sibling artifacts, to your application crashing due to different code in the sibling
476
            artifacts to being compromised by differing code in the sibling artifacts. So, although the more permissive lock
477
            styles will allow the lock to work on a wider range of machines /are apparently more convenient, the convenience comes
478
            with a potential price and using these styles should be considered carefully.
479

480
            Expects a dictionary of resolve names from `[python].resolves` to style values.
481
            If a resolve is not set in the dictionary, it will default to 'universal'.
482

483
            Examples:
484
            - `{{'data-science': 'strict', 'web-app': 'universal'}}` - use strict style for data-science resolve, universal for web-app
485
            - `{{'python-default': 'sources'}}` - use sources style for the default resolve
486

487
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
488
            resolves.
489

490
            See https://docs.pex-tool.org/api/pex.html for more information on lockfile styles.
491
            """
492
        ),
493
        advanced=True,
494
    )
495

496
    _resolves_to_complete_platforms = DictOption[list[str]](
4✔
497
        help=softwrap(
498
            f"""
499
            The platforms the built PEX should be compatible with when generating lockfiles.
500

501
            Complete platforms allow you to create lockfiles for specific target platforms
502
            (e.g., different CPU architectures or operating systems) rather than the default
503
            universal platforms. This is particularly useful for cross-platform builds or
504
            when you need strict platform-specific dependencies.
505

506
            You can give a list of multiple complete platforms to create a multiplatform lockfile,
507
            meaning that the lockfile will include wheels for all of the supported environments.
508

509
            Expects a dictionary of resolve names from `[python].resolves` to lists of addresses of
510
            `file` or `resource` targets that point to files containing complete platform JSON as
511
            described by Pex (https://pex.readthedocs.io/en/latest/buildingpex.html#complete-platform).
512

513
            For example:
514
            `{{'python-default': ['3rdparty/platforms:linux_aarch64', '3rdparty/platforms:macos_arm64']}}`.
515

516
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
517
            resolves.
518

519
            Complete platform JSON files can be generated using PEX's interpreter inspect command on
520
            the target platform: `pex3 interpreter inspect --markers --tags > platform.json`
521

522
            See https://docs.pex-tool.org for more information.
523
            """
524
        ),
525
        advanced=True,
526
    )
527

528
    invalid_lockfile_behavior = EnumOption(
4✔
529
        default=InvalidLockfileBehavior.error,
530
        help=softwrap(
531
            """
532
            The behavior when a lockfile has requirements or interpreter constraints that are
533
            not compatible with what the current build is using.
534

535
            We recommend keeping the default of `error` for CI builds.
536

537
            Note that `warn` will still expect a Pants lockfile header, it only won't error if
538
            the lockfile is stale and should be regenerated.
539

540
            Use `ignore` to avoid needing a lockfile header at all, e.g. if you are manually
541
            managing lockfiles rather than using the `generate-lockfiles` goal.
542
            """
543
        ),
544
        advanced=True,
545
    )
546
    resolves_generate_lockfiles = BoolOption(
4✔
547
        default=True,
548
        help=softwrap(
549
            """
550
            If False, Pants will not attempt to generate lockfiles for `[python].resolves` when
551
            running the `generate-lockfiles` goal.
552

553
            This is intended to allow you to manually generate lockfiles for your own code,
554
            rather than using Pex lockfiles. For example, when adopting Pants in a project already
555
            using Poetry, you can use `poetry export --dev` to create a requirements.txt-style
556
            lockfile understood by Pants, then point `[python].resolves` to the file.
557

558
            If you set this to False, Pants will not attempt to validate the metadata headers
559
            for your user lockfiles. This is useful so that you can keep
560
            `[python].invalid_lockfile_behavior` to `error` or `warn` if you'd like so that tool
561
            lockfiles continue to be validated, while user lockfiles are skipped.
562

563
            Warning: it will likely be slower to install manually generated user lockfiles than Pex
564
            ones because Pants cannot as efficiently extract the subset of requirements used for a
565
            particular task. See the option `[python].run_against_entire_lockfile`.
566
            """
567
        ),
568
        advanced=True,
569
    )
570
    run_against_entire_lockfile = BoolOption(
4✔
571
        default=False,
572
        help=softwrap(
573
            """
574
            If enabled, when running binaries, tests, and repls, Pants will use the entire
575
            lockfile file instead of just the relevant subset.
576

577
            If you are using Pex lockfiles, we generally do not recommend this. You will already
578
            get similar performance benefits to this option, without the downsides.
579

580
            Otherwise, this option can improve performance and reduce cache size.
581
            But it has two consequences:
582
            1) All cached test results will be invalidated if any requirement in the lockfile
583
               changes, rather than just those that depend on the changed requirement.
584
            2) Requirements unneeded by a test/run/repl will be present on the sys.path, which
585
               might in rare cases cause their behavior to change.
586

587
            This option does not affect packaging deployable artifacts, such as
588
            PEX files, wheels and cloud functions, which will still use just the exact
589
            subset of requirements needed.
590
            """
591
        ),
592
        advanced=True,
593
    )
594

595
    __constraints_deprecation_msg = softwrap(
4✔
596
        f"""
597
        We encourage instead migrating to `[python].enable_resolves` and `[python].resolves`,
598
        which is an improvement over this option. The `[python].resolves` feature ensures that
599
        your lockfiles are fully comprehensive, i.e. include all transitive dependencies;
600
        uses hashes for better supply chain security; and supports advanced features like VCS
601
        and local requirements, along with options `[python].resolves_to_only_binary`.
602

603
        To migrate, stop setting `[python].requirement_constraints` and
604
        `[python].resolve_all_constraints`, and instead set `[python].enable_resolves` to
605
        `true`. Then, run `{bin_name()} generate-lockfiles`.
606
        """
607
    )
608
    requirement_constraints = FileOption(
4✔
609
        default=None,
610
        help=softwrap(
611
            """
612
            When resolving third-party requirements for your own code (vs. tools you run),
613
            use this constraints file to determine which versions to use.
614

615
            Mutually exclusive with `[python].enable_resolves`, which we generally recommend as an
616
            improvement over constraints file.
617

618
            See https://pip.pypa.io/en/stable/user_guide/#constraints-files for more
619
            information on the format of constraint files and how constraints are applied in
620
            Pex and pip.
621

622
            This only applies when resolving user requirements, rather than tools you run
623
            like Black and Pytest. To constrain tools, set `[tool].lockfile`, e.g.
624
            `[black].lockfile`.
625
            """
626
        ),
627
        advanced=True,
628
        mutually_exclusive_group="lockfile",
629
        removal_version="3.0.0.dev0",
630
        removal_hint=__constraints_deprecation_msg,
631
    )
632
    _resolve_all_constraints = BoolOption(
4✔
633
        default=True,
634
        help=softwrap(
635
            """
636
            (Only relevant when using `[python].requirement_constraints.`) If enabled, when
637
            resolving requirements, Pants will first resolve your entire
638
            constraints file as a single global resolve. Then, if the code uses a subset of
639
            your constraints file, Pants will extract the relevant requirements from that
640
            global resolve so that only what's actually needed gets used. If disabled, Pants
641
            will not use a global resolve and will resolve each subset of your requirements
642
            independently.
643

644
            Usually this option should be enabled because it can result in far fewer resolves.
645
            """
646
        ),
647
        advanced=True,
648
        removal_version="3.0.0.dev0",
649
        removal_hint=__constraints_deprecation_msg,
650
    )
651
    resolver_manylinux = StrOption(
4✔
652
        default="manylinux2014",
653
        help=softwrap(
654
            """
655
            Whether to allow resolution of manylinux wheels when resolving requirements for
656
            foreign linux platforms. The value should be a manylinux platform upper bound,
657
            e.g. `'manylinux2010'`, or else the string `'no'` to disallow.
658
            """
659
        ),
660
        advanced=True,
661
    )
662

663
    tailor_source_targets = BoolOption(
4✔
664
        default=True,
665
        help=softwrap(
666
            """
667
            If true, add `python_sources`, `python_tests`, and `python_test_utils` targets with
668
            the `tailor` goal."""
669
        ),
670
        advanced=True,
671
    )
672
    tailor_ignore_empty_init_files = BoolOption(
4✔
673
        "--tailor-ignore-empty-init-files",
674
        default=True,
675
        help=softwrap(
676
            """
677
            If true, don't add `python_sources` targets for `__init__.py` files that are both empty
678
            and where there are no other Python files in the directory.
679

680
            Empty and solitary `__init__.py` files usually exist as import scaffolding rather than
681
            true library code, so it can be noisy to add BUILD files.
682

683
            Even if this option is set to true, Pants will still ensure the empty `__init__.py`
684
            files are included in the sandbox when running processes.
685

686
            If you set to false, you may also want to set `[python-infer].init_files = "always"`.
687
            """
688
        ),
689
        advanced=True,
690
    )
691
    tailor_requirements_targets = BoolOption(
4✔
692
        default=True,
693
        help=softwrap(
694
            """
695
            If true, add `python_requirements`, `poetry_requirements`, and `pipenv_requirements`
696
            target generators with the `tailor` goal.
697

698
            `python_requirements` targets are added for any file that matches the pattern
699
            `*requirements*.txt`. You will need to manually add `python_requirements` for different
700
            file names like `reqs.txt`.
701

702
            `poetry_requirements` targets are added for `pyproject.toml` files with `[tool.poetry`
703
            in them.
704
            """
705
        ),
706
        advanced=True,
707
    )
708
    tailor_pex_binary_targets = BoolOption(
4✔
709
        default=False,
710
        help=softwrap(
711
            """
712
            If true, add `pex_binary` targets for Python files named `__main__.py` or with a
713
            `__main__` clause with the `tailor` goal.
714
            """
715
        ),
716
        advanced=True,
717
    )
718
    tailor_py_typed_targets = BoolOption(
4✔
719
        default=True,
720
        help=softwrap(
721
            """
722
            If true, add `resource` targets for marker files named `py.typed` with the `tailor` goal.
723
            """
724
        ),
725
        advanced=True,
726
    )
727
    tailor_test_file_globs = StrListOption(
4✔
728
        default=list(DEFAULT_TEST_FILE_GLOBS),
729
        help=softwrap(
730
            """
731
        Globs to match your test files. Used to decide which files should tailor
732
        python_tests() vs python_sources() targets.
733

734
        NB: This doesn't change the default set of files that a target owns, just the files
735
          that trigger tailoring of a target. If you need those to match (and you almost always do)
736
          then you should also create custom target type macros whose globs match these, and list
737
          them as substitutes for python_tests() and python_sources() in `[tailor].alias_mapping`.
738
          (see {doc_url("docs/writing-plugins/macros")} and
739
          {doc_url("reference/goals/tailor#alias_mapping")}).
740

741
        For example, you can set `[python].tailor_test_file_globs` to `["*_mytests.py"]`, and then
742
        create `my_pants_macros.py` with:
743

744
        ```
745
        def my_python_tests(**kwargs):
746
            if "sources" not in kwargs:
747
                kwargs["sources"] = ["*_mytests.py"]
748
            python_tests(**kwargs)
749

750

751
        def my_python_sources(**kwargs):
752
            if "sources" not in kwargs:
753
                kwargs["sources"] = ["*.py", "!*_mytests.py"]
754
            python_sources(**kwargs)
755
        ```
756

757
        And set the following in `pants.toml`:
758

759
        [global]
760
        build_file_prelude_globs = ["my_pants_macros.py"]
761

762
        [tailor]
763
        alias_mapping = { python_sources = "my_python_sources", python_tests = "my_python_tests" }
764
        """
765
        ),
766
        metavar="glob",
767
    )
768
    tailor_testutils_file_globs = StrListOption(
4✔
769
        default=list(DEFAULT_TESTUTIL_FILE_GLOBS),
770
        help=softwrap(
771
            """
772
        Globs to match your testutil files. Used to decide which files should tailor
773
        python_test_utils() vs python_sources() targets.
774

775
        See tailor_test_file_globs above for caveats and usage.
776
        """
777
        ),
778
        metavar="glob",
779
    )
780
    macos_big_sur_compatibility = BoolOption(
4✔
781
        default=False,
782
        help=softwrap(
783
            """
784
            If set, and if running on macOS Big Sur, use `macosx_10_16` as the platform
785
            when building wheels. Otherwise, the default of `macosx_11_0` will be used.
786
            This may be required for `pip` to be able to install the resulting distribution
787
            on Big Sur.
788
            """
789
        ),
790
        advanced=True,
791
    )
792
    enable_lockfile_targets = BoolOption(
4✔
793
        default=True,
794
        help=softwrap(
795
            """
796
            Create targets for all Python lockfiles defined in `[python].resolves`.
797

798
            The lockfile targets will then be used as dependencies to the `python_requirement`
799
            targets that use them, invalidating source targets per resolve when the lockfile
800
            changes.
801

802
            If another targets address is in conflict with the created lockfile target, it will
803
            shadow the lockfile target and it will not be available as a dependency for any
804
            `python_requirement` targets.
805
            """
806
        ),
807
        advanced=True,
808
    )
809
    repl_history = BoolOption(
4✔
810
        default=True,
811
        help="Whether to use the standard Python command history file when running a repl.",
812
    )
813

814
    @property
4✔
815
    def enable_synthetic_lockfiles(self) -> bool:
4✔
816
        return self.enable_resolves and self.enable_lockfile_targets
3✔
817

818
    @memoized_property
4✔
819
    def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]:
4✔
820
        result = {}
4✔
821
        unrecognized_resolves = []
4✔
822
        for resolve, ics in self._resolves_to_interpreter_constraints.items():
4✔
823
            if resolve not in self.resolves:
1✔
UNCOV
824
                unrecognized_resolves.append(resolve)
×
825
            if ics and self.warn_on_python2_usage:
1✔
826
                # Side-step import cycle.
827
                from pants.backend.python.util_rules.interpreter_constraints import (
1✔
828
                    warn_on_python2_usage_in_interpreter_constraints,
829
                )
830

831
                warn_on_python2_usage_in_interpreter_constraints(
1✔
832
                    ics,
833
                    description_of_origin=f"the `[python].resolves_to_interpreter_constraints` option for resolve {resolve}",
834
                )
835

836
            result[resolve] = tuple(ics)
1✔
837
        if unrecognized_resolves:
4✔
UNCOV
838
            raise UnrecognizedResolveNamesError(
×
839
                unrecognized_resolves,
840
                self.resolves.keys(),
841
                description_of_origin="the option `[python].resolves_to_interpreter_constraints`",
842
            )
843
        return result
4✔
844

845
    def _resolves_to_option_helper(
4✔
846
        self,
847
        option_value: dict[str, _T],
848
        option_name: str,
849
    ) -> dict[str, _T]:
850
        all_valid_resolves = set(self.resolves)
4✔
851
        unrecognized_resolves = set(option_value.keys()) - {
4✔
852
            RESOLVE_OPTION_KEY__DEFAULT,
853
            *all_valid_resolves,
854
        }
855
        if unrecognized_resolves:
4✔
UNCOV
856
            raise UnrecognizedResolveNamesError(
×
857
                sorted(unrecognized_resolves),
858
                {*all_valid_resolves, RESOLVE_OPTION_KEY__DEFAULT},
859
                description_of_origin=f"the option `[python].{option_name}`",
860
            )
861
        default_val = option_value.get(RESOLVE_OPTION_KEY__DEFAULT)
4✔
862
        if not default_val:
4✔
863
            return option_value
4✔
UNCOV
864
        return {resolve: option_value.get(resolve, default_val) for resolve in all_valid_resolves}
×
865

866
    @memoized_method
4✔
867
    def resolves_to_constraints_file(self) -> dict[str, str]:
4✔
868
        return self._resolves_to_option_helper(
4✔
869
            self._resolves_to_constraints_file,
870
            "resolves_to_constraints_file",
871
        )
872

873
    @memoized_method
4✔
874
    def resolves_to_no_binary(self) -> dict[str, list[str]]:
4✔
875
        return {
4✔
876
            resolve: [canonicalize_name(v) for v in vals]
877
            for resolve, vals in self._resolves_to_option_helper(
878
                self._resolves_to_no_binary,
879
                "resolves_to_no_binary",
880
            ).items()
881
        }
882

883
    @memoized_method
4✔
884
    def resolves_to_only_binary(self) -> dict[str, list[str]]:
4✔
885
        return {
4✔
886
            resolve: sorted([canonicalize_name(v) for v in vals])
887
            for resolve, vals in self._resolves_to_option_helper(
888
                self._resolves_to_only_binary,
889
                "resolves_to_only_binary",
890
            ).items()
891
        }
892

893
    @memoized_method
4✔
894
    def resolves_to_excludes(self) -> dict[str, list[str]]:
4✔
895
        return {
4✔
896
            resolve: sorted(vals)
897
            for resolve, vals in self._resolves_to_option_helper(
898
                self._resolves_to_excludes,
899
                "resolves_to_excludes",
900
            ).items()
901
        }
902

903
    @memoized_method
4✔
904
    def resolves_to_overrides(self) -> dict[str, list[str]]:
4✔
905
        return {
4✔
906
            resolve: sorted(vals)
907
            for resolve, vals in self._resolves_to_option_helper(
908
                self._resolves_to_overrides,
909
                "resolves_to_overrides",
910
            ).items()
911
        }
912

913
    @memoized_method
4✔
914
    def resolves_to_sources(self) -> dict[str, list[str]]:
4✔
915
        return {
4✔
916
            resolve: sorted(vals)
917
            for resolve, vals in self._resolves_to_option_helper(
918
                self._resolves_to_sources,
919
                "resolves_to_sources",
920
            ).items()
921
        }
922

923
    @memoized_method
4✔
924
    def resolves_to_lock_style(self) -> dict[str, str]:
4✔
925
        return self._resolves_to_option_helper(
4✔
926
            self._resolves_to_lock_style,
927
            "resolves_to_lock_style",
928
        )
929

930
    @memoized_method
4✔
931
    def resolves_to_complete_platforms(self) -> dict[str, list[str]]:
4✔
932
        return self._resolves_to_option_helper(
4✔
933
            self._resolves_to_complete_platforms,
934
            "resolves_to_complete_platforms",
935
        )
936

937
    @property
4✔
938
    def manylinux(self) -> str | None:
4✔
939
        manylinux = cast(str | None, self.resolver_manylinux)
4✔
940
        if manylinux is None or manylinux.lower() in ("false", "no", "none"):
4✔
941
            return None
×
942
        return manylinux
4✔
943

944
    @property
4✔
945
    def resolve_all_constraints(self) -> bool:
4✔
946
        if (
4✔
947
            self._resolve_all_constraints
948
            and not self.options.is_default("resolve_all_constraints")
949
            and not self.requirement_constraints
950
        ):
UNCOV
951
            raise ValueError(
×
952
                softwrap(
953
                    """
954
                    `[python].resolve_all_constraints` is enabled, so
955
                    `[python].requirement_constraints` must also be set.
956
                    """
957
                )
958
            )
959
        return self._resolve_all_constraints
4✔
960

961
    @property
4✔
962
    def scratch_dir(self):
4✔
963
        return os.path.join(self.options.pants_workdir, *self.options_scope.split("."))
×
964

965
    def compatibility_or_constraints(
4✔
966
        self, compatibility: Iterable[str] | None, resolve: str | None
967
    ) -> tuple[str, ...]:
968
        """Return either the given `compatibility` field or the global interpreter constraints.
969

970
        If interpreter constraints are supplied by the CLI flag, return those only.
971
        """
972
        if self.options.is_flagged("interpreter_constraints"):
4✔
973
            return self.interpreter_constraints
3✔
974
        if compatibility:
4✔
975
            return tuple(compatibility)
3✔
976
        if resolve and self.default_to_resolve_interpreter_constraints:
4✔
977
            return self.resolves_to_interpreter_constraints.get(
1✔
978
                resolve, self.interpreter_constraints
979
            )
980
        return self.interpreter_constraints
4✔
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