* bpo-44893: Implement EntryPoint as simple class and deprecate tuple access in favor of attribute access. Syncs with importlib_metadata 4.8.1.
* Apply refactorings found in importlib_metadata 4.8.2.
import collections
from . import _adapters, _meta
-from ._meta import PackageMetadata
from ._collections import FreezableDefaultDict, Pair
-from ._functools import method_cache
-from ._itertools import unique_everseen
+from ._functools import method_cache, pass_none
+from ._itertools import always_iterable, unique_everseen
from ._meta import PackageMetadata, SimplePath
from contextlib import suppress
return line and not line.startswith('#')
-class EntryPoint(
- collections.namedtuple('EntryPointBase', 'name value group')):
+class DeprecatedTuple:
+ """
+ Provide subscript item access for backward compatibility.
+
+ >>> recwarn = getfixture('recwarn')
+ >>> ep = EntryPoint(name='name', value='value', group='group')
+ >>> ep[:]
+ ('name', 'value', 'group')
+ >>> ep[0]
+ 'name'
+ >>> len(recwarn)
+ 1
+ """
+
+ _warn = functools.partial(
+ warnings.warn,
+ "EntryPoint tuple interface is deprecated. Access members by name.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ def __getitem__(self, item):
+ self._warn()
+ return self._key()[item]
+
+
+class EntryPoint(DeprecatedTuple):
"""An entry point as defined by Python packaging conventions.
See `the packaging docs on entry points
dist: Optional['Distribution'] = None
+ def __init__(self, name, value, group):
+ vars(self).update(name=name, value=value, group=group)
+
def load(self):
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
return list(re.finditer(r'\w+', match.group('extras') or ''))
def _for(self, dist):
- self.dist = dist
+ vars(self).update(dist=dist)
return self
def __iter__(self):
warnings.warn(msg, DeprecationWarning)
return iter((self.name, self))
- def __reduce__(self):
- return (
- self.__class__,
- (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))
+ def _key(self):
+ return self.name, self.value, self.group
+
+ def __lt__(self, other):
+ return self._key() < other._key()
+
+ def __eq__(self, other):
+ return self._key() == other._key()
+
+ def __setattr__(self, name, value):
+ raise AttributeError("EntryPoint objects are immutable.")
+
+ def __repr__(self):
+ return (
+ f'EntryPoint(name={self.name!r}, value={self.value!r}, '
+ f'group={self.group!r})'
+ )
+
+ def __hash__(self):
+ return hash(self._key())
+
class DeprecatedList(list):
"""
stacklevel=2,
)
- def __setitem__(self, *args, **kwargs):
- self._warn()
- return super().__setitem__(*args, **kwargs)
-
- def __delitem__(self, *args, **kwargs):
- self._warn()
- return super().__delitem__(*args, **kwargs)
-
- def append(self, *args, **kwargs):
- self._warn()
- return super().append(*args, **kwargs)
-
- def reverse(self, *args, **kwargs):
- self._warn()
- return super().reverse(*args, **kwargs)
-
- def extend(self, *args, **kwargs):
- self._warn()
- return super().extend(*args, **kwargs)
-
- def pop(self, *args, **kwargs):
- self._warn()
- return super().pop(*args, **kwargs)
-
- def remove(self, *args, **kwargs):
- self._warn()
- return super().remove(*args, **kwargs)
-
- def __iadd__(self, *args, **kwargs):
- self._warn()
- return super().__iadd__(*args, **kwargs)
+ def _wrap_deprecated_method(method_name: str): # type: ignore
+ def wrapped(self, *args, **kwargs):
+ self._warn()
+ return getattr(super(), method_name)(*args, **kwargs)
+
+ return wrapped
+
+ for method_name in [
+ '__setitem__',
+ '__delitem__',
+ 'append',
+ 'reverse',
+ 'extend',
+ 'pop',
+ 'remove',
+ '__iadd__',
+ 'insert',
+ 'sort',
+ ]:
+ locals()[method_name] = _wrap_deprecated_method(method_name)
def __add__(self, other):
if not isinstance(other, tuple):
other = tuple(other)
return self.__class__(tuple(self) + other)
- def insert(self, *args, **kwargs):
- self._warn()
- return super().insert(*args, **kwargs)
-
- def sort(self, *args, **kwargs):
- self._warn()
- return super().sort(*args, **kwargs)
-
def __eq__(self, other):
if not isinstance(other, tuple):
self._warn()
"""
Return the set of all names of all entry points.
"""
- return set(ep.name for ep in self)
+ return {ep.name for ep in self}
@property
def groups(self):
>>> EntryPoints().groups
set()
"""
- return set(ep.group for ep in self)
+ return {ep.group for ep in self}
@classmethod
def _from_text_for(cls, text, dist):
return cls(ep._for(dist) for ep in cls._from_text(text))
- @classmethod
- def _from_text(cls, text):
- return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
-
@staticmethod
- def _parse_groups(text):
+ def _from_text(text):
return (
- (item.value.name, item.value.value, item.name)
- for item in Sectioned.section_pairs(text)
+ EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
+ for item in Sectioned.section_pairs(text or '')
)
missing.
Result may be empty if the metadata exists but is empty.
"""
- file_lines = self._read_files_distinfo() or self._read_files_egginfo()
def make_file(name, hash=None, size_str=None):
result = PackagePath(name)
result.dist = self
return result
- return file_lines and list(starmap(make_file, csv.reader(file_lines)))
+ @pass_none
+ def make_files(lines):
+ return list(starmap(make_file, csv.reader(lines)))
+
+ return make_files(self._read_files_distinfo() or self._read_files_egginfo())
def _read_files_distinfo(self):
"""
"""
Micro-optimized class for searching a path for
children.
+
+ >>> FastPath('').children()
+ ['...']
"""
@functools.lru_cache() # type: ignore
"""
pkg_to_dist = collections.defaultdict(list)
for dist in distributions():
- for pkg in (dist.read_text('top_level.txt') or '').split():
+ for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
pkg_to_dist[pkg].append(dist.metadata['Name'])
return dict(pkg_to_dist)
+
+
+def _top_level_declared(dist):
+ return (dist.read_text('top_level.txt') or '').split()
+
+
+def _top_level_inferred(dist):
+ return {
+ f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+ for f in always_iterable(dist.files)
+ if f.suffix == ".py"
+ }
wrapper.cache_clear = lambda: None
return wrapper
+
+
+# From jaraco.functools 3.3
+def pass_none(func):
+ """
+ Wrap func so it's not called if its first param is None
+
+ >>> print_text = pass_none(print)
+ >>> print_text('text')
+ text
+ >>> print_text(None)
+ """
+
+ @functools.wraps(func)
+ def wrapper(param, *args, **kwargs):
+ if param is not None:
+ return func(param, *args, **kwargs)
+
+ return wrapper
if k not in seen:
seen_add(k)
yield element
+
+
+# copied from more_itertools 8.8
+def always_iterable(obj, base_type=(str, bytes)):
+ """If *obj* is iterable, return an iterator over its items::
+
+ >>> obj = (1, 2, 3)
+ >>> list(always_iterable(obj))
+ [1, 2, 3]
+
+ If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+ >>> obj = 1
+ >>> list(always_iterable(obj))
+ [1]
+
+ If *obj* is ``None``, return an empty iterable:
+
+ >>> obj = None
+ >>> list(always_iterable(None))
+ []
+
+ By default, binary and text strings are not considered iterable::
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj))
+ ['foo']
+
+ If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+ returns ``True`` won't be considered iterable.
+
+ >>> obj = {'a': 1}
+ >>> list(always_iterable(obj)) # Iterate over the dict's keys
+ ['a']
+ >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
+ [{'a': 1}]
+
+ Set *base_type* to ``None`` to avoid any special handling and treat objects
+ Python considers iterable as iterable:
+
+ >>> obj = 'foo'
+ >>> list(always_iterable(obj, base_type=None))
+ ['f', 'o', 'o']
+ """
+ if obj is None:
+ return iter(())
+
+ if (base_type is not None) and isinstance(obj, base_type):
+ return iter((obj,))
+
+ try:
+ return iter(obj)
+ except TypeError:
+ return iter((obj,))
def joinpath(self) -> 'SimplePath':
... # pragma: no cover
- def __div__(self) -> 'SimplePath':
+ def __truediv__(self) -> 'SimplePath':
... # pragma: no cover
def parent(self) -> 'SimplePath':
return hash(self.lower())
def __contains__(self, other):
- return super(FoldedCase, self).lower().__contains__(other.lower())
+ return super().lower().__contains__(other.lower())
def in_(self, other):
"Does self appear in other?"
# cache lower since it's likely to be called frequently.
@method_cache
def lower(self):
- return super(FoldedCase, self).lower()
+ return super().lower()
def index(self, sub):
return self.lower().index(sub.lower())
import contextlib
from test.support.os_helper import FS_NONASCII
+from test.support import requires_zlib
from typing import Dict, Union
+try:
+ from importlib import resources
+
+ getattr(resources, 'files')
+ getattr(resources, 'as_file')
+except (ImportError, AttributeError):
+ import importlib_resources as resources # type: ignore
+
@contextlib.contextmanager
def tempdir():
class SiteDir(Fixtures):
def setUp(self):
- super(SiteDir, self).setUp()
+ super().setUp()
self.site_dir = self.fixtures.enter_context(tempdir())
sys.path.remove(str(dir))
def setUp(self):
- super(OnSysPath, self).setUp()
+ super().setUp()
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
}
def setUp(self):
- super(DistInfoPkg, self).setUp()
+ super().setUp()
build_files(DistInfoPkg.files, self.site_dir)
def make_uppercase(self):
}
def setUp(self):
- super(DistInfoPkgWithDot, self).setUp()
+ super().setUp()
build_files(DistInfoPkgWithDot.files, self.site_dir)
}
def setUp(self):
- super(DistInfoPkgWithDotLegacy, self).setUp()
+ super().setUp()
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
class DistInfoPkgOffPath(SiteDir):
def setUp(self):
- super(DistInfoPkgOffPath, self).setUp()
+ super().setUp()
build_files(DistInfoPkg.files, self.site_dir)
}
def setUp(self):
- super(EggInfoPkg, self).setUp()
+ super().setUp()
build_files(EggInfoPkg.files, prefix=self.site_dir)
}
def setUp(self):
- super(EggInfoFile, self).setUp()
+ super().setUp()
build_files(EggInfoFile.files, prefix=self.site_dir)
class NullFinder:
def find_module(self, name):
pass
+
+
+@requires_zlib()
+class ZipFixtures:
+ root = 'test.test_importlib.data'
+
+ def _fixture_on_path(self, filename):
+ pkg_file = resources.files(self.root).joinpath(filename)
+ file = self.resources.enter_context(resources.as_file(pkg_file))
+ assert file.name.startswith('example'), file.name
+ sys.path.insert(0, str(file))
+ self.resources.callback(sys.path.pop, 0)
+
+ def setUp(self):
+ # Add self.zip_name to the front of sys.path.
+ self.resources = contextlib.ExitStack()
+ self.addCleanup(self.resources.close)
distributions,
entry_points,
metadata,
+ packages_distributions,
version,
)
site_dir = '/access-denied'
def setUp(self):
- super(InaccessibleSysPath, self).setUp()
+ super().setUp()
self.setUpPyfakefs()
self.fs.create_dir(self.site_dir, perm_bits=000)
class TestEntryPoints(unittest.TestCase):
def __init__(self, *args):
- super(TestEntryPoints, self).__init__(*args)
- self.ep = importlib.metadata.EntryPoint('name', 'value', 'group')
+ super().__init__(*args)
+ self.ep = importlib.metadata.EntryPoint(
+ name='name', value='value', group='group'
+ )
def test_entry_point_pickleable(self):
revived = pickle.loads(pickle.dumps(self.ep))
assert revived == self.ep
+ def test_positional_args(self):
+ """
+ Capture legacy (namedtuple) construction, discouraged.
+ """
+ EntryPoint('name', 'value', 'group')
+
def test_immutable(self):
"""EntryPoints should be immutable"""
with self.assertRaises(AttributeError):
# EntryPoint objects are sortable, but result is undefined.
sorted(
[
- EntryPoint('b', 'val', 'group'),
- EntryPoint('a', 'val', 'group'),
+ EntryPoint(name='b', value='val', group='group'),
+ EntryPoint(name='a', value='val', group='group'),
]
)
prefix=self.site_dir,
)
list(distributions())
+
+
+class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase):
+ def test_packages_distributions_example(self):
+ self._fixture_on_path('example-21.12-py3-none-any.whl')
+ assert packages_distributions()['example'] == ['example']
+
+ def test_packages_distributions_example2(self):
+ """
+ Test packages_distributions on a wheel built
+ by trampolim.
+ """
+ self._fixture_on_path('example2-1.0.0-py3-none-any.whl')
+ assert packages_distributions()['example2'] == ['example2']
+
+
+class PackagesDistributionsTest(
+ fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase
+):
+ def test_packages_distributions_neither_toplevel_nor_files(self):
+ """
+ Test a package built without 'top-level.txt' or a file list.
+ """
+ fixtures.build_files(
+ {
+ 'trim_example-1.0.0.dist-info': {
+ 'METADATA': """
+ Name: trim_example
+ Version: 1.0.0
+ """,
+ }
+ },
+ prefix=self.site_dir,
+ )
+ packages_distributions()
@contextlib.contextmanager
def suppress_known_deprecation():
with warnings.catch_warnings(record=True) as ctx:
- warnings.simplefilter('default')
+ warnings.simplefilter('default', category=DeprecationWarning)
yield ctx
for ep in entries
)
# ns:sub doesn't exist in alt_pkg
- assert 'ns:sub' not in entries
+ assert 'ns:sub' not in entries.names
def test_entry_points_missing_name(self):
with self.assertRaises(KeyError):
file.read_text()
def test_file_hash_repr(self):
- assertRegex = self.assertRegex
-
util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
- assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
+ self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
def test_files_dist_info(self):
self._test_files(files('distinfo-pkg'))
import sys
import unittest
-from contextlib import ExitStack
+from . import fixtures
from importlib.metadata import (
PackageNotFoundError,
distribution,
files,
version,
)
-from importlib import resources
-from test.support import requires_zlib
-
-
-@requires_zlib()
-class TestZip(unittest.TestCase):
- root = 'test.test_importlib.data'
-
- def _fixture_on_path(self, filename):
- pkg_file = resources.files(self.root).joinpath(filename)
- file = self.resources.enter_context(resources.as_file(pkg_file))
- assert file.name.startswith('example-'), file.name
- sys.path.insert(0, str(file))
- self.resources.callback(sys.path.pop, 0)
+class TestZip(fixtures.ZipFixtures, unittest.TestCase):
def setUp(self):
- # Find the path to the example-*.whl so we can add it to the front of
- # sys.path, where we'll then try to find the metadata thereof.
- self.resources = ExitStack()
- self.addCleanup(self.resources.close)
+ super().setUp()
self._fixture_on_path('example-21.12-py3-none-any.whl')
def test_zip_version(self):
assert len(dists) == 1
-@requires_zlib()
class TestEgg(TestZip):
def setUp(self):
- # Find the path to the example-*.egg so we can add it to the front of
- # sys.path, where we'll then try to find the metadata thereof.
- self.resources = ExitStack()
- self.addCleanup(self.resources.close)
+ super().setUp()
self._fixture_on_path('example-21.12-py3.6.egg')
def test_files(self):
--- /dev/null
+EntryPoint objects are no longer tuples. Recommended means to access is by
+attribute ('.name', '.group') or accessor ('.load()'). Access by index is
+deprecated and will raise deprecation warning.