bpo-42382: In importlib.metadata, `EntryPoint` objects now expose `dist` (#23758)
* bpo-42382: In importlib.metadata, `EntryPoint` objects now expose a `.dist` object referencing the `Distribution` when constructed from a `Distribution`. Also, sync importlib_metadata 3.3: - Add support for package discovery under package normalization rules. - The object returned by `metadata()` now has a formally-defined protocol called `PackageMetadata` with declared support for the `.get_all()` method. * Add blurb * Remove latent footnote.
This commit is contained in:
parent
f4936ad1c4
commit
dfdca85dfa
|
@ -115,8 +115,9 @@ Every distribution includes some metadata, which you can extract using the
|
||||||
|
|
||||||
>>> wheel_metadata = metadata('wheel') # doctest: +SKIP
|
>>> wheel_metadata = metadata('wheel') # doctest: +SKIP
|
||||||
|
|
||||||
The keys of the returned data structure [#f1]_ name the metadata keywords, and
|
The keys of the returned data structure, a ``PackageMetadata``,
|
||||||
their values are returned unparsed from the distribution metadata::
|
name the metadata keywords, and
|
||||||
|
the values are returned unparsed from the distribution metadata::
|
||||||
|
|
||||||
>>> wheel_metadata['Requires-Python'] # doctest: +SKIP
|
>>> wheel_metadata['Requires-Python'] # doctest: +SKIP
|
||||||
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
|
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
|
||||||
|
@ -259,9 +260,3 @@ a custom finder, return instances of this derived ``Distribution`` in the
|
||||||
|
|
||||||
|
|
||||||
.. rubric:: Footnotes
|
.. rubric:: Footnotes
|
||||||
|
|
||||||
.. [#f1] Technically, the returned distribution metadata object is an
|
|
||||||
:class:`email.message.EmailMessage`
|
|
||||||
instance, but this is an implementation detail, and not part of the
|
|
||||||
stable API. You should only use dictionary-like methods and syntax
|
|
||||||
to access the metadata contents.
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import abc
|
import abc
|
||||||
|
@ -18,6 +17,7 @@ from contextlib import suppress
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from importlib.abc import MetaPathFinder
|
from importlib.abc import MetaPathFinder
|
||||||
from itertools import starmap
|
from itertools import starmap
|
||||||
|
from typing import Any, List, Optional, Protocol, TypeVar, Union
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -31,7 +31,7 @@ __all__ = [
|
||||||
'metadata',
|
'metadata',
|
||||||
'requires',
|
'requires',
|
||||||
'version',
|
'version',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PackageNotFoundError(ModuleNotFoundError):
|
class PackageNotFoundError(ModuleNotFoundError):
|
||||||
|
@ -43,7 +43,7 @@ class PackageNotFoundError(ModuleNotFoundError):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
name, = self.args
|
(name,) = self.args
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class EntryPoint(
|
||||||
r'(?P<module>[\w.]+)\s*'
|
r'(?P<module>[\w.]+)\s*'
|
||||||
r'(:\s*(?P<attr>[\w.]+))?\s*'
|
r'(:\s*(?P<attr>[\w.]+))?\s*'
|
||||||
r'(?P<extras>\[.*\])?\s*$'
|
r'(?P<extras>\[.*\])?\s*$'
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
A regular expression describing the syntax for an entry point,
|
A regular expression describing the syntax for an entry point,
|
||||||
which might look like:
|
which might look like:
|
||||||
|
@ -77,6 +77,8 @@ class EntryPoint(
|
||||||
following the attr, and following any extras.
|
following the attr, and following any extras.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
dist: Optional['Distribution'] = None
|
||||||
|
|
||||||
def load(self):
|
def load(self):
|
||||||
"""Load the entry point from its definition. If only a module
|
"""Load the entry point from its definition. If only a module
|
||||||
is indicated by the value, return that module. Otherwise,
|
is indicated by the value, return that module. Otherwise,
|
||||||
|
@ -104,23 +106,27 @@ class EntryPoint(
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_config(cls, config):
|
def _from_config(cls, config):
|
||||||
return [
|
return (
|
||||||
cls(name, value, group)
|
cls(name, value, group)
|
||||||
for group in config.sections()
|
for group in config.sections()
|
||||||
for name, value in config.items(group)
|
for name, value in config.items(group)
|
||||||
]
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_text(cls, text):
|
def _from_text(cls, text):
|
||||||
config = ConfigParser(delimiters='=')
|
config = ConfigParser(delimiters='=')
|
||||||
# case sensitive: https://stackoverflow.com/q/1611799/812183
|
# case sensitive: https://stackoverflow.com/q/1611799/812183
|
||||||
config.optionxform = str
|
config.optionxform = str
|
||||||
try:
|
config.read_string(text)
|
||||||
config.read_string(text)
|
return cls._from_config(config)
|
||||||
except AttributeError: # pragma: nocover
|
|
||||||
# Python 2 has no read_string
|
@classmethod
|
||||||
config.readfp(io.StringIO(text))
|
def _from_text_for(cls, text, dist):
|
||||||
return EntryPoint._from_config(config)
|
return (ep._for(dist) for ep in cls._from_text(text))
|
||||||
|
|
||||||
|
def _for(self, dist):
|
||||||
|
self.dist = dist
|
||||||
|
return self
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
|
@ -132,7 +138,7 @@ class EntryPoint(
|
||||||
return (
|
return (
|
||||||
self.__class__,
|
self.__class__,
|
||||||
(self.name, self.value, self.group),
|
(self.name, self.value, self.group),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PackagePath(pathlib.PurePosixPath):
|
class PackagePath(pathlib.PurePosixPath):
|
||||||
|
@ -159,6 +165,25 @@ class FileHash:
|
||||||
return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
|
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:
|
class Distribution:
|
||||||
"""A Python distribution package."""
|
"""A Python distribution package."""
|
||||||
|
|
||||||
|
@ -210,9 +235,8 @@ class Distribution:
|
||||||
raise ValueError("cannot accept context and kwargs")
|
raise ValueError("cannot accept context and kwargs")
|
||||||
context = context or DistributionFinder.Context(**kwargs)
|
context = context or DistributionFinder.Context(**kwargs)
|
||||||
return itertools.chain.from_iterable(
|
return itertools.chain.from_iterable(
|
||||||
resolver(context)
|
resolver(context) for resolver in cls._discover_resolvers()
|
||||||
for resolver in cls._discover_resolvers()
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def at(path):
|
def at(path):
|
||||||
|
@ -227,24 +251,24 @@ class Distribution:
|
||||||
def _discover_resolvers():
|
def _discover_resolvers():
|
||||||
"""Search the meta_path for resolvers."""
|
"""Search the meta_path for resolvers."""
|
||||||
declared = (
|
declared = (
|
||||||
getattr(finder, 'find_distributions', None)
|
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
|
||||||
for finder in sys.meta_path
|
)
|
||||||
)
|
|
||||||
return filter(None, declared)
|
return filter(None, declared)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _local(cls, root='.'):
|
def _local(cls, root='.'):
|
||||||
from pep517 import build, meta
|
from pep517 import build, meta
|
||||||
|
|
||||||
system = build.compat_system(root)
|
system = build.compat_system(root)
|
||||||
builder = functools.partial(
|
builder = functools.partial(
|
||||||
meta.build,
|
meta.build,
|
||||||
source_dir=root,
|
source_dir=root,
|
||||||
system=system,
|
system=system,
|
||||||
)
|
)
|
||||||
return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
|
return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self) -> PackageMetadata:
|
||||||
"""Return the parsed metadata for this Distribution.
|
"""Return the parsed metadata for this Distribution.
|
||||||
|
|
||||||
The returned object will have keys that name the various bits of
|
The returned object will have keys that name the various bits of
|
||||||
|
@ -257,9 +281,14 @@ class Distribution:
|
||||||
# effect is to just end up using the PathDistribution's self._path
|
# effect is to just end up using the PathDistribution's self._path
|
||||||
# (which points to the egg-info file) attribute unchanged.
|
# (which points to the egg-info file) attribute unchanged.
|
||||||
or self.read_text('')
|
or self.read_text('')
|
||||||
)
|
)
|
||||||
return email.message_from_string(text)
|
return email.message_from_string(text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the 'Name' metadata for the distribution package."""
|
||||||
|
return self.metadata['Name']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
"""Return the 'Version' metadata for the distribution package."""
|
"""Return the 'Version' metadata for the distribution package."""
|
||||||
|
@ -267,7 +296,7 @@ class Distribution:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entry_points(self):
|
def entry_points(self):
|
||||||
return EntryPoint._from_text(self.read_text('entry_points.txt'))
|
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def files(self):
|
def files(self):
|
||||||
|
@ -324,9 +353,10 @@ class Distribution:
|
||||||
section_pairs = cls._read_sections(source.splitlines())
|
section_pairs = cls._read_sections(source.splitlines())
|
||||||
sections = {
|
sections = {
|
||||||
section: list(map(operator.itemgetter('line'), results))
|
section: list(map(operator.itemgetter('line'), results))
|
||||||
for section, results in
|
for section, results in itertools.groupby(
|
||||||
itertools.groupby(section_pairs, operator.itemgetter('section'))
|
section_pairs, operator.itemgetter('section')
|
||||||
}
|
)
|
||||||
|
}
|
||||||
return cls._convert_egg_info_reqs_to_simple_reqs(sections)
|
return cls._convert_egg_info_reqs_to_simple_reqs(sections)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -350,6 +380,7 @@ class Distribution:
|
||||||
requirement. This method converts the former to the
|
requirement. This method converts the former to the
|
||||||
latter. See _test_deps_from_requires_text for an example.
|
latter. See _test_deps_from_requires_text for an example.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def make_condition(name):
|
def make_condition(name):
|
||||||
return name and 'extra == "{name}"'.format(name=name)
|
return name and 'extra == "{name}"'.format(name=name)
|
||||||
|
|
||||||
|
@ -438,48 +469,69 @@ class FastPath:
|
||||||
names = zip_path.root.namelist()
|
names = zip_path.root.namelist()
|
||||||
self.joinpath = zip_path.joinpath
|
self.joinpath = zip_path.joinpath
|
||||||
|
|
||||||
return dict.fromkeys(
|
return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
|
||||||
child.split(posixpath.sep, 1)[0]
|
|
||||||
for child in names
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_egg(self, search):
|
|
||||||
base = self.base
|
|
||||||
return (
|
|
||||||
base == search.versionless_egg_name
|
|
||||||
or base.startswith(search.prefix)
|
|
||||||
and base.endswith('.egg'))
|
|
||||||
|
|
||||||
def search(self, name):
|
def search(self, name):
|
||||||
for child in self.children():
|
return (
|
||||||
n_low = child.lower()
|
self.joinpath(child)
|
||||||
if (n_low in name.exact_matches
|
for child in self.children()
|
||||||
or n_low.startswith(name.prefix)
|
if name.matches(child, self.base)
|
||||||
and n_low.endswith(name.suffixes)
|
)
|
||||||
# legacy case:
|
|
||||||
or self.is_egg(name) and n_low == 'egg-info'):
|
|
||||||
yield self.joinpath(child)
|
|
||||||
|
|
||||||
|
|
||||||
class Prepared:
|
class Prepared:
|
||||||
"""
|
"""
|
||||||
A prepared search for metadata on a possibly-named package.
|
A prepared search for metadata on a possibly-named package.
|
||||||
"""
|
"""
|
||||||
normalized = ''
|
|
||||||
prefix = ''
|
normalized = None
|
||||||
suffixes = '.dist-info', '.egg-info'
|
suffixes = '.dist-info', '.egg-info'
|
||||||
exact_matches = [''][:0]
|
exact_matches = [''][:0]
|
||||||
versionless_egg_name = ''
|
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
if name is None:
|
if name is None:
|
||||||
return
|
return
|
||||||
self.normalized = name.lower().replace('-', '_')
|
self.normalized = self.normalize(name)
|
||||||
self.prefix = self.normalized + '-'
|
self.exact_matches = [self.normalized + suffix for suffix in self.suffixes]
|
||||||
self.exact_matches = [
|
|
||||||
self.normalized + suffix for suffix in self.suffixes]
|
@staticmethod
|
||||||
self.versionless_egg_name = self.normalized + '.egg'
|
def normalize(name):
|
||||||
|
"""
|
||||||
|
PEP 503 normalization plus dashes as underscores.
|
||||||
|
"""
|
||||||
|
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def legacy_normalize(name):
|
||||||
|
"""
|
||||||
|
Normalize the package name as found in the convention in
|
||||||
|
older packaging tools versions and specs.
|
||||||
|
"""
|
||||||
|
return name.lower().replace('-', '_')
|
||||||
|
|
||||||
|
def matches(self, cand, base):
|
||||||
|
low = cand.lower()
|
||||||
|
pre, ext = os.path.splitext(low)
|
||||||
|
name, sep, rest = pre.partition('-')
|
||||||
|
return (
|
||||||
|
low in self.exact_matches
|
||||||
|
or ext in self.suffixes
|
||||||
|
and (not self.normalized or name.replace('.', '_') == self.normalized)
|
||||||
|
# legacy case:
|
||||||
|
or self.is_egg(base)
|
||||||
|
and low == 'egg-info'
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_egg(self, base):
|
||||||
|
normalized = self.legacy_normalize(self.name or '')
|
||||||
|
prefix = normalized + '-' if normalized else ''
|
||||||
|
versionless_egg_name = normalized + '.egg' if self.name else ''
|
||||||
|
return (
|
||||||
|
base == versionless_egg_name
|
||||||
|
or base.startswith(prefix)
|
||||||
|
and base.endswith('.egg')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MetadataPathFinder(DistributionFinder):
|
class MetadataPathFinder(DistributionFinder):
|
||||||
|
@ -500,9 +552,8 @@ class MetadataPathFinder(DistributionFinder):
|
||||||
def _search_paths(cls, name, paths):
|
def _search_paths(cls, name, paths):
|
||||||
"""Find metadata directories in paths heuristically."""
|
"""Find metadata directories in paths heuristically."""
|
||||||
return itertools.chain.from_iterable(
|
return itertools.chain.from_iterable(
|
||||||
path.search(Prepared(name))
|
path.search(Prepared(name)) for path in map(FastPath, paths)
|
||||||
for path in map(FastPath, paths)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PathDistribution(Distribution):
|
class PathDistribution(Distribution):
|
||||||
|
@ -515,9 +566,15 @@ class PathDistribution(Distribution):
|
||||||
self._path = path
|
self._path = path
|
||||||
|
|
||||||
def read_text(self, filename):
|
def read_text(self, filename):
|
||||||
with suppress(FileNotFoundError, IsADirectoryError, KeyError,
|
with suppress(
|
||||||
NotADirectoryError, PermissionError):
|
FileNotFoundError,
|
||||||
|
IsADirectoryError,
|
||||||
|
KeyError,
|
||||||
|
NotADirectoryError,
|
||||||
|
PermissionError,
|
||||||
|
):
|
||||||
return self._path.joinpath(filename).read_text(encoding='utf-8')
|
return self._path.joinpath(filename).read_text(encoding='utf-8')
|
||||||
|
|
||||||
read_text.__doc__ = Distribution.read_text.__doc__
|
read_text.__doc__ = Distribution.read_text.__doc__
|
||||||
|
|
||||||
def locate_file(self, path):
|
def locate_file(self, path):
|
||||||
|
@ -541,11 +598,11 @@ def distributions(**kwargs):
|
||||||
return Distribution.discover(**kwargs)
|
return Distribution.discover(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def metadata(distribution_name):
|
def metadata(distribution_name) -> PackageMetadata:
|
||||||
"""Get the metadata for the named package.
|
"""Get the metadata for the named package.
|
||||||
|
|
||||||
:param distribution_name: The name of the distribution package to query.
|
:param distribution_name: The name of the distribution package to query.
|
||||||
:return: An email.Message containing the parsed metadata.
|
:return: A PackageMetadata containing the parsed metadata.
|
||||||
"""
|
"""
|
||||||
return Distribution.from_name(distribution_name).metadata
|
return Distribution.from_name(distribution_name).metadata
|
||||||
|
|
||||||
|
@ -565,15 +622,11 @@ def entry_points():
|
||||||
|
|
||||||
:return: EntryPoint objects for all installed packages.
|
:return: EntryPoint objects for all installed packages.
|
||||||
"""
|
"""
|
||||||
eps = itertools.chain.from_iterable(
|
eps = itertools.chain.from_iterable(dist.entry_points for dist in distributions())
|
||||||
dist.entry_points for dist in distributions())
|
|
||||||
by_group = operator.attrgetter('group')
|
by_group = operator.attrgetter('group')
|
||||||
ordered = sorted(eps, key=by_group)
|
ordered = sorted(eps, key=by_group)
|
||||||
grouped = itertools.groupby(ordered, by_group)
|
grouped = itertools.groupby(ordered, by_group)
|
||||||
return {
|
return {group: tuple(eps) for group, eps in grouped}
|
||||||
group: tuple(eps)
|
|
||||||
for group, eps in grouped
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def files(distribution_name):
|
def files(distribution_name):
|
||||||
|
|
|
@ -7,6 +7,7 @@ import textwrap
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
from test.support.os_helper import FS_NONASCII
|
from test.support.os_helper import FS_NONASCII
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
|
@ -71,8 +72,13 @@ class OnSysPath(Fixtures):
|
||||||
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
|
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
|
||||||
|
|
||||||
|
|
||||||
|
# Except for python/mypy#731, prefer to define
|
||||||
|
# FilesDef = Dict[str, Union['FilesDef', str]]
|
||||||
|
FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]]
|
||||||
|
|
||||||
|
|
||||||
class DistInfoPkg(OnSysPath, SiteDir):
|
class DistInfoPkg(OnSysPath, SiteDir):
|
||||||
files = {
|
files: FilesDef = {
|
||||||
"distinfo_pkg-1.0.0.dist-info": {
|
"distinfo_pkg-1.0.0.dist-info": {
|
||||||
"METADATA": """
|
"METADATA": """
|
||||||
Name: distinfo-pkg
|
Name: distinfo-pkg
|
||||||
|
@ -86,19 +92,55 @@ class DistInfoPkg(OnSysPath, SiteDir):
|
||||||
[entries]
|
[entries]
|
||||||
main = mod:main
|
main = mod:main
|
||||||
ns:sub = mod:main
|
ns:sub = mod:main
|
||||||
"""
|
""",
|
||||||
},
|
},
|
||||||
"mod.py": """
|
"mod.py": """
|
||||||
def main():
|
def main():
|
||||||
print("hello world")
|
print("hello world")
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DistInfoPkg, self).setUp()
|
super(DistInfoPkg, self).setUp()
|
||||||
build_files(DistInfoPkg.files, self.site_dir)
|
build_files(DistInfoPkg.files, self.site_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class DistInfoPkgWithDot(OnSysPath, SiteDir):
|
||||||
|
files: FilesDef = {
|
||||||
|
"pkg_dot-1.0.0.dist-info": {
|
||||||
|
"METADATA": """
|
||||||
|
Name: pkg.dot
|
||||||
|
Version: 1.0.0
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DistInfoPkgWithDot, self).setUp()
|
||||||
|
build_files(DistInfoPkgWithDot.files, self.site_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
|
||||||
|
files: FilesDef = {
|
||||||
|
"pkg.dot-1.0.0.dist-info": {
|
||||||
|
"METADATA": """
|
||||||
|
Name: pkg.dot
|
||||||
|
Version: 1.0.0
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
"pkg.lot.egg-info": {
|
||||||
|
"METADATA": """
|
||||||
|
Name: pkg.lot
|
||||||
|
Version: 1.0.0
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DistInfoPkgWithDotLegacy, self).setUp()
|
||||||
|
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
|
||||||
|
|
||||||
|
|
||||||
class DistInfoPkgOffPath(SiteDir):
|
class DistInfoPkgOffPath(SiteDir):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DistInfoPkgOffPath, self).setUp()
|
super(DistInfoPkgOffPath, self).setUp()
|
||||||
|
@ -106,7 +148,7 @@ class DistInfoPkgOffPath(SiteDir):
|
||||||
|
|
||||||
|
|
||||||
class EggInfoPkg(OnSysPath, SiteDir):
|
class EggInfoPkg(OnSysPath, SiteDir):
|
||||||
files = {
|
files: FilesDef = {
|
||||||
"egginfo_pkg.egg-info": {
|
"egginfo_pkg.egg-info": {
|
||||||
"PKG-INFO": """
|
"PKG-INFO": """
|
||||||
Name: egginfo-pkg
|
Name: egginfo-pkg
|
||||||
|
@ -129,13 +171,13 @@ class EggInfoPkg(OnSysPath, SiteDir):
|
||||||
[test]
|
[test]
|
||||||
pytest
|
pytest
|
||||||
""",
|
""",
|
||||||
"top_level.txt": "mod\n"
|
"top_level.txt": "mod\n",
|
||||||
},
|
},
|
||||||
"mod.py": """
|
"mod.py": """
|
||||||
def main():
|
def main():
|
||||||
print("hello world")
|
print("hello world")
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(EggInfoPkg, self).setUp()
|
super(EggInfoPkg, self).setUp()
|
||||||
|
@ -143,7 +185,7 @@ class EggInfoPkg(OnSysPath, SiteDir):
|
||||||
|
|
||||||
|
|
||||||
class EggInfoFile(OnSysPath, SiteDir):
|
class EggInfoFile(OnSysPath, SiteDir):
|
||||||
files = {
|
files: FilesDef = {
|
||||||
"egginfo_file.egg-info": """
|
"egginfo_file.egg-info": """
|
||||||
Metadata-Version: 1.0
|
Metadata-Version: 1.0
|
||||||
Name: egginfo_file
|
Name: egginfo_file
|
||||||
|
@ -156,7 +198,7 @@ class EggInfoFile(OnSysPath, SiteDir):
|
||||||
Description: UNKNOWN
|
Description: UNKNOWN
|
||||||
Platform: UNKNOWN
|
Platform: UNKNOWN
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(EggInfoFile, self).setUp()
|
super(EggInfoFile, self).setUp()
|
||||||
|
@ -164,12 +206,12 @@ class EggInfoFile(OnSysPath, SiteDir):
|
||||||
|
|
||||||
|
|
||||||
class LocalPackage:
|
class LocalPackage:
|
||||||
files = {
|
files: FilesDef = {
|
||||||
"setup.py": """
|
"setup.py": """
|
||||||
import setuptools
|
import setuptools
|
||||||
setuptools.setup(name="local-pkg", version="2.0.1")
|
setuptools.setup(name="local-pkg", version="2.0.1")
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.fixtures = contextlib.ExitStack()
|
self.fixtures = contextlib.ExitStack()
|
||||||
|
@ -214,8 +256,7 @@ def build_files(file_defs, prefix=pathlib.Path()):
|
||||||
|
|
||||||
class FileBuilder:
|
class FileBuilder:
|
||||||
def unicode_filename(self):
|
def unicode_filename(self):
|
||||||
return FS_NONASCII or \
|
return FS_NONASCII or self.skip("File system does not support non-ascii.")
|
||||||
self.skip("File system does not support non-ascii.")
|
|
||||||
|
|
||||||
|
|
||||||
def DALS(str):
|
def DALS(str):
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import pickle
|
import pickle
|
||||||
|
@ -14,10 +12,14 @@ except ImportError:
|
||||||
|
|
||||||
from . import fixtures
|
from . import fixtures
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
Distribution, EntryPoint,
|
Distribution,
|
||||||
PackageNotFoundError, distributions,
|
EntryPoint,
|
||||||
entry_points, metadata, version,
|
PackageNotFoundError,
|
||||||
)
|
distributions,
|
||||||
|
entry_points,
|
||||||
|
metadata,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||||
|
@ -70,12 +72,11 @@ class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||||
name='ep',
|
name='ep',
|
||||||
value='importlib.metadata',
|
value='importlib.metadata',
|
||||||
group='grp',
|
group='grp',
|
||||||
)
|
)
|
||||||
assert ep.load() is importlib.metadata
|
assert ep.load() is importlib.metadata
|
||||||
|
|
||||||
|
|
||||||
class NameNormalizationTests(
|
class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||||
fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pkg_with_dashes(site_dir):
|
def pkg_with_dashes(site_dir):
|
||||||
"""
|
"""
|
||||||
|
@ -144,11 +145,15 @@ class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||||
metadata_dir.mkdir()
|
metadata_dir.mkdir()
|
||||||
metadata = metadata_dir / 'METADATA'
|
metadata = metadata_dir / 'METADATA'
|
||||||
with metadata.open('w', encoding='utf-8') as fp:
|
with metadata.open('w', encoding='utf-8') as fp:
|
||||||
fp.write(textwrap.dedent("""
|
fp.write(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
Name: portend
|
Name: portend
|
||||||
|
|
||||||
pôrˈtend
|
pôrˈtend
|
||||||
""").lstrip())
|
"""
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
return 'portend'
|
return 'portend'
|
||||||
|
|
||||||
def test_metadata_loads(self):
|
def test_metadata_loads(self):
|
||||||
|
@ -162,24 +167,12 @@ class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
|
||||||
assert meta.get_payload() == 'pôrˈtend\n'
|
assert meta.get_payload() == 'pôrˈtend\n'
|
||||||
|
|
||||||
|
|
||||||
class DiscoveryTests(fixtures.EggInfoPkg,
|
class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase):
|
||||||
fixtures.DistInfoPkg,
|
|
||||||
unittest.TestCase):
|
|
||||||
|
|
||||||
def test_package_discovery(self):
|
def test_package_discovery(self):
|
||||||
dists = list(distributions())
|
dists = list(distributions())
|
||||||
assert all(
|
assert all(isinstance(dist, Distribution) for dist in dists)
|
||||||
isinstance(dist, Distribution)
|
assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists)
|
||||||
for dist in dists
|
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
|
||||||
)
|
|
||||||
assert any(
|
|
||||||
dist.metadata['Name'] == 'egginfo-pkg'
|
|
||||||
for dist in dists
|
|
||||||
)
|
|
||||||
assert any(
|
|
||||||
dist.metadata['Name'] == 'distinfo-pkg'
|
|
||||||
for dist in dists
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_invalid_usage(self):
|
def test_invalid_usage(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
|
@ -265,10 +258,21 @@ class TestEntryPoints(unittest.TestCase):
|
||||||
def test_attr(self):
|
def test_attr(self):
|
||||||
assert self.ep.attr is None
|
assert self.ep.attr is None
|
||||||
|
|
||||||
|
def test_sortable(self):
|
||||||
|
"""
|
||||||
|
EntryPoint objects are sortable, but result is undefined.
|
||||||
|
"""
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
EntryPoint('b', 'val', 'group'),
|
||||||
|
EntryPoint('a', 'val', 'group'),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FileSystem(
|
class FileSystem(
|
||||||
fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder,
|
fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase
|
||||||
unittest.TestCase):
|
):
|
||||||
def test_unicode_dir_on_sys_path(self):
|
def test_unicode_dir_on_sys_path(self):
|
||||||
"""
|
"""
|
||||||
Ensure a Unicode subdirectory of a directory on sys.path
|
Ensure a Unicode subdirectory of a directory on sys.path
|
||||||
|
@ -277,5 +281,5 @@ class FileSystem(
|
||||||
fixtures.build_files(
|
fixtures.build_files(
|
||||||
{self.unicode_filename(): {}},
|
{self.unicode_filename(): {}},
|
||||||
prefix=self.site_dir,
|
prefix=self.site_dir,
|
||||||
)
|
)
|
||||||
list(distributions())
|
list(distributions())
|
||||||
|
|
|
@ -2,20 +2,26 @@ import re
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from collections.abc import Iterator
|
|
||||||
|
|
||||||
from . import fixtures
|
from . import fixtures
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
Distribution, PackageNotFoundError, distribution,
|
Distribution,
|
||||||
entry_points, files, metadata, requires, version,
|
PackageNotFoundError,
|
||||||
)
|
distribution,
|
||||||
|
entry_points,
|
||||||
|
files,
|
||||||
|
metadata,
|
||||||
|
requires,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APITests(
|
class APITests(
|
||||||
fixtures.EggInfoPkg,
|
fixtures.EggInfoPkg,
|
||||||
fixtures.DistInfoPkg,
|
fixtures.DistInfoPkg,
|
||||||
fixtures.EggInfoFile,
|
fixtures.DistInfoPkgWithDot,
|
||||||
unittest.TestCase):
|
fixtures.EggInfoFile,
|
||||||
|
unittest.TestCase,
|
||||||
|
):
|
||||||
|
|
||||||
version_pattern = r'\d+\.\d+(\.\d)?'
|
version_pattern = r'\d+\.\d+(\.\d)?'
|
||||||
|
|
||||||
|
@ -33,16 +39,28 @@ class APITests(
|
||||||
with self.assertRaises(PackageNotFoundError):
|
with self.assertRaises(PackageNotFoundError):
|
||||||
distribution('does-not-exist')
|
distribution('does-not-exist')
|
||||||
|
|
||||||
|
def test_name_normalization(self):
|
||||||
|
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
|
||||||
|
for name in names:
|
||||||
|
with self.subTest(name):
|
||||||
|
assert distribution(name).metadata['Name'] == 'pkg.dot'
|
||||||
|
|
||||||
|
def test_prefix_not_matched(self):
|
||||||
|
prefixes = 'p', 'pkg', 'pkg.'
|
||||||
|
for prefix in prefixes:
|
||||||
|
with self.subTest(prefix):
|
||||||
|
with self.assertRaises(PackageNotFoundError):
|
||||||
|
distribution(prefix)
|
||||||
|
|
||||||
def test_for_top_level(self):
|
def test_for_top_level(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
distribution('egginfo-pkg').read_text('top_level.txt').strip(),
|
distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod'
|
||||||
'mod')
|
)
|
||||||
|
|
||||||
def test_read_text(self):
|
def test_read_text(self):
|
||||||
top_level = [
|
top_level = [
|
||||||
path for path in files('egginfo-pkg')
|
path for path in files('egginfo-pkg') if path.name == 'top_level.txt'
|
||||||
if path.name == 'top_level.txt'
|
][0]
|
||||||
][0]
|
|
||||||
self.assertEqual(top_level.read_text(), 'mod\n')
|
self.assertEqual(top_level.read_text(), 'mod\n')
|
||||||
|
|
||||||
def test_entry_points(self):
|
def test_entry_points(self):
|
||||||
|
@ -51,6 +69,13 @@ class APITests(
|
||||||
self.assertEqual(ep.value, 'mod:main')
|
self.assertEqual(ep.value, 'mod:main')
|
||||||
self.assertEqual(ep.extras, [])
|
self.assertEqual(ep.extras, [])
|
||||||
|
|
||||||
|
def test_entry_points_distribution(self):
|
||||||
|
entries = dict(entry_points()['entries'])
|
||||||
|
for entry in ("main", "ns:sub"):
|
||||||
|
ep = entries[entry]
|
||||||
|
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
|
||||||
|
self.assertEqual(ep.dist.version, "1.0.0")
|
||||||
|
|
||||||
def test_metadata_for_this_package(self):
|
def test_metadata_for_this_package(self):
|
||||||
md = metadata('egginfo-pkg')
|
md = metadata('egginfo-pkg')
|
||||||
assert md['author'] == 'Steven Ma'
|
assert md['author'] == 'Steven Ma'
|
||||||
|
@ -75,13 +100,8 @@ class APITests(
|
||||||
def test_file_hash_repr(self):
|
def test_file_hash_repr(self):
|
||||||
assertRegex = self.assertRegex
|
assertRegex = self.assertRegex
|
||||||
|
|
||||||
util = [
|
util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
|
||||||
p for p in files('distinfo-pkg')
|
assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
|
||||||
if p.name == 'mod.py'
|
|
||||||
][0]
|
|
||||||
assertRegex(
|
|
||||||
repr(util.hash),
|
|
||||||
'<FileHash mode: sha256 value: .*>')
|
|
||||||
|
|
||||||
def test_files_dist_info(self):
|
def test_files_dist_info(self):
|
||||||
self._test_files(files('distinfo-pkg'))
|
self._test_files(files('distinfo-pkg'))
|
||||||
|
@ -99,10 +119,7 @@ class APITests(
|
||||||
def test_requires_egg_info(self):
|
def test_requires_egg_info(self):
|
||||||
deps = requires('egginfo-pkg')
|
deps = requires('egginfo-pkg')
|
||||||
assert len(deps) == 2
|
assert len(deps) == 2
|
||||||
assert any(
|
assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
|
||||||
dep == 'wheel >= 1.0; python_version >= "2.7"'
|
|
||||||
for dep in deps
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_requires_dist_info(self):
|
def test_requires_dist_info(self):
|
||||||
deps = requires('distinfo-pkg')
|
deps = requires('distinfo-pkg')
|
||||||
|
@ -112,7 +129,8 @@ class APITests(
|
||||||
assert "pytest; extra == 'test'" in deps
|
assert "pytest; extra == 'test'" in deps
|
||||||
|
|
||||||
def test_more_complex_deps_requires_text(self):
|
def test_more_complex_deps_requires_text(self):
|
||||||
requires = textwrap.dedent("""
|
requires = textwrap.dedent(
|
||||||
|
"""
|
||||||
dep1
|
dep1
|
||||||
dep2
|
dep2
|
||||||
|
|
||||||
|
@ -124,7 +142,8 @@ class APITests(
|
||||||
|
|
||||||
[extra2:python_version < "3"]
|
[extra2:python_version < "3"]
|
||||||
dep5
|
dep5
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
deps = sorted(Distribution._deps_from_requires_text(requires))
|
deps = sorted(Distribution._deps_from_requires_text(requires))
|
||||||
expected = [
|
expected = [
|
||||||
'dep1',
|
'dep1',
|
||||||
|
@ -132,7 +151,7 @@ class APITests(
|
||||||
'dep3; python_version < "3"',
|
'dep3; python_version < "3"',
|
||||||
'dep4; extra == "extra1"',
|
'dep4; extra == "extra1"',
|
||||||
'dep5; (python_version < "3") and extra == "extra2"',
|
'dep5; (python_version < "3") and extra == "extra2"',
|
||||||
]
|
]
|
||||||
# It's important that the environment marker expression be
|
# It's important that the environment marker expression be
|
||||||
# wrapped in parentheses to avoid the following 'and' binding more
|
# wrapped in parentheses to avoid the following 'and' binding more
|
||||||
# tightly than some other part of the environment expression.
|
# tightly than some other part of the environment expression.
|
||||||
|
@ -140,17 +159,27 @@ class APITests(
|
||||||
assert deps == expected
|
assert deps == expected
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
|
||||||
|
def test_name_normalization(self):
|
||||||
|
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
|
||||||
|
for name in names:
|
||||||
|
with self.subTest(name):
|
||||||
|
assert distribution(name).metadata['Name'] == 'pkg.dot'
|
||||||
|
|
||||||
|
def test_name_normalization_versionless_egg_info(self):
|
||||||
|
names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
|
||||||
|
for name in names:
|
||||||
|
with self.subTest(name):
|
||||||
|
assert distribution(name).metadata['Name'] == 'pkg.lot'
|
||||||
|
|
||||||
|
|
||||||
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
|
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
|
||||||
def test_find_distributions_specified_path(self):
|
def test_find_distributions_specified_path(self):
|
||||||
dists = Distribution.discover(path=[str(self.site_dir)])
|
dists = Distribution.discover(path=[str(self.site_dir)])
|
||||||
assert any(
|
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
|
||||||
dist.metadata['Name'] == 'distinfo-pkg'
|
|
||||||
for dist in dists
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_distribution_at_pathlib(self):
|
def test_distribution_at_pathlib(self):
|
||||||
"""Demonstrate how to load metadata direct from a directory.
|
"""Demonstrate how to load metadata direct from a directory."""
|
||||||
"""
|
|
||||||
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
|
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
|
||||||
dist = Distribution.at(dist_info_path)
|
dist = Distribution.at(dist_info_path)
|
||||||
assert dist.version == '1.0.0'
|
assert dist.version == '1.0.0'
|
||||||
|
|
|
@ -3,8 +3,12 @@ import unittest
|
||||||
|
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
distribution, entry_points, files, PackageNotFoundError,
|
PackageNotFoundError,
|
||||||
version, distributions,
|
distribution,
|
||||||
|
distributions,
|
||||||
|
entry_points,
|
||||||
|
files,
|
||||||
|
version,
|
||||||
)
|
)
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
In ``importlib.metadata``: - ``EntryPoint`` objects now expose a ``.dist``
|
||||||
|
object referencing the ``Distribution`` when constructed from a
|
||||||
|
``Distribution``. - Add support for package discovery under package
|
||||||
|
normalization rules. - The object returned by ``metadata()`` now has a
|
||||||
|
formally-defined protocol called ``PackageMetadata`` with declared support
|
||||||
|
for the ``.get_all()`` method. - Synced with importlib_metadata 3.3.
|
Loading…
Reference in New Issue