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

pantsbuild / pants / 21803785359

08 Feb 2026 07:13PM UTC coverage: 43.3% (-37.0%) from 80.277%
21803785359

Pull #23085

github

web-flow
Merge 7c1cd926d into 40389cc58
Pull Request #23085: A helper method for indexing paths by source root

2 of 6 new or added lines in 1 file covered. (33.33%)

17114 existing lines in 539 files now uncovered.

26075 of 60219 relevant lines covered (43.3%)

0.43 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.setuptools import PythonDistributionFieldSet
×
UNCOV
10
from pants.backend.python.subsystems.twine import TwineSubsystem
×
UNCOV
11
from pants.backend.python.target_types import PythonDistribution
×
UNCOV
12
from pants.backend.python.util_rules.pex import (
×
13
    VenvPexProcess,
14
    create_venv_pex,
15
    setup_venv_pex_process,
16
)
UNCOV
17
from pants.core.goals.package import PackageFieldSet
×
UNCOV
18
from pants.core.goals.publish import (
×
19
    CheckSkipRequest,
20
    CheckSkipResult,
21
    PublishFieldSet,
22
    PublishOutputData,
23
    PublishPackages,
24
    PublishProcesses,
25
    PublishRequest,
26
)
UNCOV
27
from pants.core.util_rules.config_files import ConfigFiles, find_config_file
×
UNCOV
28
from pants.core.util_rules.env_vars import environment_vars_subset
×
UNCOV
29
from pants.engine.env_vars import EnvironmentVars, EnvironmentVarsRequest
×
UNCOV
30
from pants.engine.fs import CreateDigest, MergeDigests, Snapshot
×
UNCOV
31
from pants.engine.intrinsics import digest_to_snapshot, merge_digests
×
UNCOV
32
from pants.engine.process import InteractiveProcess
×
UNCOV
33
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
×
UNCOV
34
from pants.engine.target import BoolField, StringSequenceField
×
UNCOV
35
from pants.engine.unions import UnionRule
×
UNCOV
36
from pants.option.global_options import GlobalOptions
×
UNCOV
37
from pants.util.strutil import help_text
×
38

UNCOV
39
logger = logging.getLogger(__name__)
×
40

41

UNCOV
42
class PythonRepositoriesField(StringSequenceField):
×
UNCOV
43
    alias = "repositories"
×
UNCOV
44
    help = help_text(
×
45
        """
46
        List of URL addresses or Twine repository aliases where to publish the Python package.
47

48
        Twine is used for publishing Python packages, so the address to any kind of repository
49
        that Twine supports may be used here.
50

51
        Aliases are prefixed with `@` to refer to a config section in your Twine configuration,
52
        such as a `.pypirc` file. Use `@pypi` to upload to the public PyPi repository, which is
53
        the default when using Twine directly.
54
        """
55
    )
56

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

60

UNCOV
61
class SkipTwineUploadField(BoolField):
×
UNCOV
62
    alias = "skip_twine"
×
UNCOV
63
    default = False
×
UNCOV
64
    help = "If true, don't publish this target's packages using Twine."
×
65

66

UNCOV
67
class PublishPythonPackageRequest(PublishRequest):
×
UNCOV
68
    pass
×
69

70

UNCOV
71
@dataclass(frozen=True)
×
UNCOV
72
class PublishPythonPackageFieldSet(PublishFieldSet):
×
UNCOV
73
    publish_request_type = PublishPythonPackageRequest
×
UNCOV
74
    required_fields = (PythonRepositoriesField,)
×
75

UNCOV
76
    repositories: PythonRepositoriesField
×
UNCOV
77
    skip_twine: SkipTwineUploadField
×
78

UNCOV
79
    def make_skip_request(self, package_fs: PackageFieldSet) -> PythonDistCheckSkipRequest | None:
×
80
        return PythonDistCheckSkipRequest(publish_fs=self, package_fs=package_fs)
×
81

UNCOV
82
    def get_output_data(self) -> PublishOutputData:
×
83
        return PublishOutputData(
×
84
            {
85
                "publisher": "twine",
86
                **super().get_output_data(),
87
            }
88
        )
89

90

UNCOV
91
class PythonDistCheckSkipRequest(CheckSkipRequest[PublishPythonPackageFieldSet]):
×
UNCOV
92
    pass
×
93

94

UNCOV
95
@rule
×
UNCOV
96
async def check_if_skip_upload(
×
97
    request: PythonDistCheckSkipRequest, twine_subsystem: TwineSubsystem
98
) -> CheckSkipResult:
99
    if twine_subsystem.skip:
×
100
        reason = f"(by `[{TwineSubsystem.name}].skip = True`)"
×
101
    elif request.publish_fs.skip_twine.value:
×
102
        reason = f"(by `{request.publish_fs.skip_twine.alias}` on {request.address})"
×
103
    elif not request.publish_fs.repositories.value:
×
104
        reason = f"(no `{request.publish_fs.repositories.alias}` specified for {request.address})"
×
105
    else:
106
        return CheckSkipResult.no_skip()
×
107
    name = (
×
108
        request.package_fs.provides.value.kwargs.get("name", "<unknown python artifact>")
109
        if isinstance(request.package_fs, PythonDistributionFieldSet)
110
        else "<unknown artifact>"
111
    )
112
    return CheckSkipResult.skip(
×
113
        names=[name],
114
        description=reason,
115
        data=request.publish_fs.get_output_data(),
116
    )
117

118

UNCOV
119
def twine_upload_args(
×
120
    twine_subsystem: TwineSubsystem,
121
    config_files: ConfigFiles,
122
    repo: str,
123
    dists: tuple[str, ...],
124
    ca_cert: Snapshot | None,
125
) -> tuple[str, ...]:
126
    args = ["upload", "--non-interactive"]
×
127

128
    if ca_cert and ca_cert.files:
×
129
        args.append(f"--cert={ca_cert.files[0]}")
×
130

131
    if config_files.snapshot.files:
×
132
        args.append(f"--config-file={config_files.snapshot.files[0]}")
×
133

134
    args.extend(twine_subsystem.args)
×
135

136
    if repo.startswith("@"):
×
137
        # Named repository from the config file.
138
        args.append(f"--repository={repo[1:]}")
×
139
    else:
140
        args.append(f"--repository-url={repo}")
×
141

142
    args.extend(dists)
×
143
    return tuple(args)
×
144

145

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

149

UNCOV
150
def twine_env_request(repo: str) -> EnvironmentVarsRequest:
×
151
    suffix = twine_env_suffix(repo)
×
152
    env_vars = [
×
153
        "TWINE_USERNAME",
154
        "TWINE_PASSWORD",
155
        "TWINE_REPOSITORY_URL",
156
    ]
157
    req = EnvironmentVarsRequest(env_vars + [f"{var}{suffix}" for var in env_vars])
×
158
    return req
×
159

160

UNCOV
161
def twine_env(env: EnvironmentVars, repo: str) -> EnvironmentVars:
×
162
    suffix = twine_env_suffix(repo)
×
163
    return EnvironmentVars(
×
164
        {key.rsplit(suffix, maxsplit=1)[0] if suffix else key: value for key, value in env.items()}
165
    )
166

167

UNCOV
168
@rule
×
UNCOV
169
async def twine_upload(
×
170
    request: PublishPythonPackageRequest,
171
    twine_subsystem: TwineSubsystem,
172
    global_options: GlobalOptions,
173
) -> PublishProcesses:
174
    dists = tuple(
×
175
        artifact.relpath
176
        for pkg in request.packages
177
        for artifact in pkg.artifacts
178
        if artifact.relpath
179
    )
180

181
    if not dists:
×
182
        return PublishProcesses()
×
183

184
    twine_pex, packages_digest, config_files = await concurrently(
×
185
        create_venv_pex(**implicitly(twine_subsystem.to_pex_request())),
186
        merge_digests(MergeDigests(pkg.digest for pkg in request.packages)),
187
        find_config_file(twine_subsystem.config_request()),
188
    )
189

190
    ca_cert_request = twine_subsystem.ca_certs_digest_request(global_options.ca_certs_path)
×
191
    ca_cert = (
×
192
        await digest_to_snapshot(**implicitly({ca_cert_request: CreateDigest}))
193
        if ca_cert_request
194
        else None
195
    )
196
    ca_cert_digest = (ca_cert.digest,) if ca_cert else ()
×
197

198
    input_digest = await merge_digests(
×
199
        MergeDigests((packages_digest, config_files.snapshot.digest, *ca_cert_digest))
200
    )
201
    pex_proc_requests: list[VenvPexProcess] = []
×
202
    twine_envs = await concurrently(
×
203
        environment_vars_subset(**implicitly({twine_env_request(repo): EnvironmentVarsRequest}))
204
        for repo in request.field_set.repositories.value
205
    )
206

207
    for repo, env in zip(request.field_set.repositories.value, twine_envs):
×
208
        pex_proc_requests.append(
×
209
            VenvPexProcess(
210
                twine_pex,
211
                argv=twine_upload_args(twine_subsystem, config_files, repo, dists, ca_cert),
212
                input_digest=input_digest,
213
                extra_env=twine_env(env, repo),
214
                description=repo,
215
            )
216
        )
217

218
    processes = await concurrently(
×
219
        setup_venv_pex_process(request, **implicitly()) for request in pex_proc_requests
220
    )
221

222
    return PublishProcesses(
×
223
        PublishPackages(
224
            names=dists,
225
            process=InteractiveProcess.from_process(process),
226
            description=process.description,
227
            data=PublishOutputData({"repository": process.description}),
228
        )
229
        for process in processes
230
    )
231

232

UNCOV
233
def rules():
×
UNCOV
234
    return (
×
235
        *collect_rules(),
236
        *PublishPythonPackageFieldSet.rules(),
237
        PythonDistribution.register_plugin_field(PythonRepositoriesField),
238
        PythonDistribution.register_plugin_field(SkipTwineUploadField),
239
        UnionRule(CheckSkipRequest, PythonDistCheckSkipRequest),
240
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc