mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
bpo-43428: Sync with importlib_metadata 3.7. (GH-24782)
* bpo-43428: Sync with importlib_metadata 3.7.2 (67234b6) * Add blurb * Reformat blurb to create separate paragraphs for each change included.
This commit is contained in:
parent
2256a2876b
commit
f917efccf8
8 changed files with 343 additions and 43 deletions
|
@ -74,18 +74,20 @@ This package provides the following functionality via its public API.
|
||||||
Entry points
|
Entry points
|
||||||
------------
|
------------
|
||||||
|
|
||||||
The ``entry_points()`` function returns a dictionary of all entry points,
|
The ``entry_points()`` function returns a collection of entry points.
|
||||||
keyed by group. Entry points are represented by ``EntryPoint`` instances;
|
Entry points are represented by ``EntryPoint`` instances;
|
||||||
each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and
|
each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and
|
||||||
a ``.load()`` method to resolve the value. There are also ``.module``,
|
a ``.load()`` method to resolve the value. There are also ``.module``,
|
||||||
``.attr``, and ``.extras`` attributes for getting the components of the
|
``.attr``, and ``.extras`` attributes for getting the components of the
|
||||||
``.value`` attribute::
|
``.value`` attribute::
|
||||||
|
|
||||||
>>> eps = entry_points() # doctest: +SKIP
|
>>> eps = entry_points() # doctest: +SKIP
|
||||||
>>> list(eps) # doctest: +SKIP
|
>>> sorted(eps.groups) # doctest: +SKIP
|
||||||
['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']
|
['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']
|
||||||
>>> scripts = eps['console_scripts'] # doctest: +SKIP
|
>>> scripts = eps.select(group='console_scripts') # doctest: +SKIP
|
||||||
>>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] # doctest: +SKIP
|
>>> 'wheel' in scripts.names # doctest: +SKIP
|
||||||
|
True
|
||||||
|
>>> wheel = scripts['wheel'] # doctest: +SKIP
|
||||||
>>> wheel # doctest: +SKIP
|
>>> wheel # doctest: +SKIP
|
||||||
EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
|
EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
|
||||||
>>> wheel.module # doctest: +SKIP
|
>>> wheel.module # doctest: +SKIP
|
||||||
|
@ -187,6 +189,17 @@ function::
|
||||||
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
|
["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
|
||||||
|
|
||||||
|
|
||||||
|
Package distributions
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
A convience method to resolve the distribution or
|
||||||
|
distributions (in the case of a namespace package) for top-level
|
||||||
|
Python packages or modules::
|
||||||
|
|
||||||
|
>>> packages_distributions()
|
||||||
|
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
|
||||||
|
|
||||||
|
|
||||||
Distributions
|
Distributions
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
19
Lib/importlib/_itertools.py
Normal file
19
Lib/importlib/_itertools.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
from itertools import filterfalse
|
||||||
|
|
||||||
|
|
||||||
|
def unique_everseen(iterable, key=None):
|
||||||
|
"List unique elements, preserving order. Remember all elements ever seen."
|
||||||
|
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||||
|
# unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||||
|
seen = set()
|
||||||
|
seen_add = seen.add
|
||||||
|
if key is None:
|
||||||
|
for element in filterfalse(seen.__contains__, iterable):
|
||||||
|
seen_add(element)
|
||||||
|
yield element
|
||||||
|
else:
|
||||||
|
for element in iterable:
|
||||||
|
k = key(element)
|
||||||
|
if k not in seen:
|
||||||
|
seen_add(k)
|
||||||
|
yield element
|
|
@ -4,20 +4,24 @@ import abc
|
||||||
import csv
|
import csv
|
||||||
import sys
|
import sys
|
||||||
import email
|
import email
|
||||||
|
import inspect
|
||||||
import pathlib
|
import pathlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import operator
|
import operator
|
||||||
|
import warnings
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import posixpath
|
import posixpath
|
||||||
import collections
|
import collections.abc
|
||||||
|
|
||||||
|
from ._itertools import unique_everseen
|
||||||
|
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from contextlib import suppress
|
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
|
from typing import Any, List, Mapping, Optional, Protocol, TypeVar, Union
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -120,18 +124,19 @@ class EntryPoint(
|
||||||
config.read_string(text)
|
config.read_string(text)
|
||||||
return cls._from_config(config)
|
return cls._from_config(config)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _from_text_for(cls, text, dist):
|
|
||||||
return (ep._for(dist) for ep in cls._from_text(text))
|
|
||||||
|
|
||||||
def _for(self, dist):
|
def _for(self, dist):
|
||||||
self.dist = dist
|
self.dist = dist
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""
|
"""
|
||||||
Supply iter so one may construct dicts of EntryPoints easily.
|
Supply iter so one may construct dicts of EntryPoints by name.
|
||||||
"""
|
"""
|
||||||
|
msg = (
|
||||||
|
"Construction of dict of EntryPoints is deprecated in "
|
||||||
|
"favor of EntryPoints."
|
||||||
|
)
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
return iter((self.name, self))
|
return iter((self.name, self))
|
||||||
|
|
||||||
def __reduce__(self):
|
def __reduce__(self):
|
||||||
|
@ -140,6 +145,143 @@ class EntryPoint(
|
||||||
(self.name, self.value, self.group),
|
(self.name, self.value, self.group),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def matches(self, **params):
|
||||||
|
attrs = (getattr(self, param) for param in params)
|
||||||
|
return all(map(operator.eq, params.values(), attrs))
|
||||||
|
|
||||||
|
|
||||||
|
class EntryPoints(tuple):
|
||||||
|
"""
|
||||||
|
An immutable collection of selectable EntryPoint objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def __getitem__(self, name): # -> EntryPoint:
|
||||||
|
try:
|
||||||
|
return next(iter(self.select(name=name)))
|
||||||
|
except StopIteration:
|
||||||
|
raise KeyError(name)
|
||||||
|
|
||||||
|
def select(self, **params):
|
||||||
|
return EntryPoints(ep for ep in self if ep.matches(**params))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def names(self):
|
||||||
|
return set(ep.name for ep in self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self):
|
||||||
|
"""
|
||||||
|
For coverage while SelectableGroups is present.
|
||||||
|
>>> EntryPoints().groups
|
||||||
|
set()
|
||||||
|
"""
|
||||||
|
return set(ep.group for ep in self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_text_for(cls, text, dist):
|
||||||
|
return cls(ep._for(dist) for ep in EntryPoint._from_text(text))
|
||||||
|
|
||||||
|
|
||||||
|
def flake8_bypass(func):
|
||||||
|
is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5])
|
||||||
|
return func if not is_flake8 else lambda: None
|
||||||
|
|
||||||
|
|
||||||
|
class Deprecated:
|
||||||
|
"""
|
||||||
|
Compatibility add-in for mapping to indicate that
|
||||||
|
mapping behavior is deprecated.
|
||||||
|
|
||||||
|
>>> recwarn = getfixture('recwarn')
|
||||||
|
>>> class DeprecatedDict(Deprecated, dict): pass
|
||||||
|
>>> dd = DeprecatedDict(foo='bar')
|
||||||
|
>>> dd.get('baz', None)
|
||||||
|
>>> dd['foo']
|
||||||
|
'bar'
|
||||||
|
>>> list(dd)
|
||||||
|
['foo']
|
||||||
|
>>> list(dd.keys())
|
||||||
|
['foo']
|
||||||
|
>>> 'foo' in dd
|
||||||
|
True
|
||||||
|
>>> list(dd.values())
|
||||||
|
['bar']
|
||||||
|
>>> len(recwarn)
|
||||||
|
1
|
||||||
|
"""
|
||||||
|
|
||||||
|
_warn = functools.partial(
|
||||||
|
warnings.warn,
|
||||||
|
"SelectableGroups dict interface is deprecated. Use select.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
self._warn()
|
||||||
|
return super().__getitem__(name)
|
||||||
|
|
||||||
|
def get(self, name, default=None):
|
||||||
|
flake8_bypass(self._warn)()
|
||||||
|
return super().get(name, default)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
self._warn()
|
||||||
|
return super().__iter__()
|
||||||
|
|
||||||
|
def __contains__(self, *args):
|
||||||
|
self._warn()
|
||||||
|
return super().__contains__(*args)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
self._warn()
|
||||||
|
return super().keys()
|
||||||
|
|
||||||
|
def values(self):
|
||||||
|
self._warn()
|
||||||
|
return super().values()
|
||||||
|
|
||||||
|
|
||||||
|
class SelectableGroups(dict):
|
||||||
|
"""
|
||||||
|
A backward- and forward-compatible result from
|
||||||
|
entry_points that fully implements the dict interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, eps):
|
||||||
|
by_group = operator.attrgetter('group')
|
||||||
|
ordered = sorted(eps, key=by_group)
|
||||||
|
grouped = itertools.groupby(ordered, by_group)
|
||||||
|
return cls((group, EntryPoints(eps)) for group, eps in grouped)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _all(self):
|
||||||
|
"""
|
||||||
|
Reconstruct a list of all entrypoints from the groups.
|
||||||
|
"""
|
||||||
|
return EntryPoints(itertools.chain.from_iterable(self.values()))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self):
|
||||||
|
return self._all.groups
|
||||||
|
|
||||||
|
@property
|
||||||
|
def names(self):
|
||||||
|
"""
|
||||||
|
for coverage:
|
||||||
|
>>> SelectableGroups().names
|
||||||
|
set()
|
||||||
|
"""
|
||||||
|
return self._all.names
|
||||||
|
|
||||||
|
def select(self, **params):
|
||||||
|
if not params:
|
||||||
|
return self
|
||||||
|
return self._all.select(**params)
|
||||||
|
|
||||||
|
|
||||||
class PackagePath(pathlib.PurePosixPath):
|
class PackagePath(pathlib.PurePosixPath):
|
||||||
"""A reference to a path in a package"""
|
"""A reference to a path in a package"""
|
||||||
|
@ -296,7 +438,7 @@ class Distribution:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entry_points(self):
|
def entry_points(self):
|
||||||
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))
|
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def files(self):
|
def files(self):
|
||||||
|
@ -485,15 +627,22 @@ class Prepared:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
normalized = None
|
normalized = None
|
||||||
suffixes = '.dist-info', '.egg-info'
|
suffixes = 'dist-info', 'egg-info'
|
||||||
exact_matches = [''][:0]
|
exact_matches = [''][:0]
|
||||||
|
egg_prefix = ''
|
||||||
|
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 = self.normalize(name)
|
self.normalized = self.normalize(name)
|
||||||
self.exact_matches = [self.normalized + suffix for suffix in self.suffixes]
|
self.exact_matches = [
|
||||||
|
self.normalized + '.' + suffix for suffix in self.suffixes
|
||||||
|
]
|
||||||
|
legacy_normalized = self.legacy_normalize(self.name)
|
||||||
|
self.egg_prefix = legacy_normalized + '-'
|
||||||
|
self.versionless_egg_name = legacy_normalized + '.egg'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(name):
|
def normalize(name):
|
||||||
|
@ -512,8 +661,9 @@ class Prepared:
|
||||||
|
|
||||||
def matches(self, cand, base):
|
def matches(self, cand, base):
|
||||||
low = cand.lower()
|
low = cand.lower()
|
||||||
pre, ext = os.path.splitext(low)
|
# rpartition is faster than splitext and suitable for this purpose.
|
||||||
name, sep, rest = pre.partition('-')
|
pre, _, ext = low.rpartition('.')
|
||||||
|
name, _, rest = pre.partition('-')
|
||||||
return (
|
return (
|
||||||
low in self.exact_matches
|
low in self.exact_matches
|
||||||
or ext in self.suffixes
|
or ext in self.suffixes
|
||||||
|
@ -524,12 +674,9 @@ class Prepared:
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_egg(self, base):
|
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 (
|
return (
|
||||||
base == versionless_egg_name
|
base == self.versionless_egg_name
|
||||||
or base.startswith(prefix)
|
or base.startswith(self.egg_prefix)
|
||||||
and base.endswith('.egg')
|
and base.endswith('.egg')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -551,8 +698,9 @@ class MetadataPathFinder(DistributionFinder):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _search_paths(cls, name, paths):
|
def _search_paths(cls, name, paths):
|
||||||
"""Find metadata directories in paths heuristically."""
|
"""Find metadata directories in paths heuristically."""
|
||||||
|
prepared = Prepared(name)
|
||||||
return itertools.chain.from_iterable(
|
return itertools.chain.from_iterable(
|
||||||
path.search(Prepared(name)) for path in map(FastPath, paths)
|
path.search(prepared) for path in map(FastPath, paths)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -617,16 +765,28 @@ def version(distribution_name):
|
||||||
return distribution(distribution_name).version
|
return distribution(distribution_name).version
|
||||||
|
|
||||||
|
|
||||||
def entry_points():
|
def entry_points(**params) -> Union[EntryPoints, SelectableGroups]:
|
||||||
"""Return EntryPoint objects for all installed packages.
|
"""Return EntryPoint objects for all installed packages.
|
||||||
|
|
||||||
:return: EntryPoint objects for all installed packages.
|
Pass selection parameters (group or name) to filter the
|
||||||
|
result to entry points matching those properties (see
|
||||||
|
EntryPoints.select()).
|
||||||
|
|
||||||
|
For compatibility, returns ``SelectableGroups`` object unless
|
||||||
|
selection parameters are supplied. In the future, this function
|
||||||
|
will return ``EntryPoints`` instead of ``SelectableGroups``
|
||||||
|
even when no selection parameters are supplied.
|
||||||
|
|
||||||
|
For maximum future compatibility, pass selection parameters
|
||||||
|
or invoke ``.select`` with parameters on the result.
|
||||||
|
|
||||||
|
:return: EntryPoints or SelectableGroups for all installed packages.
|
||||||
"""
|
"""
|
||||||
eps = itertools.chain.from_iterable(dist.entry_points for dist in distributions())
|
unique = functools.partial(unique_everseen, key=operator.attrgetter('name'))
|
||||||
by_group = operator.attrgetter('group')
|
eps = itertools.chain.from_iterable(
|
||||||
ordered = sorted(eps, key=by_group)
|
dist.entry_points for dist in unique(distributions())
|
||||||
grouped = itertools.groupby(ordered, by_group)
|
)
|
||||||
return {group: tuple(eps) for group, eps in grouped}
|
return SelectableGroups.load(eps).select(**params)
|
||||||
|
|
||||||
|
|
||||||
def files(distribution_name):
|
def files(distribution_name):
|
||||||
|
@ -646,3 +806,19 @@ def requires(distribution_name):
|
||||||
packaging.requirement.Requirement.
|
packaging.requirement.Requirement.
|
||||||
"""
|
"""
|
||||||
return distribution(distribution_name).requires
|
return distribution(distribution_name).requires
|
||||||
|
|
||||||
|
|
||||||
|
def packages_distributions() -> Mapping[str, List[str]]:
|
||||||
|
"""
|
||||||
|
Return a mapping of top-level packages to their
|
||||||
|
distributions.
|
||||||
|
|
||||||
|
>>> pkgs = packages_distributions()
|
||||||
|
>>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values())
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
pkg_to_dist = collections.defaultdict(list)
|
||||||
|
for dist in distributions():
|
||||||
|
for pkg in (dist.read_text('top_level.txt') or '').split():
|
||||||
|
pkg_to_dist[pkg].append(dist.metadata['Name'])
|
||||||
|
return dict(pkg_to_dist)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import textwrap
|
||||||
import contextlib
|
import contextlib
|
||||||
import unittest
|
|
||||||
|
|
||||||
from test.support.os_helper import FS_NONASCII
|
from test.support.os_helper import FS_NONASCII
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
@ -221,7 +220,6 @@ class LocalPackage:
|
||||||
build_files(self.files)
|
build_files(self.files)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def build_files(file_defs, prefix=pathlib.Path()):
|
def build_files(file_defs, prefix=pathlib.Path()):
|
||||||
"""Build a set of files/directories, as described by the
|
"""Build a set of files/directories, as described by the
|
||||||
|
|
||||||
|
@ -260,9 +258,6 @@ class FileBuilder:
|
||||||
def unicode_filename(self):
|
def unicode_filename(self):
|
||||||
return FS_NONASCII or self.skip("File system does not support non-ascii.")
|
return FS_NONASCII or self.skip("File system does not support non-ascii.")
|
||||||
|
|
||||||
def skip(self, reason):
|
|
||||||
raise unittest.SkipTest(reason)
|
|
||||||
|
|
||||||
|
|
||||||
def DALS(str):
|
def DALS(str):
|
||||||
"Dedent and left-strip"
|
"Dedent and left-strip"
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import pickle
|
import pickle
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
import warnings
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -58,13 +59,11 @@ class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
|
||||||
importlib.import_module('does_not_exist')
|
importlib.import_module('does_not_exist')
|
||||||
|
|
||||||
def test_resolve(self):
|
def test_resolve(self):
|
||||||
entries = dict(entry_points()['entries'])
|
ep = entry_points(group='entries')['main']
|
||||||
ep = entries['main']
|
|
||||||
self.assertEqual(ep.load().__name__, "main")
|
self.assertEqual(ep.load().__name__, "main")
|
||||||
|
|
||||||
def test_entrypoint_with_colon_in_name(self):
|
def test_entrypoint_with_colon_in_name(self):
|
||||||
entries = dict(entry_points()['entries'])
|
ep = entry_points(group='entries')['ns:sub']
|
||||||
ep = entries['ns:sub']
|
|
||||||
self.assertEqual(ep.value, 'mod:main')
|
self.assertEqual(ep.value, 'mod:main')
|
||||||
|
|
||||||
def test_resolve_without_attr(self):
|
def test_resolve_without_attr(self):
|
||||||
|
@ -250,7 +249,8 @@ class TestEntryPoints(unittest.TestCase):
|
||||||
json should not expect to be able to dump an EntryPoint
|
json should not expect to be able to dump an EntryPoint
|
||||||
"""
|
"""
|
||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
json.dumps(self.ep)
|
with warnings.catch_warnings(record=True):
|
||||||
|
json.dumps(self.ep)
|
||||||
|
|
||||||
def test_module(self):
|
def test_module(self):
|
||||||
assert self.ep.module == 'value'
|
assert self.ep.module == 'value'
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
|
import warnings
|
||||||
|
|
||||||
from . import fixtures
|
from . import fixtures
|
||||||
from importlib.metadata import (
|
from importlib.metadata import (
|
||||||
|
@ -64,18 +65,97 @@ class APITests(
|
||||||
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):
|
||||||
entries = dict(entry_points()['entries'])
|
eps = entry_points()
|
||||||
|
assert 'entries' in eps.groups
|
||||||
|
entries = eps.select(group='entries')
|
||||||
|
assert 'main' in entries.names
|
||||||
ep = entries['main']
|
ep = entries['main']
|
||||||
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):
|
def test_entry_points_distribution(self):
|
||||||
entries = dict(entry_points()['entries'])
|
entries = entry_points(group='entries')
|
||||||
for entry in ("main", "ns:sub"):
|
for entry in ("main", "ns:sub"):
|
||||||
ep = entries[entry]
|
ep = entries[entry]
|
||||||
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
|
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
|
||||||
self.assertEqual(ep.dist.version, "1.0.0")
|
self.assertEqual(ep.dist.version, "1.0.0")
|
||||||
|
|
||||||
|
def test_entry_points_unique_packages(self):
|
||||||
|
"""
|
||||||
|
Entry points should only be exposed for the first package
|
||||||
|
on sys.path with a given name.
|
||||||
|
"""
|
||||||
|
alt_site_dir = self.fixtures.enter_context(fixtures.tempdir())
|
||||||
|
self.fixtures.enter_context(self.add_sys_path(alt_site_dir))
|
||||||
|
alt_pkg = {
|
||||||
|
"distinfo_pkg-1.1.0.dist-info": {
|
||||||
|
"METADATA": """
|
||||||
|
Name: distinfo-pkg
|
||||||
|
Version: 1.1.0
|
||||||
|
""",
|
||||||
|
"entry_points.txt": """
|
||||||
|
[entries]
|
||||||
|
main = mod:altmain
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fixtures.build_files(alt_pkg, alt_site_dir)
|
||||||
|
entries = entry_points(group='entries')
|
||||||
|
assert not any(
|
||||||
|
ep.dist.name == 'distinfo-pkg' and ep.dist.version == '1.0.0'
|
||||||
|
for ep in entries
|
||||||
|
)
|
||||||
|
# ns:sub doesn't exist in alt_pkg
|
||||||
|
assert 'ns:sub' not in entries
|
||||||
|
|
||||||
|
def test_entry_points_missing_name(self):
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
entry_points(group='entries')['missing']
|
||||||
|
|
||||||
|
def test_entry_points_missing_group(self):
|
||||||
|
assert entry_points(group='missing') == ()
|
||||||
|
|
||||||
|
def test_entry_points_dict_construction(self):
|
||||||
|
"""
|
||||||
|
Prior versions of entry_points() returned simple lists and
|
||||||
|
allowed casting those lists into maps by name using ``dict()``.
|
||||||
|
Capture this now deprecated use-case.
|
||||||
|
"""
|
||||||
|
with warnings.catch_warnings(record=True) as caught:
|
||||||
|
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||||
|
eps = dict(entry_points(group='entries'))
|
||||||
|
|
||||||
|
assert 'main' in eps
|
||||||
|
assert eps['main'] == entry_points(group='entries')['main']
|
||||||
|
|
||||||
|
# check warning
|
||||||
|
expected = next(iter(caught))
|
||||||
|
assert expected.category is DeprecationWarning
|
||||||
|
assert "Construction of dict of EntryPoints is deprecated" in str(expected)
|
||||||
|
|
||||||
|
def test_entry_points_groups_getitem(self):
|
||||||
|
"""
|
||||||
|
Prior versions of entry_points() returned a dict. Ensure
|
||||||
|
that callers using '.__getitem__()' are supported but warned to
|
||||||
|
migrate.
|
||||||
|
"""
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
entry_points()['entries'] == entry_points(group='entries')
|
||||||
|
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
entry_points()['missing']
|
||||||
|
|
||||||
|
def test_entry_points_groups_get(self):
|
||||||
|
"""
|
||||||
|
Prior versions of entry_points() returned a dict. Ensure
|
||||||
|
that callers using '.get()' are supported but warned to
|
||||||
|
migrate.
|
||||||
|
"""
|
||||||
|
with warnings.catch_warnings(record=True):
|
||||||
|
entry_points().get('missing', 'default') == 'default'
|
||||||
|
entry_points().get('entries', 'default') == entry_points()['entries']
|
||||||
|
entry_points().get('missing', ()) == ()
|
||||||
|
|
||||||
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'
|
||||||
|
|
|
@ -41,7 +41,7 @@ class TestZip(unittest.TestCase):
|
||||||
version('definitely-not-installed')
|
version('definitely-not-installed')
|
||||||
|
|
||||||
def test_zip_entry_points(self):
|
def test_zip_entry_points(self):
|
||||||
scripts = dict(entry_points()['console_scripts'])
|
scripts = entry_points(group='console_scripts')
|
||||||
entry_point = scripts['example']
|
entry_point = scripts['example']
|
||||||
self.assertEqual(entry_point.value, 'example:main')
|
self.assertEqual(entry_point.value, 'example:main')
|
||||||
entry_point = scripts['Example']
|
entry_point = scripts['Example']
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
Include changes from `importlib_metadata 3.7
|
||||||
|
<https://importlib-metadata.readthedocs.io/en/latest/history.html#v3-7-0>`_:
|
||||||
|
|
||||||
|
Performance enhancements to distribution discovery.
|
||||||
|
|
||||||
|
``entry_points`` only returns unique distributions.
|
||||||
|
|
||||||
|
Introduces new ``EntryPoints`` object
|
||||||
|
for containing a set of entry points with convenience methods for selecting
|
||||||
|
entry points by group or name. ``entry_points`` now returns this object if
|
||||||
|
selection parameters are supplied but continues to return a dict object for
|
||||||
|
compatibility. Users are encouraged to rely on the selection interface. The
|
||||||
|
dict object result is likely to be deprecated in the future.
|
||||||
|
|
||||||
|
Added
|
||||||
|
packages_distributions function to return a mapping of packages to the
|
||||||
|
distributions that provide them.
|
Loading…
Add table
Add a link
Reference in a new issue