Skip to content

Commit

Permalink
Validate metadata with packaging lib
Browse files Browse the repository at this point in the history
  • Loading branch information
browniebroke committed Oct 13, 2024
1 parent ded15b9 commit 8dcb555
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies = [
"keyring >= 15.1; platform_machine != 'ppc64le' and platform_machine != 's390x'",
"rfc3986 >= 1.4.0",
"rich >= 12.0.0",
"packaging >= 24.1",

# workaround for #1116
"pkginfo < 1.11",
Expand Down
74 changes: 74 additions & 0 deletions tests/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,80 @@ def test_main(monkeypatch):
assert check_stub.calls == [pretend.call(["dist/*"], strict=False)]


def test_fails_invalid_classifiers(tmp_path, capsys, caplog):
sdist = build_sdist(
tmp_path,
{
"setup.cfg": (
"""
[metadata]
name = test-package
version = 0.0.1
long_description = file:README.md
long_description_content_type = text/markdown
classifiers =
Framework | Django | 5
"""
),
"README.md": (
"""
# test-package
A test package.
"""
),
},
)

assert check.check([sdist])

assert capsys.readouterr().out == f"Checking {sdist}: FAILED\n"

assert len(caplog.record_tuples) > 0
assert caplog.record_tuples == [
(
"twine.commands.check",
logging.ERROR,
"`Framework | Django | 5` is not a valid classifier"
" and would prevent upload to PyPI.\n",
)
]


def test_passes_valid_classifiers(tmp_path, capsys, caplog):
sdist = build_sdist(
tmp_path,
{
"setup.cfg": (
"""
[metadata]
name = test-package
version = 0.0.1
long_description = file:README.md
long_description_content_type = text/markdown
classifiers =
Programming Language :: Python :: 3
Framework :: Django
Framework :: Django :: 5.1
"""
),
"README.md": (
"""
# test-package
A test package.
"""
),
},
)

assert not check.check([sdist])

assert capsys.readouterr().out == f"Checking {sdist}: PASSED\n"

assert caplog.record_tuples == []


# TODO: Test print() color output

# TODO: Test log formatting
3 changes: 3 additions & 0 deletions twine/commands/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ def _check_file(
if rendering_result is None:
is_ok = False

# Will raise an exception if the metadata is invalid
package.metadata_obj()

return warnings, is_ok


Expand Down
45 changes: 45 additions & 0 deletions twine/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import sys
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union, cast

from packaging.metadata import Metadata as PackagingMetadata

if sys.version_info >= (3, 10):
import importlib.metadata as importlib_metadata
else:
Expand Down Expand Up @@ -142,6 +144,49 @@ def from_filename(cls, filename: str, comment: Optional[str]) -> "PackageFile":

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

def metadata_obj(self) -> PackagingMetadata:
meta = self.metadata
# TODO: this should probably take metadata_dictionary as base and modify
# it to remove the keys that aren't accepted by packaging.RawMetadata
data = {
# Metadata 1.0 - PEP 241
"metadata_version": meta.metadata_version,
"name": self.safe_name,
"version": meta.version,
"platforms": meta.supported_platforms,
"summary": meta.summary,
"description": meta.description,
"keywords": meta.keywords,
"home_page": meta.home_page,
"author": meta.author,
"author_email": meta.author_email,
"license": meta.license,
# Metadata 1.1 - PEP 314
"supported_platforms": meta.supported_platforms,
"download_url": meta.download_url,
"classifiers": meta.classifiers,
"requires": meta.requires,
"provides": meta.provides,
"obsoletes": meta.obsoletes,
# Metadata 1.2 - PEP 345
"maintainer": meta.maintainer,
"maintainer_email": meta.maintainer_email,
"requires_dist": meta.requires_dist,
"provides_dist": meta.provides_dist,
"obsoletes_dist": meta.obsoletes_dist,
"requires_python": meta.requires_python,
"requires_external": meta.requires_external,
"project_urls": meta.project_urls,
# Metadata 2.1 - PEP 566
"description_content_type": meta.description_content_type,
"provides_extra": meta.provides_extras,
}
if meta.metadata_version and meta.metadata_version > "2.1":
# Metadata 2.2 - PEP 643
data.update({"dynamic": meta.dynamic})

return PackagingMetadata.from_raw(data, validate=True) # type: ignore[arg-type]

def metadata_dictionary(self) -> Dict[str, MetadataValue]:
"""Merge multiple sources of metadata into a single dictionary.
Expand Down

0 comments on commit 8dcb555

Please sign in to comment.