--- /dev/null
+image: quay.io/python-devs/ci-image
+
+stages:
+ - test
+ - qa
+ - docs
+ - codecov
+ - deploy
+
+qa:
+ script:
+ - tox -e qa
+
+tests:
+ script:
+ - tox -e py27,py35,py36,py37,py38
+
+coverage:
+ script:
+ - tox -e py27-cov,py35-cov,py36-cov,py37-cov,py38-cov
+ artifacts:
+ paths:
+ - coverage.xml
+
+benchmark:
+ script:
+ - tox -e perf
+
+diffcov:
+ script:
+ - tox -e py27-diffcov,py35-diffcov,py36-diffcov,py37-diffcov,py38-diffcov
+
+docs:
+ script:
+ - tox -e docs
+
+codecov:
+ stage: codecov
+ dependencies:
+ - coverage
+ script:
+ - codecov
+ when: on_success
+
+release:
+ stage: deploy
+ only:
+ - /^v\d+\.\d+(\.\d+)?([abc]\d*)?$/
+ script:
+ - tox -e release
--- /dev/null
+python:
+ version: 3
+ extra_requirements:
+ - docs
+ pip_install: true
--- /dev/null
+Copyright 2017-2019 Jason R. Coombs, Barry Warsaw
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
--- /dev/null
+include *.py MANIFEST.in LICENSE README.rst
+global-include *.txt *.rst *.ini *.cfg *.toml *.whl *.egg
+exclude .gitignore
+prune build
+prune .tox
--- /dev/null
+Metadata-Version: 2.1
+Name: importlib_metadata
+Version: 2.0.0
+Summary: Read metadata from Python packages
+Home-page: http://importlib-metadata.readthedocs.io/
+Author: Jason R. Coombs
+Author-email: jaraco@jaraco.com
+License: Apache Software License
+Description: =========================
+ ``importlib_metadata``
+ =========================
+
+ ``importlib_metadata`` is a library to access the metadata for a Python
+ package. It is intended to be ported to Python 3.8.
+
+
+ Usage
+ =====
+
+ See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+ for usage details.
+
+ `Finder authors
+ <https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
+ also add support for custom package installers. See the above documentation
+ for details.
+
+
+ Caveats
+ =======
+
+ This project primarily supports third-party packages installed by PyPA
+ tools (or other conforming packages). It does not support:
+
+ - Packages in the stdlib.
+ - Packages installed without metadata.
+
+ Project details
+ ===============
+
+ * Project home: https://gitlab.com/python-devs/importlib_metadata
+ * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues
+ * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git
+ * Documentation: http://importlib_metadata.readthedocs.io/
+
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 2
+Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
+Provides-Extra: testing
+Provides-Extra: docs
--- /dev/null
+=========================
+ ``importlib_metadata``
+=========================
+
+``importlib_metadata`` is a library to access the metadata for a Python
+package. It is intended to be ported to Python 3.8.
+
+
+Usage
+=====
+
+See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+for usage details.
+
+`Finder authors
+<https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
+also add support for custom package installers. See the above documentation
+for details.
+
+
+Caveats
+=======
+
+This project primarily supports third-party packages installed by PyPA
+tools (or other conforming packages). It does not support:
+
+- Packages in the stdlib.
+- Packages installed without metadata.
+
+Project details
+===============
+
+ * Project home: https://gitlab.com/python-devs/importlib_metadata
+ * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues
+ * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git
+ * Documentation: http://importlib_metadata.readthedocs.io/
--- /dev/null
+codecov:
+ token: 5eb1bc45-1b7f-43e6-8bc1-f2b02833dba9
--- /dev/null
+[run]
+branch = true
+parallel = true
+omit =
+ setup*
+ .tox/*/lib/python*
+ */tests/*.py
+ */testing/*.py
+ /usr/local/*
+ */mod.py
+plugins =
+ coverplug
+
+[report]
+exclude_lines =
+ pragma: nocover
+ raise NotImplementedError
+ raise AssertionError
+ assert\s
+ nocoverpy${PYV}
+
+[paths]
+source =
+ importlib_metadata
--- /dev/null
+"""Coverage plugin to add exclude lines based on the Python version."""
+
+import sys
+
+from coverage import CoveragePlugin
+
+
+class MyConfigPlugin(CoveragePlugin):
+ def configure(self, config):
+ opt_name = 'report:exclude_lines'
+ exclude_lines = config.get_option(opt_name)
+ # Python >= 3.6 has os.PathLike.
+ if sys.version_info >= (3, 6):
+ exclude_lines.append('pragma: >=36')
+ else:
+ exclude_lines.append('pragma: <=35')
+ config.set_option(opt_name, exclude_lines)
+
+
+def coverage_init(reg, options):
+ reg.add_configurer(MyConfigPlugin())
--- /dev/null
+Metadata-Version: 2.1
+Name: importlib-metadata
+Version: 2.0.0
+Summary: Read metadata from Python packages
+Home-page: http://importlib-metadata.readthedocs.io/
+Author: Jason R. Coombs
+Author-email: jaraco@jaraco.com
+License: Apache Software License
+Description: =========================
+ ``importlib_metadata``
+ =========================
+
+ ``importlib_metadata`` is a library to access the metadata for a Python
+ package. It is intended to be ported to Python 3.8.
+
+
+ Usage
+ =====
+
+ See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+ for usage details.
+
+ `Finder authors
+ <https://docs.python.org/3/reference/import.html#finders-and-loaders>`_ can
+ also add support for custom package installers. See the above documentation
+ for details.
+
+
+ Caveats
+ =======
+
+ This project primarily supports third-party packages installed by PyPA
+ tools (or other conforming packages). It does not support:
+
+ - Packages in the stdlib.
+ - Packages installed without metadata.
+
+ Project details
+ ===============
+
+ * Project home: https://gitlab.com/python-devs/importlib_metadata
+ * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues
+ * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git
+ * Documentation: http://importlib_metadata.readthedocs.io/
+
+Platform: UNKNOWN
+Classifier: Development Status :: 3 - Alpha
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 2
+Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
+Provides-Extra: testing
+Provides-Extra: docs
--- /dev/null
+.gitlab-ci.yml
+.readthedocs.yml
+LICENSE
+MANIFEST.in
+README.rst
+codecov.yml
+coverage.ini
+coverplug.py
+pyproject.toml
+setup.cfg
+setup.py
+tox.ini
+importlib_metadata/__init__.py
+importlib_metadata/_compat.py
+importlib_metadata.egg-info/PKG-INFO
+importlib_metadata.egg-info/SOURCES.txt
+importlib_metadata.egg-info/dependency_links.txt
+importlib_metadata.egg-info/requires.txt
+importlib_metadata.egg-info/top_level.txt
+importlib_metadata/docs/__init__.py
+importlib_metadata/docs/changelog.rst
+importlib_metadata/docs/conf.py
+importlib_metadata/docs/index.rst
+importlib_metadata/docs/using.rst
+importlib_metadata/tests/__init__.py
+importlib_metadata/tests/fixtures.py
+importlib_metadata/tests/test_api.py
+importlib_metadata/tests/test_integration.py
+importlib_metadata/tests/test_main.py
+importlib_metadata/tests/test_zip.py
+importlib_metadata/tests/data/__init__.py
+importlib_metadata/tests/data/example-21.12-py3-none-any.whl
+importlib_metadata/tests/data/example-21.12-py3.6.egg
+prepare/example/setup.py
+prepare/example/example/__init__.py
\ No newline at end of file
--- /dev/null
+zipp>=0.5
+
+[:python_version < "3"]
+pathlib2
+contextlib2
+configparser>=3.5
+
+[docs]
+sphinx
+rst.linker
+
+[testing]
+packaging
+pep517
+
+[testing:python_version < "3.9"]
+importlib_resources>=1.3
--- /dev/null
+importlib_metadata
--- /dev/null
+from __future__ import unicode_literals, absolute_import
+
+import io
+import os
+import re
+import abc
+import csv
+import sys
+import zipp
+import operator
+import functools
+import itertools
+import posixpath
+import collections
+
+from ._compat import (
+ install,
+ NullFinder,
+ ConfigParser,
+ suppress,
+ map,
+ FileNotFoundError,
+ IsADirectoryError,
+ NotADirectoryError,
+ PermissionError,
+ pathlib,
+ ModuleNotFoundError,
+ MetaPathFinder,
+ email_message_from_string,
+ PyPy_repr,
+ unique_ordered,
+ str,
+ )
+from importlib import import_module
+from itertools import starmap
+
+
+__metaclass__ = type
+
+
+__all__ = [
+ 'Distribution',
+ 'DistributionFinder',
+ 'PackageNotFoundError',
+ 'distribution',
+ 'distributions',
+ 'entry_points',
+ 'files',
+ 'metadata',
+ 'requires',
+ 'version',
+ ]
+
+
+class PackageNotFoundError(ModuleNotFoundError):
+ """The package was not found."""
+
+ def __str__(self):
+ tmpl = "No package metadata was found for {self.name}"
+ return tmpl.format(**locals())
+
+ @property
+ def name(self):
+ name, = self.args
+ return name
+
+
+class EntryPoint(
+ PyPy_repr,
+ collections.namedtuple('EntryPointBase', 'name value group')):
+ """An entry point as defined by Python packaging conventions.
+
+ See `the packaging docs on entry points
+ <https://packaging.python.org/specifications/entry-points/>`_
+ for more information.
+ """
+
+ pattern = re.compile(
+ r'(?P<module>[\w.]+)\s*'
+ r'(:\s*(?P<attr>[\w.]+))?\s*'
+ r'(?P<extras>\[.*\])?\s*$'
+ )
+ """
+ A regular expression describing the syntax for an entry point,
+ which might look like:
+
+ - module
+ - package.module
+ - package.module:attribute
+ - package.module:object.attribute
+ - package.module:attr [extra1, extra2]
+
+ Other combinations are possible as well.
+
+ The expression is lenient about whitespace around the ':',
+ following the attr, and following any extras.
+ """
+
+ def load(self):
+ """Load the entry point from its definition. If only a module
+ is indicated by the value, return that module. Otherwise,
+ return the named object.
+ """
+ match = self.pattern.match(self.value)
+ module = import_module(match.group('module'))
+ attrs = filter(None, (match.group('attr') or '').split('.'))
+ return functools.reduce(getattr, attrs, module)
+
+ @property
+ def module(self):
+ match = self.pattern.match(self.value)
+ return match.group('module')
+
+ @property
+ def attr(self):
+ match = self.pattern.match(self.value)
+ return match.group('attr')
+
+ @property
+ def extras(self):
+ match = self.pattern.match(self.value)
+ return list(re.finditer(r'\w+', match.group('extras') or ''))
+
+ @classmethod
+ def _from_config(cls, config):
+ return [
+ cls(name, value, group)
+ for group in config.sections()
+ for name, value in config.items(group)
+ ]
+
+ @classmethod
+ def _from_text(cls, text):
+ config = ConfigParser(delimiters='=')
+ # case sensitive: https://stackoverflow.com/q/1611799/812183
+ config.optionxform = str
+ try:
+ config.read_string(text)
+ except AttributeError: # pragma: nocover
+ # Python 2 has no read_string
+ config.readfp(io.StringIO(text))
+ return EntryPoint._from_config(config)
+
+ def __iter__(self):
+ """
+ Supply iter so one may construct dicts of EntryPoints easily.
+ """
+ return iter((self.name, self))
+
+ def __reduce__(self):
+ return (
+ self.__class__,
+ (self.name, self.value, self.group),
+ )
+
+
+class PackagePath(pathlib.PurePosixPath):
+ """A reference to a path in a package"""
+
+ def read_text(self, encoding='utf-8'):
+ with self.locate().open(encoding=encoding) as stream:
+ return stream.read()
+
+ def read_binary(self):
+ with self.locate().open('rb') as stream:
+ return stream.read()
+
+ def locate(self):
+ """Return a path-like object for this path"""
+ return self.dist.locate_file(self)
+
+
+class FileHash:
+ def __init__(self, spec):
+ self.mode, _, self.value = spec.partition('=')
+
+ def __repr__(self):
+ return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
+
+
+class Distribution:
+ """A Python distribution package."""
+
+ @abc.abstractmethod
+ def read_text(self, filename):
+ """Attempt to load metadata file given by the name.
+
+ :param filename: The name of the file in the distribution info.
+ :return: The text if found, otherwise None.
+ """
+
+ @abc.abstractmethod
+ def locate_file(self, path):
+ """
+ Given a path to a file in this distribution, return a path
+ to it.
+ """
+
+ @classmethod
+ def from_name(cls, name):
+ """Return the Distribution for the given package name.
+
+ :param name: The name of the distribution package to search for.
+ :return: The Distribution instance (or subclass thereof) for the named
+ package, if found.
+ :raises PackageNotFoundError: When the named package's distribution
+ metadata cannot be found.
+ """
+ for resolver in cls._discover_resolvers():
+ dists = resolver(DistributionFinder.Context(name=name))
+ dist = next(iter(dists), None)
+ if dist is not None:
+ return dist
+ else:
+ raise PackageNotFoundError(name)
+
+ @classmethod
+ def discover(cls, **kwargs):
+ """Return an iterable of Distribution objects for all packages.
+
+ Pass a ``context`` or pass keyword arguments for constructing
+ a context.
+
+ :context: A ``DistributionFinder.Context`` object.
+ :return: Iterable of Distribution objects for all packages.
+ """
+ context = kwargs.pop('context', None)
+ if context and kwargs:
+ raise ValueError("cannot accept context and kwargs")
+ context = context or DistributionFinder.Context(**kwargs)
+ return itertools.chain.from_iterable(
+ resolver(context)
+ for resolver in cls._discover_resolvers()
+ )
+
+ @staticmethod
+ def at(path):
+ """Return a Distribution for the indicated metadata path
+
+ :param path: a string or path-like object
+ :return: a concrete Distribution instance for the path
+ """
+ return PathDistribution(pathlib.Path(path))
+
+ @staticmethod
+ def _discover_resolvers():
+ """Search the meta_path for resolvers."""
+ declared = (
+ getattr(finder, 'find_distributions', None)
+ for finder in sys.meta_path
+ )
+ return filter(None, declared)
+
+ @classmethod
+ def _local(cls, root='.'):
+ from pep517 import build, meta
+ system = build.compat_system(root)
+ builder = functools.partial(
+ meta.build,
+ source_dir=root,
+ system=system,
+ )
+ return PathDistribution(zipp.Path(meta.build_as_zip(builder)))
+
+ @property
+ def metadata(self):
+ """Return the parsed metadata for this Distribution.
+
+ 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')
+ # 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 version(self):
+ """Return the 'Version' metadata for the distribution package."""
+ return self.metadata['Version']
+
+ @property
+ def entry_points(self):
+ return EntryPoint._from_text(self.read_text('entry_points.txt'))
+
+ @property
+ def files(self):
+ """Files in this distribution.
+
+ :return: List of PackagePath for this distribution or None
+
+ Result is `None` if the metadata file that enumerates files
+ (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
+ 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.hash = FileHash(hash) if hash else None
+ result.size = int(size_str) if size_str else None
+ result.dist = self
+ return result
+
+ return file_lines and list(starmap(make_file, csv.reader(file_lines)))
+
+ def _read_files_distinfo(self):
+ """
+ Read the lines of RECORD
+ """
+ text = self.read_text('RECORD')
+ return text and text.splitlines()
+
+ def _read_files_egginfo(self):
+ """
+ SOURCES.txt might contain literal commas, so wrap each line
+ in quotes.
+ """
+ text = self.read_text('SOURCES.txt')
+ return text and map('"{}"'.format, text.splitlines())
+
+ @property
+ def requires(self):
+ """Generated requirements specified for this Distribution"""
+ reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
+ return reqs and list(reqs)
+
+ def _read_dist_info_reqs(self):
+ return self.metadata.get_all('Requires-Dist')
+
+ def _read_egg_info_reqs(self):
+ source = self.read_text('requires.txt')
+ return source and self._deps_from_requires_text(source)
+
+ @classmethod
+ def _deps_from_requires_text(cls, source):
+ section_pairs = cls._read_sections(source.splitlines())
+ sections = {
+ section: list(map(operator.itemgetter('line'), results))
+ for section, results in
+ itertools.groupby(section_pairs, operator.itemgetter('section'))
+ }
+ return cls._convert_egg_info_reqs_to_simple_reqs(sections)
+
+ @staticmethod
+ def _read_sections(lines):
+ section = None
+ for line in filter(None, lines):
+ section_match = re.match(r'\[(.*)\]$', line)
+ if section_match:
+ section = section_match.group(1)
+ continue
+ yield locals()
+
+ @staticmethod
+ def _convert_egg_info_reqs_to_simple_reqs(sections):
+ """
+ Historically, setuptools would solicit and store 'extra'
+ requirements, including those with environment markers,
+ in separate sections. More modern tools expect each
+ dependency to be defined separately, with any relevant
+ extras and environment markers attached directly to that
+ requirement. This method converts the former to the
+ latter. See _test_deps_from_requires_text for an example.
+ """
+ def make_condition(name):
+ return name and 'extra == "{name}"'.format(name=name)
+
+ def parse_condition(section):
+ section = section or ''
+ extra, sep, markers = section.partition(':')
+ if extra and markers:
+ markers = '({markers})'.format(markers=markers)
+ conditions = list(filter(None, [markers, make_condition(extra)]))
+ return '; ' + ' and '.join(conditions) if conditions else ''
+
+ for section, deps in sections.items():
+ for dep in deps:
+ yield dep + parse_condition(section)
+
+
+class DistributionFinder(MetaPathFinder):
+ """
+ A MetaPathFinder capable of discovering installed distributions.
+ """
+
+ class Context:
+ """
+ Keyword arguments presented by the caller to
+ ``distributions()`` or ``Distribution.discover()``
+ to narrow the scope of a search for distributions
+ in all DistributionFinders.
+
+ Each DistributionFinder may expect any parameters
+ and should attempt to honor the canonical
+ parameters defined below when appropriate.
+ """
+
+ name = None
+ """
+ Specific name for which a distribution finder should match.
+ A name of ``None`` matches all distributions.
+ """
+
+ def __init__(self, **kwargs):
+ vars(self).update(kwargs)
+
+ @property
+ def path(self):
+ """
+ The path that a distribution finder should search.
+
+ Typically refers to Python package paths and defaults
+ to ``sys.path``.
+ """
+ return vars(self).get('path', sys.path)
+
+ @abc.abstractmethod
+ def find_distributions(self, context=Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching the ``context``,
+ a DistributionFinder.Context instance.
+ """
+
+
+class FastPath:
+ """
+ Micro-optimized class for searching a path for
+ children.
+ """
+
+ def __init__(self, root):
+ self.root = str(root)
+ self.base = os.path.basename(self.root).lower()
+
+ def joinpath(self, child):
+ return pathlib.Path(self.root, child)
+
+ def children(self):
+ with suppress(Exception):
+ return os.listdir(self.root or '')
+ with suppress(Exception):
+ return self.zip_children()
+ return []
+
+ def zip_children(self):
+ zip_path = zipp.Path(self.root)
+ names = zip_path.root.namelist()
+ self.joinpath = zip_path.joinpath
+
+ return unique_ordered(
+ child.split(posixpath.sep, 1)[0]
+ for child in names
+ )
+
+ def is_egg(self, search):
+ base = self.base
+ return (
+ base == search.versionless_egg_name
+ or base.startswith(search.prefix)
+ and base.endswith('.egg'))
+
+ def search(self, name):
+ for child in self.children():
+ n_low = child.lower()
+ if (n_low in name.exact_matches
+ or n_low.startswith(name.prefix)
+ and n_low.endswith(name.suffixes)
+ # legacy case:
+ or self.is_egg(name) and n_low == 'egg-info'):
+ yield self.joinpath(child)
+
+
+class Prepared:
+ """
+ A prepared search for metadata on a possibly-named package.
+ """
+ normalized = ''
+ prefix = ''
+ suffixes = '.dist-info', '.egg-info'
+ exact_matches = [''][:0]
+ versionless_egg_name = ''
+
+ def __init__(self, name):
+ self.name = name
+ if name is None:
+ return
+ self.normalized = name.lower().replace('-', '_')
+ self.prefix = self.normalized + '-'
+ self.exact_matches = [
+ self.normalized + suffix for suffix in self.suffixes]
+ self.versionless_egg_name = self.normalized + '.egg'
+
+
+@install
+class MetadataPathFinder(NullFinder, DistributionFinder):
+ """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().
+ """
+
+ def find_distributions(self, context=DistributionFinder.Context()):
+ """
+ Find distributions.
+
+ Return an iterable of all Distribution instances capable of
+ loading the metadata for packages matching ``context.name``
+ (or all names if ``None`` indicated) along the paths in the list
+ of directories ``context.path``.
+ """
+ found = self._search_paths(context.name, context.path)
+ return map(PathDistribution, found)
+
+ @classmethod
+ def _search_paths(cls, name, paths):
+ """Find metadata directories in paths heuristically."""
+ return itertools.chain.from_iterable(
+ path.search(Prepared(name))
+ for path in map(FastPath, paths)
+ )
+
+
+class PathDistribution(Distribution):
+ def __init__(self, path):
+ """Construct a distribution from a path to the metadata directory.
+
+ :param path: A pathlib.Path or similar object supporting
+ .joinpath(), __div__, .parent, and .read_text().
+ """
+ self._path = path
+
+ def read_text(self, filename):
+ with suppress(FileNotFoundError, IsADirectoryError, KeyError,
+ NotADirectoryError, PermissionError):
+ return self._path.joinpath(filename).read_text(encoding='utf-8')
+ read_text.__doc__ = Distribution.read_text.__doc__
+
+ def locate_file(self, path):
+ return self._path.parent / path
+
+
+def distribution(distribution_name):
+ """Get the ``Distribution`` instance for the named package.
+
+ :param distribution_name: The name of the distribution package as a string.
+ :return: A ``Distribution`` instance (or subclass thereof).
+ """
+ return Distribution.from_name(distribution_name)
+
+
+def distributions(**kwargs):
+ """Get all ``Distribution`` instances in the current environment.
+
+ :return: An iterable of ``Distribution`` instances.
+ """
+ return Distribution.discover(**kwargs)
+
+
+def metadata(distribution_name):
+ """Get the metadata for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: An email.Message containing the parsed metadata.
+ """
+ return Distribution.from_name(distribution_name).metadata
+
+
+def version(distribution_name):
+ """Get the version string for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: The version string for the package as defined in the package's
+ "Version" metadata key.
+ """
+ return distribution(distribution_name).version
+
+
+def entry_points():
+ """Return EntryPoint objects for all installed packages.
+
+ :return: EntryPoint objects for all installed packages.
+ """
+ eps = itertools.chain.from_iterable(
+ dist.entry_points for dist in distributions())
+ by_group = operator.attrgetter('group')
+ ordered = sorted(eps, key=by_group)
+ grouped = itertools.groupby(ordered, by_group)
+ return {
+ group: tuple(eps)
+ for group, eps in grouped
+ }
+
+
+def files(distribution_name):
+ """Return a list of files for the named package.
+
+ :param distribution_name: The name of the distribution package to query.
+ :return: List of files composing the distribution.
+ """
+ return distribution(distribution_name).files
+
+
+def requires(distribution_name):
+ """
+ Return a list of requirements for the named package.
+
+ :return: An iterator of requirements, suitable for
+ packaging.requirement.Requirement.
+ """
+ return distribution(distribution_name).requires
--- /dev/null
+from __future__ import absolute_import, unicode_literals
+
+import io
+import abc
+import sys
+import email
+
+
+if sys.version_info > (3,): # pragma: nocover
+ import builtins
+ from configparser import ConfigParser
+ import contextlib
+ FileNotFoundError = builtins.FileNotFoundError
+ IsADirectoryError = builtins.IsADirectoryError
+ NotADirectoryError = builtins.NotADirectoryError
+ PermissionError = builtins.PermissionError
+ map = builtins.map
+ from itertools import filterfalse
+else: # pragma: nocover
+ from backports.configparser import ConfigParser
+ from itertools import imap as map # type: ignore
+ from itertools import ifilterfalse as filterfalse
+ import contextlib2 as contextlib
+ FileNotFoundError = IOError, OSError
+ IsADirectoryError = IOError, OSError
+ NotADirectoryError = IOError, OSError
+ PermissionError = IOError, OSError
+
+str = type('')
+
+suppress = contextlib.suppress
+
+if sys.version_info > (3, 5): # pragma: nocover
+ import pathlib
+else: # pragma: nocover
+ import pathlib2 as pathlib
+
+try:
+ ModuleNotFoundError = builtins.FileNotFoundError
+except (NameError, AttributeError): # pragma: nocover
+ ModuleNotFoundError = ImportError # type: ignore
+
+
+if sys.version_info >= (3,): # pragma: nocover
+ from importlib.abc import MetaPathFinder
+else: # pragma: nocover
+ class MetaPathFinder(object):
+ __metaclass__ = abc.ABCMeta
+
+
+__metaclass__ = type
+__all__ = [
+ 'install', 'NullFinder', 'MetaPathFinder', 'ModuleNotFoundError',
+ 'pathlib', 'ConfigParser', 'map', 'suppress', 'FileNotFoundError',
+ 'NotADirectoryError', 'email_message_from_string',
+ ]
+
+
+def install(cls):
+ """
+ Class decorator for installation on sys.meta_path.
+
+ Adds the backport DistributionFinder to sys.meta_path and
+ attempts to disable the finder functionality of the stdlib
+ DistributionFinder.
+ """
+ sys.meta_path.append(cls())
+ disable_stdlib_finder()
+ return cls
+
+
+def disable_stdlib_finder():
+ """
+ Give the backport primacy for discovering path-based distributions
+ by monkey-patching the stdlib O_O.
+
+ See #91 for more background for rationale on this sketchy
+ behavior.
+ """
+ def matches(finder):
+ return (
+ getattr(finder, '__module__', None) == '_frozen_importlib_external'
+ and hasattr(finder, 'find_distributions')
+ )
+ for finder in filter(matches, sys.meta_path): # pragma: nocover
+ del finder.find_distributions
+
+
+class NullFinder:
+ """
+ A "Finder" (aka "MetaClassFinder") that never finds any modules,
+ but may find distributions.
+ """
+ @staticmethod
+ def find_spec(*args, **kwargs):
+ return None
+
+ # In Python 2, the import system requires finders
+ # to have a find_module() method, but this usage
+ # is deprecated in Python 3 in favor of find_spec().
+ # For the purposes of this finder (i.e. being present
+ # on sys.meta_path but having no other import
+ # system functionality), the two methods are identical.
+ find_module = find_spec
+
+
+def py2_message_from_string(text): # nocoverpy3
+ # Work around https://bugs.python.org/issue25545 where
+ # email.message_from_string cannot handle Unicode on Python 2.
+ io_buffer = io.StringIO(text)
+ return email.message_from_file(io_buffer)
+
+
+email_message_from_string = (
+ py2_message_from_string
+ if sys.version_info < (3,) else
+ email.message_from_string
+ )
+
+
+class PyPy_repr:
+ """
+ Override repr for EntryPoint objects on PyPy to avoid __iter__ access.
+ Ref #97, #102.
+ """
+ affected = hasattr(sys, 'pypy_version_info')
+
+ def __compat_repr__(self): # pragma: nocover
+ def make_param(name):
+ value = getattr(self, name)
+ return '{name}={value!r}'.format(**locals())
+ params = ', '.join(map(make_param, self._fields))
+ return 'EntryPoint({params})'.format(**locals())
+
+ if affected: # pragma: nocover
+ __repr__ = __compat_repr__
+ del affected
+
+
+# from itertools recipes
+def unique_everseen(iterable): # pragma: nocover
+ "List unique elements, preserving order. Remember all elements ever seen."
+ seen = set()
+ seen_add = seen.add
+
+ for element in filterfalse(seen.__contains__, iterable):
+ seen_add(element)
+ yield element
+
+
+unique_ordered = (
+ unique_everseen if sys.version_info < (3, 7) else dict.fromkeys)
--- /dev/null
+=========================
+ importlib_metadata NEWS
+=========================
+
+v2.0.0
+======
+
+* ``importlib_metadata`` no longer presents a
+ ``__version__`` attribute. Consumers wishing to
+ resolve the version of the package should query it
+ directly with
+ ``importlib_metadata.version('importlib-metadata')``.
+ Closes #71.
+
+v1.7.0
+======
+
+* ``PathNotFoundError`` now has a custom ``__str__``
+ mentioning "package metadata" being missing to help
+ guide users to the cause when the package is installed
+ but no metadata is present. Closes #124.
+
+v1.6.1
+======
+
+* Added ``Distribution._local()`` as a provisional
+ demonstration of how to load metadata for a local
+ package. Implicitly requires that
+ `pep517 <https://pypi.org/project/pep517>`_ is
+ installed. Ref #42.
+* Ensure inputs to FastPath are Unicode. Closes #121.
+* Tests now rely on ``importlib.resources.files`` (and
+ backport) instead of the older ``path`` function.
+* Support any iterable from ``find_distributions``.
+ Closes #122.
+
+v1.6.0
+======
+
+* Added ``module`` and ``attr`` attributes to ``EntryPoint``
+
+v1.5.2
+======
+
+* Fix redundant entries from ``FastPath.zip_children``.
+ Closes #117.
+
+v1.5.1
+======
+
+* Improve reliability and consistency of compatibility
+ imports for contextlib and pathlib when running tests.
+ Closes #116.
+
+v1.5.0
+======
+
+* Additional performance optimizations in FastPath now
+ saves an additional 20% on a typical call.
+* Correct for issue where PyOxidizer finder has no
+ ``__module__`` attribute. Closes #110.
+
+v1.4.0
+======
+
+* Through careful optimization, ``distribution()`` is
+ 3-4x faster. Thanks to Antony Lee for the
+ contribution. Closes #95.
+
+* When searching through ``sys.path``, if any error
+ occurs attempting to list a path entry, that entry
+ is skipped, making the system much more lenient
+ to errors. Closes #94.
+
+v1.3.0
+======
+
+* Improve custom finders documentation. Closes #105.
+
+v1.2.0
+======
+
+* Once again, drop support for Python 3.4. Ref #104.
+
+v1.1.3
+======
+
+* Restored support for Python 3.4 due to improper version
+ compatibility declarations in the v1.1.0 and v1.1.1
+ releases. Closes #104.
+
+v1.1.2
+======
+
+* Repaired project metadata to correctly declare the
+ ``python_requires`` directive. Closes #103.
+
+v1.1.1
+======
+
+* Fixed ``repr(EntryPoint)`` on PyPy 3 also. Closes #102.
+
+v1.1.0
+======
+
+* Dropped support for Python 3.4.
+* EntryPoints are now pickleable. Closes #96.
+* Fixed ``repr(EntryPoint)`` on PyPy 2. Closes #97.
+
+v1.0.0
+======
+
+* Project adopts semver for versioning.
+
+* Removed compatibility shim introduced in 0.23.
+
+* For better compatibility with the stdlib implementation and to
+ avoid the same distributions being discovered by the stdlib and
+ backport implementations, the backport now disables the
+ stdlib DistributionFinder during initialization (import time).
+ Closes #91 and closes #100.
+
+0.23
+====
+* Added a compatibility shim to prevent failures on beta releases
+ of Python before the signature changed to accept the
+ "context" parameter on find_distributions. This workaround
+ will have a limited lifespan, not to extend beyond release of
+ Python 3.8 final.
+
+0.22
+====
+* Renamed ``package`` parameter to ``distribution_name``
+ as `recommended <https://bugs.python.org/issue34632#msg349423>`_
+ in the following functions: ``distribution``, ``metadata``,
+ ``version``, ``files``, and ``requires``. This
+ backward-incompatible change is expected to have little impact
+ as these functions are assumed to be primarily used with
+ positional parameters.
+
+0.21
+====
+* ``importlib.metadata`` now exposes the ``DistributionFinder``
+ metaclass and references it in the docs for extending the
+ search algorithm.
+* Add ``Distribution.at`` for constructing a Distribution object
+ from a known metadata directory on the file system. Closes #80.
+* Distribution finders now receive a context object that
+ supplies ``.path`` and ``.name`` properties. This change
+ introduces a fundamental backward incompatibility for
+ any projects implementing a ``find_distributions`` method
+ on a ``MetaPathFinder``. This new layer of abstraction
+ allows this context to be supplied directly or constructed
+ on demand and opens the opportunity for a
+ ``find_distributions`` method to solicit additional
+ context from the caller. Closes #85.
+
+0.20
+====
+* Clarify in the docs that calls to ``.files`` could return
+ ``None`` when the metadata is not present. Closes #69.
+* Return all requirements and not just the first for dist-info
+ packages. Closes #67.
+
+0.19
+====
+* Restrain over-eager egg metadata resolution.
+* Add support for entry points with colons in the name. Closes #75.
+
+0.18
+====
+* Parse entry points case sensitively. Closes #68
+* Add a version constraint on the backport configparser package. Closes #66
+
+0.17
+====
+* Fix a permission problem in the tests on Windows.
+
+0.16
+====
+* Don't crash if there exists an EGG-INFO directory on sys.path.
+
+0.15
+====
+* Fix documentation.
+
+0.14
+====
+* Removed ``local_distribution`` function from the API.
+ **This backward-incompatible change removes this
+ behavior summarily**. Projects should remove their
+ reliance on this behavior. A replacement behavior is
+ under review in the `pep517 project
+ <https://github.com/pypa/pep517>`_. Closes #42.
+
+0.13
+====
+* Update docstrings to match PEP 8. Closes #63.
+* Merged modules into one module. Closes #62.
+
+0.12
+====
+* Add support for eggs. !65; Closes #19.
+
+0.11
+====
+* Support generic zip files (not just wheels). Closes #59
+* Support zip files with multiple distributions in them. Closes #60
+* Fully expose the public API in ``importlib_metadata.__all__``.
+
+0.10
+====
+* The ``Distribution`` ABC is now officially part of the public API.
+ Closes #37.
+* Fixed support for older single file egg-info formats. Closes #43.
+* Fixed a testing bug when ``$CWD`` has spaces in the path. Closes #50.
+* Add Python 3.8 to the ``tox`` testing matrix.
+
+0.9
+===
+* Fixed issue where entry points without an attribute would raise an
+ Exception. Closes #40.
+* Removed unused ``name`` parameter from ``entry_points()``. Closes #44.
+* ``DistributionFinder`` classes must now be instantiated before
+ being placed on ``sys.meta_path``.
+
+0.8
+===
+* This library can now discover/enumerate all installed packages. **This
+ backward-incompatible change alters the protocol finders must
+ implement to support distribution package discovery.** Closes #24.
+* The signature of ``find_distributions()`` on custom installer finders
+ should now accept two parameters, ``name`` and ``path`` and
+ these parameters must supply defaults.
+* The ``entry_points()`` method no longer accepts a package name
+ but instead returns all entry points in a dictionary keyed by the
+ ``EntryPoint.group``. The ``resolve`` method has been removed. Instead,
+ call ``EntryPoint.load()``, which has the same semantics as
+ ``pkg_resources`` and ``entrypoints``. **This is a backward incompatible
+ change.**
+* Metadata is now always returned as Unicode text regardless of
+ Python version. Closes #29.
+* This library can now discover metadata for a 'local' package (found
+ in the current-working directory). Closes #27.
+* Added ``files()`` function for resolving files from a distribution.
+* Added a new ``requires()`` function, which returns the requirements
+ for a package suitable for parsing by
+ ``packaging.requirements.Requirement``. Closes #18.
+* The top-level ``read_text()`` function has been removed. Use
+ ``PackagePath.read_text()`` on instances returned by the ``files()``
+ function. **This is a backward incompatible change.**
+* Release dates are now automatically injected into the changelog
+ based on SCM tags.
+
+0.7
+===
+* Fixed issue where packages with dashes in their names would
+ not be discovered. Closes #21.
+* Distribution lookup is now case-insensitive. Closes #20.
+* Wheel distributions can no longer be discovered by their module
+ name. Like Path distributions, they must be indicated by their
+ distribution package name.
+
+0.6
+===
+* Removed ``importlib_metadata.distribution`` function. Now
+ the public interface is primarily the utility functions exposed
+ in ``importlib_metadata.__all__``. Closes #14.
+* Added two new utility functions ``read_text`` and
+ ``metadata``.
+
+0.5
+===
+* Updated README and removed details about Distribution
+ class, now considered private. Closes #15.
+* Added test suite support for Python 3.4+.
+* Fixed SyntaxErrors on Python 3.4 and 3.5. !12
+* Fixed errors on Windows joining Path elements. !15
+
+0.4
+===
+* Housekeeping.
+
+0.3
+===
+* Added usage documentation. Closes #8
+* Add support for getting metadata from wheels on ``sys.path``. Closes #9
+
+0.2
+===
+* Added ``importlib_metadata.entry_points()``. Closes #1
+* Added ``importlib_metadata.resolve()``. Closes #12
+* Add support for Python 2.7. Closes #4
+
+0.1
+===
+* Initial release.
+
+
+..
+ Local Variables:
+ mode: change-log-mode
+ indent-tabs-mode: nil
+ sentence-end-double-space: t
+ fill-column: 78
+ coding: utf-8
+ End:
--- /dev/null
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# importlib_metadata documentation build configuration file, created by
+# sphinx-quickstart on Thu Nov 30 10:21:00 2017.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'rst.linker',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.viewcode',
+ ]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = 'importlib_metadata'
+copyright = '2017-2019, Jason R. Coombs, Barry Warsaw'
+author = 'Jason R. Coombs, Barry Warsaw'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'default'
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# This is required for the alabaster theme
+# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
+html_sidebars = {
+ '**': [
+ 'relations.html', # needs 'show_related': True theme option to display
+ 'searchbox.html',
+ ]
+ }
+
+
+# -- Options for HTMLHelp output ------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'importlib_metadatadoc'
+
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+ }
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'importlib_metadata.tex',
+ 'importlib\\_metadata Documentation',
+ 'Brett Cannon, Barry Warsaw', 'manual'),
+ ]
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'importlib_metadata', 'importlib_metadata Documentation',
+ [author], 1)
+ ]
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'importlib_metadata', 'importlib_metadata Documentation',
+ author, 'importlib_metadata', 'One line description of project.',
+ 'Miscellaneous'),
+ ]
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3', None),
+ 'importlib_resources': (
+ 'https://importlib-resources.readthedocs.io/en/latest/', None
+ ),
+ }
+
+
+# For rst.linker, inject release dates into changelog.rst
+link_files = {
+ 'changelog.rst': dict(
+ replace=[
+ dict(
+ pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n',
+ with_scm='{text}\n{rev[timestamp]:%Y-%m-%d}\n\n',
+ ),
+ ],
+ ),
+ }
--- /dev/null
+===============================
+ Welcome to importlib_metadata
+===============================
+
+``importlib_metadata`` is a library which provides an API for accessing an
+installed package's metadata (see :pep:`566`), such as its entry points or its top-level
+name. This functionality intends to replace most uses of ``pkg_resources``
+`entry point API`_ and `metadata API`_. Along with :mod:`importlib.resources` in
+Python 3.7 and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older
+versions of Python), this can eliminate the need to use the older and less
+efficient ``pkg_resources`` package.
+
+``importlib_metadata`` is a backport of Python 3.8's standard library
+:doc:`importlib.metadata <library/importlib.metadata>` module for Python 2.7, and 3.4 through 3.7. Users of
+Python 3.8 and beyond are encouraged to use the standard library module.
+When imported on Python 3.8 and later, ``importlib_metadata`` replaces the
+DistributionFinder behavior from the stdlib, but leaves the API in tact.
+Developers looking for detailed API descriptions should refer to the Python
+3.8 standard library documentation.
+
+The documentation here includes a general :ref:`usage <using>` guide.
+
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ using.rst
+ changelog (links).rst
+
+
+Project details
+===============
+
+ * Project home: https://gitlab.com/python-devs/importlib_metadata
+ * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues
+ * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git
+ * Documentation: http://importlib_metadata.readthedocs.io/
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
+
+.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
+.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
--- /dev/null
+.. _using:
+
+=================================
+ Using :mod:`!importlib_metadata`
+=================================
+
+``importlib_metadata`` is a library that provides for access to installed
+package metadata. Built in part on Python's import system, this library
+intends to replace similar functionality in the `entry point
+API`_ and `metadata API`_ of ``pkg_resources``. Along with
+:mod:`importlib.resources` in Python 3.7
+and newer (backported as :doc:`importlib_resources <importlib_resources:index>` for older versions of
+Python), this can eliminate the need to use the older and less efficient
+``pkg_resources`` package.
+
+By "installed package" we generally mean a third-party package installed into
+Python's ``site-packages`` directory via tools such as `pip
+<https://pypi.org/project/pip/>`_. Specifically,
+it means a package with either a discoverable ``dist-info`` or ``egg-info``
+directory, and metadata defined by :pep:`566` or its older specifications.
+By default, package metadata can live on the file system or in zip archives on
+:data:`sys.path`. Through an extension mechanism, the metadata can live almost
+anywhere.
+
+
+Overview
+========
+
+Let's say you wanted to get the version string for a package you've installed
+using ``pip``. We start by creating a virtual environment and installing
+something into it::
+
+ $ python3 -m venv example
+ $ source example/bin/activate
+ (example) $ pip install importlib_metadata
+ (example) $ pip install wheel
+
+You can get the version string for ``wheel`` by running the following::
+
+ (example) $ python
+ >>> from importlib_metadata import version
+ >>> version('wheel')
+ '0.32.3'
+
+You can also get the set of entry points keyed by group, such as
+``console_scripts``, ``distutils.commands`` and others. Each group contains a
+sequence of :ref:`EntryPoint <entry-points>` objects.
+
+You can get the :ref:`metadata for a distribution <metadata>`::
+
+ >>> list(metadata('wheel'))
+ ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist']
+
+You can also get a :ref:`distribution's version number <version>`, list its
+:ref:`constituent files <files>`, and get a list of the distribution's
+:ref:`requirements`.
+
+
+Functional API
+==============
+
+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,
+keyed by group. Entry points are represented by ``EntryPoint`` instances;
+each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and
+a ``.load()`` method to resolve the value. There are also ``.module``,
+``.attr``, and ``.extras`` attributes for getting the components of the
+``.value`` attribute::
+
+ >>> eps = entry_points()
+ >>> list(eps)
+ ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation']
+ >>> scripts = eps['console_scripts']
+ >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0]
+ >>> wheel
+ EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts')
+ >>> wheel.module
+ 'wheel.cli'
+ >>> wheel.attr
+ 'main'
+ >>> wheel.extras
+ []
+ >>> main = wheel.load()
+ >>> main
+ <function main at 0x103528488>
+
+The ``group`` and ``name`` are arbitrary values defined by the package author
+and usually a client will wish to resolve all entry points for a particular
+group. Read `the setuptools docs
+<https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`_
+for more information on entry points, their definition, and usage.
+
+
+.. _metadata:
+
+Distribution metadata
+---------------------
+
+Every distribution includes some metadata, which you can extract using the
+``metadata()`` function::
+
+ >>> wheel_metadata = metadata('wheel')
+
+The keys of the returned data structure [#f1]_ name the metadata keywords, and
+their values are returned unparsed from the distribution metadata::
+
+ >>> wheel_metadata['Requires-Python']
+ '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
+
+
+.. _version:
+
+Distribution versions
+---------------------
+
+The ``version()`` function is the quickest way to get a distribution's version
+number, as a string::
+
+ >>> version('wheel')
+ '0.32.3'
+
+
+.. _files:
+
+Distribution files
+------------------
+
+You can also get the full set of files contained within a distribution. The
+``files()`` function takes a distribution package name and returns all of the
+files installed by this distribution. Each file object returned is a
+``PackagePath``, a :class:`pathlib.Path` derived object with additional ``dist``,
+``size``, and ``hash`` properties as indicated by the metadata. For example::
+
+ >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0]
+ >>> util
+ PackagePath('wheel/util.py')
+ >>> util.size
+ 859
+ >>> util.dist
+ <importlib_metadata._hooks.PathDistribution object at 0x101e0cef0>
+ >>> util.hash
+ <FileHash mode: sha256 value: bYkw5oMccfazVCoYQwKkkemoVyMAFoR34mmKBx8R1NI>
+
+Once you have the file, you can also read its contents::
+
+ >>> print(util.read_text())
+ import base64
+ import sys
+ ...
+ def as_bytes(s):
+ if isinstance(s, text_type):
+ return s.encode('utf-8')
+ return s
+
+In the case where the metadata file listing files
+(RECORD or SOURCES.txt) is missing, ``files()`` will
+return ``None``. The caller may wish to wrap calls to
+``files()`` in `always_iterable
+<https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.always_iterable>`_
+or otherwise guard against this condition if the target
+distribution is not known to have the metadata present.
+
+.. _requirements:
+
+Distribution requirements
+-------------------------
+
+To get the full set of requirements for a distribution, use the ``requires()``
+function::
+
+ >>> requires('wheel')
+ ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; 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`
+for additional details.
+
+
+Extending the search algorithm
+==============================
+
+Because package metadata is not available through :data:`sys.path` searches, or
+package loaders directly, the metadata for a package is found through import
+system `finders`_. To find a distribution package's metadata,
+``importlib.metadata`` queries the list of :term:`meta path finders <meta path finder>` on
+:data:`sys.meta_path`.
+
+By default ``importlib_metadata`` installs a finder for distribution packages
+found on the file system. This finder doesn't actually find any *packages*,
+but it can find the packages' metadata.
+
+The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the
+interface expected of finders by Python's import system.
+``importlib_metadata`` extends this protocol by looking for an optional
+``find_distributions`` callable on the finders from
+:data:`sys.meta_path` and presents this extended interface as the
+``DistributionFinder`` abstract base class, which defines this abstract
+method::
+
+ @abc.abstractmethod
+ def find_distributions(context=DistributionFinder.Context()):
+ """Return an iterable of all Distribution instances capable of
+ loading the metadata for packages for the indicated ``context``.
+ """
+
+The ``DistributionFinder.Context`` object provides ``.path`` and ``.name``
+properties indicating the path to search and name to match and may
+supply other relevant context.
+
+What this means in practice is that to support finding distribution package
+metadata in locations other than the file system, subclass
+``Distribution`` and implement the abstract methods. Then from
+a custom finder, return instances of this derived ``Distribution`` in the
+``find_distributions()`` method.
+
+
+.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points
+.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api
+.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders
+
+
+.. rubric:: Footnotes
+
+.. [#f1] Technically, the returned distribution metadata object is an
+ :class:`email.message.EmailMessage`
+ instance, but this is an implementation detail, and not part of the
+ stable API. You should only use dictionary-like methods and syntax
+ to access the metadata contents.
--- /dev/null
+from __future__ import unicode_literals
+
+import os
+import sys
+import shutil
+import tempfile
+import textwrap
+import test.support
+
+from .._compat import pathlib, contextlib
+
+
+__metaclass__ = type
+
+
+@contextlib.contextmanager
+def tempdir():
+ tmpdir = tempfile.mkdtemp()
+ try:
+ yield pathlib.Path(tmpdir)
+ finally:
+ shutil.rmtree(tmpdir)
+
+
+@contextlib.contextmanager
+def save_cwd():
+ orig = os.getcwd()
+ try:
+ yield
+ finally:
+ os.chdir(orig)
+
+
+@contextlib.contextmanager
+def tempdir_as_cwd():
+ with tempdir() as tmp:
+ with save_cwd():
+ os.chdir(str(tmp))
+ yield tmp
+
+
+@contextlib.contextmanager
+def install_finder(finder):
+ sys.meta_path.append(finder)
+ try:
+ yield
+ finally:
+ sys.meta_path.remove(finder)
+
+
+class Fixtures:
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+
+
+class SiteDir(Fixtures):
+ def setUp(self):
+ super(SiteDir, self).setUp()
+ self.site_dir = self.fixtures.enter_context(tempdir())
+
+
+class OnSysPath(Fixtures):
+ @staticmethod
+ @contextlib.contextmanager
+ def add_sys_path(dir):
+ sys.path[:0] = [str(dir)]
+ try:
+ yield
+ finally:
+ sys.path.remove(str(dir))
+
+ def setUp(self):
+ super(OnSysPath, self).setUp()
+ self.fixtures.enter_context(self.add_sys_path(self.site_dir))
+
+
+class DistInfoPkg(OnSysPath, SiteDir):
+ files = {
+ "distinfo_pkg-1.0.0.dist-info": {
+ "METADATA": """
+ Name: distinfo-pkg
+ Author: Steven Ma
+ Version: 1.0.0
+ Requires-Dist: wheel >= 1.0
+ Requires-Dist: pytest; extra == 'test'
+ """,
+ "RECORD": "mod.py,sha256=abc,20\n",
+ "entry_points.txt": """
+ [entries]
+ main = mod:main
+ ns:sub = mod:main
+ """
+ },
+ "mod.py": """
+ def main():
+ print("hello world")
+ """,
+ }
+
+ def setUp(self):
+ super(DistInfoPkg, self).setUp()
+ build_files(DistInfoPkg.files, self.site_dir)
+
+
+class DistInfoPkgOffPath(SiteDir):
+ def setUp(self):
+ super(DistInfoPkgOffPath, self).setUp()
+ build_files(DistInfoPkg.files, self.site_dir)
+
+
+class EggInfoPkg(OnSysPath, SiteDir):
+ files = {
+ "egginfo_pkg.egg-info": {
+ "PKG-INFO": """
+ Name: egginfo-pkg
+ Author: Steven Ma
+ License: Unknown
+ Version: 1.0.0
+ Classifier: Intended Audience :: Developers
+ Classifier: Topic :: Software Development :: Libraries
+ """,
+ "SOURCES.txt": """
+ mod.py
+ egginfo_pkg.egg-info/top_level.txt
+ """,
+ "entry_points.txt": """
+ [entries]
+ main = mod:main
+ """,
+ "requires.txt": """
+ wheel >= 1.0; python_version >= "2.7"
+ [test]
+ pytest
+ """,
+ "top_level.txt": "mod\n"
+ },
+ "mod.py": """
+ def main():
+ print("hello world")
+ """,
+ }
+
+ def setUp(self):
+ super(EggInfoPkg, self).setUp()
+ build_files(EggInfoPkg.files, prefix=self.site_dir)
+
+
+class EggInfoFile(OnSysPath, 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:
+ files = {
+ "setup.py": """
+ import setuptools
+ setuptools.setup(name="local-pkg", version="2.0.1")
+ """,
+ }
+
+ def setUp(self):
+ self.fixtures = contextlib.ExitStack()
+ self.addCleanup(self.fixtures.close)
+ self.fixtures.enter_context(tempdir_as_cwd())
+ build_files(self.files)
+
+
+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.
+
+ For example:
+
+ {"README.txt": "A README file",
+ "foo": {
+ "__init__.py": "",
+ "bar": {
+ "__init__.py": "",
+ },
+ "baz.py": "# Some code",
+ }
+ }
+ """
+ for name, contents in file_defs.items():
+ full_name = prefix / name
+ if isinstance(contents, dict):
+ full_name.mkdir()
+ build_files(contents, prefix=full_name)
+ else:
+ if isinstance(contents, bytes):
+ with full_name.open('wb') as f:
+ f.write(contents)
+ else:
+ with full_name.open('w') as f:
+ f.write(DALS(contents))
+
+
+class FileBuilder:
+ def unicode_filename(self):
+ return test.support.FS_NONASCII or \
+ self.skip("File system does not support non-ascii.")
+
+
+def DALS(str):
+ "Dedent and left-strip"
+ return textwrap.dedent(str).lstrip()
+
+
+class NullFinder:
+ def find_module(self, name):
+ pass
--- /dev/null
+import re
+import textwrap
+import unittest
+
+from . import fixtures
+from .. import (
+ Distribution, PackageNotFoundError, distribution,
+ entry_points, files, metadata, requires, version,
+ )
+
+try:
+ from collections.abc import Iterator
+except ImportError:
+ from collections import Iterator # noqa: F401
+
+try:
+ from builtins import str as text
+except ImportError:
+ from __builtin__ import unicode as text
+
+
+class APITests(
+ fixtures.EggInfoPkg,
+ fixtures.DistInfoPkg,
+ fixtures.EggInfoFile,
+ unittest.TestCase):
+
+ version_pattern = r'\d+\.\d+(\.\d)?'
+
+ def test_retrieves_version_of_self(self):
+ pkg_version = version('egginfo-pkg')
+ assert isinstance(pkg_version, text)
+ assert re.match(self.version_pattern, pkg_version)
+
+ def test_retrieves_version_of_distinfo_pkg(self):
+ pkg_version = version('distinfo-pkg')
+ assert isinstance(pkg_version, text)
+ assert re.match(self.version_pattern, pkg_version)
+
+ def test_for_name_does_not_exist(self):
+ with self.assertRaises(PackageNotFoundError):
+ distribution('does-not-exist')
+
+ def test_for_top_level(self):
+ self.assertEqual(
+ distribution('egginfo-pkg').read_text('top_level.txt').strip(),
+ 'mod')
+
+ def test_read_text(self):
+ top_level = [
+ path for path in files('egginfo-pkg')
+ if path.name == 'top_level.txt'
+ ][0]
+ self.assertEqual(top_level.read_text(), 'mod\n')
+
+ def test_entry_points(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['main']
+ self.assertEqual(ep.value, 'mod:main')
+ self.assertEqual(ep.extras, [])
+
+ def test_metadata_for_this_package(self):
+ md = metadata('egginfo-pkg')
+ assert md['author'] == 'Steven Ma'
+ assert md['LICENSE'] == 'Unknown'
+ assert md['Name'] == 'egginfo-pkg'
+ classifiers = md.get_all('Classifier')
+ assert 'Topic :: Software Development :: Libraries' in classifiers
+
+ def test_importlib_metadata_version(self):
+ resolved = version('importlib-metadata')
+ assert re.match(self.version_pattern, resolved)
+
+ @staticmethod
+ def _test_files(files):
+ root = files[0].root
+ for file in files:
+ assert file.root == root
+ assert not file.hash or file.hash.value
+ assert not file.hash or file.hash.mode == 'sha256'
+ assert not file.size or file.size >= 0
+ assert file.locate().exists()
+ assert isinstance(file.read_binary(), bytes)
+ if file.name.endswith('.py'):
+ file.read_text()
+
+ def test_file_hash_repr(self):
+ try:
+ assertRegex = self.assertRegex
+ except AttributeError:
+ # Python 2
+ assertRegex = self.assertRegexpMatches
+
+ util = [
+ p for p in files('distinfo-pkg')
+ if p.name == 'mod.py'
+ ][0]
+ assertRegex(
+ repr(util.hash),
+ '<FileHash mode: sha256 value: .*>')
+
+ def test_files_dist_info(self):
+ self._test_files(files('distinfo-pkg'))
+
+ def test_files_egg_info(self):
+ self._test_files(files('egginfo-pkg'))
+
+ def test_version_egg_info_file(self):
+ self.assertEqual(version('egginfo-file'), '0.1')
+
+ def test_requires_egg_info_file(self):
+ requirements = requires('egginfo-file')
+ self.assertIsNone(requirements)
+
+ def test_requires_egg_info(self):
+ deps = requires('egginfo-pkg')
+ assert len(deps) == 2
+ assert any(
+ dep == 'wheel >= 1.0; python_version >= "2.7"'
+ for dep in deps
+ )
+
+ def test_requires_dist_info(self):
+ deps = requires('distinfo-pkg')
+ assert len(deps) == 2
+ assert all(deps)
+ assert 'wheel >= 1.0' in deps
+ assert "pytest; extra == 'test'" in deps
+
+ def test_more_complex_deps_requires_text(self):
+ requires = textwrap.dedent("""
+ dep1
+ dep2
+
+ [:python_version < "3"]
+ dep3
+
+ [extra1]
+ dep4
+
+ [extra2:python_version < "3"]
+ dep5
+ """)
+ deps = sorted(Distribution._deps_from_requires_text(requires))
+ expected = [
+ 'dep1',
+ 'dep2',
+ 'dep3; python_version < "3"',
+ 'dep4; extra == "extra1"',
+ 'dep5; (python_version < "3") and extra == "extra2"',
+ ]
+ # It's important that the environment marker expression be
+ # wrapped in parentheses to avoid the following 'and' binding more
+ # tightly than some other part of the environment expression.
+
+ assert deps == expected
+
+
+class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
+ def test_find_distributions_specified_path(self):
+ dists = Distribution.discover(path=[str(self.site_dir)])
+ assert any(
+ dist.metadata['Name'] == 'distinfo-pkg'
+ for dist in dists
+ )
+
+ def test_distribution_at_pathlib(self):
+ """Demonstrate how to load metadata direct from a directory.
+ """
+ dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
+ dist = Distribution.at(dist_info_path)
+ assert dist.version == '1.0.0'
+
+ def test_distribution_at_str(self):
+ dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
+ dist = Distribution.at(str(dist_info_path))
+ assert dist.version == '1.0.0'
--- /dev/null
+# coding: utf-8
+
+from __future__ import unicode_literals
+
+import unittest
+import packaging.requirements
+import packaging.version
+
+from . import fixtures
+from .. import (
+ Distribution,
+ _compat,
+ version,
+ )
+
+
+class IntegrationTests(fixtures.DistInfoPkg, unittest.TestCase):
+
+ def test_package_spec_installed(self):
+ """
+ Illustrate the recommended procedure to determine if
+ a specified version of a package is installed.
+ """
+ def is_installed(package_spec):
+ req = packaging.requirements.Requirement(package_spec)
+ return version(req.name) in req.specifier
+
+ assert is_installed('distinfo-pkg==1.0')
+ assert is_installed('distinfo-pkg>=1.0,<2.0')
+ assert not is_installed('distinfo-pkg<1.0')
+
+
+class FinderTests(fixtures.Fixtures, unittest.TestCase):
+
+ def test_finder_without_module(self):
+ class ModuleFreeFinder(fixtures.NullFinder):
+ """
+ A finder without an __module__ attribute
+ """
+ def __getattribute__(self, name):
+ if name == '__module__':
+ raise AttributeError(name)
+ return super().__getattribute__(name)
+
+ self.fixtures.enter_context(
+ fixtures.install_finder(ModuleFreeFinder()))
+ _compat.disable_stdlib_finder()
+
+
+class LocalProjectTests(fixtures.LocalPackage, unittest.TestCase):
+ def test_find_local(self):
+ dist = Distribution._local()
+ assert dist.metadata['Name'] == 'local-pkg'
+ assert dist.version == '2.0.1'
--- /dev/null
+# coding: utf-8
+from __future__ import unicode_literals
+
+import re
+import json
+import pickle
+import textwrap
+import unittest
+import importlib
+import importlib_metadata
+import pyfakefs.fake_filesystem_unittest as ffs
+
+from . import fixtures
+from .. import (
+ Distribution, EntryPoint, MetadataPathFinder,
+ PackageNotFoundError, distributions,
+ entry_points, metadata, version,
+ )
+
+try:
+ from builtins import str as text
+except ImportError:
+ from __builtin__ import unicode as text
+
+
+class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
+ version_pattern = r'\d+\.\d+(\.\d)?'
+
+ def test_retrieves_version_of_self(self):
+ dist = Distribution.from_name('distinfo-pkg')
+ assert isinstance(dist.version, text)
+ assert re.match(self.version_pattern, dist.version)
+
+ def test_for_name_does_not_exist(self):
+ with self.assertRaises(PackageNotFoundError):
+ Distribution.from_name('does-not-exist')
+
+ def test_package_not_found_mentions_metadata(self):
+ """
+ When a package is not found, that could indicate that the
+ packgae is not installed or that it is installed without
+ metadata. Ensure the exception mentions metadata to help
+ guide users toward the cause. See #124.
+ """
+ with self.assertRaises(PackageNotFoundError) as ctx:
+ Distribution.from_name('does-not-exist')
+
+ assert "metadata" in str(ctx.exception)
+
+ def test_new_style_classes(self):
+ self.assertIsInstance(Distribution, type)
+ self.assertIsInstance(MetadataPathFinder, type)
+
+
+class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
+ def test_import_nonexistent_module(self):
+ # Ensure that the MetadataPathFinder does not crash an import of a
+ # non-existent module.
+ with self.assertRaises(ImportError):
+ importlib.import_module('does_not_exist')
+
+ def test_resolve(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['main']
+ self.assertEqual(ep.load().__name__, "main")
+
+ def test_entrypoint_with_colon_in_name(self):
+ entries = dict(entry_points()['entries'])
+ ep = entries['ns:sub']
+ self.assertEqual(ep.value, 'mod:main')
+
+ def test_resolve_without_attr(self):
+ ep = EntryPoint(
+ name='ep',
+ value='importlib_metadata',
+ group='grp',
+ )
+ assert ep.load() is importlib_metadata
+
+
+class NameNormalizationTests(
+ fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ @staticmethod
+ def pkg_with_dashes(site_dir):
+ """
+ Create minimal metadata for a package with dashes
+ in the name (and thus underscores in the filename).
+ """
+ metadata_dir = site_dir / 'my_pkg.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w') as strm:
+ strm.write('Version: 1.0\n')
+ return 'my-pkg'
+
+ def test_dashes_in_dist_name_found_as_underscores(self):
+ """
+ For a package with a dash in the name, the dist-info metadata
+ uses underscores in the name. Ensure the metadata loads.
+ """
+ pkg_name = self.pkg_with_dashes(self.site_dir)
+ assert version(pkg_name) == '1.0'
+
+ @staticmethod
+ def pkg_with_mixed_case(site_dir):
+ """
+ Create minimal metadata for a package with mixed case
+ in the name.
+ """
+ metadata_dir = site_dir / 'CherryPy.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w') as strm:
+ strm.write('Version: 1.0\n')
+ return 'CherryPy'
+
+ def test_dist_name_found_as_any_case(self):
+ """
+ Ensure the metadata loads when queried with any case.
+ """
+ pkg_name = self.pkg_with_mixed_case(self.site_dir)
+ assert version(pkg_name) == '1.0'
+ assert version(pkg_name.lower()) == '1.0'
+ assert version(pkg_name.upper()) == '1.0'
+
+
+class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ @staticmethod
+ def pkg_with_non_ascii_description(site_dir):
+ """
+ Create minimal metadata for a package with non-ASCII in
+ the description.
+ """
+ metadata_dir = site_dir / 'portend.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w', encoding='utf-8') as fp:
+ fp.write('Description: pôrˈtend\n')
+ return 'portend'
+
+ @staticmethod
+ def pkg_with_non_ascii_description_egg_info(site_dir):
+ """
+ Create minimal metadata for an egg-info package with
+ non-ASCII in the description.
+ """
+ metadata_dir = site_dir / 'portend.dist-info'
+ metadata_dir.mkdir()
+ metadata = metadata_dir / 'METADATA'
+ with metadata.open('w', encoding='utf-8') as fp:
+ fp.write(textwrap.dedent("""
+ Name: portend
+
+ pôrˈtend
+ """).lstrip())
+ return 'portend'
+
+ def test_metadata_loads(self):
+ pkg_name = self.pkg_with_non_ascii_description(self.site_dir)
+ meta = metadata(pkg_name)
+ assert meta['Description'] == 'pôrˈtend'
+
+ def test_metadata_loads_egg_info(self):
+ pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir)
+ meta = metadata(pkg_name)
+ assert meta.get_payload() == 'pôrˈtend\n'
+
+
+class DiscoveryTests(fixtures.EggInfoPkg,
+ fixtures.DistInfoPkg,
+ unittest.TestCase):
+
+ def test_package_discovery(self):
+ dists = list(distributions())
+ assert all(
+ isinstance(dist, Distribution)
+ for dist in dists
+ )
+ assert any(
+ dist.metadata['Name'] == 'egginfo-pkg'
+ for dist in dists
+ )
+ assert any(
+ dist.metadata['Name'] == 'distinfo-pkg'
+ for dist in dists
+ )
+
+ def test_invalid_usage(self):
+ with self.assertRaises(ValueError):
+ list(distributions(context='something', name='else'))
+
+
+class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
+ def test_egg_info(self):
+ # make an `EGG-INFO` directory that's unrelated
+ self.site_dir.joinpath('EGG-INFO').mkdir()
+ # used to crash with `IsADirectoryError`
+ with self.assertRaises(PackageNotFoundError):
+ version('unknown-package')
+
+ def test_egg(self):
+ egg = self.site_dir.joinpath('foo-3.6.egg')
+ egg.mkdir()
+ with self.add_sys_path(egg):
+ with self.assertRaises(PackageNotFoundError):
+ version('foo')
+
+
+class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
+ site_dir = '/does-not-exist'
+
+ def test_discovery(self):
+ """
+ Discovering distributions should succeed even if
+ there is an invalid path on sys.path.
+ """
+ importlib_metadata.distributions()
+
+
+class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
+ site_dir = '/access-denied'
+
+ def setUp(self):
+ super(InaccessibleSysPath, self).setUp()
+ self.setUpPyfakefs()
+ self.fs.create_dir(self.site_dir, perm_bits=000)
+
+ def test_discovery(self):
+ """
+ Discovering distributions should succeed even if
+ there is an invalid path on sys.path.
+ """
+ list(importlib_metadata.distributions())
+
+
+class TestEntryPoints(unittest.TestCase):
+ def __init__(self, *args):
+ super(TestEntryPoints, self).__init__(*args)
+ self.ep = importlib_metadata.EntryPoint('name', 'value', 'group')
+
+ def test_entry_point_pickleable(self):
+ revived = pickle.loads(pickle.dumps(self.ep))
+ assert revived == self.ep
+
+ def test_immutable(self):
+ """EntryPoints should be immutable"""
+ with self.assertRaises(AttributeError):
+ self.ep.name = 'badactor'
+
+ def test_repr(self):
+ assert 'EntryPoint' in repr(self.ep)
+ assert 'name=' in repr(self.ep)
+ assert "'name'" in repr(self.ep)
+
+ def test_hashable(self):
+ """EntryPoints should be hashable"""
+ hash(self.ep)
+
+ def test_json_dump(self):
+ """
+ json should not expect to be able to dump an EntryPoint
+ """
+ with self.assertRaises(Exception):
+ json.dumps(self.ep)
+
+ def test_module(self):
+ assert self.ep.module == 'value'
+
+ def test_attr(self):
+ assert self.ep.attr is None
+
+
+class FileSystem(
+ fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder,
+ unittest.TestCase):
+ def test_unicode_dir_on_sys_path(self):
+ """
+ Ensure a Unicode subdirectory of a directory on sys.path
+ does not crash.
+ """
+ fixtures.build_files(
+ {self.unicode_filename(): {}},
+ prefix=self.site_dir,
+ )
+ list(distributions())
--- /dev/null
+import sys
+import unittest
+
+from .. import (
+ distribution, entry_points, files, PackageNotFoundError,
+ version, distributions,
+ )
+
+try:
+ from importlib import resources
+ getattr(resources, 'files')
+ getattr(resources, 'as_file')
+except (ImportError, AttributeError):
+ import importlib_resources as resources
+
+try:
+ from contextlib import ExitStack
+except ImportError:
+ from contextlib2 import ExitStack
+
+
+class TestZip(unittest.TestCase):
+ root = 'importlib_metadata.tests.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):
+ # 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)
+ self._fixture_on_path('example-21.12-py3-none-any.whl')
+
+ def test_zip_version(self):
+ self.assertEqual(version('example'), '21.12')
+
+ def test_zip_version_does_not_match(self):
+ with self.assertRaises(PackageNotFoundError):
+ version('definitely-not-installed')
+
+ def test_zip_entry_points(self):
+ scripts = dict(entry_points()['console_scripts'])
+ entry_point = scripts['example']
+ self.assertEqual(entry_point.value, 'example:main')
+ entry_point = scripts['Example']
+ self.assertEqual(entry_point.value, 'example:main')
+
+ def test_missing_metadata(self):
+ self.assertIsNone(distribution('example').read_text('does not exist'))
+
+ def test_case_insensitive(self):
+ self.assertEqual(version('Example'), '21.12')
+
+ def test_files(self):
+ for file in files('example'):
+ path = str(file.dist.locate_file(file))
+ assert '.whl/' in path, path
+
+ def test_one_distribution(self):
+ dists = list(distributions(path=sys.path[:1]))
+ assert len(dists) == 1
+
+
+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)
+ self._fixture_on_path('example-21.12-py3.6.egg')
+
+ def test_files(self):
+ for file in files('example'):
+ path = str(file.dist.locate_file(file))
+ assert '.egg/' in path, path
--- /dev/null
+def main():
+ return 'example'
--- /dev/null
+from setuptools import setup
+setup(
+ name='example',
+ version='21.12',
+ license='Apache Software License',
+ packages=['example'],
+ entry_points={
+ 'console_scripts': ['example = example:main', 'Example=example:main'],
+ },
+ )
--- /dev/null
+[build-system]
+requires = ["setuptools>=30.3", "wheel", "setuptools_scm"]
--- /dev/null
+[metadata]
+name = importlib_metadata
+author = Jason R. Coombs
+author_email = jaraco@jaraco.com
+url = http://importlib-metadata.readthedocs.io/
+description = Read metadata from Python packages
+long_description = file: README.rst
+license = Apache Software License
+classifiers =
+ Development Status :: 3 - Alpha
+ Intended Audience :: Developers
+ License :: OSI Approved :: Apache Software License
+ Topic :: Software Development :: Libraries
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 2
+
+[options]
+python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*
+setup_requires = setuptools-scm
+install_requires =
+ zipp>=0.5
+ pathlib2; python_version < '3'
+ contextlib2; python_version < '3'
+ configparser>=3.5; python_version < '3'
+packages = find:
+
+[options.package_data]
+* = *.zip, *.file, *.txt, *.toml
+importlib_metadata =
+ docs/*
+ docs/_static/*
+importlib_metadata.tests.data =
+ *.egg
+ *.whl
+
+[mypy]
+ignore_missing_imports = True
+strict_optional = False
+
+[mypy-importlib_metadata.docs.*]
+ignore_errors = True
+
+[mypy-importlib_metadata.tests.*]
+ignore_errors = True
+
+[wheel]
+universal = 1
+
+[options.extras_require]
+testing =
+ importlib_resources>=1.3; python_version < "3.9"
+ packaging
+ pep517
+docs =
+ sphinx
+ rst.linker
+
+[egg_info]
+tag_build =
+tag_date = 0
+
--- /dev/null
+from setuptools import setup
+
+setup(use_scm_version=True)
--- /dev/null
+[tox]
+envlist = {py27,py35,py36,py37,py38}{,-cov,-diffcov},qa,docs,perf
+skip_missing_interpreters = True
+minversion = 3.2
+# Ensure that a late version of pip is used even on tox-venv.
+requires =
+ tox-pip-version>=0.0.6
+
+[testenv]
+pip_version = pip
+commands =
+ !cov,!diffcov: python -m unittest discover {posargs}
+ cov,diffcov: python -m coverage run {[coverage]rc} -m unittest discover {posargs}
+ cov,diffcov: python -m coverage combine {[coverage]rc}
+ cov: python -m coverage html {[coverage]rc}
+ cov: python -m coverage xml {[coverage]rc}
+ cov: python -m coverage report -m {[coverage]rc} --fail-under=100
+ diffcov: python -m coverage xml {[coverage]rc}
+ diffcov: diff-cover coverage.xml --html-report diffcov.html
+ diffcov: diff-cover coverage.xml --fail-under=100
+usedevelop = True
+passenv =
+ PYTHON*
+ LANG*
+ LC_*
+ PYV
+deps =
+ cov,diffcov: coverage>=4.5
+ diffcov: diff_cover
+ pyfakefs
+setenv =
+ cov: COVERAGE_PROCESS_START={[coverage]rcfile}
+ cov: COVERAGE_OPTIONS="-p"
+ cov: COVERAGE_FILE={toxinidir}/.coverage
+ py27: PYV=2
+ py35,py36,py37,py38: PYV=3
+ # workaround deprecation warnings in pip's vendored packages
+ PYTHONWARNINGS=ignore:Using or importing the ABCs:DeprecationWarning:pip._vendor
+extras =
+ testing
+
+
+[testenv:qa]
+basepython = python3.7
+commands =
+ python -m flake8 importlib_metadata
+ mypy importlib_metadata
+deps =
+ mypy
+ flake8
+ flufl.flake8
+extras =
+
+
+[testenv:docs]
+basepython = python3
+commands =
+ sphinx-build importlib_metadata/docs build/sphinx/html
+extras =
+ docs
+
+
+[testenv:perf]
+use_develop = False
+deps =
+ ipython
+commands =
+ python -m timeit -s 'import importlib_metadata' -- 'importlib_metadata.distribution("ipython")'
+
+
+[testenv:release]
+basepython = python3
+deps =
+ twine
+ wheel
+ setuptools
+ keyring
+ setuptools_scm
+passenv =
+ TWINE_PASSWORD
+setenv =
+ TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
+commands =
+ python setup.py sdist bdist_wheel
+ python -m twine {posargs} upload dist/*
+
+
+[coverage]
+rcfile = {toxinidir}/coverage.ini
+rc = --rcfile="{[coverage]rcfile}"
+
+
+[flake8]
+hang-closing = True
+jobs = 1
+max-line-length = 79
+enable-extensions = U4