diff --git a/changelog/861.feature.rst b/changelog/861.feature.rst new file mode 100644 index 00000000..b04d252e --- /dev/null +++ b/changelog/861.feature.rst @@ -0,0 +1 @@ +Show more helpful error message for invalid metadata. diff --git a/setup.cfg b/setup.cfg index e7ead763..c3ac2f0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,7 @@ install_requires= readme_renderer >= 21.0 requests >= 2.20 requests-toolbelt >= 0.8.0, != 0.9.0 + urllib3 >= 1.26.0 tqdm >= 4.14 importlib_metadata >= 3.6 keyring >= 15.1 diff --git a/tests/test_package.py b/tests/test_package.py index 62108b5d..3d2e2799 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -337,23 +337,46 @@ def test_fips_metadata_excludes_md5_and_blake2(monkeypatch): assert "blake2_256_digest" not in mddict -def test_pkginfo_returns_no_metadata(monkeypatch): +@pytest.mark.parametrize( + "read_data, missing_fields", + [ + pytest.param( + b"Metadata-Version: 2.3\nName: test-package\nVersion: 1.0.0\n", + "Name, Version", + id="unsupported Metadata-Version", + ), + pytest.param( + b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: UNKNOWN\n", + "Name, Version", + id="missing Name and Version", + ), + pytest.param( + b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: 1.0.0\n", + "Name", + id="missing Name", + ), + pytest.param( + b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", + "Version", + id="missing Version", + ), + ], +) +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. """ - - def EmptyDist(filename): - return pretend.stub(name=None, version=None) - - monkeypatch.setattr(package_file, "DIST_TYPES", {"bdist_wheel": EmptyDist}) + monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_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 "Invalid distribution metadata" in err.value.args[0] - assert "1.0, 1.1, 1.2, 2.0, 2.1, and 2.2" in err.value.args[0] + assert ( + f"Metadata is missing required fields: {missing_fields}." in err.value.args[0] + ) + assert "1.0, 1.1, 1.2, 2.0, 2.1, 2.2" in err.value.args[0] def test_malformed_from_file(monkeypatch): diff --git a/twine/cli.py b/twine/cli.py index 5f7162db..f65bc035 100644 --- a/twine/cli.py +++ b/twine/cli.py @@ -14,8 +14,8 @@ import argparse from typing import Any, List, Tuple -from importlib_metadata import entry_points -from importlib_metadata import version +import importlib_metadata +from packaging import requirements import twine @@ -23,14 +23,9 @@ def list_dependencies_and_versions() -> List[Tuple[str, str]]: - deps = ( - "importlib_metadata", - "pkginfo", - "requests", - "requests-toolbelt", - "tqdm", - ) - return [(dep, version(dep)) for dep in deps] # type: ignore[no-untyped-call] # python/importlib_metadata#288 # noqa: E501 + requires = importlib_metadata.requires("twine") # type: ignore[no-untyped-call] # python/importlib_metadata#288 # noqa: E501 + deps = [requirements.Requirement(r).name for r in requires] + return [(dep, importlib_metadata.version(dep)) for dep in deps] # type: ignore[no-untyped-call] # python/importlib_metadata#288 # noqa: E501 def dep_versions() -> str: @@ -40,7 +35,9 @@ def dep_versions() -> str: def dispatch(argv: List[str]) -> Any: - registered_commands = entry_points(group="twine.registered_commands") + registered_commands = importlib_metadata.entry_points( + group="twine.registered_commands" + ) parser = argparse.ArgumentParser(prog="twine") parser.add_argument( "--version", diff --git a/twine/package.py b/twine/package.py index da468a89..0619ba6e 100644 --- a/twine/package.py +++ b/twine/package.py @@ -101,15 +101,21 @@ 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 back empty metadata. At the very least, we should have a name - # and version - if not (meta.name and meta.version): + # If pkginfo encounters a metadata version it doesn't support, it may give us + # 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( - "Invalid distribution metadata. " - "This version of twine supports Metadata-Version " - f"{', '.join(supported_metadata[:-1])}, and {supported_metadata[-1]}" + "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)}." ) py_version: Optional[str] diff --git a/twine/repository.py b/twine/repository.py index a0164fa5..087a2f31 100644 --- a/twine/repository.py +++ b/twine/repository.py @@ -77,19 +77,13 @@ def __init__( @staticmethod def _make_adapter_with_retries() -> adapters.HTTPAdapter: - retry_kwargs = dict( + retry = urllib3.Retry( + allowed_methods=["GET"], connect=5, total=10, status_forcelist=[500, 501, 502, 503], ) - try: - retry = urllib3.Retry(allowed_methods=["GET"], **retry_kwargs) - except TypeError: # pragma: no cover - # Avoiding DeprecationWarning starting in urllib3 1.26 - # Remove when that's the mininum version - retry = urllib3.Retry(method_whitelist=["GET"], **retry_kwargs) - return adapters.HTTPAdapter(max_retries=retry) @staticmethod