Skip to content

Commit 7722b0d

Browse files
committed
Merge branch 'master' into bugfix/94-bad-path
2 parents 19815ed + b22b8a7 commit 7722b0d

File tree

6 files changed

+101
-70
lines changed

6 files changed

+101
-70
lines changed

importlib_metadata/__init__.py

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import operator
1111
import functools
1212
import itertools
13+
import posixpath
1314
import collections
1415

1516
from ._compat import (
@@ -23,7 +24,6 @@
2324
NotADirectoryError,
2425
PermissionError,
2526
pathlib,
26-
PYPY_OPEN_BUG,
2727
ModuleNotFoundError,
2828
MetaPathFinder,
2929
email_message_from_string,
@@ -389,10 +389,6 @@ def path(self):
389389
"""
390390
return vars(self).get('path', sys.path)
391391

392-
@property
393-
def pattern(self):
394-
return '.*' if self.name is None else re.escape(self.name)
395-
396392
@abc.abstractmethod
397393
def find_distributions(self, context=Context()):
398394
"""
@@ -404,6 +400,73 @@ def find_distributions(self, context=Context()):
404400
"""
405401

406402

403+
class FastPath:
404+
"""
405+
Micro-optimized class for searching a path for
406+
children.
407+
"""
408+
409+
def __init__(self, root):
410+
self.root = root
411+
412+
def joinpath(self, child):
413+
return pathlib.Path(self.root, child)
414+
415+
def children(self):
416+
with suppress(Exception):
417+
return os.listdir(self.root or '')
418+
with suppress(Exception):
419+
return self.zip_children()
420+
return []
421+
422+
def zip_children(self):
423+
zip_path = zipp.Path(self.root)
424+
names = zip_path.root.namelist()
425+
self.joinpath = zip_path.joinpath
426+
427+
return (
428+
posixpath.split(child)[0]
429+
for child in names
430+
)
431+
432+
def is_egg(self, search):
433+
root_n_low = os.path.split(self.root)[1].lower()
434+
435+
return (
436+
root_n_low == search.normalized + '.egg'
437+
or root_n_low.startswith(search.prefix)
438+
and root_n_low.endswith('.egg'))
439+
440+
def search(self, name):
441+
for child in self.children():
442+
n_low = child.lower()
443+
if (n_low in name.exact_matches
444+
or n_low.startswith(name.prefix)
445+
and n_low.endswith(name.suffixes)
446+
# legacy case:
447+
or self.is_egg(name) and n_low == 'egg-info'):
448+
yield self.joinpath(child)
449+
450+
451+
class Prepared:
452+
"""
453+
A prepared search for metadata on a possibly-named package.
454+
"""
455+
normalized = ''
456+
prefix = ''
457+
suffixes = '.dist-info', '.egg-info'
458+
exact_matches = [''][:0]
459+
460+
def __init__(self, name):
461+
self.name = name
462+
if name is None:
463+
return
464+
self.normalized = name.lower().replace('-', '_')
465+
self.prefix = self.normalized + '-'
466+
self.exact_matches = [
467+
self.normalized + suffix for suffix in self.suffixes]
468+
469+
407470
@install
408471
class MetadataPathFinder(NullFinder, DistributionFinder):
409472
"""A degenerate finder for distribution packages on the file system.
@@ -421,45 +484,17 @@ def find_distributions(self, context=DistributionFinder.Context()):
421484
(or all names if ``None`` indicated) along the paths in the list
422485
of directories ``context.path``.
423486
"""
424-
found = self._search_paths(context.pattern, context.path)
487+
found = self._search_paths(context.name, context.path)
425488
return map(PathDistribution, found)
426489

427490
@classmethod
428-
def _search_paths(cls, pattern, paths):
491+
def _search_paths(cls, name, paths):
429492
"""Find metadata directories in paths heuristically."""
430493
return itertools.chain.from_iterable(
431-
cls._search_path(path, pattern)
432-
for path in map(cls._switch_path, paths)
494+
path.search(Prepared(name))
495+
for path in map(FastPath, paths)
433496
)
434497

435-
@staticmethod
436-
def _switch_path(path):
437-
if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch
438-
with suppress(Exception):
439-
return zipp.Path(path)
440-
return pathlib.Path(path)
441-
442-
@classmethod
443-
def _matches_info(cls, normalized, item):
444-
template = r'{pattern}(-.*)?\.(dist|egg)-info'
445-
manifest = template.format(pattern=normalized)
446-
return re.match(manifest, item.name, flags=re.IGNORECASE)
447-
448-
@classmethod
449-
def _matches_legacy(cls, normalized, item):
450-
template = r'{pattern}-.*\.egg[\\/]EGG-INFO'
451-
manifest = template.format(pattern=normalized)
452-
return re.search(manifest, str(item), flags=re.IGNORECASE)
453-
454-
@classmethod
455-
def _search_path(cls, root, pattern):
456-
if not root.is_dir():
457-
return ()
458-
normalized = pattern.replace('-', '_')
459-
return (item for item in root.iterdir()
460-
if cls._matches_info(normalized, item)
461-
or cls._matches_legacy(normalized, item))
462-
463498

464499
class PathDistribution(Distribution):
465500
def __init__(self, path):

importlib_metadata/_compat.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,6 @@ def py2_message_from_string(text): # nocoverpy3
111111
email.message_from_string
112112
)
113113

114-
# https://bitbucket.org/pypy/pypy/issues/3021/ioopen-directory-leaks-a-file-descriptor
115-
PYPY_OPEN_BUG = getattr(sys, 'pypy_version_info', (9, 9, 9))[:3] <= (7, 1, 1)
116-
117114

118115
class PyPy_repr:
119116
"""

importlib_metadata/docs/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
importlib_metadata NEWS
33
=========================
44

5+
v1.4.0
6+
======
7+
8+
* Through careful optimization, ``distribution()`` is
9+
3-4x faster. Thanks to Antony Lee for the
10+
contribution. Closes #95.
11+
512
v1.3.0
613
======
714

importlib_metadata/docs/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@
166166
# Example configuration for intersphinx: refer to the Python standard library.
167167
intersphinx_mapping = {
168168
'python': ('https://docs.python.org/3', None),
169+
'importlib_resources': (
170+
'https://importlib-resources.readthedocs.io/en/latest/', None
171+
),
169172
}
170173

171174

importlib_metadata/docs/index.rst

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
===============================
44

55
``importlib_metadata`` is a library which provides an API for accessing an
6-
installed package's `metadata`_, such as its entry points or its top-level
6+
installed package's metadata (see :pep:`566`), such as its entry points or its top-level
77
name. This functionality intends to replace most uses of ``pkg_resources``
8-
`entry point API`_ and `metadata API`_. Along with ``importlib.resources`` in
9-
`Python 3.7 and newer`_ (backported as `importlib_resources`_ for older
8+
`entry point API`_ and `metadata API`_. Along with :mod:`importlib.resources` in
9+
Python 3.7 and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older
1010
versions of Python), this can eliminate the need to use the older and less
1111
efficient ``pkg_resources`` package.
1212

1313
``importlib_metadata`` is a backport of Python 3.8's standard library
14-
`importlib.metadata`_ module for Python 2.7, and 3.4 through 3.7. Users of
14+
:doc:`importlib.metadata <library/importlib.metadata>` module for Python 2.7, and 3.4 through 3.7. Users of
1515
Python 3.8 and beyond are encouraged to use the standard library module.
1616
When imported on Python 3.8 and later, ``importlib_metadata`` replaces the
1717
DistributionFinder behavior from the stdlib, but leaves the API in tact.
@@ -46,9 +46,5 @@ Indices and tables
4646
* :ref:`search`
4747

4848

49-
.. _`metadata`: https://www.python.org/dev/peps/pep-0566/
5049
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
5150
.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
52-
.. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources
53-
.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html
54-
.. _`importlib.metadata`: https://docs.python.org/3/library/importlib.metadata.html

importlib_metadata/docs/using.rst

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
.. _using:
22

3-
==========================
4-
Using importlib_metadata
5-
==========================
3+
=================================
4+
Using :mod:`!importlib_metadata`
5+
=================================
66

77
``importlib_metadata`` is a library that provides for access to installed
88
package metadata. Built in part on Python's import system, this library
99
intends to replace similar functionality in the `entry point
1010
API`_ and `metadata API`_ of ``pkg_resources``. Along with
11-
``importlib.resources`` in `Python 3.7
12-
and newer`_ (backported as `importlib_resources`_ for older versions of
11+
:mod:`importlib.resources` in Python 3.7
12+
and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older versions of
1313
Python), this can eliminate the need to use the older and less efficient
1414
``pkg_resources`` package.
1515

1616
By "installed package" we generally mean a third-party package installed into
1717
Python's ``site-packages`` directory via tools such as `pip
1818
<https://pypi.org/project/pip/>`_. Specifically,
1919
it means a package with either a discoverable ``dist-info`` or ``egg-info``
20-
directory, and metadata defined by `PEP 566`_ or its older specifications.
20+
directory, and metadata defined by :pep:`566` or its older specifications.
2121
By default, package metadata can live on the file system or in zip archives on
22-
``sys.path``. Through an extension mechanism, the metadata can live almost
22+
:data:`sys.path`. Through an extension mechanism, the metadata can live almost
2323
anywhere.
2424

2525

@@ -127,7 +127,7 @@ Distribution files
127127
You can also get the full set of files contained within a distribution. The
128128
``files()`` function takes a distribution package name and returns all of the
129129
files installed by this distribution. Each file object returned is a
130-
``PackagePath``, a `pathlib.Path`_ derived object with additional ``dist``,
130+
``PackagePath``, a :class:`pathlib.Path` derived object with additional ``dist``,
131131
``size``, and ``hash`` properties as indicated by the metadata. For example::
132132

133133
>>> util = [p for p in files('wheel') if 'util.py' in str(p)][0]
@@ -196,18 +196,18 @@ instance::
196196
>>> d.metadata['License']
197197
'MIT'
198198

199-
The full set of available metadata is not described here. See `PEP 566
200-
<https://www.python.org/dev/peps/pep-0566/>`_ for additional details.
199+
The full set of available metadata is not described here. See :pep:`566`
200+
for additional details.
201201

202202

203203
Extending the search algorithm
204204
==============================
205205

206-
Because package metadata is not available through ``sys.path`` searches, or
206+
Because package metadata is not available through :data:`sys.path` searches, or
207207
package loaders directly, the metadata for a package is found through import
208208
system `finders`_. To find a distribution package's metadata,
209-
``importlib_metadata`` queries the list of `meta path finders`_ on
210-
`sys.meta_path`_.
209+
``importlib.metadata`` queries the list of :term:`meta path finders <meta path finder>` on
210+
:data:`sys.meta_path`.
211211

212212
By default ``importlib_metadata`` installs a finder for distribution packages
213213
found on the file system. This finder doesn't actually find any *packages*,
@@ -217,7 +217,7 @@ The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
217217
interface expected of finders by Python's import system.
218218
``importlib_metadata`` extends this protocol by looking for an optional
219219
``find_distributions`` callable on the finders from
220-
``sys.meta_path`` and presents this extended interface as the
220+
:data:`sys.meta_path` and presents this extended interface as the
221221
``DistributionFinder`` abstract base class, which defines this abstract
222222
method::
223223

@@ -240,20 +240,13 @@ a custom finder, return instances of this derived ``Distribution`` in the
240240

241241
.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
242242
.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
243-
.. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources
244-
.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html
245-
.. _`PEP 566`: https://www.python.org/dev/peps/pep-0566/
246243
.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders
247-
.. _`meta path finders`: https://docs.python.org/3/glossary.html#term-meta-path-finder
248-
.. _`sys.meta_path`: https://docs.python.org/3/library/sys.html#sys.meta_path
249-
.. _`pathlib.Path`: https://docs.python.org/3/library/pathlib.html#pathlib.Path
250244

251245

252246
.. rubric:: Footnotes
253247

254248
.. [#f1] Technically, the returned distribution metadata object is an
255-
`email.message.Message
256-
<https://docs.python.org/3/library/email.message.html#email.message.EmailMessage>`_
249+
:class:`email.message.EmailMessage`
257250
instance, but this is an implementation detail, and not part of the
258251
stable API. You should only use dictionary-like methods and syntax
259252
to access the metadata contents.

0 commit comments

Comments
 (0)