``sys.path``. Through an extension mechanism, the metadata can live almost
anywhere.
+.. note:: Although this package supports loading metadata from wheels
+ on ``sys.path``, that support is provisional and does not serve to
+ contravene the `PEP 427 directive
+ <https://www.python.org/dev/peps/pep-0427/#is-it-possible-to-import-python-code-directly-from-a-wheel-file>`_,
+ which states that relying on this format is discouraged, and use is
+ at your own risk.
+
Overview
========
:ref:`requirements`_.
-Distributions
-=============
-
-.. CAUTION:: The ``Distribution`` class described here may or may not end up
- in the final stable public API. Consider this class `provisional
- <https://www.python.org/dev/peps/pep-0411/>`_ until the 1.0
- release.
-
-While the above API is the most common and convenient usage, you can get all
-of that information from the ``Distribution`` class. A ``Distribution`` is an
-abstract object that represents the metadata for a Python package. You can
-get the ``Distribution`` instance::
-
- >>> from importlib_metadata import distribution
- >>> dist = distribution('wheel')
-
-Thus, an alternative way to get the version number is through the
-``Distribution`` instance::
-
- >>> dist.version
- '0.32.3'
-
-There are all kinds of additional metadata available on the ``Distribution``
-instance::
-
- >>> d.metadata['Requires-Python']
- '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
- >>> d.metadata['License']
- 'MIT'
-
-The full set of available metadata is not described here. See `PEP 566
-<https://www.python.org/dev/peps/pep-0566/>`_ for additional details.
-
-
Functional API
==============
["pytest (>=3.0.0) ; extra == 'test'"]
+Distributions
+=============
+
+While the above API is the most common and convenient usage, you can get all
+of that information from the ``Distribution`` class. A ``Distribution`` is an
+abstract object that represents the metadata for a Python package. You can
+get the ``Distribution`` instance::
+
+ >>> from importlib_metadata import distribution
+ >>> dist = distribution('wheel')
+
+Thus, an alternative way to get the version number is through the
+``Distribution`` instance::
+
+ >>> dist.version
+ '0.32.3'
+
+There are all kinds of additional metadata available on the ``Distribution``
+instance::
+
+ >>> d.metadata['Requires-Python']
+ '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
+ >>> d.metadata['License']
+ 'MIT'
+
+The full set of available metadata is not described here. See `PEP 566
+<https://www.python.org/dev/peps/pep-0566/>`_ for additional details.
+
Extending the search algorithm
==============================
import re
import sys
+import zipfile
import itertools
-from . import zipp
from .api import Distribution
from .abc import DistributionFinder
from contextlib import suppress
return None
-@install
-class MetadataPathFinder(NullFinder):
+class MetadataPathBaseFinder(NullFinder):
"""A degenerate finder for distribution packages on the file system.
This finder supplies only a find_distributions() method for versions
of Python that do not have a PathFinder find_distributions().
"""
- search_template = r'{pattern}(-.*)?\.(dist|egg)-info'
def find_distributions(self, name=None, path=None):
"""Return an iterable of all Distribution instances capable of
for path in map(Path, paths)
)
+ @classmethod
+ def _predicate(cls, pattern, root, item):
+ return re.match(pattern, str(item.name), flags=re.IGNORECASE)
+
@classmethod
def _search_path(cls, root, pattern):
if not root.is_dir():
return ()
normalized = pattern.replace('-', '_')
+ matcher = cls.search_template.format(pattern=normalized)
+ return (item for item in root.iterdir()
+ if cls._predicate(matcher, root, item))
+
+
+@install
+class MetadataPathFinder(MetadataPathBaseFinder):
+ search_template = r'{pattern}(-.*)?\.(dist|egg)-info'
+
+
+@install
+class MetadataPathEggInfoFileFinder(MetadataPathBaseFinder):
+ search_template = r'{pattern}(-.*)?\.egg-info'
+
+ @classmethod
+ def _predicate(cls, pattern, root, item):
return (
- item
- for item in root.iterdir()
- if item.is_dir()
- and re.match(
- cls.search_template.format(pattern=normalized),
- str(item.name),
- flags=re.IGNORECASE,
- )
- )
+ (root / item).is_file() and
+ re.match(pattern, str(item.name), flags=re.IGNORECASE))
class PathDistribution(Distribution):
self._path = path
def read_text(self, filename):
- with suppress(FileNotFoundError):
+ with suppress(FileNotFoundError, NotADirectoryError):
with self._path.joinpath(filename).open(encoding='utf-8') as fp:
return fp.read()
return None
class WheelDistribution(Distribution):
def __init__(self, archive):
- self._archive = zipp.Path(archive)
+ self._archive = zipfile.Path(archive)
name, version = archive.name.split('-')[0:2]
self._dist_info = '{}-{}.dist-info'.format(name, version)
The returned object will have keys that name the various bits of
metadata. See PEP 566 for details.
"""
- text = self.read_text('METADATA') or self.read_text('PKG-INFO')
+ text = (
+ self.read_text('METADATA')
+ or self.read_text('PKG-INFO')
+ # This last clause is here to support old egg-info files. Its
+ # effect is to just end up using the PathDistribution's self._path
+ # (which points to the egg-info file) attribute unchanged.
+ or self.read_text('')
+ )
return email.message_from_string(text)
@property
def _read_egg_info_reqs(self):
source = self.read_text('requires.txt')
- return self._deps_from_requires_text(source)
+ return source and self._deps_from_requires_text(source)
@classmethod
def _deps_from_requires_text(cls, source):
+++ /dev/null
-"""
->>> root = Path(getfixture('zipfile_abcde'))
->>> a, b = root.iterdir()
->>> a
-Path('abcde.zip', 'a.txt')
->>> b
-Path('abcde.zip', 'b/')
->>> b.name
-'b'
->>> c = b / 'c.txt'
->>> c
-Path('abcde.zip', 'b/c.txt')
->>> c.name
-'c.txt'
->>> c.read_text()
-'content of c'
->>> c.exists()
-True
->>> (b / 'missing.txt').exists()
-False
->>> str(c)
-'abcde.zip/b/c.txt'
-"""
-
-from __future__ import division
-
-import io
-import sys
-import posixpath
-import zipfile
-import operator
-import functools
-
-__metaclass__ = type
-
-
-class Path:
- __repr = '{self.__class__.__name__}({self.root.filename!r}, {self.at!r})'
-
- def __init__(self, root, at=''):
- self.root = root if isinstance(root, zipfile.ZipFile) \
- else zipfile.ZipFile(self._pathlib_compat(root))
- self.at = at
-
- @staticmethod
- def _pathlib_compat(path):
- """
- For path-like objects, convert to a filename for compatibility
- on Python 3.6.1 and earlier.
- """
- try:
- return path.__fspath__()
- except AttributeError:
- return str(path)
-
- @property
- def open(self):
- return functools.partial(self.root.open, self.at)
-
- @property
- def name(self):
- return posixpath.basename(self.at.rstrip('/'))
-
- def read_text(self, *args, **kwargs):
- with self.open() as strm:
- return io.TextIOWrapper(strm, *args, **kwargs).read()
-
- def read_bytes(self):
- with self.open() as strm:
- return strm.read()
-
- def _is_child(self, path):
- return posixpath.dirname(path.at.rstrip('/')) == self.at.rstrip('/')
-
- def _next(self, at):
- return Path(self.root, at)
-
- def is_dir(self):
- return not self.at or self.at.endswith('/')
-
- def is_file(self):
- return not self.is_dir()
-
- def exists(self):
- return self.at in self.root.namelist()
-
- def iterdir(self):
- if not self.is_dir():
- raise ValueError("Can't listdir a file")
- names = map(operator.attrgetter('filename'), self.root.infolist())
- subs = map(self._next, names)
- return filter(self._is_child, subs)
-
- def __str__(self):
- return posixpath.join(self.root.filename, self.at)
-
- def __repr__(self):
- return self.__repr.format(self=self)
-
- def __truediv__(self, add):
- add = self._pathlib_compat(add)
- next = posixpath.join(self.at, add)
- next_dir = posixpath.join(self.at, add, '')
- names = self.root.namelist()
- return self._next(
- next_dir if next not in names and next_dir in names else next
- )
-
- if sys.version_info < (3,):
- __div__ = __truediv__
\ No newline at end of file
build_files(EggInfoPkg.files, prefix=self.site_dir)
+class EggInfoFile(SiteDir):
+ files = {
+ "egginfo_file.egg-info": """
+ Metadata-Version: 1.0
+ Name: egginfo_file
+ Version: 0.1
+ Summary: An example package
+ Home-page: www.example.com
+ Author: Eric Haffa-Vee
+ Author-email: eric@example.coms
+ License: UNKNOWN
+ Description: UNKNOWN
+ Platform: UNKNOWN
+ """,
+ }
+
+ def setUp(self):
+ super(EggInfoFile, self).setUp()
+ build_files(EggInfoFile.files, prefix=self.site_dir)
+
+
class LocalPackage:
def setUp(self):
self.fixtures = ExitStack()
def build_files(file_defs, prefix=pathlib.Path()):
- """
- Build a set of files/directories, as described by the
- file_defs dictionary.
- Each key/value pair in the dictionary is interpreted as
- a filename/contents
- pair. If the contents value is a dictionary, a directory
- is created, and the
- dictionary interpreted as the files within it, recursively.
+ """Build a set of files/directories, as described by the
+
+ file_defs dictionary. Each key/value pair in the dictionary is
+ interpreted as a filename/contents pair. If the contents value is a
+ dictionary, a directory is created, and the dictionary interpreted
+ as the files within it, recursively.
+
For example:
+
{"README.txt": "A README file",
"foo": {
"__init__.py": "",
from . import fixtures
-class APITests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase):
+class APITests(
+ fixtures.EggInfoPkg,
+ fixtures.DistInfoPkg,
+ fixtures.EggInfoFile,
+ unittest.TestCase):
+
version_pattern = r'\d+\.\d+(\.\d)?'
def test_retrieves_version_of_self(self):
def test_files_egg_info(self):
self._test_files(importlib.metadata.files('egginfo-pkg'))
+ def test_version_egg_info_file(self):
+ version = importlib_metadata.version('egginfo-file')
+ self.assertEqual(version, '0.1')
+
+ def test_requires_egg_info_file(self):
+ requirements = importlib_metadata.requires('egginfo-file')
+ self.assertIsNone(requirements)
+
def test_requires(self):
deps = importlib.metadata.requires('egginfo-pkg')
assert any(