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

pantsbuild / pants / 21042790249

15 Jan 2026 06:57PM UTC coverage: 43.263% (-35.4%) from 78.666%
21042790249

Pull #23021

github

web-flow
Merge cc03ad8de into d250c80fe
Pull Request #23021: WIP gh workflow scie pex

23 of 33 new or added lines in 3 files covered. (69.7%)

16147 existing lines in 521 files now uncovered.

26164 of 60477 relevant lines covered (43.26%)

0.87 hits per line

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

81.59
/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
2✔
5

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

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

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

68
logger = logging.getLogger(__name__)
2✔
69

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

73

74
# -----------------------------------------------------------------------------------------------
75
# Common fields
76
# -----------------------------------------------------------------------------------------------
77

78

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

83

84
class PythonDependenciesField(Dependencies):
2✔
85
    pass
2✔
86

87

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

91

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

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

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

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

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

112
    def value_or_configured_default(
2✔
113
        self, python_setup: PythonSetup, resolve: PythonResolveField | None
114
    ) -> tuple[str, ...]:
115
        """Return either the given `compatibility` field or the global interpreter constraints.
116

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

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

136

137
class PythonResolveLikeFieldToValueRequest(ResolveLikeFieldToValueRequest):
2✔
138
    pass
2✔
139

140

141
class PythonResolveField(StringField, AsyncFieldMixin, ResolveLikeField):
2✔
142
    alias = "resolve"
2✔
143
    required = False
2✔
144
    help = help_text(
2✔
145
        """
146
        The resolve from `[python].resolves` to use.
147

148
        If not defined, will default to `[python].default_resolve`.
149

150
        All dependencies must share the same value for their `resolve` field.
151
        """
152
    )
153

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

167
    def get_resolve_like_field_to_value_request(self) -> type[ResolveLikeFieldToValueRequest]:
2✔
168
        return PythonResolveLikeFieldToValueRequest
×
169

170

171
class PrefixedPythonResolveField(PythonResolveField):
2✔
172
    alias = "python_resolve"
2✔
173

174

175
class PythonRunGoalUseSandboxField(TriBoolField):
2✔
176
    alias = "run_goal_use_sandbox"
2✔
177
    help = help_text(
2✔
178
        """
179
        Whether to use a sandbox when `run`ning this target. Defaults to
180
        `[python].default_run_goal_use_sandbox`.
181

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

185
        If false, runs of this target with the `run` goal will use the in-repo sources
186
        directly.
187

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

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

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

203

204
# -----------------------------------------------------------------------------------------------
205
# Target generation support
206
# -----------------------------------------------------------------------------------------------
207

208

209
class PythonFilesGeneratorSettingsRequest(TargetFilesGeneratorSettingsRequest):
2✔
210
    pass
2✔
211

212

213
# -----------------------------------------------------------------------------------------------
214
# `pex_binary` and `pex_binaries` target
215
# -----------------------------------------------------------------------------------------------
216

217

218
# See `target_types_rules.py` for a dependency injection rule.
219
class PexBinaryDependenciesField(Dependencies):
2✔
220
    supports_transitive_excludes = True
2✔
221

222

223
class MainSpecification(ABC):
2✔
224
    @abstractmethod
225
    def iter_pex_args(self) -> Iterator[str]: ...
226

227
    @property
228
    @abstractmethod
229
    def spec(self) -> str: ...
230

231

232
@dataclass(frozen=True)
2✔
233
class EntryPoint(MainSpecification):
2✔
234
    module: str
2✔
235
    function: str | None = None
2✔
236

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

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

295
    def iter_pex_args(self) -> Iterator[str]:
2✔
296
        yield "--entry-point"
×
297
        yield self.spec
×
298

299
    @property
2✔
300
    def spec(self) -> str:
2✔
301
        return self.module if self.function is None else f"{self.module}:{self.function}"
2✔
302

303

304
@dataclass(frozen=True)
2✔
305
class ConsoleScript(MainSpecification):
2✔
306
    name: str
2✔
307

308
    def iter_pex_args(self) -> Iterator[str]:
2✔
309
        yield "--console-script"
×
310
        yield self.name
×
311

312
    @property
2✔
313
    def spec(self) -> str:
2✔
314
        return self.name
2✔
315

316

317
@dataclass(frozen=True)
2✔
318
class Executable(MainSpecification):
2✔
319
    executable: str
2✔
320

321
    @classmethod
2✔
322
    def create(cls, address: Address, filename: str) -> Executable:
2✔
323
        # spec_path is relative to the workspace. The rule is responsible for
324
        # stripping the source root as needed.
UNCOV
325
        return cls(os.path.join(address.spec_path, filename).lstrip(os.path.sep))
×
326

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

333
    @property
2✔
334
    def spec(self) -> str:
2✔
335
        return self.executable
×
336

337

338
class EntryPointField(AsyncFieldMixin, Field):
2✔
339
    alias = "entry_point"
2✔
340
    default = None
2✔
341
    help = help_text(
2✔
342
        """
343
        Set the entry point, i.e. what gets run when executing `./my_app.pex`, to a module.
344

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

348
          1) `'app.py'`, Pants will convert into the module `path.to.app`;
349
          2) `'app.py:func'`, Pants will convert into `path.to.app:func`.
350

351
        You may only set one of: this field, or the `script` field, or the `executable` field.
352
        Leave off all three fields to have no entry point.
353
        """
354
    )
355
    value: EntryPoint | None
2✔
356

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

369

370
class PexEntryPointField(EntryPointField):
2✔
371
    # Specialist subclass for use with `PexBinary` targets.
372
    pass
2✔
373

374

375
# See `target_types_rules.py` for the `ResolvePexEntryPointRequest -> ResolvedPexEntryPoint` rule.
376
@dataclass(frozen=True)
2✔
377
class ResolvedPexEntryPoint:
2✔
378
    val: EntryPoint | None
2✔
379
    file_name_used: bool
2✔
380

381

382
@dataclass(frozen=True)
2✔
383
class ResolvePexEntryPointRequest:
2✔
384
    """Determine the `entry_point` for a `pex_binary` after applying all syntactic sugar."""
385

386
    entry_point_field: EntryPointField
2✔
387

388

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

397
        You may only set one of: this field, or the `entry_point` field, or the `executable` field.
398
        Leave off all three fields to have no entry point.
399
        """
400
    )
401
    value: ConsoleScript | None
2✔
402

403
    @classmethod
2✔
404
    def compute_value(cls, raw_value: str | None, address: Address) -> ConsoleScript | None:
2✔
UNCOV
405
        value = super().compute_value(raw_value, address)
×
UNCOV
406
        if value is None:
×
UNCOV
407
            return None
×
UNCOV
408
        if not isinstance(value, str):
×
409
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
UNCOV
410
        return ConsoleScript(value)
×
411

412

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

422
        You may only set one of: this field, or the `entry_point` field, or the `script` field.
423
        Leave off all three fields to have no entry point.
424
        """
425
    )
426
    value: Executable | None
2✔
427

428
    @classmethod
2✔
429
    def compute_value(cls, raw_value: str | None, address: Address) -> Executable | None:
2✔
UNCOV
430
        value = super().compute_value(raw_value, address)
×
UNCOV
431
        if value is None:
×
UNCOV
432
            return None
×
UNCOV
433
        if not isinstance(value, str):
×
434
            raise InvalidFieldTypeException(address, cls.alias, value, expected_type="a string")
×
UNCOV
435
        return Executable.create(address, value)
×
436

437

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

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

452

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

462
        This is different to `{PexArgsField.alias}`: `{PexArgsField.alias}` records
463
        arguments used by the packaged PEX when executed, `{PexExtraBuildArgsField.alias}`
464
        passes arguments to the process that does the packaging.
465
        """
466
    )
467

468

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

486

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

496

497
class PexCompletePlatformsField(SpecialCasedDependencies):
2✔
498
    alias = "complete_platforms"
2✔
499
    help = help_text(
2✔
500
        f"""
501
        The platforms the built PEX should be compatible with.
502

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

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

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

512
        See {doc_url("docs/python/overview/pex#generating-the-complete_platforms-file")} for details on how to create this file.
513
        """
514
    )
515

516

517
class PexInheritPathField(StringField):
2✔
518
    alias = "inherit_path"
2✔
519
    valid_choices = ("false", "fallback", "prefer")
2✔
520
    help = help_text(
2✔
521
        """
522
        Whether to inherit the `sys.path` (aka PYTHONPATH) of the environment that the binary runs in.
523

524
        Use `false` to not inherit `sys.path`; use `fallback` to inherit `sys.path` after packaged
525
        dependencies; and use `prefer` to inherit `sys.path` before packaged dependencies.
526
        """
527
    )
528

529
    # TODO(#9388): deprecate allowing this to be a `bool`.
530
    @classmethod
2✔
531
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
2✔
UNCOV
532
        if isinstance(raw_value, bool):
×
533
            return "prefer" if raw_value else "false"
×
UNCOV
534
        return super().compute_value(raw_value, address)
×
535

536

537
class PexStripEnvField(BoolField):
2✔
538
    alias = "strip_pex_env"
2✔
539
    default = True
2✔
540
    help = help_text(
2✔
541
        """
542
        Whether or not to strip the PEX runtime environment of `PEX*` environment variables.
543

544
        Most applications have no need for the `PEX*` environment variables that are used to
545
        control PEX startup; so these variables are scrubbed from the environment by Pex before
546
        transferring control to the application by default. This prevents any subprocesses that
547
        happen to execute other PEX files from inheriting these control knob values since most
548
        would be undesired; e.g.: PEX_MODULE or PEX_PATH.
549
        """
550
    )
551

552

553
class PexIgnoreErrorsField(BoolField):
2✔
554
    alias = "ignore_errors"
2✔
555
    default = False
2✔
556
    help = "Should PEX ignore errors when it cannot resolve dependencies?"
2✔
557

558

559
class PexShBootField(BoolField):
2✔
560
    alias = "sh_boot"
2✔
561
    default = False
2✔
562
    help = help_text(
2✔
563
        """
564
        Should PEX create a modified ZIPAPP that uses `/bin/sh` to boot?
565

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

581

582
class PexShebangField(StringField):
2✔
583
    alias = "shebang"
2✔
584
    help = help_text(
2✔
585
        """
586
        Set the generated PEX to use this shebang, rather than the default of PEX choosing a
587
        shebang based on the interpreter constraints.
588

589
        This influences the behavior of running `./result.pex`. You can ignore the shebang by
590
        instead running `/path/to/python_interpreter ./result.pex`.
591
        """
592
    )
593

594

595
class PexEmitWarningsField(TriBoolField):
2✔
596
    alias = "emit_warnings"
2✔
597
    help = help_text(
2✔
598
        """
599
        Whether or not to emit PEX warnings at runtime.
600

601
        The default is determined by the option `emit_warnings` in the `[pex-binary-defaults]` scope.
602
        """
603
    )
604

605
    def value_or_global_default(self, pex_binary_defaults: PexBinaryDefaults) -> bool:
2✔
606
        if self.value is None:
×
607
            return pex_binary_defaults.emit_warnings
×
608

609
        return self.value
×
610

611

612
class PexExecutionMode(Enum):
2✔
613
    ZIPAPP = "zipapp"
2✔
614
    VENV = "venv"
2✔
615

616

617
class PexExecutionModeField(StringField):
2✔
618
    alias = "execution_mode"
2✔
619
    valid_choices = PexExecutionMode
2✔
620
    expected_type = str
2✔
621
    default = PexExecutionMode.ZIPAPP.value
2✔
622
    help = help_text(
2✔
623
        f"""
624
        The mode the generated PEX file will run in.
625

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

631
        The fastest execution mode in the steady state is {PexExecutionMode.VENV.value!r}, which
632
        generates a virtual environment from the PEX file on first run, but then achieves near
633
        native virtual environment start times. This mode also benefits from a traditional virtual
634
        environment `sys.path`, giving maximum compatibility with stdlib and third party APIs.
635
        """
636
    )
637

638

639
class PexLayout(Enum):
2✔
640
    ZIPAPP = "zipapp"
2✔
641
    PACKED = "packed"
2✔
642
    LOOSE = "loose"
2✔
643

644

645
class PexLayoutField(StringField):
2✔
646
    alias = "layout"
2✔
647
    valid_choices = PexLayout
2✔
648
    expected_type = str
2✔
649
    default = PexLayout.ZIPAPP.value
2✔
650
    help = help_text(
2✔
651
        f"""
652
        The layout used for the PEX binary.
653

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

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

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

668
        Both zipapp and packed layouts install themselves in the `$PEX_ROOT` as loose apps by
669
        default before executing, but these layouts compose with
670
        `{PexExecutionModeField.alias}='{PexExecutionMode.ZIPAPP.value}'` as well.
671
        """
672
    )
673

674

675
class PexIncludeRequirementsField(BoolField):
2✔
676
    alias = "include_requirements"
2✔
677
    default = True
2✔
678
    help = help_text(
2✔
679
        """
680
        Whether to include the third party requirements the binary depends on in the
681
        packaged PEX file.
682
        """
683
    )
684

685

686
class PexIncludeSourcesField(BoolField):
2✔
687
    alias = "include_sources"
2✔
688
    default = True
2✔
689
    help = help_text(
2✔
690
        """
691
        Whether to include your first party sources the binary uses in the packaged PEX file.
692
        """
693
    )
694

695

696
class PexIncludeToolsField(BoolField):
2✔
697
    alias = "include_tools"
2✔
698
    default = False
2✔
699
    help = help_text(
2✔
700
        """
701
        Whether to include Pex tools in the PEX bootstrap code.
702

703
        With tools included, the generated PEX file can be executed with `PEX_TOOLS=1 <pex file> --help`
704
        to gain access to all the available tools.
705
        """
706
    )
707

708

709
class PexVenvSitePackagesCopies(BoolField):
2✔
710
    alias = "venv_site_packages_copies"
2✔
711
    default = False
2✔
712
    help = help_text(
2✔
713
        """
714
        If execution_mode is venv, populate the venv site packages using hard links or copies of resolved PEX dependencies instead of symlinks.
715

716
        This can be used to work around problems with tools or libraries that are confused by symlinked source files.
717
        """
718
    )
719

720

721
class PexVenvHermeticScripts(BoolField):
2✔
722
    alias = "venv_hermetic_scripts"
2✔
723
    default = True
2✔
724
    help = help_text(
2✔
725
        """
726
        If execution_mode is "venv", emit a hermetic venv `pex` script and hermetic console scripts.
727

728
        The venv `pex` script and the venv console scripts are constructed to be hermetic by
729
        default; Python is executed with `-sE` to restrict the `sys.path` to the PEX venv contents
730
        only. Setting this field to `False` elides the Python `-sE` restrictions and can be used to
731
        interoperate with frameworks that use `PYTHONPATH` manipulation to run code.
732
        """
733
    )
734

735

736
class PexScieField(StringField):
2✔
737
    alias = "scie"
2✔
738
    valid_choices = ("lazy", "eager")
2✔
739
    default = None
2✔
740
    help = help_text(
2✔
741
        """
742
        Create one or more native executable scies from your PEX that include
743
        a portable CPython interpreter along with your PEX making for a truly
744
        hermetic PEX that can run on machines with no Python installed at
745
        all. If your PEX has multiple targets then one PEX scie will be made
746
        for each platform, selecting the latest compatible portable CPython or
747
        PyPy interpreter as appropriate. Note that only Python>=3.8 is
748
        supported. If you'd like to explicitly control the target platforms or
749
        the exact portable CPython selected, see `scie_platform`,
750
        `scie_pbs_release` and `scie_python_version`.  Specifying `lazy` will
751
        fetch the portable CPython interpreter just in time on first boot of
752
        the PEX scie on a given machine if needed. Specifying `eager` will
753
        embed the portable CPython interpreter in your PEX scie making for a
754
        larger file, but requiring no internet access to boot. See
755
        https://science.scie.app for further details.
756

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

759
        NOTE: `pants run` will always run the "regular" PEX, use `package` to
760
        create scie PEXs.  """
761
    )
762

763

764
class ScieNameStyle(StrEnum):
2✔
765
    DYNAMIC = "dynamic"
2✔
766
    PLATFORM_PARENT_DIR = "platform-parent-dir"
2✔
767
    PLATFORM_FILE_SUFFIX = "platform-file-suffix"
2✔
768

769

770
class PexScieBindResourcePathField(StringSequenceField):
2✔
771
    alias = "scie_bind_resource_path"
2✔
772
    default = None
2✔
773
    help = help_text(
2✔
774
        """ Specifies an environment variable to bind the path of a resource
775
        in the PEX to in the form `<env var name>=<resource rel path>`. For
776
        example `WINDOWS_X64_CONSOLE_TRAMPOLINE=pex/windows/stubs/uv-
777
        trampoline-x86_64-console.exe` would lookup the path of the
778
        `pex/windows/stubs/uv-trampoline-x86_64-console.exe` file on the
779
        `sys.path` and bind its absolute path to the
780
        WINDOWS_X64_CONSOLE_TRAMPOLINE environment variable.  N.B.: resource
781
        paths must use the Unix path separator of `/`. These will be converted
782
        to the runtime host path separator as needed.
783
        """
784
    )
785

786

787
class PexScieExeField(StringSequenceField):
2✔
788
    alias = "scie_exe"
2✔
789
    default = None
2✔
790
    help = help_text(
2✔
791
        """
792
        Specify a custom PEX scie entry point instead of using
793
        the PEX's entrypoint. When specifying a custom entry
794
        point additional args can be set via `scie_args` and
795
        environment variables can be set via `scie_env`.
796
        Scie placeholders can be used in `scie_exe`.
797
        """
798
    )
799

800

801
class PexScieArgsField(StringSequenceField):
2✔
802
    alias = "scie_args"
2✔
803
    default = None
2✔
804
    help = help_text(
2✔
805
        """ Additional arguments to pass to the custom `scie_exe` entry
806
        point. Scie placeholders can be used in `scie_args`,
807
        """
808
    )
809

810

811
class PexScieEnvField(StringSequenceField):
2✔
812
    alias = "scie_env"
2✔
813
    default = None
2✔
814
    help = help_text(
2✔
815
        """
816
        Environment variables to set when executing the custom
817
        `scie_exe` entry point. Scie placeholders can be
818
        used in `scie_env`.
819
        """
820
    )
821

822

823
class PexScieLoadDotenvField(TriBoolField):
2✔
824
    alias = "scie_load_dotenv"
2✔
825
    required = False
2✔
826
    default = None
2✔
827
    help = help_text(
2✔
828
        """ Have the scie launcher load `.env` files and apply the loaded env
829
        vars to the PEX scie environment. See the 'load_dotenv' docs here for
830
        more on the `.env` loading specifics: https://github.com/a-
831
        scie/jump/blob/main/docs/packaging.md#optional-fields (Pex default:
832
        False) """
833
    )
834

835

836
class PexScieNameStyleField(StringField):
2✔
837
    alias = "scie_name_style"
2✔
838
    valid_choices = ScieNameStyle
2✔
839
    expected_type = str
2✔
840
    default = ScieNameStyle.DYNAMIC
2✔
841
    help = help_text(
2✔
842
        """
843
        Control how the output file translates to a scie name. By default
844
        (`dynamic`), the platform is used as a file suffix only when needed
845
        for disambiguation when targeting a local platform.  Specifying
846
        `platform-file-suffix` forces the scie target platform name to be
847
        added as a suffix of the output filename; Specifying
848
        `platform-parent-dir` places the scie in a sub- directory with the
849
        name of the platform it targets."""
850
    )
851

852

853
class PexScieBusyBox(StringField):
2✔
854
    alias = "scie_busybox"
2✔
855
    default = None
2✔
856
    help = help_text(
2✔
857
        """
858
        Make the PEX scie a BusyBox over the specified entry points. The entry
859
        points can either be console scripts or entry point specifiers. To
860
        select all console scripts in all distributions contained in the PEX,
861
        use `@`. To just pick all the console scripts from a particular
862
        project name's distributions in the PEX, use `@<project name>`; e.g.:
863
        `@ansible-core`. To exclude all the console scripts from a project,
864
        prefix with a `!`; e.g.: `@,!@ansible-core` selects all console
865
        scripts except those provided by the `ansible- core` project. To
866
        select an individual console script, just use its name or prefix the
867
        name with `!` to exclude that individual console script. To specify an
868
        arbitrary entry point in a module contained within one of the
869
        distributions in the PEX, use a string of the form
870
        `<name>=<module>(:<function>)`; e.g.: 'run- baz=foo.bar:baz' to
871
        execute the `baz` function in the `foo.bar` module as the entry point
872
        named `run-baz`.
873

874
        A BusyBox scie has no default entrypoint; instead, when run, it
875
        inspects argv0; if that matches one of its embedded entry points, it
876
        runs that entry point; if not, it lists all available entrypoints for
877
        you to pick from. To run a given entry point, you specify it as the
878
        first argument and all other arguments after that are forwarded to
879
        that entry point. BusyBox PEX scies allow you to install all their
880
        contained entry points into a given directory.  For more information,
881
        run `SCIE=help <your PEX scie>` and review the `install` command help.
882

883
        NOTE: This is only available for formal Python entry points
884
        <https://packaging.python.org/en/latest/specifications/entry-points/>
885
        and not the informal use by the `pex_binary` field `entry_point` to
886
        run first party files.
887
        """
888
    )
889

890

891
class PexSciePexEntrypointEnvPassthrough(TriBoolField):
2✔
892
    alias = "scie_pex_entrypoint_env_passthrough"
2✔
893
    required = False
2✔
894
    default = None
2✔
895
    help = help_text(
2✔
896
        """
897
        Allow overriding the primary entrypoint at runtime via
898
        PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
899
        when using --venv with a script entrypoint this adds
900
        modest startup overhead on the order of 10ms. Defaults
901
        to false for busybox scies and true for single
902
        entrypoint scies.
903
        """
904
    )
905

906

907
class PexSciePlatformField(StringSequenceField):
2✔
908
    alias = "scie_platform"
2✔
909
    valid_choices = (
2✔
910
        "current",
911
        "linux-aarch64",
912
        "linux-armv7l",
913
        "linux-powerpc64",
914
        "linux-riscv64",
915
        "linux-s390x",
916
        "linux-x86_64",
917
        "macos-aarch64",
918
        "macos-x86_64",
919
    )
920
    expected_type = str
2✔
921
    help = help_text(
2✔
922
        """ The platform to produce the native PEX scie executable for.  You
923
        can use a value of `current` to select the current platform. If left
924
        unspecified, the platforms implied by the targets selected to build
925
        the PEX with are used. Those targets are influenced by the current
926
        interpreter running Pex as well as use of `complete_platforms` and
927
        `interpreter_constraints`. Note that, in general, `scie_platform`
928
        should only be used to select a subset of the platforms implied by the
929
        targets selected via other options.  """
930
    )
931

932

933
class PexSciePbsReleaseField(StringField):
2✔
934
    alias = "scie_pbs_release"
2✔
935
    default = None
2✔
936
    help = help_text(
2✔
937
        """ The Python Standalone Builds release to use when a CPython
938
        interpreter distribution is needed for the PEX scie. Currently,
939
        releases are dates of the form YYYYMMDD, e.g.: '20240713'. See their
940
        GitHub releases page at
941
        <https://github.com/astral-sh/python-build-standalone/releases> to
942
        discover available releases. If left unspecified the latest release is
943
        used.
944
        """
945
    )
946

947

948
class PexSciePythonVersion(StringField):
2✔
949
    alias = "scie_python_version"
2✔
950
    default = None
2✔
951
    help = help_text(
2✔
952
        """ The portable CPython version to select. Can be either in
953
        `<major>.<minor>` form; e.g.: '3.11', or else fully specified as
954
        `<major>.<minor>.<patch>`; e.g.: '3.11.3'. If you don't specify this
955
        option, Pex will do its best to guess appropriate portable CPython
956
        versions. N.B.: Python Standalone Builds does not provide all patch
957
        versions; so you should check their releases at
958
        <https://github.com/astral-sh/python-build-standalone/releases> if you
959
        wish to pin down to the patch level.
960
        """
961
    )
962

963

964
class PexSciePbsFreeThreaded(TriBoolField):
2✔
965
    alias = "scie_pbs_free_threaded"
2✔
966
    default = None
2✔
967
    help = help_text(
2✔
968
        """
969
        Should the Python Standalone Builds CPython
970
        distributions be free-threaded. If left unspecified or
971
        otherwise turned off, creating a scie from a PEX with
972
        free-threaded abi wheels will automatically turn this
973
        option on. Note that this option is not compatible
974
        with `scie_pbs_stripped=True`. (Pex default: False)
975
        """
976
    )
977

978

979
class PexSciePbsDebug(TriBoolField):
2✔
980
    alias = "scie_pbs_debug"
2✔
981
    default = None
2✔
982
    help = help_text(
2✔
983
        """ Should the Python Standalone Builds CPython distributions be debug
984
        builds. Note that this option is not compatible with
985
        `scie_pbs_stripped=True`. (default: False) """
986
    )
987

988

989
class PexSciePbsStripped(TriBoolField):
2✔
990
    alias = "scie_pbs_stripped"
2✔
991
    required = False
2✔
992
    default = None
2✔
993
    help = help_text(
2✔
994
        """ Should the Python Standalone Builds CPython distributions used be
995
        stripped of debug symbols or not. For Linux and Windows particularly,
996
        the stripped distributions are less than half the size of the
997
        distributions that ship with debug symbols.  Note that this option is
998
        not compatible with `scie_pbs_free_threaded=True` or
999
        `scie_pbs_debug=True`. (Pex default: False) """
1000
    )
1001

1002

1003
class PexScieHashAlgField(StringField):
2✔
1004
    alias = "scie_hash_alg"
2✔
1005
    help = help_text(
2✔
1006
        """ Output a checksum file for each scie generated that is compatible
1007
        with the shasum family of tools. For each unique algorithm specified,
1008
        a sibling file to each scie executable will be generated with the same
1009
        stem as that scie file and hash algorithm name suffix.  The file will
1010
        contain the hex fingerprint of the scie executable using that
1011
        algorithm to hash it. Supported algorithms include at least md5, sha1,
1012
        sha256, sha384 and sha512. For the complete list of supported hash
1013
        algorithms, see the science tool --hash documentation here:
1014
        <https://science.scie.app/cli.html#science-lift-build>.  """
1015
    )
1016

1017

1018
_PEX_BINARY_COMMON_FIELDS = (
2✔
1019
    EnvironmentField,
1020
    InterpreterConstraintsField,
1021
    PythonResolveField,
1022
    PexBinaryDependenciesField,
1023
    PexCheckField,
1024
    PexCompletePlatformsField,
1025
    PexInheritPathField,
1026
    PexStripEnvField,
1027
    PexIgnoreErrorsField,
1028
    PexShBootField,
1029
    PexShebangField,
1030
    PexEmitWarningsField,
1031
    PexLayoutField,
1032
    PexExecutionModeField,
1033
    PexIncludeRequirementsField,
1034
    PexIncludeSourcesField,
1035
    PexIncludeToolsField,
1036
    PexVenvSitePackagesCopies,
1037
    PexVenvHermeticScripts,
1038
    PexExtraBuildArgsField,
1039
    RestartableField,
1040
)
1041

1042
_PEX_SCIE_BINARY_FIELDS = (
2✔
1043
    PexScieField,
1044
    PexScieBindResourcePathField,
1045
    PexScieExeField,
1046
    PexScieArgsField,
1047
    PexScieEnvField,
1048
    PexScieLoadDotenvField,
1049
    PexScieNameStyleField,
1050
    PexScieBusyBox,
1051
    PexSciePexEntrypointEnvPassthrough,
1052
    PexSciePlatformField,
1053
    PexSciePbsReleaseField,
1054
    PexSciePythonVersion,
1055
    PexSciePbsFreeThreaded,
1056
    PexSciePbsDebug,
1057
    PexSciePbsStripped,
1058
    PexScieHashAlgField,
1059
)
1060

1061

1062
class PexBinary(Target):
2✔
1063
    alias = "pex_binary"
2✔
1064
    core_fields = (
2✔
1065
        *COMMON_TARGET_FIELDS,
1066
        *_PEX_BINARY_COMMON_FIELDS,
1067
        *_PEX_SCIE_BINARY_FIELDS,
1068
        PexEntryPointField,
1069
        PexScriptField,
1070
        PexExecutableField,
1071
        PexArgsField,
1072
        PexEnvField,
1073
        OutputPathField,
1074
    )
1075
    help = help_text(
2✔
1076
        f"""
1077
        A Python target that can be converted into an executable PEX file.
1078

1079
        PEX files are self-contained executable files that contain a complete Python environment
1080
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
1081
        """
1082
    )
1083

1084
    def validate(self) -> None:
2✔
UNCOV
1085
        got_entry_point = self[PexEntryPointField].value is not None
×
UNCOV
1086
        got_script = self[PexScriptField].value is not None
×
UNCOV
1087
        got_executable = self[PexExecutableField].value is not None
×
1088

UNCOV
1089
        if (got_entry_point + got_script + got_executable) > 1:
×
UNCOV
1090
            raise InvalidTargetException(
×
1091
                softwrap(
1092
                    f"""
1093
                    The `{self.alias}` target {self.address} cannot set more than one of the
1094
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
1095
                    `{self[PexExecutableField].alias}` fields at the same time.
1096
                    To fix, please remove all but one.
1097
                    """
1098
                )
1099
            )
1100

1101

1102
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
2✔
1103
    alias = "entry_points"
2✔
1104
    default = None
2✔
1105
    help = help_text(
2✔
1106
        """
1107
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
1108

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

1113
        If you want the entry point to be for a third-party dependency or to use a console
1114
        script, use the `pex_binary` target directly.
1115
        """
1116
    )
1117

1118

1119
class PexBinariesOverrideField(OverridesField):
2✔
1120
    help = help_text(
2✔
1121
        f"""
1122
        Override the field values for generated `{PexBinary.alias}` targets.
1123

1124
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
1125
        their overrides. You may either use a single string or a tuple of strings to override
1126
        multiple targets.
1127

1128
        For example:
1129

1130
            overrides={{
1131
              "foo.py": {{"execution_mode": "venv"]}},
1132
              "bar.py:main": {{"restartable": True]}},
1133
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
1134
            }}
1135

1136
        Every key is validated to belong to this target's `entry_points` field.
1137

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

1142
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
1143
        same field more than one time for the `entry_point`.
1144
        """
1145
    )
1146

1147

1148
class PexBinariesGeneratorTarget(TargetGenerator):
2✔
1149
    alias = "pex_binaries"
2✔
1150
    help = help_text(
2✔
1151
        """
1152
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
1153

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

1158
        This target generator does not work well to generate `pex_binary` targets where the entry
1159
        point is for a third-party dependency. Dependency inference will not work for those, so
1160
        you will have to set lots of custom metadata for each binary; prefer an explicit
1161
        `pex_binary` target in that case. This target generator works best when the entry point
1162
        is a first-party file, like `app.py` or `app.py:main`.
1163
        """
1164
    )
1165
    generated_target_cls = PexBinary
2✔
1166
    core_fields = (
2✔
1167
        *COMMON_TARGET_FIELDS,
1168
        PexEntryPointsField,
1169
        PexBinariesOverrideField,
1170
    )
1171
    copied_fields = COMMON_TARGET_FIELDS
2✔
1172
    moved_fields = _PEX_BINARY_COMMON_FIELDS
2✔
1173

1174

1175
class PexBinaryDefaults(Subsystem):
2✔
1176
    options_scope = "pex-binary-defaults"
2✔
1177
    help = "Default settings for creating PEX executables."
2✔
1178

1179
    emit_warnings = BoolOption(
2✔
1180
        default=True,
1181
        help=softwrap(
1182
            """
1183
            Whether built PEX binaries should emit PEX warnings at runtime by default.
1184

1185
            Can be overridden by specifying the `emit_warnings` parameter of individual
1186
            `pex_binary` targets
1187
            """
1188
        ),
1189
        advanced=True,
1190
    )
1191

1192

1193
# -----------------------------------------------------------------------------------------------
1194
# `python_test` and `python_tests` targets
1195
# -----------------------------------------------------------------------------------------------
1196

1197

1198
class PythonTestSourceField(PythonSourceField):
2✔
1199
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
2✔
1200

1201
    def validate_resolved_files(self, files: Sequence[str]) -> None:
2✔
1202
        super().validate_resolved_files(files)
×
1203
        file = files[0]
×
1204
        file_name = os.path.basename(file)
×
1205
        if file_name == "conftest.py":
×
1206
            raise InvalidFieldException(
×
1207
                softwrap(
1208
                    f"""
1209
                    The {repr(self.alias)} field in target {self.address} should not be set to the
1210
                    file 'conftest.py', but was set to {repr(self.value)}.
1211

1212
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1213
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
1214
                    autogenerate a `python_test_utils` target.
1215
                    """
1216
                )
1217
            )
1218

1219

1220
class PythonTestsDependenciesField(PythonDependenciesField):
2✔
1221
    supports_transitive_excludes = True
2✔
1222

1223

1224
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
2✔
1225
    alias = "entry_point_dependencies"
2✔
1226
    help = help_text(
2✔
1227
        lambda: f"""
1228
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
1229

1230
        This is a dict where each key is a `{PythonDistribution.alias}` address
1231
        and the value is a list or tuple of entry point groups and/or entry points
1232
        on that target. The strings in the value list/tuple must be one of:
1233
        - "entry.point.group/entry-point-name" to depend on a named entry point
1234
        - "entry.point.group" (without a "/") to depend on an entry point group
1235
        - "*" to get all entry points on the target
1236

1237
        For example:
1238

1239
            {PythonTestsEntryPointDependenciesField.alias}={{
1240
                "//foo/address:dist_tgt": ["*"],  # all entry points
1241
                "bar:dist_tgt": ["console_scripts"],  # only from this group
1242
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
1243
                "another:dist_tgt": [  # multiple entry points
1244
                    "console_scripts/my-script",
1245
                    "console_scripts/another-script",
1246
                    "entry.point.group/entry-point-name",
1247
                    "other.group",
1248
                    "gui_scripts",
1249
                ],
1250
            }}
1251

1252
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
1253
        will be added as dependencies so that they are available on PYTHONPATH
1254
        during tests.
1255

1256
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
1257
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
1258
        `entry_points.txt` file will only include the entry points requested on this
1259
        field. This allows the tests, or the code under test, to lookup entry points'
1260
        metadata using an API like the `importlib.metadata.entry_points()` API in the
1261
        standard library (available on older Python interpreters via the
1262
        `importlib-metadata` distribution).
1263
        """
1264
    )
1265

1266

1267
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
1268
class PythonTestsTimeoutField(IntField):
2✔
1269
    alias = "timeout"
2✔
1270
    help = help_text(
2✔
1271
        """
1272
        A timeout (in seconds) used by each test file belonging to this target.
1273

1274
        If unset, will default to `[test].timeout_default`; if that option is also unset,
1275
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
1276
        applies if the option `--test-timeouts` is set to true (the default).
1277
        """
1278
    )
1279
    valid_numbers = ValidNumbers.positive_only
2✔
1280

1281
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
2✔
1282
        """Determine the timeout (in seconds) after resolving conflicting global options in the
1283
        `pytest` and `test` scopes.
1284

1285
        This function is deprecated and should be replaced by the similarly named one in
1286
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
1287
        """
1288

1289
        enabled = test.options.timeouts
×
1290
        timeout_default = test.options.timeout_default
×
1291
        timeout_maximum = test.options.timeout_maximum
×
1292

1293
        if not enabled:
×
1294
            return None
×
1295
        if self.value is None:
×
1296
            if timeout_default is None:
×
1297
                return None
×
1298
            result = cast(int, timeout_default)
×
1299
        else:
1300
            result = self.value
×
1301
        if timeout_maximum is not None:
×
1302
            return min(result, cast(int, timeout_maximum))
×
1303
        return result
×
1304

1305

1306
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
2✔
1307
    pass
2✔
1308

1309

1310
class PythonTestsXdistConcurrencyField(IntField):
2✔
1311
    alias = "xdist_concurrency"
2✔
1312
    help = help_text(
2✔
1313
        """
1314
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1315

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

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

1324
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1325
        """
1326
    )
1327

1328

1329
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
2✔
1330
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
2✔
1331

1332

1333
class SkipPythonTestsField(BoolField):
2✔
1334
    alias = "skip_tests"
2✔
1335
    default = False
2✔
1336
    help = "If true, don't run this target's tests."
2✔
1337

1338

1339
_PYTHON_TEST_MOVED_FIELDS = (
2✔
1340
    PythonTestsDependenciesField,
1341
    # This field is registered in the experimental backend for now.
1342
    # PythonTestsEntryPointDependenciesField,
1343
    PythonResolveField,
1344
    PythonRunGoalUseSandboxField,
1345
    PythonTestsTimeoutField,
1346
    PythonTestsXdistConcurrencyField,
1347
    PythonTestsBatchCompatibilityTagField,
1348
    RuntimePackageDependenciesField,
1349
    PythonTestsExtraEnvVarsField,
1350
    InterpreterConstraintsField,
1351
    SkipPythonTestsField,
1352
    EnvironmentField,
1353
)
1354

1355

1356
class PythonTestTarget(Target):
2✔
1357
    alias = "python_test"
2✔
1358
    core_fields = (
2✔
1359
        *COMMON_TARGET_FIELDS,
1360
        *_PYTHON_TEST_MOVED_FIELDS,
1361
        PythonTestsDependenciesField,
1362
        PythonTestSourceField,
1363
    )
1364
    help = help_text(
2✔
1365
        f"""
1366
        A single Python test file, written in either Pytest style or unittest style.
1367

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

1372
        See {doc_url("docs/python/goals/test")}
1373
        """
1374
    )
1375

1376

1377
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1378
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
2✔
1379
    default = ("test_*.py", "*_test.py", "tests.py")
2✔
1380
    help = generate_multiple_sources_field_help_message(
2✔
1381
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1382
    )
1383

1384
    def validate_resolved_files(self, files: Sequence[str]) -> None:
2✔
1385
        super().validate_resolved_files(files)
×
1386
        # We don't technically need to error for `conftest.py` here because `PythonTestSourceField`
1387
        # already validates this, but we get a better error message this way so that users don't
1388
        # have to reason about generated targets.
1389
        conftest_files = [fp for fp in files if os.path.basename(fp) == "conftest.py"]
×
1390
        if conftest_files:
×
1391
            raise InvalidFieldException(
×
1392
                softwrap(
1393
                    f"""
1394
                    The {repr(self.alias)} field in target {self.address} should not include the
1395
                    file 'conftest.py', but included these: {conftest_files}.
1396

1397
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1398
                    You can run `{bin_name()} tailor` after removing the files from the
1399
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1400
                    `python_test_utils` target.
1401
                    """
1402
                )
1403
            )
1404

1405

1406
class PythonTestsOverrideField(OverridesField):
2✔
1407
    help = generate_file_based_overrides_field_help_message(
2✔
1408
        PythonTestTarget.alias,
1409
        """
1410
        overrides={
1411
            "foo_test.py": {"timeout": 120},
1412
            "bar_test.py": {"timeout": 200},
1413
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1414
        }
1415
        """,
1416
    )
1417

1418

1419
class PythonTestsGeneratorTarget(TargetFilesGenerator):
2✔
1420
    alias = "python_tests"
2✔
1421
    core_fields = (
2✔
1422
        *COMMON_TARGET_FIELDS,
1423
        PythonTestsGeneratingSourcesField,
1424
        PythonTestsOverrideField,
1425
    )
1426
    generated_target_cls = PythonTestTarget
2✔
1427
    copied_fields = COMMON_TARGET_FIELDS
2✔
1428
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
2✔
1429
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1430
    help = "Generate a `python_test` target for each file in the `sources` field."
2✔
1431

1432

1433
# -----------------------------------------------------------------------------------------------
1434
# `python_source`, `python_sources`, and `python_test_utils` targets
1435
# -----------------------------------------------------------------------------------------------
1436

1437

1438
class PythonSourceTarget(Target):
2✔
1439
    alias = "python_source"
2✔
1440
    core_fields = (
2✔
1441
        *COMMON_TARGET_FIELDS,
1442
        InterpreterConstraintsField,
1443
        PythonDependenciesField,
1444
        PythonResolveField,
1445
        PythonRunGoalUseSandboxField,
1446
        PythonSourceField,
1447
        RestartableField,
1448
    )
1449
    help = "A single Python source file."
2✔
1450

1451

1452
class PythonSourcesOverridesField(OverridesField):
2✔
1453
    help = generate_file_based_overrides_field_help_message(
2✔
1454
        PythonSourceTarget.alias,
1455
        """
1456
        overrides={
1457
            "foo.py": {"skip_pylint": True]},
1458
            "bar.py": {"skip_flake8": True]},
1459
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1460
        }"
1461
        """,
1462
    )
1463

1464

1465
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1466
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
2✔
1467
    help = generate_multiple_sources_field_help_message(
2✔
1468
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1469
    )
1470

1471

1472
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1473
    default = (
2✔
1474
        ("*.py", "*.pyi")
1475
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1476
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1477
    )
1478
    help = generate_multiple_sources_field_help_message(
2✔
1479
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1480
    )
1481

1482

1483
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
2✔
1484
    alias = "python_test_utils"
2✔
1485
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1486
    core_fields = (
2✔
1487
        *COMMON_TARGET_FIELDS,
1488
        PythonTestUtilsGeneratingSourcesField,
1489
        PythonSourcesOverridesField,
1490
    )
1491
    generated_target_cls = PythonSourceTarget
2✔
1492
    copied_fields = COMMON_TARGET_FIELDS
2✔
1493
    moved_fields = (
2✔
1494
        PythonResolveField,
1495
        PythonRunGoalUseSandboxField,
1496
        PythonDependenciesField,
1497
        InterpreterConstraintsField,
1498
    )
1499
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1500
    help = help_text(
2✔
1501
        """
1502
        Generate a `python_source` target for each file in the `sources` field.
1503

1504
        This target generator is intended for test utility files like `conftest.py` or
1505
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1506
        way as the `python_sources` target generator does, only that the `sources` field has a
1507
        different default. So it is valid to use `python_sources` instead. However, this target
1508
        can be helpful to better model your code by keeping separate test support files vs.
1509
        production files.
1510
        """
1511
    )
1512

1513

1514
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
2✔
1515
    alias = "python_sources"
2✔
1516
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1517
    core_fields = (
2✔
1518
        *COMMON_TARGET_FIELDS,
1519
        PythonSourcesGeneratingSourcesField,
1520
        PythonSourcesOverridesField,
1521
    )
1522
    generated_target_cls = PythonSourceTarget
2✔
1523
    copied_fields = COMMON_TARGET_FIELDS
2✔
1524
    moved_fields = (
2✔
1525
        PythonResolveField,
1526
        PythonRunGoalUseSandboxField,
1527
        PythonDependenciesField,
1528
        InterpreterConstraintsField,
1529
        RestartableField,
1530
    )
1531
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1532
    help = help_text(
2✔
1533
        """
1534
        Generate a `python_source` target for each file in the `sources` field.
1535

1536
        You can either use this target generator or `python_test_utils` for test utility files
1537
        like `conftest.py`. They behave identically, but can help to better model and keep
1538
        separate test support files vs. production files.
1539
        """
1540
    )
1541

1542

1543
# -----------------------------------------------------------------------------------------------
1544
# `python_requirement` target
1545
# -----------------------------------------------------------------------------------------------
1546

1547

1548
class _PipRequirementSequenceField(Field):
2✔
1549
    value: tuple[PipRequirement, ...]
2✔
1550

1551
    @classmethod
2✔
1552
    def compute_value(
2✔
1553
        cls, raw_value: Iterable[str] | None, address: Address
1554
    ) -> tuple[PipRequirement, ...]:
UNCOV
1555
        value = super().compute_value(raw_value, address)
×
UNCOV
1556
        if value is None:
×
1557
            return ()
×
UNCOV
1558
        invalid_type_error = InvalidFieldTypeException(
×
1559
            address,
1560
            cls.alias,
1561
            value,
1562
            expected_type="an iterable of pip-style requirement strings (e.g. a list)",
1563
        )
UNCOV
1564
        if isinstance(value, str) or not isinstance(value, collections.abc.Iterable):
×
UNCOV
1565
            raise invalid_type_error
×
UNCOV
1566
        result = []
×
UNCOV
1567
        for v in value:
×
1568
            # We allow passing a pre-parsed `PipRequirement`. This is intended for macros which
1569
            # might have already parsed so that we can avoid parsing multiple times.
UNCOV
1570
            if isinstance(v, PipRequirement):
×
UNCOV
1571
                result.append(v)
×
UNCOV
1572
            elif isinstance(v, str):
×
UNCOV
1573
                try:
×
UNCOV
1574
                    parsed = PipRequirement.parse(
×
1575
                        v, description_of_origin=f"the '{cls.alias}' field for the target {address}"
1576
                    )
UNCOV
1577
                except ValueError as e:
×
UNCOV
1578
                    raise InvalidFieldException(e)
×
UNCOV
1579
                result.append(parsed)
×
1580
            else:
UNCOV
1581
                raise invalid_type_error
×
UNCOV
1582
        return tuple(result)
×
1583

1584

1585
class PythonRequirementDependenciesField(Dependencies):
2✔
1586
    pass
2✔
1587

1588

1589
class PythonRequirementsField(_PipRequirementSequenceField):
2✔
1590
    alias = "requirements"
2✔
1591
    required = True
2✔
1592
    help = help_text(
2✔
1593
        """
1594
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1595

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

1600
        If the requirement depends on some other requirement to work, such as needing
1601
        `setuptools` to be built, use the `dependencies` field instead.
1602
        """
1603
    )
1604

1605

1606
_default_module_mapping_url = git_url(
2✔
1607
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1608
)
1609

1610

1611
class PythonRequirementModulesField(StringSequenceField):
2✔
1612
    alias = "modules"
2✔
1613
    help = help_text(
2✔
1614
        f"""
1615
        The modules this requirement provides (used for dependency inference).
1616

1617
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1618
        "easy_install"]`.
1619

1620
        Usually you can leave this field off. If unspecified, Pants will first look at the
1621
        default module mapping ({_default_module_mapping_url}), and then will default to
1622
        the normalized project name. For example, the requirement `Django` would default to
1623
        the module `django`.
1624

1625
        Mutually exclusive with the `type_stub_modules` field.
1626
        """
1627
    )
1628

1629

1630
class PythonRequirementTypeStubModulesField(StringSequenceField):
2✔
1631
    alias = "type_stub_modules"
2✔
1632
    help = help_text(
2✔
1633
        f"""
1634
        The modules this requirement provides if the requirement is a type stub (used for
1635
        dependency inference).
1636

1637
        For example, the requirement `types-requests` provides `["requests"]`.
1638

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

1646
        Mutually exclusive with the `modules` field.
1647
        """
1648
    )
1649

1650

1651
def normalize_module_mapping(
2✔
1652
    mapping: Mapping[str, Iterable[str]] | None,
1653
) -> FrozenDict[str, tuple[str, ...]]:
UNCOV
1654
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
×
1655

1656

1657
class PythonRequirementResolveField(PythonResolveField):
2✔
1658
    alias = "resolve"
2✔
1659
    required = False
2✔
1660
    help = help_text(
2✔
1661
        """
1662
        The resolve from `[python].resolves` that this requirement is included in.
1663

1664
        If not defined, will default to `[python].default_resolve`.
1665

1666
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1667
        it will include all requirements that are declared with that resolve.
1668
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1669
        they use via their `resolve` field; so, for your first-party code to use a
1670
        particular `python_requirement` target, that requirement must be included in the resolve
1671
        used by that code.
1672
        """
1673
    )
1674

1675

1676
class PythonRequirementFindLinksField(StringSequenceField):
2✔
1677
    # NB: This is solely used for `pants_requirements` target generation
1678
    alias = "_find_links"
2✔
1679
    required = False
2✔
1680
    default = ()
2✔
1681
    help = "<Internal>"
2✔
1682

1683

1684
class PythonRequirementEntryPointField(EntryPointField):
2✔
1685
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1686
    pass
2✔
1687

1688

1689
class PythonRequirementTarget(Target):
2✔
1690
    alias = "python_requirement"
2✔
1691
    core_fields = (
2✔
1692
        *COMMON_TARGET_FIELDS,
1693
        PythonRequirementsField,
1694
        PythonRequirementDependenciesField,
1695
        PythonRequirementModulesField,
1696
        PythonRequirementTypeStubModulesField,
1697
        PythonRequirementResolveField,
1698
        PythonRequirementEntryPointField,
1699
        PythonRequirementFindLinksField,
1700
    )
1701
    help = help_text(
2✔
1702
        f"""
1703
        A Python requirement installable by pip.
1704

1705
        This target is useful when you want to declare Python requirements inline in a
1706
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1707
        the target generator `python_requirements` to convert each
1708
        requirement into a `python_requirement` target automatically. For Poetry, use
1709
        `poetry_requirements`.
1710

1711
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1712
        """
1713
    )
1714

1715
    def validate(self) -> None:
2✔
UNCOV
1716
        if (
×
1717
            self[PythonRequirementModulesField].value
1718
            and self[PythonRequirementTypeStubModulesField].value
1719
        ):
1720
            raise InvalidTargetException(
×
1721
                softwrap(
1722
                    f"""
1723
                    The `{self.alias}` target {self.address} cannot set both the
1724
                    `{self[PythonRequirementModulesField].alias}` and
1725
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1726
                    To fix, please remove one.
1727
                    """
1728
                )
1729
            )
1730

1731

1732
# -----------------------------------------------------------------------------------------------
1733
# `python_distribution` target
1734
# -----------------------------------------------------------------------------------------------
1735

1736

1737
# See `target_types_rules.py` for a dependency injection rule.
1738
class PythonDistributionDependenciesField(Dependencies):
2✔
1739
    supports_transitive_excludes = True
2✔
1740

1741

1742
class PythonProvidesField(ScalarField, AsyncFieldMixin):
2✔
1743
    alias = "provides"
2✔
1744
    expected_type = PythonArtifact
2✔
1745
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
2✔
1746
    value: PythonArtifact
2✔
1747
    required = True
2✔
1748
    help = help_text(
2✔
1749
        f"""
1750
        The setup.py kwargs for the external artifact built from this target.
1751

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

1756
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1757
        """
1758
    )
1759

1760
    @classmethod
2✔
1761
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
2✔
UNCOV
1762
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1763

1764

1765
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
2✔
1766
    alias = "entry_points"
2✔
1767
    required = False
2✔
1768
    help = help_text(
2✔
1769
        f"""
1770
        Any entry points, such as `console_scripts` and `gui_scripts`.
1771

1772
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1773
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1774
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1775
        `pex_binary` target.
1776

1777
        Example:
1778

1779
            entry_points={{
1780
              "console_scripts": {{
1781
                "my-script": "project.app:main",
1782
                "another-script": "project/subdir:pex_binary_tgt"
1783
              }}
1784
            }}
1785

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

1792
        Pants will attempt to infer dependencies, which you can confirm by running:
1793

1794
            {bin_name()} dependencies <python_distribution target address>
1795
        """
1796
    )
1797

1798

1799
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
2✔
1800
    help = help_text(
2✔
1801
        """
1802
        The path to the directory to write the distribution file to, relative the dist directory.
1803

1804
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1805
        level of the dist dir.
1806
        """
1807
    )
1808
    alias = "output_path"
2✔
1809
    default = ""
2✔
1810

1811

1812
@dataclass(frozen=True)
2✔
1813
class PythonDistributionEntryPoint:
2✔
1814
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1815

1816
    entry_point: EntryPoint
2✔
1817
    pex_binary_address: Address | None
2✔
1818

1819

1820
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1821
@dataclass(frozen=True)
2✔
1822
class ResolvedPythonDistributionEntryPoints:
2✔
1823
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1824
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
2✔
1825

1826
    @property
2✔
1827
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
2✔
1828
        """Filters out all entry points from pex binary targets."""
1829
        return FrozenDict(
×
1830
            {
1831
                category: FrozenDict(
1832
                    {
1833
                        ep_name: ep_val.entry_point
1834
                        for ep_name, ep_val in entry_points.items()
1835
                        if not ep_val.pex_binary_address
1836
                    }
1837
                )
1838
                for category, entry_points in self.val.items()
1839
            }
1840
        )
1841

1842
    @property
2✔
1843
    def pex_binary_addresses(self) -> Addresses:
2✔
1844
        """Returns the addresses to all pex binary targets owning entry points used."""
1845
        return Addresses(
×
1846
            ep_val.pex_binary_address
1847
            for category, entry_points in self.val.items()
1848
            for ep_val in entry_points.values()
1849
            if ep_val.pex_binary_address
1850
        )
1851

1852

1853
@dataclass(frozen=True)
2✔
1854
class ResolvePythonDistributionEntryPointsRequest:
2✔
1855
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1856
    that should be resolved into a setuptools entry point.
1857

1858
    If the `entry_points_field` is present, inspect the specified entry points.
1859
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1860

1861
    This is to support inspecting one or the other depending on use case, using the same
1862
    logic for resolving pex_binary addresses etc.
1863
    """
1864

1865
    entry_points_field: PythonDistributionEntryPointsField | None = None
2✔
1866
    provides_field: PythonProvidesField | None = None
2✔
1867

1868
    def __post_init__(self):
2✔
1869
        # Must provide at least one of these fields.
UNCOV
1870
        assert self.entry_points_field or self.provides_field
×
1871

1872

1873
class WheelField(BoolField):
2✔
1874
    alias = "wheel"
2✔
1875
    default = True
2✔
1876
    help = "Whether to build a wheel for the distribution."
2✔
1877

1878

1879
class SDistField(BoolField):
2✔
1880
    alias = "sdist"
2✔
1881
    default = True
2✔
1882
    help = "Whether to build an sdist for the distribution."
2✔
1883

1884

1885
class ConfigSettingsField(DictStringToStringSequenceField):
2✔
1886
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1887

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

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

1893
    - Build frontends should support string values, and may also support other mechanisms
1894
      (apparently meaning other types).
1895

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

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

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

1911

1912
class WheelConfigSettingsField(ConfigSettingsField):
2✔
1913
    alias = "wheel_config_settings"
2✔
1914
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
2✔
1915

1916

1917
class SDistConfigSettingsField(ConfigSettingsField):
2✔
1918
    alias = "sdist_config_settings"
2✔
1919
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
2✔
1920

1921

1922
class BuildBackendEnvVarsField(StringSequenceField):
2✔
1923
    alias = "env_vars"
2✔
1924
    required = False
2✔
1925
    help = help_text(
2✔
1926
        """
1927
        Environment variables to set when running the PEP-517 build backend.
1928

1929
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1930
        or just `ENV_VAR` to copy the value from Pants's own environment.
1931
        """
1932
    )
1933

1934

1935
class GenerateSetupField(TriBoolField):
2✔
1936
    alias = "generate_setup"
2✔
1937
    required = False
2✔
1938
    # The default behavior if this field is unspecified is controlled by the
1939
    # --generate-setup-default option in the setup-py-generation scope.
1940
    default = None
2✔
1941

1942
    help = help_text(
2✔
1943
        """
1944
        Whether to generate setup information for this distribution, based on analyzing
1945
        sources and dependencies. Set to False to use existing setup information, such as
1946
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1947
        """
1948
    )
1949

1950

1951
class LongDescriptionPathField(StringField):
2✔
1952
    alias = "long_description_path"
2✔
1953
    required = False
2✔
1954

1955
    help = help_text(
2✔
1956
        """
1957
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1958

1959
        Path is relative to the build root.
1960

1961
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1962

1963
        This field won't automatically set `long_description_content_type` field for you.
1964
        You have to specify this field yourself in the `provides` field.
1965
        """
1966
    )
1967

1968

1969
class PythonDistribution(Target):
2✔
1970
    alias: ClassVar[str] = "python_distribution"
2✔
1971
    core_fields = (
2✔
1972
        *COMMON_TARGET_FIELDS,
1973
        InterpreterConstraintsField,
1974
        PythonDistributionDependenciesField,
1975
        PythonDistributionEntryPointsField,
1976
        PythonProvidesField,
1977
        GenerateSetupField,
1978
        WheelField,
1979
        SDistField,
1980
        WheelConfigSettingsField,
1981
        SDistConfigSettingsField,
1982
        BuildBackendEnvVarsField,
1983
        LongDescriptionPathField,
1984
        PythonDistributionOutputPathField,
1985
    )
1986
    help = help_text(
2✔
1987
        f"""
1988
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
1989

1990
        See {doc_url("docs/python/overview/building-distributions")}.
1991
        """
1992
    )
1993

1994

1995
# -----------------------------------------------------------------------------------------------
1996
# `vcs_version` target
1997
# -----------------------------------------------------------------------------------------------
1998

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

2006

2007
class VCSVersionDummySourceField(OptionalSingleSourceField):
2✔
2008
    """A dummy SourceField for participation in the codegen machinery."""
2009

2010
    alias = "_dummy_source"  # Leading underscore omits the field from help.
2✔
2011
    help = "A version string generated from VCS information"
2✔
2012

2013

2014
class VersionTagRegexField(StringField):
2✔
2015
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
2✔
2016
    alias = "tag_regex"
2✔
2017
    help = help_text(
2✔
2018
        """
2019
        A Python regex string to extract the version string from a VCS tag.
2020

2021
        The regex needs to contain either a single match group, or a group named version,
2022
        that captures the actual version information.
2023

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

2026
        See https://github.com/pypa/setuptools_scm for implementation details.
2027
        """
2028
    )
2029

2030

2031
class VersionGenerateToField(StringField):
2✔
2032
    required = True
2✔
2033
    alias = "generate_to"
2✔
2034
    help = help_text(
2✔
2035
        """
2036
        Generate the version data to this relative path, using the template field.
2037

2038
        Note that the generated output will not be written to disk in the source tree, but
2039
        will be available as a generated dependency to code that depends on this target.
2040
        """
2041
    )
2042

2043

2044
class VersionTemplateField(StringField):
2✔
2045
    required = True
2✔
2046
    alias = "template"
2✔
2047
    help = help_text(
2✔
2048
        """
2049
        Generate the version data using this format string, which takes a version format kwarg.
2050

2051
        E.g., `'version = "{version}"'`
2052
        """
2053
    )
2054

2055

2056
class VersionVersionSchemeField(StringField):
2✔
2057
    alias = "version_scheme"
2✔
2058
    help = help_text(
2✔
2059
        """
2060
        The version scheme to configure `setuptools_scm` to use.
2061
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
2062
        """
2063
    )
2064

2065

2066
class VersionLocalSchemeField(StringField):
2✔
2067
    alias = "local_scheme"
2✔
2068
    help = help_text(
2✔
2069
        """
2070
        The local scheme to configure `setuptools_scm` to use.
2071
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
2072
        """
2073
    )
2074

2075

2076
class VCSVersion(Target):
2✔
2077
    alias = "vcs_version"
2✔
2078
    core_fields = (
2✔
2079
        *COMMON_TARGET_FIELDS,
2080
        VersionTagRegexField,
2081
        VersionVersionSchemeField,
2082
        VersionLocalSchemeField,
2083
        VCSVersionDummySourceField,
2084
        VersionGenerateToField,
2085
        VersionTemplateField,
2086
    )
2087
    help = help_text(
2✔
2088
        f"""
2089
        Generates a version string from VCS state.
2090

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

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

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