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

pantsbuild / pants / 19250292619

11 Nov 2025 12:09AM UTC coverage: 77.865% (-2.4%) from 80.298%
19250292619

push

github

web-flow
flag non-runnable targets used with `code_quality_tool` (#22875)

2 of 5 new or added lines in 2 files covered. (40.0%)

1487 existing lines in 72 files now uncovered.

71448 of 91759 relevant lines covered (77.86%)

3.22 hits per line

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

91.2
/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
11✔
5

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

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

17
from pants.backend.python.macros.python_artifact import PythonArtifact
11✔
18
from pants.backend.python.subsystems.setup import PythonSetup
11✔
19
from pants.core.environments.target_types import EnvironmentField
11✔
20
from pants.core.goals.generate_lockfiles import UnrecognizedResolveNamesError
11✔
21
from pants.core.goals.package import OutputPathField
11✔
22
from pants.core.goals.run import RestartableField
11✔
23
from pants.core.goals.test import (
11✔
24
    RuntimePackageDependenciesField,
25
    TestExtraEnvVarsField,
26
    TestsBatchCompatibilityTagField,
27
    TestSubsystem,
28
)
29
from pants.engine.addresses import Address, Addresses
11✔
30
from pants.engine.target import (
11✔
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
11✔
61
from pants.option.subsystem import Subsystem
11✔
62
from pants.util.docutil import bin_name, doc_url, git_url
11✔
63
from pants.util.frozendict import FrozenDict
11✔
64
from pants.util.pip_requirement import PipRequirement
11✔
65
from pants.util.strutil import help_text, softwrap
11✔
66

67
logger = logging.getLogger(__name__)
11✔
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):
11✔
79
    # Note that Python scripts often have no file ending.
80
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")
11✔
81

82

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

86

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

90

91
class InterpreterConstraintsField(StringSequenceField, AsyncFieldMixin):
11✔
92
    alias = "interpreter_constraints"
11✔
93
    help = help_text(
11✔
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_configured_default(
11✔
112
        self, python_setup: PythonSetup, resolve: PythonResolveField | None
113
    ) -> tuple[str, ...]:
114
        """Return either the given `compatibility` field or the global interpreter constraints.
115

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

124
            warn_on_python2_usage_in_interpreter_constraints(
×
125
                self.value,
126
                description_of_origin=f"the `{self.alias}` field on target at `{self.address}`",
127
            )
128
        return python_setup.compatibility_or_constraints(
1✔
129
            self.value,
130
            resolve.normalized_value(python_setup)
131
            if resolve and python_setup.enable_resolves
132
            else None,
133
        )
134

135

136
class PythonResolveField(StringField, AsyncFieldMixin):
11✔
137
    alias = "resolve"
11✔
138
    required = False
11✔
139
    help = help_text(
11✔
140
        """
141
        The resolve from `[python].resolves` to use.
142

143
        If not defined, will default to `[python].default_resolve`.
144

145
        All dependencies must share the same value for their `resolve` field.
146
        """
147
    )
148

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

162

163
class PrefixedPythonResolveField(PythonResolveField):
11✔
164
    alias = "python_resolve"
11✔
165

166

167
class PythonRunGoalUseSandboxField(TriBoolField):
11✔
168
    alias = "run_goal_use_sandbox"
11✔
169
    help = help_text(
11✔
170
        """
171
        Whether to use a sandbox when `run`ning this target. Defaults to
172
        `[python].default_run_goal_use_sandbox`.
173

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

177
        If false, runs of this target with the `run` goal will use the in-repo sources
178
        directly.
179

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

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

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

195

196
# -----------------------------------------------------------------------------------------------
197
# Target generation support
198
# -----------------------------------------------------------------------------------------------
199

200

201
class PythonFilesGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
11✔
202
    pass
11✔
203

204

205
# -----------------------------------------------------------------------------------------------
206
# `pex_binary` and `pex_binaries` target
207
# -----------------------------------------------------------------------------------------------
208

209

210
# See `target_types_rules.py` for a dependency injection rule.
211
class PexBinaryDependenciesField(Dependencies):
11✔
212
    supports_transitive_excludes = True
11✔
213

214

215
class MainSpecification(ABC):
11✔
216
    @abstractmethod
217
    def iter_pex_args(self) -> Iterator[str]: ...
218

219
    @property
220
    @abstractmethod
221
    def spec(self) -> str: ...
222

223

224
@dataclass(frozen=True)
11✔
225
class EntryPoint(MainSpecification):
11✔
226
    module: str
11✔
227
    function: str | None = None
11✔
228

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

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

287
    def iter_pex_args(self) -> Iterator[str]:
11✔
288
        yield "--entry-point"
×
289
        yield self.spec
×
290

291
    @property
11✔
292
    def spec(self) -> str:
11✔
293
        return self.module if self.function is None else f"{self.module}:{self.function}"
6✔
294

295

296
@dataclass(frozen=True)
11✔
297
class ConsoleScript(MainSpecification):
11✔
298
    name: str
11✔
299

300
    def iter_pex_args(self) -> Iterator[str]:
11✔
301
        yield "--console-script"
×
302
        yield self.name
×
303

304
    @property
11✔
305
    def spec(self) -> str:
11✔
306
        return self.name
11✔
307

308

309
@dataclass(frozen=True)
11✔
310
class Executable(MainSpecification):
11✔
311
    executable: str
11✔
312

313
    @classmethod
11✔
314
    def create(cls, address: Address, filename: str) -> Executable:
11✔
315
        # spec_path is relative to the workspace. The rule is responsible for
316
        # stripping the source root as needed.
317
        return cls(os.path.join(address.spec_path, filename).lstrip(os.path.sep))
1✔
318

319
    def iter_pex_args(self) -> Iterator[str]:
11✔
320
        yield "--executable"
×
321
        # We do NOT yield self.executable or self.spec
322
        # as the path needs additional processing in the rule graph.
323
        # see: build_pex in util_rules/pex
324

325
    @property
11✔
326
    def spec(self) -> str:
11✔
327
        return self.executable
×
328

329

330
class EntryPointField(AsyncFieldMixin, Field):
11✔
331
    alias = "entry_point"
11✔
332
    default = None
11✔
333
    help = help_text(
11✔
334
        """
335
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.
336

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

340
          1) `'app.py'`, Pants will convert into the module `path.to.app`;
341
          2) `'app.py:func'`, Pants will convert into `path.to.app:func`.
342

343
        You may only set one of: this field, or the `script` field, or the `executable` field.
344
        Leave off all three fields to have no entry point.
345
        """
346
    )
347
    value: EntryPoint | None
11✔
348

349
    @classmethod
11✔
350
    def compute_value(cls, raw_value: str | None, address: Address) -> EntryPoint | None:
11✔
351
        value = super().compute_value(raw_value, address)
5✔
352
        if value is None:
5✔
353
            return None
5✔
354
        if not isinstance(value, str):
1✔
355
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
356
        try:
1✔
357
            return EntryPoint.parse(value, provenance=f"for {address}")
1✔
358
        except ValueError as e:
1✔
359
            raise InvalidFieldException(str(e))
1✔
360

361

362
class PexEntryPointField(EntryPointField):
11✔
363
    # Specialist subclass for use with `PexBinary` targets.
364
    pass
11✔
365

366

367
# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
368
@dataclass(frozen=True)
11✔
369
class ResolvedPexEntryPoint:
11✔
370
    val: EntryPoint | None
11✔
371
    file_name_used: bool
11✔
372

373

374
@dataclass(frozen=True)
11✔
375
class ResolvePexEntryPointRequest:
11✔
376
    """Determine the `entry_point` for a `pex_binary` after applying all syntactic sugar."""
377

378
    entry_point_field: EntryPointField
11✔
379

380

381
class PexScriptField(Field):
11✔
382
    alias = "script"
11✔
383
    default = None
11✔
384
    help = help_text(
11✔
385
        """
386
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a script or
387
        console_script as defined by any of the distributions in the PEX.
388

389
        You may only set one of: this field, or the `entry_point` field, or the `executable` field.
390
        Leave off all three fields to have no entry point.
391
        """
392
    )
393
    value: ConsoleScript | None
11✔
394

395
    @classmethod
11✔
396
    def compute_value(cls, raw_value: str | None, address: Address) -> ConsoleScript | None:
11✔
397
        value = super().compute_value(raw_value, address)
1✔
398
        if value is None:
1✔
399
            return None
1✔
400
        if not isinstance(value, str):
1✔
401
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
402
        return ConsoleScript(value)
1✔
403

404

405
class PexExecutableField(Field):
11✔
406
    alias = "executable"
11✔
407
    default = None
11✔
408
    help = help_text(
11✔
409
        """
410
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to an execuatble
411
        local python script. This executable python script is typically something that cannot
412
        be imported so it cannot be used via `script` or `entry_point`.
413

414
        You may only set one of: this field, or the `entry_point` field, or the `script` field.
415
        Leave off all three fields to have no entry point.
416
        """
417
    )
418
    value: Executable | None
11✔
419

420
    @classmethod
11✔
421
    def compute_value(cls, raw_value: str | None, address: Address) -> Executable | None:
11✔
422
        value = super().compute_value(raw_value, address)
1✔
423
        if value is None:
1✔
424
            return None
1✔
425
        if not isinstance(value, str):
1✔
426
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
427
        return Executable.create(address, value)
1✔
428

429

430
class PexArgsField(StringSequenceField):
11✔
431
    alias: ClassVar[str] = "args"
11✔
432
    help = help_text(
11✔
433
        lambda: f"""
434
        Freeze these command-line args into the PEX. Allows you to run generic entry points
435
        on specific arguments without creating a shim file.
436

437
        This is different to `{PexExtraBuildArgsField.alias}`: `{PexArgsField.alias}`
438
        records arguments used by the packaged PEX when executed,
439
        `{PexExtraBuildArgsField.alias}` passes arguments to the process that does the
440
        packaging.
441
        """
442
    )
443

444

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

454
        This is different to `{PexArgsField.alias}`: `{PexArgsField.alias}` records
455
        arguments used by the packaged PEX when executed, `{PexExtraBuildArgsField.alias}`
456
        passes arguments to the process that does the packaging.
457
        """
458
    )
459

460

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

478

479
class PexEnvField(DictStringToStringField):
11✔
480
    alias = "env"
11✔
481
    help = help_text(
11✔
482
        """
483
        Freeze these environment variables into the PEX. Allows you to run generic entry points
484
        on a specific environment without creating a shim file.
485
        """
486
    )
487

488

489
class PexCompletePlatformsField(SpecialCasedDependencies):
11✔
490
    alias = "complete_platforms"
11✔
491
    help = help_text(
11✔
492
        f"""
493
        The platforms the built PEX should be compatible with.
494

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

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

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

504
        See {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")} for details on how to create this file.
505
        """
506
    )
507

508

509
class PexInheritPathField(StringField):
11✔
510
    alias = "inherit_path"
11✔
511
    valid_choices = ("false", "fallback", "prefer")
11✔
512
    help = help_text(
11✔
513
        """
514
        Whether to inherit the `sys.path` (aka PYTHONPATH) of the environment that the binary runs in.
515

516
        Use `false` to not inherit `sys.path`; use `fallback` to inherit `sys.path` after packaged
517
        dependencies; and use `prefer` to inherit `sys.path` before packaged dependencies.
518
        """
519
    )
520

521
    # TODO(#9388): deprecate allowing this to be a `bool`.
522
    @classmethod
11✔
523
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
11✔
524
        if isinstance(raw_value, bool):
1✔
525
            return "prefer" if raw_value else "false"
×
526
        return super().compute_value(raw_value, address)
1✔
527

528

529
class PexStripEnvField(BoolField):
11✔
530
    alias = "strip_pex_env"
11✔
531
    default = True
11✔
532
    help = help_text(
11✔
533
        """
534
        Whether or not to strip the PEX runtime environment of `PEX*` environment variables.
535

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

544

545
class PexIgnoreErrorsField(BoolField):
11✔
546
    alias = "ignore_errors"
11✔
547
    default = False
11✔
548
    help = "Should PEX ignore errors when it cannot resolve dependencies?"
11✔
549

550

551
class PexShBootField(BoolField):
11✔
552
    alias = "sh_boot"
11✔
553
    default = False
11✔
554
    help = help_text(
11✔
555
        """
556
        Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot?
557

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

573

574
class PexShebangField(StringField):
11✔
575
    alias = "shebang"
11✔
576
    help = help_text(
11✔
577
        """
578
        Set the generated PEX to use this shebang, rather than the default of PEX choosing a
579
        shebang based on the interpreter constraints.
580

581
        This influences the behavior of running `./result.pex`. You can ignore the shebang by
582
        instead running `/path/to/python_interpreter ./result.pex`.
583
        """
584
    )
585

586

587
class PexEmitWarningsField(TriBoolField):
11✔
588
    alias = "emit_warnings"
11✔
589
    help = help_text(
11✔
590
        """
591
        Whether or not to emit PEX warnings at runtime.
592

593
        The default is determined by the option `emit_warnings` in the `[pex-binary-defaults]` scope.
594
        """
595
    )
596

597
    def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> bool:
11✔
598
        if self.value is None:
×
599
            return pex_binary_defaults.emit_warnings
×
600

601
        return self.value
×
602

603

604
class PexExecutionMode(Enum):
11✔
605
    ZIPAPP = "zipapp"
11✔
606
    VENV = "venv"
11✔
607

608

609
class PexExecutionModeField(StringField):
11✔
610
    alias = "execution_mode"
11✔
611
    valid_choices = PexExecutionMode
11✔
612
    expected_type = str
11✔
613
    default = PexExecutionMode.ZIPAPP.value
11✔
614
    help = help_text(
11✔
615
        f"""
616
        The mode the generated PEX file will run in.
617

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

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

630

631
class PexLayout(Enum):
11✔
632
    ZIPAPP = "zipapp"
11✔
633
    PACKED = "packed"
11✔
634
    LOOSE = "loose"
11✔
635

636

637
class PexLayoutField(StringField):
11✔
638
    alias = "layout"
11✔
639
    valid_choices = PexLayout
11✔
640
    expected_type = str
11✔
641
    default = PexLayout.ZIPAPP.value
11✔
642
    help = help_text(
11✔
643
        f"""
644
        The layout used for the PEX binary.
645

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

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

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

660
        Both zipapp and packed layouts install themselves in the `$PEX_ROOT` as loose apps by
661
        default before executing, but these layouts compose with
662
        `{PexExecutionModeField.alias}='{PexExecutionMode.ZIPAPP.value}'` as well.
663
        """
664
    )
665

666

667
class PexIncludeRequirementsField(BoolField):
11✔
668
    alias = "include_requirements"
11✔
669
    default = True
11✔
670
    help = help_text(
11✔
671
        """
672
        Whether to include the third party requirements the binary depends on in the
673
        packaged PEX file.
674
        """
675
    )
676

677

678
class PexIncludeSourcesField(BoolField):
11✔
679
    alias = "include_sources"
11✔
680
    default = True
11✔
681
    help = help_text(
11✔
682
        """
683
        Whether to include your first party sources the binary uses in the packaged PEX file.
684
        """
685
    )
686

687

688
class PexIncludeToolsField(BoolField):
11✔
689
    alias = "include_tools"
11✔
690
    default = False
11✔
691
    help = help_text(
11✔
692
        """
693
        Whether to include Pex tools in the PEX bootstrap code.
694

695
        With tools included, the generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help`
696
        to gain access to all the available tools.
697
        """
698
    )
699

700

701
class PexVenvSitePackagesCopies(BoolField):
11✔
702
    alias = "venv_site_packages_copies"
11✔
703
    default = False
11✔
704
    help = help_text(
11✔
705
        """
706
        If execution_mode is venv, populate the venv site packages using hard links or copies of resolved PEX dependencies instead of symlinks.
707

708
        This can be used to work around problems with tools or libraries that are confused by symlinked source files.
709
        """
710
    )
711

712

713
class PexVenvHermeticScripts(BoolField):
11✔
714
    alias = "venv_hermetic_scripts"
11✔
715
    default = True
11✔
716
    help = help_text(
11✔
717
        """
718
        If execution_mode is "venv", emit a hermetic venv `pex` script and hermetic console scripts.
719

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

727

728
_PEX_BINARY_COMMON_FIELDS = (
11✔
729
    EnvironmentField,
730
    InterpreterConstraintsField,
731
    PythonResolveField,
732
    PexBinaryDependenciesField,
733
    PexCheckField,
734
    PexCompletePlatformsField,
735
    PexInheritPathField,
736
    PexStripEnvField,
737
    PexIgnoreErrorsField,
738
    PexShBootField,
739
    PexShebangField,
740
    PexEmitWarningsField,
741
    PexLayoutField,
742
    PexExecutionModeField,
743
    PexIncludeRequirementsField,
744
    PexIncludeSourcesField,
745
    PexIncludeToolsField,
746
    PexVenvSitePackagesCopies,
747
    PexVenvHermeticScripts,
748
    PexExtraBuildArgsField,
749
    RestartableField,
750
)
751

752

753
class PexBinary(Target):
11✔
754
    alias = "pex_binary"
11✔
755
    core_fields = (
11✔
756
        *COMMON_TARGET_FIELDS,
757
        *_PEX_BINARY_COMMON_FIELDS,
758
        PexEntryPointField,
759
        PexScriptField,
760
        PexExecutableField,
761
        PexArgsField,
762
        PexEnvField,
763
        OutputPathField,
764
    )
765
    help = help_text(
11✔
766
        f"""
767
        A Python target that can be converted into an executable PEX file.
768

769
        PEX files are self-contained executable files that contain a complete Python environment
770
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
771
        """
772
    )
773

774
    def validate(self) -> None:
11✔
775
        got_entry_point = self[PexEntryPointField].value is not None
1✔
776
        got_script = self[PexScriptField].value is not None
1✔
777
        got_executable = self[PexExecutableField].value is not None
1✔
778

779
        if (got_entry_point + got_script + got_executable) > 1:
1✔
780
            raise InvalidTargetException(
1✔
781
                softwrap(
782
                    f"""
783
                    The `{self.alias}` target {self.address} cannot set more than one of the
784
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
785
                    `{self[PexExecutableField].alias}` fields at the same time.
786
                    To fix, please remove all but one.
787
                    """
788
                )
789
            )
790

791

792
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
11✔
793
    alias = "entry_points"
11✔
794
    default = None
11✔
795
    help = help_text(
11✔
796
        """
797
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
798

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

803
        If you want the entry point to be for a third-party dependency or to use a console
804
        script, use the `pex_binary` target directly.
805
        """
806
    )
807

808

809
class PexBinariesOverrideField(OverridesField):
11✔
810
    help = help_text(
11✔
811
        f"""
812
        Override the field values for generated `{PexBinary.alias}` targets.
813

814
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
815
        their overrides. You may either use a single string or a tuple of strings to override
816
        multiple targets.
817

818
        For example:
819

820
            overrides={{
821
              "foo.py": {{"execution_mode": "venv"]}},
822
              "bar.py:main": {{"restartable": True]}},
823
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
824
            }}
825

826
        Every key is validated to belong to this target's `entry_points` field.
827

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

832
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
833
        same field more than one time for the `entry_point`.
834
        """
835
    )
836

837

838
class PexBinariesGeneratorTarget(TargetGenerator):
11✔
839
    alias = "pex_binaries"
11✔
840
    help = help_text(
11✔
841
        """
842
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
843

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

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

864

865
class PexBinaryDefaults(Subsystem):
11✔
866
    options_scope = "pex-binary-defaults"
11✔
867
    help = "Default settings for creating PEX executables."
11✔
868

869
    emit_warnings = BoolOption(
11✔
870
        default=True,
871
        help=softwrap(
872
            """
873
            Whether built PEX binaries should emit PEX warnings at runtime by default.
874

875
            Can be overridden by specifying the `emit_warnings` parameter of individual
876
            `pex_binary` targets
877
            """
878
        ),
879
        advanced=True,
880
    )
881

882

883
# -----------------------------------------------------------------------------------------------
884
# `python_test` and `python_tests` targets
885
# -----------------------------------------------------------------------------------------------
886

887

888
class PythonTestSourceField(PythonSourceField):
11✔
889
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
11✔
890

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

902
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
903
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
904
                    autogenerate a `python_test_utils` target.
905
                    """
906
                )
907
            )
908

909

910
class PythonTestsDependenciesField(PythonDependenciesField):
11✔
911
    supports_transitive_excludes = True
11✔
912

913

914
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
11✔
915
    alias = "entry_point_dependencies"
11✔
916
    help = help_text(
11✔
917
        lambda: f"""
918
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
919

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

927
        For example:
928

929
            {PythonTestsEntryPointDependenciesField.alias}={{
930
                "//foo/address:dist_tgt": ["*"],  # all entry points
931
                "bar:dist_tgt": ["console_scripts"],  # only from this group
932
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
933
                "another:dist_tgt": [  # multiple entry points
934
                    "console_scripts/my-script",
935
                    "console_scripts/another-script",
936
                    "entry.point.group/entry-point-name",
937
                    "other.group",
938
                    "gui_scripts",
939
                ],
940
            }}
941

942
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
943
        will be added as dependencies so that they are available on PYTHONPATH
944
        during tests.
945

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

956

957
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
958
class PythonTestsTimeoutField(IntField):
11✔
959
    alias = "timeout"
11✔
960
    help = help_text(
11✔
961
        """
962
        A timeout (in seconds) used by each test file belonging to this target.
963

964
        If unset, will default to `[test].timeout_default`; if that option is also unset,
965
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
966
        applies if the option `--test-timeouts` is set to true (the default).
967
        """
968
    )
969
    valid_numbers = ValidNumbers.positive_only
11✔
970

971
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
11✔
972
        """Determine the timeout (in seconds) after resolving conflicting global options in the
973
        `pytest` and `test` scopes.
974

975
        This function is deprecated and should be replaced by the similarly named one in
976
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
977
        """
978

979
        enabled = test.options.timeouts
×
980
        timeout_default = test.options.timeout_default
×
981
        timeout_maximum = test.options.timeout_maximum
×
982

983
        if not enabled:
×
984
            return None
×
985
        if self.value is None:
×
986
            if timeout_default is None:
×
987
                return None
×
988
            result = cast(int, timeout_default)
×
989
        else:
990
            result = self.value
×
991
        if timeout_maximum is not None:
×
992
            return min(result, cast(int, timeout_maximum))
×
993
        return result
×
994

995

996
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
11✔
997
    pass
11✔
998

999

1000
class PythonTestsXdistConcurrencyField(IntField):
11✔
1001
    alias = "xdist_concurrency"
11✔
1002
    help = help_text(
11✔
1003
        """
1004
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1005

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

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

1014
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1015
        """
1016
    )
1017

1018

1019
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
11✔
1020
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
11✔
1021

1022

1023
class SkipPythonTestsField(BoolField):
11✔
1024
    alias = "skip_tests"
11✔
1025
    default = False
11✔
1026
    help = "If true, don't run this target's tests."
11✔
1027

1028

1029
_PYTHON_TEST_MOVED_FIELDS = (
11✔
1030
    PythonTestsDependenciesField,
1031
    # This field is registered in the experimental backend for now.
1032
    # PythonTestsEntryPointDependenciesField,
1033
    PythonResolveField,
1034
    PythonRunGoalUseSandboxField,
1035
    PythonTestsTimeoutField,
1036
    PythonTestsXdistConcurrencyField,
1037
    PythonTestsBatchCompatibilityTagField,
1038
    RuntimePackageDependenciesField,
1039
    PythonTestsExtraEnvVarsField,
1040
    InterpreterConstraintsField,
1041
    SkipPythonTestsField,
1042
    EnvironmentField,
1043
)
1044

1045

1046
class PythonTestTarget(Target):
11✔
1047
    alias = "python_test"
11✔
1048
    core_fields = (
11✔
1049
        *COMMON_TARGET_FIELDS,
1050
        *_PYTHON_TEST_MOVED_FIELDS,
1051
        PythonTestsDependenciesField,
1052
        PythonTestSourceField,
1053
    )
1054
    help = help_text(
11✔
1055
        f"""
1056
        A single Python test file, written in either Pytest style or unittest style.
1057

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

1062
        See {doc_url("docs/python/goals/test")}
1063
        """
1064
    )
1065

1066

1067
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
11✔
1068
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
11✔
1069
    default = ("test_*.py", "*_test.py", "tests.py")
11✔
1070
    help = generate_multiple_sources_field_help_message(
11✔
1071
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1072
    )
1073

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

1087
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1088
                    You can run `{bin_name()} tailor` after removing the files from the
1089
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1090
                    `python_test_utils` target.
1091
                    """
1092
                )
1093
            )
1094

1095

1096
class PythonTestsOverrideField(OverridesField):
11✔
1097
    help = generate_file_based_overrides_field_help_message(
11✔
1098
        PythonTestTarget.alias,
1099
        """
1100
        overrides={
1101
            "foo_test.py": {"timeout": 120},
1102
            "bar_test.py": {"timeout": 200},
1103
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1104
        }
1105
        """,
1106
    )
1107

1108

1109
class PythonTestsGeneratorTarget(TargetFilesGenerator):
11✔
1110
    alias = "python_tests"
11✔
1111
    core_fields = (
11✔
1112
        *COMMON_TARGET_FIELDS,
1113
        PythonTestsGeneratingSourcesField,
1114
        PythonTestsOverrideField,
1115
    )
1116
    generated_target_cls = PythonTestTarget
11✔
1117
    copied_fields = COMMON_TARGET_FIELDS
11✔
1118
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
11✔
1119
    settings_request_cls = PythonFilesGeneratorSettingsRequest
11✔
1120
    help = "Generate a `python_test` target for each file in the `sources` field."
11✔
1121

1122

1123
# -----------------------------------------------------------------------------------------------
1124
# `python_source`, `python_sources`, and `python_test_utils` targets
1125
# -----------------------------------------------------------------------------------------------
1126

1127

1128
class PythonSourceTarget(Target):
11✔
1129
    alias = "python_source"
11✔
1130
    core_fields = (
11✔
1131
        *COMMON_TARGET_FIELDS,
1132
        InterpreterConstraintsField,
1133
        PythonDependenciesField,
1134
        PythonResolveField,
1135
        PythonRunGoalUseSandboxField,
1136
        PythonSourceField,
1137
        RestartableField,
1138
    )
1139
    help = "A single Python source file."
11✔
1140

1141

1142
class PythonSourcesOverridesField(OverridesField):
11✔
1143
    help = generate_file_based_overrides_field_help_message(
11✔
1144
        PythonSourceTarget.alias,
1145
        """
1146
        overrides={
1147
            "foo.py": {"skip_pylint": True]},
1148
            "bar.py": {"skip_flake8": True]},
1149
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1150
        }"
1151
        """,
1152
    )
1153

1154

1155
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
11✔
1156
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
11✔
1157
    help = generate_multiple_sources_field_help_message(
11✔
1158
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1159
    )
1160

1161

1162
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
11✔
1163
    default = (
11✔
1164
        ("*.py", "*.pyi")
1165
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1166
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1167
    )
1168
    help = generate_multiple_sources_field_help_message(
11✔
1169
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1170
    )
1171

1172

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

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

1203

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

1226
        You can either use this target generator or `python_test_utils` for test utility files
1227
        like `conftest.py`. They behave identically, but can help to better model and keep
1228
        separate test support files vs. production files.
1229
        """
1230
    )
1231

1232

1233
# -----------------------------------------------------------------------------------------------
1234
# `python_requirement` target
1235
# -----------------------------------------------------------------------------------------------
1236

1237

1238
class _PipRequirementSequenceField(Field):
11✔
1239
    value: tuple[PipRequirement, ...]
11✔
1240

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

1274

1275
class PythonRequirementDependenciesField(Dependencies):
11✔
1276
    pass
11✔
1277

1278

1279
class PythonRequirementsField(_PipRequirementSequenceField):
11✔
1280
    alias = "requirements"
11✔
1281
    required = True
11✔
1282
    help = help_text(
11✔
1283
        """
1284
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1285

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

1290
        If the requirement depends on some other requirement to work, such as needing
1291
        `setuptools` to be built, use the `dependencies` field instead.
1292
        """
1293
    )
1294

1295

1296
_default_module_mapping_url = git_url(
11✔
1297
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1298
)
1299

1300

1301
class PythonRequirementModulesField(StringSequenceField):
11✔
1302
    alias = "modules"
11✔
1303
    help = help_text(
11✔
1304
        f"""
1305
        The modules this requirement provides (used for dependency inference).
1306

1307
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1308
        "easy_install"]`.
1309

1310
        Usually you can leave this field off. If unspecified, Pants will first look at the
1311
        default module mapping ({_default_module_mapping_url}), and then will default to
1312
        the normalized project name. For example, the requirement `Django` would default to
1313
        the module `django`.
1314

1315
        Mutually exclusive with the `type_stub_modules` field.
1316
        """
1317
    )
1318

1319

1320
class PythonRequirementTypeStubModulesField(StringSequenceField):
11✔
1321
    alias = "type_stub_modules"
11✔
1322
    help = help_text(
11✔
1323
        f"""
1324
        The modules this requirement provides if the requirement is a type stub (used for
1325
        dependency inference).
1326

1327
        For example, the requirement `types-requests` provides `["requests"]`.
1328

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

1336
        Mutually exclusive with the `modules` field.
1337
        """
1338
    )
1339

1340

1341
def normalize_module_mapping(
11✔
1342
    mapping: Mapping[str, Iterable[str]] | None,
1343
) -> FrozenDict[str, tuple[str, ...]]:
1344
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
1✔
1345

1346

1347
class PythonRequirementResolveField(PythonResolveField):
11✔
1348
    alias = "resolve"
11✔
1349
    required = False
11✔
1350
    help = help_text(
11✔
1351
        """
1352
        The resolve from `[python].resolves` that this requirement is included in.
1353

1354
        If not defined, will default to `[python].default_resolve`.
1355

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

1365

1366
class PythonRequirementFindLinksField(StringSequenceField):
11✔
1367
    # NB: This is solely used for `pants_requirements` target generation
1368
    alias = "_find_links"
11✔
1369
    required = False
11✔
1370
    default = ()
11✔
1371
    help = "<Internal>"
11✔
1372

1373

1374
class PythonRequirementEntryPointField(EntryPointField):
11✔
1375
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1376
    pass
11✔
1377

1378

1379
class PythonRequirementTarget(Target):
11✔
1380
    alias = "python_requirement"
11✔
1381
    core_fields = (
11✔
1382
        *COMMON_TARGET_FIELDS,
1383
        PythonRequirementsField,
1384
        PythonRequirementDependenciesField,
1385
        PythonRequirementModulesField,
1386
        PythonRequirementTypeStubModulesField,
1387
        PythonRequirementResolveField,
1388
        PythonRequirementEntryPointField,
1389
        PythonRequirementFindLinksField,
1390
    )
1391
    help = help_text(
11✔
1392
        f"""
1393
        A Python requirement installable by pip.
1394

1395
        This target is useful when you want to declare Python requirements inline in a
1396
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1397
        the target generator `python_requirements` to convert each
1398
        requirement into a `python_requirement` target automatically. For Poetry, use
1399
        `poetry_requirements`.
1400

1401
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1402
        """
1403
    )
1404

1405
    def validate(self) -> None:
11✔
1406
        if (
4✔
1407
            self[PythonRequirementModulesField].value
1408
            and self[PythonRequirementTypeStubModulesField].value
1409
        ):
1410
            raise InvalidTargetException(
×
1411
                softwrap(
1412
                    f"""
1413
                    The `{self.alias}` target {self.address} cannot set both the
1414
                    `{self[PythonRequirementModulesField].alias}` and
1415
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1416
                    To fix, please remove one.
1417
                    """
1418
                )
1419
            )
1420

1421

1422
# -----------------------------------------------------------------------------------------------
1423
# `python_distribution` target
1424
# -----------------------------------------------------------------------------------------------
1425

1426

1427
# See `target_types_rules.py` for a dependency injection rule.
1428
class PythonDistributionDependenciesField(Dependencies):
11✔
1429
    supports_transitive_excludes = True
11✔
1430

1431

1432
class PythonProvidesField(ScalarField, AsyncFieldMixin):
11✔
1433
    alias = "provides"
11✔
1434
    expected_type = PythonArtifact
11✔
1435
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
11✔
1436
    value: PythonArtifact
11✔
1437
    required = True
11✔
1438
    help = help_text(
11✔
1439
        f"""
1440
        The setup.py kwargs for the external artifact built from this target.
1441

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

1446
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1447
        """
1448
    )
1449

1450
    @classmethod
11✔
1451
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
11✔
UNCOV
1452
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1453

1454

1455
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
11✔
1456
    alias = "entry_points"
11✔
1457
    required = False
11✔
1458
    help = help_text(
11✔
1459
        f"""
1460
        Any entry points, such as `console_scripts` and `gui_scripts`.
1461

1462
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1463
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1464
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1465
        `pex_binary` target.
1466

1467
        Example:
1468

1469
            entry_points={{
1470
              "console_scripts": {{
1471
                "my-script": "project.app:main",
1472
                "another-script": "project/subdir:pex_binary_tgt"
1473
              }}
1474
            }}
1475

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

1482
        Pants will attempt to infer dependencies, which you can confirm by running:
1483

1484
            {bin_name()} dependencies <python_distribution target address>
1485
        """
1486
    )
1487

1488

1489
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
11✔
1490
    help = help_text(
11✔
1491
        """
1492
        The path to the directory to write the distribution file to, relative the dist directory.
1493

1494
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1495
        level of the dist dir.
1496
        """
1497
    )
1498
    alias = "output_path"
11✔
1499
    default = ""
11✔
1500

1501

1502
@dataclass(frozen=True)
11✔
1503
class PythonDistributionEntryPoint:
11✔
1504
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1505

1506
    entry_point: EntryPoint
11✔
1507
    pex_binary_address: Address | None
11✔
1508

1509

1510
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1511
@dataclass(frozen=True)
11✔
1512
class ResolvedPythonDistributionEntryPoints:
11✔
1513
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1514
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
11✔
1515

1516
    @property
11✔
1517
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
11✔
1518
        """Filters out all entry points from pex binary targets."""
1519
        return FrozenDict(
×
1520
            {
1521
                category: FrozenDict(
1522
                    {
1523
                        ep_name: ep_val.entry_point
1524
                        for ep_name, ep_val in entry_points.items()
1525
                        if not ep_val.pex_binary_address
1526
                    }
1527
                )
1528
                for category, entry_points in self.val.items()
1529
            }
1530
        )
1531

1532
    @property
11✔
1533
    def pex_binary_addresses(self) -> Addresses:
11✔
1534
        """Returns the addresses to all pex binary targets owning entry points used."""
1535
        return Addresses(
×
1536
            ep_val.pex_binary_address
1537
            for category, entry_points in self.val.items()
1538
            for ep_val in entry_points.values()
1539
            if ep_val.pex_binary_address
1540
        )
1541

1542

1543
@dataclass(frozen=True)
11✔
1544
class ResolvePythonDistributionEntryPointsRequest:
11✔
1545
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1546
    that should be resolved into a setuptools entry point.
1547

1548
    If the `entry_points_field` is present, inspect the specified entry points.
1549
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1550

1551
    This is to support inspecting one or the other depending on use case, using the same
1552
    logic for resolving pex_binary addresses etc.
1553
    """
1554

1555
    entry_points_field: PythonDistributionEntryPointsField | None = None
11✔
1556
    provides_field: PythonProvidesField | None = None
11✔
1557

1558
    def __post_init__(self):
11✔
1559
        # Must provide at least one of these fields.
1560
        assert self.entry_points_field or self.provides_field
1✔
1561

1562

1563
class WheelField(BoolField):
11✔
1564
    alias = "wheel"
11✔
1565
    default = True
11✔
1566
    help = "Whether to build a wheel for the distribution."
11✔
1567

1568

1569
class SDistField(BoolField):
11✔
1570
    alias = "sdist"
11✔
1571
    default = True
11✔
1572
    help = "Whether to build an sdist for the distribution."
11✔
1573

1574

1575
class ConfigSettingsField(DictStringToStringSequenceField):
11✔
1576
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1577

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

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

1583
    - Build frontends should support string values, and may also support other mechanisms
1584
      (apparently meaning other types).
1585

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

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

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

1601

1602
class WheelConfigSettingsField(ConfigSettingsField):
11✔
1603
    alias = "wheel_config_settings"
11✔
1604
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
11✔
1605

1606

1607
class SDistConfigSettingsField(ConfigSettingsField):
11✔
1608
    alias = "sdist_config_settings"
11✔
1609
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
11✔
1610

1611

1612
class BuildBackendEnvVarsField(StringSequenceField):
11✔
1613
    alias = "env_vars"
11✔
1614
    required = False
11✔
1615
    help = help_text(
11✔
1616
        """
1617
        Environment variables to set when running the PEP-517 build backend.
1618

1619
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1620
        or just `ENV_VAR` to copy the value from Pants's own environment.
1621
        """
1622
    )
1623

1624

1625
class GenerateSetupField(TriBoolField):
11✔
1626
    alias = "generate_setup"
11✔
1627
    required = False
11✔
1628
    # The default behavior if this field is unspecified is controlled by the
1629
    # --generate-setup-default option in the setup-py-generation scope.
1630
    default = None
11✔
1631

1632
    help = help_text(
11✔
1633
        """
1634
        Whether to generate setup information for this distribution, based on analyzing
1635
        sources and dependencies. Set to False to use existing setup information, such as
1636
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1637
        """
1638
    )
1639

1640

1641
class LongDescriptionPathField(StringField):
11✔
1642
    alias = "long_description_path"
11✔
1643
    required = False
11✔
1644

1645
    help = help_text(
11✔
1646
        """
1647
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1648

1649
        Path is relative to the build root.
1650

1651
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1652

1653
        This field won't automatically set `long_description_content_type` field for you.
1654
        You have to specify this field yourself in the `provides` field.
1655
        """
1656
    )
1657

1658

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

1680
        See {doc_url("docs/python/overview/building-distributions")}.
1681
        """
1682
    )
1683

1684

1685
# -----------------------------------------------------------------------------------------------
1686
# `vcs_version` target
1687
# -----------------------------------------------------------------------------------------------
1688

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

1696

1697
class VCSVersionDummySourceField(OptionalSingleSourceField):
11✔
1698
    """A dummy SourceField for participation in the codegen machinery."""
1699

1700
    alias = "_dummy_source"  # Leading underscore omits the field from help.
11✔
1701
    help = "A version string generated from VCS information"
11✔
1702

1703

1704
class VersionTagRegexField(StringField):
11✔
1705
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
11✔
1706
    alias = "tag_regex"
11✔
1707
    help = help_text(
11✔
1708
        """
1709
        A Python regex string to extract the version string from a VCS tag.
1710

1711
        The regex needs to contain either a single match group, or a group named version,
1712
        that captures the actual version information.
1713

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

1716
        See https://github.com/pypa/setuptools_scm for implementation details.
1717
        """
1718
    )
1719

1720

1721
class VersionGenerateToField(StringField):
11✔
1722
    required = True
11✔
1723
    alias = "generate_to"
11✔
1724
    help = help_text(
11✔
1725
        """
1726
        Generate the version data to this relative path, using the template field.
1727

1728
        Note that the generated output will not be written to disk in the source tree, but
1729
        will be available as a generated dependency to code that depends on this target.
1730
        """
1731
    )
1732

1733

1734
class VersionTemplateField(StringField):
11✔
1735
    required = True
11✔
1736
    alias = "template"
11✔
1737
    help = help_text(
11✔
1738
        """
1739
        Generate the version data using this format string, which takes a version format kwarg.
1740

1741
        E.g., `'version = "{version}"'`
1742
        """
1743
    )
1744

1745

1746
class VersionVersionSchemeField(StringField):
11✔
1747
    alias = "version_scheme"
11✔
1748
    help = help_text(
11✔
1749
        """
1750
        The version scheme to configure `setuptools_scm` to use.
1751
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
1752
        """
1753
    )
1754

1755

1756
class VersionLocalSchemeField(StringField):
11✔
1757
    alias = "local_scheme"
11✔
1758
    help = help_text(
11✔
1759
        """
1760
        The local scheme to configure `setuptools_scm` to use.
1761
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
1762
        """
1763
    )
1764

1765

1766
class VCSVersion(Target):
11✔
1767
    alias = "vcs_version"
11✔
1768
    core_fields = (
11✔
1769
        *COMMON_TARGET_FIELDS,
1770
        VersionTagRegexField,
1771
        VersionVersionSchemeField,
1772
        VersionLocalSchemeField,
1773
        VCSVersionDummySourceField,
1774
        VersionGenerateToField,
1775
        VersionTemplateField,
1776
    )
1777
    help = help_text(
11✔
1778
        f"""
1779
        Generates a version string from VCS state.
1780

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

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

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