Skip to content

Commit

Permalink
Switch from pkginfo to packaging for parsing distribution metadata
Browse files Browse the repository at this point in the history
The packaging package is maintained by the PyPA and it is the de-facto
reference implementation for the packaging standards. Using packaging
for parsing metadata guarantees support for the latest metadata
versions.

warehouse, the Python package index implementation used by PyPI, also
uses packaging for parsing metadata. This guarantees that metadata
parsing is the same on the client and server side, for the most
prominent index.
  • Loading branch information
dnicolodi committed Nov 24, 2024
1 parent dd61356 commit f1beb8a
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 209 deletions.
5 changes: 1 addition & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ classifiers = [
]
requires-python = ">=3.8"
dependencies = [
"pkginfo >= 1.8.1",
"packaging >= 24.0",
"readme-renderer >= 35.0",
"requests >= 2.20",
"requests-toolbelt >= 0.8.0, != 0.9.0",
Expand All @@ -40,9 +40,6 @@ 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",
]
dynamic = ["version"]

Expand Down
126 changes: 68 additions & 58 deletions tests/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_sign_file(monkeypatch):
package = package_file.PackageFile(
filename=filename,
comment=None,
metadata=pretend.stub(name="deprecated-pypirc"),
metadata=dict(name="deprecated-pypirc", version="1.2.3"),
python_version=None,
filetype=None,
)
Expand All @@ -51,7 +51,7 @@ def test_sign_file_with_identity(monkeypatch):
package = package_file.PackageFile(
filename=filename,
comment=None,
metadata=pretend.stub(name="deprecated-pypirc"),
metadata=dict(name="deprecated-pypirc", version="1.2.3"),
python_version=None,
filetype=None,
)
Expand Down Expand Up @@ -106,7 +106,7 @@ def test_package_signed_name_is_correct():
package = package_file.PackageFile(
filename=filename,
comment=None,
metadata=pretend.stub(name="deprecated-pypirc"),
metadata=dict(name="deprecated-pypirc", version="1.2.3"),
python_version=None,
filetype=None,
)
Expand Down Expand Up @@ -163,7 +163,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=dict(name=pkg_name, version="1.2.3"),
python_version=None,
filetype=None,
)
Expand All @@ -172,9 +172,7 @@ def test_package_safe_name_is_correct(pkg_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()) == {
assert set(package_file.PackageMetadata.__annotations__) == {
# identify release
"name",
"version",
Expand All @@ -200,6 +198,8 @@ def test_metadata_dictionary_keys():
"md5_digest",
"sha256_digest",
"blake2_256_digest",
"gpg_signature",
"attestations",
# PEP 314
"provides",
"requires",
Expand All @@ -216,16 +216,19 @@ def test_metadata_dictionary_keys():
"description_content_type",
# Metadata 2.2
"dynamic",
# Metadata 2.4
"license_expression",
"license_files",
}


@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(
meta = dict(
name="whatever",
version=pretend.stub(),
version="1.2.3",
metadata_version=pretend.stub(),
summary=pretend.stub(),
home_page=pretend.stub(),
Expand All @@ -236,10 +239,10 @@ def test_metadata_dictionary_values(gpg_signature, attestation):
license=pretend.stub(),
description=pretend.stub(),
keywords=pretend.stub(),
platforms=pretend.stub(),
platform=pretend.stub(),
classifiers=pretend.stub(),
download_url=pretend.stub(),
supported_platforms=pretend.stub(),
supported_platform=pretend.stub(),
provides=pretend.stub(),
requires=pretend.stub(),
obsoletes=pretend.stub(),
Expand All @@ -249,7 +252,7 @@ def test_metadata_dictionary_values(gpg_signature, attestation):
requires_dist=pretend.stub(),
requires_external=pretend.stub(),
requires_python=pretend.stub(),
provides_extras=pretend.stub(),
provides_extra=pretend.stub(),
description_content_type=pretend.stub(),
dynamic=pretend.stub(),
)
Expand All @@ -269,48 +272,48 @@ def test_metadata_dictionary_values(gpg_signature, attestation):

# identify release
assert result["name"] == package.safe_name
assert result["version"] == meta.version
assert result["version"] == package.version == meta["version"]

# file content
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["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["platform"]
assert result["classifiers"] == meta["classifiers"]
assert result["download_url"] == meta["download_url"]
assert result["supported_platform"] == meta["supported_platform"]
assert result["comment"] == package.comment

# PEP 314
assert result["provides"] == meta.provides
assert result["requires"] == meta.requires
assert result["obsoletes"] == meta.obsoletes
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
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
assert result["provides_extra"] == meta["provides_extra"]
assert result["description_content_type"] == meta["description_content_type"]

# Metadata 2.2
assert result["dynamic"] == meta.dynamic
assert result["dynamic"] == meta["dynamic"]

# GPG signature
assert result.get("gpg_signature") == gpg_signature
Expand Down Expand Up @@ -381,46 +384,56 @@ 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: 102.3\nName: test-package\nVersion: 1.0.0\n",
"Name, Version",
"'102.3' is not a valid metadata version",
id="unsupported Metadata-Version",
),
pytest.param(
b"Metadata-Version: 2.3\nName: UNKNOWN\nVersion: UNKNOWN\n",
"Name, Version",
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",
),
],
)
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.
This could be caused by a version number or format it doesn't support yet.
Expand All @@ -431,10 +444,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]
)
assert "1.0, 1.1, 1.2, 2.0, 2.1, 2.2" in err.value.args[0]
assert exception_message in err.value.args[0]


def test_malformed_from_file(monkeypatch):
Expand Down
13 changes: 6 additions & 7 deletions tests/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,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

Expand All @@ -115,7 +115,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

Expand All @@ -125,8 +125,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
Expand All @@ -143,8 +143,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
Expand All @@ -161,8 +161,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
Expand Down Expand Up @@ -308,8 +308,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(
Expand Down
11 changes: 5 additions & 6 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
]
)
def example_wheel(request):
file_name = os.path.join(helpers.TESTS_DIR, request.param)
return wheel.Wheel(file_name)
return wheel.Wheel(os.path.join(helpers.TESTS_DIR, request.param))


def test_version_parsing(example_wheel):
Expand All @@ -58,7 +57,7 @@ def test_find_metadata_files():
["package", "METADATA.json"],
["package", "METADATA.txt"],
]
candidates = wheel.Wheel.find_candidate_metadata_files(names)
candidates = wheel.find_candidate_metadata_files(names)
assert expected == candidates


Expand All @@ -75,7 +74,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():
Expand All @@ -85,7 +84,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):
Expand All @@ -100,4 +99,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()
Loading

0 comments on commit f1beb8a

Please sign in to comment.