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

pantsbuild / pants / 20333307239

18 Dec 2025 10:07AM UTC coverage: 75.452% (-4.8%) from 80.295%
20333307239

Pull #22949

github

web-flow
Merge b07232683 into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

51 of 96 new or added lines in 5 files covered. (53.13%)

2857 existing lines in 120 files now uncovered.

66315 of 87890 relevant lines covered (75.45%)

2.78 hits per line

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

91.92
/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
9✔
5

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

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

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

67
logger = logging.getLogger(__name__)
9✔
68

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

72

73
# -----------------------------------------------------------------------------------------------
74
# Common fields
75
# -----------------------------------------------------------------------------------------------
76

77

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

82

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

86

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

90

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

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

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

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

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

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

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

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

135

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

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

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

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

162

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

166

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

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

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

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

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

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

195

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

200

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

204

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

209

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

214

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

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

223

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

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

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

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

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

295

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

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

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

308

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

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

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

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

329

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

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

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

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

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

361

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

366

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

373

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

378
    entry_point_field: EntryPointField
9✔
379

380

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

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

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

404

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

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

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

429

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

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

444

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

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

460

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

478

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

488

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

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

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

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

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

508

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

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

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

528

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

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

544

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

550

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

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

573

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

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

586

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

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

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

601
        return self.value
×
602

603

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

608

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

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

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

630

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

636

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

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

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

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

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

666

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

677

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

687

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

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

700

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

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

712

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

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

727

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

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

751
        NOTE: `pants run` will always run the "regular" PEX, use `package` to
752
        create scie PEXs.  """
753
    )
754

755

756
class ScieNameStyle(StrEnum):
9✔
757
    DYNAMIC = "dynamic"
9✔
758
    PLATFORM_PARENT_DIR = "platform-parent-dir"
9✔
759
    PLATFORM_FILE_SUFFIX = "platform-file-suffix"
9✔
760

761

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

778

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

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

809
        NOTE: This is only available for formal Python entry points
810
        <https://packaging.python.org/en/latest/specifications/entry-points/>
811
        and not the informal use by the `pex_binary` field `entry_point` to
812
        run first party files.
813
        """
814
    )
815

816

817
class PexScieBusyboxPexEntrypointEnvPassthrough(TriBoolField):
9✔
818
    alias = "scie_busybox_pex_entrypoint_env_passthrough"
9✔
819
    required = False
9✔
820
    default = None
9✔
821
    help = help_text(
9✔
822
        """ When creating a busybox, allow overriding the primary entrypoint
823
        at runtime via PEX_INTERPRETER, PEX_SCRIPT and PEX_MODULE. Note that
824
        when using the `venv` execution mode this adds modest startup overhead
825
        on the order of 10ms.  """
826
    )
827

828

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

854

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

869

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

885

886
class PexSciePbsStripped(TriBoolField):
9✔
887
    alias = "scie_pbs_stripped"
9✔
888
    required = False
9✔
889
    default = None
9✔
890
    help = help_text(
9✔
891
        """ Should the Python Standalone Builds CPython distributions used be
892
        stripped of debug symbols or not. For Linux and Windows particularly,
893
        the stripped distributions are less than half the size of the
894
        distributions that ship with debug symbols.  """
895
    )
896

897

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

912

913
_PEX_BINARY_COMMON_FIELDS = (
9✔
914
    EnvironmentField,
915
    InterpreterConstraintsField,
916
    PythonResolveField,
917
    PexBinaryDependenciesField,
918
    PexCheckField,
919
    PexCompletePlatformsField,
920
    PexInheritPathField,
921
    PexStripEnvField,
922
    PexIgnoreErrorsField,
923
    PexShBootField,
924
    PexShebangField,
925
    PexEmitWarningsField,
926
    PexLayoutField,
927
    PexExecutionModeField,
928
    PexIncludeRequirementsField,
929
    PexIncludeSourcesField,
930
    PexIncludeToolsField,
931
    PexVenvSitePackagesCopies,
932
    PexVenvHermeticScripts,
933
    PexExtraBuildArgsField,
934
    RestartableField,
935
)
936

937
_PEX_SCIE_BINARY_FIELDS = (
9✔
938
    PexScieField,
939
    PexScieNameStyleField,
940
    PexScieBusyBox,
941
    PexScieBusyboxPexEntrypointEnvPassthrough,
942
    PexSciePlatformField,
943
    PexSciePbsReleaseField,
944
    PexSciePythonVersion,
945
    PexSciePbsStripped,
946
    PexScieHashAlgField,
947
)
948

949

950
class PexBinary(Target):
9✔
951
    alias = "pex_binary"
9✔
952
    core_fields = (
9✔
953
        *COMMON_TARGET_FIELDS,
954
        *_PEX_BINARY_COMMON_FIELDS,
955
        *_PEX_SCIE_BINARY_FIELDS,
956
        PexEntryPointField,
957
        PexScriptField,
958
        PexExecutableField,
959
        PexArgsField,
960
        PexEnvField,
961
        OutputPathField,
962
    )
963
    help = help_text(
9✔
964
        f"""
965
        A Python target that can be converted into an executable PEX file.
966

967
        PEX files are self-contained executable files that contain a complete Python environment
968
        capable of running the target. For more information, see {doc_url("docs/python/overview/pex")}.
969
        """
970
    )
971

972
    def validate(self) -> None:
9✔
973
        got_entry_point = self[PexEntryPointField].value is not None
1✔
974
        got_script = self[PexScriptField].value is not None
1✔
975
        got_executable = self[PexExecutableField].value is not None
1✔
976

977
        if (got_entry_point + got_script + got_executable) > 1:
1✔
978
            raise InvalidTargetException(
1✔
979
                softwrap(
980
                    f"""
981
                    The `{self.alias}` target {self.address} cannot set more than one of the
982
                    `{self[PexEntryPointField].alias}`, `{self[PexScriptField].alias}`, and
983
                    `{self[PexExecutableField].alias}` fields at the same time.
984
                    To fix, please remove all but one.
985
                    """
986
                )
987
            )
988

989

990
class PexEntryPointsField(StringSequenceField, AsyncFieldMixin):
9✔
991
    alias = "entry_points"
9✔
992
    default = None
9✔
993
    help = help_text(
9✔
994
        """
995
        The entry points for each binary, i.e. what gets run when when executing `./my_app.pex.`
996

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

1001
        If you want the entry point to be for a third-party dependency or to use a console
1002
        script, use the `pex_binary` target directly.
1003
        """
1004
    )
1005

1006

1007
class PexBinariesOverrideField(OverridesField):
9✔
1008
    help = help_text(
9✔
1009
        f"""
1010
        Override the field values for generated `{PexBinary.alias}` targets.
1011

1012
        Expects a dictionary mapping values from the `entry_points` field to a dictionary for
1013
        their overrides. You may either use a single string or a tuple of strings to override
1014
        multiple targets.
1015

1016
        For example:
1017

1018
            overrides={{
1019
              "foo.py": {{"execution_mode": "venv"]}},
1020
              "bar.py:main": {{"restartable": True]}},
1021
              ("foo.py", "bar.py:main"): {{"tags": ["legacy"]}},
1022
            }}
1023

1024
        Every key is validated to belong to this target's `entry_points` field.
1025

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

1030
        You can specify the same `entry_point` in multiple keys, so long as you don't override the
1031
        same field more than one time for the `entry_point`.
1032
        """
1033
    )
1034

1035

1036
class PexBinariesGeneratorTarget(TargetGenerator):
9✔
1037
    alias = "pex_binaries"
9✔
1038
    help = help_text(
9✔
1039
        """
1040
        Generate a `pex_binary` target for each entry_point in the `entry_points` field.
1041

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

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

1062

1063
class PexBinaryDefaults(Subsystem):
9✔
1064
    options_scope = "pex-binary-defaults"
9✔
1065
    help = "Default settings for creating PEX executables."
9✔
1066

1067
    emit_warnings = BoolOption(
9✔
1068
        default=True,
1069
        help=softwrap(
1070
            """
1071
            Whether built PEX binaries should emit PEX warnings at runtime by default.
1072

1073
            Can be overridden by specifying the `emit_warnings` parameter of individual
1074
            `pex_binary` targets
1075
            """
1076
        ),
1077
        advanced=True,
1078
    )
1079

1080

1081
# -----------------------------------------------------------------------------------------------
1082
# `python_test` and `python_tests` targets
1083
# -----------------------------------------------------------------------------------------------
1084

1085

1086
class PythonTestSourceField(PythonSourceField):
9✔
1087
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
9✔
1088

1089
    def validate_resolved_files(self, files: Sequence[str]) -> None:
9✔
1090
        super().validate_resolved_files(files)
×
1091
        file = files[0]
×
1092
        file_name = os.path.basename(file)
×
1093
        if file_name == "conftest.py":
×
1094
            raise InvalidFieldException(
×
1095
                softwrap(
1096
                    f"""
1097
                    The {repr(self.alias)} field in target {self.address} should not be set to the
1098
                    file 'conftest.py', but was set to {repr(self.value)}.
1099

1100
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1101
                    You can run `{bin_name()} tailor` after removing this target ({self.address}) to
1102
                    autogenerate a `python_test_utils` target.
1103
                    """
1104
                )
1105
            )
1106

1107

1108
class PythonTestsDependenciesField(PythonDependenciesField):
9✔
1109
    supports_transitive_excludes = True
9✔
1110

1111

1112
class PythonTestsEntryPointDependenciesField(DictStringToStringSequenceField):
9✔
1113
    alias = "entry_point_dependencies"
9✔
1114
    help = help_text(
9✔
1115
        lambda: f"""
1116
        Dependencies on entry point metadata of `{PythonDistribution.alias}` targets.
1117

1118
        This is a dict where each key is a `{PythonDistribution.alias}` address
1119
        and the value is a list or tuple of entry point groups and/or entry points
1120
        on that target. The strings in the value list/tuple must be one of:
1121
        - "entry.point.group/entry-point-name" to depend on a named entry point
1122
        - "entry.point.group" (without a "/") to depend on an entry point group
1123
        - "*" to get all entry points on the target
1124

1125
        For example:
1126

1127
            {PythonTestsEntryPointDependenciesField.alias}={{
1128
                "//foo/address:dist_tgt": ["*"],  # all entry points
1129
                "bar:dist_tgt": ["console_scripts"],  # only from this group
1130
                "foo/bar/baz:dist_tgt": ["console_scripts/my-script"],  # a single entry point
1131
                "another:dist_tgt": [  # multiple entry points
1132
                    "console_scripts/my-script",
1133
                    "console_scripts/another-script",
1134
                    "entry.point.group/entry-point-name",
1135
                    "other.group",
1136
                    "gui_scripts",
1137
                ],
1138
            }}
1139

1140
        Code for matching `entry_points` on `{PythonDistribution.alias}` targets
1141
        will be added as dependencies so that they are available on PYTHONPATH
1142
        during tests.
1143

1144
        Plus, an `entry_points.txt` file will be generated in the sandbox so that
1145
        each of the `{PythonDistribution.alias}`s appear to be "installed". The
1146
        `entry_points.txt` file will only include the entry points requested on this
1147
        field. This allows the tests, or the code under test, to lookup entry points'
1148
        metadata using an API like the `importlib.metadata.entry_points()` API in the
1149
        standard library (available on older Python interpreters via the
1150
        `importlib-metadata` distribution).
1151
        """
1152
    )
1153

1154

1155
# TODO This field class should extend from a core `TestTimeoutField` once the deprecated options in `pytest` get removed.
1156
class PythonTestsTimeoutField(IntField):
9✔
1157
    alias = "timeout"
9✔
1158
    help = help_text(
9✔
1159
        """
1160
        A timeout (in seconds) used by each test file belonging to this target.
1161

1162
        If unset, will default to `[test].timeout_default`; if that option is also unset,
1163
        then the test will never time out. Will never exceed `[test].timeout_maximum`. Only
1164
        applies if the option `--test-timeouts` is set to true (the default).
1165
        """
1166
    )
1167
    valid_numbers = ValidNumbers.positive_only
9✔
1168

1169
    def calculate_from_global_options(self, test: TestSubsystem, pytest: PyTest) -> int | None:
9✔
1170
        """Determine the timeout (in seconds) after resolving conflicting global options in the
1171
        `pytest` and `test` scopes.
1172

1173
        This function is deprecated and should be replaced by the similarly named one in
1174
        `TestTimeoutField` once the deprecated options in the `pytest` scope are removed.
1175
        """
1176

1177
        enabled = test.options.timeouts
×
1178
        timeout_default = test.options.timeout_default
×
1179
        timeout_maximum = test.options.timeout_maximum
×
1180

1181
        if not enabled:
×
1182
            return None
×
1183
        if self.value is None:
×
1184
            if timeout_default is None:
×
1185
                return None
×
1186
            result = cast(int, timeout_default)
×
1187
        else:
1188
            result = self.value
×
1189
        if timeout_maximum is not None:
×
1190
            return min(result, cast(int, timeout_maximum))
×
1191
        return result
×
1192

1193

1194
class PythonTestsExtraEnvVarsField(TestExtraEnvVarsField):
9✔
1195
    pass
9✔
1196

1197

1198
class PythonTestsXdistConcurrencyField(IntField):
9✔
1199
    alias = "xdist_concurrency"
9✔
1200
    help = help_text(
9✔
1201
        """
1202
        Maximum number of CPUs to allocate to run each test file belonging to this target.
1203

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

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

1212
        Set this field to `0` to explicitly disable use of `pytest-xdist` for a target.
1213
        """
1214
    )
1215

1216

1217
class PythonTestsBatchCompatibilityTagField(TestsBatchCompatibilityTagField):
9✔
1218
    help = help_text(TestsBatchCompatibilityTagField.format_help("python_test", "pytest"))
9✔
1219

1220

1221
class SkipPythonTestsField(BoolField):
9✔
1222
    alias = "skip_tests"
9✔
1223
    default = False
9✔
1224
    help = "If true, don't run this target's tests."
9✔
1225

1226

1227
_PYTHON_TEST_MOVED_FIELDS = (
9✔
1228
    PythonTestsDependenciesField,
1229
    # This field is registered in the experimental backend for now.
1230
    # PythonTestsEntryPointDependenciesField,
1231
    PythonResolveField,
1232
    PythonRunGoalUseSandboxField,
1233
    PythonTestsTimeoutField,
1234
    PythonTestsXdistConcurrencyField,
1235
    PythonTestsBatchCompatibilityTagField,
1236
    RuntimePackageDependenciesField,
1237
    PythonTestsExtraEnvVarsField,
1238
    InterpreterConstraintsField,
1239
    SkipPythonTestsField,
1240
    EnvironmentField,
1241
)
1242

1243

1244
class PythonTestTarget(Target):
9✔
1245
    alias = "python_test"
9✔
1246
    core_fields = (
9✔
1247
        *COMMON_TARGET_FIELDS,
1248
        *_PYTHON_TEST_MOVED_FIELDS,
1249
        PythonTestsDependenciesField,
1250
        PythonTestSourceField,
1251
    )
1252
    help = help_text(
9✔
1253
        f"""
1254
        A single Python test file, written in either Pytest style or unittest style.
1255

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

1260
        See {doc_url("docs/python/goals/test")}
1261
        """
1262
    )
1263

1264

1265
class PythonTestsGeneratingSourcesField(PythonGeneratingSourcesBase):
9✔
1266
    expected_file_extensions = (".py", "")  # Note that this does not include `.pyi`.
9✔
1267
    default = ("test_*.py", "*_test.py", "tests.py")
9✔
1268
    help = generate_multiple_sources_field_help_message(
9✔
1269
        "Example: `sources=['test_*.py', '*_test.py', 'tests.py']`"
1270
    )
1271

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

1285
                    Instead, use a `python_source` target or the target generator `python_test_utils`.
1286
                    You can run `{bin_name()} tailor` after removing the files from the
1287
                    {repr(self.alias)} field of this target ({self.address}) to autogenerate a
1288
                    `python_test_utils` target.
1289
                    """
1290
                )
1291
            )
1292

1293

1294
class PythonTestsOverrideField(OverridesField):
9✔
1295
    help = generate_file_based_overrides_field_help_message(
9✔
1296
        PythonTestTarget.alias,
1297
        """
1298
        overrides={
1299
            "foo_test.py": {"timeout": 120},
1300
            "bar_test.py": {"timeout": 200},
1301
            ("foo_test.py", "bar_test.py"): {"tags": ["slow_tests"]},
1302
        }
1303
        """,
1304
    )
1305

1306

1307
class PythonTestsGeneratorTarget(TargetFilesGenerator):
9✔
1308
    alias = "python_tests"
9✔
1309
    core_fields = (
9✔
1310
        *COMMON_TARGET_FIELDS,
1311
        PythonTestsGeneratingSourcesField,
1312
        PythonTestsOverrideField,
1313
    )
1314
    generated_target_cls = PythonTestTarget
9✔
1315
    copied_fields = COMMON_TARGET_FIELDS
9✔
1316
    moved_fields = _PYTHON_TEST_MOVED_FIELDS
9✔
1317
    settings_request_cls = PythonFilesGeneratorSettingsRequest
9✔
1318
    help = "Generate a `python_test` target for each file in the `sources` field."
9✔
1319

1320

1321
# -----------------------------------------------------------------------------------------------
1322
# `python_source`, `python_sources`, and `python_test_utils` targets
1323
# -----------------------------------------------------------------------------------------------
1324

1325

1326
class PythonSourceTarget(Target):
9✔
1327
    alias = "python_source"
9✔
1328
    core_fields = (
9✔
1329
        *COMMON_TARGET_FIELDS,
1330
        InterpreterConstraintsField,
1331
        PythonDependenciesField,
1332
        PythonResolveField,
1333
        PythonRunGoalUseSandboxField,
1334
        PythonSourceField,
1335
        RestartableField,
1336
    )
1337
    help = "A single Python source file."
9✔
1338

1339

1340
class PythonSourcesOverridesField(OverridesField):
9✔
1341
    help = generate_file_based_overrides_field_help_message(
9✔
1342
        PythonSourceTarget.alias,
1343
        """
1344
        overrides={
1345
            "foo.py": {"skip_pylint": True]},
1346
            "bar.py": {"skip_flake8": True]},
1347
            ("foo.py", "bar.py"): {"tags": ["linter_disabled"]},
1348
        }"
1349
        """,
1350
    )
1351

1352

1353
class PythonTestUtilsGeneratingSourcesField(PythonGeneratingSourcesBase):
9✔
1354
    default = ("conftest.py", "test_*.pyi", "*_test.pyi", "tests.pyi")
9✔
1355
    help = generate_multiple_sources_field_help_message(
9✔
1356
        "Example: `sources=['conftest.py', 'test_*.pyi', '*_test.pyi', 'tests.pyi']`"
1357
    )
1358

1359

1360
class PythonSourcesGeneratingSourcesField(PythonGeneratingSourcesBase):
9✔
1361
    default = (
9✔
1362
        ("*.py", "*.pyi")
1363
        + tuple(f"!{pat}" for pat in PythonTestsGeneratingSourcesField.default)
1364
        + tuple(f"!{pat}" for pat in PythonTestUtilsGeneratingSourcesField.default)
1365
    )
1366
    help = generate_multiple_sources_field_help_message(
9✔
1367
        "Example: `sources=['example.py', 'new_*.py', '!old_ignore.py']`"
1368
    )
1369

1370

1371
class PythonTestUtilsGeneratorTarget(TargetFilesGenerator):
9✔
1372
    alias = "python_test_utils"
9✔
1373
    # Keep in sync with `PythonSourcesGeneratorTarget`, outside of the `sources` field.
1374
    core_fields = (
9✔
1375
        *COMMON_TARGET_FIELDS,
1376
        PythonTestUtilsGeneratingSourcesField,
1377
        PythonSourcesOverridesField,
1378
    )
1379
    generated_target_cls = PythonSourceTarget
9✔
1380
    copied_fields = COMMON_TARGET_FIELDS
9✔
1381
    moved_fields = (
9✔
1382
        PythonResolveField,
1383
        PythonRunGoalUseSandboxField,
1384
        PythonDependenciesField,
1385
        InterpreterConstraintsField,
1386
    )
1387
    settings_request_cls = PythonFilesGeneratorSettingsRequest
9✔
1388
    help = help_text(
9✔
1389
        """
1390
        Generate a `python_source` target for each file in the `sources` field.
1391

1392
        This target generator is intended for test utility files like `conftest.py` or
1393
        `my_test_utils.py`. Technically, it generates `python_source` targets in the exact same
1394
        way as the `python_sources` target generator does, only that the `sources` field has a
1395
        different default. So it is valid to use `python_sources` instead. However, this target
1396
        can be helpful to better model your code by keeping separate test support files vs.
1397
        production files.
1398
        """
1399
    )
1400

1401

1402
class PythonSourcesGeneratorTarget(TargetFilesGenerator):
9✔
1403
    alias = "python_sources"
9✔
1404
    # Keep in sync with `PythonTestUtilsGeneratorTarget`, outside of the `sources` field.
1405
    core_fields = (
9✔
1406
        *COMMON_TARGET_FIELDS,
1407
        PythonSourcesGeneratingSourcesField,
1408
        PythonSourcesOverridesField,
1409
    )
1410
    generated_target_cls = PythonSourceTarget
9✔
1411
    copied_fields = COMMON_TARGET_FIELDS
9✔
1412
    moved_fields = (
9✔
1413
        PythonResolveField,
1414
        PythonRunGoalUseSandboxField,
1415
        PythonDependenciesField,
1416
        InterpreterConstraintsField,
1417
        RestartableField,
1418
    )
1419
    settings_request_cls = PythonFilesGeneratorSettingsRequest
9✔
1420
    help = help_text(
9✔
1421
        """
1422
        Generate a `python_source` target for each file in the `sources` field.
1423

1424
        You can either use this target generator or `python_test_utils` for test utility files
1425
        like `conftest.py`. They behave identically, but can help to better model and keep
1426
        separate test support files vs. production files.
1427
        """
1428
    )
1429

1430

1431
# -----------------------------------------------------------------------------------------------
1432
# `python_requirement` target
1433
# -----------------------------------------------------------------------------------------------
1434

1435

1436
class _PipRequirementSequenceField(Field):
9✔
1437
    value: tuple[PipRequirement, ...]
9✔
1438

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

1472

1473
class PythonRequirementDependenciesField(Dependencies):
9✔
1474
    pass
9✔
1475

1476

1477
class PythonRequirementsField(_PipRequirementSequenceField):
9✔
1478
    alias = "requirements"
9✔
1479
    required = True
9✔
1480
    help = help_text(
9✔
1481
        """
1482
        A pip-style requirement string, e.g. `["Django==3.2.8"]`.
1483

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

1488
        If the requirement depends on some other requirement to work, such as needing
1489
        `setuptools` to be built, use the `dependencies` field instead.
1490
        """
1491
    )
1492

1493

1494
_default_module_mapping_url = git_url(
9✔
1495
    "src/python/pants/backend/python/dependency_inference/default_module_mapping.py"
1496
)
1497

1498

1499
class PythonRequirementModulesField(StringSequenceField):
9✔
1500
    alias = "modules"
9✔
1501
    help = help_text(
9✔
1502
        f"""
1503
        The modules this requirement provides (used for dependency inference).
1504

1505
        For example, the requirement `setuptools` provides `["setuptools", "pkg_resources",
1506
        "easy_install"]`.
1507

1508
        Usually you can leave this field off. If unspecified, Pants will first look at the
1509
        default module mapping ({_default_module_mapping_url}), and then will default to
1510
        the normalized project name. For example, the requirement `Django` would default to
1511
        the module `django`.
1512

1513
        Mutually exclusive with the `type_stub_modules` field.
1514
        """
1515
    )
1516

1517

1518
class PythonRequirementTypeStubModulesField(StringSequenceField):
9✔
1519
    alias = "type_stub_modules"
9✔
1520
    help = help_text(
9✔
1521
        f"""
1522
        The modules this requirement provides if the requirement is a type stub (used for
1523
        dependency inference).
1524

1525
        For example, the requirement `types-requests` provides `["requests"]`.
1526

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

1534
        Mutually exclusive with the `modules` field.
1535
        """
1536
    )
1537

1538

1539
def normalize_module_mapping(
9✔
1540
    mapping: Mapping[str, Iterable[str]] | None,
1541
) -> FrozenDict[str, tuple[str, ...]]:
1542
    return FrozenDict({canonicalize_project_name(k): tuple(v) for k, v in (mapping or {}).items()})
1✔
1543

1544

1545
class PythonRequirementResolveField(PythonResolveField):
9✔
1546
    alias = "resolve"
9✔
1547
    required = False
9✔
1548
    help = help_text(
9✔
1549
        """
1550
        The resolve from `[python].resolves` that this requirement is included in.
1551

1552
        If not defined, will default to `[python].default_resolve`.
1553

1554
        When generating a lockfile for a particular resolve via the `generate-lockfiles` goal,
1555
        it will include all requirements that are declared with that resolve.
1556
        First-party targets like `python_source` and `pex_binary` then declare which resolve
1557
        they use via their `resolve` field; so, for your first-party code to use a
1558
        particular `python_requirement` target, that requirement must be included in the resolve
1559
        used by that code.
1560
        """
1561
    )
1562

1563

1564
class PythonRequirementFindLinksField(StringSequenceField):
9✔
1565
    # NB: This is solely used for `pants_requirements` target generation
1566
    alias = "_find_links"
9✔
1567
    required = False
9✔
1568
    default = ()
9✔
1569
    help = "<Internal>"
9✔
1570

1571

1572
class PythonRequirementEntryPointField(EntryPointField):
9✔
1573
    # Specialist subclass for matching `PythonRequirementTarget` when running.
1574
    pass
9✔
1575

1576

1577
class PythonRequirementTarget(Target):
9✔
1578
    alias = "python_requirement"
9✔
1579
    core_fields = (
9✔
1580
        *COMMON_TARGET_FIELDS,
1581
        PythonRequirementsField,
1582
        PythonRequirementDependenciesField,
1583
        PythonRequirementModulesField,
1584
        PythonRequirementTypeStubModulesField,
1585
        PythonRequirementResolveField,
1586
        PythonRequirementEntryPointField,
1587
        PythonRequirementFindLinksField,
1588
    )
1589
    help = help_text(
9✔
1590
        f"""
1591
        A Python requirement installable by pip.
1592

1593
        This target is useful when you want to declare Python requirements inline in a
1594
        BUILD file. If you have a `requirements.txt` file already, you can instead use
1595
        the target generator `python_requirements` to convert each
1596
        requirement into a `python_requirement` target automatically. For Poetry, use
1597
        `poetry_requirements`.
1598

1599
        See {doc_url("docs/python/overview/third-party-dependencies")}.
1600
        """
1601
    )
1602

1603
    def validate(self) -> None:
9✔
1604
        if (
4✔
1605
            self[PythonRequirementModulesField].value
1606
            and self[PythonRequirementTypeStubModulesField].value
1607
        ):
1608
            raise InvalidTargetException(
×
1609
                softwrap(
1610
                    f"""
1611
                    The `{self.alias}` target {self.address} cannot set both the
1612
                    `{self[PythonRequirementModulesField].alias}` and
1613
                    `{self[PythonRequirementTypeStubModulesField].alias}` fields at the same time.
1614
                    To fix, please remove one.
1615
                    """
1616
                )
1617
            )
1618

1619

1620
# -----------------------------------------------------------------------------------------------
1621
# `python_distribution` target
1622
# -----------------------------------------------------------------------------------------------
1623

1624

1625
# See `target_types_rules.py` for a dependency injection rule.
1626
class PythonDistributionDependenciesField(Dependencies):
9✔
1627
    supports_transitive_excludes = True
9✔
1628

1629

1630
class PythonProvidesField(ScalarField, AsyncFieldMixin):
9✔
1631
    alias = "provides"
9✔
1632
    expected_type = PythonArtifact
9✔
1633
    expected_type_help = "python_artifact(name='my-dist', **kwargs)"
9✔
1634
    value: PythonArtifact
9✔
1635
    required = True
9✔
1636
    help = help_text(
9✔
1637
        f"""
1638
        The setup.py kwargs for the external artifact built from this target.
1639

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

1644
        See {doc_url("docs/writing-plugins/common-plugin-tasks/custom-python-artifact-kwargs")} for how to write a plugin to dynamically generate kwargs.
1645
        """
1646
    )
1647

1648
    @classmethod
9✔
1649
    def compute_value(cls, raw_value: PythonArtifact | None, address: Address) -> PythonArtifact:
9✔
UNCOV
1650
        return cast(PythonArtifact, super().compute_value(raw_value, address))
×
1651

1652

1653
class PythonDistributionEntryPointsField(NestedDictStringToStringField, AsyncFieldMixin):
9✔
1654
    alias = "entry_points"
9✔
1655
    required = False
9✔
1656
    help = help_text(
9✔
1657
        f"""
1658
        Any entry points, such as `console_scripts` and `gui_scripts`.
1659

1660
        Specify as a nested dictionary, with a dictionary for each type of entry point,
1661
        e.g. `console_scripts` vs. `gui_scripts`. Each dictionary maps the entry point name to
1662
        either a setuptools entry point (`"path.to.module:func"`) or a Pants target address to a
1663
        `pex_binary` target.
1664

1665
        Example:
1666

1667
            entry_points={{
1668
              "console_scripts": {{
1669
                "my-script": "project.app:main",
1670
                "another-script": "project/subdir:pex_binary_tgt"
1671
              }}
1672
            }}
1673

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

1680
        Pants will attempt to infer dependencies, which you can confirm by running:
1681

1682
            {bin_name()} dependencies <python_distribution target address>
1683
        """
1684
    )
1685

1686

1687
class PythonDistributionOutputPathField(StringField, AsyncFieldMixin):
9✔
1688
    help = help_text(
9✔
1689
        """
1690
        The path to the directory to write the distribution file to, relative the dist directory.
1691

1692
        If undefined, this defaults to the empty path, i.e. the output goes at the top
1693
        level of the dist dir.
1694
        """
1695
    )
1696
    alias = "output_path"
9✔
1697
    default = ""
9✔
1698

1699

1700
@dataclass(frozen=True)
9✔
1701
class PythonDistributionEntryPoint:
9✔
1702
    """Note that this stores if the entry point comes from an address to a `pex_binary` target."""
1703

1704
    entry_point: EntryPoint
9✔
1705
    pex_binary_address: Address | None
9✔
1706

1707

1708
# See `target_type_rules.py` for the `Resolve..Request -> Resolved..` rule
1709
@dataclass(frozen=True)
9✔
1710
class ResolvedPythonDistributionEntryPoints:
9✔
1711
    # E.g. {"console_scripts": {"ep": PythonDistributionEntryPoint(...)}}.
1712
    val: FrozenDict[str, FrozenDict[str, PythonDistributionEntryPoint]] = FrozenDict()
9✔
1713

1714
    @property
9✔
1715
    def explicit_modules(self) -> FrozenDict[str, FrozenDict[str, EntryPoint]]:
9✔
1716
        """Filters out all entry points from pex binary targets."""
1717
        return FrozenDict(
×
1718
            {
1719
                category: FrozenDict(
1720
                    {
1721
                        ep_name: ep_val.entry_point
1722
                        for ep_name, ep_val in entry_points.items()
1723
                        if not ep_val.pex_binary_address
1724
                    }
1725
                )
1726
                for category, entry_points in self.val.items()
1727
            }
1728
        )
1729

1730
    @property
9✔
1731
    def pex_binary_addresses(self) -> Addresses:
9✔
1732
        """Returns the addresses to all pex binary targets owning entry points used."""
1733
        return Addresses(
×
1734
            ep_val.pex_binary_address
1735
            for category, entry_points in self.val.items()
1736
            for ep_val in entry_points.values()
1737
            if ep_val.pex_binary_address
1738
        )
1739

1740

1741
@dataclass(frozen=True)
9✔
1742
class ResolvePythonDistributionEntryPointsRequest:
9✔
1743
    """Looks at the entry points to see if it is a setuptools entry point, or a BUILD target address
1744
    that should be resolved into a setuptools entry point.
1745

1746
    If the `entry_points_field` is present, inspect the specified entry points.
1747
    If the `provides_field` is present, inspect the `provides_field.kwargs["entry_points"]`.
1748

1749
    This is to support inspecting one or the other depending on use case, using the same
1750
    logic for resolving pex_binary addresses etc.
1751
    """
1752

1753
    entry_points_field: PythonDistributionEntryPointsField | None = None
9✔
1754
    provides_field: PythonProvidesField | None = None
9✔
1755

1756
    def __post_init__(self):
9✔
1757
        # Must provide at least one of these fields.
1758
        assert self.entry_points_field or self.provides_field
1✔
1759

1760

1761
class WheelField(BoolField):
9✔
1762
    alias = "wheel"
9✔
1763
    default = True
9✔
1764
    help = "Whether to build a wheel for the distribution."
9✔
1765

1766

1767
class SDistField(BoolField):
9✔
1768
    alias = "sdist"
9✔
1769
    default = True
9✔
1770
    help = "Whether to build an sdist for the distribution."
9✔
1771

1772

1773
class ConfigSettingsField(DictStringToStringSequenceField):
9✔
1774
    """Values for config_settings (see https://www.python.org/dev/peps/pep-0517/#config-settings).
1775

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

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

1781
    - Build frontends should support string values, and may also support other mechanisms
1782
      (apparently meaning other types).
1783

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

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

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

1799

1800
class WheelConfigSettingsField(ConfigSettingsField):
9✔
1801
    alias = "wheel_config_settings"
9✔
1802
    help = "PEP-517 config settings to pass to the build backend when building a wheel."
9✔
1803

1804

1805
class SDistConfigSettingsField(ConfigSettingsField):
9✔
1806
    alias = "sdist_config_settings"
9✔
1807
    help = "PEP-517 config settings to pass to the build backend when building an sdist."
9✔
1808

1809

1810
class BuildBackendEnvVarsField(StringSequenceField):
9✔
1811
    alias = "env_vars"
9✔
1812
    required = False
9✔
1813
    help = help_text(
9✔
1814
        """
1815
        Environment variables to set when running the PEP-517 build backend.
1816

1817
        Entries are either strings in the form `ENV_VAR=value` to set an explicit value;
1818
        or just `ENV_VAR` to copy the value from Pants's own environment.
1819
        """
1820
    )
1821

1822

1823
class GenerateSetupField(TriBoolField):
9✔
1824
    alias = "generate_setup"
9✔
1825
    required = False
9✔
1826
    # The default behavior if this field is unspecified is controlled by the
1827
    # --generate-setup-default option in the setup-py-generation scope.
1828
    default = None
9✔
1829

1830
    help = help_text(
9✔
1831
        """
1832
        Whether to generate setup information for this distribution, based on analyzing
1833
        sources and dependencies. Set to False to use existing setup information, such as
1834
        existing `setup.py`, `setup.cfg`, `pyproject.toml` files or similar.
1835
        """
1836
    )
1837

1838

1839
class LongDescriptionPathField(StringField):
9✔
1840
    alias = "long_description_path"
9✔
1841
    required = False
9✔
1842

1843
    help = help_text(
9✔
1844
        """
1845
        Path to a file that will be used to fill the `long_description` field in `setup.py`.
1846

1847
        Path is relative to the build root.
1848

1849
        Alternatively, you can set the `long_description` in the `provides` field, but not both.
1850

1851
        This field won't automatically set `long_description_content_type` field for you.
1852
        You have to specify this field yourself in the `provides` field.
1853
        """
1854
    )
1855

1856

1857
class PythonDistribution(Target):
9✔
1858
    alias: ClassVar[str] = "python_distribution"
9✔
1859
    core_fields = (
9✔
1860
        *COMMON_TARGET_FIELDS,
1861
        InterpreterConstraintsField,
1862
        PythonDistributionDependenciesField,
1863
        PythonDistributionEntryPointsField,
1864
        PythonProvidesField,
1865
        GenerateSetupField,
1866
        WheelField,
1867
        SDistField,
1868
        WheelConfigSettingsField,
1869
        SDistConfigSettingsField,
1870
        BuildBackendEnvVarsField,
1871
        LongDescriptionPathField,
1872
        PythonDistributionOutputPathField,
1873
    )
1874
    help = help_text(
9✔
1875
        f"""
1876
        A publishable Python setuptools distribution (e.g. an sdist or wheel).
1877

1878
        See {doc_url("docs/python/overview/building-distributions")}.
1879
        """
1880
    )
1881

1882

1883
# -----------------------------------------------------------------------------------------------
1884
# `vcs_version` target
1885
# -----------------------------------------------------------------------------------------------
1886

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

1894

1895
class VCSVersionDummySourceField(OptionalSingleSourceField):
9✔
1896
    """A dummy SourceField for participation in the codegen machinery."""
1897

1898
    alias = "_dummy_source"  # Leading underscore omits the field from help.
9✔
1899
    help = "A version string generated from VCS information"
9✔
1900

1901

1902
class VersionTagRegexField(StringField):
9✔
1903
    default = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
9✔
1904
    alias = "tag_regex"
9✔
1905
    help = help_text(
9✔
1906
        """
1907
        A Python regex string to extract the version string from a VCS tag.
1908

1909
        The regex needs to contain either a single match group, or a group named version,
1910
        that captures the actual version information.
1911

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

1914
        See https://github.com/pypa/setuptools_scm for implementation details.
1915
        """
1916
    )
1917

1918

1919
class VersionGenerateToField(StringField):
9✔
1920
    required = True
9✔
1921
    alias = "generate_to"
9✔
1922
    help = help_text(
9✔
1923
        """
1924
        Generate the version data to this relative path, using the template field.
1925

1926
        Note that the generated output will not be written to disk in the source tree, but
1927
        will be available as a generated dependency to code that depends on this target.
1928
        """
1929
    )
1930

1931

1932
class VersionTemplateField(StringField):
9✔
1933
    required = True
9✔
1934
    alias = "template"
9✔
1935
    help = help_text(
9✔
1936
        """
1937
        Generate the version data using this format string, which takes a version format kwarg.
1938

1939
        E.g., `'version = "{version}"'`
1940
        """
1941
    )
1942

1943

1944
class VersionVersionSchemeField(StringField):
9✔
1945
    alias = "version_scheme"
9✔
1946
    help = help_text(
9✔
1947
        """
1948
        The version scheme to configure `setuptools_scm` to use.
1949
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations
1950
        """
1951
    )
1952

1953

1954
class VersionLocalSchemeField(StringField):
9✔
1955
    alias = "local_scheme"
9✔
1956
    help = help_text(
9✔
1957
        """
1958
        The local scheme to configure `setuptools_scm` to use.
1959
        See https://setuptools-scm.readthedocs.io/en/latest/extending/#available-implementations_1
1960
        """
1961
    )
1962

1963

1964
class VCSVersion(Target):
9✔
1965
    alias = "vcs_version"
9✔
1966
    core_fields = (
9✔
1967
        *COMMON_TARGET_FIELDS,
1968
        VersionTagRegexField,
1969
        VersionVersionSchemeField,
1970
        VersionLocalSchemeField,
1971
        VCSVersionDummySourceField,
1972
        VersionGenerateToField,
1973
        VersionTemplateField,
1974
    )
1975
    help = help_text(
9✔
1976
        f"""
1977
        Generates a version string from VCS state.
1978

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

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

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