Skip to content

Commit 6eef087

Browse files
committed
Implement JSON as an adapter on the PackageMetadata object, simplifying the experience for the consumer.
1 parent 51568a1 commit 6eef087

File tree

4 files changed

+97
-68
lines changed

4 files changed

+97
-68
lines changed

importlib_metadata/__init__.py

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66
import zipp
77
import email
8-
import string
98
import pathlib
109
import operator
1110
import textwrap
@@ -16,10 +15,10 @@
1615
import contextlib
1716
import collections
1817

18+
from . import _adapters, _meta
1919
from ._collections import FreezableDefaultDict, Pair
2020
from ._compat import (
2121
NullFinder,
22-
Protocol,
2322
PyPy_repr,
2423
install,
2524
)
@@ -30,7 +29,7 @@
3029
from importlib import import_module
3130
from importlib.abc import MetaPathFinder
3231
from itertools import starmap
33-
from typing import Any, List, Mapping, Optional, TypeVar, Union
32+
from typing import List, Mapping, Optional, Union
3433

3534

3635
__all__ = [
@@ -394,25 +393,6 @@ def __repr__(self):
394393
return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
395394

396395

397-
_T = TypeVar("_T")
398-
399-
400-
class PackageMetadata(Protocol):
401-
def __len__(self) -> int:
402-
... # pragma: no cover
403-
404-
def __contains__(self, item: str) -> bool:
405-
... # pragma: no cover
406-
407-
def __getitem__(self, key: str) -> str:
408-
... # pragma: no cover
409-
410-
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
411-
"""
412-
Return all values associated with a possibly multi-valued key.
413-
"""
414-
415-
416396
class Distribution:
417397
"""A Python distribution package."""
418398

@@ -497,7 +477,7 @@ def _local(cls, root='.'):
497477
return PathDistribution(zipp.Path(meta.build_as_zip(builder)))
498478

499479
@property
500-
def metadata(self) -> PackageMetadata:
480+
def metadata(self) -> _meta.PackageMetadata:
501481
"""Return the parsed metadata for this Distribution.
502482
503483
The returned object will have keys that name the various bits of
@@ -511,7 +491,7 @@ def metadata(self) -> PackageMetadata:
511491
# (which points to the egg-info file) attribute unchanged.
512492
or self.read_text('')
513493
)
514-
return email.message_from_string(text)
494+
return _adapters.JSONMeta(email.message_from_string(text))
515495

516496
@property
517497
def name(self):
@@ -843,7 +823,7 @@ def distributions(**kwargs):
843823
return Distribution.discover(**kwargs)
844824

845825

846-
def metadata(distribution_name) -> PackageMetadata:
826+
def metadata(distribution_name) -> _meta.PackageMetadata:
847827
"""Get the metadata for the named package.
848828
849829
:param distribution_name: The name of the distribution package to query.
@@ -920,43 +900,3 @@ def packages_distributions() -> Mapping[str, List[str]]:
920900
for pkg in (dist.read_text('top_level.txt') or '').split():
921901
pkg_to_dist[pkg].append(dist.metadata['Name'])
922902
return dict(pkg_to_dist)
923-
924-
925-
def as_json(metadata: PackageMetadata):
926-
"""
927-
Convert PackageMetadata to a JSON-compatible format
928-
per PEP 0566.
929-
"""
930-
# TODO: Need to match case-insensitive
931-
multiple_use = {
932-
'Classifier',
933-
'Obsoletes-Dist',
934-
'Platform',
935-
'Project-URL',
936-
'Provides-Dist',
937-
'Provides-Extra',
938-
'Requires-Dist',
939-
'Requires-External',
940-
'Supported-Platform',
941-
}
942-
943-
def redent(value):
944-
"Correct for RFC822 indentation"
945-
if not value or '\n' not in value:
946-
return value
947-
return textwrap.dedent(' ' * 8 + value)
948-
949-
def transform(key):
950-
value = (
951-
metadata.get_all(key) if key in multiple_use else redent(metadata.get(key))
952-
)
953-
if key == 'Keywords':
954-
value = value.split(string.whitespace)
955-
if not value and key == 'Description':
956-
value = metadata.get_payload()
957-
tk = key.lower().replace('-', '_')
958-
return tk, value
959-
960-
desc = ['Description'] if metadata.get_payload() else [] # type: ignore
961-
keys = itertools.chain(metadata, desc) # type: ignore
962-
return dict(map(transform, keys)) # type: ignore

importlib_metadata/_adapters.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import string
2+
import textwrap
3+
import itertools
4+
5+
from . import _meta
6+
7+
8+
class JSONMeta(_meta.PackageMetadata):
9+
def __init__(self, orig: _meta.PackageMetadata):
10+
self.orig = orig
11+
12+
def __getitem__(self, item):
13+
return self.orig.__getitem__(item)
14+
15+
def __len__(self):
16+
return self.orig.__len__() # pragma: nocover
17+
18+
def __contains__(self, item):
19+
return self.orig.__contains__(item) # pragma: nocover
20+
21+
def __iter__(self):
22+
return self.orig.__iter__()
23+
24+
def get_all(self, name):
25+
return self.orig.get_all(name)
26+
27+
def get_payload(self):
28+
return self.orig.get_payload()
29+
30+
@property
31+
def json(self):
32+
"""
33+
Convert PackageMetadata to a JSON-compatible format
34+
per PEP 0566.
35+
"""
36+
# TODO: Need to match case-insensitive
37+
multiple_use = {
38+
'Classifier',
39+
'Obsoletes-Dist',
40+
'Platform',
41+
'Project-URL',
42+
'Provides-Dist',
43+
'Provides-Extra',
44+
'Requires-Dist',
45+
'Requires-External',
46+
'Supported-Platform',
47+
}
48+
49+
def redent(value):
50+
"Correct for RFC822 indentation"
51+
if not value or '\n' not in value:
52+
return value
53+
return textwrap.dedent(' ' * 8 + value)
54+
55+
def transform(key):
56+
value = self.get_all(key) if key in multiple_use else redent(self[key])
57+
if key == 'Keywords':
58+
value = value.split(string.whitespace)
59+
if not value and key == 'Description':
60+
value = self.get_payload()
61+
tk = key.lower().replace('-', '_')
62+
return tk, value
63+
64+
desc = ['Description'] if self.get_payload() else []
65+
keys = itertools.chain(self, desc)
66+
return dict(map(transform, keys))

importlib_metadata/_meta.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from ._compat import Protocol
2+
from typing import Any, List, TypeVar, Union
3+
4+
5+
_T = TypeVar("_T")
6+
7+
8+
class PackageMetadata(Protocol):
9+
def __len__(self) -> int:
10+
... # pragma: no cover
11+
12+
def __contains__(self, item: str) -> bool:
13+
... # pragma: no cover
14+
15+
def __getitem__(self, key: str) -> str:
16+
... # pragma: no cover
17+
18+
def get_payload(self) -> str:
19+
... # pragma: no cover
20+
21+
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
22+
"""
23+
Return all values associated with a possibly multi-valued key.
24+
"""

tests/test_api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from importlib_metadata import (
99
Distribution,
1010
PackageNotFoundError,
11-
as_json,
1211
distribution,
1312
entry_points,
1413
files,
@@ -248,13 +247,13 @@ def test_more_complex_deps_requires_text(self):
248247
assert deps == expected
249248

250249
def test_as_json(self):
251-
md = as_json(metadata('distinfo-pkg'))
250+
md = metadata('distinfo-pkg').json
252251
assert 'name' in md
253252
desc = md['description']
254253
assert desc.startswith('Once upon a time\nThere was')
255254

256255
def test_as_json_egg_info(self):
257-
md = as_json(metadata('egginfo-pkg'))
256+
md = metadata('egginfo-pkg').json
258257
assert 'name' in md
259258
desc = md['description']
260259
assert desc.startswith('Once upon a time\nThere was')

0 commit comments

Comments
 (0)