Skip to content

Commit dd61356

Browse files
woodruffwsigmavirus24
authored andcommitted
check: ignore attestations, like signatures
This fixes a bug that I accidentally introduced with attestations support: `twine upload` learned the difference between distributions and attestations, but `twine check` didn't. As a result, `twine check dist/*` would fail with an `InvalidDistribution` error whenever attestations are present in the dist directory, like so: ``` Checking dist/svgcheck-0.9.0.tar.gz: PASSED Checking dist/svgcheck-0.9.0.tar.gz.publish.attestation: ERROR InvalidDistribution: Unknown distribution format: 'svgcheck-0.9.0.tar.gz.publish.attestation' ``` This fixes the behavior of `twine check` by having it skip attestations in the input list, like it does with `.asc` signatures. To do this, I reused the `_split_inputs` helper that was added with #1095, meaning that `twine upload` and `twine check` now have the same input splitting/filtering logic. See pypa/gh-action-pypi-publish#283 for some additional breakage context. Signed-off-by: William Woodruff <[email protected]>
1 parent 2b33343 commit dd61356

File tree

5 files changed

+84
-84
lines changed

5 files changed

+84
-84
lines changed

tests/test_commands.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from tests import helpers
56
from twine import commands
67
from twine import exceptions
78

@@ -49,3 +50,43 @@ def test_find_dists_handles_real_files():
4950
]
5051
files = commands._find_dists(expected)
5152
assert expected == files
53+
54+
55+
def test_split_inputs():
56+
"""Split inputs into dists, signatures, and attestations."""
57+
inputs = [
58+
helpers.WHEEL_FIXTURE,
59+
helpers.WHEEL_FIXTURE + ".asc",
60+
helpers.WHEEL_FIXTURE + ".build.attestation",
61+
helpers.WHEEL_FIXTURE + ".publish.attestation",
62+
helpers.SDIST_FIXTURE,
63+
helpers.SDIST_FIXTURE + ".asc",
64+
helpers.NEW_WHEEL_FIXTURE,
65+
helpers.NEW_WHEEL_FIXTURE + ".frob.attestation",
66+
helpers.NEW_SDIST_FIXTURE,
67+
]
68+
69+
inputs = commands._split_inputs(inputs)
70+
71+
assert inputs.dists == [
72+
helpers.WHEEL_FIXTURE,
73+
helpers.SDIST_FIXTURE,
74+
helpers.NEW_WHEEL_FIXTURE,
75+
helpers.NEW_SDIST_FIXTURE,
76+
]
77+
78+
expected_signatures = {
79+
os.path.basename(dist) + ".asc": dist + ".asc"
80+
for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE]
81+
}
82+
assert inputs.signatures == expected_signatures
83+
84+
assert inputs.attestations_by_dist == {
85+
helpers.WHEEL_FIXTURE: [
86+
helpers.WHEEL_FIXTURE + ".build.attestation",
87+
helpers.WHEEL_FIXTURE + ".publish.attestation",
88+
],
89+
helpers.SDIST_FIXTURE: [],
90+
helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"],
91+
helpers.NEW_SDIST_FIXTURE: [],
92+
}

tests/test_upload.py

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -116,46 +116,6 @@ def test_make_package_attestations_flagged_but_missing(upload_settings):
116116
upload._make_package(helpers.NEW_WHEEL_FIXTURE, {}, [], upload_settings)
117117

118118

119-
def test_split_inputs():
120-
"""Split inputs into dists, signatures, and attestations."""
121-
inputs = [
122-
helpers.WHEEL_FIXTURE,
123-
helpers.WHEEL_FIXTURE + ".asc",
124-
helpers.WHEEL_FIXTURE + ".build.attestation",
125-
helpers.WHEEL_FIXTURE + ".publish.attestation",
126-
helpers.SDIST_FIXTURE,
127-
helpers.SDIST_FIXTURE + ".asc",
128-
helpers.NEW_WHEEL_FIXTURE,
129-
helpers.NEW_WHEEL_FIXTURE + ".frob.attestation",
130-
helpers.NEW_SDIST_FIXTURE,
131-
]
132-
133-
inputs = upload._split_inputs(inputs)
134-
135-
assert inputs.dists == [
136-
helpers.WHEEL_FIXTURE,
137-
helpers.SDIST_FIXTURE,
138-
helpers.NEW_WHEEL_FIXTURE,
139-
helpers.NEW_SDIST_FIXTURE,
140-
]
141-
142-
expected_signatures = {
143-
os.path.basename(dist) + ".asc": dist + ".asc"
144-
for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE]
145-
}
146-
assert inputs.signatures == expected_signatures
147-
148-
assert inputs.attestations_by_dist == {
149-
helpers.WHEEL_FIXTURE: [
150-
helpers.WHEEL_FIXTURE + ".build.attestation",
151-
helpers.WHEEL_FIXTURE + ".publish.attestation",
152-
],
153-
helpers.SDIST_FIXTURE: [],
154-
helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"],
155-
helpers.NEW_SDIST_FIXTURE: [],
156-
}
157-
158-
159119
def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
160120
"""Print PyPI release URLS for each uploaded package."""
161121
stub_repository.release_urls = lambda packages: {RELEASE_URL, NEW_RELEASE_URL}

twine/commands/__init__.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1818
# See the License for the specific language governing permissions and
1919
# limitations under the License.
20+
import fnmatch
2021
import glob
2122
import os.path
22-
from typing import List
23+
from typing import Dict, List, NamedTuple
2324

2425
from twine import exceptions
2526

@@ -52,3 +53,41 @@ def _find_dists(dists: List[str]) -> List[str]:
5253
# Otherwise, files will be filenames that exist
5354
uploads.extend(files)
5455
return _group_wheel_files_first(uploads)
56+
57+
58+
class Inputs(NamedTuple):
59+
"""Represents structured user inputs."""
60+
61+
dists: List[str]
62+
signatures: Dict[str, str]
63+
attestations_by_dist: Dict[str, List[str]]
64+
65+
66+
def _split_inputs(
67+
inputs: List[str],
68+
) -> Inputs:
69+
"""
70+
Split the unstructured list of input files provided by the user into groups.
71+
72+
Three groups are returned: upload files (i.e. dists), signatures, and attestations.
73+
74+
Upload files are returned as a linear list, signatures are returned as a
75+
dict of ``basename -> path``, and attestations are returned as a dict of
76+
``dist-path -> [attestation-path]``.
77+
"""
78+
signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")}
79+
attestations = fnmatch.filter(inputs, "*.*.attestation")
80+
dists = [
81+
dist
82+
for dist in inputs
83+
if dist not in (set(signatures.values()) | set(attestations))
84+
]
85+
86+
attestations_by_dist = {}
87+
for dist in dists:
88+
dist_basename = os.path.basename(dist)
89+
attestations_by_dist[dist] = [
90+
a for a in attestations if os.path.basename(a).startswith(dist_basename)
91+
]
92+
93+
return Inputs(dists, signatures, attestations_by_dist)

twine/commands/check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def check(
127127
:return:
128128
``True`` if there are rendering errors, otherwise ``False``.
129129
"""
130-
uploads = [i for i in commands._find_dists(dists) if not i.endswith(".asc")]
130+
uploads, _, _ = commands._split_inputs(dists)
131131
if not uploads: # Return early, if there are no files to check.
132132
logger.error("No files to check.")
133133
return False

twine/commands/upload.py

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

2220
import requests
2321
from rich import print
@@ -110,44 +108,6 @@ def _make_package(
110108
return package
111109

112110

113-
class Inputs(NamedTuple):
114-
"""Represents structured user inputs."""
115-
116-
dists: List[str]
117-
signatures: Dict[str, str]
118-
attestations_by_dist: Dict[str, List[str]]
119-
120-
121-
def _split_inputs(
122-
inputs: List[str],
123-
) -> Inputs:
124-
"""
125-
Split the unstructured list of input files provided by the user into groups.
126-
127-
Three groups are returned: upload files (i.e. dists), signatures, and attestations.
128-
129-
Upload files are returned as a linear list, signatures are returned as a
130-
dict of ``basename -> path``, and attestations are returned as a dict of
131-
``dist-path -> [attestation-path]``.
132-
"""
133-
signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")}
134-
attestations = fnmatch.filter(inputs, "*.*.attestation")
135-
dists = [
136-
dist
137-
for dist in inputs
138-
if dist not in (set(signatures.values()) | set(attestations))
139-
]
140-
141-
attestations_by_dist = {}
142-
for dist in dists:
143-
dist_basename = os.path.basename(dist)
144-
attestations_by_dist[dist] = [
145-
a for a in attestations if os.path.basename(a).startswith(dist_basename)
146-
]
147-
148-
return Inputs(dists, signatures, attestations_by_dist)
149-
150-
151111
def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
152112
"""Upload one or more distributions to a repository, and display the progress.
153113
@@ -187,7 +147,7 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
187147

188148
dists = commands._find_dists(dists)
189149
# Determine if the user has passed in pre-signed distributions or any attestations.
190-
uploads, signatures, attestations_by_dist = _split_inputs(dists)
150+
uploads, signatures, attestations_by_dist = commands._split_inputs(dists)
191151

192152
print(f"Uploading distributions to {utils.sanitize_url(repository_url)}")
193153

0 commit comments

Comments
 (0)