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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

0.0
/src/python/pants/backend/python/util_rules/vcs_versioning.py
1
# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

UNCOV
4
from __future__ import annotations
×
5

UNCOV
6
from collections import defaultdict
×
UNCOV
7
from typing import DefaultDict, cast
×
8

UNCOV
9
import toml
×
10

UNCOV
11
from pants.backend.python.dependency_inference.module_mapper import (
×
12
    FirstPartyPythonMappingImpl,
13
    FirstPartyPythonMappingImplMarker,
14
    ModuleProvider,
15
    ModuleProviderType,
16
    ResolveName,
17
)
UNCOV
18
from pants.backend.python.subsystems.setup import PythonSetup
×
UNCOV
19
from pants.backend.python.subsystems.setuptools_scm import SetuptoolsSCM
×
UNCOV
20
from pants.backend.python.target_types import (
×
21
    PythonResolveField,
22
    PythonSourceField,
23
    VCSVersion,
24
    VCSVersionDummySourceField,
25
    VersionGenerateToField,
26
    VersionLocalSchemeField,
27
    VersionTagRegexField,
28
    VersionTemplateField,
29
    VersionVersionSchemeField,
30
)
UNCOV
31
from pants.backend.python.util_rules.pex import PexRequest, VenvPexProcess, create_venv_pex
×
UNCOV
32
from pants.core.util_rules.stripped_source_files import StrippedFileNameRequest, strip_file_name
×
UNCOV
33
from pants.engine.environment import ChosenLocalEnvironmentName, EnvironmentName
×
UNCOV
34
from pants.engine.fs import CreateDigest, FileContent
×
UNCOV
35
from pants.engine.internals.selectors import concurrently
×
UNCOV
36
from pants.engine.intrinsics import create_digest, digest_to_snapshot, execute_process
×
UNCOV
37
from pants.engine.process import ProcessCacheScope
×
UNCOV
38
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
39
from pants.engine.target import AllTargets, GeneratedSources, GenerateSourcesRequest, Targets
×
UNCOV
40
from pants.engine.unions import UnionRule
×
UNCOV
41
from pants.util.logging import LogLevel
×
UNCOV
42
from pants.util.strutil import softwrap
×
UNCOV
43
from pants.vcs.git import GitWorktreeRequest, get_git_worktree
×
44

45

UNCOV
46
class VCSVersioningError(Exception):
×
UNCOV
47
    pass
×
48

49

50
# Note that even though setuptools_scm is Python-centric, we could easily use it to generate
51
# version data for use in other languages!
UNCOV
52
class GeneratePythonFromSetuptoolsSCMRequest(GenerateSourcesRequest):
×
UNCOV
53
    input = VCSVersionDummySourceField
×
UNCOV
54
    output = PythonSourceField
×
55

56

UNCOV
57
@rule
×
UNCOV
58
async def generate_python_from_setuptools_scm(
×
59
    request: GeneratePythonFromSetuptoolsSCMRequest,
60
    setuptools_scm: SetuptoolsSCM,
61
    local_environment_name: ChosenLocalEnvironmentName,
62
) -> GeneratedSources:
63
    # A MaybeGitWorktree is uncacheable, so this enclosing rule will run every time its result
64
    # is needed, and the process invocation below caches at session scope, meaning this rule
65
    # will always return a result based on the current underlying git state.
66
    maybe_git_worktree = await get_git_worktree(
×
67
        **implicitly(
68
            {GitWorktreeRequest(): GitWorktreeRequest, local_environment_name.val: EnvironmentName}
69
        )
70
    )
71
    if not maybe_git_worktree.git_worktree:
×
72
        raise VCSVersioningError(
×
73
            softwrap(
74
                f"""
75
                Trying to determine the version for the {request.protocol_target.address} target at
76
                {request.protocol_target.address}, but {maybe_git_worktree.failure_reason}.
77
                """
78
            )
79
        )
80

81
    # Generate the setuptools_scm config. We don't use any existing pyproject.toml config,
82
    # because we don't want to let setuptools_scm itself write the output file. This is because
83
    # it would do so relative to the --root, meaning it will write outside the sandbox,
84
    # directly into the workspace, which is obviously not what we want.
85
    # It's unfortunate that setuptools_scm does not have separate config for "where is the .git
86
    # directory" and "where should I write output to".
87
    config: dict[str, dict[str, dict[str, str]]] = {}
×
88
    tool_config = config.setdefault("tool", {}).setdefault("setuptools_scm", {})
×
89
    if tag_regex := request.protocol_target[VersionTagRegexField].value:
×
90
        tool_config["tag_regex"] = tag_regex
×
91
    if version_scheme := request.protocol_target[VersionVersionSchemeField].value:
×
92
        tool_config["version_scheme"] = version_scheme
×
93
    if local_scheme := request.protocol_target[VersionLocalSchemeField].value:
×
94
        tool_config["local_scheme"] = local_scheme
×
95
    config_path = "pyproject.synthetic.toml"
×
96

97
    input_digest_get = create_digest(
×
98
        CreateDigest(
99
            [
100
                FileContent(config_path, toml.dumps(config).encode()),
101
            ]
102
        )
103
    )
104

105
    setuptools_scm_pex_get = create_venv_pex(
×
106
        **implicitly(
107
            {
108
                setuptools_scm.to_pex_request(): PexRequest,
109
                local_environment_name.val: EnvironmentName,
110
            }
111
        )
112
    )
113
    setuptools_scm_pex, input_digest = await concurrently(setuptools_scm_pex_get, input_digest_get)
×
114

115
    argv = ["--root", str(maybe_git_worktree.git_worktree.worktree), "--config", config_path]
×
116

117
    result = await execute_process(
×
118
        **implicitly(
119
            {
120
                VenvPexProcess(
121
                    setuptools_scm_pex,
122
                    argv=argv,
123
                    input_digest=input_digest,
124
                    description=f"Run setuptools_scm for {request.protocol_target.address.spec}",
125
                    level=LogLevel.INFO,
126
                    cache_scope=ProcessCacheScope.PER_SESSION,
127
                ): VenvPexProcess,
128
                local_environment_name.val: EnvironmentName,
129
            }
130
        ),
131
    )
132
    version = result.stdout.decode().strip()
×
133
    write_to = cast(str, request.protocol_target[VersionGenerateToField].value)
×
134
    write_to_template = cast(str, request.protocol_target[VersionTemplateField].value)
×
135
    output_content = write_to_template.format(version=version)
×
136
    output_snapshot = await digest_to_snapshot(
×
137
        **implicitly(CreateDigest([FileContent(write_to, output_content.encode())]))
138
    )
139
    return GeneratedSources(output_snapshot)
×
140

141

142
# This is only used to register our implementation with the plugin hook via unions.
UNCOV
143
class PythonVCSVersionMappingMarker(FirstPartyPythonMappingImplMarker):
×
UNCOV
144
    pass
×
145

146

UNCOV
147
class VCSVersionPythonResolveField(PythonResolveField):
×
UNCOV
148
    alias = "python_resolve"
×
149

150

UNCOV
151
class AllVCSVersionTargets(Targets):
×
152
    # This class exists so map_to_python_modules isn't invalidated on any change to any target.
UNCOV
153
    pass
×
154

155

UNCOV
156
@rule(desc="Find all vcs_version targets in project", level=LogLevel.DEBUG)
×
UNCOV
157
async def find_all_vcs_version_targets(targets: AllTargets) -> AllVCSVersionTargets:
×
158
    return AllVCSVersionTargets(tgt for tgt in targets if tgt.has_field(VersionGenerateToField))
×
159

160

UNCOV
161
@rule
×
UNCOV
162
async def map_to_python_modules(
×
163
    vcs_version_targets: AllVCSVersionTargets,
164
    python_setup: PythonSetup,
165
    _: PythonVCSVersionMappingMarker,
166
) -> FirstPartyPythonMappingImpl:
167
    suffix = ".py"
×
168

169
    targets = [
×
170
        tgt
171
        for tgt in vcs_version_targets
172
        if cast(str, tgt[VersionGenerateToField].value).endswith(suffix)
173
    ]
174
    stripped_files = await concurrently(
×
175
        strip_file_name(StrippedFileNameRequest(cast(str, tgt[VersionGenerateToField].value)))
176
        for tgt in targets
177
    )
178
    resolves_to_modules_to_providers: DefaultDict[
×
179
        ResolveName, DefaultDict[str, list[ModuleProvider]]
180
    ] = defaultdict(lambda: defaultdict(list))
181
    for tgt, stripped_file in zip(targets, stripped_files):
×
182
        resolve = tgt[PythonResolveField].normalized_value(python_setup)
×
183
        module = stripped_file.value[: -len(suffix)].replace("/", ".")
×
184
        resolves_to_modules_to_providers[resolve][module].append(
×
185
            ModuleProvider(tgt.address, ModuleProviderType.IMPL)
186
        )
187
    return FirstPartyPythonMappingImpl.create(resolves_to_modules_to_providers)
×
188

189

UNCOV
190
def rules():
×
UNCOV
191
    return (
×
192
        *collect_rules(),
193
        UnionRule(GenerateSourcesRequest, GeneratePythonFromSetuptoolsSCMRequest),
194
        UnionRule(FirstPartyPythonMappingImplMarker, PythonVCSVersionMappingMarker),
195
        VCSVersion.register_plugin_field(VCSVersionPythonResolveField),
196
    )
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