summaryrefslogtreecommitdiffstats
path: root/Lib/importlib
diff options
context:
space:
mode:
authorJason R. Coombs <jaraco@jaraco.com>2021-05-02 21:03:40 (GMT)
committerGitHub <noreply@github.com>2021-05-02 21:03:40 (GMT)
commit37e0c7850de902179b28f1378fbbc38a5ed3628c (patch)
treeecc352d5d7eaf99485bc4c2735d2a5f14f532084 /Lib/importlib
parent0ad1e0384c8afc5259a6d03363491d89500a5d03 (diff)
downloadcpython-37e0c7850de902179b28f1378fbbc38a5ed3628c.zip
cpython-37e0c7850de902179b28f1378fbbc38a5ed3628c.tar.gz
cpython-37e0c7850de902179b28f1378fbbc38a5ed3628c.tar.bz2
bpo-43926: Cleaner metadata with PEP 566 JSON support. (GH-25565)
* bpo-43926: Cleaner metadata with PEP 566 JSON support. * Add blurb * Add versionchanged and versionadded declarations for changes to metadata. * Use descriptor for PEP 566
Diffstat (limited to 'Lib/importlib')
-rw-r--r--Lib/importlib/metadata/__init__.py (renamed from Lib/importlib/metadata.py)28
-rw-r--r--Lib/importlib/metadata/_adapters.py67
-rw-r--r--Lib/importlib/metadata/_collections.py (renamed from Lib/importlib/_collections.py)0
-rw-r--r--Lib/importlib/metadata/_functools.py (renamed from Lib/importlib/_functools.py)0
-rw-r--r--Lib/importlib/metadata/_itertools.py (renamed from Lib/importlib/_itertools.py)0
-rw-r--r--Lib/importlib/metadata/_meta.py29
-rw-r--r--Lib/importlib/metadata/_text.py99
7 files changed, 200 insertions, 23 deletions
diff --git a/Lib/importlib/metadata.py b/Lib/importlib/metadata/__init__.py
index 7a427eb..1421621 100644
--- a/Lib/importlib/metadata.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -14,6 +14,7 @@ import itertools
import posixpath
import collections
+from . import _adapters, _meta
from ._collections import FreezableDefaultDict, Pair
from ._functools import method_cache
from ._itertools import unique_everseen
@@ -22,7 +23,7 @@ from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
-from typing import Any, List, Mapping, Optional, Protocol, TypeVar, Union
+from typing import List, Mapping, Optional, Union
__all__ = [
@@ -385,25 +386,6 @@ class FileHash:
return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
-_T = TypeVar("_T")
-
-
-class PackageMetadata(Protocol):
- def __len__(self) -> int:
- ... # pragma: no cover
-
- def __contains__(self, item: str) -> bool:
- ... # pragma: no cover
-
- def __getitem__(self, key: str) -> str:
- ... # pragma: no cover
-
- def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
- """
- Return all values associated with a possibly multi-valued key.
- """
-
-
class Distribution:
"""A Python distribution package."""
@@ -488,7 +470,7 @@ class Distribution:
return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
@property
- def metadata(self) -> PackageMetadata:
+ def metadata(self) -> _meta.PackageMetadata:
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
@@ -502,7 +484,7 @@ class Distribution:
# (which points to the egg-info file) attribute unchanged.
or self.read_text('')
)
- return email.message_from_string(text)
+ return _adapters.Message(email.message_from_string(text))
@property
def name(self):
@@ -829,7 +811,7 @@ def distributions(**kwargs):
return Distribution.discover(**kwargs)
-def metadata(distribution_name) -> PackageMetadata:
+def metadata(distribution_name) -> _meta.PackageMetadata:
"""Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query.
diff --git a/Lib/importlib/metadata/_adapters.py b/Lib/importlib/metadata/_adapters.py
new file mode 100644
index 0000000..ab08618
--- /dev/null
+++ b/Lib/importlib/metadata/_adapters.py
@@ -0,0 +1,67 @@
+import re
+import textwrap
+import email.message
+
+from ._text import FoldedCase
+
+
+class Message(email.message.Message):
+ multiple_use_keys = set(
+ map(
+ FoldedCase,
+ [
+ 'Classifier',
+ 'Obsoletes-Dist',
+ 'Platform',
+ 'Project-URL',
+ 'Provides-Dist',
+ 'Provides-Extra',
+ 'Requires-Dist',
+ 'Requires-External',
+ 'Supported-Platform',
+ ],
+ )
+ )
+ """
+ Keys that may be indicated multiple times per PEP 566.
+ """
+
+ def __new__(cls, orig: email.message.Message):
+ res = super().__new__(cls)
+ vars(res).update(vars(orig))
+ return res
+
+ def __init__(self, *args, **kwargs):
+ self._headers = self._repair_headers()
+
+ # suppress spurious error from mypy
+ def __iter__(self):
+ return super().__iter__()
+
+ def _repair_headers(self):
+ def redent(value):
+ "Correct for RFC822 indentation"
+ if not value or '\n' not in value:
+ return value
+ return textwrap.dedent(' ' * 8 + value)
+
+ headers = [(key, redent(value)) for key, value in vars(self)['_headers']]
+ if self._payload:
+ headers.append(('Description', self.get_payload()))
+ return headers
+
+ @property
+ def json(self):
+ """
+ Convert PackageMetadata to a JSON-compatible format
+ per PEP 0566.
+ """
+
+ def transform(key):
+ value = self.get_all(key) if key in self.multiple_use_keys else self[key]
+ if key == 'Keywords':
+ value = re.split(r'\s+', value)
+ tk = key.lower().replace('-', '_')
+ return tk, value
+
+ return dict(map(transform, map(FoldedCase, self)))
diff --git a/Lib/importlib/_collections.py b/Lib/importlib/metadata/_collections.py
index cf0954e..cf0954e 100644
--- a/Lib/importlib/_collections.py
+++ b/Lib/importlib/metadata/_collections.py
diff --git a/Lib/importlib/_functools.py b/Lib/importlib/metadata/_functools.py
index 73f50d0..73f50d0 100644
--- a/Lib/importlib/_functools.py
+++ b/Lib/importlib/metadata/_functools.py
diff --git a/Lib/importlib/_itertools.py b/Lib/importlib/metadata/_itertools.py
index dd45f2f..dd45f2f 100644
--- a/Lib/importlib/_itertools.py
+++ b/Lib/importlib/metadata/_itertools.py
diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py
new file mode 100644
index 0000000..04d9a02
--- /dev/null
+++ b/Lib/importlib/metadata/_meta.py
@@ -0,0 +1,29 @@
+from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union
+
+
+_T = TypeVar("_T")
+
+
+class PackageMetadata(Protocol):
+ def __len__(self) -> int:
+ ... # pragma: no cover
+
+ def __contains__(self, item: str) -> bool:
+ ... # pragma: no cover
+
+ def __getitem__(self, key: str) -> str:
+ ... # pragma: no cover
+
+ def __iter__(self) -> Iterator[str]:
+ ... # pragma: no cover
+
+ def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
+ """
+ Return all values associated with a possibly multi-valued key.
+ """
+
+ @property
+ def json(self) -> Dict[str, Union[str, List[str]]]:
+ """
+ A JSON-compatible form of the metadata.
+ """
diff --git a/Lib/importlib/metadata/_text.py b/Lib/importlib/metadata/_text.py
new file mode 100644
index 0000000..766979d
--- /dev/null
+++ b/Lib/importlib/metadata/_text.py
@@ -0,0 +1,99 @@
+import re
+
+from ._functools import method_cache
+
+
+# from jaraco.text 3.5
+class FoldedCase(str):
+ """
+ A case insensitive string class; behaves just like str
+ except compares equal when the only variation is case.
+
+ >>> s = FoldedCase('hello world')
+
+ >>> s == 'Hello World'
+ True
+
+ >>> 'Hello World' == s
+ True
+
+ >>> s != 'Hello World'
+ False
+
+ >>> s.index('O')
+ 4
+
+ >>> s.split('O')
+ ['hell', ' w', 'rld']
+
+ >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta']))
+ ['alpha', 'Beta', 'GAMMA']
+
+ Sequence membership is straightforward.
+
+ >>> "Hello World" in [s]
+ True
+ >>> s in ["Hello World"]
+ True
+
+ You may test for set inclusion, but candidate and elements
+ must both be folded.
+
+ >>> FoldedCase("Hello World") in {s}
+ True
+ >>> s in {FoldedCase("Hello World")}
+ True
+
+ String inclusion works as long as the FoldedCase object
+ is on the right.
+
+ >>> "hello" in FoldedCase("Hello World")
+ True
+
+ But not if the FoldedCase object is on the left:
+
+ >>> FoldedCase('hello') in 'Hello World'
+ False
+
+ In that case, use in_:
+
+ >>> FoldedCase('hello').in_('Hello World')
+ True
+
+ >>> FoldedCase('hello') > FoldedCase('Hello')
+ False
+ """
+
+ def __lt__(self, other):
+ return self.lower() < other.lower()
+
+ def __gt__(self, other):
+ return self.lower() > other.lower()
+
+ def __eq__(self, other):
+ return self.lower() == other.lower()
+
+ def __ne__(self, other):
+ return self.lower() != other.lower()
+
+ def __hash__(self):
+ return hash(self.lower())
+
+ def __contains__(self, other):
+ return super(FoldedCase, self).lower().__contains__(other.lower())
+
+ def in_(self, other):
+ "Does self appear in other?"
+ return self in FoldedCase(other)
+
+ # cache lower since it's likely to be called frequently.
+ @method_cache
+ def lower(self):
+ return super(FoldedCase, self).lower()
+
+ def index(self, sub):
+ return self.lower().index(sub.lower())
+
+ def split(self, splitter=' ', maxsplit=0):
+ pattern = re.compile(re.escape(splitter), re.I)
+ return pattern.split(self, maxsplit)