| .. _using: | |
| ======================================================== | |
| :mod:`!importlib.metadata` -- Accessing package metadata | |
| ======================================================== | |
| .. module:: importlib.metadata | |
| :synopsis: Accessing package metadata | |
| .. versionadded:: 3.8 | |
| .. versionchanged:: 3.10 | |
| ``importlib.metadata`` is no longer provisional. | |
| **Source code:** :source:`Lib/importlib/metadata/__init__.py` | |
| ``importlib.metadata`` is a library that provides access to | |
| the metadata of an installed `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_, | |
| such as its entry points | |
| or its top-level names (`Import Package <https://packaging.python.org/en/latest/glossary/#term-Import-Package>`_\s, modules, if any). | |
| Built in part on Python's import system, this library | |
| provides the entry point and metadata APIs that were previously | |
| exposed by the now-removed ``pkg_resources`` package. Along with | |
| :mod:`importlib.resources`, it supersedes ``pkg_resources``. | |
| ``importlib.metadata`` operates on third-party *distribution packages* | |
| installed into Python's ``site-packages`` directory via tools such as | |
| :pypi:`pip`. | |
| Specifically, it works with distributions with discoverable | |
| ``dist-info`` or ``egg-info`` directories, | |
| and metadata defined by the `Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_. | |
| .. important:: | |
| These are *not* necessarily equivalent to or correspond 1:1 with | |
| the top-level *import package* names | |
| that can be imported inside Python code. | |
| One *distribution package* can contain multiple *import packages* | |
| (and single modules), | |
| and one top-level *import package* | |
| may map to multiple *distribution packages* | |
| if it is a namespace package. | |
| You can use :ref:`packages_distributions() <package-distributions>` | |
| to get a mapping between them. | |
| By default, distribution 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. | |
| .. seealso:: | |
| https://importlib-metadata.readthedocs.io/ | |
| The documentation for ``importlib_metadata``, which supplies a | |
| backport of ``importlib.metadata``. | |
| This includes an `API reference | |
| <https://importlib-metadata.readthedocs.io/en/latest/api.html>`__ | |
| for this module's classes and functions, | |
| as well as a `migration guide | |
| <https://importlib-metadata.readthedocs.io/en/latest/migration.html>`__ | |
| for existing users of ``pkg_resources``. | |
| Overview | |
| ======== | |
| Let's say you wanted to get the version string for a | |
| `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ you've installed | |
| using ``pip``. We start by creating a virtual environment and installing | |
| something into it: | |
| .. code-block:: shell-session | |
| $ python -m venv example | |
| $ source example/bin/activate | |
| (example) $ python -m pip install wheel | |
| You can get the version string for ``wheel`` by running the following: | |
| .. code-block:: pycon | |
| (example) $ python | |
| >>> from importlib.metadata import version # doctest: +SKIP | |
| >>> version('wheel') # doctest: +SKIP | |
| '0.32.3' | |
| You can also get a collection of entry points selectable by properties of the EntryPoint (typically 'group' or 'name'), such as | |
| ``console_scripts``, ``distutils.commands`` and others. Each group contains a | |
| collection of :ref:`EntryPoint <entry-points>` objects. | |
| You can get the :ref:`metadata for a distribution <metadata>`:: | |
| >>> from importlib.metadata import metadata # doctest: +SKIP | |
| >>> list(metadata('wheel')) # doctest: +SKIP | |
| ['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`. | |
| .. exception:: PackageNotFoundError | |
| Subclass of :class:`ModuleNotFoundError` raised by several functions in this | |
| module when queried for a distribution package which is not installed in the | |
| current Python environment. | |
| .. exception:: MetadataNotFound | |
| Subclass of :class:`FileNotFoundError` raised when attempting to load metadata | |
| from a distribution folder that is empty or otherwise does not contain a | |
| metadata file. | |
| Functional API | |
| ============== | |
| This package provides the following functionality via its public API. | |
| .. _entry-points: | |
| Entry points | |
| ------------ | |
| .. function:: entry_points(**select_params) | |
| Returns a :class:`EntryPoints` instance describing entry points for the | |
| current environment. Any given keyword parameters are passed to the | |
| :meth:`!select` method for comparison to the attributes of | |
| the individual entry point definitions. | |
| Note: to query for entry points based on :attr:`!EntryPoint.dist` attribute, | |
| use :meth:`Distribution.entry_points` instead (as different :class:`Distribution` | |
| instances do not currently compare equal, even if they have the same attributes) | |
| .. class:: EntryPoints | |
| Details of a collection of installed entry points. | |
| Also provides a ``.groups`` attribute that reports all identified entry | |
| point groups, and a ``.names`` attribute that reports all identified entry | |
| point names. | |
| .. class:: EntryPoint | |
| Details of an installed entry point. | |
| Each :class:`!EntryPoint` instance has ``.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, and ``.dist`` for obtaining | |
| information regarding the distribution package that provides the entry point. | |
| Query all entry points:: | |
| >>> eps = entry_points() # doctest: +SKIP | |
| The :func:`!entry_points` function returns a :class:`!EntryPoints` object, | |
| a collection of all :class:`!EntryPoint` objects with ``names`` and ``groups`` | |
| attributes for convenience:: | |
| >>> sorted(eps.groups) # doctest: +SKIP | |
| ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] | |
| :class:`!EntryPoints` has a :meth:`!select` method to select entry points | |
| matching specific properties. Select entry points in the | |
| ``console_scripts`` group:: | |
| >>> scripts = eps.select(group='console_scripts') # doctest: +SKIP | |
| Equivalently, since :func:`!entry_points` passes keyword arguments | |
| through to select:: | |
| >>> scripts = entry_points(group='console_scripts') # doctest: +SKIP | |
| Pick out a specific script named "wheel" (found in the wheel project):: | |
| >>> 'wheel' in scripts.names # doctest: +SKIP | |
| True | |
| >>> wheel = scripts['wheel'] # doctest: +SKIP | |
| Equivalently, query for that entry point during selection:: | |
| >>> (wheel,) = entry_points(group='console_scripts', name='wheel') # doctest: +SKIP | |
| >>> (wheel,) = entry_points().select(group='console_scripts', name='wheel') # doctest: +SKIP | |
| Inspect the resolved entry point:: | |
| >>> wheel # doctest: +SKIP | |
| EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') | |
| >>> wheel.module # doctest: +SKIP | |
| 'wheel.cli' | |
| >>> wheel.attr # doctest: +SKIP | |
| 'main' | |
| >>> wheel.extras # doctest: +SKIP | |
| [] | |
| >>> main = wheel.load() # doctest: +SKIP | |
| >>> main # doctest: +SKIP | |
| <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.pypa.io/en/latest/userguide/entry_point.html>`_ | |
| for more information on entry points, their definition, and usage. | |
| .. versionchanged:: 3.12 | |
| The "selectable" entry points were introduced in ``importlib_metadata`` | |
| 3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted | |
| no parameters and always returned a dictionary of entry points, keyed | |
| by group. With ``importlib_metadata`` 5.0 and Python 3.12, | |
| ``entry_points`` always returns an ``EntryPoints`` object. See | |
| :pypi:`backports.entry_points_selectable` | |
| for compatibility options. | |
| .. versionchanged:: 3.13 | |
| ``EntryPoint`` objects no longer present a tuple-like interface | |
| (:meth:`~object.__getitem__`). | |
| .. _metadata: | |
| Distribution metadata | |
| --------------------- | |
| .. function:: metadata(distribution_name) | |
| Return the distribution metadata corresponding to the named | |
| distribution package as a :class:`PackageMetadata` instance. | |
| Raises :exc:`PackageNotFoundError` if the named distribution | |
| package is not installed in the current Python environment. | |
| Raises :exc:`MetadataNotFound` if a distribution package is | |
| present but no METADATA file is present. | |
| .. class:: PackageMetadata | |
| A concrete implementation of the | |
| `PackageMetadata protocol <https://importlib-metadata.readthedocs.io/en/latest/api.html#importlib_metadata.PackageMetadata>`_. | |
| In addition to providing the defined protocol methods and attributes, subscripting | |
| the instance is equivalent to calling the :meth:`!get` method. | |
| Every `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ | |
| includes some metadata, which you can extract using the :func:`!metadata` function:: | |
| >>> wheel_metadata = metadata('wheel') # doctest: +SKIP | |
| The keys of the returned data structure name the metadata keywords, and | |
| the values are returned unparsed from the distribution metadata:: | |
| >>> wheel_metadata['Requires-Python'] # doctest: +SKIP | |
| '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' | |
| :class:`PackageMetadata` also presents a :attr:`!json` attribute that returns | |
| all the metadata in a JSON-compatible form per :PEP:`566`:: | |
| >>> wheel_metadata.json['requires_python'] | |
| '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' | |
| The full set of available metadata is not described here. | |
| See the PyPA `Core metadata specification <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_ for additional details. | |
| .. versionchanged:: 3.15 | |
| Previously and incidentally, if a METADATA file was missing from a distribution, an | |
| empty ``PackageMetadata`` would be returned, indistinguishable from | |
| an empty METADATA file. Now, a missing METADATA file triggers a | |
| ``MetadataNotFound`` exception. | |
| .. versionchanged:: 3.10 | |
| The ``Description`` is now included in the metadata when presented | |
| through the payload. Line continuation characters have been removed. | |
| The ``json`` attribute was added. | |
| .. _version: | |
| Distribution versions | |
| --------------------- | |
| .. function:: version(distribution_name) | |
| Return the installed distribution package | |
| `version <https://packaging.python.org/en/latest/specifications/core-metadata/#version>`__ | |
| for the named distribution package. | |
| Raises :exc:`PackageNotFoundError` if the named distribution | |
| package is not installed in the current Python environment. | |
| The :func:`!version` function is the quickest way to get a | |
| `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_'s version | |
| number, as a string:: | |
| >>> version('wheel') # doctest: +SKIP | |
| '0.32.3' | |
| .. _files: | |
| Distribution files | |
| ------------------ | |
| .. function:: files(distribution_name) | |
| Return the full set of files contained within the named | |
| distribution package as :class:`PackagePath` instances. | |
| Raises :exc:`PackageNotFoundError` if the named distribution | |
| package is not installed in the current Python environment. | |
| Returns :const:`None` if the distribution is found but the installation | |
| database records reporting the files associated with the distribution package | |
| are missing. | |
| .. class:: PackagePath | |
| A :class:`pathlib.PurePath` derived object with additional ``dist``, | |
| ``size``, and ``hash`` properties corresponding to the distribution | |
| package's installation metadata for that file, also: | |
| .. method:: locate() | |
| If possible, return the concrete :class:`SimplePath` allowing to access data, | |
| or raise a :exc:`NotImplementedError` otherwise. | |
| .. class:: SimplePath | |
| A protocol representing a minimal subset of :class:`pathlib.Path` that allows to | |
| check if it ``exists()``, to traverse using ``joinpath()`` and ``parent``, | |
| and to retrieve data using ``read_text()`` and ``read_bytes()``. | |
| The :func:`!files` function takes a | |
| `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ | |
| name and returns all of the files installed by this distribution. For example:: | |
| >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] # doctest: +SKIP | |
| >>> util # doctest: +SKIP | |
| PackagePath('wheel/util.py') | |
| >>> util.size # doctest: +SKIP | |
| 859 | |
| >>> util.dist # doctest: +SKIP | |
| <importlib.metadata._hooks.PathDistribution object at 0x101e0cef0> | |
| >>> util.hash # doctest: +SKIP | |
| <FileHash mode: sha256 value: bYkw5oMccfazVCoYQwKkkemoVyMAFoR34mmKBx8R1NI> | |
| Once you have the file, you can also read its contents:: | |
| >>> print(util.read_text()) # doctest: +SKIP | |
| import base64 | |
| import sys | |
| ... | |
| def as_bytes(s): | |
| if isinstance(s, text_type): | |
| return s.encode('utf-8') | |
| return s | |
| You can also use the :meth:`!locate` method to get the absolute | |
| path to the file:: | |
| >>> util.locate() # doctest: +SKIP | |
| PosixPath('/home/gustav/example/lib/site-packages/wheel/util.py') | |
| In the case where the metadata file listing files | |
| (``RECORD`` or ``SOURCES.txt``) is missing, :func:`!files` will | |
| return :const:`None`. The caller may wish to wrap calls to | |
| :func:`!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 | |
| ------------------------- | |
| .. function:: requires(distribution_name) | |
| Return the declared dependency specifiers for the named | |
| distribution package. | |
| Raises :exc:`PackageNotFoundError` if the named distribution | |
| package is not installed in the current Python environment. | |
| To get the full set of requirements for a `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_, | |
| use the :func:`!requires` | |
| function:: | |
| >>> requires('wheel') # doctest: +SKIP | |
| ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"] | |
| .. _package-distributions: | |
| .. _import-distribution-package-mapping: | |
| Mapping import to distribution packages | |
| --------------------------------------- | |
| .. function:: packages_distributions() | |
| Return a mapping from the top level module and import package | |
| names found via :data:`sys.meta_path` to the names of the distribution | |
| packages (if any) that provide the corresponding files. | |
| To allow for namespace packages (which may have members provided by | |
| multiple distribution packages), each top level import name maps to a | |
| list of distribution names rather than mapping directly to a single name. | |
| A convenience method to resolve the `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ | |
| name (or names, in the case of a namespace package) | |
| that provide each importable top-level | |
| Python module or `Import Package <https://packaging.python.org/en/latest/glossary/#term-Import-Package>`_:: | |
| >>> packages_distributions() | |
| {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} | |
| Some editable installs, `do not supply top-level names | |
| <https://github.com/pypa/packaging-problems/issues/609>`_, and thus this | |
| function is not reliable with such installs. | |
| .. versionadded:: 3.10 | |
| .. _distributions: | |
| Distributions | |
| ============= | |
| While the module level API described above is the most common and convenient usage, | |
| all that information is accessible from the :class:`Distribution` class. | |
| :class:`!Distribution` is an abstract object that represents the metadata for | |
| a Python `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_. | |
| Get the concrete :class:`!Distribution` subclass instance for an installed | |
| distribution package by calling the :func:`distribution` function:: | |
| >>> from importlib.metadata import distribution # doctest: +SKIP | |
| >>> dist = distribution('wheel') # doctest: +SKIP | |
| >>> type(dist) # doctest: +SKIP | |
| <class 'importlib.metadata.PathDistribution'> | |
| .. function:: distribution(distribution_name) | |
| Return a :class:`Distribution` instance describing the named | |
| distribution package. | |
| Raises :exc:`PackageNotFoundError` if the named distribution | |
| package is not installed in the current Python environment. | |
| Thus, an alternative way to get e.g. the version number is through the | |
| :attr:`Distribution.version` attribute:: | |
| >>> dist.version # doctest: +SKIP | |
| '0.32.3' | |
| The same applies for :func:`entry_points` and :func:`files`. | |
| .. class:: Distribution | |
| Details of an installed distribution package. | |
| Note: different :class:`!Distribution` instances do not currently compare | |
| equal, even if they relate to the same installed distribution and | |
| accordingly have the same attributes. | |
| .. staticmethod:: at(path) | |
| .. classmethod:: from_name(name) | |
| Return a :class:`!Distribution` instance at the given path or | |
| with the given name. | |
| .. classmethod:: discover(*, context=None, **kwargs) | |
| Returns an iterable of :class:`!Distribution` instances for all packages | |
| (see distribution-discovery_). | |
| The optional argument *context* is a :class:`DistributionFinder.Context` | |
| instance, used to modify the search for distributions. Alternatively, | |
| *kwargs* may contain keyword arguments for constructing a new | |
| :class:`!DistributionFinder.Context`. | |
| .. attribute:: metadata | |
| :type: PackageMetadata | |
| Raises :exc:`MetadataNotFound` if the METADATA file is not present in | |
| the distribution. | |
| There are all kinds of additional metadata available on :class:`!Distribution` | |
| instances as a :class:`PackageMetadata` instance:: | |
| >>> dist.metadata['Requires-Python'] # doctest: +SKIP | |
| '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' | |
| >>> dist.metadata['License'] # doctest: +SKIP | |
| 'MIT' | |
| The full set of available metadata is not described here. | |
| See the PyPA `Core metadata specification <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_ for additional details. | |
| .. attribute:: name | |
| :type: str | |
| .. attribute:: requires | |
| :type: list[str] | |
| .. attribute:: version | |
| :type: str | |
| A few metadata fields are also available as shortcut properties. | |
| .. versionadded:: 3.10 | |
| The ``name`` shortcut was added. | |
| .. attribute:: origin | |
| For editable packages, an ``origin`` property may present :pep:`610` | |
| metadata (for non-editable packages, ``origin`` is :const:`None`):: | |
| >>> dist.origin.url | |
| 'file:///path/to/wheel-0.32.3.editable-py3-none-any.whl' | |
| The ``origin`` object follows the `Direct URL Data Structure | |
| <https://packaging.python.org/en/latest/specifications/direct-url-data-structure/>`_. | |
| .. versionadded:: 3.13 | |
| .. attribute:: entry_points | |
| :type: EntryPoints | |
| The entry points provided by this distribution package. | |
| .. attribute:: files | |
| :type: list[PackagePath] | None | |
| All files contained in this distribution package. | |
| Like :func:`files`, this returns :const:`None` if there are no records. | |
| The following two abstract methods need to be implemented when implementing-custom-providers_: | |
| .. method:: locate_file(path) | |
| Like :meth:`!PackagePath.locate`, return a :class:`SimplePath` for the given path. | |
| Takes a :class:`os.PathLike` or a :class:`str`. | |
| .. method:: read_text(filename) | |
| A shortcut for ``distribution.locate_file(filename).read_text()``. | |
| .. _distribution-discovery: | |
| Distribution Discovery | |
| ====================== | |
| By default, this package provides built-in support for discovery of metadata | |
| for file system and zip file `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_\s. | |
| This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: | |
| - ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``. | |
| - ``importlib.metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports. | |
| .. class:: DistributionFinder | |
| A :class:`~importlib.abc.MetaPathFinder` subclass capable of discovering | |
| installed distributions. | |
| Custom providers should implement this interface in order to | |
| supply metadata. | |
| .. class:: Context(**kwargs) | |
| A :class:`!Context` gives a custom provider a means to | |
| solicit additional details from the callers of distribution discovery | |
| functions like :func:`distributions` or :meth:`Distribution.discover` | |
| beyond :attr:`!.name` and :attr:`!.path` when searching | |
| for distributions. | |
| For example, a provider could expose suites of packages in either a | |
| "public" or "private" ``realm``. A caller of distribution discovery | |
| functions may wish to query only for distributions in a particular realm | |
| and could call ``distributions(realm="private")`` to signal to the | |
| custom provider to only include distributions from that | |
| realm. | |
| Each :class:`!DistributionFinder` must expect any parameters and should | |
| attempt to honor the canonical parameters defined below when | |
| appropriate. | |
| See the section on :ref:`implementing-custom-providers` for more details. | |
| .. attribute:: name | |
| Specific name for which a distribution finder should match. | |
| A :attr:`!.name` of ``None`` matches all distributions. | |
| .. attribute:: path | |
| A property providing the sequence of directory paths that a | |
| distribution finder should search. | |
| Typically refers to Python installed package paths such as | |
| "site-packages" directories and defaults to :attr:`sys.path`. | |
| .. function:: distributions(**kwargs) | |
| Returns an iterable of :class:`Distribution` instances for all packages. | |
| The *kwargs* argument may contain either a keyword argument ``context``, a | |
| :class:`DistributionFinder.Context` instance, or pass keyword arguments for | |
| constructing a new :class:`!DistributionFinder.Context`. The | |
| :class:`!DistributionFinder.Context` is used to modify the search for | |
| distributions. | |
| .. _implementing-custom-providers: | |
| Implementing Custom Providers | |
| ============================= | |
| ``importlib.metadata`` address two API surfaces, one for *consumers* | |
| and another for *providers*. Most users are consumers, consuming | |
| metadata provided by the packages. There are other use-cases, however, | |
| where users wish to expose metadata through some other mechanism, | |
| such as alongside a custom importer. Such a use case calls for a | |
| *custom provider*. | |
| Because `Distribution Package <https://packaging.python.org/en/latest/glossary/#term-Distribution-Package>`_ metadata | |
| is not available through :data:`sys.path` searches, or | |
| package loaders directly, | |
| the metadata for a distribution is found through import | |
| system :ref:`finders <finders-and-loaders>`. 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`. | |
| The implementation has hooks integrated into the ``PathFinder``, | |
| serving metadata for distribution packages found on the file system. | |
| 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 | |
| :class:`DistributionFinder` abstract base class, which defines this abstract | |
| method:: | |
| @abc.abstractmethod | |
| def find_distributions(context=DistributionFinder.Context()) -> Iterable[Distribution]: | |
| """Return an iterable of all Distribution instances capable of | |
| loading the metadata for packages for the indicated ``context``. | |
| """ | |
| The :class:`DistributionFinder.Context` object provides | |
| :attr:`~DistributionFinder.Context.path` and | |
| :attr:`~DistributionFinder.Context.name` properties indicating the path to | |
| search and name to match and may supply other relevant context sought by the | |
| consumer. | |
| In practice, to support finding distribution package | |
| metadata in locations other than the file system, subclass | |
| :class:`!Distribution` and implement the abstract methods. Then from | |
| a custom finder, return instances of this derived :class:`!Distribution` in the | |
| ``find_distributions()`` method. | |
| Example | |
| ------- | |
| Imagine a custom finder that loads Python modules from a database:: | |
| class DatabaseImporter(importlib.abc.MetaPathFinder): | |
| def __init__(self, db): | |
| self.db = db | |
| def find_spec(self, fullname, target=None) -> ModuleSpec: | |
| return self.db.spec_from_name(fullname) | |
| sys.meta_path.append(DatabaseImporter(connect_db(...))) | |
| That importer now presumably provides importable modules from a | |
| database, but it provides no metadata or entry points. For this | |
| custom importer to provide metadata, it would also need to implement | |
| :class:`DistributionFinder`:: | |
| from importlib.metadata import DistributionFinder | |
| class DatabaseImporter(DistributionFinder): | |
| ... | |
| def find_distributions(self, context=DistributionFinder.Context()): | |
| query = dict(name=context.name) if context.name else {} | |
| for dist_record in self.db.query_distributions(query): | |
| yield DatabaseDistribution(dist_record) | |
| In this way, ``query_distributions`` would return records for | |
| each distribution served by the database matching the query. For | |
| example, if ``requests-1.0`` is in the database, ``find_distributions`` | |
| would yield a ``DatabaseDistribution`` for ``Context(name='requests')`` | |
| or ``Context(name=None)``. | |
| For the sake of simplicity, this example ignores ``context.path``\. The | |
| ``path`` attribute defaults to ``sys.path`` and is the set of import paths to | |
| be considered in the search. A ``DatabaseImporter`` could potentially function | |
| without any concern for a search path. Assuming the importer does no | |
| partitioning, the "path" would be irrelevant. In order to illustrate the | |
| purpose of ``path``, the example would need to illustrate a more complex | |
| ``DatabaseImporter`` whose behavior varied depending on | |
| ``sys.path``/``PYTHONPATH``. In that case, the ``find_distributions`` should | |
| honor the ``context.path`` and only yield ``Distribution``\ s pertinent to that | |
| path. | |
| ``DatabaseDistribution``, then, would look something like:: | |
| class DatabaseDistribution(importlib.metadata.Distribution): | |
| def __init__(self, record): | |
| self.record = record | |
| def read_text(self, filename): | |
| """ | |
| Read a file like "METADATA" for the current distribution. | |
| """ | |
| if filename == "METADATA": | |
| return f"""Name: {self.record.name} | |
| Version: {self.record.version} | |
| """ | |
| if filename == "entry_points.txt": | |
| return "\n".join( | |
| f"""[{ep.group}]\n{ep.name}={ep.value}""" | |
| for ep in self.record.entry_points) | |
| def locate_file(self, path): | |
| raise RuntimeError("This distribution has no file system") | |
| This basic implementation should provide metadata and entry points for | |
| packages served by the ``DatabaseImporter``, assuming that the | |
| ``record`` supplies suitable ``.name``, ``.version``, and | |
| ``.entry_points`` attributes. | |
| The ``DatabaseDistribution`` may also provide other metadata files, like | |
| ``RECORD`` (required for :attr:`!Distribution.files`) or override the | |
| implementation of :attr:`!Distribution.files`. See the source for more inspiration. |