bpo-34632: Add importlib.metadata (GH-12547) · python/cpython@1bbf7b6 (original) (raw)
``
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
``
35
`+
something into it::
`
``
36
+
``
37
`+
.. highlight:: none
`
``
38
+
``
39
`+
$ python3 -m venv example
`
``
40
`+
$ source example/bin/activate
`
``
41
`+
(example) $ pip install wheel
`
``
42
+
``
43
You can get the version string for ``wheel`` by running the following::
``
44
+
``
45
`+
.. highlight:: none
`
``
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
`` +
``
``
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
`+
`
``
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.
`