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

pantsbuild / pants / 20428083889

22 Dec 2025 09:43AM UTC coverage: 80.285% (-0.01%) from 80.296%
20428083889

Pull #21918

github

web-flow
Merge c895684e5 into 06f105be8
Pull Request #21918: [WIP] partition protobuf dependency inference by any "resolve-like" fields from plugins

191 of 263 new or added lines in 9 files covered. (72.62%)

44 existing lines in 2 files now uncovered.

78686 of 98008 relevant lines covered (80.29%)

3.65 hits per line

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

91.99
/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
13✔
5

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

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

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

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

83

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

87

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

91

92
class InterpreterConstraintsField(StringSequenceField, AsyncFieldMixin):
13✔
93
    alias = "interpreter_constraints"
13✔
94
    help = help_text(
13✔
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(
13✔
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
        """
119
        if self.value and python_setup.warn_on_python2_usage:
1✔
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
            )
129
        return python_setup.compatibility_or_constraints(
1✔
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):
13✔
138
    pass
13✔
139

140

141
class PythonResolveField(StringField, AsyncFieldMixin, ResolveLikeField):
13✔
142
    alias = "resolve"
13✔
143
    required = False
13✔
144
    help = help_text(
13✔
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:
13✔
155
        """Get the value after applying the default and validating that the key is recognized."""
156
        if not python_setup.enable_resolves:
2✔
157
            return "<ignore>"
×
158
        resolve = self.value or python_setup.default_resolve
2✔
159
        if resolve not in python_setup.resolves:
2✔
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
            )
165
        return resolve
2✔
166

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

170

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

174

175
class PythonRunGoalUseSandboxField(TriBoolField):
13✔
176
    alias = "run_goal_use_sandbox"
13✔
177
    help = help_text(
13✔
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):
13✔
210
    pass
13✔
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):
13✔
220
    supports_transitive_excludes = True
13✔
221

222

223
class MainSpecification(ABC):
13✔
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)
13✔
233
class EntryPoint(MainSpecification):
13✔
234
    module: str
13✔
235
    function: str | None = None
13✔
236

237
    @classmethod
13✔
238
    def parse(cls, value: str, provenance: str | None = None) -> EntryPoint:
13✔
239
        given = f"entry point {provenance}" if provenance else "entry point"
1✔
240
        entry_point = value.strip()
1✔
241
        if not entry_point:
1✔
242
            raise ValueError(
1✔
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
            )
251
        module_or_path, sep, func = entry_point.partition(":")
1✔
252
        if not module_or_path:
1✔
253
            raise ValueError(f"The {given} must specify a module; given: {value!r}")
1✔
254
        if ":" in func:
1✔
255
            raise ValueError(
1✔
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
            )
263
        if sep and not func:
1✔
264
            logger.warning(
1✔
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
            )
273
        return cls(module=module_or_path, function=func if func else None)
1✔
274

275
    def __post_init__(self):
13✔
276
        if ":" in self.module:
13✔
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:
13✔
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]:
13✔
296
        yield "--entry-point"
×
297
        yield self.spec
×
298

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

303

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

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

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

316

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

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

327
    def iter_pex_args(self) -> Iterator[str]:
13✔
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
13✔
334
    def spec(self) -> str:
13✔
335
        return self.executable
×
336

337

338
class EntryPointField(AsyncFieldMixin, Field):
13✔
339
    alias = "entry_point"
13✔
340
    default = None
13✔
341
    help = help_text(
13✔
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
13✔
356

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

369

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

374

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

381

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

386
    entry_point_field: EntryPointField
13✔
387

388

389
class PexScriptField(Field):
13✔
390
    alias = "script"
13✔
391
    default = None
13✔
392
    help = help_text(
13✔
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
13✔
402

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

412

413
class PexExecutableField(Field):
13✔
414
    alias = "executable"
13✔
415
    default = None
13✔
416
    help = help_text(
13✔
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
13✔
427

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

437

438
class PexArgsField(StringSequenceField):
13✔
439
    alias: ClassVar[str] = "args"
13✔
440
    help = help_text(
13✔
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):
13✔
454
    alias: ClassVar[str] = "extra_build_args"
13✔
455
    default = ()
13✔
456
    help = help_text(
13✔
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):
13✔
470
    alias = "check"
13✔
471
    valid_choices = ("none", "warn", "error")
13✔
472
    expected_type = str
13✔
473
    default = "warn"
13✔
474
    help = help_text(
13✔
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):
13✔
488
    alias = "env"
13✔
489
    help = help_text(
13✔
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):
13✔
498
    alias = "complete_platforms"
13✔
499
    help = help_text(
13✔
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):
13✔
518
    alias = "inherit_path"
13✔
519
    valid_choices = ("false", "fallback", "prefer")
13✔
520
    help = help_text(
13✔
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
13✔
531
    def compute_value(cls, raw_value: str | bool | None, address: Address) -> str | None:
13✔
532
        if isinstance(raw_value, bool):
1✔
533
            return "prefer" if raw_value else "false"
×
534
        return super().compute_value(raw_value, address)
1✔
535

536

537
class PexStripEnvField(BoolField):
13✔
538
    alias = "strip_pex_env"
13✔
539
    default = True
13✔
540
    help = help_text(
13✔
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):
13✔
554
    alias = "ignore_errors"
13✔
555
    default = False
13✔
556
    help = "Should PEX ignore errors when it cannot resolve dependencies?"
13✔
557

558

559
class PexShBootField(BoolField):
13✔
560
    alias = "sh_boot"
13✔
561
    default = False
13✔
562
    help = help_text(
13✔
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):
13✔
583
    alias = "shebang"
13✔
584
    help = help_text(
13✔
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):
13✔
596
    alias = "emit_warnings"
13✔
597
    help = help_text(
13✔
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:
13✔
606
        if self.value is None:
×
607
            return pex_binary_defaults.emit_warnings
×
608

609
        return self.value
×
610

611

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

616

617
class PexExecutionModeField(StringField):
13✔
618
    alias = "execution_mode"
13✔
619
    valid_choices = PexExecutionMode
13✔
620
    expected_type = str
13✔
621
    default = PexExecutionMode.ZIPAPP.value
13✔
622
    help = help_text(
13✔
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):
13✔
640
    ZIPAPP = "zipapp"
13✔
641
    PACKED = "packed"
13✔
642
    LOOSE = "loose"
13✔
643

644

645
class PexLayoutField(StringField):
13✔
646
    alias = "layout"
13✔
647
    valid_choices = PexLayout
13✔
648
    expected_type = str
13✔
649
    default = PexLayout.ZIPAPP.value
13✔
650
    help = help_text(
13✔
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):
13✔
676
    alias = "include_requirements"
13✔
677
    default = True
13✔
678
    help = help_text(
13✔
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):
13✔
687
    alias = "include_sources"
13✔
688
    default = True
13✔
689
    help = help_text(
13✔
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):
13✔
697
    alias = "include_tools"
13✔
698
    default = False
13✔
699
    help = help_text(
13✔
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):
13✔
710
    alias = "venv_site_packages_copies"
13✔
711
    default = False
13✔
712
    help = help_text(
13✔
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):
13✔
722
    alias = "venv_hermetic_scripts"
13✔
723
    default = True
13✔
724
    help = help_text(
13✔
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):
13✔
737
    alias = "scie"
13✔
738
    valid_choices = ("lazy", "eager")
13✔
739
    default = None
13✔
740
    help = help_text(
13✔
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):
13✔
765
    DYNAMIC = "dynamic"
13✔
766
    PLATFORM_PARENT_DIR = "platform-parent-dir"
13✔
767
    PLATFORM_FILE_SUFFIX = "platform-file-suffix"
13✔
768

769

770
class PexScieNameStyleField(StringField):
13✔
771
    alias = "scie_name_style"
13✔
772
    valid_choices = ScieNameStyle
13✔
773
    expected_type = str
13✔
774
    default = ScieNameStyle.DYNAMIC
13✔
775
    help = help_text(
13✔
776
        """
777
        Control how the output file translates to a scie name. By default
778
        (`dynamic`), the platform is used as a file suffix only when needed
779
        for disambiguation when targeting a local platform.  Specifying
780
        `platform-file-suffix` forces the scie target platform name to be
781
        added as a suffix of the output filename; Specifying
782
        `platform-parent-dir` places the scie in a sub- directory with the
783
        name of the platform it targets."""
784
    )
785

786

787
class PexScieBusyBox(StringField):
13✔
788
    alias = "scie_busybox"
13✔
789
    default = None
13✔
790
    help = help_text(
13✔
791
        """
792
        Make the PEX scie a BusyBox over the specified entry points. The entry
793
        points can either be console scripts or entry point specifiers. To
794
        select all console scripts in all distributions contained in the PEX,
795
        use `@`. To just pick all the console scripts from a particular
796
        project name's distributions in the PEX, use `@<project name>`; e.g.:
797
        `@ansible-core`. To exclude all the console scripts from a project,
798
        prefix with a `!`; e.g.: `@,!@ansible-core` selects all console
799
        scripts except those provided by the `ansible- core` project. To
800
        select an individual console script, just use its name or prefix the
801
        name with `!` to exclude that individual console script. To specify an
802
        arbitrary entry point in a module contained within one of the
803
        distributions in the PEX, use a string of the form
804
        `<name>=<module>(:<function>)`; e.g.: 'run- baz=foo.bar:baz' to
805
        execute the `baz` function in the `foo.bar` module as the entry point
806
        named `run-baz`.
807

808
        A BusyBox scie has no default entrypoint; instead, when run, it
809
        inspects argv0; if that matches one of its embedded entry points, it
810
        runs that entry point; if not, it lists all available entrypoints for
811
        you to pick from. To run a given entry point, you specify it as the
812
        first argument and all other arguments after that are forwarded to
813
        that entry point. BusyBox PEX scies allow you to install all their
814
        contained entry points into a given directory.  For more information,
815
        run `SCIE=help <your PEX scie>` and review the `install` command help.
816

817
        NOTE: This is only available for formal Python entry points
818
        <https://packaging.python.org/en/latest/specifications/entry-points/>
819
        and not the informal use by the `pex_binary` field `entry_point` to
820
        run first party files.
821
        """
822
    )
823

824

825
class PexScieBusyboxPexEntrypointEnvPassthrough(TriBoolField):
13✔
826
    alias = "scie_busybox_pex_entrypoint_env_passthrough"
13✔
827
    required = False
13✔
828
    default = None
13✔
829
    help = help_text(
13✔
830
        """ When creating a busybox, allow overriding the primary entrypoint
831
        at runtime via PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
832
        when using the `venv` execution mode this adds modest startup overhead
833
        on the order of 10ms.  """
834
    )
835

836

837
class PexSciePlatformField(StringSequenceField):
13✔
838
    alias = "scie_platform"
13✔
839
    valid_choices = (
13✔
840
        "current",
841
        "linux-aarch64",
842
        "linux-armv7l",
843
        "linux-powerpc64",
844
        "linux-riscv64",
845
        "linux-s390x",
846
        "linux-x86_64",
847
        "macos-aarch64",
848
        "macos-x86_64",
849
    )
850
    expected_type = str
13✔
851
    help = help_text(
13✔
852
        """ The platform to produce the native PEX scie executable for.  You
853
        can use a value of `current` to select the current platform. If left
854
        unspecified, the platforms implied by the targets selected to build
855
        the PEX with are used. Those targets are influenced by the current
856
        interpreter running Pex as well as use of `complete_platforms` and
857
        `interpreter_constraints`. Note that, in general, `scie_platform`
858
        should only be used to select a subset of the platforms implied by the
859
        targets selected via other options.  """
860
    )
861

862

863
class PexSciePbsReleaseField(StringField):
13✔
864
    alias = "scie_pbs_release"
13✔
865
    default = None
13✔
866
    help = help_text(
13✔
867
        """ The Python Standalone Builds release to use when a CPython
868
        interpreter distribution is needed for the PEX scie. Currently,
869
        releases are dates of the form YYYYMMDD, e.g.: '20240713'. See their
870
        GitHub releases page at
871
        <https://github.com/astral-sh/python-build-standalone/releases> to
872
        discover available releases. If left unspecified the latest release is
873
        used.
874
        """
875
    )
876

877

878
class PexSciePythonVersion(StringField):
13✔
879
    alias = "scie_python_version"
13✔
880
    default = None
13✔
881
    help = help_text(
13✔
882
        """ The portable CPython version to select. Can be either in
883
        `<major>.<minor>` form; e.g.: '3.11', or else fully specified as
884
        `<major>.<minor>.<patch>`; e.g.: '3.11.3'. If you don't specify this
885
        option, Pex will do its best to guess appropriate portable CPython
886
        versions. N.B.: Python Standalone Builds does not provide all patch
887
        versions; so you should check their releases at
888
        <https://github.com/astral-sh/python-build-standalone/releases> if you
889
        wish to pin down to the patch level.
890
        """
891
    )
892

893

894
class PexSciePbsStripped(TriBoolField):
13✔
895
    alias = "scie_pbs_stripped"
13✔
896
    required = False
13✔
897
    default = None
13✔
898
    help = help_text(
13✔
899
        """ Should the Python Standalone Builds CPython distributions used be
900
        stripped of debug symbols or not. For Linux and Windows particularly,
901
        the stripped distributions are less than half the size of the
902
        distributions that ship with debug symbols.  """
903
    )
904

905

906
class PexScieHashAlgField(StringField):
13✔
907
    alias = "scie_hash_alg"
13✔
908
    help = help_text(
13✔
909
        """ Output a checksum file for each scie generated that is compatible
910
        with the shasum family of tools. For each unique algorithm specified,
911
        a sibling file to each scie executable will be generated with the same
912
        stem as that scie file and hash algorithm name suffix.  The file will
913
        contain the hex fingerprint of the scie executable using that
914
        algorithm to hash it. Supported algorithms include at least md5, sha1,
915
        sha256, sha384 and sha512. For the complete list of supported hash
916
        algorithms, see the science tool --hash documentation here:
917
        <https://science.scie.app/cli.html#science-lift-build>.  """
918
    )
919

920

921
_PEX_BINARY_COMMON_FIELDS = (
13✔
922
    EnvironmentField,
923
    InterpreterConstraintsField,
924
    PythonResolveField,
925
    PexBinaryDependenciesField,
926
    PexCheckField,
927
    PexCompletePlatformsField,
928
    PexInheritPathField,
929
    PexStripEnvField,
930
    PexIgnoreErrorsField,
931
    PexShBootField,
932
    PexShebangField,
933
    PexEmitWarningsField,
934
    PexLayoutField,
935
    PexExecutionModeField,
936
    PexIncludeRequirementsField,
937
    PexIncludeSourcesField,
938
    PexIncludeToolsField,
939
    PexVenvSitePackagesCopies,
940
    PexVenvHermeticScripts,
941
    PexExtraBuildArgsField,
942
    RestartableField,
943
)
944

945
_PEX_SCIE_BINARY_FIELDS = (
13✔
946
    PexScieField,
947
    PexScieNameStyleField,
948
    PexScieBusyBox,
949
    PexScieBusyboxPexEntrypointEnvPassthrough,
950
    PexSciePlatformField,
951
    PexSciePbsReleaseField,
952
    PexSciePythonVersion,
953
    PexSciePbsStripped,
954
    PexScieHashAlgField,
955
)
956

957

958
class PexBinary(Target):
13✔
959
    alias = "pex_binary"
13✔
960
    core_fields = (
13✔
961
        *COMMON_TARGET_FIELDS,
962
        *_PEX_BINARY_COMMON_FIELDS,
963
        *_PEX_SCIE_BINARY_FIELDS,
964
        PexEntryPointField,
965
        PexScriptField,
966
        PexExecutableField,
967
        PexArgsField,
968
        PexEnvField,
969
        OutputPathField,
970
    )
971
    help = help_text(
13✔
972
        f"""
973
        A Python target that can be converted into an executable PEX file.
974

975
        PEX files are self-contained executable files that contain a complete Python environment
976
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
977
        """
978
    )
979

980
    def validate(self) -> None:
13✔
981
        got_entry_point = self[PexEntryPointField].value is not None
1✔
982
        got_script = self[PexScriptField].value is not None
1✔
983
        got_executable = self[PexExecutableField].value is not None
1✔
984

985
        if (got_entry_point + got_script + got_executable) > 1:
1✔
986
            raise InvalidTargetException(
1✔
987
                softwrap(
988
                    f"""
989
                    The `{self.alias}` target {self.address} cannot set more than one of the
990
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
991
                    `{self[PexExecutableField].alias}` fields at the same time.
992
                    To fix, please remove all but one.
993
                    """
994
                )
995
            )
996

997

998
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
13✔
999
    alias = "entry_points"
13✔
1000
    default = None
13✔
1001
    help = help_text(
13✔
1002
        """
1003
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
1004

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

1009
        If you want the entry point to be for a third-party dependency or to use a console
1010
        script, use the `pex_binary` target directly.
1011
        """
1012
    )
1013

1014

1015
class PexBinariesOverrideField(OverridesField):
13✔
1016
    help = help_text(
13✔
1017
        f"""
1018
        Override the field values for generated `{PexBinary.alias}` targets.
1019

1020
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
1021
        their overrides. You may either use a single string or a tuple of strings to override
1022
        multiple targets.
1023

1024
        For example:
1025

1026
            overrides={{
1027
              "foo.py": {{"execution_mode": "venv"]}},
1028
              "bar.py:main": {{"restartable": True]}},
1029
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
1030
            }}
1031

1032
        Every key is validated to belong to this target's `entry_points` field.
1033

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

1038
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
1039
        same field more than one time for the `entry_point`.
1040
        """
1041
    )
1042

1043

1044
class PexBinariesGeneratorTarget(TargetGenerator):
13✔
1045
    alias = "pex_binaries"
13✔
1046
    help = help_text(
13✔
1047
        """
1048
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
1049

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

1054
        This target generator does not work well to generate `pex_binary` targets where the entry
1055
        point is for a third-party dependency. Dependency inference will not work for those, so
1056
        you will have to set lots of custom metadata for each binary; prefer an explicit
1057
        `pex_binary` target in that case. This target generator works best when the entry point
1058
        is a first-party file, like `app.py` or `app.py:main`.
1059
        """
1060
    )
1061
    generated_target_cls = PexBinary
13✔
1062
    core_fields = (
13✔
1063
        *COMMON_TARGET_FIELDS,
1064
        PexEntryPointsField,
1065
        PexBinariesOverrideField,
1066
    )
1067
    copied_fields = COMMON_TARGET_FIELDS
13✔
1068
    moved_fields = _PEX_BINARY_COMMON_FIELDS
13✔
1069

1070

1071
class PexBinaryDefaults(Subsystem):
13✔
1072
    options_scope = "pex-binary-defaults"
13✔
1073
    help = "Default settings for creating PEX executables."
13✔
1074

1075
    emit_warnings = BoolOption(
13✔
1076
        default=True,
1077
        help=softwrap(
1078
            """
1079
            Whether built PEX binaries should emit PEX warnings at runtime by default.
1080

1081
            Can be overridden by specifying the `emit_warnings` parameter of individual
1082
            `pex_binary` targets
1083
            """
1084
        ),
1085
        advanced=True,
1086
    )
1087

1088

1089
# -----------------------------------------------------------------------------------------------
1090
# `python_test` and `python_tests` targets
1091
# -----------------------------------------------------------------------------------------------
1092

1093

1094
class PythonTestSourceField(PythonSourceField):
13✔
1095
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
13✔
1096

1097
    def validate_resolved_files(self, files: Sequence[str]) -> None:
13✔
1098
        super().validate_resolved_files(files)
×
1099
        file = files[0]
×
1100
        file_name = os.path.basename(file)
×
1101
        if file_name == "conftest.py":
×
1102
            raise InvalidFieldException(
×
1103
                softwrap(
1104
                    f"""
1105
                    The {repr(self.alias)} field in target {self.address} should not be set to the
1106
                    file 'conftest.py', but was set to {repr(self.value)}.
1107

1108
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1109
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
1110
                    autogenerate a `python_test_utils` target.
1111
                    """
1112
                )
1113
            )
1114

1115

1116
class PythonTestsDependenciesField(PythonDependenciesField):
13✔
1117
    supports_transitive_excludes = True
13✔
1118

1119

1120
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
13✔
1121
    alias = "entry_point_dependencies"
13✔
1122
    help = help_text(
13✔
1123
        lambda: f"""
1124
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
1125

1126
        This is a dict where each key is a `{PythonDistribution.alias}` address
1127
        and the value is a list or tuple of entry point groups and/or entry points
1128
        on that target. The strings in the value list/tuple must be one of:
1129
        - "entry.point.group/entry-point-name" to depend on a named entry point
1130
        - "entry.point.group" (without a "/") to depend on an entry point group
1131
        - "*" to get all entry points on the target
1132

1133
        For example:
1134

1135
            {PythonTestsEntryPointDependenciesField.alias}={{
1136
                "//foo/address:dist_tgt": ["*"],  # all entry points
1137
                "bar:dist_tgt": ["console_scripts"],  # only from this group
1138
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
1139
                "another:dist_tgt": [  # multiple entry points
1140
                    "console_scripts/my-script",
1141
                    "console_scripts/another-script",
1142
                    "entry.point.group/entry-point-name",
1143
                    "other.group",
1144
                    "gui_scripts",
1145
                ],
1146
            }}
1147

1148
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
1149
        will be added as dependencies so that they are available on PYTHONPATH
1150
        during tests.
1151

1152
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
1153
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
1154
        `entry_points.txt` file will only include the entry points requested on this
1155
        field. This allows the tests, or the code under test, to lookup entry points'
1156
        metadata using an API like the `importlib.metadata.entry_points()` API in the
1157
        standard library (available on older Python interpreters via the
1158
        `importlib-metadata` distribution).
1159
        """
1160
    )
1161

1162

1163
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
1164
class PythonTestsTimeoutField(IntField):
13✔
1165
    alias = "timeout"
13✔
1166
    help = help_text(
13✔
1167
        """
1168
        A timeout (in seconds) used by each test file belonging to this target.
1169

1170
        If unset, will default to `[test].timeout_default`; if that option is also unset,
1171
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
1172
        applies if the option `--test-timeouts` is set to true (the default).
1173
        """
1174
    )
1175
    valid_numbers = ValidNumbers.positive_only
13✔
1176

1177
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
13✔
1178
        """Determine the timeout (in seconds) after resolving conflicting global options in the
1179
        `pytest` and `test` scopes.
1180

1181
        This function is deprecated and should be replaced by the similarly named one in
1182
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
1183
        """
1184

1185
        enabled = test.options.timeouts
×
1186
        timeout_default = test.options.timeout_default
×
1187
        timeout_maximum = test.options.timeout_maximum
×
1188

1189
        if not enabled:
×
1190
            return None
×
1191
        if self.value is None:
×
1192
            if timeout_default is None:
×
1193
                return None
×
1194
            result = cast(int, timeout_default)
×
1195
        else:
1196
            result = self.value
×
1197
        if timeout_maximum is not None:
×
1198
            return min(result, cast(int, timeout_maximum))
×
1199
        return result
×
1200

1201

1202
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
13✔
1203
    pass
13✔
1204

1205

1206
class PythonTestsXdistConcurrencyField(IntField):
13✔
1207
    alias = "xdist_concurrency"
13✔
1208
    help = help_text(
13✔
1209
        """
1210
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1211

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

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

1220
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1221
        """
1222
    )
1223

1224

1225
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
13✔
1226
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
13✔
1227

1228

1229
class SkipPythonTestsField(BoolField):
13✔
1230
    alias = "skip_tests"
13✔
1231
    default = False
13✔
1232
    help = "If true, don't run this target's tests."
13✔
1233

1234

1235
_PYTHON_TEST_MOVED_FIELDS = (
13✔
1236
    PythonTestsDependenciesField,
1237
    # This field is registered in the experimental backend for now.
1238
    # PythonTestsEntryPointDependenciesField,
1239
    PythonResolveField,
1240
    PythonRunGoalUseSandboxField,
1241
    PythonTestsTimeoutField,
1242
    PythonTestsXdistConcurrencyField,
1243
    PythonTestsBatchCompatibilityTagField,
1244
    RuntimePackageDependenciesField,
1245
    PythonTestsExtraEnvVarsField,
1246
    InterpreterConstraintsField,
1247
    SkipPythonTestsField,
1248
    EnvironmentField,
1249
)
1250

1251

1252
class PythonTestTarget(Target):
13✔
1253
    alias = "python_test"
13✔
1254
    core_fields = (
13✔
1255
        *COMMON_TARGET_FIELDS,
1256
        *_PYTHON_TEST_MOVED_FIELDS,
1257
        PythonTestsDependenciesField,
1258
        PythonTestSourceField,
1259
    )
1260
    help = help_text(
13✔
1261
        f"""
1262
        A single Python test file, written in either Pytest style or unittest style.
1263

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

1268
        See {doc_url("docs/python/goals/test")}
1269
        """
1270
    )
1271

1272

1273
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
13✔
1274
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
13✔
1275
    default = ("test_*.py", "*_test.py", "tests.py")
13✔
1276
    help = generate_multiple_sources_field_help_message(
13✔
1277
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1278
    )
1279

1280
    def validate_resolved_files(self, files: Sequence[str]) -> None:
13✔
1281
        super().validate_resolved_files(files)
×
1282
        # We don't technically need to error for `conftest.py` here because `PythonTestSourceField`
1283
        # already validates this, but we get a better error message this way so that users don't
1284
        # have to reason about generated targets.
1285
        conftest_files = [fp for fp in files if os.path.basename(fp) == "conftest.py"]
×
1286
        if conftest_files:
×
1287
            raise InvalidFieldException(
×
1288
                softwrap(
1289
                    f"""
1290
                    The {repr(self.alias)} field in target {self.address} should not include the
1291
                    file 'conftest.py', but included these: {conftest_files}.
1292

1293
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1294
                    You can run `{bin_name()} tailor` after removing the files from the
1295
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1296
                    `python_test_utils` target.
1297
                    """
1298
                )
1299
            )
1300

1301

1302
class PythonTestsOverrideField(OverridesField):
13✔
1303
    help = generate_file_based_overrides_field_help_message(
13✔
1304
        PythonTestTarget.alias,
1305
        """
1306
        overrides={
1307
            "foo_test.py": {"timeout": 120},
1308
            "bar_test.py": {"timeout": 200},
1309
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1310
        }
1311
        """,
1312
    )
1313

1314

1315
class PythonTestsGeneratorTarget(TargetFilesGenerator):
13✔
1316
    alias = "python_tests"
13✔
1317
    core_fields = (
13✔
1318
        *COMMON_TARGET_FIELDS,
1319
        PythonTestsGeneratingSourcesField,
1320
        PythonTestsOverrideField,
1321
    )
1322
    generated_target_cls = PythonTestTarget
13✔
1323
    copied_fields = COMMON_TARGET_FIELDS
13✔
1324
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
13✔
1325
    settings_request_cls = PythonFilesGeneratorSettingsRequest
13✔
1326
    help = "Generate a `python_test` target for each file in the `sources` field."
13✔
1327

1328

1329
# -----------------------------------------------------------------------------------------------
1330
# `python_source`, `python_sources`, and `python_test_utils` targets
1331
# -----------------------------------------------------------------------------------------------
1332

1333

1334
class PythonSourceTarget(Target):
13✔
1335
    alias = "python_source"
13✔
1336
    core_fields = (
13✔
1337
        *COMMON_TARGET_FIELDS,
1338
        InterpreterConstraintsField,
1339
        PythonDependenciesField,
1340
        PythonResolveField,
1341
        PythonRunGoalUseSandboxField,
1342
        PythonSourceField,
1343
        RestartableField,
1344
    )
1345
    help = "A single Python source file."
13✔
1346

1347

1348
class PythonSourcesOverridesField(OverridesField):
13✔
1349
    help = generate_file_based_overrides_field_help_message(
13✔
1350
        PythonSourceTarget.alias,
1351
        """
1352
        overrides={
1353
            "foo.py": {"skip_pylint": True]},
1354
            "bar.py": {"skip_flake8": True]},
1355
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1356
        }"
1357
        """,
1358
    )
1359

1360

1361
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
13✔
1362
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
13✔
1363
    help = generate_multiple_sources_field_help_message(
13✔
1364
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1365
    )
1366

1367

1368
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
13✔
1369
    default = (
13✔
1370
        ("*.py", "*.pyi")
1371
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1372
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1373
    )
1374
    help = generate_multiple_sources_field_help_message(
13✔
1375
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1376
    )
1377

1378

1379
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
13✔
1380
    alias = "python_test_utils"
13✔
1381
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1382
    core_fields = (
13✔
1383
        *COMMON_TARGET_FIELDS,
1384
        PythonTestUtilsGeneratingSourcesField,
1385
        PythonSourcesOverridesField,
1386
    )
1387
    generated_target_cls = PythonSourceTarget
13✔
1388
    copied_fields = COMMON_TARGET_FIELDS
13✔
1389
    moved_fields = (
13✔
1390
        PythonResolveField,
1391
        PythonRunGoalUseSandboxField,
1392
        PythonDependenciesField,
1393
        InterpreterConstraintsField,
1394
    )
1395
    settings_request_cls = PythonFilesGeneratorSettingsRequest
13✔
1396
    help = help_text(
13✔
1397
        """
1398
        Generate a `python_source` target for each file in the `sources` field.
1399

1400
        This target generator is intended for test utility files like `conftest.py` or
1401
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1402
        way as the `python_sources` target generator does, only that the `sources` field has a
1403
        different default. So it is valid to use `python_sources` instead. However, this target
1404
        can be helpful to better model your code by keeping separate test support files vs.
1405
        production files.
1406
        """
1407
    )
1408

1409

1410
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
13✔
1411
    alias = "python_sources"
13✔
1412
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1413
    core_fields = (
13✔
1414
        *COMMON_TARGET_FIELDS,
1415
        PythonSourcesGeneratingSourcesField,
1416
        PythonSourcesOverridesField,
1417
    )
1418
    generated_target_cls = PythonSourceTarget
13✔
1419
    copied_fields = COMMON_TARGET_FIELDS
13✔
1420
    moved_fields = (
13✔
1421
        PythonResolveField,
1422
        PythonRunGoalUseSandboxField,
1423
        PythonDependenciesField,
1424
        InterpreterConstraintsField,
1425
        RestartableField,
1426
    )
1427
    settings_request_cls = PythonFilesGeneratorSettingsRequest
13✔
1428
    help = help_text(
13✔
1429
        """
1430
        Generate a `python_source` target for each file in the `sources` field.
1431

1432
        You can either use this target generator or `python_test_utils` for test utility files
1433
        like `conftest.py`. They behave identically, but can help to better model and keep
1434
        separate test support files vs. production files.
1435
        """
1436
    )
1437

1438

1439
# -----------------------------------------------------------------------------------------------
1440
# `python_requirement` target
1441
# -----------------------------------------------------------------------------------------------
1442

1443

1444
class _PipRequirementSequenceField(Field):
13✔
1445
    value: tuple[PipRequirement, ...]
13✔
1446

1447
    @classmethod
13✔
1448
    def compute_value(
13✔
1449
        cls, raw_value: Iterable[str] | None, address: Address
1450
    ) -> tuple[PipRequirement, ...]:
1451
        value = super().compute_value(raw_value, address)
6✔
1452
        if value is None:
6✔
1453
            return ()
×
1454
        invalid_type_error = InvalidFieldTypeException(
6✔
1455
            address,
1456
            cls.alias,
1457
            value,
1458
            expected_type="an iterable of pip-style requirement strings (e.g. a list)",
1459
        )
1460
        if isinstance(value, str) or not isinstance(value, collections.abc.Iterable):
6✔
1461
            raise invalid_type_error
1✔
1462
        result = []
6✔
1463
        for v in value:
6✔
1464
            # We allow passing a pre-parsed `PipRequirement`. This is intended for macros which
1465
            # might have already parsed so that we can avoid parsing multiple times.
1466
            if isinstance(v, PipRequirement):
6✔
1467
                result.append(v)
1✔
1468
            elif isinstance(v, str):
6✔
1469
                try:
6✔
1470
                    parsed = PipRequirement.parse(
6✔
1471
                        v, description_of_origin=f"the '{cls.alias}' field for the target {address}"
1472
                    )
1473
                except ValueError as e:
1✔
1474
                    raise InvalidFieldException(e)
1✔
1475
                result.append(parsed)
6✔
1476
            else:
1477
                raise invalid_type_error
1✔
1478
        return tuple(result)
6✔
1479

1480

1481
class PythonRequirementDependenciesField(Dependencies):
13✔
1482
    pass
13✔
1483

1484

1485
class PythonRequirementsField(_PipRequirementSequenceField):
13✔
1486
    alias = "requirements"
13✔
1487
    required = True
13✔
1488
    help = help_text(
13✔
1489
        """
1490
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1491

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

1496
        If the requirement depends on some other requirement to work, such as needing
1497
        `setuptools` to be built, use the `dependencies` field instead.
1498
        """
1499
    )
1500

1501

1502
_default_module_mapping_url = git_url(
13✔
1503
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1504
)
1505

1506

1507
class PythonRequirementModulesField(StringSequenceField):
13✔
1508
    alias = "modules"
13✔
1509
    help = help_text(
13✔
1510
        f"""
1511
        The modules this requirement provides (used for dependency inference).
1512

1513
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1514
        "easy_install"]`.
1515

1516
        Usually you can leave this field off. If unspecified, Pants will first look at the
1517
        default module mapping ({_default_module_mapping_url}), and then will default to
1518
        the normalized project name. For example, the requirement `Django` would default to
1519
        the module `django`.
1520

1521
        Mutually exclusive with the `type_stub_modules` field.
1522
        """
1523
    )
1524

1525

1526
class PythonRequirementTypeStubModulesField(StringSequenceField):
13✔
1527
    alias = "type_stub_modules"
13✔
1528
    help = help_text(
13✔
1529
        f"""
1530
        The modules this requirement provides if the requirement is a type stub (used for
1531
        dependency inference).
1532

1533
        For example, the requirement `types-requests` provides `["requests"]`.
1534

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

1542
        Mutually exclusive with the `modules` field.
1543
        """
1544
    )
1545

1546

1547
def normalize_module_mapping(
13✔
1548
    mapping: Mapping[str, Iterable[str]] | None,
1549
) -> FrozenDict[str, tuple[str, ...]]:
1550
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
1✔
1551

1552

1553
class PythonRequirementResolveField(PythonResolveField):
13✔
1554
    alias = "resolve"
13✔
1555
    required = False
13✔
1556
    help = help_text(
13✔
1557
        """
1558
        The resolve from `[python].resolves` that this requirement is included in.
1559

1560
        If not defined, will default to `[python].default_resolve`.
1561

1562
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1563
        it will include all requirements that are declared with that resolve.
1564
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1565
        they use via their `resolve` field; so, for your first-party code to use a
1566
        particular `python_requirement` target, that requirement must be included in the resolve
1567
        used by that code.
1568
        """
1569
    )
1570

1571

1572
class PythonRequirementFindLinksField(StringSequenceField):
13✔
1573
    # NB: This is solely used for `pants_requirements` target generation
1574
    alias = "_find_links"
13✔
1575
    required = False
13✔
1576
    default = ()
13✔
1577
    help = "<Internal>"
13✔
1578

1579

1580
class PythonRequirementEntryPointField(EntryPointField):
13✔
1581
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1582
    pass
13✔
1583

1584

1585
class PythonRequirementTarget(Target):
13✔
1586
    alias = "python_requirement"
13✔
1587
    core_fields = (
13✔
1588
        *COMMON_TARGET_FIELDS,
1589
        PythonRequirementsField,
1590
        PythonRequirementDependenciesField,
1591
        PythonRequirementModulesField,
1592
        PythonRequirementTypeStubModulesField,
1593
        PythonRequirementResolveField,
1594
        PythonRequirementEntryPointField,
1595
        PythonRequirementFindLinksField,
1596
    )
1597
    help = help_text(
13✔
1598
        f"""
1599
        A Python requirement installable by pip.
1600

1601
        This target is useful when you want to declare Python requirements inline in a
1602
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1603
        the target generator `python_requirements` to convert each
1604
        requirement into a `python_requirement` target automatically. For Poetry, use
1605
        `poetry_requirements`.
1606

1607
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1608
        """
1609
    )
1610

1611
    def validate(self) -> None:
13✔
1612
        if (
5✔
1613
            self[PythonRequirementModulesField].value
1614
            and self[PythonRequirementTypeStubModulesField].value
1615
        ):
1616
            raise InvalidTargetException(
×
1617
                softwrap(
1618
                    f"""
1619
                    The `{self.alias}` target {self.address} cannot set both the
1620
                    `{self[PythonRequirementModulesField].alias}` and
1621
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1622
                    To fix, please remove one.
1623
                    """
1624
                )
1625
            )
1626

1627

1628
# -----------------------------------------------------------------------------------------------
1629
# `python_distribution` target
1630
# -----------------------------------------------------------------------------------------------
1631

1632

1633
# See `target_types_rules.py` for a dependency injection rule.
1634
class PythonDistributionDependenciesField(Dependencies):
13✔
1635
    supports_transitive_excludes = True
13✔
1636

1637

1638
class PythonProvidesField(ScalarField, AsyncFieldMixin):
13✔
1639
    alias = "provides"
13✔
1640
    expected_type = PythonArtifact
13✔
1641
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
13✔
1642
    value: PythonArtifact
13✔
1643
    required = True
13✔
1644
    help = help_text(
13✔
1645
        f"""
1646
        The setup.py kwargs for the external artifact built from this target.
1647

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

1652
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1653
        """
1654
    )
1655

1656
    @classmethod
13✔
1657
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
13✔
1658
        return cast(PythonArtifact, super().compute_value(raw_value, address))
1✔
1659

1660

1661
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
13✔
1662
    alias = "entry_points"
13✔
1663
    required = False
13✔
1664
    help = help_text(
13✔
1665
        f"""
1666
        Any entry points, such as `console_scripts` and `gui_scripts`.
1667

1668
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1669
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1670
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1671
        `pex_binary` target.
1672

1673
        Example:
1674

1675
            entry_points={{
1676
              "console_scripts": {{
1677
                "my-script": "project.app:main",
1678
                "another-script": "project/subdir:pex_binary_tgt"
1679
              }}
1680
            }}
1681

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

1688
        Pants will attempt to infer dependencies, which you can confirm by running:
1689

1690
            {bin_name()} dependencies <python_distribution target address>
1691
        """
1692
    )
1693

1694

1695
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
13✔
1696
    help = help_text(
13✔
1697
        """
1698
        The path to the directory to write the distribution file to, relative the dist directory.
1699

1700
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1701
        level of the dist dir.
1702
        """
1703
    )
1704
    alias = "output_path"
13✔
1705
    default = ""
13✔
1706

1707

1708
@dataclass(frozen=True)
13✔
1709
class PythonDistributionEntryPoint:
13✔
1710
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1711

1712
    entry_point: EntryPoint
13✔
1713
    pex_binary_address: Address | None
13✔
1714

1715

1716
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1717
@dataclass(frozen=True)
13✔
1718
class ResolvedPythonDistributionEntryPoints:
13✔
1719
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1720
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
13✔
1721

1722
    @property
13✔
1723
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
13✔
1724
        """Filters out all entry points from pex binary targets."""
1725
        return FrozenDict(
×
1726
            {
1727
                category: FrozenDict(
1728
                    {
1729
                        ep_name: ep_val.entry_point
1730
                        for ep_name, ep_val in entry_points.items()
1731
                        if not ep_val.pex_binary_address
1732
                    }
1733
                )
1734
                for category, entry_points in self.val.items()
1735
            }
1736
        )
1737

1738
    @property
13✔
1739
    def pex_binary_addresses(self) -> Addresses:
13✔
1740
        """Returns the addresses to all pex binary targets owning entry points used."""
1741
        return Addresses(
×
1742
            ep_val.pex_binary_address
1743
            for category, entry_points in self.val.items()
1744
            for ep_val in entry_points.values()
1745
            if ep_val.pex_binary_address
1746
        )
1747

1748

1749
@dataclass(frozen=True)
13✔
1750
class ResolvePythonDistributionEntryPointsRequest:
13✔
1751
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1752
    that should be resolved into a setuptools entry point.
1753

1754
    If the `entry_points_field` is present, inspect the specified entry points.
1755
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1756

1757
    This is to support inspecting one or the other depending on use case, using the same
1758
    logic for resolving pex_binary addresses etc.
1759
    """
1760

1761
    entry_points_field: PythonDistributionEntryPointsField | None = None
13✔
1762
    provides_field: PythonProvidesField | None = None
13✔
1763

1764
    def __post_init__(self):
13✔
1765
        # Must provide at least one of these fields.
1766
        assert self.entry_points_field or self.provides_field
1✔
1767

1768

1769
class WheelField(BoolField):
13✔
1770
    alias = "wheel"
13✔
1771
    default = True
13✔
1772
    help = "Whether to build a wheel for the distribution."
13✔
1773

1774

1775
class SDistField(BoolField):
13✔
1776
    alias = "sdist"
13✔
1777
    default = True
13✔
1778
    help = "Whether to build an sdist for the distribution."
13✔
1779

1780

1781
class ConfigSettingsField(DictStringToStringSequenceField):
13✔
1782
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1783

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

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

1789
    - Build frontends should support string values, and may also support other mechanisms
1790
      (apparently meaning other types).
1791

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

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

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

1807

1808
class WheelConfigSettingsField(ConfigSettingsField):
13✔
1809
    alias = "wheel_config_settings"
13✔
1810
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
13✔
1811

1812

1813
class SDistConfigSettingsField(ConfigSettingsField):
13✔
1814
    alias = "sdist_config_settings"
13✔
1815
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
13✔
1816

1817

1818
class BuildBackendEnvVarsField(StringSequenceField):
13✔
1819
    alias = "env_vars"
13✔
1820
    required = False
13✔
1821
    help = help_text(
13✔
1822
        """
1823
        Environment variables to set when running the PEP-517 build backend.
1824

1825
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1826
        or just `ENV_VAR` to copy the value from Pants's own environment.
1827
        """
1828
    )
1829

1830

1831
class GenerateSetupField(TriBoolField):
13✔
1832
    alias = "generate_setup"
13✔
1833
    required = False
13✔
1834
    # The default behavior if this field is unspecified is controlled by the
1835
    # --generate-setup-default option in the setup-py-generation scope.
1836
    default = None
13✔
1837

1838
    help = help_text(
13✔
1839
        """
1840
        Whether to generate setup information for this distribution, based on analyzing
1841
        sources and dependencies. Set to False to use existing setup information, such as
1842
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1843
        """
1844
    )
1845

1846

1847
class LongDescriptionPathField(StringField):
13✔
1848
    alias = "long_description_path"
13✔
1849
    required = False
13✔
1850

1851
    help = help_text(
13✔
1852
        """
1853
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1854

1855
        Path is relative to the build root.
1856

1857
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1858

1859
        This field won't automatically set `long_description_content_type` field for you.
1860
        You have to specify this field yourself in the `provides` field.
1861
        """
1862
    )
1863

1864

1865
class PythonDistribution(Target):
13✔
1866
    alias: ClassVar[str] = "python_distribution"
13✔
1867
    core_fields = (
13✔
1868
        *COMMON_TARGET_FIELDS,
1869
        InterpreterConstraintsField,
1870
        PythonDistributionDependenciesField,
1871
        PythonDistributionEntryPointsField,
1872
        PythonProvidesField,
1873
        GenerateSetupField,
1874
        WheelField,
1875
        SDistField,
1876
        WheelConfigSettingsField,
1877
        SDistConfigSettingsField,
1878
        BuildBackendEnvVarsField,
1879
        LongDescriptionPathField,
1880
        PythonDistributionOutputPathField,
1881
    )
1882
    help = help_text(
13✔
1883
        f"""
1884
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
1885

1886
        See {doc_url("docs/python/overview/building-distributions")}.
1887
        """
1888
    )
1889

1890

1891
# -----------------------------------------------------------------------------------------------
1892
# `vcs_version` target
1893
# -----------------------------------------------------------------------------------------------
1894

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

1902

1903
class VCSVersionDummySourceField(OptionalSingleSourceField):
13✔
1904
    """A dummy SourceField for participation in the codegen machinery."""
1905

1906
    alias = "_dummy_source"  # Leading underscore omits the field from help.
13✔
1907
    help = "A version string generated from VCS information"
13✔
1908

1909

1910
class VersionTagRegexField(StringField):
13✔
1911
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
13✔
1912
    alias = "tag_regex"
13✔
1913
    help = help_text(
13✔
1914
        """
1915
        A Python regex string to extract the version string from a VCS tag.
1916

1917
        The regex needs to contain either a single match group, or a group named version,
1918
        that captures the actual version information.
1919

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

1922
        See https://github.com/pypa/setuptools_scm for implementation details.
1923
        """
1924
    )
1925

1926

1927
class VersionGenerateToField(StringField):
13✔
1928
    required = True
13✔
1929
    alias = "generate_to"
13✔
1930
    help = help_text(
13✔
1931
        """
1932
        Generate the version data to this relative path, using the template field.
1933

1934
        Note that the generated output will not be written to disk in the source tree, but
1935
        will be available as a generated dependency to code that depends on this target.
1936
        """
1937
    )
1938

1939

1940
class VersionTemplateField(StringField):
13✔
1941
    required = True
13✔
1942
    alias = "template"
13✔
1943
    help = help_text(
13✔
1944
        """
1945
        Generate the version data using this format string, which takes a version format kwarg.
1946

1947
        E.g., `'version = "{version}"'`
1948
        """
1949
    )
1950

1951

1952
class VersionVersionSchemeField(StringField):
13✔
1953
    alias = "version_scheme"
13✔
1954
    help = help_text(
13✔
1955
        """
1956
        The version scheme to configure `setuptools_scm` to use.
1957
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
1958
        """
1959
    )
1960

1961

1962
class VersionLocalSchemeField(StringField):
13✔
1963
    alias = "local_scheme"
13✔
1964
    help = help_text(
13✔
1965
        """
1966
        The local scheme to configure `setuptools_scm` to use.
1967
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
1968
        """
1969
    )
1970

1971

1972
class VCSVersion(Target):
13✔
1973
    alias = "vcs_version"
13✔
1974
    core_fields = (
13✔
1975
        *COMMON_TARGET_FIELDS,
1976
        VersionTagRegexField,
1977
        VersionVersionSchemeField,
1978
        VersionLocalSchemeField,
1979
        VCSVersionDummySourceField,
1980
        VersionGenerateToField,
1981
        VersionTemplateField,
1982
    )
1983
    help = help_text(
13✔
1984
        f"""
1985
        Generates a version string from VCS state.
1986

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

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

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

© 2025 Coveralls, Inc