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

pantsbuild / pants / 25405422172

05 May 2026 10:18PM UTC coverage: 92.879% (-0.07%) from 92.944%
25405422172

Pull #23319

github

web-flow
Merge c82d0f333 into e8b784f89
Pull Request #23319: [pants_ng] Scaffolding for a pants_ng mode.

25 of 76 new or added lines in 9 files covered. (32.89%)

209 existing lines in 15 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

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

97.5
/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
12✔
5

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

12
from packaging.utils import canonicalize_name
12✔
13

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

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

31

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

38

39
@enum.unique
12✔
40
class Resolver(enum.StrEnum):
12✔
41
    pex = enum.auto()
12✔
42
    uv = enum.auto()
12✔
43

44

45
RESOLVE_OPTION_KEY__DEFAULT = "__default__"
12✔
46

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

49

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

53

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

58
    default_interpreter_universe = [
12✔
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(
12✔
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(
12✔
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
12✔
104
    def interpreter_constraints(self) -> tuple[str, ...]:
12✔
105
        if not self._interpreter_constraints:
12✔
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:
12✔
111
                return (">=3.9,<3.15",)
12✔
UNCOV
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:
12✔
137
            # Side-step import cycle.
138
            from pants.backend.python.util_rules.interpreter_constraints import (
12✔
139
                warn_on_python2_usage_in_interpreter_constraints,
140
            )
141

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

147
        return self._interpreter_constraints
12✔
148

149
    interpreter_versions_universe = StrListOption(
12✔
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(
12✔
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](
12✔
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(
12✔
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(
12✔
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
12✔
266
    def default_to_resolve_interpreter_constraints(self) -> bool:
12✔
267
        if self._default_to_resolve_interpreter_constraints and not self.enable_resolves:
4✔
UNCOV
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
4✔
278

279
    separate_lockfile_metadata_file = BoolOption(
12✔
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(
12✔
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(
12✔
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
    resolver = EnumOption(
12✔
316
        default=Resolver.pex,
317
        help=softwrap(
318
            """
319
            Which tool to use for generating lockfiles.
320

321
            - `pex` (default): Use pip via PEX.
322
            - `uv` (experimental): Use uv.
323
            """
324
        ),
325
        advanced=True,
326
    )
327
    _resolves_to_interpreter_constraints = DictOption[list[str]](
12✔
328
        help=softwrap(
329
            """
330
            Override the interpreter constraints to use when generating a resolve's lockfile
331
            with the `generate-lockfiles` goal.
332

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

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

342
            Pants will validate that the interpreter constraints of your code using a
343
            resolve are compatible with that resolve's own constraints. For example, if your
344
            code is set to use `['==3.9.*']` via the `interpreter_constraints` field, but it's
345
            using a resolve whose interpreter constraints are set to `['==3.7.*']`, then
346
            Pants will error explaining the incompatibility.
347

348
            The keys must be defined as resolves in `[python].resolves`.
349
            """
350
        ),
351
        advanced=True,
352
    )
353
    _resolves_to_constraints_file = DictOption[str](
12✔
354
        help=softwrap(
355
            f"""
356
            When generating a resolve's lockfile, use a constraints file to pin the version of
357
            certain requirements. This is particularly useful to pin the versions of transitive
358
            dependencies of your direct requirements.
359

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

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

369
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
370
            resolves.
371
            """
372
        ),
373
        advanced=True,
374
    )
375
    _resolves_to_no_binary = DictOption[list[str]](
12✔
376
        help=softwrap(
377
            f"""
378
            When generating a resolve's lockfile, do not use binary packages (i.e. wheels) for
379
            these 3rdparty project names.
380

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

386
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
387
            resolves.
388

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

392
            Note that some packages are tricky to compile and may fail to install when this option
393
            is used on them. See https://pip.pypa.io/en/stable/cli/pip_install/#install-no-binary
394
            for details.
395
            """
396
        ),
397
        advanced=True,
398
    )
399
    _resolves_to_only_binary = DictOption[list[str]](
12✔
400
        help=softwrap(
401
            f"""
402
            When generating a resolve's lockfile, do not use source packages (i.e. sdists) for
403
            these 3rdparty project names, e.g `['django', 'requests']`.
404

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

410
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
411
            resolves.
412

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

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

455
    _resolves_to_sources = DictOption[list[str]](
12✔
456
        help=softwrap(""" Defines a limited scope to use a named find links repo or
457
            index for specific dependencies in a resolve and its lockfile.
458
            Sources take the form `<name>=<scope>` where the name must match
459
            a find links repo or index defined via `[python-repos].indexes` or
460
            `[python-repos].find_links`. The scope can be a project name
461
            (e.g., `internal=torch` to resolve the `torch` project from the
462
            `internal` repo), a project name with a marker (e.g.,
463
            `internal=torch; sys_platform != 'darwin'` to resolve `torch` from
464
            the `internal` repo except on macOS), or just a marker (e.g.,
465
            `piwheels=platform_machine == 'armv7l'` to resolve from the
466
            `piwheels` repo when targeting 32bit ARM machines)."""),
467
        advanced=True,
468
    )
469

470
    _resolves_to_lock_style = DictOption[str](
12✔
471
        help=softwrap(
472
            f"""
473
            The style of lock to generate. Valid values are 'strict', 'sources', or 'universal'
474
            (additional styles may be supported in future PEX versions).
475

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

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

495
            Examples:
496
            - `{{'data-science': 'strict', 'web-app': 'universal'}}` - use strict style for data-science resolve, universal for web-app
497
            - `{{'python-default': 'sources'}}` - use sources style for the default resolve
498

499
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
500
            resolves.
501

502
            See https://docs.pex-tool.org/api/pex.html for more information on lockfile styles.
503
            """
504
        ),
505
        advanced=True,
506
    )
507

508
    _resolves_to_complete_platforms = DictOption[list[str]](
12✔
509
        help=softwrap(
510
            f"""
511
            The platforms the built PEX should be compatible with when generating lockfiles.
512

513
            Complete platforms allow you to create lockfiles for specific target platforms
514
            (e.g., different CPU architectures or operating systems) rather than the default
515
            universal platforms. This is particularly useful for cross-platform builds or
516
            when you need strict platform-specific dependencies.
517

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

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

525
            For example:
526
            `{{'python-default': ['3rdparty/platforms:linux_aarch64', '3rdparty/platforms:macos_arm64']}}`.
527

528
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
529
            resolves.
530

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

534
            See https://docs.pex-tool.org for more information.
535
            """
536
        ),
537
        advanced=True,
538
    )
539

540
    _resolves_to_uploaded_prior_to = DictOption[str](
12✔
541
        help=softwrap(
542
            f"""
543
            Filter packages by upload time when generating lockfiles, only considering
544
            packages that were uploaded before the specified datetime. This is useful for
545
            creating reproducible lockfiles that reflect the state of a package index at a
546
            specific point in time.  Only applies to packages from remote indexes,
547
            not local files or VCS requirements.
548

549
            For example:
550
            `{{'python-default': '2025-03-16T00:00:00Z'}}`.
551

552
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
553
            resolves.
554

555
            See https://pip.pypa.io/en/stable/user_guide/#filtering-by-upload-time for more
556
            information and valid formats.
557
            """
558
        ),
559
        advanced=True,
560
    )
561

562
    invalid_lockfile_behavior = EnumOption(
12✔
563
        default=InvalidLockfileBehavior.error,
564
        help=softwrap(
565
            """
566
            The behavior when a lockfile has requirements or interpreter constraints that are
567
            not compatible with what the current build is using.
568

569
            We recommend keeping the default of `error` for CI builds.
570

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

574
            Use `ignore` to avoid needing a lockfile header at all, e.g. if you are manually
575
            managing lockfiles rather than using the `generate-lockfiles` goal.
576
            """
577
        ),
578
        advanced=True,
579
    )
580
    resolves_generate_lockfiles = BoolOption(
12✔
581
        default=True,
582
        help=softwrap(
583
            """
584
            If False, Pants will not attempt to generate lockfiles for `[python].resolves` when
585
            running the `generate-lockfiles` goal.
586

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

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

597
            Warning: it will likely be slower to install manually generated user lockfiles than Pex
598
            ones because Pants cannot as efficiently extract the subset of requirements used for a
599
            particular task. See the option `[python].run_against_entire_lockfile`.
600
            """
601
        ),
602
        advanced=True,
603
    )
604
    run_against_entire_lockfile = BoolOption(
12✔
605
        default=False,
606
        help=softwrap(
607
            """
608
            If enabled, when running binaries, tests, and repls, Pants will use the entire
609
            lockfile file instead of just the relevant subset.
610

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

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

621
            This option does not affect packaging deployable artifacts, such as
622
            PEX files, wheels and cloud functions, which will still use just the exact
623
            subset of requirements needed.
624
            """
625
        ),
626
        advanced=True,
627
    )
628

629
    __constraints_deprecation_msg = softwrap(
12✔
630
        f"""
631
        We encourage instead migrating to `[python].enable_resolves` and `[python].resolves`,
632
        which is an improvement over this option. The `[python].resolves` feature ensures that
633
        your lockfiles are fully comprehensive, i.e. include all transitive dependencies;
634
        uses hashes for better supply chain security; and supports advanced features like VCS
635
        and local requirements, along with options `[python].resolves_to_only_binary`.
636

637
        To migrate, stop setting `[python].requirement_constraints` and
638
        `[python].resolve_all_constraints`, and instead set `[python].enable_resolves` to
639
        `true`. Then, run `{bin_name()} generate-lockfiles`.
640
        """
641
    )
642
    requirement_constraints = FileOption(
12✔
643
        default=None,
644
        help=softwrap(
645
            """
646
            When resolving third-party requirements for your own code (vs. tools you run),
647
            use this constraints file to determine which versions to use.
648

649
            Mutually exclusive with `[python].enable_resolves`, which we generally recommend as an
650
            improvement over constraints file.
651

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

656
            This only applies when resolving user requirements, rather than tools you run
657
            like Black and Pytest. To constrain tools, set `[tool].lockfile`, e.g.
658
            `[black].lockfile`.
659
            """
660
        ),
661
        advanced=True,
662
        mutually_exclusive_group="lockfile",
663
        removal_version="3.0.0.dev0",
664
        removal_hint=__constraints_deprecation_msg,
665
    )
666
    _resolve_all_constraints = BoolOption(
12✔
667
        default=True,
668
        help=softwrap(
669
            """
670
            (Only relevant when using `[python].requirement_constraints.`) If enabled, when
671
            resolving requirements, Pants will first resolve your entire
672
            constraints file as a single global resolve. Then, if the code uses a subset of
673
            your constraints file, Pants will extract the relevant requirements from that
674
            global resolve so that only what's actually needed gets used. If disabled, Pants
675
            will not use a global resolve and will resolve each subset of your requirements
676
            independently.
677

678
            Usually this option should be enabled because it can result in far fewer resolves.
679
            """
680
        ),
681
        advanced=True,
682
        removal_version="3.0.0.dev0",
683
        removal_hint=__constraints_deprecation_msg,
684
    )
685
    resolver_manylinux = StrOption(
12✔
686
        default="manylinux2014",
687
        help=softwrap(
688
            """
689
            Whether to allow resolution of manylinux wheels when resolving requirements for
690
            foreign linux platforms. The value should be a manylinux platform upper bound,
691
            e.g. `'manylinux2010'`, or else the string `'no'` to disallow.
692
            """
693
        ),
694
        advanced=True,
695
    )
696

697
    tailor_source_targets = BoolOption(
12✔
698
        default=True,
699
        help=softwrap(
700
            """
701
            If true, add `python_sources`, `python_tests`, and `python_test_utils` targets with
702
            the `tailor` goal."""
703
        ),
704
        advanced=True,
705
    )
706
    tailor_ignore_empty_init_files = BoolOption(
12✔
707
        "--tailor-ignore-empty-init-files",
708
        default=True,
709
        help=softwrap(
710
            """
711
            If true, don't add `python_sources` targets for `__init__.py` files that are both empty
712
            and where there are no other Python files in the directory.
713

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

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

720
            If you set to false, you may also want to set `[python-infer].init_files = "always"`.
721
            """
722
        ),
723
        advanced=True,
724
    )
725
    tailor_requirements_targets = BoolOption(
12✔
726
        default=True,
727
        help=softwrap(
728
            """
729
            If true, add `python_requirements`, `poetry_requirements`, and `pipenv_requirements`
730
            target generators with the `tailor` goal.
731

732
            `python_requirements` targets are added for any file that matches the pattern
733
            `*requirements*.txt`. You will need to manually add `python_requirements` for different
734
            file names like `reqs.txt`.
735

736
            `poetry_requirements` targets are added for `pyproject.toml` files with `[tool.poetry`
737
            in them.
738
            """
739
        ),
740
        advanced=True,
741
    )
742
    tailor_pex_binary_targets = BoolOption(
12✔
743
        default=False,
744
        help=softwrap(
745
            """
746
            If true, add `pex_binary` targets for Python files named `__main__.py` or with a
747
            `__main__` clause with the `tailor` goal.
748
            """
749
        ),
750
        advanced=True,
751
    )
752
    tailor_py_typed_targets = BoolOption(
12✔
753
        default=True,
754
        help=softwrap(
755
            """
756
            If true, add `resource` targets for marker files named `py.typed` with the `tailor` goal.
757
            """
758
        ),
759
        advanced=True,
760
    )
761
    tailor_test_file_globs = StrListOption(
12✔
762
        default=list(DEFAULT_TEST_FILE_GLOBS),
763
        help=softwrap(
764
            """
765
        Globs to match your test files. Used to decide which files should tailor
766
        python_tests() vs python_sources() targets.
767

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

775
        For example, you can set `[python].tailor_test_file_globs` to `["*_mytests.py"]`, and then
776
        create `my_pants_macros.py` with:
777

778
        ```
779
        def my_python_tests(**kwargs):
780
            if "sources" not in kwargs:
781
                kwargs["sources"] = ["*_mytests.py"]
782
            python_tests(**kwargs)
783

784

785
        def my_python_sources(**kwargs):
786
            if "sources" not in kwargs:
787
                kwargs["sources"] = ["*.py", "!*_mytests.py"]
788
            python_sources(**kwargs)
789
        ```
790

791
        And set the following in `pants.toml`:
792

793
        [global]
794
        build_file_prelude_globs = ["my_pants_macros.py"]
795

796
        [tailor]
797
        alias_mapping = { python_sources = "my_python_sources", python_tests = "my_python_tests" }
798
        """
799
        ),
800
        metavar="glob",
801
    )
802
    tailor_testutils_file_globs = StrListOption(
12✔
803
        default=list(DEFAULT_TESTUTIL_FILE_GLOBS),
804
        help=softwrap(
805
            """
806
        Globs to match your testutil files. Used to decide which files should tailor
807
        python_test_utils() vs python_sources() targets.
808

809
        See tailor_test_file_globs above for caveats and usage.
810
        """
811
        ),
812
        metavar="glob",
813
    )
814
    macos_big_sur_compatibility = BoolOption(
12✔
815
        default=False,
816
        help=softwrap(
817
            """
818
            If set, and if running on macOS Big Sur, use `macosx_10_16` as the platform
819
            when building wheels. Otherwise, the default of `macosx_11_0` will be used.
820
            This may be required for `pip` to be able to install the resulting distribution
821
            on Big Sur.
822
            """
823
        ),
824
        advanced=True,
825
    )
826
    enable_lockfile_targets = BoolOption(
12✔
827
        default=True,
828
        help=softwrap(
829
            """
830
            Create targets for all Python lockfiles defined in `[python].resolves`.
831

832
            The lockfile targets will then be used as dependencies to the `python_requirement`
833
            targets that use them, invalidating source targets per resolve when the lockfile
834
            changes.
835

836
            If another targets address is in conflict with the created lockfile target, it will
837
            shadow the lockfile target and it will not be available as a dependency for any
838
            `python_requirement` targets.
839
            """
840
        ),
841
        advanced=True,
842
    )
843
    repl_history = BoolOption(
12✔
844
        default=True,
845
        help="Whether to use the standard Python command history file when running a repl.",
846
    )
847

848
    @property
12✔
849
    def enable_synthetic_lockfiles(self) -> bool:
12✔
850
        return self.enable_resolves and self.enable_lockfile_targets
11✔
851

852
    @memoized_property
12✔
853
    def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]:
12✔
854
        result = {}
12✔
855
        unrecognized_resolves = []
12✔
856
        for resolve, ics in self._resolves_to_interpreter_constraints.items():
12✔
857
            if resolve not in self.resolves:
3✔
858
                unrecognized_resolves.append(resolve)
1✔
859
            if ics and self.warn_on_python2_usage:
3✔
860
                # Side-step import cycle.
861
                from pants.backend.python.util_rules.interpreter_constraints import (
1✔
862
                    warn_on_python2_usage_in_interpreter_constraints,
863
                )
864

865
                warn_on_python2_usage_in_interpreter_constraints(
1✔
866
                    ics,
867
                    description_of_origin=f"the `[python].resolves_to_interpreter_constraints` option for resolve {resolve}",
868
                )
869

870
            result[resolve] = tuple(ics)
3✔
871
        if unrecognized_resolves:
12✔
872
            raise UnrecognizedResolveNamesError(
1✔
873
                unrecognized_resolves,
874
                self.resolves.keys(),
875
                description_of_origin="the option `[python].resolves_to_interpreter_constraints`",
876
            )
877
        return result
12✔
878

879
    def _resolves_to_option_helper(
12✔
880
        self,
881
        option_value: dict[str, _T],
882
        option_name: str,
883
    ) -> dict[str, _T]:
884
        all_valid_resolves = set(self.resolves)
12✔
885
        unrecognized_resolves = set(option_value.keys()) - {
12✔
886
            RESOLVE_OPTION_KEY__DEFAULT,
887
            *all_valid_resolves,
888
        }
889
        if unrecognized_resolves:
12✔
890
            raise UnrecognizedResolveNamesError(
1✔
891
                sorted(unrecognized_resolves),
892
                {*all_valid_resolves, RESOLVE_OPTION_KEY__DEFAULT},
893
                description_of_origin=f"the option `[python].{option_name}`",
894
            )
895
        default_val = option_value.get(RESOLVE_OPTION_KEY__DEFAULT)
12✔
896
        if not default_val:
12✔
897
            return option_value
12✔
898
        return {resolve: option_value.get(resolve, default_val) for resolve in all_valid_resolves}
2✔
899

900
    @memoized_method
12✔
901
    def resolves_to_constraints_file(self) -> dict[str, str]:
12✔
902
        return self._resolves_to_option_helper(
12✔
903
            self._resolves_to_constraints_file,
904
            "resolves_to_constraints_file",
905
        )
906

907
    @memoized_method
12✔
908
    def resolves_to_no_binary(self) -> dict[str, list[str]]:
12✔
909
        return {
12✔
910
            resolve: [canonicalize_name(v) for v in vals]
911
            for resolve, vals in self._resolves_to_option_helper(
912
                self._resolves_to_no_binary,
913
                "resolves_to_no_binary",
914
            ).items()
915
        }
916

917
    @memoized_method
12✔
918
    def resolves_to_only_binary(self) -> dict[str, list[str]]:
12✔
919
        return {
12✔
920
            resolve: sorted([canonicalize_name(v) for v in vals])
921
            for resolve, vals in self._resolves_to_option_helper(
922
                self._resolves_to_only_binary,
923
                "resolves_to_only_binary",
924
            ).items()
925
        }
926

927
    @memoized_method
12✔
928
    def resolves_to_excludes(self) -> dict[str, list[str]]:
12✔
929
        return {
12✔
930
            resolve: sorted(vals)
931
            for resolve, vals in self._resolves_to_option_helper(
932
                self._resolves_to_excludes,
933
                "resolves_to_excludes",
934
            ).items()
935
        }
936

937
    @memoized_method
12✔
938
    def resolves_to_overrides(self) -> dict[str, list[str]]:
12✔
939
        return {
12✔
940
            resolve: sorted(vals)
941
            for resolve, vals in self._resolves_to_option_helper(
942
                self._resolves_to_overrides,
943
                "resolves_to_overrides",
944
            ).items()
945
        }
946

947
    @memoized_method
12✔
948
    def resolves_to_sources(self) -> dict[str, list[str]]:
12✔
949
        return {
12✔
950
            resolve: sorted(vals)
951
            for resolve, vals in self._resolves_to_option_helper(
952
                self._resolves_to_sources,
953
                "resolves_to_sources",
954
            ).items()
955
        }
956

957
    @memoized_method
12✔
958
    def resolves_to_lock_style(self) -> dict[str, str]:
12✔
959
        return self._resolves_to_option_helper(
12✔
960
            self._resolves_to_lock_style,
961
            "resolves_to_lock_style",
962
        )
963

964
    @memoized_method
12✔
965
    def resolves_to_complete_platforms(self) -> dict[str, list[str]]:
12✔
966
        return self._resolves_to_option_helper(
12✔
967
            self._resolves_to_complete_platforms,
968
            "resolves_to_complete_platforms",
969
        )
970

971
    @memoized_method
12✔
972
    def resolves_to_uploaded_prior_to(self) -> dict[str, str]:
12✔
973
        return self._resolves_to_option_helper(
12✔
974
            self._resolves_to_uploaded_prior_to,
975
            "resolves_to_uploaded_prior_to",
976
        )
977

978
    @property
12✔
979
    def manylinux(self) -> str | None:
12✔
980
        manylinux = cast(str | None, self.resolver_manylinux)
12✔
981
        if manylinux is None or manylinux.lower() in ("false", "no", "none"):
12✔
UNCOV
982
            return None
×
983
        return manylinux
12✔
984

985
    @property
12✔
986
    def resolve_all_constraints(self) -> bool:
12✔
987
        if (
11✔
988
            self._resolve_all_constraints
989
            and not self.options.is_default("resolve_all_constraints")
990
            and not self.requirement_constraints
991
        ):
992
            raise ValueError(
1✔
993
                softwrap(
994
                    """
995
                    `[python].resolve_all_constraints` is enabled, so
996
                    `[python].requirement_constraints` must also be set.
997
                    """
998
                )
999
            )
1000
        return self._resolve_all_constraints
11✔
1001

1002
    @property
12✔
1003
    def scratch_dir(self):
12✔
UNCOV
1004
        return os.path.join(self.options.pants_workdir, *self.options_scope.split("."))
×
1005

1006
    def compatibility_or_constraints(
12✔
1007
        self, compatibility: Iterable[str] | None, resolve: str | None
1008
    ) -> tuple[str, ...]:
1009
        """Return either the given `compatibility` field or the global interpreter constraints.
1010

1011
        If interpreter constraints are supplied by the CLI flag, return those only.
1012
        """
1013
        if self.options.is_flagged("interpreter_constraints"):
12✔
1014
            return self.interpreter_constraints
8✔
1015
        if compatibility:
12✔
1016
            return tuple(compatibility)
11✔
1017
        if resolve and self.default_to_resolve_interpreter_constraints:
12✔
1018
            return self.resolves_to_interpreter_constraints.get(
1✔
1019
                resolve, self.interpreter_constraints
1020
            )
1021
        return self.interpreter_constraints
12✔
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