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

pantsbuild / pants / 20974506033

13 Jan 2026 10:14PM UTC coverage: 43.251% (-37.0%) from 80.269%
20974506033

Pull #22976

github

web-flow
Merge a16a40040 into c12556724
Pull Request #22976: WIP: Add the ability to set stdin for a Process

2 of 4 new or added lines in 2 files covered. (50.0%)

17213 existing lines in 540 files now uncovered.

26146 of 60452 relevant lines covered (43.25%)

0.86 hits per line

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

81.09
/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 PexScieLoadDotenvField(TriBoolField):
2✔
771
    alias = "scie_load_dotenv"
2✔
772
    required = False
2✔
773
    default = None
2✔
774
    help = help_text(
2✔
775
        """ Have the scie launcher load `.env` files and apply the loaded env
776
        vars to the PEX scie environment. See the 'load_dotenv' docs here for
777
        more on the `.env` loading specifics: https://github.com/a-
778
        scie/jump/blob/main/docs/packaging.md#optional-fields (Pex default:
779
        False) """
780
    )
781

782

783
class PexScieNameStyleField(StringField):
2✔
784
    alias = "scie_name_style"
2✔
785
    valid_choices = ScieNameStyle
2✔
786
    expected_type = str
2✔
787
    default = ScieNameStyle.DYNAMIC
2✔
788
    help = help_text(
2✔
789
        """
790
        Control how the output file translates to a scie name. By default
791
        (`dynamic`), the platform is used as a file suffix only when needed
792
        for disambiguation when targeting a local platform.  Specifying
793
        `platform-file-suffix` forces the scie target platform name to be
794
        added as a suffix of the output filename; Specifying
795
        `platform-parent-dir` places the scie in a sub- directory with the
796
        name of the platform it targets."""
797
    )
798

799

800
class PexScieBusyBox(StringField):
2✔
801
    alias = "scie_busybox"
2✔
802
    default = None
2✔
803
    help = help_text(
2✔
804
        """
805
        Make the PEX scie a BusyBox over the specified entry points. The entry
806
        points can either be console scripts or entry point specifiers. To
807
        select all console scripts in all distributions contained in the PEX,
808
        use `@`. To just pick all the console scripts from a particular
809
        project name's distributions in the PEX, use `@<project name>`; e.g.:
810
        `@ansible-core`. To exclude all the console scripts from a project,
811
        prefix with a `!`; e.g.: `@,!@ansible-core` selects all console
812
        scripts except those provided by the `ansible- core` project. To
813
        select an individual console script, just use its name or prefix the
814
        name with `!` to exclude that individual console script. To specify an
815
        arbitrary entry point in a module contained within one of the
816
        distributions in the PEX, use a string of the form
817
        `<name>=<module>(:<function>)`; e.g.: 'run- baz=foo.bar:baz' to
818
        execute the `baz` function in the `foo.bar` module as the entry point
819
        named `run-baz`.
820

821
        A BusyBox scie has no default entrypoint; instead, when run, it
822
        inspects argv0; if that matches one of its embedded entry points, it
823
        runs that entry point; if not, it lists all available entrypoints for
824
        you to pick from. To run a given entry point, you specify it as the
825
        first argument and all other arguments after that are forwarded to
826
        that entry point. BusyBox PEX scies allow you to install all their
827
        contained entry points into a given directory.  For more information,
828
        run `SCIE=help <your PEX scie>` and review the `install` command help.
829

830
        NOTE: This is only available for formal Python entry points
831
        <https://packaging.python.org/en/latest/specifications/entry-points/>
832
        and not the informal use by the `pex_binary` field `entry_point` to
833
        run first party files.
834
        """
835
    )
836

837

838
class PexSciePexEntrypointEnvPassthrough(TriBoolField):
2✔
839
    alias = "scie_pex_entrypoint_env_passthrough"
2✔
840
    required = False
2✔
841
    default = None
2✔
842
    help = help_text(
2✔
843
        """
844
        Allow overriding the primary entrypoint at runtime via
845
        PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
846
        when using --venv with a script entrypoint this adds
847
        modest startup overhead on the order of 10ms. Defaults
848
        to false for busybox scies and true for single
849
        entrypoint scies.
850
        """
851
    )
852

853

854
class PexSciePlatformField(StringSequenceField):
2✔
855
    alias = "scie_platform"
2✔
856
    valid_choices = (
2✔
857
        "current",
858
        "linux-aarch64",
859
        "linux-armv7l",
860
        "linux-powerpc64",
861
        "linux-riscv64",
862
        "linux-s390x",
863
        "linux-x86_64",
864
        "macos-aarch64",
865
        "macos-x86_64",
866
    )
867
    expected_type = str
2✔
868
    help = help_text(
2✔
869
        """ The platform to produce the native PEX scie executable for.  You
870
        can use a value of `current` to select the current platform. If left
871
        unspecified, the platforms implied by the targets selected to build
872
        the PEX with are used. Those targets are influenced by the current
873
        interpreter running Pex as well as use of `complete_platforms` and
874
        `interpreter_constraints`. Note that, in general, `scie_platform`
875
        should only be used to select a subset of the platforms implied by the
876
        targets selected via other options.  """
877
    )
878

879

880
class PexSciePbsReleaseField(StringField):
2✔
881
    alias = "scie_pbs_release"
2✔
882
    default = None
2✔
883
    help = help_text(
2✔
884
        """ The Python Standalone Builds release to use when a CPython
885
        interpreter distribution is needed for the PEX scie. Currently,
886
        releases are dates of the form YYYYMMDD, e.g.: '20240713'. See their
887
        GitHub releases page at
888
        <https://github.com/astral-sh/python-build-standalone/releases> to
889
        discover available releases. If left unspecified the latest release is
890
        used.
891
        """
892
    )
893

894

895
class PexSciePythonVersion(StringField):
2✔
896
    alias = "scie_python_version"
2✔
897
    default = None
2✔
898
    help = help_text(
2✔
899
        """ The portable CPython version to select. Can be either in
900
        `<major>.<minor>` form; e.g.: '3.11', or else fully specified as
901
        `<major>.<minor>.<patch>`; e.g.: '3.11.3'. If you don't specify this
902
        option, Pex will do its best to guess appropriate portable CPython
903
        versions. N.B.: Python Standalone Builds does not provide all patch
904
        versions; so you should check their releases at
905
        <https://github.com/astral-sh/python-build-standalone/releases> if you
906
        wish to pin down to the patch level.
907
        """
908
    )
909

910

911
class PexSciePbsFreeThreaded(TriBoolField):
2✔
912
    alias = "scie_pbs_free_threaded"
2✔
913
    default = None
2✔
914
    help = help_text(
2✔
915
        """
916
        Should the Python Standalone Builds CPython
917
        distributions be free-threaded. If left unspecified or
918
        otherwise turned off, creating a scie from a PEX with
919
        free-threaded abi wheels will automatically turn this
920
        option on. Note that this option is not compatible
921
        with `scie_pbs_stripped=True`. (Pex default: False)
922
        """
923
    )
924

925

926
class PexSciePbsDebug(TriBoolField):
2✔
927
    alias = "scie_pbs_debug"
2✔
928
    default = None
2✔
929
    help = help_text(
2✔
930
        """ Should the Python Standalone Builds CPython distributions be debug
931
        builds. Note that this option is not compatible with
932
        `scie_pbs_stripped=True`. (default: False) """
933
    )
934

935

936
class PexSciePbsStripped(TriBoolField):
2✔
937
    alias = "scie_pbs_stripped"
2✔
938
    required = False
2✔
939
    default = None
2✔
940
    help = help_text(
2✔
941
        """ Should the Python Standalone Builds CPython distributions used be
942
        stripped of debug symbols or not. For Linux and Windows particularly,
943
        the stripped distributions are less than half the size of the
944
        distributions that ship with debug symbols.  Note that this option is
945
        not compatible with `scie_pbs_free_threaded=True` or
946
        `scie_pbs_debug=True`. (Pex default: False) """
947
    )
948

949

950
class PexScieHashAlgField(StringField):
2✔
951
    alias = "scie_hash_alg"
2✔
952
    help = help_text(
2✔
953
        """ Output a checksum file for each scie generated that is compatible
954
        with the shasum family of tools. For each unique algorithm specified,
955
        a sibling file to each scie executable will be generated with the same
956
        stem as that scie file and hash algorithm name suffix.  The file will
957
        contain the hex fingerprint of the scie executable using that
958
        algorithm to hash it. Supported algorithms include at least md5, sha1,
959
        sha256, sha384 and sha512. For the complete list of supported hash
960
        algorithms, see the science tool --hash documentation here:
961
        <https://science.scie.app/cli.html#science-lift-build>.  """
962
    )
963

964

965
_PEX_BINARY_COMMON_FIELDS = (
2✔
966
    EnvironmentField,
967
    InterpreterConstraintsField,
968
    PythonResolveField,
969
    PexBinaryDependenciesField,
970
    PexCheckField,
971
    PexCompletePlatformsField,
972
    PexInheritPathField,
973
    PexStripEnvField,
974
    PexIgnoreErrorsField,
975
    PexShBootField,
976
    PexShebangField,
977
    PexEmitWarningsField,
978
    PexLayoutField,
979
    PexExecutionModeField,
980
    PexIncludeRequirementsField,
981
    PexIncludeSourcesField,
982
    PexIncludeToolsField,
983
    PexVenvSitePackagesCopies,
984
    PexVenvHermeticScripts,
985
    PexExtraBuildArgsField,
986
    RestartableField,
987
)
988

989
_PEX_SCIE_BINARY_FIELDS = (
2✔
990
    PexScieField,
991
    PexScieLoadDotenvField,
992
    PexScieNameStyleField,
993
    PexScieBusyBox,
994
    PexSciePexEntrypointEnvPassthrough,
995
    PexSciePlatformField,
996
    PexSciePbsReleaseField,
997
    PexSciePythonVersion,
998
    PexSciePbsFreeThreaded,
999
    PexSciePbsDebug,
1000
    PexSciePbsStripped,
1001
    PexScieHashAlgField,
1002
)
1003

1004

1005
class PexBinary(Target):
2✔
1006
    alias = "pex_binary"
2✔
1007
    core_fields = (
2✔
1008
        *COMMON_TARGET_FIELDS,
1009
        *_PEX_BINARY_COMMON_FIELDS,
1010
        *_PEX_SCIE_BINARY_FIELDS,
1011
        PexEntryPointField,
1012
        PexScriptField,
1013
        PexExecutableField,
1014
        PexArgsField,
1015
        PexEnvField,
1016
        OutputPathField,
1017
    )
1018
    help = help_text(
2✔
1019
        f"""
1020
        A Python target that can be converted into an executable PEX file.
1021

1022
        PEX files are self-contained executable files that contain a complete Python environment
1023
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
1024
        """
1025
    )
1026

1027
    def validate(self) -> None:
2✔
UNCOV
1028
        got_entry_point = self[PexEntryPointField].value is not None
×
UNCOV
1029
        got_script = self[PexScriptField].value is not None
×
UNCOV
1030
        got_executable = self[PexExecutableField].value is not None
×
1031

UNCOV
1032
        if (got_entry_point + got_script + got_executable) > 1:
×
UNCOV
1033
            raise InvalidTargetException(
×
1034
                softwrap(
1035
                    f"""
1036
                    The `{self.alias}` target {self.address} cannot set more than one of the
1037
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
1038
                    `{self[PexExecutableField].alias}` fields at the same time.
1039
                    To fix, please remove all but one.
1040
                    """
1041
                )
1042
            )
1043

1044

1045
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
2✔
1046
    alias = "entry_points"
2✔
1047
    default = None
2✔
1048
    help = help_text(
2✔
1049
        """
1050
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
1051

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

1056
        If you want the entry point to be for a third-party dependency or to use a console
1057
        script, use the `pex_binary` target directly.
1058
        """
1059
    )
1060

1061

1062
class PexBinariesOverrideField(OverridesField):
2✔
1063
    help = help_text(
2✔
1064
        f"""
1065
        Override the field values for generated `{PexBinary.alias}` targets.
1066

1067
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
1068
        their overrides. You may either use a single string or a tuple of strings to override
1069
        multiple targets.
1070

1071
        For example:
1072

1073
            overrides={{
1074
              "foo.py": {{"execution_mode": "venv"]}},
1075
              "bar.py:main": {{"restartable": True]}},
1076
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
1077
            }}
1078

1079
        Every key is validated to belong to this target's `entry_points` field.
1080

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

1085
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
1086
        same field more than one time for the `entry_point`.
1087
        """
1088
    )
1089

1090

1091
class PexBinariesGeneratorTarget(TargetGenerator):
2✔
1092
    alias = "pex_binaries"
2✔
1093
    help = help_text(
2✔
1094
        """
1095
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
1096

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

1101
        This target generator does not work well to generate `pex_binary` targets where the entry
1102
        point is for a third-party dependency. Dependency inference will not work for those, so
1103
        you will have to set lots of custom metadata for each binary; prefer an explicit
1104
        `pex_binary` target in that case. This target generator works best when the entry point
1105
        is a first-party file, like `app.py` or `app.py:main`.
1106
        """
1107
    )
1108
    generated_target_cls = PexBinary
2✔
1109
    core_fields = (
2✔
1110
        *COMMON_TARGET_FIELDS,
1111
        PexEntryPointsField,
1112
        PexBinariesOverrideField,
1113
    )
1114
    copied_fields = COMMON_TARGET_FIELDS
2✔
1115
    moved_fields = _PEX_BINARY_COMMON_FIELDS
2✔
1116

1117

1118
class PexBinaryDefaults(Subsystem):
2✔
1119
    options_scope = "pex-binary-defaults"
2✔
1120
    help = "Default settings for creating PEX executables."
2✔
1121

1122
    emit_warnings = BoolOption(
2✔
1123
        default=True,
1124
        help=softwrap(
1125
            """
1126
            Whether built PEX binaries should emit PEX warnings at runtime by default.
1127

1128
            Can be overridden by specifying the `emit_warnings` parameter of individual
1129
            `pex_binary` targets
1130
            """
1131
        ),
1132
        advanced=True,
1133
    )
1134

1135

1136
# -----------------------------------------------------------------------------------------------
1137
# `python_test` and `python_tests` targets
1138
# -----------------------------------------------------------------------------------------------
1139

1140

1141
class PythonTestSourceField(PythonSourceField):
2✔
1142
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
2✔
1143

1144
    def validate_resolved_files(self, files: Sequence[str]) -> None:
2✔
1145
        super().validate_resolved_files(files)
×
1146
        file = files[0]
×
1147
        file_name = os.path.basename(file)
×
1148
        if file_name == "conftest.py":
×
1149
            raise InvalidFieldException(
×
1150
                softwrap(
1151
                    f"""
1152
                    The {repr(self.alias)} field in target {self.address} should not be set to the
1153
                    file 'conftest.py', but was set to {repr(self.value)}.
1154

1155
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1156
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
1157
                    autogenerate a `python_test_utils` target.
1158
                    """
1159
                )
1160
            )
1161

1162

1163
class PythonTestsDependenciesField(PythonDependenciesField):
2✔
1164
    supports_transitive_excludes = True
2✔
1165

1166

1167
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
2✔
1168
    alias = "entry_point_dependencies"
2✔
1169
    help = help_text(
2✔
1170
        lambda: f"""
1171
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
1172

1173
        This is a dict where each key is a `{PythonDistribution.alias}` address
1174
        and the value is a list or tuple of entry point groups and/or entry points
1175
        on that target. The strings in the value list/tuple must be one of:
1176
        - "entry.point.group/entry-point-name" to depend on a named entry point
1177
        - "entry.point.group" (without a "/") to depend on an entry point group
1178
        - "*" to get all entry points on the target
1179

1180
        For example:
1181

1182
            {PythonTestsEntryPointDependenciesField.alias}={{
1183
                "//foo/address:dist_tgt": ["*"],  # all entry points
1184
                "bar:dist_tgt": ["console_scripts"],  # only from this group
1185
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
1186
                "another:dist_tgt": [  # multiple entry points
1187
                    "console_scripts/my-script",
1188
                    "console_scripts/another-script",
1189
                    "entry.point.group/entry-point-name",
1190
                    "other.group",
1191
                    "gui_scripts",
1192
                ],
1193
            }}
1194

1195
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
1196
        will be added as dependencies so that they are available on PYTHONPATH
1197
        during tests.
1198

1199
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
1200
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
1201
        `entry_points.txt` file will only include the entry points requested on this
1202
        field. This allows the tests, or the code under test, to lookup entry points'
1203
        metadata using an API like the `importlib.metadata.entry_points()` API in the
1204
        standard library (available on older Python interpreters via the
1205
        `importlib-metadata` distribution).
1206
        """
1207
    )
1208

1209

1210
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
1211
class PythonTestsTimeoutField(IntField):
2✔
1212
    alias = "timeout"
2✔
1213
    help = help_text(
2✔
1214
        """
1215
        A timeout (in seconds) used by each test file belonging to this target.
1216

1217
        If unset, will default to `[test].timeout_default`; if that option is also unset,
1218
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
1219
        applies if the option `--test-timeouts` is set to true (the default).
1220
        """
1221
    )
1222
    valid_numbers = ValidNumbers.positive_only
2✔
1223

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

1228
        This function is deprecated and should be replaced by the similarly named one in
1229
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
1230
        """
1231

1232
        enabled = test.options.timeouts
×
1233
        timeout_default = test.options.timeout_default
×
1234
        timeout_maximum = test.options.timeout_maximum
×
1235

1236
        if not enabled:
×
1237
            return None
×
1238
        if self.value is None:
×
1239
            if timeout_default is None:
×
1240
                return None
×
1241
            result = cast(int, timeout_default)
×
1242
        else:
1243
            result = self.value
×
1244
        if timeout_maximum is not None:
×
1245
            return min(result, cast(int, timeout_maximum))
×
1246
        return result
×
1247

1248

1249
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
2✔
1250
    pass
2✔
1251

1252

1253
class PythonTestsXdistConcurrencyField(IntField):
2✔
1254
    alias = "xdist_concurrency"
2✔
1255
    help = help_text(
2✔
1256
        """
1257
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1258

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

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

1267
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1268
        """
1269
    )
1270

1271

1272
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
2✔
1273
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
2✔
1274

1275

1276
class SkipPythonTestsField(BoolField):
2✔
1277
    alias = "skip_tests"
2✔
1278
    default = False
2✔
1279
    help = "If true, don't run this target's tests."
2✔
1280

1281

1282
_PYTHON_TEST_MOVED_FIELDS = (
2✔
1283
    PythonTestsDependenciesField,
1284
    # This field is registered in the experimental backend for now.
1285
    # PythonTestsEntryPointDependenciesField,
1286
    PythonResolveField,
1287
    PythonRunGoalUseSandboxField,
1288
    PythonTestsTimeoutField,
1289
    PythonTestsXdistConcurrencyField,
1290
    PythonTestsBatchCompatibilityTagField,
1291
    RuntimePackageDependenciesField,
1292
    PythonTestsExtraEnvVarsField,
1293
    InterpreterConstraintsField,
1294
    SkipPythonTestsField,
1295
    EnvironmentField,
1296
)
1297

1298

1299
class PythonTestTarget(Target):
2✔
1300
    alias = "python_test"
2✔
1301
    core_fields = (
2✔
1302
        *COMMON_TARGET_FIELDS,
1303
        *_PYTHON_TEST_MOVED_FIELDS,
1304
        PythonTestsDependenciesField,
1305
        PythonTestSourceField,
1306
    )
1307
    help = help_text(
2✔
1308
        f"""
1309
        A single Python test file, written in either Pytest style or unittest style.
1310

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

1315
        See {doc_url("docs/python/goals/test")}
1316
        """
1317
    )
1318

1319

1320
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1321
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
2✔
1322
    default = ("test_*.py", "*_test.py", "tests.py")
2✔
1323
    help = generate_multiple_sources_field_help_message(
2✔
1324
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1325
    )
1326

1327
    def validate_resolved_files(self, files: Sequence[str]) -> None:
2✔
1328
        super().validate_resolved_files(files)
×
1329
        # We don't technically need to error for `conftest.py` here because `PythonTestSourceField`
1330
        # already validates this, but we get a better error message this way so that users don't
1331
        # have to reason about generated targets.
1332
        conftest_files = [fp for fp in files if os.path.basename(fp) == "conftest.py"]
×
1333
        if conftest_files:
×
1334
            raise InvalidFieldException(
×
1335
                softwrap(
1336
                    f"""
1337
                    The {repr(self.alias)} field in target {self.address} should not include the
1338
                    file 'conftest.py', but included these: {conftest_files}.
1339

1340
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1341
                    You can run `{bin_name()} tailor` after removing the files from the
1342
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1343
                    `python_test_utils` target.
1344
                    """
1345
                )
1346
            )
1347

1348

1349
class PythonTestsOverrideField(OverridesField):
2✔
1350
    help = generate_file_based_overrides_field_help_message(
2✔
1351
        PythonTestTarget.alias,
1352
        """
1353
        overrides={
1354
            "foo_test.py": {"timeout": 120},
1355
            "bar_test.py": {"timeout": 200},
1356
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1357
        }
1358
        """,
1359
    )
1360

1361

1362
class PythonTestsGeneratorTarget(TargetFilesGenerator):
2✔
1363
    alias = "python_tests"
2✔
1364
    core_fields = (
2✔
1365
        *COMMON_TARGET_FIELDS,
1366
        PythonTestsGeneratingSourcesField,
1367
        PythonTestsOverrideField,
1368
    )
1369
    generated_target_cls = PythonTestTarget
2✔
1370
    copied_fields = COMMON_TARGET_FIELDS
2✔
1371
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
2✔
1372
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1373
    help = "Generate a `python_test` target for each file in the `sources` field."
2✔
1374

1375

1376
# -----------------------------------------------------------------------------------------------
1377
# `python_source`, `python_sources`, and `python_test_utils` targets
1378
# -----------------------------------------------------------------------------------------------
1379

1380

1381
class PythonSourceTarget(Target):
2✔
1382
    alias = "python_source"
2✔
1383
    core_fields = (
2✔
1384
        *COMMON_TARGET_FIELDS,
1385
        InterpreterConstraintsField,
1386
        PythonDependenciesField,
1387
        PythonResolveField,
1388
        PythonRunGoalUseSandboxField,
1389
        PythonSourceField,
1390
        RestartableField,
1391
    )
1392
    help = "A single Python source file."
2✔
1393

1394

1395
class PythonSourcesOverridesField(OverridesField):
2✔
1396
    help = generate_file_based_overrides_field_help_message(
2✔
1397
        PythonSourceTarget.alias,
1398
        """
1399
        overrides={
1400
            "foo.py": {"skip_pylint": True]},
1401
            "bar.py": {"skip_flake8": True]},
1402
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1403
        }"
1404
        """,
1405
    )
1406

1407

1408
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1409
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
2✔
1410
    help = generate_multiple_sources_field_help_message(
2✔
1411
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1412
    )
1413

1414

1415
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
2✔
1416
    default = (
2✔
1417
        ("*.py", "*.pyi")
1418
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1419
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1420
    )
1421
    help = generate_multiple_sources_field_help_message(
2✔
1422
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1423
    )
1424

1425

1426
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
2✔
1427
    alias = "python_test_utils"
2✔
1428
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1429
    core_fields = (
2✔
1430
        *COMMON_TARGET_FIELDS,
1431
        PythonTestUtilsGeneratingSourcesField,
1432
        PythonSourcesOverridesField,
1433
    )
1434
    generated_target_cls = PythonSourceTarget
2✔
1435
    copied_fields = COMMON_TARGET_FIELDS
2✔
1436
    moved_fields = (
2✔
1437
        PythonResolveField,
1438
        PythonRunGoalUseSandboxField,
1439
        PythonDependenciesField,
1440
        InterpreterConstraintsField,
1441
    )
1442
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1443
    help = help_text(
2✔
1444
        """
1445
        Generate a `python_source` target for each file in the `sources` field.
1446

1447
        This target generator is intended for test utility files like `conftest.py` or
1448
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1449
        way as the `python_sources` target generator does, only that the `sources` field has a
1450
        different default. So it is valid to use `python_sources` instead. However, this target
1451
        can be helpful to better model your code by keeping separate test support files vs.
1452
        production files.
1453
        """
1454
    )
1455

1456

1457
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
2✔
1458
    alias = "python_sources"
2✔
1459
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1460
    core_fields = (
2✔
1461
        *COMMON_TARGET_FIELDS,
1462
        PythonSourcesGeneratingSourcesField,
1463
        PythonSourcesOverridesField,
1464
    )
1465
    generated_target_cls = PythonSourceTarget
2✔
1466
    copied_fields = COMMON_TARGET_FIELDS
2✔
1467
    moved_fields = (
2✔
1468
        PythonResolveField,
1469
        PythonRunGoalUseSandboxField,
1470
        PythonDependenciesField,
1471
        InterpreterConstraintsField,
1472
        RestartableField,
1473
    )
1474
    settings_request_cls = PythonFilesGeneratorSettingsRequest
2✔
1475
    help = help_text(
2✔
1476
        """
1477
        Generate a `python_source` target for each file in the `sources` field.
1478

1479
        You can either use this target generator or `python_test_utils` for test utility files
1480
        like `conftest.py`. They behave identically, but can help to better model and keep
1481
        separate test support files vs. production files.
1482
        """
1483
    )
1484

1485

1486
# -----------------------------------------------------------------------------------------------
1487
# `python_requirement` target
1488
# -----------------------------------------------------------------------------------------------
1489

1490

1491
class _PipRequirementSequenceField(Field):
2✔
1492
    value: tuple[PipRequirement, ...]
2✔
1493

1494
    @classmethod
2✔
1495
    def compute_value(
2✔
1496
        cls, raw_value: Iterable[str] | None, address: Address
1497
    ) -> tuple[PipRequirement, ...]:
UNCOV
1498
        value = super().compute_value(raw_value, address)
×
UNCOV
1499
        if value is None:
×
1500
            return ()
×
UNCOV
1501
        invalid_type_error = InvalidFieldTypeException(
×
1502
            address,
1503
            cls.alias,
1504
            value,
1505
            expected_type="an iterable of pip-style requirement strings (e.g. a list)",
1506
        )
UNCOV
1507
        if isinstance(value, str) or not isinstance(value, collections.abc.Iterable):
×
UNCOV
1508
            raise invalid_type_error
×
UNCOV
1509
        result = []
×
UNCOV
1510
        for v in value:
×
1511
            # We allow passing a pre-parsed `PipRequirement`. This is intended for macros which
1512
            # might have already parsed so that we can avoid parsing multiple times.
UNCOV
1513
            if isinstance(v, PipRequirement):
×
UNCOV
1514
                result.append(v)
×
UNCOV
1515
            elif isinstance(v, str):
×
UNCOV
1516
                try:
×
UNCOV
1517
                    parsed = PipRequirement.parse(
×
1518
                        v, description_of_origin=f"the '{cls.alias}' field for the target {address}"
1519
                    )
UNCOV
1520
                except ValueError as e:
×
UNCOV
1521
                    raise InvalidFieldException(e)
×
UNCOV
1522
                result.append(parsed)
×
1523
            else:
UNCOV
1524
                raise invalid_type_error
×
UNCOV
1525
        return tuple(result)
×
1526

1527

1528
class PythonRequirementDependenciesField(Dependencies):
2✔
1529
    pass
2✔
1530

1531

1532
class PythonRequirementsField(_PipRequirementSequenceField):
2✔
1533
    alias = "requirements"
2✔
1534
    required = True
2✔
1535
    help = help_text(
2✔
1536
        """
1537
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1538

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

1543
        If the requirement depends on some other requirement to work, such as needing
1544
        `setuptools` to be built, use the `dependencies` field instead.
1545
        """
1546
    )
1547

1548

1549
_default_module_mapping_url = git_url(
2✔
1550
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1551
)
1552

1553

1554
class PythonRequirementModulesField(StringSequenceField):
2✔
1555
    alias = "modules"
2✔
1556
    help = help_text(
2✔
1557
        f"""
1558
        The modules this requirement provides (used for dependency inference).
1559

1560
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1561
        "easy_install"]`.
1562

1563
        Usually you can leave this field off. If unspecified, Pants will first look at the
1564
        default module mapping ({_default_module_mapping_url}), and then will default to
1565
        the normalized project name. For example, the requirement `Django` would default to
1566
        the module `django`.
1567

1568
        Mutually exclusive with the `type_stub_modules` field.
1569
        """
1570
    )
1571

1572

1573
class PythonRequirementTypeStubModulesField(StringSequenceField):
2✔
1574
    alias = "type_stub_modules"
2✔
1575
    help = help_text(
2✔
1576
        f"""
1577
        The modules this requirement provides if the requirement is a type stub (used for
1578
        dependency inference).
1579

1580
        For example, the requirement `types-requests` provides `["requests"]`.
1581

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

1589
        Mutually exclusive with the `modules` field.
1590
        """
1591
    )
1592

1593

1594
def normalize_module_mapping(
2✔
1595
    mapping: Mapping[str, Iterable[str]] | None,
1596
) -> FrozenDict[str, tuple[str, ...]]:
UNCOV
1597
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
×
1598

1599

1600
class PythonRequirementResolveField(PythonResolveField):
2✔
1601
    alias = "resolve"
2✔
1602
    required = False
2✔
1603
    help = help_text(
2✔
1604
        """
1605
        The resolve from `[python].resolves` that this requirement is included in.
1606

1607
        If not defined, will default to `[python].default_resolve`.
1608

1609
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1610
        it will include all requirements that are declared with that resolve.
1611
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1612
        they use via their `resolve` field; so, for your first-party code to use a
1613
        particular `python_requirement` target, that requirement must be included in the resolve
1614
        used by that code.
1615
        """
1616
    )
1617

1618

1619
class PythonRequirementFindLinksField(StringSequenceField):
2✔
1620
    # NB: This is solely used for `pants_requirements` target generation
1621
    alias = "_find_links"
2✔
1622
    required = False
2✔
1623
    default = ()
2✔
1624
    help = "<Internal>"
2✔
1625

1626

1627
class PythonRequirementEntryPointField(EntryPointField):
2✔
1628
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1629
    pass
2✔
1630

1631

1632
class PythonRequirementTarget(Target):
2✔
1633
    alias = "python_requirement"
2✔
1634
    core_fields = (
2✔
1635
        *COMMON_TARGET_FIELDS,
1636
        PythonRequirementsField,
1637
        PythonRequirementDependenciesField,
1638
        PythonRequirementModulesField,
1639
        PythonRequirementTypeStubModulesField,
1640
        PythonRequirementResolveField,
1641
        PythonRequirementEntryPointField,
1642
        PythonRequirementFindLinksField,
1643
    )
1644
    help = help_text(
2✔
1645
        f"""
1646
        A Python requirement installable by pip.
1647

1648
        This target is useful when you want to declare Python requirements inline in a
1649
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1650
        the target generator `python_requirements` to convert each
1651
        requirement into a `python_requirement` target automatically. For Poetry, use
1652
        `poetry_requirements`.
1653

1654
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1655
        """
1656
    )
1657

1658
    def validate(self) -> None:
2✔
UNCOV
1659
        if (
×
1660
            self[PythonRequirementModulesField].value
1661
            and self[PythonRequirementTypeStubModulesField].value
1662
        ):
1663
            raise InvalidTargetException(
×
1664
                softwrap(
1665
                    f"""
1666
                    The `{self.alias}` target {self.address} cannot set both the
1667
                    `{self[PythonRequirementModulesField].alias}` and
1668
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1669
                    To fix, please remove one.
1670
                    """
1671
                )
1672
            )
1673

1674

1675
# -----------------------------------------------------------------------------------------------
1676
# `python_distribution` target
1677
# -----------------------------------------------------------------------------------------------
1678

1679

1680
# See `target_types_rules.py` for a dependency injection rule.
1681
class PythonDistributionDependenciesField(Dependencies):
2✔
1682
    supports_transitive_excludes = True
2✔
1683

1684

1685
class PythonProvidesField(ScalarField, AsyncFieldMixin):
2✔
1686
    alias = "provides"
2✔
1687
    expected_type = PythonArtifact
2✔
1688
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
2✔
1689
    value: PythonArtifact
2✔
1690
    required = True
2✔
1691
    help = help_text(
2✔
1692
        f"""
1693
        The setup.py kwargs for the external artifact built from this target.
1694

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

1699
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1700
        """
1701
    )
1702

1703
    @classmethod
2✔
1704
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
2✔
UNCOV
1705
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1706

1707

1708
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
2✔
1709
    alias = "entry_points"
2✔
1710
    required = False
2✔
1711
    help = help_text(
2✔
1712
        f"""
1713
        Any entry points, such as `console_scripts` and `gui_scripts`.
1714

1715
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1716
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1717
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1718
        `pex_binary` target.
1719

1720
        Example:
1721

1722
            entry_points={{
1723
              "console_scripts": {{
1724
                "my-script": "project.app:main",
1725
                "another-script": "project/subdir:pex_binary_tgt"
1726
              }}
1727
            }}
1728

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

1735
        Pants will attempt to infer dependencies, which you can confirm by running:
1736

1737
            {bin_name()} dependencies <python_distribution target address>
1738
        """
1739
    )
1740

1741

1742
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
2✔
1743
    help = help_text(
2✔
1744
        """
1745
        The path to the directory to write the distribution file to, relative the dist directory.
1746

1747
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1748
        level of the dist dir.
1749
        """
1750
    )
1751
    alias = "output_path"
2✔
1752
    default = ""
2✔
1753

1754

1755
@dataclass(frozen=True)
2✔
1756
class PythonDistributionEntryPoint:
2✔
1757
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1758

1759
    entry_point: EntryPoint
2✔
1760
    pex_binary_address: Address | None
2✔
1761

1762

1763
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1764
@dataclass(frozen=True)
2✔
1765
class ResolvedPythonDistributionEntryPoints:
2✔
1766
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1767
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
2✔
1768

1769
    @property
2✔
1770
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
2✔
1771
        """Filters out all entry points from pex binary targets."""
1772
        return FrozenDict(
×
1773
            {
1774
                category: FrozenDict(
1775
                    {
1776
                        ep_name: ep_val.entry_point
1777
                        for ep_name, ep_val in entry_points.items()
1778
                        if not ep_val.pex_binary_address
1779
                    }
1780
                )
1781
                for category, entry_points in self.val.items()
1782
            }
1783
        )
1784

1785
    @property
2✔
1786
    def pex_binary_addresses(self) -> Addresses:
2✔
1787
        """Returns the addresses to all pex binary targets owning entry points used."""
1788
        return Addresses(
×
1789
            ep_val.pex_binary_address
1790
            for category, entry_points in self.val.items()
1791
            for ep_val in entry_points.values()
1792
            if ep_val.pex_binary_address
1793
        )
1794

1795

1796
@dataclass(frozen=True)
2✔
1797
class ResolvePythonDistributionEntryPointsRequest:
2✔
1798
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1799
    that should be resolved into a setuptools entry point.
1800

1801
    If the `entry_points_field` is present, inspect the specified entry points.
1802
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1803

1804
    This is to support inspecting one or the other depending on use case, using the same
1805
    logic for resolving pex_binary addresses etc.
1806
    """
1807

1808
    entry_points_field: PythonDistributionEntryPointsField | None = None
2✔
1809
    provides_field: PythonProvidesField | None = None
2✔
1810

1811
    def __post_init__(self):
2✔
1812
        # Must provide at least one of these fields.
UNCOV
1813
        assert self.entry_points_field or self.provides_field
×
1814

1815

1816
class WheelField(BoolField):
2✔
1817
    alias = "wheel"
2✔
1818
    default = True
2✔
1819
    help = "Whether to build a wheel for the distribution."
2✔
1820

1821

1822
class SDistField(BoolField):
2✔
1823
    alias = "sdist"
2✔
1824
    default = True
2✔
1825
    help = "Whether to build an sdist for the distribution."
2✔
1826

1827

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

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

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

1836
    - Build frontends should support string values, and may also support other mechanisms
1837
      (apparently meaning other types).
1838

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

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

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

1854

1855
class WheelConfigSettingsField(ConfigSettingsField):
2✔
1856
    alias = "wheel_config_settings"
2✔
1857
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
2✔
1858

1859

1860
class SDistConfigSettingsField(ConfigSettingsField):
2✔
1861
    alias = "sdist_config_settings"
2✔
1862
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
2✔
1863

1864

1865
class BuildBackendEnvVarsField(StringSequenceField):
2✔
1866
    alias = "env_vars"
2✔
1867
    required = False
2✔
1868
    help = help_text(
2✔
1869
        """
1870
        Environment variables to set when running the PEP-517 build backend.
1871

1872
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1873
        or just `ENV_VAR` to copy the value from Pants's own environment.
1874
        """
1875
    )
1876

1877

1878
class GenerateSetupField(TriBoolField):
2✔
1879
    alias = "generate_setup"
2✔
1880
    required = False
2✔
1881
    # The default behavior if this field is unspecified is controlled by the
1882
    # --generate-setup-default option in the setup-py-generation scope.
1883
    default = None
2✔
1884

1885
    help = help_text(
2✔
1886
        """
1887
        Whether to generate setup information for this distribution, based on analyzing
1888
        sources and dependencies. Set to False to use existing setup information, such as
1889
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1890
        """
1891
    )
1892

1893

1894
class LongDescriptionPathField(StringField):
2✔
1895
    alias = "long_description_path"
2✔
1896
    required = False
2✔
1897

1898
    help = help_text(
2✔
1899
        """
1900
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1901

1902
        Path is relative to the build root.
1903

1904
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1905

1906
        This field won't automatically set `long_description_content_type` field for you.
1907
        You have to specify this field yourself in the `provides` field.
1908
        """
1909
    )
1910

1911

1912
class PythonDistribution(Target):
2✔
1913
    alias: ClassVar[str] = "python_distribution"
2✔
1914
    core_fields = (
2✔
1915
        *COMMON_TARGET_FIELDS,
1916
        InterpreterConstraintsField,
1917
        PythonDistributionDependenciesField,
1918
        PythonDistributionEntryPointsField,
1919
        PythonProvidesField,
1920
        GenerateSetupField,
1921
        WheelField,
1922
        SDistField,
1923
        WheelConfigSettingsField,
1924
        SDistConfigSettingsField,
1925
        BuildBackendEnvVarsField,
1926
        LongDescriptionPathField,
1927
        PythonDistributionOutputPathField,
1928
    )
1929
    help = help_text(
2✔
1930
        f"""
1931
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
1932

1933
        See {doc_url("docs/python/overview/building-distributions")}.
1934
        """
1935
    )
1936

1937

1938
# -----------------------------------------------------------------------------------------------
1939
# `vcs_version` target
1940
# -----------------------------------------------------------------------------------------------
1941

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

1949

1950
class VCSVersionDummySourceField(OptionalSingleSourceField):
2✔
1951
    """A dummy SourceField for participation in the codegen machinery."""
1952

1953
    alias = "_dummy_source"  # Leading underscore omits the field from help.
2✔
1954
    help = "A version string generated from VCS information"
2✔
1955

1956

1957
class VersionTagRegexField(StringField):
2✔
1958
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
2✔
1959
    alias = "tag_regex"
2✔
1960
    help = help_text(
2✔
1961
        """
1962
        A Python regex string to extract the version string from a VCS tag.
1963

1964
        The regex needs to contain either a single match group, or a group named version,
1965
        that captures the actual version information.
1966

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

1969
        See https://github.com/pypa/setuptools_scm for implementation details.
1970
        """
1971
    )
1972

1973

1974
class VersionGenerateToField(StringField):
2✔
1975
    required = True
2✔
1976
    alias = "generate_to"
2✔
1977
    help = help_text(
2✔
1978
        """
1979
        Generate the version data to this relative path, using the template field.
1980

1981
        Note that the generated output will not be written to disk in the source tree, but
1982
        will be available as a generated dependency to code that depends on this target.
1983
        """
1984
    )
1985

1986

1987
class VersionTemplateField(StringField):
2✔
1988
    required = True
2✔
1989
    alias = "template"
2✔
1990
    help = help_text(
2✔
1991
        """
1992
        Generate the version data using this format string, which takes a version format kwarg.
1993

1994
        E.g., `'version = "{version}"'`
1995
        """
1996
    )
1997

1998

1999
class VersionVersionSchemeField(StringField):
2✔
2000
    alias = "version_scheme"
2✔
2001
    help = help_text(
2✔
2002
        """
2003
        The version scheme to configure `setuptools_scm` to use.
2004
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
2005
        """
2006
    )
2007

2008

2009
class VersionLocalSchemeField(StringField):
2✔
2010
    alias = "local_scheme"
2✔
2011
    help = help_text(
2✔
2012
        """
2013
        The local scheme to configure `setuptools_scm` to use.
2014
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
2015
        """
2016
    )
2017

2018

2019
class VCSVersion(Target):
2✔
2020
    alias = "vcs_version"
2✔
2021
    core_fields = (
2✔
2022
        *COMMON_TARGET_FIELDS,
2023
        VersionTagRegexField,
2024
        VersionVersionSchemeField,
2025
        VersionLocalSchemeField,
2026
        VCSVersionDummySourceField,
2027
        VersionGenerateToField,
2028
        VersionTemplateField,
2029
    )
2030
    help = help_text(
2✔
2031
        f"""
2032
        Generates a version string from VCS state.
2033

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

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

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