Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 04deaee

Browse files
authored
bpo-44893: Implement EntryPoint as simple class with attributes. (GH-30150)
* 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.
1 parent 109d966 commit 04deaee

11 files changed

Lines changed: 269 additions & 108 deletions

File tree

Lib/importlib/metadata/__init__.py

Lines changed: 100 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@
1515
import collections
1616

1717
from . import _adapters, _meta
18-
from ._meta import PackageMetadata
1918
from ._collections import FreezableDefaultDict, Pair
20-
from ._functools import method_cache
21-
from ._itertools import unique_everseen
19+
from ._functools import method_cache, pass_none
20+
from ._itertools import always_iterable, unique_everseen
2221
from ._meta import PackageMetadata, SimplePath
2322

2423
from contextlib import suppress
@@ -121,8 +120,33 @@ def valid(line):
121120
return line and not line.startswith('#')
122121

123122

124-
class EntryPoint(
125-
collections.namedtuple('EntryPointBase', 'name value group')):
123+
class DeprecatedTuple:
124+
"""
125+
Provide subscript item access for backward compatibility.
126+
127+
>>> recwarn = getfixture('recwarn')
128+
>>> ep = EntryPoint(name='name', value='value', group='group')
129+
>>> ep[:]
130+
('name', 'value', 'group')
131+
>>> ep[0]
132+
'name'
133+
>>> len(recwarn)
134+
1
135+
"""
136+
137+
_warn = functools.partial(
138+
warnings.warn,
139+
"EntryPoint tuple interface is deprecated. Access members by name.",
140+
DeprecationWarning,
141+
stacklevel=2,
142+
)
143+
144+
def __getitem__(self, item):
145+
self._warn()
146+
return self._key()[item]
147+
148+
149+
class EntryPoint(DeprecatedTuple):
126150
"""An entry point as defined by Python packaging conventions.
127151
128152
See `the packaging docs on entry points
@@ -153,6 +177,9 @@ class EntryPoint(
153177

154178
dist: Optional['Distribution'] = None
155179

180+
def __init__(self, name, value, group):
181+
vars(self).update(name=name, value=value, group=group)
182+
156183
def load(self):
157184
"""Load the entry point from its definition. If only a module
158185
is indicated by the value, return that module. Otherwise,
@@ -179,7 +206,7 @@ def extras(self):
179206
return list(re.finditer(r'\w+', match.group('extras') or ''))
180207

181208
def _for(self, dist):
182-
self.dist = dist
209+
vars(self).update(dist=dist)
183210
return self
184211

185212
def __iter__(self):
@@ -193,16 +220,31 @@ def __iter__(self):
193220
warnings.warn(msg, DeprecationWarning)
194221
return iter((self.name, self))
195222

196-
def __reduce__(self):
197-
return (
198-
self.__class__,
199-
(self.name, self.value, self.group),
200-
)
201-
202223
def matches(self, **params):
203224
attrs = (getattr(self, param) for param in params)
204225
return all(map(operator.eq, params.values(), attrs))
205226

227+
def _key(self):
228+
return self.name, self.value, self.group
229+
230+
def __lt__(self, other):
231+
return self._key() < other._key()
232+
233+
def __eq__(self, other):
234+
return self._key() == other._key()
235+
236+
def __setattr__(self, name, value):
237+
raise AttributeError("EntryPoint objects are immutable.")
238+
239+
def __repr__(self):
240+
return (
241+
f'EntryPoint(name={self.name!r}, value={self.value!r}, '
242+
f'group={self.group!r})'
243+
)
244+
245+
def __hash__(self):
246+
return hash(self._key())
247+
206248

207249
class DeprecatedList(list):
208250
"""
@@ -243,52 +285,33 @@ class DeprecatedList(list):
243285
stacklevel=2,
244286
)
245287

246-
def __setitem__(self, *args, **kwargs):
247-
self._warn()
248-
return super().__setitem__(*args, **kwargs)
249-
250-
def __delitem__(self, *args, **kwargs):
251-
self._warn()
252-
return super().__delitem__(*args, **kwargs)
253-
254-
def append(self, *args, **kwargs):
255-
self._warn()
256-
return super().append(*args, **kwargs)
257-
258-
def reverse(self, *args, **kwargs):
259-
self._warn()
260-
return super().reverse(*args, **kwargs)
261-
262-
def extend(self, *args, **kwargs):
263-
self._warn()
264-
return super().extend(*args, **kwargs)
265-
266-
def pop(self, *args, **kwargs):
267-
self._warn()
268-
return super().pop(*args, **kwargs)
269-
270-
def remove(self, *args, **kwargs):
271-
self._warn()
272-
return super().remove(*args, **kwargs)
273-
274-
def __iadd__(self, *args, **kwargs):
275-
self._warn()
276-
return super().__iadd__(*args, **kwargs)
288+
def _wrap_deprecated_method(method_name: str): # type: ignore
289+
def wrapped(self, *args, **kwargs):
290+
self._warn()
291+
return getattr(super(), method_name)(*args, **kwargs)
292+
293+
return wrapped
294+
295+
for method_name in [
296+
'__setitem__',
297+
'__delitem__',
298+
'append',
299+
'reverse',
300+
'extend',
301+
'pop',
302+
'remove',
303+
'__iadd__',
304+
'insert',
305+
'sort',
306+
]:
307+
locals()[method_name] = _wrap_deprecated_method(method_name)
277308

278309
def __add__(self, other):
279310
if not isinstance(other, tuple):
280311
self._warn()
281312
other = tuple(other)
282313
return self.__class__(tuple(self) + other)
283314

284-
def insert(self, *args, **kwargs):
285-
self._warn()
286-
return super().insert(*args, **kwargs)
287-
288-
def sort(self, *args, **kwargs):
289-
self._warn()
290-
return super().sort(*args, **kwargs)
291-
292315
def __eq__(self, other):
293316
if not isinstance(other, tuple):
294317
self._warn()
@@ -333,7 +356,7 @@ def names(self):
333356
"""
334357
Return the set of all names of all entry points.
335358
"""
336-
return set(ep.name for ep in self)
359+
return {ep.name for ep in self}
337360

338361
@property
339362
def groups(self):
@@ -344,21 +367,17 @@ def groups(self):
344367
>>> EntryPoints().groups
345368
set()
346369
"""
347-
return set(ep.group for ep in self)
370+
return {ep.group for ep in self}
348371

349372
@classmethod
350373
def _from_text_for(cls, text, dist):
351374
return cls(ep._for(dist) for ep in cls._from_text(text))
352375

353-
@classmethod
354-
def _from_text(cls, text):
355-
return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
356-
357376
@staticmethod
358-
def _parse_groups(text):
377+
def _from_text(text):
359378
return (
360-
(item.value.name, item.value.value, item.name)
361-
for item in Sectioned.section_pairs(text)
379+
EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
380+
for item in Sectioned.section_pairs(text or '')
362381
)
363382

364383

@@ -611,7 +630,6 @@ def files(self):
611630
missing.
612631
Result may be empty if the metadata exists but is empty.
613632
"""
614-
file_lines = self._read_files_distinfo() or self._read_files_egginfo()
615633

616634
def make_file(name, hash=None, size_str=None):
617635
result = PackagePath(name)
@@ -620,7 +638,11 @@ def make_file(name, hash=None, size_str=None):
620638
result.dist = self
621639
return result
622640

623-
return file_lines and list(starmap(make_file, csv.reader(file_lines)))
641+
@pass_none
642+
def make_files(lines):
643+
return list(starmap(make_file, csv.reader(lines)))
644+
645+
return make_files(self._read_files_distinfo() or self._read_files_egginfo())
624646

625647
def _read_files_distinfo(self):
626648
"""
@@ -742,6 +764,9 @@ class FastPath:
742764
"""
743765
Micro-optimized class for searching a path for
744766
children.
767+
768+
>>> FastPath('').children()
769+
['...']
745770
"""
746771

747772
@functools.lru_cache() # type: ignore
@@ -1011,6 +1036,18 @@ def packages_distributions() -> Mapping[str, List[str]]:
10111036
"""
10121037
pkg_to_dist = collections.defaultdict(list)
10131038
for dist in distributions():
1014-
for pkg in (dist.read_text('top_level.txt') or '').split():
1039+
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
10151040
pkg_to_dist[pkg].append(dist.metadata['Name'])
10161041
return dict(pkg_to_dist)
1042+
1043+
1044+
def _top_level_declared(dist):
1045+
return (dist.read_text('top_level.txt') or '').split()
1046+
1047+
1048+
def _top_level_inferred(dist):
1049+
return {
1050+
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
1051+
for f in always_iterable(dist.files)
1052+
if f.suffix == ".py"
1053+
}

Lib/importlib/metadata/_functools.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,22 @@ def wrapper(self, *args, **kwargs):
8383
wrapper.cache_clear = lambda: None
8484

8585
return wrapper
86+
87+
88+
# From jaraco.functools 3.3
89+
def pass_none(func):
90+
"""
91+
Wrap func so it's not called if its first param is None
92+
93+
>>> print_text = pass_none(print)
94+
>>> print_text('text')
95+
text
96+
>>> print_text(None)
97+
"""
98+
99+
@functools.wraps(func)
100+
def wrapper(param, *args, **kwargs):
101+
if param is not None:
102+
return func(param, *args, **kwargs)
103+
104+
return wrapper

Lib/importlib/metadata/_itertools.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,57 @@ def unique_everseen(iterable, key=None):
1717
if k not in seen:
1818
seen_add(k)
1919
yield element
20+
21+
22+
# copied from more_itertools 8.8
23+
def always_iterable(obj, base_type=(str, bytes)):
24+
"""If *obj* is iterable, return an iterator over its items::
25+
26+
>>> obj = (1, 2, 3)
27+
>>> list(always_iterable(obj))
28+
[1, 2, 3]
29+
30+
If *obj* is not iterable, return a one-item iterable containing *obj*::
31+
32+
>>> obj = 1
33+
>>> list(always_iterable(obj))
34+
[1]
35+
36+
If *obj* is ``None``, return an empty iterable:
37+
38+
>>> obj = None
39+
>>> list(always_iterable(None))
40+
[]
41+
42+
By default, binary and text strings are not considered iterable::
43+
44+
>>> obj = 'foo'
45+
>>> list(always_iterable(obj))
46+
['foo']
47+
48+
If *base_type* is set, objects for which ``isinstance(obj, base_type)``
49+
returns ``True`` won't be considered iterable.
50+
51+
>>> obj = {'a': 1}
52+
>>> list(always_iterable(obj)) # Iterate over the dict's keys
53+
['a']
54+
>>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit
55+
[{'a': 1}]
56+
57+
Set *base_type* to ``None`` to avoid any special handling and treat objects
58+
Python considers iterable as iterable:
59+
60+
>>> obj = 'foo'
61+
>>> list(always_iterable(obj, base_type=None))
62+
['f', 'o', 'o']
63+
"""
64+
if obj is None:
65+
return iter(())
66+
67+
if (base_type is not None) and isinstance(obj, base_type):
68+
return iter((obj,))
69+
70+
try:
71+
return iter(obj)
72+
except TypeError:
73+
return iter((obj,))

Lib/importlib/metadata/_meta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class SimplePath(Protocol):
3737
def joinpath(self) -> 'SimplePath':
3838
... # pragma: no cover
3939

40-
def __div__(self) -> 'SimplePath':
40+
def __truediv__(self) -> 'SimplePath':
4141
... # pragma: no cover
4242

4343
def parent(self) -> 'SimplePath':

Lib/importlib/metadata/_text.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def __hash__(self):
8080
return hash(self.lower())
8181

8282
def __contains__(self, other):
83-
return super(FoldedCase, self).lower().__contains__(other.lower())
83+
return super().lower().__contains__(other.lower())
8484

8585
def in_(self, other):
8686
"Does self appear in other?"
@@ -89,7 +89,7 @@ def in_(self, other):
8989
# cache lower since it's likely to be called frequently.
9090
@method_cache
9191
def lower(self):
92-
return super(FoldedCase, self).lower()
92+
return super().lower()
9393

9494
def index(self, sub):
9595
return self.lower().index(sub.lower())
1.14 KB
Binary file not shown.

0 commit comments

Comments
 (0)