diff --git a/changelog/1180.misc.txt b/changelog/1180.misc.txt new file mode 100644 index 00000000..db3d8158 --- /dev/null +++ b/changelog/1180.misc.txt @@ -0,0 +1,10 @@ +- ``packaging`` is used instead of ``pkginfo`` for parsing and validating + metadata. This aligns metadata validation to the one performed by PyPI. + ``packaging`` version 24.0 or later is required. Support for metadata + version 2.4 requires ``packaging`` 24.2 or later. ``pkginfo`` is not a + dependency anymore. +- With ``packaging`` version 24.2 or later, metadata fields added with + metadata version 2.4 as defined by PEP 639 are now sent to the package index + when a distribution is uploaded. This results in licensing information to + appear correctly on the package page on PyPI when uploading packages using + metadata version 2.4. diff --git a/docs/conf.py b/docs/conf.py index ddef188a..df1e1abe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -283,17 +283,12 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "requests": ("https://requests.readthedocs.io/en/latest/", None), + "packaging": ("https://packaging.pypa.io/en/latest/", None), } # Be strict about the invalid references: nitpicky = True -# TODO: Try to add these to intersphinx_mapping -nitpick_ignore_regex = [ - (r"py:.*", r"pkginfo.*"), - ("py:class", r"warnings\.WarningMessage"), -] - # -- Options for apidoc output ------------------------------------------------ autodoc_default_options = { diff --git a/mypy.ini b/mypy.ini index 0c09ee80..aea869cb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -5,8 +5,7 @@ show_traceback = True warn_redundant_casts = True warn_unused_configs = True warn_unused_ignores = True -; Enabling this will fail on subclasses of untyped imports, e.g. pkginfo -; disallow_subclassing_any = True +disallow_subclassing_any = True disallow_any_generics = True disallow_untyped_calls = True disallow_untyped_defs = True diff --git a/pyproject.toml b/pyproject.toml index e976d56d..62c32c04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "pkginfo >= 1.8.1", "readme-renderer >= 35.0", "requests >= 2.20", "requests-toolbelt >= 0.8.0, != 0.9.0", @@ -41,7 +40,7 @@ dependencies = [ "keyring >= 15.1; platform_machine != 'ppc64le' and platform_machine != 's390x'", "rfc3986 >= 1.4.0", "rich >= 12.0.0", - "packaging", + "packaging >= 24.0", "id", ] dynamic = ["version"] diff --git a/tests/fixtures/everything.metadata b/tests/fixtures/everything.metadata new file mode 100644 index 00000000..eea1351b --- /dev/null +++ b/tests/fixtures/everything.metadata @@ -0,0 +1,44 @@ +Metadata-Version: 2.4 +Name: BeagleVote +Version: 1.0a2 +Platform: ObscureUnix +Platform: RareDOS +Supported-Platform: RedHat 7.2 +Supported-Platform: i386-win32-2791 +Summary: A module for collecting votes from beagles. +Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM +Keywords: dog,puppy,voting,election +Home-page: http://www.example.com/~cschultz/bvote/ +Download-URL: …/BeagleVote-0.45.tgz +Author: C. Schultz, Universal Features Syndicate, + Los Angeles, CA +Author-email: "C. Schultz" +Maintainer: C. Schultz, Universal Features Syndicate, + Los Angeles, CA +Maintainer-email: "C. Schultz" +License: This software may only be obtained by sending the + author a postcard, and then the user promises not + to redistribute it. +License-Expression: Apache-2.0 OR BSD-2-Clause +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console (Text Based) +Provides-Extra: pdf +Requires-Dist: reportlab; extra == 'pdf' +Requires-Dist: pkginfo +Requires-Dist: PasteDeploy +Requires-Dist: zope.interface (>3.5.0) +Requires-Dist: pywin32 >1.0; sys_platform == 'win32' +Requires-Python: >=3 +Requires-External: C +Requires-External: libpng (>=1.5) +Requires-External: make; sys_platform != "win32" +Project-URL: Bug Tracker, http://bitbucket.org/tarek/distribute/issues/ +Project-URL: Documentation, https://example.com/BeagleVote +Provides-Dist: OtherProject +Provides-Dist: AnotherProject (3.4) +Provides-Dist: virtual_package; python_version >= "3.4" +Dynamic: Obsoletes-Dist + +This description intentionally left blank. diff --git a/tests/fixtures/twine-1.5.0.zip b/tests/fixtures/twine-1.5.0.zip new file mode 100644 index 00000000..55cbf7a1 Binary files /dev/null and b/tests/fixtures/twine-1.5.0.zip differ diff --git a/tests/test_package.py b/tests/test_package.py index 61f4b1d2..d843fa17 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -16,6 +16,7 @@ import pretend import pytest +from packaging import metadata from twine import exceptions from twine import package as package_file @@ -31,7 +32,7 @@ def test_sign_file(monkeypatch): package = package_file.PackageFile( filename=filename, comment=None, - metadata=pretend.stub(name="deprecated-pypirc"), + metadata=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), python_version=None, filetype=None, ) @@ -51,7 +52,7 @@ def test_sign_file_with_identity(monkeypatch): package = package_file.PackageFile( filename=filename, comment=None, - metadata=pretend.stub(name="deprecated-pypirc"), + metadata=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), python_version=None, filetype=None, ) @@ -106,7 +107,7 @@ def test_package_signed_name_is_correct(): package = package_file.PackageFile( filename=filename, comment=None, - metadata=pretend.stub(name="deprecated-pypirc"), + metadata=metadata.RawMetadata(name="deprecated-pypirc", version="1.2.3"), python_version=None, filetype=None, ) @@ -163,7 +164,7 @@ def test_package_safe_name_is_correct(pkg_name, expected_name): package = package_file.PackageFile( filename="tests/fixtures/deprecated-pypirc", comment=None, - metadata=pretend.stub(name=pkg_name), + metadata=metadata.RawMetadata(name=pkg_name, version="1.2.3"), python_version=None, filetype=None, ) @@ -171,87 +172,49 @@ def test_package_safe_name_is_correct(pkg_name, expected_name): assert package.safe_name == expected_name -def test_metadata_dictionary_keys(): - """Merge multiple sources of metadata into a single dictionary.""" - package = package_file.PackageFile.from_filename(helpers.SDIST_FIXTURE, None) - assert set(package.metadata_dictionary()) == { - # identify release - "name", - "version", - # file content - "filetype", - "pyversion", - # additional meta-data - "metadata_version", - "summary", - "home_page", - "author", - "author_email", - "maintainer", - "maintainer_email", - "license", - "description", - "keywords", - "platform", - "classifiers", - "download_url", - "supported_platform", - "comment", - "md5_digest", - "sha256_digest", - "blake2_256_digest", - # PEP 314 - "provides", - "requires", - "obsoletes", - # Metadata 1.2 - "project_urls", - "provides_dist", - "obsoletes_dist", - "requires_dist", - "requires_external", - "requires_python", - # Metadata 2.1 - "provides_extra", - "description_content_type", - # Metadata 2.2 - "dynamic", - } +def test_metadata_keys_consistency(): + """Check that the translation keys exist in the respective ``TypedDict``.""" + raw_keys = metadata.RawMetadata.__annotations__.keys() + assert set(package_file._RAW_TO_PACKAGE_METADATA.keys()).issubset(raw_keys) + package_keys = package_file.PackageMetadata.__annotations__.keys() + assert set(package_file._RAW_TO_PACKAGE_METADATA.values()).issubset(package_keys) @pytest.mark.parametrize("gpg_signature", [(None), (pretend.stub())]) @pytest.mark.parametrize("attestation", [(None), ({"fake": "attestation"})]) def test_metadata_dictionary_values(gpg_signature, attestation): - """Pass values from pkginfo.Distribution through to dictionary.""" - meta = pretend.stub( - name="whatever", - version=pretend.stub(), + """Pass values from ``metadata.RawMetadata`` through to ``PackageMetadata``.""" + meta = metadata.RawMetadata( metadata_version=pretend.stub(), + name="whatever", + version="1.2.3", + platforms=pretend.stub(), summary=pretend.stub(), + description=pretend.stub(), + keywords=pretend.stub(), home_page=pretend.stub(), author=pretend.stub(), author_email=pretend.stub(), - maintainer=pretend.stub(), - maintainer_email=pretend.stub(), license=pretend.stub(), - description=pretend.stub(), - keywords=pretend.stub(), - platforms=pretend.stub(), - classifiers=pretend.stub(), - download_url=pretend.stub(), supported_platforms=pretend.stub(), + download_url=pretend.stub(), + classifiers=pretend.stub(), provides=pretend.stub(), requires=pretend.stub(), obsoletes=pretend.stub(), - project_urls=pretend.stub(), + maintainer=pretend.stub(), + maintainer_email=pretend.stub(), + requires_dist=pretend.stub(), provides_dist=pretend.stub(), obsoletes_dist=pretend.stub(), - requires_dist=pretend.stub(), - requires_external=pretend.stub(), requires_python=pretend.stub(), - provides_extras=pretend.stub(), + requires_external=pretend.stub(), + project_urls=pretend.stub(), description_content_type=pretend.stub(), + provides_extra=pretend.stub(), dynamic=pretend.stub(), + license_expression=pretend.stub(), + license_files=pretend.stub(), ) package = package_file.PackageFile( @@ -267,51 +230,53 @@ def test_metadata_dictionary_values(gpg_signature, attestation): result = package.metadata_dictionary() - # identify release + # Medata 1.0 - PEP 241 + assert result["metadata_version"] == meta["metadata_version"] assert result["name"] == package.safe_name - assert result["version"] == meta.version - - # file content + assert result["version"] == package.version == meta["version"] + assert result["platform"] == meta["platforms"] + assert result["summary"] == meta["summary"] + assert result["description"] == meta["description"] + assert result["keywords"] == meta["keywords"] + assert result["home_page"] == meta["home_page"] + assert result["author"] == meta["author"] + assert result["author_email"] == meta["author_email"] + assert result["license"] == meta["license"] + + # Metadata 1.1 - PEP 314 + assert result["supported_platform"] == meta["supported_platforms"] + assert result["download_url"] == meta["download_url"] + assert result["classifiers"] == meta["classifiers"] + assert result["provides"] == meta["provides"] + assert result["requires"] == meta["requires"] + assert result["obsoletes"] == meta["obsoletes"] + + # Metadata 1.2 - PEP 345 + assert result["maintainer"] == meta["maintainer"] + assert result["maintainer_email"] == meta["maintainer_email"] + assert result["requires_dist"] == meta["requires_dist"] + assert result["provides_dist"] == meta["provides_dist"] + assert result["obsoletes_dist"] == meta["obsoletes_dist"] + assert result["requires_python"] == meta["requires_python"] + assert result["requires_external"] == meta["requires_external"] + assert result["project_urls"] == meta["project_urls"] + + # Metadata 2.1 - PEP 566 + assert result["description_content_type"] == meta["description_content_type"] + assert result["provides_extra"] == meta["provides_extra"] + + # Metadata 2.2 - PEP 643 + assert result["dynamic"] == meta["dynamic"] + + # Metadata 2.4 - PEP 639 + assert result["license_expression"] == meta["license_expression"] + assert result["license_file"] == meta["license_files"] + + # Additional metadata + assert result["comment"] == package.comment assert result["filetype"] == package.filetype assert result["pyversion"] == package.python_version - # additional meta-data - assert result["metadata_version"] == meta.metadata_version - assert result["summary"] == meta.summary - assert result["home_page"] == meta.home_page - assert result["author"] == meta.author - assert result["author_email"] == meta.author_email - assert result["maintainer"] == meta.maintainer - assert result["maintainer_email"] == meta.maintainer_email - assert result["license"] == meta.license - assert result["description"] == meta.description - assert result["keywords"] == meta.keywords - assert result["platform"] == meta.platforms - assert result["classifiers"] == meta.classifiers - assert result["download_url"] == meta.download_url - assert result["supported_platform"] == meta.supported_platforms - assert result["comment"] == package.comment - - # PEP 314 - assert result["provides"] == meta.provides - assert result["requires"] == meta.requires - assert result["obsoletes"] == meta.obsoletes - - # Metadata 1.2 - assert result["project_urls"] == meta.project_urls - assert result["provides_dist"] == meta.provides_dist - assert result["obsoletes_dist"] == meta.obsoletes_dist - assert result["requires_dist"] == meta.requires_dist - assert result["requires_external"] == meta.requires_external - assert result["requires_python"] == meta.requires_python - - # Metadata 2.1 - assert result["provides_extra"] == meta.provides_extras - assert result["description_content_type"] == meta.description_content_type - - # Metadata 2.2 - assert result["dynamic"] == meta.dynamic - # GPG signature assert result.get("gpg_signature") == gpg_signature @@ -381,41 +346,61 @@ def test_fips_metadata_excludes_md5_and_blake2(monkeypatch): @pytest.mark.parametrize( - "read_data, missing_fields", + "read_data, exception_message", [ pytest.param( - b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: UNKNOWN\n", - "Name, Version", + b"Metadata-Version: 102.3\nName: test-package\nVersion: 1.0.0\n", + "'102.3' is not a valid metadata version", + id="unsupported Metadata-Version", + ), + pytest.param( + b"Metadata-Version: 2.3\nName: test-package\nVersion: UNKNOWN\n", + "'UNKNOWN' is invalid for 'version'", + id="invalid Version", + ), + pytest.param( + b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", + "'UNKNOWN' is invalid for 'version'", + id="invalid Version", + ), + pytest.param( + b"Metadata-Version: 2.3\n", + "'name' is a required field; 'version' is a required field", id="missing Name and Version", ), pytest.param( - b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: UNKNOWN\n", - "Name, Version", + b"Metadata-Version: 2.2\n", + "'name' is a required field; 'version' is a required field", id="missing Name and Version", ), pytest.param( - b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: 1.0.0\n", - "Name", + b"Metadata-Version: 2.3\nVersion: 1.0.0\n", + "'name' is a required field", id="missing Name", ), pytest.param( - b"Metadata-Version: 2.2\nName: UNKNOWN\nVersion: 1.0.0\n", - "Name", + b"Metadata-Version: 2.2\nVersion: 1.0.0\n", + "'name' is a required field", id="missing Name", ), pytest.param( - b"Metadata-Version: 2.3\nName: test-package\nVersion: UNKNOWN\n", - "Version", + b"Metadata-Version: 2.3\nName: test-package\n", + "'version' is a required field", id="missing Version", ), pytest.param( - b"Metadata-Version: 2.2\nName: test-package\nVersion: UNKNOWN\n", - "Version", + b"Metadata-Version: 2.2\nName: test-package\n", + "'version' is a required field", id="missing Version", ), + pytest.param( + b"Metadata-Version: 2.2\nName: test-package\nVersion: 1.0.0\nFoo: bar\n", + "unrecognized or malformed field 'foo'", + id="unrecognized field", + ), ], ) -def test_pkginfo_returns_no_metadata(read_data, missing_fields, monkeypatch): +def test_pkginfo_returns_no_metadata(read_data, exception_message, monkeypatch): """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" @@ -423,34 +408,7 @@ def test_pkginfo_returns_no_metadata(read_data, missing_fields, monkeypatch): with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) - 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] + assert exception_message in err.value.args[0] def test_malformed_from_file(monkeypatch): @@ -474,3 +432,34 @@ def test_package_from_unrecognized_file_error(): with pytest.raises(exceptions.InvalidDistribution) as err: package_file.PackageFile.from_filename(filename, comment=None) assert "Unknown distribution format" in err.value.args[0] + + +@pytest.mark.parametrize( + "read_data, filtered", + [ + pytest.param( + "Metadata-Version: 2.1\n" + "Name: test-package\n" + "Version: 1.0.0\n" + "License-File: LICENSE\n", + True, + id="invalid License-File", + ), + pytest.param( + "Metadata-Version: 2.4\n" + "Name: test-package\n" + "Version: 1.0.0\n" + "License-File: LICENSE\n", + False, + id="valid License-File", + ), + ], +) +def test_setuptools_license_file(read_data, filtered, monkeypatch): + """Drop License-File metadata entries if Metadata-Version is less than 2.4.""" + monkeypatch.setattr(package_file.wheel.Wheel, "read", lambda _: read_data) + filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" + + package = package_file.PackageFile.from_filename(filename, comment=None) + meta = package.metadata_dictionary() + assert filtered != ("license_file" in meta) diff --git a/tests/test_repository.py b/tests/test_repository.py index 2c6bd454..7526a36f 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -18,6 +18,7 @@ import pytest import requests +from twine import package from twine import repository from twine import utils @@ -37,37 +38,46 @@ def test_gpg_signature_structure_is_preserved(): "gpg_signature": ("filename.asc", "filecontent"), } - tuples = repository.Repository._convert_data_to_list_of_tuples(data) + tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) assert tuples == [("gpg_signature", ("filename.asc", "filecontent"))] -def test_content_structure_is_preserved(): - """Preserve 'content' key when converting metadata.""" - data = { - "content": ("filename", "filecontent"), - } - - tuples = repository.Repository._convert_data_to_list_of_tuples(data) - assert tuples == [("content", ("filename", "filecontent"))] - - def test_iterables_are_flattened(): """Flatten iterable metadata to list of tuples.""" data = { "platform": ["UNKNOWN"], } - tuples = repository.Repository._convert_data_to_list_of_tuples(data) + tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN")] data = { "platform": ["UNKNOWN", "ANOTHERPLATFORM"], } - tuples = repository.Repository._convert_data_to_list_of_tuples(data) + tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) assert tuples == [("platform", "UNKNOWN"), ("platform", "ANOTHERPLATFORM")] +def test_all_metadata_fields_are_flattened(monkeypatch): + """Verify that package metadata fields are correctly flattened.""" + # This file contains all metadata fields known up to metadata version 2.4. + metadata = open("tests/fixtures/everything.metadata") + monkeypatch.setattr(package.wheel.Wheel, "read", metadata.read) + filename = "tests/fixtures/twine-1.5.0-py2.py3-none-any.whl" + data = package.PackageFile.from_filename( + filename, comment="comment" + ).metadata_dictionary() + tuples = repository.Repository._convert_metadata_to_list_of_tuples(data) + # Verifies that all metadata fields parsed by ``packaging.metadata`` are + # correctly flattened into a list of (str, str) tuples. This does not + # apply to the ``gpg_signature`` field, but this field is not added here + # as there are specific tests for it. + for key, value in tuples: + assert isinstance(key, str) + assert isinstance(value, str) + + def test_set_client_certificate(default_repo): """Set client certificate for session.""" assert default_repo.session.cert is None @@ -103,7 +113,7 @@ def test_package_is_uploaded_404s(default_repo): default_repo.session = pretend.stub( get=lambda url, headers: response_with(status_code=404) ) - package = pretend.stub(safe_name="fake", metadata=pretend.stub(version="2.12.0")) + package = pretend.stub(safe_name="fake", version="2.12.0") assert default_repo.package_is_uploaded(package) is False @@ -115,7 +125,7 @@ def test_package_is_uploaded_200s_with_no_releases(default_repo): status_code=200, _content=b'{"releases": {}}', _content_consumed=True ), ) - package = pretend.stub(safe_name="fake", metadata=pretend.stub(version="2.12.0")) + package = pretend.stub(safe_name="fake", version="2.12.0") assert default_repo.package_is_uploaded(package) is False @@ -125,8 +135,8 @@ def test_package_is_uploaded_with_releases_using_cache(default_repo): default_repo._releases_json_data = {"fake": {"0.1": [{"filename": "fake.whl"}]}} package = pretend.stub( safe_name="fake", + version="0.1", basefilename="fake.whl", - metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package) is True @@ -143,8 +153,8 @@ def test_package_is_uploaded_with_releases_not_using_cache(default_repo): ) package = pretend.stub( safe_name="fake", + version="0.1", basefilename="fake.whl", - metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package, bypass_cache=True) is True @@ -161,8 +171,8 @@ def test_package_is_uploaded_different_filenames(default_repo): ) package = pretend.stub( safe_name="fake", + version="0.1", basefilename="foo.whl", - metadata=pretend.stub(version="0.1"), ) assert default_repo.package_is_uploaded(package) is False @@ -308,8 +318,7 @@ def test_upload_retry(tmpdir, default_repo, caplog): def test_release_urls(package_meta, repository_url, release_urls): """Generate a set of PyPI release URLs for a list of packages.""" packages = [ - pretend.stub(safe_name=name, metadata=pretend.stub(version=version)) - for name, version in package_meta + pretend.stub(safe_name=name, version=version) for name, version in package_meta ] repo = repository.Repository( diff --git a/tests/test_sdist.py b/tests/test_sdist.py new file mode 100644 index 00000000..27bbabc8 --- /dev/null +++ b/tests/test_sdist.py @@ -0,0 +1,198 @@ +import io +import os +import pathlib +import tarfile +import textwrap +import zipfile + +import pytest + +from twine import exceptions +from twine import sdist + +from .helpers import TESTS_DIR + + +@pytest.fixture( + params=[ + "fixtures/twine-1.5.0.tar.gz", + "fixtures/twine-1.6.5.tar.gz", + "fixtures/twine-1.5.0.zip", + ] +) +def example_sdist(request): + file_name = os.path.join(TESTS_DIR, request.param) + return sdist.SDist(file_name) + + +@pytest.fixture(params=["tar.gz", "zip"]) +def archive_format(request): + return request.param + + +def build_archive(path, name, archive_format, files): + filepath = path / f"{name}.{archive_format}" + + if archive_format == "tar.gz": + with tarfile.open(filepath, "x:gz") as archive: + for mname, content in files.items(): + if isinstance(content, tarfile.TarInfo): + content.name = mname + archive.addfile(content) + else: + data = textwrap.dedent(content).encode("utf8") + member = tarfile.TarInfo(mname) + member.size = len(data) + archive.addfile(member, io.BytesIO(data)) + return str(filepath) + + if archive_format == "zip": + with zipfile.ZipFile(filepath, mode="w") as archive: + for mname, content in files.items(): + archive.writestr(mname, textwrap.dedent(content)) + return str(filepath) + + raise ValueError(format) + + +def test_read_example(example_sdist): + """Parse metadata from a valid sdist file.""" + metadata = example_sdist.read() + assert b"Metadata-Version: 1.1" in metadata + assert b"Name: twine" in metadata + assert b"Version: 1." in metadata + + +def test_read_non_existent(): + """Raise an exception when sdist file doesn't exist.""" + file_name = str(pathlib.Path("/foo/bar/baz.tar.gz").resolve()) + with pytest.raises(exceptions.InvalidDistribution, match="No such file"): + sdist.SDist(file_name).read() + + +def test_formar_not_supported(): + """Raise an exception when sdist is not a .tar.gz or a .zip.""" + file_name = str(pathlib.Path("/foo/bar/baz.foo").resolve()) + with pytest.raises(exceptions.InvalidDistribution, match="Unsupported sdist"): + sdist.SDist(file_name).read() + + +def test_read(archive_format, tmp_path): + """Read PKG-INFO from a valid sdist.""" + filename = build_archive( + tmp_path, + "test-1.2.3", + archive_format, + { + "test-1.2.3/README": "README", + "test-1.2.3/PKG-INFO": """ + Metadata-Version: 1.1 + Name: test + Version: 1.2.3 + """, + }, + ) + + metadata = sdist.SDist(filename).read() + assert b"Metadata-Version: 1.1" in metadata + assert b"Name: test" in metadata + assert b"Version: 1.2.3" in metadata + + +def test_missing_pkg_info(archive_format, tmp_path): + """Raise an exception when sdist does not contain PKG-INFO.""" + filename = build_archive( + tmp_path, + "test-1.2.3", + archive_format, + { + "test-1.2.3/README": "README", + }, + ) + + with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): + sdist.SDist(filename).read() + + +def test_invalid_pkg_info(archive_format, tmp_path): + """Raise an exception when PKG-INFO does not contain ``Metadata-Version``.""" + filename = build_archive( + tmp_path, + "test-1.2.3", + archive_format, + { + "test-1.2.3/README": "README", + "test-1.2.3/PKG-INFO": """ + Name: test + Version: 1.2.3. + """, + }, + ) + + with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): + sdist.SDist(filename).read() + + +def test_pkg_info_directory(archive_format, tmp_path): + """Raise an exception when PKG-INFO is a directory.""" + filename = build_archive( + tmp_path, + "test-1.2.3", + archive_format, + { + "test-1.2.3/README": "README", + "test-1.2.3/PKG-INFO/content": """ + Metadata-Version: 1.1 + Name: test + Version: 1.2.3. + """, + }, + ) + + with pytest.raises(exceptions.InvalidDistribution, match="No PKG-INFO in archive"): + sdist.SDist(filename).read() + + +def test_pkg_info_not_regular_file(tmp_path): + """Raise an exception when PKG-INFO is a directory.""" + link = tarfile.TarInfo() + link.type = tarfile.LNKTYPE + link.linkname = "README" + + filename = build_archive( + tmp_path, + "test-1.2.3", + "tar.gz", + { + "test-1.2.3/README": "README", + "test-1.2.3/PKG-INFO": link, + }, + ) + + with pytest.raises(exceptions.InvalidDistribution, match="PKG-INFO is not a reg"): + sdist.SDist(filename).read() + + +def test_multiple_top_level(archive_format, tmp_path): + """Raise an exception when there are too many top-level members.""" + filename = build_archive( + tmp_path, + "test-1.2.3", + archive_format, + { + "test-1.2.3/README": "README", + "test-1.2.3/PKG-INFO": """ + Metadata-Version: 1.1 + Name: test + Version: 1.2.3. + """, + "test-2.0.0/README": "README", + }, + ) + + with pytest.raises(exceptions.InvalidDistribution, match="Too many top-level"): + sdist.SDist(filename).read() + + +def test_py_version(example_sdist): + assert example_sdist.py_version == "source" diff --git a/tests/test_wheel.py b/tests/test_wheel.py index eae3bd95..0dedf6cc 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -75,7 +75,7 @@ def test_read_non_existent_wheel_file_name(): with pytest.raises( exceptions.InvalidDistribution, match=re.escape(f"No such file: {file_name}") ): - wheel.Wheel(file_name) + wheel.Wheel(file_name).read() def test_read_invalid_wheel_extension(): @@ -85,7 +85,7 @@ def test_read_invalid_wheel_extension(): exceptions.InvalidDistribution, match=re.escape(f"Not a known archive format for file: {file_name}"), ): - wheel.Wheel(file_name) + wheel.Wheel(file_name).read() def test_read_wheel_empty_metadata(tmpdir): @@ -100,4 +100,4 @@ def test_read_wheel_empty_metadata(tmpdir): f"No METADATA in archive or METADATA missing 'Metadata-Version': {whl_file}" ), ): - wheel.Wheel(whl_file) + wheel.Wheel(whl_file).read() diff --git a/twine/cli.py b/twine/cli.py index f44426e3..d23a4bf8 100644 --- a/twine/cli.py +++ b/twine/cli.py @@ -77,7 +77,7 @@ def configure_output() -> None: def list_dependencies_and_versions() -> List[Tuple[str, str]]: deps = [ "keyring", # optional for non-desktop use - "pkginfo", + "packaging", "requests", "requests-toolbelt", "urllib3", diff --git a/twine/commands/check.py b/twine/commands/check.py index 9637651e..47d15a17 100644 --- a/twine/commands/check.py +++ b/twine/commands/check.py @@ -18,7 +18,7 @@ import io import logging import re -from typing import Dict, List, Optional, Tuple, cast +from typing import Dict, List, Tuple import readme_renderer.rst from rich import print @@ -84,8 +84,8 @@ def _check_file( package = package_file.PackageFile.from_filename(filename, comment=None) metadata = package.metadata_dictionary() - description = cast(Optional[str], metadata["description"]) - description_content_type = cast(Optional[str], metadata["description_content_type"]) + description = metadata.get("description") + description_content_type = metadata.get("description_content_type") if description_content_type is None: warnings.append( diff --git a/twine/distribution.py b/twine/distribution.py new file mode 100644 index 00000000..61e2ab7f --- /dev/null +++ b/twine/distribution.py @@ -0,0 +1,8 @@ +class Distribution: + + def read(self) -> bytes: + raise NotImplementedError + + @property + def py_version(self) -> str: + return "any" diff --git a/twine/package.py b/twine/package.py index e31d4fd4..5d8db29a 100644 --- a/twine/package.py +++ b/twine/package.py @@ -18,36 +18,31 @@ import os import re import subprocess -import sys -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 typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypedDict + +from packaging import metadata +from packaging import version from rich import print from twine import exceptions +from twine import sdist from twine import wheel +# Monkeypatch Metadata 2.0 support +metadata._VALID_METADATA_VERSIONS = [ + "1.0", + "1.1", + "1.2", + "2.0", + "2.1", + "2.2", + "2.3", + "2.4", +] + DIST_TYPES = { "bdist_wheel": wheel.Wheel, - "sdist": pkginfo.SDist, + "sdist": sdist.SDist, } DIST_EXTENSIONS = { @@ -56,8 +51,6 @@ ".zip": "sdist", } -MetadataValue = Union[Optional[str], Sequence[str], Tuple[str, bytes]] - logger = logging.getLogger(__name__) @@ -72,11 +65,101 @@ 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.""" +# Map ``metadata.RawMetadata`` fields to ``PackageMetadata`` fields. Some +# fields are renamed to match the names expected in the upload form. +_RAW_TO_PACKAGE_METADATA = { + # Metadata 1.0 - PEP 241 + "metadata_version": "metadata_version", + "name": "name", + "version": "version", + "platforms": "platform", # Renamed + "summary": "summary", + "description": "description", + "keywords": "keywords", + "home_page": "home_page", + "author": "author", + "author_email": "author_email", + "license": "license", + # Metadata 1.1 - PEP 314 + "supported_platforms": "supported_platform", # Renamed + "download_url": "download_url", + "classifiers": "classifiers", + "requires": "requires", + "provides": "provides", + "obsoletes": "obsoletes", + # Metadata 1.2 - PEP 345 + "maintainer": "maintainer", + "maintainer_email": "maintainer_email", + "requires_dist": "requires_dist", + "provides_dist": "provides_dist", + "obsoletes_dist": "obsoletes_dist", + "requires_python": "requires_python", + "requires_external": "requires_external", + "project_urls": "project_urls", + # Metadata 2.1 - PEP 566 + "description_content_type": "description_content_type", + "provides_extra": "provides_extra", + # Metadata 2.2 - PEP 643 + "dynamic": "dynamic", + # Metadata 2.4 - PEP 639 + "license_expression": "license_expression", + "license_files": "license_file", # Renamed +} + + +class PackageMetadata(TypedDict, total=False): + # Metadata 1.0 - PEP 241 + metadata_version: str name: str version: str + platform: List[str] + summary: str + description: str + keywords: List[str] + home_page: str + author: str + author_email: str + license: str + + # Metadata 1.1 - PEP 314 + supported_platform: List[str] + download_url: str + classifiers: List[str] + requires: List[str] + provides: List[str] + obsoletes: List[str] + + # Metadata 1.2 - PEP 345 + maintainer: str + maintainer_email: str + requires_dist: List[str] + provides_dist: List[str] + obsoletes_dist: List[str] + requires_python: str + requires_external: List[str] + project_urls: Dict[str, str] + + # Metadata 2.1 - PEP 566 + description_content_type: str + provides_extra: List[str] + + # Metadata 2.2 - PEP 643 + dynamic: List[str] + + # Metadata 2.4 - PEP 639 + license_expression: str + license_file: List[str] + + # Additional metadata + comment: Optional[str] + pyversion: str + filetype: str + gpg_signature: Tuple[str, bytes] + attestations: str + md5_digest: str + sha256_digest: Optional[str] + blake2_256_digest: str class PackageFile: @@ -84,9 +167,9 @@ def __init__( self, filename: str, comment: Optional[str], - metadata: CheckedDistribution, - python_version: Optional[str], - filetype: Optional[str], + metadata: metadata.RawMetadata, + python_version: str, + filetype: str, ) -> None: self.filename = filename self.basefilename = os.path.basename(filename) @@ -94,7 +177,8 @@ def __init__( self.metadata = metadata self.python_version = python_version self.filetype = filetype - self.safe_name = _safe_name(metadata.name) + self.safe_name = _safe_name(metadata["name"]) + self.version: str = metadata["version"] self.signed_filename = self.filename + ".asc" self.signed_basefilename = self.basefilename + ".asc" self.gpg_signature: Optional[Tuple[str, bytes]] = None @@ -114,8 +198,9 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile": for ext, dtype in DIST_EXTENSIONS.items(): if filename.endswith(ext): try: - with warnings.catch_warnings(record=True) as captured: - meta = DIST_TYPES[dtype](filename) + dist = DIST_TYPES[dtype](filename) + data = dist.read() + py_version = dist.py_version except EOFError: raise exceptions.InvalidDistribution( "Invalid distribution file: '%s'" % os.path.basename(filename) @@ -127,101 +212,58 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile": "Unknown distribution format: '%s'" % os.path.basename(filename) ) - supported_metadata = list(pkginfo.distribution.HEADER_ATTRS) - if cls._is_unknown_metadata_version(captured): + # Parse and validate metadata. + meta, unparsed = metadata.parse_email(data) + if unparsed: raise exceptions.InvalidDistribution( - "Make sure the distribution is using a supported Metadata-Version: " - f"{', '.join(supported_metadata)}." + "Invalid distribution metadata: {}".format( + "; ".join( + f"unrecognized or malformed field {key!r}" for key in unparsed + ) + ) ) - # 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: - 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)}." + # setuptools emits License-File metadata fields while declaring + # Metadata-Version 2.1. This is invalid because the metadata + # specification does not allow to add arbitrary fields, and because + # the semantic implemented by setuptools is different than the one + # described in PEP 639. However, rejecting these packages would be + # too disruptive. Drop License-File metadata entries from the data + # sent to the package index if the declared metadata version is less + # than 2.4. + if version.Version(meta.get("metadata_version", "0")) < version.Version("2.4"): + meta.pop("license_files", None) + try: + metadata.Metadata.from_raw(meta) + except metadata.ExceptionGroup as group: + raise exceptions.InvalidDistribution( + "Invalid distribution metadata: {}".format( + "; ".join(sorted(str(e) for e in group.exceptions)) ) - raise exceptions.InvalidDistribution(msg) - - if dtype == "bdist_wheel": - py_version = cast(wheel.Wheel, meta).py_version - elif dtype == "sdist": - py_version = "source" - else: - # This should not be reached. - raise ValueError - - 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") + return cls(filename, comment, meta, py_version, dtype) - def metadata_dictionary(self) -> Dict[str, MetadataValue]: + def metadata_dictionary(self) -> PackageMetadata: """Merge multiple sources of metadata into a single dictionary. Includes values from filename, PKG-INFO, hashers, and signature. """ - meta = self.metadata - data: Dict[str, MetadataValue] = { - # identify release - "name": self.safe_name, - "version": meta.version, - # file content - "filetype": self.filetype, - "pyversion": self.python_version, - # additional meta-data - "metadata_version": meta.metadata_version, - "summary": meta.summary, - "home_page": meta.home_page, - "author": meta.author, - "author_email": meta.author_email, - "maintainer": meta.maintainer, - "maintainer_email": meta.maintainer_email, - "license": meta.license, - "description": meta.description, - "keywords": meta.keywords, - "platform": meta.platforms, - "classifiers": meta.classifiers, - "download_url": meta.download_url, - "supported_platform": meta.supported_platforms, - "comment": self.comment, - "sha256_digest": self.sha2_digest, - # PEP 314 - "provides": meta.provides, - "requires": meta.requires, - "obsoletes": meta.obsoletes, - # Metadata 1.2 - "project_urls": meta.project_urls, - "provides_dist": meta.provides_dist, - "obsoletes_dist": meta.obsoletes_dist, - "requires_dist": meta.requires_dist, - "requires_external": meta.requires_external, - "requires_python": meta.requires_python, - # Metadata 2.1 - "provides_extra": meta.provides_extras, - "description_content_type": meta.description_content_type, - # Metadata 2.2 - "dynamic": meta.dynamic, - } + data = PackageMetadata() + for key, value in self.metadata.items(): + field = _RAW_TO_PACKAGE_METADATA.get(key) + if field: + # A ``TypedDict`` only support literal key names. Here key + # names are computed but they can only be valid key names. + data[field] = value # type: ignore[literal-required] + + # override name with safe name + data["name"] = self.safe_name + # file content + data["pyversion"] = self.python_version + data["filetype"] = self.filetype + # additional meta-data + data["comment"] = self.comment + data["sha256_digest"] = self.sha2_digest if self.gpg_signature is not None: data["gpg_signature"] = self.gpg_signature diff --git a/twine/repository.py b/twine/repository.py index f04f9dbc..3b5f68a8 100644 --- a/twine/repository.py +++ b/twine/repository.py @@ -22,8 +22,6 @@ from twine import package as package_file from twine.utils import make_requests_session -KEYWORDS_TO_NOT_FLATTEN = {"gpg_signature", "attestations", "content"} - LEGACY_PYPI = "https://pypi.python.org/" LEGACY_TEST_PYPI = "https://testpypi.python.org/" WAREHOUSE = "https://upload.pypi.org/" @@ -62,13 +60,27 @@ def close(self) -> None: self.session.close() @staticmethod - def _convert_data_to_list_of_tuples(data: Dict[str, Any]) -> List[Tuple[str, Any]]: - data_to_send = [] + def _convert_metadata_to_list_of_tuples( + data: package_file.PackageMetadata, + ) -> List[Tuple[str, Any]]: + # This does what ``warehouse.forklift.parse_form_metadata()`` does, in reverse. + data_to_send: List[Tuple[str, Any]] = [] for key, value in data.items(): - if key in KEYWORDS_TO_NOT_FLATTEN or not isinstance(value, (list, tuple)): + if key == "gpg_signature": + assert isinstance(value, tuple) data_to_send.append((key, value)) - else: + elif key == "project_urls": + assert isinstance(value, dict) + for name, url in value.items(): + data_to_send.append((key, f"{name}, {url}")) + elif key == "keywords": + assert isinstance(value, list) + data_to_send.append((key, ", ".join(value))) + elif isinstance(value, (list, tuple)): data_to_send.extend((key, item) for item in value) + else: + assert isinstance(value, str) or value is None + data_to_send.append((key, value)) return data_to_send def set_certificate_authority(self, cacert: Optional[str]) -> None: @@ -80,12 +92,12 @@ def set_client_certificate(self, clientcert: Optional[str]) -> None: self.session.cert = clientcert def register(self, package: package_file.PackageFile) -> requests.Response: - data = package.metadata_dictionary() - data.update({":action": "submit", "protocol_version": "1"}) - print(f"Registering {package.basefilename}") - data_to_send = self._convert_data_to_list_of_tuples(data) + metadata = package.metadata_dictionary() + data_to_send = self._convert_metadata_to_list_of_tuples(metadata) + data_to_send.append((":action", "submit")) + data_to_send.append(("protocol_version", "1")) encoder = requests_toolbelt.MultipartEncoder(data_to_send) resp = self.session.post( self.url, @@ -98,19 +110,12 @@ def register(self, package: package_file.PackageFile) -> requests.Response: return resp def _upload(self, package: package_file.PackageFile) -> requests.Response: - data = package.metadata_dictionary() - data.update( - { - # action - ":action": "file_upload", - "protocol_version": "1", - } - ) - - data_to_send = self._convert_data_to_list_of_tuples(data) - print(f"Uploading {package.basefilename}") + metadata = package.metadata_dictionary() + data_to_send = self._convert_metadata_to_list_of_tuples(metadata) + data_to_send.append((":action", "file_upload")) + data_to_send.append(("protocol_version", "1")) with open(package.filename, "rb") as fp: data_to_send.append( ( @@ -197,7 +202,7 @@ def package_is_uploaded( releases = {} self._releases_json_data[safe_name] = releases - packages = releases.get(package.metadata.version, []) + packages = releases.get(package.version, []) for uploaded_package in packages: if uploaded_package["filename"] == package.basefilename: @@ -214,7 +219,7 @@ def release_urls(self, packages: List[package_file.PackageFile]) -> Set[str]: return set() return { - f"{url}project/{package.safe_name}/{package.metadata.version}/" + f"{url}project/{package.safe_name}/{package.version}/" for package in packages } diff --git a/twine/sdist.py b/twine/sdist.py new file mode 100644 index 00000000..4808e882 --- /dev/null +++ b/twine/sdist.py @@ -0,0 +1,83 @@ +import os +import tarfile +import zipfile +from contextlib import suppress + +from twine import distribution +from twine import exceptions + + +class SDist(distribution.Distribution): + def __new__(cls, filename: str) -> "SDist": + if cls is not SDist: + return object.__new__(cls) + + FORMATS = { + ".tar.gz": TarGzSDist, + ".zip": ZipSDist, + } + + for extension, impl in FORMATS.items(): + if filename.endswith(extension): + return impl(filename) + raise exceptions.InvalidDistribution(f"Unsupported sdist format: {filename}") + + def __init__(self, filename: str) -> None: + if not os.path.exists(filename): + raise exceptions.InvalidDistribution(f"No such file: {filename}") + self.filename = filename + + @property + def py_version(self) -> str: + return "source" + + +class TarGzSDist(SDist): + + def read(self) -> bytes: + with tarfile.open(self.filename, "r:gz") as sdist: + # The sdist must contain a single top-level direcotry... + root = os.path.commonpath(sdist.getnames()) + if root in {".", "/", ""}: + raise exceptions.InvalidDistribution( + "Too many top-level members in sdist archive: {self.filename}" + ) + # ...containing the package metadata in a ``PKG-INFO`` file. + with suppress(KeyError): + member = sdist.getmember(root.rstrip("/") + "/PKG-INFO") + if not member.isfile(): + raise exceptions.InvalidDistribution( + "PKG-INFO is not a regular file: {self.filename}" + ) + fd = sdist.extractfile(member) + assert fd is not None, "for mypy" + data = fd.read() + if b"Metadata-Version" in data: + return data + + raise exceptions.InvalidDistribution( + "No PKG-INFO in archive or " + f"PKG-INFO missing 'Metadata-Version': {self.filename}" + ) + + +class ZipSDist(SDist): + + def read(self) -> bytes: + with zipfile.ZipFile(self.filename) as sdist: + # The sdist must contain a single top-level direcotry... + root = os.path.commonpath(sdist.namelist()) + if root in {".", "/", ""}: + raise exceptions.InvalidDistribution( + "Too many top-level members in sdist archive: {self.filename}" + ) + # ...containing the package metadata in a ``PKG-INFO`` file. + with suppress(KeyError): + data = sdist.read(root.rstrip("/") + "/PKG-INFO") + if b"Metadata-Version" in data: + return data + + raise exceptions.InvalidDistribution( + "No PKG-INFO in archive or " + f"PKG-INFO missing 'Metadata-Version': {self.filename}" + ) diff --git a/twine/wheel.py b/twine/wheel.py index a2a8ba8a..c1d82352 100644 --- a/twine/wheel.py +++ b/twine/wheel.py @@ -11,22 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import io import os import re import zipfile -from typing import List, Optional -from typing import cast as type_cast - -from pkginfo import distribution +from typing import List +from twine import distribution from twine import exceptions -# Monkeypatch Metadata 2.0 support -distribution.HEADER_ATTRS_2_0 = distribution.HEADER_ATTRS_1_2 -distribution.HEADER_ATTRS.update({"2.0": distribution.HEADER_ATTRS_2_0}) - - wheel_file_re = re.compile( r"""^(?P(?P.+?)(-(?P\d.+?))?) ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) @@ -36,15 +28,14 @@ class Wheel(distribution.Distribution): - def __init__(self, filename: str, metadata_version: Optional[str] = None) -> None: + def __init__(self, filename: str) -> None: + if not os.path.exists(filename): + raise exceptions.InvalidDistribution(f"No such file: {filename}") self.filename = filename - self.basefilename = os.path.basename(self.filename) - self.metadata_version = metadata_version - self.extractMetadata() @property def py_version(self) -> str: - wheel_info = wheel_file_re.match(self.basefilename) + wheel_info = wheel_file_re.match(os.path.basename(self.filename)) if wheel_info is None: return "any" else: @@ -88,12 +79,3 @@ def read_file(name: str) -> bytes: "No METADATA in archive or METADATA missing 'Metadata-Version': " "%s (searched %s)" % (fqn, ",".join(searched_files)) ) - - def parse(self, data: bytes) -> None: - super().parse(data) - - fp = io.StringIO(data.decode("utf-8", errors="replace")) - # msg is ``email.message.Message`` which is a legacy API documented - # here: https://docs.python.org/3/library/email.compat32-message.html - msg = distribution.parse(fp) - self.description = type_cast(str, msg.get_payload())