Skip to content

Commit

Permalink
Add json_metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
sbidoul committed May 7, 2022
1 parent cf3696a commit 410b395
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 1 deletion.
15 changes: 15 additions & 0 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from typing import (
IO,
TYPE_CHECKING,
Any,
Collection,
Container,
Dict,
Iterable,
Iterator,
List,
Expand All @@ -35,6 +37,8 @@
from pip._internal.utils.misc import is_local, normalize_path
from pip._internal.utils.urls import url_to_path

from .json import msg_to_json

if TYPE_CHECKING:
from typing import Protocol
else:
Expand Down Expand Up @@ -359,6 +363,17 @@ def metadata(self) -> email.message.Message:
"""
raise NotImplementedError()

@property
def json_metadata(self) -> Dict[str, Any]:
"""PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
This should return an empty dict if the metadata file is unavailable.
:raises NoneMetadataError: If the metadata file is available, but does
not contain valid metadata.
"""
return msg_to_json(self.metadata)

@property
def metadata_version(self) -> Optional[str]:
"""Value of "Metadata-Version:" in distribution metadata, if available."""
Expand Down
80 changes: 80 additions & 0 deletions src/pip/_internal/metadata/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Extracted from https://github.com/pfmoore/pkg_metadata

from email.header import Header, decode_header, make_header
from email.message import Message
from typing import Any, Dict, List, Union

METADATA_FIELDS = [
# Name, Multiple-Use
("Metadata-Version", False),
("Name", False),
("Version", False),
("Dynamic", True),
("Platform", True),
("Supported-Platform", True),
("Summary", False),
("Description", False),
("Description-Content-Type", False),
("Keywords", False),
("Home-page", False),
("Download-URL", False),
("Author", False),
("Author-email", False),
("Maintainer", False),
("Maintainer-email", False),
("License", False),
("Classifier", True),
("Requires-Dist", True),
("Requires-Python", False),
("Requires-External", True),
("Project-URL", True),
("Provides-Extra", True),
("Provides-Dist", True),
("Obsoletes-Dist", True),
]


def json_name(field: str) -> str:
return field.lower().replace("-", "_")


def msg_to_json(msg: Message) -> Dict[str, Any]:
def sanitise_header(h: Union[Header, str]) -> str:
if isinstance(h, Header):
chunks = []
for bytes, encoding in decode_header(h):
if encoding == "unknown-8bit":
try:
# See if UTF-8 works
bytes.decode("utf-8")
encoding = "utf-8"
except UnicodeDecodeError:
# If not, latin1 at least won't fail
encoding = "latin1"
chunks.append((bytes, encoding))
return str(make_header(chunks))
return str(h)

result = {}
for field, multi in METADATA_FIELDS:
if field not in msg:
continue
key = json_name(field)
if multi:
value: Union[str, List[str]] = [
sanitise_header(v) for v in msg.get_all(field)
]
else:
value = sanitise_header(msg.get(field))
if key == "keywords":
if "," in value:
value = [v.strip() for v in value.split(",")]
else:
value = value.split()
result[key] = value

payload = msg.get_payload()
if payload:
result["description"] = payload

return result
19 changes: 18 additions & 1 deletion tests/unit/metadata/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import pytest
from pip._vendor.packaging.utils import NormalizedName

from pip._internal.metadata import BaseDistribution
from pip._internal.metadata import BaseDistribution, get_wheel_distribution
from pip._internal.metadata.base import FilesystemWheel
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo
from tests.lib.path import Path
from tests.lib.wheel import make_wheel


@mock.patch.object(BaseDistribution, "read_text", side_effect=FileNotFoundError)
Expand Down Expand Up @@ -55,3 +58,17 @@ class FakeDistribution(BaseDistribution):
mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
assert direct_url.url == "https://e.c/p.tgz"
assert isinstance(direct_url.info, ArchiveInfo)


def test_json_metadata(tmpdir: Path) -> None:
"""Basic test of BaseDistribution json_metadata.
More tests are available in the original pkg_metadata project where this
function comes from, and which we may vendor in the future.
"""
wheel_path = make_wheel(name="pkga", version="1.0.1").save_to_dir(tmpdir)
wheel = FilesystemWheel(wheel_path)
dist = get_wheel_distribution(wheel, "pkga")
json_metadata = dist.json_metadata
assert json_metadata["name"] == "pkga"
assert json_metadata["version"] == "1.0.1"

0 comments on commit 410b395

Please sign in to comment.