Skip to content

Commit dc078be

Browse files
committed
Add json_metadata
1 parent cf3696a commit dc078be

File tree

3 files changed

+113
-1
lines changed

3 files changed

+113
-1
lines changed

src/pip/_internal/metadata/base.py

+15
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from typing import (
99
IO,
1010
TYPE_CHECKING,
11+
Any,
1112
Collection,
1213
Container,
14+
Dict,
1315
Iterable,
1416
Iterator,
1517
List,
@@ -35,6 +37,8 @@
3537
from pip._internal.utils.misc import is_local, normalize_path
3638
from pip._internal.utils.urls import url_to_path
3739

40+
from .json import msg_to_json
41+
3842
if TYPE_CHECKING:
3943
from typing import Protocol
4044
else:
@@ -359,6 +363,17 @@ def metadata(self) -> email.message.Message:
359363
"""
360364
raise NotImplementedError()
361365

366+
@property
367+
def json_metadata(self) -> Dict[str, Any]:
368+
"""PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
369+
370+
This should return an empty dict if the metadata file is unavailable.
371+
372+
:raises NoneMetadataError: If the metadata file is available, but does
373+
not contain valid metadata.
374+
"""
375+
return msg_to_json(self.metadata)
376+
362377
@property
363378
def metadata_version(self) -> Optional[str]:
364379
"""Value of "Metadata-Version:" in distribution metadata, if available."""

src/pip/_internal/metadata/json.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Extracted from https://github.com/pfmoore/pkg_metadata
2+
3+
from email.header import Header, decode_header, make_header
4+
from email.message import Message
5+
from typing import Any, Dict, List, Union
6+
7+
METADATA_FIELDS = [
8+
# Name, Multiple-Use
9+
("Metadata-Version", False),
10+
("Name", False),
11+
("Version", False),
12+
("Dynamic", True),
13+
("Platform", True),
14+
("Supported-Platform", True),
15+
("Summary", False),
16+
("Description", False),
17+
("Description-Content-Type", False),
18+
("Keywords", False),
19+
("Home-page", False),
20+
("Download-URL", False),
21+
("Author", False),
22+
("Author-email", False),
23+
("Maintainer", False),
24+
("Maintainer-email", False),
25+
("License", False),
26+
("Classifier", True),
27+
("Requires-Dist", True),
28+
("Requires-Python", False),
29+
("Requires-External", True),
30+
("Project-URL", True),
31+
("Provides-Extra", True),
32+
("Provides-Dist", True),
33+
("Obsoletes-Dist", True),
34+
]
35+
36+
37+
def json_name(field: str) -> str:
38+
return field.lower().replace("-", "_")
39+
40+
41+
def msg_to_json(msg: Message) -> Dict[str, Any]:
42+
def sanitise_header(h: Union[Header, str]) -> str:
43+
if isinstance(h, Header):
44+
chunks = []
45+
for bytes, encoding in decode_header(h):
46+
if encoding == "unknown-8bit":
47+
try:
48+
# See if UTF-8 works
49+
bytes.decode("utf-8")
50+
encoding = "utf-8"
51+
except UnicodeDecodeError:
52+
# If not, latin1 at least won't fail
53+
encoding = "latin1"
54+
chunks.append((bytes, encoding))
55+
return str(make_header(chunks))
56+
return str(h)
57+
58+
result = {}
59+
for field, multi in METADATA_FIELDS:
60+
if field not in msg:
61+
continue
62+
key = json_name(field)
63+
if multi:
64+
value: Union[str, List[str]] = [
65+
sanitise_header(v) for v in msg.get_all(field)
66+
]
67+
else:
68+
value = sanitise_header(msg.get(field))
69+
if key == "keywords":
70+
if "," in value:
71+
value = [v.strip() for v in value.split(",")]
72+
else:
73+
value = value.split()
74+
result[key] = value
75+
76+
payload = msg.get_payload()
77+
if payload:
78+
result["description"] = payload
79+
80+
return result

tests/unit/metadata/test_metadata.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import logging
2+
from pathlib import Path
23
from typing import cast
34
from unittest import mock
45

56
import pytest
67
from pip._vendor.packaging.utils import NormalizedName
78

8-
from pip._internal.metadata import BaseDistribution
9+
from pip._internal.metadata import BaseDistribution, get_wheel_distribution
10+
from pip._internal.metadata.base import FilesystemWheel
911
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo
12+
from tests.lib.wheel import make_wheel
1013

1114

1215
@mock.patch.object(BaseDistribution, "read_text", side_effect=FileNotFoundError)
@@ -55,3 +58,17 @@ class FakeDistribution(BaseDistribution):
5558
mock_read_text.assert_called_once_with(DIRECT_URL_METADATA_NAME)
5659
assert direct_url.url == "https://e.c/p.tgz"
5760
assert isinstance(direct_url.info, ArchiveInfo)
61+
62+
63+
def test_json_metadata(tmpdir: Path) -> None:
64+
"""Basic test of BaseDistribution json_metadata.
65+
66+
More tests are available in the original pkg_metadata project where this
67+
function comes from, and which we may vendor in the future.
68+
"""
69+
wheel_path = make_wheel(name="pkga", version="1.0.1").save_to_dir(tmpdir)
70+
wheel = FilesystemWheel(wheel_path)
71+
dist = get_wheel_distribution(wheel, "pkga")
72+
json_metadata = dist.json_metadata
73+
assert json_metadata["name"] == "pkga"
74+
assert json_metadata["version"] == "1.0.1"

0 commit comments

Comments
 (0)