Skip to content

Commit de2acee

Browse files
authored
Merge pull request #1095 from woodruffw-forks/ww/attestations-flag
twine/upload: attestations scaffolding
2 parents 5de10e8 + 40f4197 commit de2acee

File tree

4 files changed

+98
-5
lines changed

4 files changed

+98
-5
lines changed

tests/test_settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,7 @@ def test_non_interactive_environment(self, monkeypatch):
164164
monkeypatch.setenv("TWINE_NON_INTERACTIVE", "0")
165165
args = self.parse_args([])
166166
assert not args.non_interactive
167+
168+
def test_attestations_flag(self):
169+
args = self.parse_args(["--attestations"])
170+
assert args.attestations

tests/test_upload.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,46 @@ def stub_sign(package, *_):
105105
]
106106

107107

108+
def test_split_inputs():
109+
"""Split inputs into dists, signatures, and attestations."""
110+
inputs = [
111+
helpers.WHEEL_FIXTURE,
112+
helpers.WHEEL_FIXTURE + ".asc",
113+
helpers.WHEEL_FIXTURE + ".build.attestation",
114+
helpers.WHEEL_FIXTURE + ".publish.attestation",
115+
helpers.SDIST_FIXTURE,
116+
helpers.SDIST_FIXTURE + ".asc",
117+
helpers.NEW_WHEEL_FIXTURE,
118+
helpers.NEW_WHEEL_FIXTURE + ".frob.attestation",
119+
helpers.NEW_SDIST_FIXTURE,
120+
]
121+
122+
inputs = upload._split_inputs(inputs)
123+
124+
assert inputs.dists == [
125+
helpers.WHEEL_FIXTURE,
126+
helpers.SDIST_FIXTURE,
127+
helpers.NEW_WHEEL_FIXTURE,
128+
helpers.NEW_SDIST_FIXTURE,
129+
]
130+
131+
expected_signatures = {
132+
os.path.basename(dist) + ".asc": dist + ".asc"
133+
for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE]
134+
}
135+
assert inputs.signatures == expected_signatures
136+
137+
assert inputs.attestations_by_dist == {
138+
helpers.WHEEL_FIXTURE: [
139+
helpers.WHEEL_FIXTURE + ".build.attestation",
140+
helpers.WHEEL_FIXTURE + ".publish.attestation",
141+
],
142+
helpers.SDIST_FIXTURE: [],
143+
helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"],
144+
helpers.NEW_SDIST_FIXTURE: [],
145+
}
146+
147+
108148
def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
109149
"""Print PyPI release URLS for each uploaded package."""
110150
stub_repository.release_urls = lambda packages: {RELEASE_URL, NEW_RELEASE_URL}

twine/commands/upload.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
import argparse
17+
import fnmatch
1718
import logging
1819
import os.path
19-
from typing import Dict, List, cast
20+
from typing import Dict, List, NamedTuple, cast
2021

2122
import requests
2223
from rich import print
@@ -91,6 +92,44 @@ def _make_package(
9192
return package
9293

9394

95+
class Inputs(NamedTuple):
96+
"""Represents structured user inputs."""
97+
98+
dists: List[str]
99+
signatures: Dict[str, str]
100+
attestations_by_dist: Dict[str, List[str]]
101+
102+
103+
def _split_inputs(
104+
inputs: List[str],
105+
) -> Inputs:
106+
"""
107+
Split the unstructured list of input files provided by the user into groups.
108+
109+
Three groups are returned: upload files (i.e. dists), signatures, and attestations.
110+
111+
Upload files are returned as a linear list, signatures are returned as a
112+
dict of ``basename -> path``, and attestations are returned as a dict of
113+
``dist-path -> [attestation-path]``.
114+
"""
115+
signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")}
116+
attestations = fnmatch.filter(inputs, "*.*.attestation")
117+
dists = [
118+
dist
119+
for dist in inputs
120+
if dist not in (set(signatures.values()) | set(attestations))
121+
]
122+
123+
attestations_by_dist = {}
124+
for dist in dists:
125+
dist_basename = os.path.basename(dist)
126+
attestations_by_dist[dist] = [
127+
a for a in attestations if os.path.basename(a).startswith(dist_basename)
128+
]
129+
130+
return Inputs(dists, signatures, attestations_by_dist)
131+
132+
94133
def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
95134
"""Upload one or more distributions to a repository, and display the progress.
96135
@@ -105,17 +144,17 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
105144
The configured options related to uploading to a repository.
106145
:param dists:
107146
The distribution files to upload to the repository. This can also include
108-
``.asc`` files; the GPG signatures will be added to the corresponding uploads.
147+
``.asc`` and ``.attestation`` files, which will be added to their respective
148+
file uploads.
109149
110150
:raises twine.exceptions.TwineException:
111151
The upload failed due to a configuration error.
112152
:raises requests.HTTPError:
113153
The repository responded with an error.
114154
"""
115155
dists = commands._find_dists(dists)
116-
# Determine if the user has passed in pre-signed distributions
117-
signatures = {os.path.basename(d): d for d in dists if d.endswith(".asc")}
118-
uploads = [i for i in dists if not i.endswith(".asc")]
156+
# Determine if the user has passed in pre-signed distributions or any attestations.
157+
uploads, signatures, _ = _split_inputs(dists)
119158

120159
upload_settings.check_repository_url()
121160
repository_url = cast(str, upload_settings.repository_config["repository"])

twine/settings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Settings:
4545
def __init__(
4646
self,
4747
*,
48+
attestations: bool = False,
4849
sign: bool = False,
4950
sign_with: str = "gpg",
5051
identity: Optional[str] = None,
@@ -64,6 +65,8 @@ def __init__(
6465
) -> None:
6566
"""Initialize our settings instance.
6667
68+
:param attestations:
69+
Whether the package file should be uploaded with attestations.
6770
:param sign:
6871
Configure whether the package file should be signed.
6972
:param sign_with:
@@ -114,6 +117,7 @@ def __init__(
114117
repository_name=repository_name,
115118
repository_url=repository_url,
116119
)
120+
self.attestations = attestations
117121
self._handle_package_signing(
118122
sign=sign,
119123
sign_with=sign_with,
@@ -175,6 +179,12 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None:
175179
" This overrides --repository. "
176180
"(Can also be set via %(env)s environment variable.)",
177181
)
182+
parser.add_argument(
183+
"--attestations",
184+
action="store_true",
185+
default=False,
186+
help="Upload each file's associated attestations.",
187+
)
178188
parser.add_argument(
179189
"-s",
180190
"--sign",

0 commit comments

Comments
 (0)