Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 1 | .. _using: |
| 2 | |
| 3 | ========================== |
| 4 | Using importlib.metadata |
| 5 | ========================== |
| 6 | |
| 7 | .. note:: |
| 8 | This functionality is provisional and may deviate from the usual |
| 9 | version semantics of the standard library. |
| 10 | |
| 11 | ``importlib.metadata`` is a library that provides for access to installed |
| 12 | package metadata. Built in part on Python's import system, this library |
| 13 | intends to replace similar functionality in the `entry point |
| 14 | API`_ and `metadata API`_ of ``pkg_resources``. Along with |
| 15 | ``importlib.resources`` in `Python 3.7 |
| 16 | and newer`_ (backported as `importlib_resources`_ for older versions of |
| 17 | Python), this can eliminate the need to use the older and less efficient |
| 18 | ``pkg_resources`` package. |
| 19 | |
| 20 | By "installed package" we generally mean a third-party package installed into |
| 21 | Python's ``site-packages`` directory via tools such as `pip |
| 22 | <https://pypi.org/project/pip/>`_. Specifically, |
| 23 | it means a package with either a discoverable ``dist-info`` or ``egg-info`` |
| 24 | directory, and metadata defined by `PEP 566`_ or its older specifications. |
| 25 | By default, package metadata can live on the file system or in zip archives on |
| 26 | ``sys.path``. Through an extension mechanism, the metadata can live almost |
| 27 | anywhere. |
| 28 | |
| 29 | |
| 30 | Overview |
| 31 | ======== |
| 32 | |
| 33 | Let's say you wanted to get the version string for a package you've installed |
| 34 | using ``pip``. We start by creating a virtual environment and installing |
Anthony Sottile | 22ccb0b | 2019-05-26 07:30:52 -0700 | [diff] [blame] | 35 | something into it: |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 36 | |
Anthony Sottile | 22ccb0b | 2019-05-26 07:30:52 -0700 | [diff] [blame] | 37 | .. code-block:: shell-session |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 38 | |
| 39 | $ python3 -m venv example |
| 40 | $ source example/bin/activate |
| 41 | (example) $ pip install wheel |
| 42 | |
Anthony Sottile | 22ccb0b | 2019-05-26 07:30:52 -0700 | [diff] [blame] | 43 | You can get the version string for ``wheel`` by running the following: |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 44 | |
Anthony Sottile | 22ccb0b | 2019-05-26 07:30:52 -0700 | [diff] [blame] | 45 | .. code-block:: pycon |
Jason R. Coombs | 1bbf7b6 | 2019-05-24 19:59:01 -0400 | [diff] [blame] | 46 | |
| 47 | (example) $ python |
| 48 | >>> from importlib.metadata import version # doctest: +SKIP |
| 49 | >>> version('wheel') # doctest: +SKIP |
| 50 | '0.32.3' |
| 51 | |
| 52 | You can also get the set of entry points keyed by group, such as |
| 53 | ``console_scripts``, ``distutils.commands`` and others. Each group contains a |
| 54 | sequence of :ref:`EntryPoint <entry-points>` objects. |
| 55 | |
| 56 | You can get the :ref:`metadata for a distribution <metadata>`:: |
| 57 | |
| 58 | >>> list(metadata('wheel')) # doctest: +SKIP |
| 59 | ['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'] |
| 60 | |
| 61 | You can also get a :ref:`distribution's version number <version>`, list its |
| 62 | :ref:`constituent files <files>`, and get a list of the distribution's |
| 63 | :ref:`requirements`. |
| 64 | |
| 65 | |
| 66 | Functional API |
| 67 | ============== |
| 68 | |
| 69 | This package provides the following functionality via its public API. |
| 70 | |
| 71 | |
| 72 | .. _entry-points: |
| 73 | |
| 74 | Entry points |
| 75 | ------------ |
| 76 | |
| 77 | The ``entry_points()`` function returns a dictionary of all entry points, |
| 78 | keyed by group. Entry points are represented by ``EntryPoint`` instances; |
| 79 | each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and |
| 80 | a ``.load()`` method to resolve the value. |
| 81 | |
| 82 | >>> eps = entry_points() # doctest: +SKIP |
| 83 | >>> list(eps) # doctest: +SKIP |
| 84 | ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] |
| 85 | >>> scripts = eps['console_scripts'] # doctest: +SKIP |
| 86 | >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] # doctest: +SKIP |
| 87 | >>> wheel # doctest: +SKIP |
| 88 | EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') |
| 89 | >>> main = wheel.load() # doctest: +SKIP |
| 90 | >>> main # doctest: +SKIP |
| 91 | <function main at 0x103528488> |
| 92 | |
| 93 | The ``group`` and ``name`` are arbitrary values defined by the package author |
| 94 | and usually a client will wish to resolve all entry points for a particular |
| 95 | group. Read `the setuptools docs |
| 96 | <https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`_ |
| 97 | for more information on entrypoints, their definition, and usage. |
| 98 | |
| 99 | |
| 100 | .. _metadata: |
| 101 | |
| 102 | Distribution metadata |
| 103 | --------------------- |
| 104 | |
| 105 | Every distribution includes some metadata, which you can extract using the |
| 106 | ``metadata()`` function:: |
| 107 | |
| 108 | >>> wheel_metadata = metadata('wheel') # doctest: +SKIP |
| 109 | |
| 110 | The keys of the returned data structure [#f1]_ name the metadata keywords, and |
| 111 | their values are returned unparsed from the distribution metadata:: |
| 112 | |
| 113 | >>> wheel_metadata['Requires-Python'] # doctest: +SKIP |
| 114 | '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' |
| 115 | |
| 116 | |
| 117 | .. _version: |
| 118 | |
| 119 | Distribution versions |
| 120 | --------------------- |
| 121 | |
| 122 | The ``version()`` function is the quickest way to get a distribution's version |
| 123 | number, as a string:: |
| 124 | |
| 125 | >>> version('wheel') # doctest: +SKIP |
| 126 | '0.32.3' |
| 127 | |
| 128 | |
| 129 | .. _files: |
| 130 | |
| 131 | Distribution files |
| 132 | ------------------ |
| 133 | |
| 134 | You can also get the full set of files contained within a distribution. The |
| 135 | ``files()`` function takes a distribution package name and returns all of the |
| 136 | files installed by this distribution. Each file object returned is a |
| 137 | ``PackagePath``, a `pathlib.Path`_ derived object with additional ``dist``, |
| 138 | ``size``, and ``hash`` properties as indicated by the metadata. For example:: |
| 139 | |
| 140 | >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] # doctest: +SKIP |
| 141 | >>> util # doctest: +SKIP |
| 142 | PackagePath('wheel/util.py') |
| 143 | >>> util.size # doctest: +SKIP |
| 144 | 859 |
| 145 | >>> util.dist # doctest: +SKIP |
| 146 | <importlib.metadata._hooks.PathDistribution object at 0x101e0cef0> |
| 147 | >>> util.hash # doctest: +SKIP |
| 148 | <FileHash mode: sha256 value: bYkw5oMccfazVCoYQwKkkemoVyMAFoR34mmKBx8R1NI> |
| 149 | |
| 150 | Once you have the file, you can also read its contents:: |
| 151 | |
| 152 | >>> print(util.read_text()) # doctest: +SKIP |
| 153 | import base64 |
| 154 | import sys |
| 155 | ... |
| 156 | def as_bytes(s): |
| 157 | if isinstance(s, text_type): |
| 158 | return s.encode('utf-8') |
| 159 | return s |
| 160 | |
| 161 | |
| 162 | .. _requirements: |
| 163 | |
| 164 | Distribution requirements |
| 165 | ------------------------- |
| 166 | |
| 167 | To get the full set of requirements for a distribution, use the ``requires()`` |
| 168 | function. Note that this returns an iterator:: |
| 169 | |
| 170 | >>> list(requires('wheel')) # doctest: +SKIP |
| 171 | ["pytest (>=3.0.0) ; extra == 'test'"] |
| 172 | |
| 173 | |
| 174 | Distributions |
| 175 | ============= |
| 176 | |
| 177 | While the above API is the most common and convenient usage, you can get all |
| 178 | of that information from the ``Distribution`` class. A ``Distribution`` is an |
| 179 | abstract object that represents the metadata for a Python package. You can |
| 180 | get the ``Distribution`` instance:: |
| 181 | |
| 182 | >>> from importlib.metadata import distribution # doctest: +SKIP |
| 183 | >>> dist = distribution('wheel') # doctest: +SKIP |
| 184 | |
| 185 | Thus, an alternative way to get the version number is through the |
| 186 | ``Distribution`` instance:: |
| 187 | |
| 188 | >>> dist.version # doctest: +SKIP |
| 189 | '0.32.3' |
| 190 | |
| 191 | There are all kinds of additional metadata available on the ``Distribution`` |
| 192 | instance:: |
| 193 | |
| 194 | >>> d.metadata['Requires-Python'] # doctest: +SKIP |
| 195 | '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' |
| 196 | >>> d.metadata['License'] # doctest: +SKIP |
| 197 | 'MIT' |
| 198 | |
| 199 | The full set of available metadata is not described here. See `PEP 566 |
| 200 | <https://www.python.org/dev/peps/pep-0566/>`_ for additional details. |
| 201 | |
| 202 | |
| 203 | Extending the search algorithm |
| 204 | ============================== |
| 205 | |
| 206 | Because package metadata is not available through ``sys.path`` searches, or |
| 207 | package loaders directly, the metadata for a package is found through import |
| 208 | system `finders`_. To find a distribution package's metadata, |
| 209 | ``importlib.metadata`` queries the list of `meta path finders`_ on |
| 210 | `sys.meta_path`_. |
| 211 | |
| 212 | By default ``importlib.metadata`` installs a finder for distribution packages |
| 213 | found on the file system. This finder doesn't actually find any *packages*, |
| 214 | but it can find the packages' metadata. |
| 215 | |
| 216 | The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the |
| 217 | interface expected of finders by Python's import system. |
| 218 | ``importlib.metadata`` extends this protocol by looking for an optional |
| 219 | ``find_distributions`` callable on the finders from |
| 220 | ``sys.meta_path``. If the finder has this method, it must return |
| 221 | an iterator over instances of the ``Distribution`` abstract class. This |
| 222 | method must have the signature:: |
| 223 | |
| 224 | def find_distributions(name=None, path=None): |
| 225 | """Return an iterable of all Distribution instances capable of |
| 226 | loading the metadata for packages matching the name |
| 227 | (or all names if not supplied) along the paths in the list |
| 228 | of directories ``path`` (defaults to sys.path). |
| 229 | """ |
| 230 | |
| 231 | What this means in practice is that to support finding distribution package |
| 232 | metadata in locations other than the file system, you should derive from |
| 233 | ``Distribution`` and implement the ``load_metadata()`` method. This takes a |
| 234 | single argument which is the name of the package whose metadata is being |
| 235 | found. This instance of the ``Distribution`` base abstract class is what your |
| 236 | finder's ``find_distributions()`` method should return. |
| 237 | |
| 238 | |
| 239 | .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points |
| 240 | .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api |
| 241 | .. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources |
| 242 | .. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html |
| 243 | .. _`PEP 566`: https://www.python.org/dev/peps/pep-0566/ |
| 244 | .. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders |
| 245 | .. _`meta path finders`: https://docs.python.org/3/glossary.html#term-meta-path-finder |
| 246 | .. _`sys.meta_path`: https://docs.python.org/3/library/sys.html#sys.meta_path |
| 247 | .. _`pathlib.Path`: https://docs.python.org/3/library/pathlib.html#pathlib.Path |
| 248 | |
| 249 | |
| 250 | .. rubric:: Footnotes |
| 251 | |
| 252 | .. [#f1] Technically, the returned distribution metadata object is an |
| 253 | `email.message.Message |
| 254 | <https://docs.python.org/3/library/email.message.html#email.message.EmailMessage>`_ |
| 255 | instance, but this is an implementation detail, and not part of the |
| 256 | stable API. You should only use dictionary-like methods and syntax |
| 257 | to access the metadata contents. |