• 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/goals/publish.py
1
# Copyright 2021 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
import logging
×
UNCOV
7
from dataclasses import dataclass
×
8

UNCOV
9
from pants.backend.python.subsystems.twine import TwineSubsystem
×
UNCOV
10
from pants.backend.python.target_types import PythonDistribution
×
UNCOV
11
from pants.backend.python.util_rules.pex import (
×
12
    VenvPexProcess,
13
    create_venv_pex,
14
    setup_venv_pex_process,
15
)
UNCOV
16
from pants.core.goals.publish import (
×
17
    PublishFieldSet,
18
    PublishOutputData,
19
    PublishPackages,
20
    PublishProcesses,
21
    PublishRequest,
22
)
UNCOV
23
from pants.core.util_rules.config_files import ConfigFiles, find_config_file
×
UNCOV
24
from pants.core.util_rules.env_vars import environment_vars_subset
×
UNCOV
25
from pants.engine.env_vars import EnvironmentVars, EnvironmentVarsRequest
×
UNCOV
26
from pants.engine.fs import CreateDigest, MergeDigests, Snapshot
×
UNCOV
27
from pants.engine.intrinsics import digest_to_snapshot, merge_digests
×
UNCOV
28
from pants.engine.process import InteractiveProcess
×
UNCOV
29
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
30
from pants.engine.target import BoolField, StringSequenceField
×
UNCOV
31
from pants.option.global_options import GlobalOptions
×
UNCOV
32
from pants.util.strutil import help_text
×
33

UNCOV
34
logger = logging.getLogger(__name__)
×
35

36

UNCOV
37
class PythonRepositoriesField(StringSequenceField):
×
UNCOV
38
    alias = "repositories"
×
UNCOV
39
    help = help_text(
×
40
        """
41
        List of URL addresses or Twine repository aliases where to publish the Python package.
42

43
        Twine is used for publishing Python packages, so the address to any kind of repository
44
        that Twine supports may be used here.
45

46
        Aliases are prefixed with `@` to refer to a config section in your Twine configuration,
47
        such as a `.pypirc` file. Use `@pypi` to upload to the public PyPi repository, which is
48
        the default when using Twine directly.
49
        """
50
    )
51

52
    # Twine uploads to 'pypi' by default, but we don't set default to ["@pypi"] here to make it
53
    # explicit in the BUILD file when a package is meant for public distribution.
54

55

UNCOV
56
class SkipTwineUploadField(BoolField):
×
UNCOV
57
    alias = "skip_twine"
×
UNCOV
58
    default = False
×
UNCOV
59
    help = "If true, don't publish this target's packages using Twine."
×
60

61

UNCOV
62
class PublishPythonPackageRequest(PublishRequest):
×
UNCOV
63
    pass
×
64

65

UNCOV
66
@dataclass(frozen=True)
×
UNCOV
67
class PublishPythonPackageFieldSet(PublishFieldSet):
×
UNCOV
68
    publish_request_type = PublishPythonPackageRequest
×
UNCOV
69
    required_fields = (PythonRepositoriesField,)
×
70

UNCOV
71
    repositories: PythonRepositoriesField
×
UNCOV
72
    skip_twine: SkipTwineUploadField
×
73

UNCOV
74
    def get_output_data(self) -> PublishOutputData:
×
75
        return PublishOutputData(
×
76
            {
77
                "publisher": "twine",
78
                **super().get_output_data(),
79
            }
80
        )
81

82
    # I'd rather opt out early here, so we don't build unnecessarily, however the error feedback is
83
    # misleading and not very helpful in that case.
84
    #
85
    # @classmethod
86
    # def opt_out(cls, tgt: Target) -> bool:
87
    #     return not tgt[PythonRepositoriesField].value
88

89

UNCOV
90
def twine_upload_args(
×
91
    twine_subsystem: TwineSubsystem,
92
    config_files: ConfigFiles,
93
    repo: str,
94
    dists: tuple[str, ...],
95
    ca_cert: Snapshot | None,
96
) -> tuple[str, ...]:
97
    args = ["upload", "--non-interactive"]
×
98

99
    if ca_cert and ca_cert.files:
×
100
        args.append(f"--cert={ca_cert.files[0]}")
×
101

102
    if config_files.snapshot.files:
×
103
        args.append(f"--config-file={config_files.snapshot.files[0]}")
×
104

105
    args.extend(twine_subsystem.args)
×
106

107
    if repo.startswith("@"):
×
108
        # Named repository from the config file.
109
        args.append(f"--repository={repo[1:]}")
×
110
    else:
111
        args.append(f"--repository-url={repo}")
×
112

113
    args.extend(dists)
×
114
    return tuple(args)
×
115

116

UNCOV
117
def twine_env_suffix(repo: str) -> str:
×
118
    return f"_{repo[1:]}".replace("-", "_").upper() if repo.startswith("@") else ""
×
119

120

UNCOV
121
def twine_env_request(repo: str) -> EnvironmentVarsRequest:
×
122
    suffix = twine_env_suffix(repo)
×
123
    env_vars = [
×
124
        "TWINE_USERNAME",
125
        "TWINE_PASSWORD",
126
        "TWINE_REPOSITORY_URL",
127
    ]
128
    req = EnvironmentVarsRequest(env_vars + [f"{var}{suffix}" for var in env_vars])
×
129
    return req
×
130

131

UNCOV
132
def twine_env(env: EnvironmentVars, repo: str) -> EnvironmentVars:
×
133
    suffix = twine_env_suffix(repo)
×
134
    return EnvironmentVars(
×
135
        {key.rsplit(suffix, maxsplit=1)[0] if suffix else key: value for key, value in env.items()}
136
    )
137

138

UNCOV
139
@rule
×
UNCOV
140
async def twine_upload(
×
141
    request: PublishPythonPackageRequest,
142
    twine_subsystem: TwineSubsystem,
143
    global_options: GlobalOptions,
144
) -> PublishProcesses:
145
    dists = tuple(
×
146
        artifact.relpath
147
        for pkg in request.packages
148
        for artifact in pkg.artifacts
149
        if artifact.relpath
150
    )
151

152
    if twine_subsystem.skip or not dists:
×
153
        return PublishProcesses()
×
154

155
    # Too verbose to provide feedback as to why some packages were skipped?
156
    skip = None
×
157
    if request.field_set.skip_twine.value:
×
158
        skip = f"(by `{request.field_set.skip_twine.alias}` on {request.field_set.address})"
×
159
    elif not request.field_set.repositories.value:
×
160
        # I'd rather have used the opt_out mechanism on the field set, but that gives no hint as to
161
        # why the target was not applicable.
162
        skip = f"(no `{request.field_set.repositories.alias}` specified for {request.field_set.address})"
×
163

164
    if skip:
×
165
        return PublishProcesses(
×
166
            [
167
                PublishPackages(
168
                    names=dists,
169
                    description=skip,
170
                ),
171
            ]
172
        )
173

174
    twine_pex, packages_digest, config_files = await concurrently(
×
175
        create_venv_pex(**implicitly(twine_subsystem.to_pex_request())),
176
        merge_digests(MergeDigests(pkg.digest for pkg in request.packages)),
177
        find_config_file(twine_subsystem.config_request()),
178
    )
179

180
    ca_cert_request = twine_subsystem.ca_certs_digest_request(global_options.ca_certs_path)
×
181
    ca_cert = (
×
182
        await digest_to_snapshot(**implicitly({ca_cert_request: CreateDigest}))
183
        if ca_cert_request
184
        else None
185
    )
186
    ca_cert_digest = (ca_cert.digest,) if ca_cert else ()
×
187

188
    input_digest = await merge_digests(
×
189
        MergeDigests((packages_digest, config_files.snapshot.digest, *ca_cert_digest))
190
    )
191
    pex_proc_requests: list[VenvPexProcess] = []
×
192
    twine_envs = await concurrently(
×
193
        environment_vars_subset(**implicitly({twine_env_request(repo): EnvironmentVarsRequest}))
194
        for repo in request.field_set.repositories.value
195
    )
196

197
    for repo, env in zip(request.field_set.repositories.value, twine_envs):
×
198
        pex_proc_requests.append(
×
199
            VenvPexProcess(
200
                twine_pex,
201
                argv=twine_upload_args(twine_subsystem, config_files, repo, dists, ca_cert),
202
                input_digest=input_digest,
203
                extra_env=twine_env(env, repo),
204
                description=repo,
205
            )
206
        )
207

208
    processes = await concurrently(
×
209
        setup_venv_pex_process(request, **implicitly()) for request in pex_proc_requests
210
    )
211

212
    return PublishProcesses(
×
213
        PublishPackages(
214
            names=dists,
215
            process=InteractiveProcess.from_process(process),
216
            description=process.description,
217
            data=PublishOutputData({"repository": process.description}),
218
        )
219
        for process in processes
220
    )
221

222

UNCOV
223
def rules():
×
UNCOV
224
    return (
×
225
        *collect_rules(),
226
        *PublishPythonPackageFieldSet.rules(),
227
        PythonDistribution.register_plugin_field(PythonRepositoriesField),
228
        PythonDistribution.register_plugin_field(SkipTwineUploadField),
229
    )
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