Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/github_actions/pypa/gh-action-pyp…
Browse files Browse the repository at this point in the history
…i-publish-1.12.2
  • Loading branch information
woodruffw authored Nov 30, 2024
2 parents 6761e87 + 22e2e61 commit 030ce4f
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 48 deletions.
1 change: 0 additions & 1 deletion changelog/1024.misc.rst

This file was deleted.

21 changes: 21 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@ schemes recommended by the Python Packaging Authority.
.. towncrier release notes start
Twine 6.0.0 (2024-11-29)
------------------------

Bugfixes
^^^^^^^^

- Restore support for pkginfo 1.11 (`#1116 <https://github.com/pypa/twine/issues/1116>`_)


Deprecations and Removals
^^^^^^^^^^^^^^^^^^^^^^^^^

- Username for PyPI and Test PyPI now defaults to __token__ but no longer overrides a username configured in the environment or supplied on the command line. Workflows still supplying anything other than __token__ for the username when uploading to PyPI or Test PyPI will now fail. Either supply __token__ or do not supply a username at all. (`#1121 <https://github.com/pypa/twine/issues/1121>`_)


Misc
^^^^

- `#1024 <https://github.com/pypa/twine/issues/1024>`_


Twine 5.1.1 (2024-06-26)
------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
# TODO: Try to add these to intersphinx_mapping
nitpick_ignore_regex = [
(r"py:.*", r"pkginfo.*"),
("py:class", r"warnings\.WarningMessage"),
]

# -- Options for apidoc output ------------------------------------------------
Expand Down
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ dependencies = [
"keyring >= 15.1; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"rfc3986 >= 1.4.0",
"rich >= 12.0.0",

# workaround for #1116
"pkginfo < 1.11",
"packaging",
]
dynamic = ["version"]

Expand Down
34 changes: 25 additions & 9 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,11 +383,6 @@ def test_fips_metadata_excludes_md5_and_blake2(monkeypatch):
@pytest.mark.parametrize(
"read_data, missing_fields",
[
pytest.param(
b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n",
"Name, Version",
id="unsupported Metadata-Version",
),
pytest.param(
b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: UNKNOWN\n",
"Name, Version",
Expand Down Expand Up @@ -421,10 +416,7 @@ def test_fips_metadata_excludes_md5_and_blake2(monkeypatch):
],
)
def test_pkginfo_returns_no_metadata(read_data, missing_fields, monkeypatch):
"""Raise an exception when pkginfo can't interpret the metadata.
This could be caused by a version number or format it doesn't support yet.
"""
"""Raise an exception when pkginfo can't interpret the metadata."""
monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data)
filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"

Expand All @@ -434,9 +426,33 @@ def test_pkginfo_returns_no_metadata(read_data, missing_fields, monkeypatch):
assert (
f"Metadata is missing required fields: {missing_fields}." in err.value.args[0]
)


def test_pkginfo_unrecognized_version(monkeypatch):
"""Raise an exception when pkginfo doesn't recognize the version."""
data = b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n"
monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: data)
filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"

with pytest.raises(exceptions.InvalidDistribution) as err:
package_file.PackageFile.from_filename(filename, comment=None)

assert "1.0, 1.1, 1.2, 2.0, 2.1, 2.2" in err.value.args[0]


def test_pkginfo_returns_no_metadata_py_below_1_11(monkeypatch):
"""Raise special msg when pkginfo can't interpret metadata on pkginfo < 1.11."""
data = b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: 1.0.0\n"
monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: data)
monkeypatch.setattr(package_file.importlib_metadata, "version", lambda pkg: "1.10")
filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"

with pytest.raises(exceptions.InvalidDistribution) as err:
package_file.PackageFile.from_filename(filename, comment=None)

assert "Make sure the distribution includes" in err.value.args[0]


def test_malformed_from_file(monkeypatch):
"""Raise an exception when malformed package file triggers EOFError."""
filename = "tests/fixtures/malformed.tar.gz"
Expand Down
5 changes: 2 additions & 3 deletions tests/test_register.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,15 @@ def none_register(*args, **settings_kwargs):
monkeypatch.setattr(register, "register", replaced_register)
testenv = {
"TWINE_REPOSITORY": repo,
# Ignored because the TWINE_REPOSITORY is PyPI/TestPyPI
"TWINE_USERNAME": "this-is-ignored",
"TWINE_USERNAME": "pypiuser",
"TWINE_PASSWORD": "pypipassword",
"TWINE_CERT": "/foo/bar.crt",
}
with helpers.set_env(**testenv):
cli.dispatch(["register", helpers.WHEEL_FIXTURE])
register_settings = replaced_register.calls[0].args[0]
assert "pypipassword" == register_settings.password
assert "__token__" == register_settings.username
assert "pypiuser" == register_settings.username
assert "/foo/bar.crt" == register_settings.cacert


Expand Down
5 changes: 2 additions & 3 deletions tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,16 +564,15 @@ def none_upload(*args, **settings_kwargs):
monkeypatch.setattr(upload, "upload", replaced_upload)
testenv = {
"TWINE_REPOSITORY": repo,
# Ignored because TWINE_REPOSITORY is PyPI/TestPyPI
"TWINE_USERNAME": "this-is-ignored",
"TWINE_USERNAME": "pypiuser",
"TWINE_PASSWORD": "pypipassword",
"TWINE_CERT": "/foo/bar.crt",
}
with helpers.set_env(**testenv):
cli.dispatch(["upload", "path/to/file"])
upload_settings = replaced_upload.calls[0].args[0]
assert "pypipassword" == upload_settings.password
assert "__token__" == upload_settings.username
assert "pypiuser" == upload_settings.username
assert "/foo/bar.crt" == upload_settings.cacert


Expand Down
30 changes: 15 additions & 15 deletions twine/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,9 @@ def choose(cls, interactive: bool) -> Type["Resolver"]:
@property
@functools.lru_cache()
def username(self) -> Optional[str]:
if cast(str, self.config["repository"]).startswith(
(utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)
):
# As of 2024-01-01, PyPI requires API tokens for uploads, meaning
# that the username is invariant.
return "__token__"
if self.is_pypi() and not self.input.username:
# Default username.
self.input.username = "__token__"

return utils.get_userpass_value(
self.input.username,
Expand Down Expand Up @@ -111,20 +108,23 @@ def password_from_keyring_or_prompt(self) -> str:
logger.info("password set from keyring")
return password

# As of 2024-01-01, PyPI requires API tokens for uploads;
# specialize the prompt to clarify that an API token must be provided.
if cast(str, self.config["repository"]).startswith(
(utils.DEFAULT_REPOSITORY, utils.TEST_REPOSITORY)
):
prompt = "API token"
else:
prompt = "password"
# Prompt for API token when required.
what = "API token" if self.is_pypi() else "password"

return self.prompt(prompt, getpass.getpass)
return self.prompt(what, getpass.getpass)

def prompt(self, what: str, how: Callable[..., str]) -> str:
return how(f"Enter your {what}: ")

def is_pypi(self) -> bool:
"""As of 2024-01-01, PyPI requires API tokens for uploads."""
return cast(str, self.config["repository"]).startswith(
(
utils.DEFAULT_REPOSITORY,
utils.TEST_REPOSITORY,
)
)


class Private(Resolver):
def prompt(self, what: str, how: Optional[Callable[..., str]] = None) -> str:
Expand Down
68 changes: 55 additions & 13 deletions twine/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@
import re
import subprocess
import sys
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union, cast
import warnings
from typing import (
Any,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
cast,
)

if sys.version_info >= (3, 10):
import importlib.metadata as importlib_metadata
else:
import importlib_metadata

import packaging.version
import pkginfo
from rich import print

Expand Down Expand Up @@ -65,12 +78,19 @@ def _safe_name(name: str) -> str:
return re.sub("[^A-Za-z0-9.]+", "-", name)


class CheckedDistribution(pkginfo.Distribution):
"""A Distribution whose name and version are confirmed to be defined."""

name: str
version: str


class PackageFile:
def __init__(
self,
filename: str,
comment: Optional[str],
metadata: pkginfo.Distribution,
metadata: CheckedDistribution,
python_version: Optional[str],
filetype: Optional[str],
) -> None:
Expand Down Expand Up @@ -100,7 +120,8 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile":
for ext, dtype in DIST_EXTENSIONS.items():
if filename.endswith(ext):
try:
meta = DIST_TYPES[dtype](filename)
with warnings.catch_warnings(record=True) as captured:
meta = DIST_TYPES[dtype](filename)
except EOFError:
raise exceptions.InvalidDistribution(
"Invalid distribution file: '%s'" % os.path.basename(filename)
Expand All @@ -112,22 +133,29 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile":
"Unknown distribution format: '%s'" % os.path.basename(filename)
)

# If pkginfo encounters a metadata version it doesn't support, it may give us
supported_metadata = list(pkginfo.distribution.HEADER_ATTRS)
if cls._is_unknown_metadata_version(captured):
raise exceptions.InvalidDistribution(
"Make sure the distribution is using a supported Metadata-Version: "
f"{', '.join(supported_metadata)}."
)
# If pkginfo <1.11 encounters a metadata version it doesn't support, it may give
# back empty metadata. At the very least, we should have a name and version,
# which could also be empty if, for example, a MANIFEST.in doesn't include
# setup.cfg.
missing_fields = [
f.capitalize() for f in ["name", "version"] if not getattr(meta, f)
]
if missing_fields:
supported_metadata = list(pkginfo.distribution.HEADER_ATTRS)
raise exceptions.InvalidDistribution(
"Metadata is missing required fields: "
f"{', '.join(missing_fields)}.\n"
"Make sure the distribution includes the files where those fields "
"are specified, and is using a supported Metadata-Version: "
f"{', '.join(supported_metadata)}."
)
msg = f"Metadata is missing required fields: {', '.join(missing_fields)}."
if cls._pkginfo_before_1_11():
msg += (
"\n"
"Make sure the distribution includes the files where those fields "
"are specified, and is using a supported Metadata-Version: "
f"{', '.join(supported_metadata)}."
)
raise exceptions.InvalidDistribution(msg)

py_version: Optional[str]
if dtype == "bdist_egg":
Expand All @@ -140,7 +168,21 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile":
else:
py_version = None

return cls(filename, comment, meta, py_version, dtype)
return cls(
filename, comment, cast(CheckedDistribution, meta), py_version, dtype
)

@staticmethod
def _is_unknown_metadata_version(
captured: Iterable[warnings.WarningMessage],
) -> bool:
NMV = getattr(pkginfo.distribution, "NewMetadataVersion", None)
return any(warning.category is NMV for warning in captured)

@staticmethod
def _pkginfo_before_1_11() -> bool:
ver = packaging.version.Version(importlib_metadata.version("pkginfo"))
return ver < packaging.version.Version("1.11")

def metadata_dictionary(self) -> Dict[str, MetadataValue]:
"""Merge multiple sources of metadata into a single dictionary.
Expand Down
5 changes: 4 additions & 1 deletion twine/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,10 @@ def _upload(self, package: package_file.PackageFile) -> requests.Response:

with open(package.filename, "rb") as fp:
data_to_send.append(
("content", (package.basefilename, fp, "application/octet-stream"))
(
"content",
(package.basefilename, fp, "application/octet-stream"),
)
)
encoder = requests_toolbelt.MultipartEncoder(data_to_send)

Expand Down

0 comments on commit 030ce4f

Please sign in to comment.