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

pantsbuild / pants / 22740642519

05 Mar 2026 11:00PM UTC coverage: 52.677% (-40.3%) from 92.931%
22740642519

Pull #23157

github

web-flow
Merge 2aa18e6d4 into f0030f5e7
Pull Request #23157: [pants ng] Partition source files by config.

31678 of 60136 relevant lines covered (52.68%)

0.53 hits per line

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

87.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
1✔
5

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

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

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

72
logger = logging.getLogger(__name__)
1✔
73

74
if TYPE_CHECKING:
75
    from pants.backend.python.subsystems.pytest import PyTest
76

77

78
# -----------------------------------------------------------------------------------------------
79
# Common fields
80
# -----------------------------------------------------------------------------------------------
81

82

83
class PythonSourceField(SingleSourceField):
1✔
84
    # Note that Python scripts often have no file ending.
85
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")
1✔
86

87

88
class PythonDependenciesField(Dependencies):
1✔
89
    pass
1✔
90

91

92
class PythonGeneratingSourcesBase(MultipleSourcesField):
1✔
93
    expected_file_extensions: ClassVar[tuple[str, ...]] = ("", ".py", ".pyi")
1✔
94

95

96
class InterpreterConstraintsField(StringSequenceField, AsyncFieldMixin):
1✔
97
    alias = "interpreter_constraints"
1✔
98
    help = help_text(
1✔
99
        f"""
100
        The Python interpreters this code is compatible with.
101

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

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

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

111
        See {doc_url("docs/python/overview/interpreter-compatibility")} for how these interpreter
112
        constraints are merged with the constraints of dependencies.
113
        """
114
    )
115

116
    def value_or_configured_default(
1✔
117
        self, python_setup: PythonSetup, resolve: PythonResolveField | None
118
    ) -> tuple[str, ...]:
119
        """Return either the given `compatibility` field or the global interpreter constraints.
120

121
        If interpreter constraints are supplied by the CLI flag, return those only.
122
        """
123
        if self.value and python_setup.warn_on_python2_usage:
1✔
124
            # Side-step import cycle.
125
            from pants.backend.python.util_rules.interpreter_constraints import (
1✔
126
                warn_on_python2_usage_in_interpreter_constraints,
127
            )
128

129
            warn_on_python2_usage_in_interpreter_constraints(
1✔
130
                self.value,
131
                description_of_origin=f"the `{self.alias}` field on target at `{self.address}`",
132
            )
133
        return python_setup.compatibility_or_constraints(
1✔
134
            self.value,
135
            resolve.normalized_value(python_setup)
136
            if resolve and python_setup.enable_resolves
137
            else None,
138
        )
139

140

141
class PythonResolveLikeFieldToValueRequest(ResolveLikeFieldToValueRequest):
1✔
142
    pass
1✔
143

144

145
class PythonResolveField(StringField, AsyncFieldMixin, ResolveLikeField):
1✔
146
    alias = "resolve"
1✔
147
    required = False
1✔
148
    help = help_text(
1✔
149
        """
150
        The resolve from `[python].resolves` to use.
151

152
        If not defined, will default to `[python].default_resolve`.
153

154
        All dependencies must share the same value for their `resolve` field.
155
        """
156
    )
157

158
    def normalized_value(self, python_setup: PythonSetup) -> str:
1✔
159
        """Get the value after applying the default and validating that the key is recognized."""
160
        if not python_setup.enable_resolves:
1✔
161
            return "<ignore>"
1✔
162
        resolve = self.value or python_setup.default_resolve
×
163
        if resolve not in python_setup.resolves:
×
164
            raise UnrecognizedResolveNamesError(
×
165
                [resolve],
166
                python_setup.resolves.keys(),
167
                description_of_origin=f"the field `{self.alias}` in the target {self.address}",
168
            )
169
        return resolve
×
170

171
    def get_resolve_like_field_to_value_request(self) -> type[ResolveLikeFieldToValueRequest]:
1✔
172
        return PythonResolveLikeFieldToValueRequest
1✔
173

174

175
class PrefixedPythonResolveField(PythonResolveField):
1✔
176
    alias = "python_resolve"
1✔
177

178

179
class PythonRunGoalUseSandboxField(TriBoolField):
1✔
180
    alias = "run_goal_use_sandbox"
1✔
181
    help = help_text(
1✔
182
        """
183
        Whether to use a sandbox when `run`ning this target. Defaults to
184
        `[python].default_run_goal_use_sandbox`.
185

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

189
        If false, runs of this target with the `run` goal will use the in-repo sources
190
        directly.
191

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

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

200
        The latter mode is similar to creating, activating, and using a virtual environment when
201
        running your files. It may also be necessary if the source being run writes files into the
202
        repo and computes their location relative to the executed files. Django's `makemigrations`
203
        command is an example of such a process.
204
        """
205
    )
206

207

208
# -----------------------------------------------------------------------------------------------
209
# Target generation support
210
# -----------------------------------------------------------------------------------------------
211

212

213
class PythonFilesGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
1✔
214
    pass
1✔
215

216

217
# -----------------------------------------------------------------------------------------------
218
# `pex_binary` and `pex_binaries` target
219
# -----------------------------------------------------------------------------------------------
220

221

222
# See `target_types_rules.py` for a dependency injection rule.
223
class PexBinaryDependenciesField(Dependencies):
1✔
224
    supports_transitive_excludes = True
1✔
225

226

227
class MainSpecification(ABC):
1✔
228
    @abstractmethod
229
    def iter_pex_args(self) -> Iterator[str]: ...
230

231
    @property
232
    @abstractmethod
233
    def spec(self) -> str: ...
234

235

236
@dataclass(frozen=True)
1✔
237
class EntryPoint(MainSpecification):
1✔
238
    module: str
1✔
239
    function: str | None = None
1✔
240

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

279
    def __post_init__(self):
1✔
280
        if ":" in self.module:
1✔
281
            raise ValueError(
×
282
                softwrap(
283
                    f"""
284
                    The `:` character is not valid in a module name. Given an entry point module of
285
                    {self.module}. Did you mean to use EntryPoint.parse?
286
                    """
287
                )
288
            )
289
        if self.function and ":" in self.function:
1✔
290
            raise ValueError(
×
291
                softwrap(
292
                    f"""
293
                    The `:` character is not valid in a function name. Given an entry point function
294
                    of {self.function}.
295
                    """
296
                )
297
            )
298

299
    def iter_pex_args(self) -> Iterator[str]:
1✔
300
        yield "--entry-point"
1✔
301
        yield self.spec
1✔
302

303
    @property
1✔
304
    def spec(self) -> str:
1✔
305
        return self.module if self.function is None else f"{self.module}:{self.function}"
1✔
306

307

308
@dataclass(frozen=True)
1✔
309
class ConsoleScript(MainSpecification):
1✔
310
    name: str
1✔
311

312
    def iter_pex_args(self) -> Iterator[str]:
1✔
313
        yield "--console-script"
1✔
314
        yield self.name
1✔
315

316
    @property
1✔
317
    def spec(self) -> str:
1✔
318
        return self.name
1✔
319

320

321
@dataclass(frozen=True)
1✔
322
class Executable(MainSpecification):
1✔
323
    executable: str
1✔
324

325
    @classmethod
1✔
326
    def create(cls, address: Address, filename: str) -> Executable:
1✔
327
        # spec_path is relative to the workspace. The rule is responsible for
328
        # stripping the source root as needed.
329
        return cls(os.path.join(address.spec_path, filename).lstrip(os.path.sep))
×
330

331
    def iter_pex_args(self) -> Iterator[str]:
1✔
332
        yield "--executable"
×
333
        # We do NOT yield self.executable or self.spec
334
        # as the path needs additional processing in the rule graph.
335
        # see: build_pex in util_rules/pex
336

337
    @property
1✔
338
    def spec(self) -> str:
1✔
339
        return self.executable
×
340

341

342
class EntryPointField(AsyncFieldMixin, Field):
1✔
343
    alias = "entry_point"
1✔
344
    default = None
1✔
345
    help = help_text(
1✔
346
        """
347
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.
348

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

352
          1) `'app.py'`, Pants will convert into the module `path.to.app`;
353
          2) `'app.py:func'`, Pants will convert into `path.to.app:func`.
354

355
        You may only set one of: this field, or the `script` field, or the `executable` field.
356
        Leave off all three fields to have no entry point.
357
        """
358
    )
359
    value: EntryPoint | None
1✔
360

361
    @classmethod
1✔
362
    def compute_value(cls, raw_value: str | None, address: Address) -> EntryPoint | None:
1✔
363
        value = super().compute_value(raw_value, address)
1✔
364
        if value is None:
1✔
365
            return None
×
366
        if not isinstance(value, str):
1✔
367
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
368
        try:
1✔
369
            return EntryPoint.parse(value, provenance=f"for {address}")
1✔
370
        except ValueError as e:
×
371
            raise InvalidFieldException(str(e))
×
372

373

374
class PexEntryPointField(EntryPointField):
1✔
375
    # Specialist subclass for use with `PexBinary` targets.
376
    pass
1✔
377

378

379
# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
380
@dataclass(frozen=True)
1✔
381
class ResolvedPexEntryPoint:
1✔
382
    val: EntryPoint | None
1✔
383
    file_name_used: bool
1✔
384

385

386
@dataclass(frozen=True)
1✔
387
class ResolvePexEntryPointRequest:
1✔
388
    """Determine the `entry_point` for a `pex_binary` after applying all syntactic sugar."""
389

390
    entry_point_field: EntryPointField
1✔
391

392

393
class PexScriptField(Field):
1✔
394
    alias = "script"
1✔
395
    default = None
1✔
396
    help = help_text(
1✔
397
        """
398
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a script or
399
        console_script as defined by any of the distributions in the PEX.
400

401
        You may only set one of: this field, or the `entry_point` field, or the `executable` field.
402
        Leave off all three fields to have no entry point.
403
        """
404
    )
405
    value: ConsoleScript | None
1✔
406

407
    @classmethod
1✔
408
    def compute_value(cls, raw_value: str | None, address: Address) -> ConsoleScript | None:
1✔
409
        value = super().compute_value(raw_value, address)
×
410
        if value is None:
×
411
            return None
×
412
        if not isinstance(value, str):
×
413
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
414
        return ConsoleScript(value)
×
415

416

417
class PexExecutableField(Field):
1✔
418
    alias = "executable"
1✔
419
    default = None
1✔
420
    help = help_text(
1✔
421
        """
422
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to an execuatble
423
        local python script. This executable python script is typically something that cannot
424
        be imported so it cannot be used via `script` or `entry_point`.
425

426
        You may only set one of: this field, or the `entry_point` field, or the `script` field.
427
        Leave off all three fields to have no entry point.
428
        """
429
    )
430
    value: Executable | None
1✔
431

432
    @classmethod
1✔
433
    def compute_value(cls, raw_value: str | None, address: Address) -> Executable | None:
1✔
434
        value = super().compute_value(raw_value, address)
×
435
        if value is None:
×
436
            return None
×
437
        if not isinstance(value, str):
×
438
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
439
        return Executable.create(address, value)
×
440

441

442
class PexArgsField(StringSequenceField):
1✔
443
    alias: ClassVar[str] = "args"
1✔
444
    help = help_text(
1✔
445
        lambda: f"""
446
        Freeze these command-line args into the PEX. Allows you to run generic entry points
447
        on specific arguments without creating a shim file.
448

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

456

457
class PexExtraBuildArgsField(StringSequenceField):
1✔
458
    alias: ClassVar[str] = "extra_build_args"
1✔
459
    default = ()
1✔
460
    help = help_text(
1✔
461
        lambda: f"""
462
        Extra arguments to pass to the `pex` invocation used to build this PEX. These are
463
        passed after all other arguments. This can be used to pass extra options that
464
        Pants doesn't have built-in support for.
465

466
        This is different to `{PexArgsField.alias}`: `{PexArgsField.alias}` records
467
        arguments used by the packaged PEX when executed, `{PexExtraBuildArgsField.alias}`
468
        passes arguments to the process that does the packaging.
469
        """
470
    )
471

472

473
class PexCheckField(StringField):
1✔
474
    alias = "check"
1✔
475
    valid_choices = ("none", "warn", "error")
1✔
476
    expected_type = str
1✔
477
    default = "warn"
1✔
478
    help = help_text(
1✔
479
        """
480
        Check that the built PEX is valid. Currently this only
481
        applies to `--layout zipapp` where the PEX zip is
482
        tested for importability of its `__main__` module by
483
        the Python zipimport module. This check will fail for
484
        PEX zips that use ZIP64 extensions since the Python
485
        zipimport zipimporter only works with 32 bit zips. The
486
        check no-ops for all other layouts.
487
        """
488
    )
489

490

491
class PexEnvField(DictStringToStringField):
1✔
492
    alias = "env"
1✔
493
    help = help_text(
1✔
494
        """
495
        Freeze these environment variables into the PEX. Allows you to run generic entry points
496
        on a specific environment without creating a shim file.
497
        """
498
    )
499

500

501
class PexCompletePlatformsField(SpecialCasedDependencies):
1✔
502
    alias = "complete_platforms"
1✔
503
    help = help_text(
1✔
504
        f"""
505
        The platforms the built PEX should be compatible with.
506

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

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

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

516
        See {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")} for details on how to create this file.
517
        """
518
    )
519

520

521
class PexCompressField(BoolField):
1✔
522
    alias = "compress"
1✔
523
    default = True
1✔
524
    help = help_text(
1✔
525
        """
526
        Whether to compress zip entries when creating either a
527
        zipapp PEX file or a packed PEX's bootstrap and
528
        dependency zip files. Does nothing for loose layout
529
        PEXes.
530
        """
531
    )
532

533

534
class PexInheritPathField(StringField):
1✔
535
    alias = "inherit_path"
1✔
536
    valid_choices = ("false", "fallback", "prefer")
1✔
537
    help = help_text(
1✔
538
        """
539
        Whether to inherit the `sys.path` (aka PYTHONPATH) of the environment that the binary runs in.
540

541
        Use `false` to not inherit `sys.path`; use `fallback` to inherit `sys.path` after packaged
542
        dependencies; and use `prefer` to inherit `sys.path` before packaged dependencies.
543
        """
544
    )
545

546
    # TODO(#9388): deprecate allowing this to be a `bool`.
547
    @classmethod
1✔
548
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
1✔
549
        if isinstance(raw_value, bool):
×
550
            return "prefer" if raw_value else "false"
×
551
        return super().compute_value(raw_value, address)
×
552

553

554
class PexStripEnvField(BoolField):
1✔
555
    alias = "strip_pex_env"
1✔
556
    default = True
1✔
557
    help = help_text(
1✔
558
        """
559
        Whether or not to strip the PEX runtime environment of `PEX*` environment variables.
560

561
        Most applications have no need for the `PEX*` environment variables that are used to
562
        control PEX startup; so these variables are scrubbed from the environment by Pex before
563
        transferring control to the application by default. This prevents any subprocesses that
564
        happen to execute other PEX files from inheriting these control knob values since most
565
        would be undesired; e.g.: PEX_MODULE or PEX_PATH.
566
        """
567
    )
568

569

570
class PexIgnoreErrorsField(BoolField):
1✔
571
    alias = "ignore_errors"
1✔
572
    default = False
1✔
573
    help = "Should PEX ignore errors when it cannot resolve dependencies?"
1✔
574

575

576
class PexShBootField(BoolField):
1✔
577
    alias = "sh_boot"
1✔
578
    default = False
1✔
579
    help = help_text(
1✔
580
        """
581
        Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot?
582

583
        If you know the machines that the PEX will be distributed to have
584
        POSIX compliant `/bin/sh` (almost all do, see:
585
        https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html);
586
        then this is probably the way you want your PEX to boot. Instead of
587
        launching via a Python shebang, the PEX will launch via a `#!/bin/sh`
588
        shebang that executes a small script embedded in the head of the PEX
589
        ZIPAPP that performs initial interpreter selection and re-execution of
590
        the underlying PEX in a way that is often more robust than a Python
591
        shebang and always faster on 2nd and subsequent runs since the sh
592
        script has a constant overhead of O(1ms) whereas the Python overhead
593
        to perform the same interpreter selection and re-execution is
594
        O(100ms).
595
        """
596
    )
597

598

599
class PexShebangField(StringField):
1✔
600
    alias = "shebang"
1✔
601
    help = help_text(
1✔
602
        """
603
        Set the generated PEX to use this shebang, rather than the default of PEX choosing a
604
        shebang based on the interpreter constraints.
605

606
        This influences the behavior of running `./result.pex`. You can ignore the shebang by
607
        instead running `/path/to/python_interpreter ./result.pex`.
608
        """
609
    )
610

611

612
class PexEmitWarningsField(TriBoolField):
1✔
613
    alias = "emit_warnings"
1✔
614
    help = help_text(
1✔
615
        """
616
        Whether or not to emit PEX warnings at runtime.
617

618
        The default is determined by the option `emit_warnings` in the `[pex-binary-defaults]` scope.
619
        """
620
    )
621

622
    def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> bool:
1✔
623
        if self.value is None:
×
624
            return pex_binary_defaults.emit_warnings
×
625

626
        return self.value
×
627

628

629
class PexExecutionMode(Enum):
1✔
630
    ZIPAPP = "zipapp"
1✔
631
    VENV = "venv"
1✔
632

633

634
class PexExecutionModeField(StringField):
1✔
635
    alias = "execution_mode"
1✔
636
    valid_choices = PexExecutionMode
1✔
637
    expected_type = str
1✔
638
    default = PexExecutionMode.ZIPAPP.value
1✔
639
    help = help_text(
1✔
640
        f"""
641
        The mode the generated PEX file will run in.
642

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

648
        The fastest execution mode in the steady state is {PexExecutionMode.VENV.value!r}, which
649
        generates a virtual environment from the PEX file on first run, but then achieves near
650
        native virtual environment start times. This mode also benefits from a traditional virtual
651
        environment `sys.path`, giving maximum compatibility with stdlib and third party APIs.
652
        """
653
    )
654

655

656
class PexLayout(Enum):
1✔
657
    ZIPAPP = "zipapp"
1✔
658
    PACKED = "packed"
1✔
659
    LOOSE = "loose"
1✔
660

661

662
class PexLayoutField(StringField):
1✔
663
    alias = "layout"
1✔
664
    valid_choices = PexLayout
1✔
665
    expected_type = str
1✔
666
    default = PexLayout.ZIPAPP.value
1✔
667
    help = help_text(
1✔
668
        f"""
669
        The layout used for the PEX binary.
670

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

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

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

685
        Both zipapp and packed layouts install themselves in the `$PEX_ROOT` as loose apps by
686
        default before executing, but these layouts compose with
687
        `{PexExecutionModeField.alias}='{PexExecutionMode.ZIPAPP.value}'` as well.
688
        """
689
    )
690

691

692
class PexIncludeRequirementsField(BoolField):
1✔
693
    alias = "include_requirements"
1✔
694
    default = True
1✔
695
    help = help_text(
1✔
696
        """
697
        Whether to include the third party requirements the binary depends on in the
698
        packaged PEX file.
699
        """
700
    )
701

702

703
class PexIncludeSourcesField(BoolField):
1✔
704
    alias = "include_sources"
1✔
705
    default = True
1✔
706
    help = help_text(
1✔
707
        """
708
        Whether to include your first party sources the binary uses in the packaged PEX file.
709
        """
710
    )
711

712

713
class PexIncludeToolsField(BoolField):
1✔
714
    alias = "include_tools"
1✔
715
    default = False
1✔
716
    help = help_text(
1✔
717
        """
718
        Whether to include Pex tools in the PEX bootstrap code.
719

720
        With tools included, the generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help`
721
        to gain access to all the available tools.
722
        """
723
    )
724

725

726
class PexVenvSitePackagesCopies(BoolField):
1✔
727
    alias = "venv_site_packages_copies"
1✔
728
    default = False
1✔
729
    help = help_text(
1✔
730
        """
731
        If execution_mode is venv, populate the venv site packages using hard links or copies of resolved PEX dependencies instead of symlinks.
732

733
        This can be used to work around problems with tools or libraries that are confused by symlinked source files.
734
        """
735
    )
736

737

738
class PexVenvHermeticScripts(BoolField):
1✔
739
    alias = "venv_hermetic_scripts"
1✔
740
    default = True
1✔
741
    help = help_text(
1✔
742
        """
743
        If execution_mode is "venv", emit a hermetic venv `pex` script and hermetic console scripts.
744

745
        The venv `pex` script and the venv console scripts are constructed to be hermetic by
746
        default; Python is executed with `-sE` to restrict the `sys.path` to the PEX venv contents
747
        only. Setting this field to `False` elides the Python `-sE` restrictions and can be used to
748
        interoperate with frameworks that use `PYTHONPATH` manipulation to run code.
749
        """
750
    )
751

752

753
class PexScieField(StringField):
1✔
754
    alias = "scie"
1✔
755
    valid_choices = ("lazy", "eager")
1✔
756
    default = None
1✔
757
    help = help_text(
1✔
758
        """
759
        Create one or more native executable scies from your PEX that include
760
        a portable CPython interpreter along with your PEX making for a truly
761
        hermetic PEX that can run on machines with no Python installed at
762
        all. If your PEX has multiple targets then one PEX scie will be made
763
        for each platform, selecting the latest compatible portable CPython or
764
        PyPy interpreter as appropriate. Note that only Python>=3.8 is
765
        supported. If you'd like to explicitly control the target platforms or
766
        the exact portable CPython selected, see `scie_platform`,
767
        `scie_pbs_release` and `scie_python_version`.  Specifying `lazy` will
768
        fetch the portable CPython interpreter just in time on first boot of
769
        the PEX scie on a given machine if needed. Specifying `eager` will
770
        embed the portable CPython interpreter in your PEX scie making for a
771
        larger file, but requiring no internet access to boot. See
772
        https://science.scie.app for further details.
773

774
        This field must be set for any other `scie_*` fields to take effect.
775

776
        NOTE: `pants run` will always run the "regular" PEX, use `package` to
777
        create scie PEXs.  """
778
    )
779

780

781
class ScieNameStyle(StrEnum):
1✔
782
    DYNAMIC = "dynamic"
1✔
783
    PLATFORM_PARENT_DIR = "platform-parent-dir"
1✔
784
    PLATFORM_FILE_SUFFIX = "platform-file-suffix"
1✔
785

786

787
class PexScieBindResourcePathField(StringSequenceField):
1✔
788
    alias = "scie_bind_resource_path"
1✔
789
    default = None
1✔
790
    help = help_text(
1✔
791
        """ Specifies an environment variable to bind the path of a resource
792
        in the PEX to in the form `<env var name>=<resource rel path>`. For
793
        example `WINDOWS_X64_CONSOLE_TRAMPOLINE=pex/windows/stubs/uv-
794
        trampoline-x86_64-console.exe` would lookup the path of the
795
        `pex/windows/stubs/uv-trampoline-x86_64-console.exe` file on the
796
        `sys.path` and bind its absolute path to the
797
        WINDOWS_X64_CONSOLE_TRAMPOLINE environment variable.  N.B.: resource
798
        paths must use the Unix path separator of `/`. These will be converted
799
        to the runtime host path separator as needed.
800
        """
801
    )
802

803

804
class PexScieExeField(StringField):
1✔
805
    alias = "scie_exe"
1✔
806
    default = None
1✔
807
    help = help_text(
1✔
808
        """
809
        Specify a custom PEX scie entry point instead of using
810
        the PEX's entrypoint. When specifying a custom entry
811
        point additional args can be set via `scie_args` and
812
        environment variables can be set via `scie_env`.
813
        Scie placeholders can be used in `scie_exe`.
814
        """
815
    )
816

817

818
class PexScieArgsField(StringSequenceField):
1✔
819
    alias = "scie_args"
1✔
820
    default = None
1✔
821
    help = help_text(
1✔
822
        """ Additional arguments to pass to the custom `scie_exe` entry
823
        point. Scie placeholders can be used in `scie_args`,
824
        """
825
    )
826

827

828
class PexScieEnvField(StringSequenceField):
1✔
829
    alias = "scie_env"
1✔
830
    default = None
1✔
831
    help = help_text(
1✔
832
        """
833
        Environment variables to set when executing the custom
834
        `scie_exe` entry point. Scie placeholders can be
835
        used in `scie_env`.
836
        """
837
    )
838

839

840
class PexScieLoadDotenvField(TriBoolField):
1✔
841
    alias = "scie_load_dotenv"
1✔
842
    required = False
1✔
843
    default = None
1✔
844
    help = help_text(
1✔
845
        """ Have the scie launcher load `.env` files and apply the loaded env
846
        vars to the PEX scie environment. See the 'load_dotenv' docs here for
847
        more on the `.env` loading specifics: https://github.com/a-
848
        scie/jump/blob/main/docs/packaging.md#optional-fields (Pex default:
849
        False) """
850
    )
851

852

853
class PexScieNameStyleField(StringField):
1✔
854
    alias = "scie_name_style"
1✔
855
    valid_choices = ScieNameStyle
1✔
856
    expected_type = str
1✔
857
    default = ScieNameStyle.DYNAMIC
1✔
858
    help = help_text(
1✔
859
        """
860
        Control how the output file translates to a scie name. By default
861
        (`dynamic`), the platform is used as a file suffix only when needed
862
        for disambiguation when targeting a local platform.  Specifying
863
        `platform-file-suffix` forces the scie target platform name to be
864
        added as a suffix of the output filename; Specifying
865
        `platform-parent-dir` places the scie in a sub- directory with the
866
        name of the platform it targets."""
867
    )
868

869

870
class PexScieBusyBox(StringField):
1✔
871
    alias = "scie_busybox"
1✔
872
    default = None
1✔
873
    help = help_text(
1✔
874
        """
875
        Make the PEX scie a BusyBox over the specified entry points. The entry
876
        points can either be console scripts or entry point specifiers. To
877
        select all console scripts in all distributions contained in the PEX,
878
        use `@`. To just pick all the console scripts from a particular
879
        project name's distributions in the PEX, use `@<project name>`; e.g.:
880
        `@ansible-core`. To exclude all the console scripts from a project,
881
        prefix with a `!`; e.g.: `@,!@ansible-core` selects all console
882
        scripts except those provided by the `ansible- core` project. To
883
        select an individual console script, just use its name or prefix the
884
        name with `!` to exclude that individual console script. To specify an
885
        arbitrary entry point in a module contained within one of the
886
        distributions in the PEX, use a string of the form
887
        `<name>=<module>(:<function>)`; e.g.: 'run- baz=foo.bar:baz' to
888
        execute the `baz` function in the `foo.bar` module as the entry point
889
        named `run-baz`.
890

891
        A BusyBox scie has no default entrypoint; instead, when run, it
892
        inspects argv0; if that matches one of its embedded entry points, it
893
        runs that entry point; if not, it lists all available entrypoints for
894
        you to pick from. To run a given entry point, you specify it as the
895
        first argument and all other arguments after that are forwarded to
896
        that entry point. BusyBox PEX scies allow you to install all their
897
        contained entry points into a given directory.  For more information,
898
        run `SCIE=help <your PEX scie>` and review the `install` command help.
899

900
        NOTE: This is only available for formal Python entry points
901
        <https://packaging.python.org/en/latest/specifications/entry-points/>
902
        and not the informal use by the `pex_binary` field `entry_point` to
903
        run first party files.
904
        """
905
    )
906

907

908
class PexSciePexEntrypointEnvPassthrough(TriBoolField):
1✔
909
    alias = "scie_pex_entrypoint_env_passthrough"
1✔
910
    required = False
1✔
911
    default = None
1✔
912
    help = help_text(
1✔
913
        """
914
        Allow overriding the primary entrypoint at runtime via
915
        PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
916
        when using --venv with a script entrypoint this adds
917
        modest startup overhead on the order of 10ms. Defaults
918
        to false for busybox scies and true for single
919
        entrypoint scies.
920
        """
921
    )
922

923

924
class PexSciePlatformField(StringSequenceField):
1✔
925
    alias = "scie_platform"
1✔
926
    valid_choices = (
1✔
927
        "current",
928
        "linux-aarch64",
929
        "linux-armv7l",
930
        "linux-powerpc64",
931
        "linux-riscv64",
932
        "linux-s390x",
933
        "linux-x86_64",
934
        "macos-aarch64",
935
        "macos-x86_64",
936
    )
937
    expected_type = str
1✔
938
    help = help_text(
1✔
939
        """ The platform to produce the native PEX scie executable for.  You
940
        can use a value of `current` to select the current platform. If left
941
        unspecified, the platforms implied by the targets selected to build
942
        the PEX with are used. Those targets are influenced by the current
943
        interpreter running Pex as well as use of `complete_platforms` and
944
        `interpreter_constraints`. Note that, in general, `scie_platform`
945
        should only be used to select a subset of the platforms implied by the
946
        targets selected via other options.  """
947
    )
948

949

950
class PexSciePbsReleaseField(StringField):
1✔
951
    alias = "scie_pbs_release"
1✔
952
    default = None
1✔
953
    help = help_text(
1✔
954
        """ The Python Standalone Builds release to use when a CPython
955
        interpreter distribution is needed for the PEX scie. Currently,
956
        releases are dates of the form YYYYMMDD, e.g.: '20240713'. See their
957
        GitHub releases page at
958
        <https://github.com/astral-sh/python-build-standalone/releases> to
959
        discover available releases. If left unspecified the latest release is
960
        used.
961
        """
962
    )
963

964

965
class PexSciePythonVersion(StringField):
1✔
966
    alias = "scie_python_version"
1✔
967
    default = None
1✔
968
    help = help_text(
1✔
969
        """ The portable CPython version to select. Can be either in
970
        `<major>.<minor>` form; e.g.: '3.11', or else fully specified as
971
        `<major>.<minor>.<patch>`; e.g.: '3.11.3'. If you don't specify this
972
        option, Pex will do its best to guess appropriate portable CPython
973
        versions. N.B.: Python Standalone Builds does not provide all patch
974
        versions; so you should check their releases at
975
        <https://github.com/astral-sh/python-build-standalone/releases> if you
976
        wish to pin down to the patch level.
977
        """
978
    )
979

980

981
class PexSciePbsFreeThreaded(TriBoolField):
1✔
982
    alias = "scie_pbs_free_threaded"
1✔
983
    default = None
1✔
984
    help = help_text(
1✔
985
        """
986
        Should the Python Standalone Builds CPython
987
        distributions be free-threaded. If left unspecified or
988
        otherwise turned off, creating a scie from a PEX with
989
        free-threaded abi wheels will automatically turn this
990
        option on. Note that this option is not compatible
991
        with `scie_pbs_stripped=True`. (Pex default: False)
992
        """
993
    )
994

995

996
class PexSciePbsDebug(TriBoolField):
1✔
997
    alias = "scie_pbs_debug"
1✔
998
    default = None
1✔
999
    help = help_text(
1✔
1000
        """ Should the Python Standalone Builds CPython distributions be debug
1001
        builds. Note that this option is not compatible with
1002
        `scie_pbs_stripped=True`. (default: False) """
1003
    )
1004

1005

1006
class PexSciePbsStripped(TriBoolField):
1✔
1007
    alias = "scie_pbs_stripped"
1✔
1008
    required = False
1✔
1009
    default = None
1✔
1010
    help = help_text(
1✔
1011
        """ Should the Python Standalone Builds CPython distributions used be
1012
        stripped of debug symbols or not. For Linux and Windows particularly,
1013
        the stripped distributions are less than half the size of the
1014
        distributions that ship with debug symbols.  Note that this option is
1015
        not compatible with `scie_pbs_free_threaded=True` or
1016
        `scie_pbs_debug=True`. (Pex default: False) """
1017
    )
1018

1019

1020
class PexScieHashAlgField(StringField):
1✔
1021
    alias = "scie_hash_alg"
1✔
1022
    help = help_text(
1✔
1023
        """ Output a checksum file for each scie generated that is compatible
1024
        with the shasum family of tools. For each unique algorithm specified,
1025
        a sibling file to each scie executable will be generated with the same
1026
        stem as that scie file and hash algorithm name suffix.  The file will
1027
        contain the hex fingerprint of the scie executable using that
1028
        algorithm to hash it. Supported algorithms include at least md5, sha1,
1029
        sha256, sha384 and sha512. For the complete list of supported hash
1030
        algorithms, see the science tool --hash documentation here:
1031
        <https://science.scie.app/cli.html#science-lift-build>.  """
1032
    )
1033

1034

1035
_PEX_BINARY_COMMON_FIELDS = (
1✔
1036
    EnvironmentField,
1037
    InterpreterConstraintsField,
1038
    PythonResolveField,
1039
    PexBinaryDependenciesField,
1040
    PexCheckField,
1041
    PexCompletePlatformsField,
1042
    PexCompressField,
1043
    PexInheritPathField,
1044
    PexStripEnvField,
1045
    PexIgnoreErrorsField,
1046
    PexShBootField,
1047
    PexShebangField,
1048
    PexEmitWarningsField,
1049
    PexLayoutField,
1050
    PexExecutionModeField,
1051
    PexIncludeRequirementsField,
1052
    PexIncludeSourcesField,
1053
    PexIncludeToolsField,
1054
    PexVenvSitePackagesCopies,
1055
    PexVenvHermeticScripts,
1056
    PexExtraBuildArgsField,
1057
    RestartableField,
1058
)
1059

1060
_PEX_SCIE_BINARY_FIELDS = (
1✔
1061
    PexScieField,
1062
    PexScieBindResourcePathField,
1063
    PexScieExeField,
1064
    PexScieArgsField,
1065
    PexScieEnvField,
1066
    PexScieLoadDotenvField,
1067
    PexScieNameStyleField,
1068
    PexScieBusyBox,
1069
    PexSciePexEntrypointEnvPassthrough,
1070
    PexSciePlatformField,
1071
    PexSciePbsReleaseField,
1072
    PexSciePythonVersion,
1073
    PexSciePbsFreeThreaded,
1074
    PexSciePbsDebug,
1075
    PexSciePbsStripped,
1076
    PexScieHashAlgField,
1077
)
1078

1079

1080
class PexBinary(Target):
1✔
1081
    alias = "pex_binary"
1✔
1082
    core_fields = (
1✔
1083
        *COMMON_TARGET_FIELDS,
1084
        *_PEX_BINARY_COMMON_FIELDS,
1085
        *_PEX_SCIE_BINARY_FIELDS,
1086
        PexEntryPointField,
1087
        PexScriptField,
1088
        PexExecutableField,
1089
        PexArgsField,
1090
        PexEnvField,
1091
        OutputPathField,
1092
    )
1093
    help = help_text(
1✔
1094
        f"""
1095
        A Python target that can be converted into an executable PEX file.
1096

1097
        PEX files are self-contained executable files that contain a complete Python environment
1098
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
1099
        """
1100
    )
1101

1102
    def validate(self) -> None:
1✔
1103
        got_entry_point = self[PexEntryPointField].value is not None
×
1104
        got_script = self[PexScriptField].value is not None
×
1105
        got_executable = self[PexExecutableField].value is not None
×
1106

1107
        if (got_entry_point + got_script + got_executable) > 1:
×
1108
            raise InvalidTargetException(
×
1109
                softwrap(
1110
                    f"""
1111
                    The `{self.alias}` target {self.address} cannot set more than one of the
1112
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
1113
                    `{self[PexExecutableField].alias}` fields at the same time.
1114
                    To fix, please remove all but one.
1115
                    """
1116
                )
1117
            )
1118

1119

1120
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
1✔
1121
    alias = "entry_points"
1✔
1122
    default = None
1✔
1123
    help = help_text(
1✔
1124
        """
1125
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
1126

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

1131
        If you want the entry point to be for a third-party dependency or to use a console
1132
        script, use the `pex_binary` target directly.
1133
        """
1134
    )
1135

1136

1137
class PexBinariesOverrideField(OverridesField):
1✔
1138
    help = help_text(
1✔
1139
        f"""
1140
        Override the field values for generated `{PexBinary.alias}` targets.
1141

1142
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
1143
        their overrides. You may either use a single string or a tuple of strings to override
1144
        multiple targets.
1145

1146
        For example:
1147

1148
            overrides={{
1149
              "foo.py": {{"execution_mode": "venv"]}},
1150
              "bar.py:main": {{"restartable": True]}},
1151
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
1152
            }}
1153

1154
        Every key is validated to belong to this target's `entry_points` field.
1155

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

1160
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
1161
        same field more than one time for the `entry_point`.
1162
        """
1163
    )
1164

1165

1166
class PexBinariesGeneratorTarget(TargetGenerator):
1✔
1167
    alias = "pex_binaries"
1✔
1168
    help = help_text(
1✔
1169
        """
1170
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
1171

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

1176
        This target generator does not work well to generate `pex_binary` targets where the entry
1177
        point is for a third-party dependency. Dependency inference will not work for those, so
1178
        you will have to set lots of custom metadata for each binary; prefer an explicit
1179
        `pex_binary` target in that case. This target generator works best when the entry point
1180
        is a first-party file, like `app.py` or `app.py:main`.
1181
        """
1182
    )
1183
    generated_target_cls = PexBinary
1✔
1184
    core_fields = (
1✔
1185
        *COMMON_TARGET_FIELDS,
1186
        PexEntryPointsField,
1187
        PexBinariesOverrideField,
1188
    )
1189
    copied_fields = COMMON_TARGET_FIELDS
1✔
1190
    moved_fields = _PEX_BINARY_COMMON_FIELDS
1✔
1191

1192

1193
class PexBinaryDefaults(Subsystem):
1✔
1194
    options_scope = "pex-binary-defaults"
1✔
1195
    help = "Default settings for creating PEX executables."
1✔
1196

1197
    emit_warnings = BoolOption(
1✔
1198
        default=True,
1199
        help=softwrap(
1200
            """
1201
            Whether built PEX binaries should emit PEX warnings at runtime by default.
1202

1203
            Can be overridden by specifying the `emit_warnings` parameter of individual
1204
            `pex_binary` targets
1205
            """
1206
        ),
1207
        advanced=True,
1208
    )
1209

1210

1211
# -----------------------------------------------------------------------------------------------
1212
# `python_test` and `python_tests` targets
1213
# -----------------------------------------------------------------------------------------------
1214

1215

1216
class PythonTestSourceField(PythonSourceField):
1✔
1217
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
1✔
1218

1219
    def validate_resolved_files(self, files: Sequence[str]) -> None:
1✔
1220
        super().validate_resolved_files(files)
1✔
1221
        file = files[0]
1✔
1222
        file_name = os.path.basename(file)
1✔
1223
        if file_name == "conftest.py":
1✔
1224
            raise InvalidFieldException(
×
1225
                softwrap(
1226
                    f"""
1227
                    The {repr(self.alias)} field in target {self.address} should not be set to the
1228
                    file 'conftest.py', but was set to {repr(self.value)}.
1229

1230
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1231
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
1232
                    autogenerate a `python_test_utils` target.
1233
                    """
1234
                )
1235
            )
1236

1237

1238
class PythonTestsDependenciesField(PythonDependenciesField):
1✔
1239
    supports_transitive_excludes = True
1✔
1240

1241

1242
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
1✔
1243
    alias = "entry_point_dependencies"
1✔
1244
    help = help_text(
1✔
1245
        lambda: f"""
1246
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
1247

1248
        This is a dict where each key is a `{PythonDistribution.alias}` address
1249
        and the value is a list or tuple of entry point groups and/or entry points
1250
        on that target. The strings in the value list/tuple must be one of:
1251
        - "entry.point.group/entry-point-name" to depend on a named entry point
1252
        - "entry.point.group" (without a "/") to depend on an entry point group
1253
        - "*" to get all entry points on the target
1254

1255
        For example:
1256

1257
            {PythonTestsEntryPointDependenciesField.alias}={{
1258
                "//foo/address:dist_tgt": ["*"],  # all entry points
1259
                "bar:dist_tgt": ["console_scripts"],  # only from this group
1260
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
1261
                "another:dist_tgt": [  # multiple entry points
1262
                    "console_scripts/my-script",
1263
                    "console_scripts/another-script",
1264
                    "entry.point.group/entry-point-name",
1265
                    "other.group",
1266
                    "gui_scripts",
1267
                ],
1268
            }}
1269

1270
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
1271
        will be added as dependencies so that they are available on PYTHONPATH
1272
        during tests.
1273

1274
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
1275
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
1276
        `entry_points.txt` file will only include the entry points requested on this
1277
        field. This allows the tests, or the code under test, to lookup entry points'
1278
        metadata using an API like the `importlib.metadata.entry_points()` API in the
1279
        standard library (available on older Python interpreters via the
1280
        `importlib-metadata` distribution).
1281
        """
1282
    )
1283

1284

1285
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
1286
class PythonTestsTimeoutField(IntField):
1✔
1287
    alias = "timeout"
1✔
1288
    help = help_text(
1✔
1289
        """
1290
        A timeout (in seconds) used by each test file belonging to this target.
1291

1292
        If unset, will default to `[test].timeout_default`; if that option is also unset,
1293
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
1294
        applies if the option `--test-timeouts` is set to true (the default).
1295
        """
1296
    )
1297
    valid_numbers = ValidNumbers.positive_only
1✔
1298

1299
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
1✔
1300
        """Determine the timeout (in seconds) after resolving conflicting global options in the
1301
        `pytest` and `test` scopes.
1302

1303
        This function is deprecated and should be replaced by the similarly named one in
1304
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
1305
        """
1306

1307
        enabled = test.options.timeouts
1✔
1308
        timeout_default = test.options.timeout_default
1✔
1309
        timeout_maximum = test.options.timeout_maximum
1✔
1310

1311
        if not enabled:
1✔
1312
            return None
×
1313
        if self.value is None:
1✔
1314
            if timeout_default is None:
1✔
1315
                return None
1✔
1316
            result = cast(int, timeout_default)
×
1317
        else:
1318
            result = self.value
×
1319
        if timeout_maximum is not None:
×
1320
            return min(result, cast(int, timeout_maximum))
×
1321
        return result
×
1322

1323

1324
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
1✔
1325
    pass
1✔
1326

1327

1328
class PythonTestsXdistConcurrencyField(IntField):
1✔
1329
    alias = "xdist_concurrency"
1✔
1330
    help = help_text(
1✔
1331
        """
1332
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1333

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

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

1342
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1343
        """
1344
    )
1345

1346

1347
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
1✔
1348
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
1✔
1349

1350

1351
class SkipPythonTestsField(BoolField):
1✔
1352
    alias = "skip_tests"
1✔
1353
    default = False
1✔
1354
    help = "If true, don't run this target's tests."
1✔
1355

1356

1357
_PYTHON_TEST_MOVED_FIELDS = (
1✔
1358
    PythonTestsDependenciesField,
1359
    # This field is registered in the experimental backend for now.
1360
    # PythonTestsEntryPointDependenciesField,
1361
    PythonResolveField,
1362
    PythonRunGoalUseSandboxField,
1363
    PythonTestsTimeoutField,
1364
    PythonTestsXdistConcurrencyField,
1365
    PythonTestsBatchCompatibilityTagField,
1366
    RuntimePackageDependenciesField,
1367
    PythonTestsExtraEnvVarsField,
1368
    InterpreterConstraintsField,
1369
    SkipPythonTestsField,
1370
    EnvironmentField,
1371
)
1372

1373

1374
class PythonTestTarget(Target):
1✔
1375
    alias = "python_test"
1✔
1376
    core_fields = (
1✔
1377
        *COMMON_TARGET_FIELDS,
1378
        *_PYTHON_TEST_MOVED_FIELDS,
1379
        PythonTestsDependenciesField,
1380
        PythonTestSourceField,
1381
    )
1382
    help = help_text(
1✔
1383
        f"""
1384
        A single Python test file, written in either Pytest style or unittest style.
1385

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

1390
        See {doc_url("docs/python/goals/test")}
1391
        """
1392
    )
1393

1394

1395
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
1✔
1396
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
1✔
1397
    default = DEFAULT_TEST_FILE_GLOBS
1✔
1398
    help = generate_multiple_sources_field_help_message(
1✔
1399
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1400
    )
1401

1402
    def validate_resolved_files(self, files: Sequence[str]) -> None:
1✔
1403
        super().validate_resolved_files(files)
1✔
1404
        # We don't technically need to error for `conftest.py` here because `PythonTestSourceField`
1405
        # already validates this, but we get a better error message this way so that users don't
1406
        # have to reason about generated targets.
1407
        conftest_files = [fp for fp in files if os.path.basename(fp) == "conftest.py"]
1✔
1408
        if conftest_files:
1✔
1409
            raise InvalidFieldException(
×
1410
                softwrap(
1411
                    f"""
1412
                    The {repr(self.alias)} field in target {self.address} should not include the
1413
                    file 'conftest.py', but included these: {conftest_files}.
1414

1415
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1416
                    You can run `{bin_name()} tailor` after removing the files from the
1417
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1418
                    `python_test_utils` target.
1419
                    """
1420
                )
1421
            )
1422

1423

1424
class PythonTestsOverrideField(OverridesField):
1✔
1425
    help = generate_file_based_overrides_field_help_message(
1✔
1426
        PythonTestTarget.alias,
1427
        """
1428
        overrides={
1429
            "foo_test.py": {"timeout": 120},
1430
            "bar_test.py": {"timeout": 200},
1431
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1432
        }
1433
        """,
1434
    )
1435

1436

1437
class PythonTestsGeneratorTarget(TargetFilesGenerator):
1✔
1438
    alias = "python_tests"
1✔
1439
    core_fields = (
1✔
1440
        *COMMON_TARGET_FIELDS,
1441
        PythonTestsGeneratingSourcesField,
1442
        PythonTestsOverrideField,
1443
    )
1444
    generated_target_cls = PythonTestTarget
1✔
1445
    copied_fields = COMMON_TARGET_FIELDS
1✔
1446
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
1✔
1447
    settings_request_cls = PythonFilesGeneratorSettingsRequest
1✔
1448
    help = "Generate a `python_test` target for each file in the `sources` field."
1✔
1449

1450

1451
# -----------------------------------------------------------------------------------------------
1452
# `python_source`, `python_sources`, and `python_test_utils` targets
1453
# -----------------------------------------------------------------------------------------------
1454

1455

1456
class PythonSourceTarget(Target):
1✔
1457
    alias = "python_source"
1✔
1458
    core_fields = (
1✔
1459
        *COMMON_TARGET_FIELDS,
1460
        InterpreterConstraintsField,
1461
        PythonDependenciesField,
1462
        PythonResolveField,
1463
        PythonRunGoalUseSandboxField,
1464
        PythonSourceField,
1465
        RestartableField,
1466
    )
1467
    help = "A single Python source file."
1✔
1468

1469

1470
class PythonSourcesOverridesField(OverridesField):
1✔
1471
    help = generate_file_based_overrides_field_help_message(
1✔
1472
        PythonSourceTarget.alias,
1473
        """
1474
        overrides={
1475
            "foo.py": {"skip_pylint": True]},
1476
            "bar.py": {"skip_flake8": True]},
1477
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1478
        }"
1479
        """,
1480
    )
1481

1482

1483
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
1✔
1484
    default = DEFAULT_TESTUTIL_FILE_GLOBS
1✔
1485
    help = generate_multiple_sources_field_help_message(
1✔
1486
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1487
    )
1488

1489

1490
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
1✔
1491
    default = (
1✔
1492
        ("*.py", "*.pyi")
1493
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1494
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1495
    )
1496
    help = generate_multiple_sources_field_help_message(
1✔
1497
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1498
    )
1499

1500

1501
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
1✔
1502
    alias = "python_test_utils"
1✔
1503
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1504
    core_fields = (
1✔
1505
        *COMMON_TARGET_FIELDS,
1506
        PythonTestUtilsGeneratingSourcesField,
1507
        PythonSourcesOverridesField,
1508
    )
1509
    generated_target_cls = PythonSourceTarget
1✔
1510
    copied_fields = COMMON_TARGET_FIELDS
1✔
1511
    moved_fields = (
1✔
1512
        PythonResolveField,
1513
        PythonRunGoalUseSandboxField,
1514
        PythonDependenciesField,
1515
        InterpreterConstraintsField,
1516
    )
1517
    settings_request_cls = PythonFilesGeneratorSettingsRequest
1✔
1518
    help = help_text(
1✔
1519
        """
1520
        Generate a `python_source` target for each file in the `sources` field.
1521

1522
        This target generator is intended for test utility files like `conftest.py` or
1523
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1524
        way as the `python_sources` target generator does, only that the `sources` field has a
1525
        different default. So it is valid to use `python_sources` instead. However, this target
1526
        can be helpful to better model your code by keeping separate test support files vs.
1527
        production files.
1528
        """
1529
    )
1530

1531

1532
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
1✔
1533
    alias = "python_sources"
1✔
1534
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1535
    core_fields = (
1✔
1536
        *COMMON_TARGET_FIELDS,
1537
        PythonSourcesGeneratingSourcesField,
1538
        PythonSourcesOverridesField,
1539
    )
1540
    generated_target_cls = PythonSourceTarget
1✔
1541
    copied_fields = COMMON_TARGET_FIELDS
1✔
1542
    moved_fields = (
1✔
1543
        PythonResolveField,
1544
        PythonRunGoalUseSandboxField,
1545
        PythonDependenciesField,
1546
        InterpreterConstraintsField,
1547
        RestartableField,
1548
    )
1549
    settings_request_cls = PythonFilesGeneratorSettingsRequest
1✔
1550
    help = help_text(
1✔
1551
        """
1552
        Generate a `python_source` target for each file in the `sources` field.
1553

1554
        You can either use this target generator or `python_test_utils` for test utility files
1555
        like `conftest.py`. They behave identically, but can help to better model and keep
1556
        separate test support files vs. production files.
1557
        """
1558
    )
1559

1560

1561
# -----------------------------------------------------------------------------------------------
1562
# `python_requirement` target
1563
# -----------------------------------------------------------------------------------------------
1564

1565

1566
class _PipRequirementSequenceField(Field):
1✔
1567
    value: tuple[PipRequirement, ...]
1✔
1568

1569
    @classmethod
1✔
1570
    def compute_value(
1✔
1571
        cls, raw_value: Iterable[str] | None, address: Address
1572
    ) -> tuple[PipRequirement, ...]:
1573
        value = super().compute_value(raw_value, address)
×
1574
        if value is None:
×
1575
            return ()
×
1576
        invalid_type_error = InvalidFieldTypeException(
×
1577
            address,
1578
            cls.alias,
1579
            value,
1580
            expected_type="an iterable of pip-style requirement strings (e.g. a list)",
1581
        )
1582
        if isinstance(value, str) or not isinstance(value, collections.abc.Iterable):
×
1583
            raise invalid_type_error
×
1584
        result = []
×
1585
        for v in value:
×
1586
            # We allow passing a pre-parsed `PipRequirement`. This is intended for macros which
1587
            # might have already parsed so that we can avoid parsing multiple times.
1588
            if isinstance(v, PipRequirement):
×
1589
                result.append(v)
×
1590
            elif isinstance(v, str):
×
1591
                try:
×
1592
                    parsed = PipRequirement.parse(
×
1593
                        v, description_of_origin=f"the '{cls.alias}' field for the target {address}"
1594
                    )
1595
                except ValueError as e:
×
1596
                    raise InvalidFieldException(e)
×
1597
                result.append(parsed)
×
1598
            else:
1599
                raise invalid_type_error
×
1600
        return tuple(result)
×
1601

1602

1603
class PythonRequirementDependenciesField(Dependencies):
1✔
1604
    pass
1✔
1605

1606

1607
class PythonRequirementsField(_PipRequirementSequenceField):
1✔
1608
    alias = "requirements"
1✔
1609
    required = True
1✔
1610
    help = help_text(
1✔
1611
        """
1612
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1613

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

1618
        If the requirement depends on some other requirement to work, such as needing
1619
        `setuptools` to be built, use the `dependencies` field instead.
1620
        """
1621
    )
1622

1623

1624
_default_module_mapping_url = git_url(
1✔
1625
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1626
)
1627

1628

1629
class PythonRequirementModulesField(StringSequenceField):
1✔
1630
    alias = "modules"
1✔
1631
    help = help_text(
1✔
1632
        f"""
1633
        The modules this requirement provides (used for dependency inference).
1634

1635
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1636
        "easy_install"]`.
1637

1638
        Usually you can leave this field off. If unspecified, Pants will first look at the
1639
        default module mapping ({_default_module_mapping_url}), and then will default to
1640
        the normalized project name. For example, the requirement `Django` would default to
1641
        the module `django`.
1642

1643
        Mutually exclusive with the `type_stub_modules` field.
1644
        """
1645
    )
1646

1647

1648
class PythonRequirementTypeStubModulesField(StringSequenceField):
1✔
1649
    alias = "type_stub_modules"
1✔
1650
    help = help_text(
1✔
1651
        f"""
1652
        The modules this requirement provides if the requirement is a type stub (used for
1653
        dependency inference).
1654

1655
        For example, the requirement `types-requests` provides `["requests"]`.
1656

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

1664
        Mutually exclusive with the `modules` field.
1665
        """
1666
    )
1667

1668

1669
def normalize_module_mapping(
1✔
1670
    mapping: Mapping[str, Iterable[str]] | None,
1671
) -> FrozenDict[str, tuple[str, ...]]:
1672
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
×
1673

1674

1675
class PythonRequirementResolveField(PythonResolveField):
1✔
1676
    alias = "resolve"
1✔
1677
    required = False
1✔
1678
    help = help_text(
1✔
1679
        """
1680
        The resolve from `[python].resolves` that this requirement is included in.
1681

1682
        If not defined, will default to `[python].default_resolve`.
1683

1684
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1685
        it will include all requirements that are declared with that resolve.
1686
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1687
        they use via their `resolve` field; so, for your first-party code to use a
1688
        particular `python_requirement` target, that requirement must be included in the resolve
1689
        used by that code.
1690
        """
1691
    )
1692

1693

1694
class PythonRequirementFindLinksField(StringSequenceField):
1✔
1695
    # NB: This is solely used for `pants_requirements` target generation
1696
    alias = "_find_links"
1✔
1697
    required = False
1✔
1698
    default = ()
1✔
1699
    help = "<Internal>"
1✔
1700

1701

1702
class PythonRequirementEntryPointField(EntryPointField):
1✔
1703
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1704
    pass
1✔
1705

1706

1707
class PythonRequirementTarget(Target):
1✔
1708
    alias = "python_requirement"
1✔
1709
    core_fields = (
1✔
1710
        *COMMON_TARGET_FIELDS,
1711
        PythonRequirementsField,
1712
        PythonRequirementDependenciesField,
1713
        PythonRequirementModulesField,
1714
        PythonRequirementTypeStubModulesField,
1715
        PythonRequirementResolveField,
1716
        PythonRequirementEntryPointField,
1717
        PythonRequirementFindLinksField,
1718
    )
1719
    help = help_text(
1✔
1720
        f"""
1721
        A Python requirement installable by pip.
1722

1723
        This target is useful when you want to declare Python requirements inline in a
1724
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1725
        the target generator `python_requirements` to convert each
1726
        requirement into a `python_requirement` target automatically. For Poetry, use
1727
        `poetry_requirements`.
1728

1729
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1730
        """
1731
    )
1732

1733
    def validate(self) -> None:
1✔
1734
        if (
×
1735
            self[PythonRequirementModulesField].value
1736
            and self[PythonRequirementTypeStubModulesField].value
1737
        ):
1738
            raise InvalidTargetException(
×
1739
                softwrap(
1740
                    f"""
1741
                    The `{self.alias}` target {self.address} cannot set both the
1742
                    `{self[PythonRequirementModulesField].alias}` and
1743
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1744
                    To fix, please remove one.
1745
                    """
1746
                )
1747
            )
1748

1749

1750
# -----------------------------------------------------------------------------------------------
1751
# `python_distribution` target
1752
# -----------------------------------------------------------------------------------------------
1753

1754

1755
# See `target_types_rules.py` for a dependency injection rule.
1756
class PythonDistributionDependenciesField(Dependencies):
1✔
1757
    supports_transitive_excludes = True
1✔
1758

1759

1760
class PythonProvidesField(ScalarField, AsyncFieldMixin):
1✔
1761
    alias = "provides"
1✔
1762
    expected_type = PythonArtifact
1✔
1763
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
1✔
1764
    value: PythonArtifact
1✔
1765
    required = True
1✔
1766
    help = help_text(
1✔
1767
        f"""
1768
        The setup.py kwargs for the external artifact built from this target.
1769

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

1774
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1775
        """
1776
    )
1777

1778
    @classmethod
1✔
1779
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
1✔
1780
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1781

1782

1783
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
1✔
1784
    alias = "entry_points"
1✔
1785
    required = False
1✔
1786
    help = help_text(
1✔
1787
        f"""
1788
        Any entry points, such as `console_scripts` and `gui_scripts`.
1789

1790
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1791
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1792
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1793
        `pex_binary` target.
1794

1795
        Example:
1796

1797
            entry_points={{
1798
              "console_scripts": {{
1799
                "my-script": "project.app:main",
1800
                "another-script": "project/subdir:pex_binary_tgt"
1801
              }}
1802
            }}
1803

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

1810
        Pants will attempt to infer dependencies, which you can confirm by running:
1811

1812
            {bin_name()} dependencies <python_distribution target address>
1813
        """
1814
    )
1815

1816

1817
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
1✔
1818
    help = help_text(
1✔
1819
        """
1820
        The path to the directory to write the distribution file to, relative the dist directory.
1821

1822
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1823
        level of the dist dir.
1824
        """
1825
    )
1826
    alias = "output_path"
1✔
1827
    default = ""
1✔
1828

1829

1830
@dataclass(frozen=True)
1✔
1831
class PythonDistributionEntryPoint:
1✔
1832
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1833

1834
    entry_point: EntryPoint
1✔
1835
    pex_binary_address: Address | None
1✔
1836

1837

1838
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1839
@dataclass(frozen=True)
1✔
1840
class ResolvedPythonDistributionEntryPoints:
1✔
1841
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1842
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
1✔
1843

1844
    @property
1✔
1845
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
1✔
1846
        """Filters out all entry points from pex binary targets."""
1847
        return FrozenDict(
×
1848
            {
1849
                category: FrozenDict(
1850
                    {
1851
                        ep_name: ep_val.entry_point
1852
                        for ep_name, ep_val in entry_points.items()
1853
                        if not ep_val.pex_binary_address
1854
                    }
1855
                )
1856
                for category, entry_points in self.val.items()
1857
            }
1858
        )
1859

1860
    @property
1✔
1861
    def pex_binary_addresses(self) -> Addresses:
1✔
1862
        """Returns the addresses to all pex binary targets owning entry points used."""
1863
        return Addresses(
×
1864
            ep_val.pex_binary_address
1865
            for category, entry_points in self.val.items()
1866
            for ep_val in entry_points.values()
1867
            if ep_val.pex_binary_address
1868
        )
1869

1870

1871
@dataclass(frozen=True)
1✔
1872
class ResolvePythonDistributionEntryPointsRequest:
1✔
1873
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1874
    that should be resolved into a setuptools entry point.
1875

1876
    If the `entry_points_field` is present, inspect the specified entry points.
1877
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1878

1879
    This is to support inspecting one or the other depending on use case, using the same
1880
    logic for resolving pex_binary addresses etc.
1881
    """
1882

1883
    entry_points_field: PythonDistributionEntryPointsField | None = None
1✔
1884
    provides_field: PythonProvidesField | None = None
1✔
1885

1886
    def __post_init__(self):
1✔
1887
        # Must provide at least one of these fields.
1888
        assert self.entry_points_field or self.provides_field
×
1889

1890

1891
class WheelField(BoolField):
1✔
1892
    alias = "wheel"
1✔
1893
    default = True
1✔
1894
    help = "Whether to build a wheel for the distribution."
1✔
1895

1896

1897
class SDistField(BoolField):
1✔
1898
    alias = "sdist"
1✔
1899
    default = True
1✔
1900
    help = "Whether to build an sdist for the distribution."
1✔
1901

1902

1903
class ConfigSettingsField(DictStringToStringSequenceField):
1✔
1904
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1905

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

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

1911
    - Build frontends should support string values, and may also support other mechanisms
1912
      (apparently meaning other types).
1913

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

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

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

1929

1930
class WheelConfigSettingsField(ConfigSettingsField):
1✔
1931
    alias = "wheel_config_settings"
1✔
1932
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
1✔
1933

1934

1935
class SDistConfigSettingsField(ConfigSettingsField):
1✔
1936
    alias = "sdist_config_settings"
1✔
1937
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
1✔
1938

1939

1940
class BuildBackendEnvVarsField(StringSequenceField):
1✔
1941
    alias = "env_vars"
1✔
1942
    required = False
1✔
1943
    help = help_text(
1✔
1944
        """
1945
        Environment variables to set when running the PEP-517 build backend.
1946

1947
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1948
        or just `ENV_VAR` to copy the value from Pants's own environment.
1949
        """
1950
    )
1951

1952

1953
class GenerateSetupField(TriBoolField):
1✔
1954
    alias = "generate_setup"
1✔
1955
    required = False
1✔
1956
    # The default behavior if this field is unspecified is controlled by the
1957
    # --generate-setup-default option in the setup-py-generation scope.
1958
    default = None
1✔
1959

1960
    help = help_text(
1✔
1961
        """
1962
        Whether to generate setup information for this distribution, based on analyzing
1963
        sources and dependencies. Set to False to use existing setup information, such as
1964
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1965
        """
1966
    )
1967

1968

1969
class LongDescriptionPathField(StringField):
1✔
1970
    alias = "long_description_path"
1✔
1971
    required = False
1✔
1972

1973
    help = help_text(
1✔
1974
        """
1975
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1976

1977
        Path is relative to the build root.
1978

1979
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1980

1981
        This field won't automatically set `long_description_content_type` field for you.
1982
        You have to specify this field yourself in the `provides` field.
1983
        """
1984
    )
1985

1986

1987
class PythonDistribution(Target):
1✔
1988
    alias: ClassVar[str] = "python_distribution"
1✔
1989
    core_fields = (
1✔
1990
        *COMMON_TARGET_FIELDS,
1991
        InterpreterConstraintsField,
1992
        PythonDistributionDependenciesField,
1993
        PythonDistributionEntryPointsField,
1994
        PythonProvidesField,
1995
        GenerateSetupField,
1996
        WheelField,
1997
        SDistField,
1998
        WheelConfigSettingsField,
1999
        SDistConfigSettingsField,
2000
        BuildBackendEnvVarsField,
2001
        LongDescriptionPathField,
2002
        PythonDistributionOutputPathField,
2003
    )
2004
    help = help_text(
1✔
2005
        f"""
2006
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
2007

2008
        See {doc_url("docs/python/overview/building-distributions")}.
2009
        """
2010
    )
2011

2012

2013
# -----------------------------------------------------------------------------------------------
2014
# `vcs_version` target
2015
# -----------------------------------------------------------------------------------------------
2016

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

2024

2025
class VCSVersionDummySourceField(OptionalSingleSourceField):
1✔
2026
    """A dummy SourceField for participation in the codegen machinery."""
2027

2028
    alias = "_dummy_source"  # Leading underscore omits the field from help.
1✔
2029
    help = "A version string generated from VCS information"
1✔
2030

2031

2032
class VersionTagRegexField(StringField):
1✔
2033
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
1✔
2034
    alias = "tag_regex"
1✔
2035
    help = help_text(
1✔
2036
        """
2037
        A Python regex string to extract the version string from a VCS tag.
2038

2039
        The regex needs to contain either a single match group, or a group named version,
2040
        that captures the actual version information.
2041

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

2044
        See https://github.com/pypa/setuptools_scm for implementation details.
2045
        """
2046
    )
2047

2048

2049
class VersionGenerateToField(StringField):
1✔
2050
    required = True
1✔
2051
    alias = "generate_to"
1✔
2052
    help = help_text(
1✔
2053
        """
2054
        Generate the version data to this relative path, using the template field.
2055

2056
        Note that the generated output will not be written to disk in the source tree, but
2057
        will be available as a generated dependency to code that depends on this target.
2058
        """
2059
    )
2060

2061

2062
class VersionTemplateField(StringField):
1✔
2063
    required = True
1✔
2064
    alias = "template"
1✔
2065
    help = help_text(
1✔
2066
        """
2067
        Generate the version data using this format string, which takes a version format kwarg.
2068

2069
        E.g., `'version = "{version}"'`
2070
        """
2071
    )
2072

2073

2074
class VersionVersionSchemeField(StringField):
1✔
2075
    alias = "version_scheme"
1✔
2076
    help = help_text(
1✔
2077
        """
2078
        The version scheme to configure `setuptools_scm` to use.
2079
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
2080
        """
2081
    )
2082

2083

2084
class VersionLocalSchemeField(StringField):
1✔
2085
    alias = "local_scheme"
1✔
2086
    help = help_text(
1✔
2087
        """
2088
        The local scheme to configure `setuptools_scm` to use.
2089
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
2090
        """
2091
    )
2092

2093

2094
class VCSVersion(Target):
1✔
2095
    alias = "vcs_version"
1✔
2096
    core_fields = (
1✔
2097
        *COMMON_TARGET_FIELDS,
2098
        VersionTagRegexField,
2099
        VersionVersionSchemeField,
2100
        VersionLocalSchemeField,
2101
        VCSVersionDummySourceField,
2102
        VersionGenerateToField,
2103
        VersionTemplateField,
2104
    )
2105
    help = help_text(
1✔
2106
        f"""
2107
        Generates a version string from VCS state.
2108

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

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

2116
        If you need functionality that is not currently exposed here, please reach out to us at
2117
        {doc_url("community/getting-help")}.
2118
        """
2119
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc