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

pantsbuild / pants / 22285099215

22 Feb 2026 08:52PM UTC coverage: 75.854% (-17.1%) from 92.936%
22285099215

Pull #23121

github

web-flow
Merge c7299df9c into ba8359840
Pull Request #23121: fix issue with optional fields in dependency validator

28 of 29 new or added lines in 2 files covered. (96.55%)

11174 existing lines in 400 files now uncovered.

53694 of 70786 relevant lines covered (75.85%)

1.88 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:
×
UNCOV
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:
UNCOV
99
    if twine_subsystem.skip:
×
UNCOV
100
        reason = f"(by `[{TwineSubsystem.name}].skip = True`)"
×
UNCOV
101
    elif request.publish_fs.skip_twine.value:
×
UNCOV
102
        reason = f"(by `{request.publish_fs.skip_twine.alias}` on {request.address})"
×
UNCOV
103
    elif not request.publish_fs.repositories.value:
×
UNCOV
104
        reason = f"(no `{request.publish_fs.repositories.alias}` specified for {request.address})"
×
105
    else:
UNCOV
106
        return CheckSkipResult.no_skip()
×
UNCOV
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
    )
UNCOV
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, ...]:
UNCOV
126
    args = ["upload", "--non-interactive"]
×
127

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

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

UNCOV
134
    args.extend(twine_subsystem.args)
×
135

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

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

145

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

149

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

160

UNCOV
161
def twine_env(env: EnvironmentVars, repo: str) -> EnvironmentVars:
×
UNCOV
162
    suffix = twine_env_suffix(repo)
×
UNCOV
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:
UNCOV
174
    dists = tuple(
×
175
        artifact.relpath
176
        for pkg in request.packages
177
        for artifact in pkg.artifacts
178
        if artifact.relpath
179
    )
180

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

UNCOV
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

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

UNCOV
198
    input_digest = await merge_digests(
×
199
        MergeDigests((packages_digest, config_files.snapshot.digest, *ca_cert_digest))
200
    )
UNCOV
201
    pex_proc_requests: list[VenvPexProcess] = []
×
UNCOV
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

UNCOV
207
    for repo, env in zip(request.field_set.repositories.value, twine_envs):
×
UNCOV
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

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

UNCOV
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