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

pantsbuild / pants / 18252174847

05 Oct 2025 01:36AM UTC coverage: 43.382% (-36.9%) from 80.261%
18252174847

push

github

web-flow
run tests on mac arm (#22717)

Just doing the minimal to pull forward the x86_64 pattern.

ref #20993

25776 of 59416 relevant lines covered (43.38%)

1.3 hits per line

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

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

4
from __future__ import annotations
3✔
5

6
import collections.abc
3✔
7
import logging
3✔
8
import os.path
3✔
9
from abc import ABC, abstractmethod
3✔
10
from collections.abc import Iterable, Iterator, Mapping, Sequence
3✔
11
from dataclasses import dataclass
3✔
12
from enum import Enum
3✔
13
from typing import TYPE_CHECKING, ClassVar, cast
3✔
14

15
from packaging.utils import canonicalize_name as canonicalize_project_name
3✔
16

17
from pants.backend.python.macros.python_artifact import PythonArtifact
3✔
18
from pants.backend.python.subsystems.setup import PythonSetup
3✔
19
from pants.core.environments.target_types import EnvironmentField
3✔
20
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
3✔
21
from pants.core.goals.package import OutputPathField
3✔
22
from pants.core.goals.run import RestartableField
3✔
23
from pants.core.goals.test import (
3✔
24
    RuntimePackageDependenciesField,
25
    TestExtraEnvVarsField,
26
    TestsBatchCompatibilityTagField,
27
    TestSubsystem,
28
)
29
from pants.engine.addresses import Address, Addresses
3✔
30
from pants.engine.target import (
3✔
31
    COMMON_TARGET_FIELDS,
32
    AsyncFieldMixin,
33
    BoolField,
34
    Dependencies,
35
    DictStringToStringField,
36
    DictStringToStringSequenceField,
37
    Field,
38
    IntField,
39
    InvalidFieldException,
40
    InvalidFieldTypeException,
41
    InvalidTargetException,
42
    MultipleSourcesField,
43
    NestedDictStringToStringField,
44
    OptionalSingleSourceField,
45
    OverridesField,
46
    ScalarField,
47
    SingleSourceField,
48
    SpecialCasedDependencies,
49
    StringField,
50
    StringSequenceField,
51
    Target,
52
    TargetFilesGenerator,
53
    TargetFilesGeneratorSettingsRequest,
54
    TargetGenerator,
55
    TriBoolField,
56
    ValidNumbers,
57
    generate_file_based_overrides_field_help_message,
58
    generate_multiple_sources_field_help_message,
59
)
60
from pants.option.option_types import BoolOption
3✔
61
from pants.option.subsystem import Subsystem
3✔
62
from pants.util.docutil import bin_name, doc_url, git_url
3✔
63
from pants.util.frozendict import FrozenDict
3✔
64
from pants.util.pip_requirement import PipRequirement
3✔
65
from pants.util.strutil import help_text, softwrap
3✔
66

67
logger = logging.getLogger(__name__)
3✔
68

69
if TYPE_CHECKING:
70
    from pants.backend.python.subsystems.pytest import PyTest
71

72

73
# -----------------------------------------------------------------------------------------------
74
# Common fields
75
# -----------------------------------------------------------------------------------------------
76

77

78
class PythonSourceField(SingleSourceField):
3✔
79
    # Note that Python scripts often have no file ending.
80
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")
3✔
81

82

83
class PythonDependenciesField(Dependencies):
3✔
84
    pass
3✔
85

86

87
class PythonGeneratingSourcesBase(MultipleSourcesField):
3✔
88
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")
3✔
89

90

91
class InterpreterConstraintsField(StringSequenceField, AsyncFieldMixin):
3✔
92
    alias = "interpreter_constraints"
3✔
93
    help = help_text(
3✔
94
        f"""
95
        The Python interpreters this code is compatible with.
96

97
        Each element should be written in pip-style format, e.g. `CPython==2.7.*` or
98
        `CPython>=3.6,<4`. You can leave off `CPython` as a shorthand, e.g. `>=2.7` will be expanded
99
        to `CPython>=2.7`.
100

101
        Specify more than one element to OR the constraints, e.g. `['PyPy==3.7.*', 'CPython==3.7.*']`
102
        means either PyPy 3.7 _or_ CPython 3.7.
103

104
        If the field is not set, it will default to the option `[python].interpreter_constraints`.
105

106
        See {doc_url("docs/python/overview/interpreter-compatibility")} for how these interpreter
107
        constraints are merged with the constraints of dependencies.
108
        """
109
    )
110

111
    def value_or_global_default(self, python_setup: PythonSetup) -> tuple[str, ...]:
3✔
112
        """Return either the given `compatibility` field or the global interpreter constraints.
113

114
        If interpreter constraints are supplied by the CLI flag, return those only.
115
        """
116
        if self.value and python_setup.warn_on_python2_usage:
×
117
            # Side-step import cycle.
118
            from pants.backend.python.util_rules.interpreter_constraints import (
×
119
                warn_on_python2_usage_in_interpreter_constraints,
120
            )
121

122
            warn_on_python2_usage_in_interpreter_constraints(
×
123
                self.value,
124
                description_of_origin=f"the `{self.alias}` field on target at `{self.address}`",
125
            )
126

127
        return python_setup.compatibility_or_constraints(self.value)
×
128

129

130
class PythonResolveField(StringField, AsyncFieldMixin):
3✔
131
    alias = "resolve"
3✔
132
    required = False
3✔
133
    help = help_text(
3✔
134
        """
135
        The resolve from `[python].resolves` to use.
136

137
        If not defined, will default to `[python].default_resolve`.
138

139
        All dependencies must share the same value for their `resolve` field.
140
        """
141
    )
142

143
    def normalized_value(self, python_setup: PythonSetup) -> str:
3✔
144
        """Get the value after applying the default and validating that the key is recognized."""
145
        if not python_setup.enable_resolves:
×
146
            return "<ignore>"
×
147
        resolve = self.value or python_setup.default_resolve
×
148
        if resolve not in python_setup.resolves:
×
149
            raise UnrecognizedResolveNamesError(
×
150
                [resolve],
151
                python_setup.resolves.keys(),
152
                description_of_origin=f"the field `{self.alias}` in the target {self.address}",
153
            )
154
        return resolve
×
155

156

157
class PrefixedPythonResolveField(PythonResolveField):
3✔
158
    alias = "python_resolve"
3✔
159

160

161
class PythonRunGoalUseSandboxField(TriBoolField):
3✔
162
    alias = "run_goal_use_sandbox"
3✔
163
    help = help_text(
3✔
164
        """
165
        Whether to use a sandbox when `run`ning this target. Defaults to
166
        `[python].default_run_goal_use_sandbox`.
167

168
        If true, runs of this target with the `run` goal will copy the needed first-party sources
169
        into a temporary sandbox and run from there.
170

171
        If false, runs of this target with the `run` goal will use the in-repo sources
172
        directly.
173

174
        Note that this field only applies when running a target with the `run` goal. No other goals
175
        (such as `test`, if applicable) consult this field.
176

177
        The former mode is more hermetic, and is closer to building and running the source as it
178
        were packaged in a `pex_binary`. Additionally, it may be necessary if your sources depend
179
        transitively on "generated" files which will be materialized in the sandbox in a source
180
        root, but are not in-repo.
181

182
        The latter mode is similar to creating, activating, and using a virtual environment when
183
        running your files. It may also be necessary if the source being run writes files into the
184
        repo and computes their location relative to the executed files. Django's `makemigrations`
185
        command is an example of such a process.
186
        """
187
    )
188

189

190
# -----------------------------------------------------------------------------------------------
191
# Target generation support
192
# -----------------------------------------------------------------------------------------------
193

194

195
class PythonFilesGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
3✔
196
    pass
3✔
197

198

199
# -----------------------------------------------------------------------------------------------
200
# `pex_binary` and `pex_binaries` target
201
# -----------------------------------------------------------------------------------------------
202

203

204
# See `target_types_rules.py` for a dependency injection rule.
205
class PexBinaryDependenciesField(Dependencies):
3✔
206
    supports_transitive_excludes = True
3✔
207

208

209
class MainSpecification(ABC):
3✔
210
    @abstractmethod
211
    def iter_pex_args(self) -> Iterator[str]: ...
212

213
    @property
214
    @abstractmethod
215
    def spec(self) -> str: ...
216

217

218
@dataclass(frozen=True)
3✔
219
class EntryPoint(MainSpecification):
3✔
220
    module: str
3✔
221
    function: str | None = None
3✔
222

223
    @classmethod
3✔
224
    def parse(cls, value: str, provenance: str | None = None) -> EntryPoint:
3✔
225
        given = f"entry point {provenance}" if provenance else "entry point"
×
226
        entry_point = value.strip()
×
227
        if not entry_point:
×
228
            raise ValueError(
×
229
                softwrap(
230
                    f"""
231
                    The {given} cannot be blank. It must indicate a Python module by name or path
232
                    and an optional nullary function in that module separated by a colon, i.e.:
233
                    module_name_or_path(':'function_name)?
234
                    """
235
                )
236
            )
237
        module_or_path, sep, func = entry_point.partition(":")
×
238
        if not module_or_path:
×
239
            raise ValueError(f"The {given} must specify a module; given: {value!r}")
×
240
        if ":" in func:
×
241
            raise ValueError(
×
242
                softwrap(
243
                    f"""
244
                    The {given} can only contain one colon separating the entry point's module from
245
                    the entry point function in that module; given: {value!r}
246
                    """
247
                )
248
            )
249
        if sep and not func:
×
250
            logger.warning(
×
251
                softwrap(
252
                    f"""
253
                    Assuming no entry point function and stripping trailing ':' from the {given}:
254
                    {value!r}. Consider deleting it to make it clear no entry point function is
255
                    intended.
256
                    """
257
                )
258
            )
259
        return cls(module=module_or_path, function=func if func else None)
×
260

261
    def __post_init__(self):
3✔
262
        if ":" in self.module:
3✔
263
            raise ValueError(
×
264
                softwrap(
265
                    f"""
266
                    The `:` character is not valid in a module name. Given an entry point module of
267
                    {self.module}. Did you mean to use EntryPoint.parse?
268
                    """
269
                )
270
            )
271
        if self.function and ":" in self.function:
3✔
272
            raise ValueError(
×
273
                softwrap(
274
                    f"""
275
                    The `:` character is not valid in a function name. Given an entry point function
276
                    of {self.function}.
277
                    """
278
                )
279
            )
280

281
    def iter_pex_args(self) -> Iterator[str]:
3✔
282
        yield "--entry-point"
×
283
        yield self.spec
×
284

285
    @property
3✔
286
    def spec(self) -> str:
3✔
287
        return self.module if self.function is None else f"{self.module}:{self.function}"
3✔
288

289

290
@dataclass(frozen=True)
3✔
291
class ConsoleScript(MainSpecification):
3✔
292
    name: str
3✔
293

294
    def iter_pex_args(self) -> Iterator[str]:
3✔
295
        yield "--console-script"
×
296
        yield self.name
×
297

298
    @property
3✔
299
    def spec(self) -> str:
3✔
300
        return self.name
3✔
301

302

303
@dataclass(frozen=True)
3✔
304
class Executable(MainSpecification):
3✔
305
    executable: str
3✔
306

307
    @classmethod
3✔
308
    def create(cls, address: Address, filename: str) -> Executable:
3✔
309
        # spec_path is relative to the workspace. The rule is responsible for
310
        # stripping the source root as needed.
311
        return cls(os.path.join(address.spec_path, filename).lstrip(os.path.sep))
×
312

313
    def iter_pex_args(self) -> Iterator[str]:
3✔
314
        yield "--executable"
×
315
        # We do NOT yield self.executable or self.spec
316
        # as the path needs additional processing in the rule graph.
317
        # see: build_pex in util_rules/pex
318

319
    @property
3✔
320
    def spec(self) -> str:
3✔
321
        return self.executable
×
322

323

324
class EntryPointField(AsyncFieldMixin, Field):
3✔
325
    alias = "entry_point"
3✔
326
    default = None
3✔
327
    help = help_text(
3✔
328
        """
329
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.
330

331
        You can specify a full module like `'path.to.module'` and `'path.to.module:func'`, or use a
332
        shorthand to specify a file name, using the same syntax as the `sources` field:
333

334
          1) `'app.py'`, Pants will convert into the module `path.to.app`;
335
          2) `'app.py:func'`, Pants will convert into `path.to.app:func`.
336

337
        You may only set one of: this field, or the `script` field, or the `executable` field.
338
        Leave off all three fields to have no entry point.
339
        """
340
    )
341
    value: EntryPoint | None
3✔
342

343
    @classmethod
3✔
344
    def compute_value(cls, raw_value: str | None, address: Address) -> EntryPoint | None:
3✔
345
        value = super().compute_value(raw_value, address)
×
346
        if value is None:
×
347
            return None
×
348
        if not isinstance(value, str):
×
349
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
350
        try:
×
351
            return EntryPoint.parse(value, provenance=f"for {address}")
×
352
        except ValueError as e:
×
353
            raise InvalidFieldException(str(e))
×
354

355

356
class PexEntryPointField(EntryPointField):
3✔
357
    # Specialist subclass for use with `PexBinary` targets.
358
    pass
3✔
359

360

361
# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
362
@dataclass(frozen=True)
3✔
363
class ResolvedPexEntryPoint:
3✔
364
    val: EntryPoint | None
3✔
365
    file_name_used: bool
3✔
366

367

368
@dataclass(frozen=True)
3✔
369
class ResolvePexEntryPointRequest:
3✔
370
    """Determine the `entry_point` for a `pex_binary` after applying all syntactic sugar."""
371

372
    entry_point_field: EntryPointField
3✔
373

374

375
class PexScriptField(Field):
3✔
376
    alias = "script"
3✔
377
    default = None
3✔
378
    help = help_text(
3✔
379
        """
380
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a script or
381
        console_script as defined by any of the distributions in the PEX.
382

383
        You may only set one of: this field, or the `entry_point` field, or the `executable` field.
384
        Leave off all three fields to have no entry point.
385
        """
386
    )
387
    value: ConsoleScript | None
3✔
388

389
    @classmethod
3✔
390
    def compute_value(cls, raw_value: str | None, address: Address) -> ConsoleScript | None:
3✔
391
        value = super().compute_value(raw_value, address)
×
392
        if value is None:
×
393
            return None
×
394
        if not isinstance(value, str):
×
395
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
396
        return ConsoleScript(value)
×
397

398

399
class PexExecutableField(Field):
3✔
400
    alias = "executable"
3✔
401
    default = None
3✔
402
    help = help_text(
3✔
403
        """
404
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to an execuatble
405
        local python script. This executable python script is typically something that cannot
406
        be imported so it cannot be used via `script` or `entry_point`.
407

408
        You may only set one of: this field, or the `entry_point` field, or the `script` field.
409
        Leave off all three fields to have no entry point.
410
        """
411
    )
412
    value: Executable | None
3✔
413

414
    @classmethod
3✔
415
    def compute_value(cls, raw_value: str | None, address: Address) -> Executable | None:
3✔
416
        value = super().compute_value(raw_value, address)
×
417
        if value is None:
×
418
            return None
×
419
        if not isinstance(value, str):
×
420
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
421
        return Executable.create(address, value)
×
422

423

424
class PexArgsField(StringSequenceField):
3✔
425
    alias: ClassVar[str] = "args"
3✔
426
    help = help_text(
3✔
427
        lambda: f"""
428
        Freeze these command-line args into the PEX. Allows you to run generic entry points
429
        on specific arguments without creating a shim file.
430

431
        This is different to `{PexExtraBuildArgsField.alias}`: `{PexArgsField.alias}`
432
        records arguments used by the packaged PEX when executed,
433
        `{PexExtraBuildArgsField.alias}` passes arguments to the process that does the
434
        packaging.
435
        """
436
    )
437

438

439
class PexExtraBuildArgsField(StringSequenceField):
3✔
440
    alias: ClassVar[str] = "extra_build_args"
3✔
441
    default = ()
3✔
442
    help = help_text(
3✔
443
        lambda: f"""
444
        Extra arguments to pass to the `pex` invocation used to build this PEX. These are
445
        passed after all other arguments. This can be used to pass extra options that
446
        Pants doesn't have built-in support for.
447

448
        This is different to `{PexArgsField.alias}`: `{PexArgsField.alias}` records
449
        arguments used by the packaged PEX when executed, `{PexExtraBuildArgsField.alias}`
450
        passes arguments to the process that does the packaging.
451
        """
452
    )
453

454

455
class PexCheckField(StringField):
3✔
456
    alias = "check"
3✔
457
    valid_choices = ("none", "warn", "error")
3✔
458
    expected_type = str
3✔
459
    default = "warn"
3✔
460
    help = help_text(
3✔
461
        """
462
        Check that the built PEX is valid. Currently this only
463
        applies to `--layout zipapp` where the PEX zip is
464
        tested for importability of its `__main__` module by
465
        the Python zipimport module. This check will fail for
466
        PEX zips that use ZIP64 extensions since the Python
467
        zipimport zipimporter only works with 32 bit zips. The
468
        check no-ops for all other layouts.
469
        """
470
    )
471

472

473
class PexEnvField(DictStringToStringField):
3✔
474
    alias = "env"
3✔
475
    help = help_text(
3✔
476
        """
477
        Freeze these environment variables into the PEX. Allows you to run generic entry points
478
        on a specific environment without creating a shim file.
479
        """
480
    )
481

482

483
class PexCompletePlatformsField(SpecialCasedDependencies):
3✔
484
    alias = "complete_platforms"
3✔
485
    help = help_text(
3✔
486
        f"""
487
        The platforms the built PEX should be compatible with.
488

489
        There must be built wheels available for all of the foreign platforms, rather than sdists.
490

491
        You can give a list of multiple complete platforms to create a multiplatform PEX,
492
        meaning that the PEX will be executable in all of the supported environments.
493

494
        Complete platforms should be addresses of `file` or `resource` targets that point to files that contain
495
        complete platform JSON as described by Pex
496
        (https://pex.readthedocs.io/en/latest/buildingpex.html#complete-platform).
497

498
        See {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")} for details on how to create this file.
499
        """
500
    )
501

502

503
class PexInheritPathField(StringField):
3✔
504
    alias = "inherit_path"
3✔
505
    valid_choices = ("false", "fallback", "prefer")
3✔
506
    help = help_text(
3✔
507
        """
508
        Whether to inherit the `sys.path` (aka PYTHONPATH) of the environment that the binary runs in.
509

510
        Use `false` to not inherit `sys.path`; use `fallback` to inherit `sys.path` after packaged
511
        dependencies; and use `prefer` to inherit `sys.path` before packaged dependencies.
512
        """
513
    )
514

515
    # TODO(#9388): deprecate allowing this to be a `bool`.
516
    @classmethod
3✔
517
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
3✔
518
        if isinstance(raw_value, bool):
×
519
            return "prefer" if raw_value else "false"
×
520
        return super().compute_value(raw_value, address)
×
521

522

523
class PexStripEnvField(BoolField):
3✔
524
    alias = "strip_pex_env"
3✔
525
    default = True
3✔
526
    help = help_text(
3✔
527
        """
528
        Whether or not to strip the PEX runtime environment of `PEX*` environment variables.
529

530
        Most applications have no need for the `PEX*` environment variables that are used to
531
        control PEX startup; so these variables are scrubbed from the environment by Pex before
532
        transferring control to the application by default. This prevents any subprocesses that
533
        happen to execute other PEX files from inheriting these control knob values since most
534
        would be undesired; e.g.: PEX_MODULE or PEX_PATH.
535
        """
536
    )
537

538

539
class PexIgnoreErrorsField(BoolField):
3✔
540
    alias = "ignore_errors"
3✔
541
    default = False
3✔
542
    help = "Should PEX ignore errors when it cannot resolve dependencies?"
3✔
543

544

545
class PexShBootField(BoolField):
3✔
546
    alias = "sh_boot"
3✔
547
    default = False
3✔
548
    help = help_text(
3✔
549
        """
550
        Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot?
551

552
        If you know the machines that the PEX will be distributed to have
553
        POSIX compliant `/bin/sh` (almost all do, see:
554
        https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html);
555
        then this is probably the way you want your PEX to boot. Instead of
556
        launching via a Python shebang, the PEX will launch via a `#!/bin/sh`
557
        shebang that executes a small script embedded in the head of the PEX
558
        ZIPAPP that performs initial interpreter selection and re-execution of
559
        the underlying PEX in a way that is often more robust than a Python
560
        shebang and always faster on 2nd and subsequent runs since the sh
561
        script has a constant overhead of O(1ms) whereas the Python overhead
562
        to perform the same interpreter selection and re-execution is
563
        O(100ms).
564
        """
565
    )
566

567

568
class PexShebangField(StringField):
3✔
569
    alias = "shebang"
3✔
570
    help = help_text(
3✔
571
        """
572
        Set the generated PEX to use this shebang, rather than the default of PEX choosing a
573
        shebang based on the interpreter constraints.
574

575
        This influences the behavior of running `./result.pex`. You can ignore the shebang by
576
        instead running `/path/to/python_interpreter ./result.pex`.
577
        """
578
    )
579

580

581
class PexEmitWarningsField(TriBoolField):
3✔
582
    alias = "emit_warnings"
3✔
583
    help = help_text(
3✔
584
        """
585
        Whether or not to emit PEX warnings at runtime.
586

587
        The default is determined by the option `emit_warnings` in the `[pex-binary-defaults]` scope.
588
        """
589
    )
590

591
    def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> bool:
3✔
592
        if self.value is None:
×
593
            return pex_binary_defaults.emit_warnings
×
594

595
        return self.value
×
596

597

598
class PexExecutionMode(Enum):
3✔
599
    ZIPAPP = "zipapp"
3✔
600
    VENV = "venv"
3✔
601

602

603
class PexExecutionModeField(StringField):
3✔
604
    alias = "execution_mode"
3✔
605
    valid_choices = PexExecutionMode
3✔
606
    expected_type = str
3✔
607
    default = PexExecutionMode.ZIPAPP.value
3✔
608
    help = help_text(
3✔
609
        f"""
610
        The mode the generated PEX file will run in.
611

612
        The traditional PEX file runs in a modified {PexExecutionMode.ZIPAPP.value!r} mode (See:
613
        https://www.python.org/dev/peps/pep-0441/) where zipped internal code and dependencies
614
        are first unpacked to disk. This mode achieves the fastest cold start times and may, for
615
        example be the best choice for cloud lambda functions.
616

617
        The fastest execution mode in the steady state is {PexExecutionMode.VENV.value!r}, which
618
        generates a virtual environment from the PEX file on first run, but then achieves near
619
        native virtual environment start times. This mode also benefits from a traditional virtual
620
        environment `sys.path`, giving maximum compatibility with stdlib and third party APIs.
621
        """
622
    )
623

624

625
class PexLayout(Enum):
3✔
626
    ZIPAPP = "zipapp"
3✔
627
    PACKED = "packed"
3✔
628
    LOOSE = "loose"
3✔
629

630

631
class PexLayoutField(StringField):
3✔
632
    alias = "layout"
3✔
633
    valid_choices = PexLayout
3✔
634
    expected_type = str
3✔
635
    default = PexLayout.ZIPAPP.value
3✔
636
    help = help_text(
3✔
637
        f"""
638
        The layout used for the PEX binary.
639

640
        By default, a PEX is created as a single file zipapp, but either a packed or loose directory
641
        tree based layout can be chosen instead.
642

643
        A packed layout PEX is an executable directory structure designed to have
644
        cache-friendly characteristics for syncing incremental updates to PEXed applications over
645
        a network. At the top level of the packed directory tree there is an executable
646
        `__main__.py` script. The directory can also be executed by passing its path to a Python
647
        executable; e.g: `python packed-pex-dir/`. The Pex bootstrap code and all dependency code
648
        are packed into individual zip files for efficient caching and syncing.
649

650
        A loose layout PEX is similar to a packed PEX, except that neither the Pex bootstrap code
651
        nor the dependency code are packed into zip files, but are instead present as collections of
652
        loose files in the directory tree providing different caching and syncing tradeoffs.
653

654
        Both zipapp and packed layouts install themselves in the `$PEX_ROOT` as loose apps by
655
        default before executing, but these layouts compose with
656
        `{PexExecutionModeField.alias}='{PexExecutionMode.ZIPAPP.value}'` as well.
657
        """
658
    )
659

660

661
class PexIncludeRequirementsField(BoolField):
3✔
662
    alias = "include_requirements"
3✔
663
    default = True
3✔
664
    help = help_text(
3✔
665
        """
666
        Whether to include the third party requirements the binary depends on in the
667
        packaged PEX file.
668
        """
669
    )
670

671

672
class PexIncludeSourcesField(BoolField):
3✔
673
    alias = "include_sources"
3✔
674
    default = True
3✔
675
    help = help_text(
3✔
676
        """
677
        Whether to include your first party sources the binary uses in the packaged PEX file.
678
        """
679
    )
680

681

682
class PexIncludeToolsField(BoolField):
3✔
683
    alias = "include_tools"
3✔
684
    default = False
3✔
685
    help = help_text(
3✔
686
        """
687
        Whether to include Pex tools in the PEX bootstrap code.
688

689
        With tools included, the generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help`
690
        to gain access to all the available tools.
691
        """
692
    )
693

694

695
class PexVenvSitePackagesCopies(BoolField):
3✔
696
    alias = "venv_site_packages_copies"
3✔
697
    default = False
3✔
698
    help = help_text(
3✔
699
        """
700
        If execution_mode is venv, populate the venv site packages using hard links or copies of resolved PEX dependencies instead of symlinks.
701

702
        This can be used to work around problems with tools or libraries that are confused by symlinked source files.
703
        """
704
    )
705

706

707
class PexVenvHermeticScripts(BoolField):
3✔
708
    alias = "venv_hermetic_scripts"
3✔
709
    default = True
3✔
710
    help = help_text(
3✔
711
        """
712
        If execution_mode is "venv", emit a hermetic venv `pex` script and hermetic console scripts.
713

714
        The venv `pex` script and the venv console scripts are constructed to be hermetic by
715
        default; Python is executed with `-sE` to restrict the `sys.path` to the PEX venv contents
716
        only. Setting this field to `False` elides the Python `-sE` restrictions and can be used to
717
        interoperate with frameworks that use `PYTHONPATH` manipulation to run code.
718
        """
719
    )
720

721

722
_PEX_BINARY_COMMON_FIELDS = (
3✔
723
    EnvironmentField,
724
    InterpreterConstraintsField,
725
    PythonResolveField,
726
    PexBinaryDependenciesField,
727
    PexCheckField,
728
    PexCompletePlatformsField,
729
    PexInheritPathField,
730
    PexStripEnvField,
731
    PexIgnoreErrorsField,
732
    PexShBootField,
733
    PexShebangField,
734
    PexEmitWarningsField,
735
    PexLayoutField,
736
    PexExecutionModeField,
737
    PexIncludeRequirementsField,
738
    PexIncludeSourcesField,
739
    PexIncludeToolsField,
740
    PexVenvSitePackagesCopies,
741
    PexVenvHermeticScripts,
742
    PexExtraBuildArgsField,
743
    RestartableField,
744
)
745

746

747
class PexBinary(Target):
3✔
748
    alias = "pex_binary"
3✔
749
    core_fields = (
3✔
750
        *COMMON_TARGET_FIELDS,
751
        *_PEX_BINARY_COMMON_FIELDS,
752
        PexEntryPointField,
753
        PexScriptField,
754
        PexExecutableField,
755
        PexArgsField,
756
        PexEnvField,
757
        OutputPathField,
758
    )
759
    help = help_text(
3✔
760
        f"""
761
        A Python target that can be converted into an executable PEX file.
762

763
        PEX files are self-contained executable files that contain a complete Python environment
764
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
765
        """
766
    )
767

768
    def validate(self) -> None:
3✔
769
        got_entry_point = self[PexEntryPointField].value is not None
×
770
        got_script = self[PexScriptField].value is not None
×
771
        got_executable = self[PexExecutableField].value is not None
×
772

773
        if (got_entry_point + got_script + got_executable) > 1:
×
774
            raise InvalidTargetException(
×
775
                softwrap(
776
                    f"""
777
                    The `{self.alias}` target {self.address} cannot set more than one of the
778
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
779
                    `{self[PexExecutableField].alias}` fields at the same time.
780
                    To fix, please remove all but one.
781
                    """
782
                )
783
            )
784

785

786
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
3✔
787
    alias = "entry_points"
3✔
788
    default = None
3✔
789
    help = help_text(
3✔
790
        """
791
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
792

793
        Use a file name, relative to the BUILD file, like `app.py`. You can also set the
794
        function to run, like `app.py:func`. Pants will convert these file names into well-formed
795
        entry points, like `app.py:func` into `path.to.app:func.`
796

797
        If you want the entry point to be for a third-party dependency or to use a console
798
        script, use the `pex_binary` target directly.
799
        """
800
    )
801

802

803
class PexBinariesOverrideField(OverridesField):
3✔
804
    help = help_text(
3✔
805
        f"""
806
        Override the field values for generated `{PexBinary.alias}` targets.
807

808
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
809
        their overrides. You may either use a single string or a tuple of strings to override
810
        multiple targets.
811

812
        For example:
813

814
            overrides={{
815
              "foo.py": {{"execution_mode": "venv"]}},
816
              "bar.py:main": {{"restartable": True]}},
817
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
818
            }}
819

820
        Every key is validated to belong to this target's `entry_points` field.
821

822
        If you'd like to override a field's value for every `{PexBinary.alias}` target
823
        generated by this target, change the field directly on this target rather than using the
824
        `overrides` field.
825

826
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
827
        same field more than one time for the `entry_point`.
828
        """
829
    )
830

831

832
class PexBinariesGeneratorTarget(TargetGenerator):
3✔
833
    alias = "pex_binaries"
3✔
834
    help = help_text(
3✔
835
        """
836
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
837

838
        This is solely meant to reduce duplication when you have multiple scripts in the same
839
        directory; it's valid to use a distinct `pex_binary` target for each script/binary
840
        instead.
841

842
        This target generator does not work well to generate `pex_binary` targets where the entry
843
        point is for a third-party dependency. Dependency inference will not work for those, so
844
        you will have to set lots of custom metadata for each binary; prefer an explicit
845
        `pex_binary` target in that case. This target generator works best when the entry point
846
        is a first-party file, like `app.py` or `app.py:main`.
847
        """
848
    )
849
    generated_target_cls = PexBinary
3✔
850
    core_fields = (
3✔
851
        *COMMON_TARGET_FIELDS,
852
        PexEntryPointsField,
853
        PexBinariesOverrideField,
854
    )
855
    copied_fields = COMMON_TARGET_FIELDS
3✔
856
    moved_fields = _PEX_BINARY_COMMON_FIELDS
3✔
857

858

859
class PexBinaryDefaults(Subsystem):
3✔
860
    options_scope = "pex-binary-defaults"
3✔
861
    help = "Default settings for creating PEX executables."
3✔
862

863
    emit_warnings = BoolOption(
3✔
864
        default=True,
865
        help=softwrap(
866
            """
867
            Whether built PEX binaries should emit PEX warnings at runtime by default.
868

869
            Can be overridden by specifying the `emit_warnings` parameter of individual
870
            `pex_binary` targets
871
            """
872
        ),
873
        advanced=True,
874
    )
875

876

877
# -----------------------------------------------------------------------------------------------
878
# `python_test` and `python_tests` targets
879
# -----------------------------------------------------------------------------------------------
880

881

882
class PythonTestSourceField(PythonSourceField):
3✔
883
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
3✔
884

885
    def validate_resolved_files(self, files: Sequence[str]) -> None:
3✔
886
        super().validate_resolved_files(files)
×
887
        file = files[0]
×
888
        file_name = os.path.basename(file)
×
889
        if file_name == "conftest.py":
×
890
            raise InvalidFieldException(
×
891
                softwrap(
892
                    f"""
893
                    The {repr(self.alias)} field in target {self.address} should not be set to the
894
                    file 'conftest.py', but was set to {repr(self.value)}.
895

896
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
897
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
898
                    autogenerate a `python_test_utils` target.
899
                    """
900
                )
901
            )
902

903

904
class PythonTestsDependenciesField(PythonDependenciesField):
3✔
905
    supports_transitive_excludes = True
3✔
906

907

908
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
3✔
909
    alias = "entry_point_dependencies"
3✔
910
    help = help_text(
3✔
911
        lambda: f"""
912
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
913

914
        This is a dict where each key is a `{PythonDistribution.alias}` address
915
        and the value is a list or tuple of entry point groups and/or entry points
916
        on that target. The strings in the value list/tuple must be one of:
917
        - "entry.point.group/entry-point-name" to depend on a named entry point
918
        - "entry.point.group" (without a "/") to depend on an entry point group
919
        - "*" to get all entry points on the target
920

921
        For example:
922

923
            {PythonTestsEntryPointDependenciesField.alias}={{
924
                "//foo/address:dist_tgt": ["*"],  # all entry points
925
                "bar:dist_tgt": ["console_scripts"],  # only from this group
926
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
927
                "another:dist_tgt": [  # multiple entry points
928
                    "console_scripts/my-script",
929
                    "console_scripts/another-script",
930
                    "entry.point.group/entry-point-name",
931
                    "other.group",
932
                    "gui_scripts",
933
                ],
934
            }}
935

936
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
937
        will be added as dependencies so that they are available on PYTHONPATH
938
        during tests.
939

940
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
941
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
942
        `entry_points.txt` file will only include the entry points requested on this
943
        field. This allows the tests, or the code under test, to lookup entry points'
944
        metadata using an API like the `importlib.metadata.entry_points()` API in the
945
        standard library (available on older Python interpreters via the
946
        `importlib-metadata` distribution).
947
        """
948
    )
949

950

951
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
952
class PythonTestsTimeoutField(IntField):
3✔
953
    alias = "timeout"
3✔
954
    help = help_text(
3✔
955
        """
956
        A timeout (in seconds) used by each test file belonging to this target.
957

958
        If unset, will default to `[test].timeout_default`; if that option is also unset,
959
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
960
        applies if the option `--test-timeouts` is set to true (the default).
961
        """
962
    )
963
    valid_numbers = ValidNumbers.positive_only
3✔
964

965
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
3✔
966
        """Determine the timeout (in seconds) after resolving conflicting global options in the
967
        `pytest` and `test` scopes.
968

969
        This function is deprecated and should be replaced by the similarly named one in
970
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
971
        """
972

973
        enabled = test.options.timeouts
×
974
        timeout_default = test.options.timeout_default
×
975
        timeout_maximum = test.options.timeout_maximum
×
976

977
        if not enabled:
×
978
            return None
×
979
        if self.value is None:
×
980
            if timeout_default is None:
×
981
                return None
×
982
            result = cast(int, timeout_default)
×
983
        else:
984
            result = self.value
×
985
        if timeout_maximum is not None:
×
986
            return min(result, cast(int, timeout_maximum))
×
987
        return result
×
988

989

990
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
3✔
991
    pass
3✔
992

993

994
class PythonTestsXdistConcurrencyField(IntField):
3✔
995
    alias = "xdist_concurrency"
3✔
996
    help = help_text(
3✔
997
        """
998
        Maximum number of CPUs to allocate to run each test file belonging to this target.
999

1000
        Tests are spread across multiple CPUs using `pytest-xdist`
1001
        (https://pytest-xdist.readthedocs.io/en/latest/index.html).
1002
        Use of `pytest-xdist` must be enabled using the `[pytest].xdist_enabled` option for
1003
        this field to have an effect.
1004

1005
        If `pytest-xdist` is enabled and this field is unset, Pants will attempt to derive
1006
        the concurrency for test sources by counting the number of tests in each file.
1007

1008
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1009
        """
1010
    )
1011

1012

1013
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
3✔
1014
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
3✔
1015

1016

1017
class SkipPythonTestsField(BoolField):
3✔
1018
    alias = "skip_tests"
3✔
1019
    default = False
3✔
1020
    help = "If true, don't run this target's tests."
3✔
1021

1022

1023
_PYTHON_TEST_MOVED_FIELDS = (
3✔
1024
    PythonTestsDependenciesField,
1025
    # This field is registered in the experimental backend for now.
1026
    # PythonTestsEntryPointDependenciesField,
1027
    PythonResolveField,
1028
    PythonRunGoalUseSandboxField,
1029
    PythonTestsTimeoutField,
1030
    PythonTestsXdistConcurrencyField,
1031
    PythonTestsBatchCompatibilityTagField,
1032
    RuntimePackageDependenciesField,
1033
    PythonTestsExtraEnvVarsField,
1034
    InterpreterConstraintsField,
1035
    SkipPythonTestsField,
1036
    EnvironmentField,
1037
)
1038

1039

1040
class PythonTestTarget(Target):
3✔
1041
    alias = "python_test"
3✔
1042
    core_fields = (
3✔
1043
        *COMMON_TARGET_FIELDS,
1044
        *_PYTHON_TEST_MOVED_FIELDS,
1045
        PythonTestsDependenciesField,
1046
        PythonTestSourceField,
1047
    )
1048
    help = help_text(
3✔
1049
        f"""
1050
        A single Python test file, written in either Pytest style or unittest style.
1051

1052
        All test util code, including `conftest.py`, should go into a dedicated `python_source`
1053
        target and then be included in the `dependencies` field. (You can use the
1054
        `python_test_utils` target to generate these `python_source` targets.)
1055

1056
        See {doc_url("docs/python/goals/test")}
1057
        """
1058
    )
1059

1060

1061
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
3✔
1062
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
3✔
1063
    default = ("test_*.py", "*_test.py", "tests.py")
3✔
1064
    help = generate_multiple_sources_field_help_message(
3✔
1065
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1066
    )
1067

1068
    def validate_resolved_files(self, files: Sequence[str]) -> None:
3✔
1069
        super().validate_resolved_files(files)
×
1070
        # We don't technically need to error for `conftest.py` here because `PythonTestSourceField`
1071
        # already validates this, but we get a better error message this way so that users don't
1072
        # have to reason about generated targets.
1073
        conftest_files = [fp for fp in files if os.path.basename(fp) == "conftest.py"]
×
1074
        if conftest_files:
×
1075
            raise InvalidFieldException(
×
1076
                softwrap(
1077
                    f"""
1078
                    The {repr(self.alias)} field in target {self.address} should not include the
1079
                    file 'conftest.py', but included these: {conftest_files}.
1080

1081
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1082
                    You can run `{bin_name()} tailor` after removing the files from the
1083
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1084
                    `python_test_utils` target.
1085
                    """
1086
                )
1087
            )
1088

1089

1090
class PythonTestsOverrideField(OverridesField):
3✔
1091
    help = generate_file_based_overrides_field_help_message(
3✔
1092
        PythonTestTarget.alias,
1093
        """
1094
        overrides={
1095
            "foo_test.py": {"timeout": 120},
1096
            "bar_test.py": {"timeout": 200},
1097
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1098
        }
1099
        """,
1100
    )
1101

1102

1103
class PythonTestsGeneratorTarget(TargetFilesGenerator):
3✔
1104
    alias = "python_tests"
3✔
1105
    core_fields = (
3✔
1106
        *COMMON_TARGET_FIELDS,
1107
        PythonTestsGeneratingSourcesField,
1108
        PythonTestsOverrideField,
1109
    )
1110
    generated_target_cls = PythonTestTarget
3✔
1111
    copied_fields = COMMON_TARGET_FIELDS
3✔
1112
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
3✔
1113
    settings_request_cls = PythonFilesGeneratorSettingsRequest
3✔
1114
    help = "Generate a `python_test` target for each file in the `sources` field."
3✔
1115

1116

1117
# -----------------------------------------------------------------------------------------------
1118
# `python_source`, `python_sources`, and `python_test_utils` targets
1119
# -----------------------------------------------------------------------------------------------
1120

1121

1122
class PythonSourceTarget(Target):
3✔
1123
    alias = "python_source"
3✔
1124
    core_fields = (
3✔
1125
        *COMMON_TARGET_FIELDS,
1126
        InterpreterConstraintsField,
1127
        PythonDependenciesField,
1128
        PythonResolveField,
1129
        PythonRunGoalUseSandboxField,
1130
        PythonSourceField,
1131
        RestartableField,
1132
    )
1133
    help = "A single Python source file."
3✔
1134

1135

1136
class PythonSourcesOverridesField(OverridesField):
3✔
1137
    help = generate_file_based_overrides_field_help_message(
3✔
1138
        PythonSourceTarget.alias,
1139
        """
1140
        overrides={
1141
            "foo.py": {"skip_pylint": True]},
1142
            "bar.py": {"skip_flake8": True]},
1143
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1144
        }"
1145
        """,
1146
    )
1147

1148

1149
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
3✔
1150
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
3✔
1151
    help = generate_multiple_sources_field_help_message(
3✔
1152
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1153
    )
1154

1155

1156
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
3✔
1157
    default = (
3✔
1158
        ("*.py", "*.pyi")
1159
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1160
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1161
    )
1162
    help = generate_multiple_sources_field_help_message(
3✔
1163
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1164
    )
1165

1166

1167
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
3✔
1168
    alias = "python_test_utils"
3✔
1169
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1170
    core_fields = (
3✔
1171
        *COMMON_TARGET_FIELDS,
1172
        PythonTestUtilsGeneratingSourcesField,
1173
        PythonSourcesOverridesField,
1174
    )
1175
    generated_target_cls = PythonSourceTarget
3✔
1176
    copied_fields = COMMON_TARGET_FIELDS
3✔
1177
    moved_fields = (
3✔
1178
        PythonResolveField,
1179
        PythonRunGoalUseSandboxField,
1180
        PythonDependenciesField,
1181
        InterpreterConstraintsField,
1182
    )
1183
    settings_request_cls = PythonFilesGeneratorSettingsRequest
3✔
1184
    help = help_text(
3✔
1185
        """
1186
        Generate a `python_source` target for each file in the `sources` field.
1187

1188
        This target generator is intended for test utility files like `conftest.py` or
1189
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1190
        way as the `python_sources` target generator does, only that the `sources` field has a
1191
        different default. So it is valid to use `python_sources` instead. However, this target
1192
        can be helpful to better model your code by keeping separate test support files vs.
1193
        production files.
1194
        """
1195
    )
1196

1197

1198
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
3✔
1199
    alias = "python_sources"
3✔
1200
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1201
    core_fields = (
3✔
1202
        *COMMON_TARGET_FIELDS,
1203
        PythonSourcesGeneratingSourcesField,
1204
        PythonSourcesOverridesField,
1205
    )
1206
    generated_target_cls = PythonSourceTarget
3✔
1207
    copied_fields = COMMON_TARGET_FIELDS
3✔
1208
    moved_fields = (
3✔
1209
        PythonResolveField,
1210
        PythonRunGoalUseSandboxField,
1211
        PythonDependenciesField,
1212
        InterpreterConstraintsField,
1213
        RestartableField,
1214
    )
1215
    settings_request_cls = PythonFilesGeneratorSettingsRequest
3✔
1216
    help = help_text(
3✔
1217
        """
1218
        Generate a `python_source` target for each file in the `sources` field.
1219

1220
        You can either use this target generator or `python_test_utils` for test utility files
1221
        like `conftest.py`. They behave identically, but can help to better model and keep
1222
        separate test support files vs. production files.
1223
        """
1224
    )
1225

1226

1227
# -----------------------------------------------------------------------------------------------
1228
# `python_requirement` target
1229
# -----------------------------------------------------------------------------------------------
1230

1231

1232
class _PipRequirementSequenceField(Field):
3✔
1233
    value: tuple[PipRequirement, ...]
3✔
1234

1235
    @classmethod
3✔
1236
    def compute_value(
3✔
1237
        cls, raw_value: Iterable[str] | None, address: Address
1238
    ) -> tuple[PipRequirement, ...]:
1239
        value = super().compute_value(raw_value, address)
×
1240
        if value is None:
×
1241
            return ()
×
1242
        invalid_type_error = InvalidFieldTypeException(
×
1243
            address,
1244
            cls.alias,
1245
            value,
1246
            expected_type="an iterable of pip-style requirement strings (e.g. a list)",
1247
        )
1248
        if isinstance(value, str) or not isinstance(value, collections.abc.Iterable):
×
1249
            raise invalid_type_error
×
1250
        result = []
×
1251
        for v in value:
×
1252
            # We allow passing a pre-parsed `PipRequirement`. This is intended for macros which
1253
            # might have already parsed so that we can avoid parsing multiple times.
1254
            if isinstance(v, PipRequirement):
×
1255
                result.append(v)
×
1256
            elif isinstance(v, str):
×
1257
                try:
×
1258
                    parsed = PipRequirement.parse(
×
1259
                        v, description_of_origin=f"the '{cls.alias}' field for the target {address}"
1260
                    )
1261
                except ValueError as e:
×
1262
                    raise InvalidFieldException(e)
×
1263
                result.append(parsed)
×
1264
            else:
1265
                raise invalid_type_error
×
1266
        return tuple(result)
×
1267

1268

1269
class PythonRequirementDependenciesField(Dependencies):
3✔
1270
    pass
3✔
1271

1272

1273
class PythonRequirementsField(_PipRequirementSequenceField):
3✔
1274
    alias = "requirements"
3✔
1275
    required = True
3✔
1276
    help = help_text(
3✔
1277
        """
1278
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1279

1280
        You can specify multiple requirements for the same project in order to use environment
1281
        markers, such as `["foo>=1.2,<1.3 ; python_version>\'3.6\'", "foo==0.9 ;
1282
        python_version<'3'\"]`.
1283

1284
        If the requirement depends on some other requirement to work, such as needing
1285
        `setuptools` to be built, use the `dependencies` field instead.
1286
        """
1287
    )
1288

1289

1290
_default_module_mapping_url = git_url(
3✔
1291
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1292
)
1293

1294

1295
class PythonRequirementModulesField(StringSequenceField):
3✔
1296
    alias = "modules"
3✔
1297
    help = help_text(
3✔
1298
        f"""
1299
        The modules this requirement provides (used for dependency inference).
1300

1301
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1302
        "easy_install"]`.
1303

1304
        Usually you can leave this field off. If unspecified, Pants will first look at the
1305
        default module mapping ({_default_module_mapping_url}), and then will default to
1306
        the normalized project name. For example, the requirement `Django` would default to
1307
        the module `django`.
1308

1309
        Mutually exclusive with the `type_stub_modules` field.
1310
        """
1311
    )
1312

1313

1314
class PythonRequirementTypeStubModulesField(StringSequenceField):
3✔
1315
    alias = "type_stub_modules"
3✔
1316
    help = help_text(
3✔
1317
        f"""
1318
        The modules this requirement provides if the requirement is a type stub (used for
1319
        dependency inference).
1320

1321
        For example, the requirement `types-requests` provides `["requests"]`.
1322

1323
        Usually you can leave this field off. If unspecified, Pants will first look at the
1324
        default module mapping ({_default_module_mapping_url}). If not found _and_ the
1325
        requirement name starts with `types-` or `stubs-`, or ends with `-types` or `-stubs`,
1326
        will default to that requirement name without the prefix/suffix. For example,
1327
        `types-requests` would default to `requests`. Otherwise, will be treated like a normal
1328
        requirement (see the `modules` field).
1329

1330
        Mutually exclusive with the `modules` field.
1331
        """
1332
    )
1333

1334

1335
def normalize_module_mapping(
3✔
1336
    mapping: Mapping[str, Iterable[str]] | None,
1337
) -> FrozenDict[str, tuple[str, ...]]:
1338
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
×
1339

1340

1341
class PythonRequirementResolveField(PythonResolveField):
3✔
1342
    alias = "resolve"
3✔
1343
    required = False
3✔
1344
    help = help_text(
3✔
1345
        """
1346
        The resolve from `[python].resolves` that this requirement is included in.
1347

1348
        If not defined, will default to `[python].default_resolve`.
1349

1350
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1351
        it will include all requirements that are declared with that resolve.
1352
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1353
        they use via their `resolve` field; so, for your first-party code to use a
1354
        particular `python_requirement` target, that requirement must be included in the resolve
1355
        used by that code.
1356
        """
1357
    )
1358

1359

1360
class PythonRequirementFindLinksField(StringSequenceField):
3✔
1361
    # NB: This is solely used for `pants_requirements` target generation
1362
    alias = "_find_links"
3✔
1363
    required = False
3✔
1364
    default = ()
3✔
1365
    help = "<Internal>"
3✔
1366

1367

1368
class PythonRequirementEntryPointField(EntryPointField):
3✔
1369
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1370
    pass
3✔
1371

1372

1373
class PythonRequirementTarget(Target):
3✔
1374
    alias = "python_requirement"
3✔
1375
    core_fields = (
3✔
1376
        *COMMON_TARGET_FIELDS,
1377
        PythonRequirementsField,
1378
        PythonRequirementDependenciesField,
1379
        PythonRequirementModulesField,
1380
        PythonRequirementTypeStubModulesField,
1381
        PythonRequirementResolveField,
1382
        PythonRequirementEntryPointField,
1383
        PythonRequirementFindLinksField,
1384
    )
1385
    help = help_text(
3✔
1386
        f"""
1387
        A Python requirement installable by pip.
1388

1389
        This target is useful when you want to declare Python requirements inline in a
1390
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1391
        the target generator `python_requirements` to convert each
1392
        requirement into a `python_requirement` target automatically. For Poetry, use
1393
        `poetry_requirements`.
1394

1395
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1396
        """
1397
    )
1398

1399
    def validate(self) -> None:
3✔
1400
        if (
×
1401
            self[PythonRequirementModulesField].value
1402
            and self[PythonRequirementTypeStubModulesField].value
1403
        ):
1404
            raise InvalidTargetException(
×
1405
                softwrap(
1406
                    f"""
1407
                    The `{self.alias}` target {self.address} cannot set both the
1408
                    `{self[PythonRequirementModulesField].alias}` and
1409
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1410
                    To fix, please remove one.
1411
                    """
1412
                )
1413
            )
1414

1415

1416
# -----------------------------------------------------------------------------------------------
1417
# `python_distribution` target
1418
# -----------------------------------------------------------------------------------------------
1419

1420

1421
# See `target_types_rules.py` for a dependency injection rule.
1422
class PythonDistributionDependenciesField(Dependencies):
3✔
1423
    supports_transitive_excludes = True
3✔
1424

1425

1426
class PythonProvidesField(ScalarField, AsyncFieldMixin):
3✔
1427
    alias = "provides"
3✔
1428
    expected_type = PythonArtifact
3✔
1429
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
3✔
1430
    value: PythonArtifact
3✔
1431
    required = True
3✔
1432
    help = help_text(
3✔
1433
        f"""
1434
        The setup.py kwargs for the external artifact built from this target.
1435

1436
        You must define `name`. You can also set almost any keyword argument accepted by setup.py
1437
        in the `setup()` function:
1438
        (https://packaging.python.org/guides/distributing-packages-using-setuptools/#setup-args).
1439

1440
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1441
        """
1442
    )
1443

1444
    @classmethod
3✔
1445
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
3✔
1446
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1447

1448

1449
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
3✔
1450
    alias = "entry_points"
3✔
1451
    required = False
3✔
1452
    help = help_text(
3✔
1453
        f"""
1454
        Any entry points, such as `console_scripts` and `gui_scripts`.
1455

1456
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1457
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1458
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1459
        `pex_binary` target.
1460

1461
        Example:
1462

1463
            entry_points={{
1464
              "console_scripts": {{
1465
                "my-script": "project.app:main",
1466
                "another-script": "project/subdir:pex_binary_tgt"
1467
              }}
1468
            }}
1469

1470
        Note that Pants will assume that any value that either starts with `:` or has `/` in it,
1471
        is a target address to a `pex_binary` target. Otherwise, it will assume it's a setuptools
1472
        entry point as defined by
1473
        https://packaging.python.org/specifications/entry-points/#entry-points-specification. Use
1474
        `//` as a prefix for target addresses if you need to disambiguate.
1475

1476
        Pants will attempt to infer dependencies, which you can confirm by running:
1477

1478
            {bin_name()} dependencies <python_distribution target address>
1479
        """
1480
    )
1481

1482

1483
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
3✔
1484
    help = help_text(
3✔
1485
        """
1486
        The path to the directory to write the distribution file to, relative the dist directory.
1487

1488
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1489
        level of the dist dir.
1490
        """
1491
    )
1492
    alias = "output_path"
3✔
1493
    default = ""
3✔
1494

1495

1496
@dataclass(frozen=True)
3✔
1497
class PythonDistributionEntryPoint:
3✔
1498
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1499

1500
    entry_point: EntryPoint
3✔
1501
    pex_binary_address: Address | None
3✔
1502

1503

1504
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1505
@dataclass(frozen=True)
3✔
1506
class ResolvedPythonDistributionEntryPoints:
3✔
1507
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1508
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
3✔
1509

1510
    @property
3✔
1511
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
3✔
1512
        """Filters out all entry points from pex binary targets."""
1513
        return FrozenDict(
×
1514
            {
1515
                category: FrozenDict(
1516
                    {
1517
                        ep_name: ep_val.entry_point
1518
                        for ep_name, ep_val in entry_points.items()
1519
                        if not ep_val.pex_binary_address
1520
                    }
1521
                )
1522
                for category, entry_points in self.val.items()
1523
            }
1524
        )
1525

1526
    @property
3✔
1527
    def pex_binary_addresses(self) -> Addresses:
3✔
1528
        """Returns the addresses to all pex binary targets owning entry points used."""
1529
        return Addresses(
×
1530
            ep_val.pex_binary_address
1531
            for category, entry_points in self.val.items()
1532
            for ep_val in entry_points.values()
1533
            if ep_val.pex_binary_address
1534
        )
1535

1536

1537
@dataclass(frozen=True)
3✔
1538
class ResolvePythonDistributionEntryPointsRequest:
3✔
1539
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1540
    that should be resolved into a setuptools entry point.
1541

1542
    If the `entry_points_field` is present, inspect the specified entry points.
1543
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1544

1545
    This is to support inspecting one or the other depending on use case, using the same
1546
    logic for resolving pex_binary addresses etc.
1547
    """
1548

1549
    entry_points_field: PythonDistributionEntryPointsField | None = None
3✔
1550
    provides_field: PythonProvidesField | None = None
3✔
1551

1552
    def __post_init__(self):
3✔
1553
        # Must provide at least one of these fields.
1554
        assert self.entry_points_field or self.provides_field
×
1555

1556

1557
class WheelField(BoolField):
3✔
1558
    alias = "wheel"
3✔
1559
    default = True
3✔
1560
    help = "Whether to build a wheel for the distribution."
3✔
1561

1562

1563
class SDistField(BoolField):
3✔
1564
    alias = "sdist"
3✔
1565
    default = True
3✔
1566
    help = "Whether to build an sdist for the distribution."
3✔
1567

1568

1569
class ConfigSettingsField(DictStringToStringSequenceField):
3✔
1570
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1571

1572
    NOTE: PEP-517 appears to be ill-defined wrt value types in config_settings. It mentions that:
1573

1574
    - Build backends may assign any semantics they like to this dictionary, i.e., the backend
1575
      decides what the value types it accepts are.
1576

1577
    - Build frontends should support string values, and may also support other mechanisms
1578
      (apparently meaning other types).
1579

1580
    Presumably, a well-behaved frontend is supposed to work with any backend, but it cannot
1581
    do so without knowledge of what types each backend expects in the config_settings values,
1582
    as it has to set those values.
1583

1584
    See a similar discussion in the context of Pip: https://github.com/pypa/pip/issues/5771 .
1585

1586
    In practice, the backend we currently care about, setuptools.build_meta, expects a
1587
    dict with one key, --global-option, whose value is a sequence of cmd-line setup options.
1588
    It ignores all other keys.  So, to accommodate setuptools, the type of this field is
1589
    DictStringToStringSequenceField, and hopefully other backends we may encounter in the future
1590
    can work with this too.  If we need to handle values that can be strings or string sequences,
1591
    as demonstrated in the example in PEP-517, then we will need to change this field's type
1592
    to an as-yet-nonexistent "DictStringToStringOrStringSequenceField".
1593
    """
1594

1595

1596
class WheelConfigSettingsField(ConfigSettingsField):
3✔
1597
    alias = "wheel_config_settings"
3✔
1598
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
3✔
1599

1600

1601
class SDistConfigSettingsField(ConfigSettingsField):
3✔
1602
    alias = "sdist_config_settings"
3✔
1603
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
3✔
1604

1605

1606
class BuildBackendEnvVarsField(StringSequenceField):
3✔
1607
    alias = "env_vars"
3✔
1608
    required = False
3✔
1609
    help = help_text(
3✔
1610
        """
1611
        Environment variables to set when running the PEP-517 build backend.
1612

1613
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1614
        or just `ENV_VAR` to copy the value from Pants's own environment.
1615
        """
1616
    )
1617

1618

1619
class GenerateSetupField(TriBoolField):
3✔
1620
    alias = "generate_setup"
3✔
1621
    required = False
3✔
1622
    # The default behavior if this field is unspecified is controlled by the
1623
    # --generate-setup-default option in the setup-py-generation scope.
1624
    default = None
3✔
1625

1626
    help = help_text(
3✔
1627
        """
1628
        Whether to generate setup information for this distribution, based on analyzing
1629
        sources and dependencies. Set to False to use existing setup information, such as
1630
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1631
        """
1632
    )
1633

1634

1635
class LongDescriptionPathField(StringField):
3✔
1636
    alias = "long_description_path"
3✔
1637
    required = False
3✔
1638

1639
    help = help_text(
3✔
1640
        """
1641
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1642

1643
        Path is relative to the build root.
1644

1645
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1646

1647
        This field won't automatically set `long_description_content_type` field for you.
1648
        You have to specify this field yourself in the `provides` field.
1649
        """
1650
    )
1651

1652

1653
class PythonDistribution(Target):
3✔
1654
    alias: ClassVar[str] = "python_distribution"
3✔
1655
    core_fields = (
3✔
1656
        *COMMON_TARGET_FIELDS,
1657
        InterpreterConstraintsField,
1658
        PythonDistributionDependenciesField,
1659
        PythonDistributionEntryPointsField,
1660
        PythonProvidesField,
1661
        GenerateSetupField,
1662
        WheelField,
1663
        SDistField,
1664
        WheelConfigSettingsField,
1665
        SDistConfigSettingsField,
1666
        BuildBackendEnvVarsField,
1667
        LongDescriptionPathField,
1668
        PythonDistributionOutputPathField,
1669
    )
1670
    help = help_text(
3✔
1671
        f"""
1672
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
1673

1674
        See {doc_url("docs/python/overview/building-distributions")}.
1675
        """
1676
    )
1677

1678

1679
# -----------------------------------------------------------------------------------------------
1680
# `vcs_version` target
1681
# -----------------------------------------------------------------------------------------------
1682

1683
# The vcs_version target is defined and registered here in the python backend because the VCS
1684
# version functionality uses a lot of python machinery in its implementation, and because it is
1685
# (at least at the time of writing) highly unlikely to be used outside a python context in practice.
1686
# However, hypothetically, the source file generated by a vcs_version target can be in any language.
1687
# Therefore any language-specific fields (such as python_resolve) are registered as plugin fields
1688
# instead of provided directly here, even though the only language in question is python.
1689

1690

1691
class VCSVersionDummySourceField(OptionalSingleSourceField):
3✔
1692
    """A dummy SourceField for participation in the codegen machinery."""
1693

1694
    alias = "_dummy_source"  # Leading underscore omits the field from help.
3✔
1695
    help = "A version string generated from VCS information"
3✔
1696

1697

1698
class VersionTagRegexField(StringField):
3✔
1699
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
3✔
1700
    alias = "tag_regex"
3✔
1701
    help = help_text(
3✔
1702
        """
1703
        A Python regex string to extract the version string from a VCS tag.
1704

1705
        The regex needs to contain either a single match group, or a group named version,
1706
        that captures the actual version information.
1707

1708
        Note that this is unrelated to the tags field and Pants's own tags concept.
1709

1710
        See https://github.com/pypa/setuptools_scm for implementation details.
1711
        """
1712
    )
1713

1714

1715
class VersionGenerateToField(StringField):
3✔
1716
    required = True
3✔
1717
    alias = "generate_to"
3✔
1718
    help = help_text(
3✔
1719
        """
1720
        Generate the version data to this relative path, using the template field.
1721

1722
        Note that the generated output will not be written to disk in the source tree, but
1723
        will be available as a generated dependency to code that depends on this target.
1724
        """
1725
    )
1726

1727

1728
class VersionTemplateField(StringField):
3✔
1729
    required = True
3✔
1730
    alias = "template"
3✔
1731
    help = help_text(
3✔
1732
        """
1733
        Generate the version data using this format string, which takes a version format kwarg.
1734

1735
        E.g., `'version = "{version}"'`
1736
        """
1737
    )
1738

1739

1740
class VersionVersionSchemeField(StringField):
3✔
1741
    alias = "version_scheme"
3✔
1742
    help = help_text(
3✔
1743
        """
1744
        The version scheme to configure `setuptools_scm` to use.
1745
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
1746
        """
1747
    )
1748

1749

1750
class VersionLocalSchemeField(StringField):
3✔
1751
    alias = "local_scheme"
3✔
1752
    help = help_text(
3✔
1753
        """
1754
        The local scheme to configure `setuptools_scm` to use.
1755
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
1756
        """
1757
    )
1758

1759

1760
class VCSVersion(Target):
3✔
1761
    alias = "vcs_version"
3✔
1762
    core_fields = (
3✔
1763
        *COMMON_TARGET_FIELDS,
1764
        VersionTagRegexField,
1765
        VersionVersionSchemeField,
1766
        VersionLocalSchemeField,
1767
        VCSVersionDummySourceField,
1768
        VersionGenerateToField,
1769
        VersionTemplateField,
1770
    )
1771
    help = help_text(
3✔
1772
        f"""
1773
        Generates a version string from VCS state.
1774

1775
        Uses a constrained but useful subset of the full functionality of setuptools_scm
1776
        (https://github.com/pypa/setuptools_scm). These constraints avoid pitfalls in the
1777
        interaction of setuptools_scm with Pants's hermetic environments.
1778

1779
        In particular, we ignore any existing setuptools_scm config. Instead you must provide
1780
        a subset of that config in this target's fields.
1781

1782
        If you need functionality that is not currently exposed here, please reach out to us at
1783
        {doc_url("community/getting-help")}.
1784
        """
1785
    )
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