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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

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

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

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

12
from packaging.utils import canonicalize_name
5✔
13

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

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

31

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

38

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

44

45
@enum.unique
5✔
46
class LockfileResolver(enum.Enum):
5✔
47
    pip = "pip"
5✔
48
    uv = "uv"
5✔
49

50

51
RESOLVE_OPTION_KEY__DEFAULT = "__default__"
5✔
52

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

55

56
class PythonSetup(Subsystem):
5✔
57
    options_scope = "python"
5✔
58
    help = "Options for Pants's Python backend."
5✔
59

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
148
        return self._interpreter_constraints
×
149

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

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

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

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

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

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

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

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

207
            If you need multiple resolves:
208

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

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

228
            For example:
229

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

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

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

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

254
    _default_to_resolve_interpreter_constraints = BoolOption(
5✔
255
        default=False,
256
        help=softwrap(
257
            """
258
            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.
259

260
            `[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.
261
            """
262
        ),
263
        advanced=True,
264
    )
265

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

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

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

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

310
            N.B.: The `latest` value selects the latest of the choices listed by PEX which is not
311
            necessarily the latest Pip version released on PyPI.
312
            """
313
        ),
314
        advanced=True,
315
    )
316
    lockfile_resolver = EnumOption(
5✔
317
        default=LockfileResolver.pip,
318
        help=softwrap(
319
            """
320
            Which resolver to use when generating Pex lockfiles with `pants generate-lockfiles`.
321

322
            - `pip` (default): Use `pex lock create` with pip's resolver.
323
            - `uv` (experimental): Use `uv pip compile` to pre-resolve the full set of pinned
324
              requirements, then run `pex lock create --no-transitive` to materialize a Pex lock.
325

326
            Limitations when using `uv`:
327
            - Only supported for non-`universal` lock styles and without `complete_platforms`.
328
            - Does not currently model all Pex-specific features (e.g. certain per-resolve
329
              `--override` behavior) during the `uv` pre-resolution step.
330
            """
331
        ),
332
        advanced=True,
333
    )
334
    _resolves_to_interpreter_constraints = DictOption[list[str]](
5✔
335
        help=softwrap(
336
            """
337
            Override the interpreter constraints to use when generating a resolve's lockfile
338
            with the `generate-lockfiles` goal.
339

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

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

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

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

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

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

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

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

393
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
394
            resolves.
395

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

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

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

417
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
418
            resolves.
419

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

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

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

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

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

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

502
            Examples:
503
            - `{{'data-science': 'strict', 'web-app': 'universal'}}` - use strict style for data-science resolve, universal for web-app
504
            - `{{'python-default': 'sources'}}` - use sources style for the default resolve
505

506
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
507
            resolves.
508

509
            See https://docs.pex-tool.org/api/pex.html for more information on lockfile styles.
510
            """
511
        ),
512
        advanced=True,
513
    )
514

515
    _resolves_to_complete_platforms = DictOption[list[str]](
5✔
516
        help=softwrap(
517
            f"""
518
            The platforms the built PEX should be compatible with when generating lockfiles.
519

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

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

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

532
            For example:
533
            `{{'python-default': ['3rdparty/platforms:linux_aarch64', '3rdparty/platforms:macos_arm64']}}`.
534

535
            You can use the key `{RESOLVE_OPTION_KEY__DEFAULT}` to set a default value for all
536
            resolves.
537

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

541
            See https://docs.pex-tool.org for more information.
542
            """
543
        ),
544
        advanced=True,
545
    )
546

547
    invalid_lockfile_behavior = EnumOption(
5✔
548
        default=InvalidLockfileBehavior.error,
549
        help=softwrap(
550
            """
551
            The behavior when a lockfile has requirements or interpreter constraints that are
552
            not compatible with what the current build is using.
553

554
            We recommend keeping the default of `error` for CI builds.
555

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

559
            Use `ignore` to avoid needing a lockfile header at all, e.g. if you are manually
560
            managing lockfiles rather than using the `generate-lockfiles` goal.
561
            """
562
        ),
563
        advanced=True,
564
    )
565
    resolves_generate_lockfiles = BoolOption(
5✔
566
        default=True,
567
        help=softwrap(
568
            """
569
            If False, Pants will not attempt to generate lockfiles for `[python].resolves` when
570
            running the `generate-lockfiles` goal.
571

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

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

582
            Warning: it will likely be slower to install manually generated user lockfiles than Pex
583
            ones because Pants cannot as efficiently extract the subset of requirements used for a
584
            particular task. See the option `[python].run_against_entire_lockfile`.
585
            """
586
        ),
587
        advanced=True,
588
    )
589
    run_against_entire_lockfile = BoolOption(
5✔
590
        default=False,
591
        help=softwrap(
592
            """
593
            If enabled, when running binaries, tests, and repls, Pants will use the entire
594
            lockfile file instead of just the relevant subset.
595

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

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

606
            This option does not affect packaging deployable artifacts, such as
607
            PEX files, wheels and cloud functions, which will still use just the exact
608
            subset of requirements needed.
609
            """
610
        ),
611
        advanced=True,
612
    )
613

614
    __constraints_deprecation_msg = softwrap(
5✔
615
        f"""
616
        We encourage instead migrating to `[python].enable_resolves` and `[python].resolves`,
617
        which is an improvement over this option. The `[python].resolves` feature ensures that
618
        your lockfiles are fully comprehensive, i.e. include all transitive dependencies;
619
        uses hashes for better supply chain security; and supports advanced features like VCS
620
        and local requirements, along with options `[python].resolves_to_only_binary`.
621

622
        To migrate, stop setting `[python].requirement_constraints` and
623
        `[python].resolve_all_constraints`, and instead set `[python].enable_resolves` to
624
        `true`. Then, run `{bin_name()} generate-lockfiles`.
625
        """
626
    )
627
    requirement_constraints = FileOption(
5✔
628
        default=None,
629
        help=softwrap(
630
            """
631
            When resolving third-party requirements for your own code (vs. tools you run),
632
            use this constraints file to determine which versions to use.
633

634
            Mutually exclusive with `[python].enable_resolves`, which we generally recommend as an
635
            improvement over constraints file.
636

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

641
            This only applies when resolving user requirements, rather than tools you run
642
            like Black and Pytest. To constrain tools, set `[tool].lockfile`, e.g.
643
            `[black].lockfile`.
644
            """
645
        ),
646
        advanced=True,
647
        mutually_exclusive_group="lockfile",
648
        removal_version="3.0.0.dev0",
649
        removal_hint=__constraints_deprecation_msg,
650
    )
651
    _resolve_all_constraints = BoolOption(
5✔
652
        default=True,
653
        help=softwrap(
654
            """
655
            (Only relevant when using `[python].requirement_constraints.`) If enabled, when
656
            resolving requirements, Pants will first resolve your entire
657
            constraints file as a single global resolve. Then, if the code uses a subset of
658
            your constraints file, Pants will extract the relevant requirements from that
659
            global resolve so that only what's actually needed gets used. If disabled, Pants
660
            will not use a global resolve and will resolve each subset of your requirements
661
            independently.
662

663
            Usually this option should be enabled because it can result in far fewer resolves.
664
            """
665
        ),
666
        advanced=True,
667
        removal_version="3.0.0.dev0",
668
        removal_hint=__constraints_deprecation_msg,
669
    )
670
    resolver_manylinux = StrOption(
5✔
671
        default="manylinux2014",
672
        help=softwrap(
673
            """
674
            Whether to allow resolution of manylinux wheels when resolving requirements for
675
            foreign linux platforms. The value should be a manylinux platform upper bound,
676
            e.g. `'manylinux2010'`, or else the string `'no'` to disallow.
677
            """
678
        ),
679
        advanced=True,
680
    )
681

682
    tailor_source_targets = BoolOption(
5✔
683
        default=True,
684
        help=softwrap(
685
            """
686
            If true, add `python_sources`, `python_tests`, and `python_test_utils` targets with
687
            the `tailor` goal."""
688
        ),
689
        advanced=True,
690
    )
691
    tailor_ignore_empty_init_files = BoolOption(
5✔
692
        "--tailor-ignore-empty-init-files",
693
        default=True,
694
        help=softwrap(
695
            """
696
            If true, don't add `python_sources` targets for `__init__.py` files that are both empty
697
            and where there are no other Python files in the directory.
698

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

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

705
            If you set to false, you may also want to set `[python-infer].init_files = "always"`.
706
            """
707
        ),
708
        advanced=True,
709
    )
710
    tailor_requirements_targets = BoolOption(
5✔
711
        default=True,
712
        help=softwrap(
713
            """
714
            If true, add `python_requirements`, `poetry_requirements`, and `pipenv_requirements`
715
            target generators with the `tailor` goal.
716

717
            `python_requirements` targets are added for any file that matches the pattern
718
            `*requirements*.txt`. You will need to manually add `python_requirements` for different
719
            file names like `reqs.txt`.
720

721
            `poetry_requirements` targets are added for `pyproject.toml` files with `[tool.poetry`
722
            in them.
723
            """
724
        ),
725
        advanced=True,
726
    )
727
    tailor_pex_binary_targets = BoolOption(
5✔
728
        default=False,
729
        help=softwrap(
730
            """
731
            If true, add `pex_binary` targets for Python files named `__main__.py` or with a
732
            `__main__` clause with the `tailor` goal.
733
            """
734
        ),
735
        advanced=True,
736
    )
737
    tailor_py_typed_targets = BoolOption(
5✔
738
        default=True,
739
        help=softwrap(
740
            """
741
            If true, add `resource` targets for marker files named `py.typed` with the `tailor` goal.
742
            """
743
        ),
744
        advanced=True,
745
    )
746
    macos_big_sur_compatibility = BoolOption(
5✔
747
        default=False,
748
        help=softwrap(
749
            """
750
            If set, and if running on macOS Big Sur, use `macosx_10_16` as the platform
751
            when building wheels. Otherwise, the default of `macosx_11_0` will be used.
752
            This may be required for `pip` to be able to install the resulting distribution
753
            on Big Sur.
754
            """
755
        ),
756
        advanced=True,
757
    )
758
    enable_lockfile_targets = BoolOption(
5✔
759
        default=True,
760
        help=softwrap(
761
            """
762
            Create targets for all Python lockfiles defined in `[python].resolves`.
763

764
            The lockfile targets will then be used as dependencies to the `python_requirement`
765
            targets that use them, invalidating source targets per resolve when the lockfile
766
            changes.
767

768
            If another targets address is in conflict with the created lockfile target, it will
769
            shadow the lockfile target and it will not be available as a dependency for any
770
            `python_requirement` targets.
771
            """
772
        ),
773
        advanced=True,
774
    )
775
    repl_history = BoolOption(
5✔
776
        default=True,
777
        help="Whether to use the standard Python command history file when running a repl.",
778
    )
779

780
    @property
5✔
781
    def enable_synthetic_lockfiles(self) -> bool:
5✔
782
        return self.enable_resolves and self.enable_lockfile_targets
×
783

784
    @memoized_property
5✔
785
    def resolves_to_interpreter_constraints(self) -> dict[str, tuple[str, ...]]:
5✔
UNCOV
786
        result = {}
×
UNCOV
787
        unrecognized_resolves = []
×
UNCOV
788
        for resolve, ics in self._resolves_to_interpreter_constraints.items():
×
UNCOV
789
            if resolve not in self.resolves:
×
UNCOV
790
                unrecognized_resolves.append(resolve)
×
UNCOV
791
            if ics and self.warn_on_python2_usage:
×
792
                # Side-step import cycle.
793
                from pants.backend.python.util_rules.interpreter_constraints import (
×
794
                    warn_on_python2_usage_in_interpreter_constraints,
795
                )
796

797
                warn_on_python2_usage_in_interpreter_constraints(
×
798
                    ics,
799
                    description_of_origin=f"the `[python].resolves_to_interpreter_constraints` option for resolve {resolve}",
800
                )
801

UNCOV
802
            result[resolve] = tuple(ics)
×
UNCOV
803
        if unrecognized_resolves:
×
UNCOV
804
            raise UnrecognizedResolveNamesError(
×
805
                unrecognized_resolves,
806
                self.resolves.keys(),
807
                description_of_origin="the option `[python].resolves_to_interpreter_constraints`",
808
            )
UNCOV
809
        return result
×
810

811
    def _resolves_to_option_helper(
5✔
812
        self,
813
        option_value: dict[str, _T],
814
        option_name: str,
815
    ) -> dict[str, _T]:
UNCOV
816
        all_valid_resolves = set(self.resolves)
×
UNCOV
817
        unrecognized_resolves = set(option_value.keys()) - {
×
818
            RESOLVE_OPTION_KEY__DEFAULT,
819
            *all_valid_resolves,
820
        }
UNCOV
821
        if unrecognized_resolves:
×
UNCOV
822
            raise UnrecognizedResolveNamesError(
×
823
                sorted(unrecognized_resolves),
824
                {*all_valid_resolves, RESOLVE_OPTION_KEY__DEFAULT},
825
                description_of_origin=f"the option `[python].{option_name}`",
826
            )
UNCOV
827
        default_val = option_value.get(RESOLVE_OPTION_KEY__DEFAULT)
×
UNCOV
828
        if not default_val:
×
UNCOV
829
            return option_value
×
UNCOV
830
        return {resolve: option_value.get(resolve, default_val) for resolve in all_valid_resolves}
×
831

832
    @memoized_method
5✔
833
    def resolves_to_constraints_file(self) -> dict[str, str]:
5✔
UNCOV
834
        return self._resolves_to_option_helper(
×
835
            self._resolves_to_constraints_file,
836
            "resolves_to_constraints_file",
837
        )
838

839
    @memoized_method
5✔
840
    def resolves_to_no_binary(self) -> dict[str, list[str]]:
5✔
UNCOV
841
        return {
×
842
            resolve: [canonicalize_name(v) for v in vals]
843
            for resolve, vals in self._resolves_to_option_helper(
844
                self._resolves_to_no_binary,
845
                "resolves_to_no_binary",
846
            ).items()
847
        }
848

849
    @memoized_method
5✔
850
    def resolves_to_only_binary(self) -> dict[str, list[str]]:
5✔
UNCOV
851
        return {
×
852
            resolve: sorted([canonicalize_name(v) for v in vals])
853
            for resolve, vals in self._resolves_to_option_helper(
854
                self._resolves_to_only_binary,
855
                "resolves_to_only_binary",
856
            ).items()
857
        }
858

859
    @memoized_method
5✔
860
    def resolves_to_excludes(self) -> dict[str, list[str]]:
5✔
861
        return {
×
862
            resolve: sorted(vals)
863
            for resolve, vals in self._resolves_to_option_helper(
864
                self._resolves_to_excludes,
865
                "resolves_to_excludes",
866
            ).items()
867
        }
868

869
    @memoized_method
5✔
870
    def resolves_to_overrides(self) -> dict[str, list[str]]:
5✔
871
        return {
×
872
            resolve: sorted(vals)
873
            for resolve, vals in self._resolves_to_option_helper(
874
                self._resolves_to_overrides,
875
                "resolves_to_overrides",
876
            ).items()
877
        }
878

879
    @memoized_method
5✔
880
    def resolves_to_sources(self) -> dict[str, list[str]]:
5✔
881
        return {
×
882
            resolve: sorted(vals)
883
            for resolve, vals in self._resolves_to_option_helper(
884
                self._resolves_to_sources,
885
                "resolves_to_sources",
886
            ).items()
887
        }
888

889
    @memoized_method
5✔
890
    def resolves_to_lock_style(self) -> dict[str, str]:
5✔
891
        return self._resolves_to_option_helper(
×
892
            self._resolves_to_lock_style,
893
            "resolves_to_lock_style",
894
        )
895

896
    @memoized_method
5✔
897
    def resolves_to_complete_platforms(self) -> dict[str, list[str]]:
5✔
898
        return self._resolves_to_option_helper(
×
899
            self._resolves_to_complete_platforms,
900
            "resolves_to_complete_platforms",
901
        )
902

903
    @property
5✔
904
    def manylinux(self) -> str | None:
5✔
905
        manylinux = cast(str | None, self.resolver_manylinux)
×
906
        if manylinux is None or manylinux.lower() in ("false", "no", "none"):
×
907
            return None
×
908
        return manylinux
×
909

910
    @property
5✔
911
    def resolve_all_constraints(self) -> bool:
5✔
UNCOV
912
        if (
×
913
            self._resolve_all_constraints
914
            and not self.options.is_default("resolve_all_constraints")
915
            and not self.requirement_constraints
916
        ):
917
            raise ValueError(
×
918
                softwrap(
919
                    """
920
                    `[python].resolve_all_constraints` is enabled, so
921
                    `[python].requirement_constraints` must also be set.
922
                    """
923
                )
924
            )
UNCOV
925
        return self._resolve_all_constraints
×
926

927
    @property
5✔
928
    def scratch_dir(self):
5✔
929
        return os.path.join(self.options.pants_workdir, *self.options_scope.split("."))
×
930

931
    def compatibility_or_constraints(
5✔
932
        self, compatibility: Iterable[str] | None, resolve: str | None
933
    ) -> tuple[str, ...]:
934
        """Return either the given `compatibility` field or the global interpreter constraints.
935

936
        If interpreter constraints are supplied by the CLI flag, return those only.
937
        """
UNCOV
938
        if self.options.is_flagged("interpreter_constraints"):
×
939
            return self.interpreter_constraints
×
UNCOV
940
        if compatibility:
×
UNCOV
941
            return tuple(compatibility)
×
UNCOV
942
        if resolve and self.default_to_resolve_interpreter_constraints:
×
UNCOV
943
            return self.resolves_to_interpreter_constraints.get(
×
944
                resolve, self.interpreter_constraints
945
            )
UNCOV
946
        return self.interpreter_constraints
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc