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

pantsbuild / pants / 24145945949

08 Apr 2026 04:14PM UTC coverage: 82.077% (-10.8%) from 92.91%
24145945949

Pull #23233

github

web-flow
Merge 089d98e3c into 9036734c9
Pull Request #23233: Introduce a LockfileFormat enum.

8 of 11 new or added lines in 4 files covered. (72.73%)

7635 existing lines in 306 files now uncovered.

63732 of 77649 relevant lines covered (82.08%)

2.96 hits per line

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

93.29
/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
7✔
5

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

12
from packaging.utils import canonicalize_name
7✔
13

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

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

31

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

38

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

44

45
@enum.unique
7✔
46
class PexBuilder(enum.Enum):
7✔
47
    pex = "pex"
7✔
48
    uv = "uv"
7✔
49

50

51
RESOLVE_OPTION_KEY__DEFAULT = "__default__"
7✔
52

53
_T = TypeVar("_T")
7✔
54

55

56
DEFAULT_TEST_FILE_GLOBS = ("test_*.py", "*_test.py", "tests.py")
7✔
57
DEFAULT_TESTUTIL_FILE_GLOBS = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
7✔
58

59

60
class PythonSetup(Subsystem):
7✔
61
    options_scope = "python"
7✔
62
    help = "Options for Pants's Python backend."
7✔
63

64
    default_interpreter_universe = [
7✔
65
        "2.7",
66
        "3.5",
67
        "3.6",
68
        "3.7",
69
        "3.8",
70
        "3.9",
71
        "3.10",
72
        "3.11",
73
        "3.12",
74
        "3.13",
75
        "3.14",
76
    ]
77

78
    _interpreter_constraints = StrListOption(
7✔
79
        default=None,
80
        help=softwrap(
81
            """
82
            The Python interpreters your codebase is compatible with.
83

84
            These constraints are used as the default value for the `interpreter_constraints`
85
            field of Python targets.
86

87
            Specify with requirement syntax, e.g. `'CPython>=2.7,<3'` (A CPython interpreter with
88
            version >=2.7 AND version <3) or `'PyPy'` (A pypy interpreter of any version). Multiple
89
            constraint strings will be ORed together.
90
            """
91
        ),
92
        metavar="<requirement>",
93
    )
94

95
    warn_on_python2_usage = BoolOption(
7✔
96
        default=True,
97
        advanced=True,
98
        help=softwrap(
99
            """\
100
            True if Pants should generate a deprecation warning when Python 2.x is used in interpreter constraints.
101

102
            As of Pants v2.24.x and later, Pants will no longer be tested regularly with Python 2.7.x. As such, going
103
            forward, Pants may or may not work with Python 2.7. This option allows disabling the deprecation
104
            warning announcing this policy change.
105
            """
106
        ),
107
    )
108

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

124
                    You specify these interpreter constraints using the `interpreter_constraints`
125
                    option in the `[python]` section of pants.toml.
126

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

131
                    Individual targets can override these default interpreter constraints,
132
                    if different parts of your codebase run against different python interpreter
133
                    versions in a single repo.
134

135
                    See {doc_url("docs/python/overview/interpreter-compatibility")} for details.
136
                    """
137
                ),
138
            )
139

140
        # Warn if Python 2.x is still in use. This warning should only be displayed once since this
141
        # function is memoized.
142
        if self.warn_on_python2_usage:
7✔
143
            # Side-step import cycle.
144
            from pants.backend.python.util_rules.interpreter_constraints import (
7✔
145
                warn_on_python2_usage_in_interpreter_constraints,
146
            )
147

148
            warn_on_python2_usage_in_interpreter_constraints(
7✔
149
                self._interpreter_constraints,
150
                description_of_origin="the `[python].interpreter_constraints` option",
151
            )
152

153
        return self._interpreter_constraints
7✔
154

155
    interpreter_versions_universe = StrListOption(
7✔
156
        default=default_interpreter_universe,
157
        help=softwrap(
158
            f"""
159
            All known Python major/minor interpreter versions that may be used by either
160
            your code or tools used by your code.
161

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

165
            This does not control which interpreter your code will use. Instead, to set your
166
            interpreter constraints, update `[python].interpreter_constraints`, the
167
            `interpreter_constraints` field, and relevant tool options like
168
            `[isort].interpreter_constraints` to tell Pants which interpreters your code
169
            actually uses. See {doc_url("docs/python/overview/interpreter-compatibility")}.
170

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

184
            This option is mutually exclusive with `[python].requirement_constraints`. We strongly
185
            recommend using this option because it:
186

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

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

209
            If you only need a single resolve, run `{bin_name()} generate-lockfiles` to
210
            generate the lockfile.
211

212
            If you need multiple resolves:
213

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

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

233
            For example:
234

235
                python_sources(
236
                    resolve=parametrize("data-science", "web-app"),
237
                )
238

239
            You can name the lockfile paths what you would like; Pants does not expect a
240
            certain file extension or location.
241

242
            Only applies if `[python].enable_resolves` is true.
243
            """
244
        ),
245
        advanced=True,
246
    )
247
    default_resolve = StrOption(
7✔
248
        default="python-default",
249
        help=softwrap(
250
            """
251
            The default value used for the `resolve` field.
252

253
            The name must be defined as a resolve in `[python].resolves`.
254
            """
255
        ),
256
        advanced=True,
257
    )
258

259
    _default_to_resolve_interpreter_constraints = BoolOption(
7✔
260
        default=False,
261
        help=softwrap(
262
            """
263
            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.
264

265
            `[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.
266
            """
267
        ),
268
        advanced=True,
269
    )
270

271
    @memoized_property
7✔
272
    def default_to_resolve_interpreter_constraints(self) -> bool:
7✔
273
        if self._default_to_resolve_interpreter_constraints and not self.enable_resolves:
1✔
274
            raise OptionsError(
×
275
                softwrap(
276
                    """
277
                You cannot set `[python].default_to_resolve_interpreter_constraints = true` without setting `[python].enable_resolves = true`.
278

279
                Please either enable resolves or set `[python].default_to_resolve_interpreter_constraints = false` (the default setting).
280
                """
281
                )
282
            )
283
        return self._default_to_resolve_interpreter_constraints
1✔
284

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

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

315
            N.B.: The `latest` value selects the latest of the choices listed by PEX which is not
316
            necessarily the latest Pip version released on PyPI.
317
            """
318
        ),
319
        advanced=True,
320
    )
321
    pex_builder = EnumOption(
7✔
322
        default=PexBuilder.pex,
323
        help=softwrap(
324
            """
325
            Which tool to use for installing dependencies when building PEX files.
326

327
            - `pex` (default): Use pip via PEX.
328
            - `uv` (experimental): Pre-install dependencies into a uv venv, then pass it
329
              to PEX via `--venv-repository`. When a PEX-native lockfile is available,
330
              uv installs the exact pinned versions with `--no-deps`.
331

332
            Only applies to non-internal, non-cross-platform PEX builds. Other builds
333
            silently fall back to pip.
334
            """
335
        ),
336
        advanced=True,
337
    )
338
    _resolves_to_interpreter_constraints = DictOption[list[str]](
7✔
339
        help=softwrap(
340
            """
341
            Override the interpreter constraints to use when generating a resolve's lockfile
342
            with the `generate-lockfiles` goal.
343

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

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

353
            Pants will validate that the interpreter constraints of your code using a
354
            resolve are compatible with that resolve's own constraints. For example, if your
355
            code is set to use `['==3.9.*']` via the `interpreter_constraints` field, but it's
356
            using a resolve whose interpreter constraints are set to `['==3.7.*']`, then
357
            Pants will error explaining the incompatibility.
358

359
            The keys must be defined as resolves in `[python].resolves`.
360
            """
361
        ),
362
        advanced=True,
363
    )
364
    _resolves_to_constraints_file = DictOption[str](
7✔
365
        help=softwrap(
366
            f"""
367
            When generating a resolve's lockfile, use a constraints file to pin the version of
368
            certain requirements. This is particularly useful to pin the versions of transitive
369
            dependencies of your direct requirements.
370

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

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

380
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
381
            resolves.
382
            """
383
        ),
384
        advanced=True,
385
    )
386
    _resolves_to_no_binary = DictOption[list[str]](
7✔
387
        help=softwrap(
388
            f"""
389
            When generating a resolve's lockfile, do not use binary packages (i.e. wheels) for
390
            these 3rdparty project names.
391

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

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

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

403
            Note that some packages are tricky to compile and may fail to install when this option
404
            is used on them. See https://pip.pypa.io/en/stable/cli/pip_install/#install-no-binary
405
            for details.
406
            """
407
        ),
408
        advanced=True,
409
    )
410
    _resolves_to_only_binary = DictOption[list[str]](
7✔
411
        help=softwrap(
412
            f"""
413
            When generating a resolve's lockfile, do not use source packages (i.e. sdists) for
414
            these 3rdparty project names, e.g `['django', 'requests']`.
415

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

421
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
422
            resolves.
423

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

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

466
    _resolves_to_sources = DictOption[list[str]](
7✔
467
        help=softwrap(""" Defines a limited scope to use a named find links repo or
468
            index for specific dependencies in a resolve and its lockfile.
469
            Sources take the form `<name>=<scope>` where the name must match
470
            a find links repo or index defined via `[python-repos].indexes` or
471
            `[python-repos].find_links`. The scope can be a project name
472
            (e.g., `internal=torch` to resolve the `torch` project from the
473
            `internal` repo), a project name with a marker (e.g.,
474
            `internal=torch; sys_platform != 'darwin'` to resolve `torch` from
475
            the `internal` repo except on macOS), or just a marker (e.g.,
476
            `piwheels=platform_machine == 'armv7l'` to resolve from the
477
            `piwheels` repo when targeting 32bit ARM machines)."""),
478
        advanced=True,
479
    )
480

481
    _resolves_to_lock_style = DictOption[str](
7✔
482
        help=softwrap(
483
            f"""
484
            The style of lock to generate. Valid values are 'strict', 'sources', or 'universal'
485
            (additional styles may be supported in future PEX versions).
486

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

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

506
            Examples:
507
            - `{{'data-science': 'strict', 'web-app': 'universal'}}` - use strict style for data-science resolve, universal for web-app
508
            - `{{'python-default': 'sources'}}` - use sources style for the default resolve
509

510
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
511
            resolves.
512

513
            See https://docs.pex-tool.org/api/pex.html for more information on lockfile styles.
514
            """
515
        ),
516
        advanced=True,
517
    )
518

519
    _resolves_to_complete_platforms = DictOption[list[str]](
7✔
520
        help=softwrap(
521
            f"""
522
            The platforms the built PEX should be compatible with when generating lockfiles.
523

524
            Complete platforms allow you to create lockfiles for specific target platforms
525
            (e.g., different CPU architectures or operating systems) rather than the default
526
            universal platforms. This is particularly useful for cross-platform builds or
527
            when you need strict platform-specific dependencies.
528

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

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

536
            For example:
537
            `{{'python-default': ['3rdparty/platforms:linux_aarch64', '3rdparty/platforms:macos_arm64']}}`.
538

539
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
540
            resolves.
541

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

545
            See https://docs.pex-tool.org for more information.
546
            """
547
        ),
548
        advanced=True,
549
    )
550

551
    _resolves_to_uploaded_prior_to = DictOption[str](
7✔
552
        help=softwrap(
553
            f"""
554
            Filter packages by upload time when generating lockfiles, only considering
555
            packages that were uploaded before the specified datetime. This is useful for
556
            creating reproducible lockfiles that reflect the state of a package index at a
557
            specific point in time.  Only applies to packages from remote indexes,
558
            not local files or VCS requirements.
559

560
            For example:
561
            `{{'python-default': '2025-03-16T00:00:00Z'}}`.
562

563
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
564
            resolves.
565

566
            See https://pip.pypa.io/en/stable/user_guide/#filtering-by-upload-time for more
567
            information and valid formats.
568
            """
569
        ),
570
        advanced=True,
571
    )
572

573
    invalid_lockfile_behavior = EnumOption(
7✔
574
        default=InvalidLockfileBehavior.error,
575
        help=softwrap(
576
            """
577
            The behavior when a lockfile has requirements or interpreter constraints that are
578
            not compatible with what the current build is using.
579

580
            We recommend keeping the default of `error` for CI builds.
581

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

585
            Use `ignore` to avoid needing a lockfile header at all, e.g. if you are manually
586
            managing lockfiles rather than using the `generate-lockfiles` goal.
587
            """
588
        ),
589
        advanced=True,
590
    )
591
    resolves_generate_lockfiles = BoolOption(
7✔
592
        default=True,
593
        help=softwrap(
594
            """
595
            If False, Pants will not attempt to generate lockfiles for `[python].resolves` when
596
            running the `generate-lockfiles` goal.
597

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

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

608
            Warning: it will likely be slower to install manually generated user lockfiles than Pex
609
            ones because Pants cannot as efficiently extract the subset of requirements used for a
610
            particular task. See the option `[python].run_against_entire_lockfile`.
611
            """
612
        ),
613
        advanced=True,
614
    )
615
    run_against_entire_lockfile = BoolOption(
7✔
616
        default=False,
617
        help=softwrap(
618
            """
619
            If enabled, when running binaries, tests, and repls, Pants will use the entire
620
            lockfile file instead of just the relevant subset.
621

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

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

632
            This option does not affect packaging deployable artifacts, such as
633
            PEX files, wheels and cloud functions, which will still use just the exact
634
            subset of requirements needed.
635
            """
636
        ),
637
        advanced=True,
638
    )
639

640
    __constraints_deprecation_msg = softwrap(
7✔
641
        f"""
642
        We encourage instead migrating to `[python].enable_resolves` and `[python].resolves`,
643
        which is an improvement over this option. The `[python].resolves` feature ensures that
644
        your lockfiles are fully comprehensive, i.e. include all transitive dependencies;
645
        uses hashes for better supply chain security; and supports advanced features like VCS
646
        and local requirements, along with options `[python].resolves_to_only_binary`.
647

648
        To migrate, stop setting `[python].requirement_constraints` and
649
        `[python].resolve_all_constraints`, and instead set `[python].enable_resolves` to
650
        `true`. Then, run `{bin_name()} generate-lockfiles`.
651
        """
652
    )
653
    requirement_constraints = FileOption(
7✔
654
        default=None,
655
        help=softwrap(
656
            """
657
            When resolving third-party requirements for your own code (vs. tools you run),
658
            use this constraints file to determine which versions to use.
659

660
            Mutually exclusive with `[python].enable_resolves`, which we generally recommend as an
661
            improvement over constraints file.
662

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

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

689
            Usually this option should be enabled because it can result in far fewer resolves.
690
            """
691
        ),
692
        advanced=True,
693
        removal_version="3.0.0.dev0",
694
        removal_hint=__constraints_deprecation_msg,
695
    )
696
    resolver_manylinux = StrOption(
7✔
697
        default="manylinux2014",
698
        help=softwrap(
699
            """
700
            Whether to allow resolution of manylinux wheels when resolving requirements for
701
            foreign linux platforms. The value should be a manylinux platform upper bound,
702
            e.g. `'manylinux2010'`, or else the string `'no'` to disallow.
703
            """
704
        ),
705
        advanced=True,
706
    )
707

708
    tailor_source_targets = BoolOption(
7✔
709
        default=True,
710
        help=softwrap(
711
            """
712
            If true, add `python_sources`, `python_tests`, and `python_test_utils` targets with
713
            the `tailor` goal."""
714
        ),
715
        advanced=True,
716
    )
717
    tailor_ignore_empty_init_files = BoolOption(
7✔
718
        "--tailor-ignore-empty-init-files",
719
        default=True,
720
        help=softwrap(
721
            """
722
            If true, don't add `python_sources` targets for `__init__.py` files that are both empty
723
            and where there are no other Python files in the directory.
724

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

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

731
            If you set to false, you may also want to set `[python-infer].init_files = "always"`.
732
            """
733
        ),
734
        advanced=True,
735
    )
736
    tailor_requirements_targets = BoolOption(
7✔
737
        default=True,
738
        help=softwrap(
739
            """
740
            If true, add `python_requirements`, `poetry_requirements`, and `pipenv_requirements`
741
            target generators with the `tailor` goal.
742

743
            `python_requirements` targets are added for any file that matches the pattern
744
            `*requirements*.txt`. You will need to manually add `python_requirements` for different
745
            file names like `reqs.txt`.
746

747
            `poetry_requirements` targets are added for `pyproject.toml` files with `[tool.poetry`
748
            in them.
749
            """
750
        ),
751
        advanced=True,
752
    )
753
    tailor_pex_binary_targets = BoolOption(
7✔
754
        default=False,
755
        help=softwrap(
756
            """
757
            If true, add `pex_binary` targets for Python files named `__main__.py` or with a
758
            `__main__` clause with the `tailor` goal.
759
            """
760
        ),
761
        advanced=True,
762
    )
763
    tailor_py_typed_targets = BoolOption(
7✔
764
        default=True,
765
        help=softwrap(
766
            """
767
            If true, add `resource` targets for marker files named `py.typed` with the `tailor` goal.
768
            """
769
        ),
770
        advanced=True,
771
    )
772
    tailor_test_file_globs = StrListOption(
7✔
773
        default=list(DEFAULT_TEST_FILE_GLOBS),
774
        help=softwrap(
775
            """
776
        Globs to match your test files. Used to decide which files should tailor
777
        python_tests() vs python_sources() targets.
778

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

786
        For example, you can set `[python].tailor_test_file_globs` to `["*_mytests.py"]`, and then
787
        create `my_pants_macros.py` with:
788

789
        ```
790
        def my_python_tests(**kwargs):
791
            if "sources" not in kwargs:
792
                kwargs["sources"] = ["*_mytests.py"]
793
            python_tests(**kwargs)
794

795

796
        def my_python_sources(**kwargs):
797
            if "sources" not in kwargs:
798
                kwargs["sources"] = ["*.py", "!*_mytests.py"]
799
            python_sources(**kwargs)
800
        ```
801

802
        And set the following in `pants.toml`:
803

804
        [global]
805
        build_file_prelude_globs = ["my_pants_macros.py"]
806

807
        [tailor]
808
        alias_mapping = { python_sources = "my_python_sources", python_tests = "my_python_tests" }
809
        """
810
        ),
811
        metavar="glob",
812
    )
813
    tailor_testutils_file_globs = StrListOption(
7✔
814
        default=list(DEFAULT_TESTUTIL_FILE_GLOBS),
815
        help=softwrap(
816
            """
817
        Globs to match your testutil files. Used to decide which files should tailor
818
        python_test_utils() vs python_sources() targets.
819

820
        See tailor_test_file_globs above for caveats and usage.
821
        """
822
        ),
823
        metavar="glob",
824
    )
825
    macos_big_sur_compatibility = BoolOption(
7✔
826
        default=False,
827
        help=softwrap(
828
            """
829
            If set, and if running on macOS Big Sur, use `macosx_10_16` as the platform
830
            when building wheels. Otherwise, the default of `macosx_11_0` will be used.
831
            This may be required for `pip` to be able to install the resulting distribution
832
            on Big Sur.
833
            """
834
        ),
835
        advanced=True,
836
    )
837
    enable_lockfile_targets = BoolOption(
7✔
838
        default=True,
839
        help=softwrap(
840
            """
841
            Create targets for all Python lockfiles defined in `[python].resolves`.
842

843
            The lockfile targets will then be used as dependencies to the `python_requirement`
844
            targets that use them, invalidating source targets per resolve when the lockfile
845
            changes.
846

847
            If another targets address is in conflict with the created lockfile target, it will
848
            shadow the lockfile target and it will not be available as a dependency for any
849
            `python_requirement` targets.
850
            """
851
        ),
852
        advanced=True,
853
    )
854
    repl_history = BoolOption(
7✔
855
        default=True,
856
        help="Whether to use the standard Python command history file when running a repl.",
857
    )
858

859
    @property
7✔
860
    def enable_synthetic_lockfiles(self) -> bool:
7✔
861
        return self.enable_resolves and self.enable_lockfile_targets
6✔
862

863
    @memoized_property
7✔
864
    def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]:
7✔
865
        result = {}
7✔
866
        unrecognized_resolves = []
7✔
867
        for resolve, ics in self._resolves_to_interpreter_constraints.items():
7✔
868
            if resolve not in self.resolves:
1✔
UNCOV
869
                unrecognized_resolves.append(resolve)
×
870
            if ics and self.warn_on_python2_usage:
1✔
871
                # Side-step import cycle.
UNCOV
872
                from pants.backend.python.util_rules.interpreter_constraints import (
×
873
                    warn_on_python2_usage_in_interpreter_constraints,
874
                )
875

UNCOV
876
                warn_on_python2_usage_in_interpreter_constraints(
×
877
                    ics,
878
                    description_of_origin=f"the `[python].resolves_to_interpreter_constraints` option for resolve {resolve}",
879
                )
880

881
            result[resolve] = tuple(ics)
1✔
882
        if unrecognized_resolves:
7✔
UNCOV
883
            raise UnrecognizedResolveNamesError(
×
884
                unrecognized_resolves,
885
                self.resolves.keys(),
886
                description_of_origin="the option `[python].resolves_to_interpreter_constraints`",
887
            )
888
        return result
7✔
889

890
    def _resolves_to_option_helper(
7✔
891
        self,
892
        option_value: dict[str, _T],
893
        option_name: str,
894
    ) -> dict[str, _T]:
895
        all_valid_resolves = set(self.resolves)
7✔
896
        unrecognized_resolves = set(option_value.keys()) - {
7✔
897
            RESOLVE_OPTION_KEY__DEFAULT,
898
            *all_valid_resolves,
899
        }
900
        if unrecognized_resolves:
7✔
UNCOV
901
            raise UnrecognizedResolveNamesError(
×
902
                sorted(unrecognized_resolves),
903
                {*all_valid_resolves, RESOLVE_OPTION_KEY__DEFAULT},
904
                description_of_origin=f"the option `[python].{option_name}`",
905
            )
906
        default_val = option_value.get(RESOLVE_OPTION_KEY__DEFAULT)
7✔
907
        if not default_val:
7✔
908
            return option_value
7✔
UNCOV
909
        return {resolve: option_value.get(resolve, default_val) for resolve in all_valid_resolves}
×
910

911
    @memoized_method
7✔
912
    def resolves_to_constraints_file(self) -> dict[str, str]:
7✔
913
        return self._resolves_to_option_helper(
7✔
914
            self._resolves_to_constraints_file,
915
            "resolves_to_constraints_file",
916
        )
917

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

928
    @memoized_method
7✔
929
    def resolves_to_only_binary(self) -> dict[str, list[str]]:
7✔
930
        return {
7✔
931
            resolve: sorted([canonicalize_name(v) for v in vals])
932
            for resolve, vals in self._resolves_to_option_helper(
933
                self._resolves_to_only_binary,
934
                "resolves_to_only_binary",
935
            ).items()
936
        }
937

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

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

958
    @memoized_method
7✔
959
    def resolves_to_sources(self) -> dict[str, list[str]]:
7✔
960
        return {
7✔
961
            resolve: sorted(vals)
962
            for resolve, vals in self._resolves_to_option_helper(
963
                self._resolves_to_sources,
964
                "resolves_to_sources",
965
            ).items()
966
        }
967

968
    @memoized_method
7✔
969
    def resolves_to_lock_style(self) -> dict[str, str]:
7✔
970
        return self._resolves_to_option_helper(
7✔
971
            self._resolves_to_lock_style,
972
            "resolves_to_lock_style",
973
        )
974

975
    @memoized_method
7✔
976
    def resolves_to_complete_platforms(self) -> dict[str, list[str]]:
7✔
977
        return self._resolves_to_option_helper(
7✔
978
            self._resolves_to_complete_platforms,
979
            "resolves_to_complete_platforms",
980
        )
981

982
    @memoized_method
7✔
983
    def resolves_to_uploaded_prior_to(self) -> dict[str, str]:
7✔
984
        return self._resolves_to_option_helper(
7✔
985
            self._resolves_to_uploaded_prior_to,
986
            "resolves_to_uploaded_prior_to",
987
        )
988

989
    @property
7✔
990
    def manylinux(self) -> str | None:
7✔
991
        manylinux = cast(str | None, self.resolver_manylinux)
7✔
992
        if manylinux is None or manylinux.lower() in ("false", "no", "none"):
7✔
993
            return None
×
994
        return manylinux
7✔
995

996
    @property
7✔
997
    def resolve_all_constraints(self) -> bool:
7✔
998
        if (
7✔
999
            self._resolve_all_constraints
1000
            and not self.options.is_default("resolve_all_constraints")
1001
            and not self.requirement_constraints
1002
        ):
UNCOV
1003
            raise ValueError(
×
1004
                softwrap(
1005
                    """
1006
                    `[python].resolve_all_constraints` is enabled, so
1007
                    `[python].requirement_constraints` must also be set.
1008
                    """
1009
                )
1010
            )
1011
        return self._resolve_all_constraints
7✔
1012

1013
    @property
7✔
1014
    def scratch_dir(self):
7✔
1015
        return os.path.join(self.options.pants_workdir, *self.options_scope.split("."))
×
1016

1017
    def compatibility_or_constraints(
7✔
1018
        self, compatibility: Iterable[str] | None, resolve: str | None
1019
    ) -> tuple[str, ...]:
1020
        """Return either the given `compatibility` field or the global interpreter constraints.
1021

1022
        If interpreter constraints are supplied by the CLI flag, return those only.
1023
        """
1024
        if self.options.is_flagged("interpreter_constraints"):
7✔
1025
            return self.interpreter_constraints
6✔
1026
        if compatibility:
7✔
1027
            return tuple(compatibility)
6✔
1028
        if resolve and self.default_to_resolve_interpreter_constraints:
7✔
1029
            return self.resolves_to_interpreter_constraints.get(
1✔
1030
                resolve, self.interpreter_constraints
1031
            )
1032
        return self.interpreter_constraints
7✔
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