Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ def test_non_interactive_environment(self, monkeypatch):
monkeypatch.setenv("TWINE_NON_INTERACTIVE", "0")
args = self.parse_args([])
assert not args.non_interactive

def test_attestations_flag(self):
args = self.parse_args(["--attestations"])
assert args.attestations
40 changes: 40 additions & 0 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,46 @@ def stub_sign(package, *_):
]


def test_split_inputs():
"""Split inputs into dists, signatures, and attestations."""
inputs = [
helpers.WHEEL_FIXTURE,
helpers.WHEEL_FIXTURE + ".asc",
helpers.WHEEL_FIXTURE + ".build.attestation",
helpers.WHEEL_FIXTURE + ".publish.attestation",
helpers.SDIST_FIXTURE,
helpers.SDIST_FIXTURE + ".asc",
helpers.NEW_WHEEL_FIXTURE,
helpers.NEW_WHEEL_FIXTURE + ".frob.attestation",
helpers.NEW_SDIST_FIXTURE,
]

dists, signatures, attestations_by_dist = upload._split_inputs(inputs)

assert dists == [
helpers.WHEEL_FIXTURE,
helpers.SDIST_FIXTURE,
helpers.NEW_WHEEL_FIXTURE,
helpers.NEW_SDIST_FIXTURE,
]

expected_signatures = {
os.path.basename(dist) + ".asc": dist + ".asc"
for dist in [helpers.WHEEL_FIXTURE, helpers.SDIST_FIXTURE]
}
assert signatures == expected_signatures

assert attestations_by_dist == {
helpers.WHEEL_FIXTURE: [
helpers.WHEEL_FIXTURE + ".build.attestation",
helpers.WHEEL_FIXTURE + ".publish.attestation",
],
helpers.SDIST_FIXTURE: [],
helpers.NEW_WHEEL_FIXTURE: [helpers.NEW_WHEEL_FIXTURE + ".frob.attestation"],
helpers.NEW_SDIST_FIXTURE: [],
}


def test_successs_prints_release_urls(upload_settings, stub_repository, capsys):
"""Print PyPI release URLS for each uploaded package."""
stub_repository.release_urls = lambda packages: {RELEASE_URL, NEW_RELEASE_URL}
Expand Down
39 changes: 34 additions & 5 deletions twine/commands/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import fnmatch
import logging
import os.path
from typing import Dict, List, cast
from typing import Dict, List, Tuple, cast

import requests
from rich import print
Expand Down Expand Up @@ -91,6 +92,34 @@ def _make_package(
return package


def _split_inputs(
inputs: List[str],
) -> Tuple[List[str], Dict[str, str], Dict[str, List[str]]]:
Comment thread
woodruffw marked this conversation as resolved.
Outdated
"""
Split the unstructured list of input files provided by the user into groups.

Three groups are returned: upload files (i.e. dists), signatures, and attestations.

Upload files are returned as a linear list, signatures are returned as a
dict of ``basename -> path``, and attestations are returned as a dict of
``dist-path -> [attestation-path]``.
"""
signatures = {os.path.basename(i): i for i in fnmatch.filter(inputs, "*.asc")}
attestations = set(fnmatch.filter(inputs, "*.*.attestation"))
dists = [
dist for dist in inputs if dist not in (set(signatures.values()) | attestations)
]

attestations_by_dist = {}
for dist in dists:
dist_basename = os.path.basename(dist)
attestations_by_dist[dist] = [
a for a in attestations if os.path.basename(a).startswith(dist_basename)
]

return list(dists), signatures, attestations_by_dist


def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
"""Upload one or more distributions to a repository, and display the progress.

Expand All @@ -105,17 +134,17 @@ def upload(upload_settings: settings.Settings, dists: List[str]) -> None:
The configured options related to uploading to a repository.
:param dists:
The distribution files to upload to the repository. This can also include
``.asc`` files; the GPG signatures will be added to the corresponding uploads.
``.asc`` and ``.attestation`` files, which will be added to their respective
file uploads.

:raises twine.exceptions.TwineException:
The upload failed due to a configuration error.
:raises requests.HTTPError:
The repository responded with an error.
"""
dists = commands._find_dists(dists)
# Determine if the user has passed in pre-signed distributions
signatures = {os.path.basename(d): d for d in dists if d.endswith(".asc")}
uploads = [i for i in dists if not i.endswith(".asc")]
# Determine if the user has passed in pre-signed distributions or any attestations.
uploads, signatures, _ = _split_inputs(dists)

upload_settings.check_repository_url()
repository_url = cast(str, upload_settings.repository_config["repository"])
Expand Down
10 changes: 10 additions & 0 deletions twine/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Settings:
def __init__(
self,
*,
attestations: bool = False,
sign: bool = False,
sign_with: str = "gpg",
identity: Optional[str] = None,
Expand All @@ -64,6 +65,8 @@ def __init__(
) -> None:
"""Initialize our settings instance.

:param attestations:
Whether the package file should be uploaded with attestations.
:param sign:
Configure whether the package file should be signed.
:param sign_with:
Expand Down Expand Up @@ -114,6 +117,7 @@ def __init__(
repository_name=repository_name,
repository_url=repository_url,
)
self.attestations = attestations
self._handle_package_signing(
sign=sign,
sign_with=sign_with,
Expand Down Expand Up @@ -175,6 +179,12 @@ def register_argparse_arguments(parser: argparse.ArgumentParser) -> None:
" This overrides --repository. "
"(Can also be set via %(env)s environment variable.)",
)
parser.add_argument(
"--attestations",
action="store_true",
default=False,
help="Upload each file's associated attestations.",
)
parser.add_argument(
"-s",
"--sign",
Expand Down