Skip to content

Commit 24eca8b

Browse files
authored
Merge pull request #1612 from ncoghlan/runtime-version-access-squash-merge
Add notes on runtime version access
2 parents 363e4a6 + e069985 commit 24eca8b

5 files changed

+93
-187
lines changed

source/discussions/single-source-version.rst

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,49 @@
1-
.. _`Single sourcing the version discussion`:
1+
.. _single-source-version:
22

33
===================================
44
Single-sourcing the Project Version
55
===================================
66

77
:Page Status: Complete
8-
:Last Reviewed: 2024-08-24
8+
:Last Reviewed: 2024-10-07
99

10-
One of the challenges in building packages is that the version string can be required in multiple places.
10+
Many Python :term:`distribution packages <Distribution Package>` publish a single
11+
Python :term:`import package <Import Package>` where it is desired that the runtime
12+
``__version__`` attribute on the import package report the same version specifier
13+
as :func:`importlib.metadata.version` reports for the distribution package
14+
(as described in :ref:`runtime-version-access`).
1115

12-
* It needs to be specified when building the package (e.g. in :file:`pyproject.toml`)
13-
This will make it available in the installed package’s metadata, from where it will be accessible at runtime using ``importlib.metadata.version("distribution_name")``.
16+
It is also frequently desired that this version information be derived from a version
17+
control system *tag* (such as ``v1.2.3``) rather than being manually updated in the
18+
source code.
1419

15-
* A package may set a module attribute (e.g., ``__version__``) to provide an alternative means of runtime access to the version of the imported package. If this is done, the value of the attribute and that used by the build system to set the distribution's version should be kept in sync in :ref:`the build systems's recommended way <Build system version handling>`.
20+
Some projects may choose to simply live with the data entry duplication, and rely
21+
on automated testing to ensure the different values do not diverge.
1622

17-
* If the code is in in a version control system (VCS), e.g. Git, the version may appear in a *tag* such as ``v1.2.3``.
18-
19-
To ensure that version numbers do not get out of sync, it is recommended that there is a single source of truth for the version number.
23+
Alternatively, a project's chosen build system may offer a way to define a single
24+
source of truth for the version number.
2025

2126
In general, the options are:
2227

23-
1) If the code is in a version control system (VCS), e.g. Git, then the version can be extracted from the VCS.
24-
25-
2) The version can be hard-coded into the :file:`pyproject.toml` file -- and the build system can copy it into other locations it may be required.
28+
1) If the code is in a version control system (VCS), such as Git, then the version can be extracted from the VCS.
2629

27-
3) The version string can be hard-coded into the source code -- either in a special purpose file, such as :file:`_version.txt`, or as a attribute in a module, such as :file:`__init__.py`, and the build system can extract it at build time.
30+
2) The version can be hard-coded into the :file:`pyproject.toml` file -- and the build system can copy it
31+
into other locations it may be required.
2832

33+
3) The version string can be hard-coded into the source code -- either in a special purpose file,
34+
such as :file:`_version.txt` (which must then be shipped as part of the project's source distribution
35+
package), or as an attribute in a particular module, such as :file:`__init__.py`. The build
36+
system can then extract it from the runtime location at build time.
2937

3038
Consult your build system's documentation for their recommended method.
3139

40+
When the intention is that a distribution package and its associated import package
41+
share the same version, it is recommended that the project include an automated test
42+
case that ensures ``import_name.__version__`` and ``importlib.metadata.version("dist-name")``
43+
report the same value (note: for many projects, ``import_name`` and ``dist-name`` will
44+
be the same name).
45+
46+
3247
.. _Build system version handling:
3348

3449
Build System Version Handling

source/discussions/versioning.rst

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ numbering scheme that readily conveys the approximate age of a release, but
153153
doesn't otherwise commit to a particular release cadence within the year.
154154

155155

156-
157156
Local version identifiers
158157
=========================
159158

@@ -172,6 +171,55 @@ since the latest release, setuptools-scm generates a version like
172171
"0.5.dev1+gd00980f", or if the repository has untracked changes, like
173172
"0.5.dev1+gd00980f.d20231217".
174173

174+
.. _runtime-version-access:
175+
176+
Accessing version information at runtime
177+
========================================
178+
179+
Version information for all :term:`distribution packages <Distribution Package>`
180+
that are locally available in the current environment can be obtained at runtime
181+
using the standard library's :func:`importlib.metadata.version` function::
182+
183+
>>> importlib.metadata.version("cryptography")
184+
'41.0.7'
185+
186+
Many projects also choose to version their top level
187+
:term:`import packages <Import Package>` by providing a package level
188+
``__version__`` attribute::
189+
190+
>>> import cryptography
191+
>>> cryptography.__version__
192+
'41.0.7'
193+
194+
This technique can be particularly valuable for CLI applications which want
195+
to ensure that version query invocations (such as ``pip -V``) run as quickly
196+
as possible.
197+
198+
Package publishers wishing to ensure their reported distribution package and
199+
import package versions are consistent with each other can review the
200+
:ref:`single-source-version` discussion for potential approaches to doing so.
201+
202+
As import packages and modules are not *required* to publish runtime
203+
version information in this way (see the rejected proposal in
204+
:pep:`PEP 396 <396>`), the ``__version__`` attribute should either only be
205+
queried with interfaces that are known to provide it (such as a project
206+
querying its own version or the version of one of its direct dependencies),
207+
or else the querying code should be designed to handle the case where the
208+
attribute is missing [#fallback-to-dist-version]_.
209+
210+
Some projects may need to publish version information for external APIs
211+
that don't meet the requirements for Python distribution package
212+
:ref:`version specifiers <version-specifiers>`. Such projects should
213+
define their own project-specific ways of obtaining the relevant information
214+
at runtime. For example, the standard library's :mod:`ssl` module offers
215+
multiple ways to access the underlying OpenSSL library version::
216+
217+
>>> ssl.OPENSSL_VERSION
218+
'OpenSSL 3.2.2 4 Jun 2024'
219+
>>> ssl.OPENSSL_VERSION_INFO
220+
(3, 2, 0, 2, 0)
221+
>>> hex(ssl.OPENSSL_VERSION_NUMBER)
222+
'0x30200020'
175223

176224
--------------------------------------------------------------------------------
177225

@@ -184,6 +232,15 @@ since the latest release, setuptools-scm generates a version like
184232
Brett Cannon <semver-brett-cannon_>`_. For a humoristic take, read about
185233
ZeroVer_.
186234
235+
.. [#fallback-to-dist-version] A full list mapping the top level names available
236+
for import to the distribution packages that provide those import packages and
237+
modules may be obtained through the standard library's
238+
:func:`importlib.metadata.packages_distributions` function. This means that
239+
even code that is attempting to infer a version to report for all importable
240+
top-level names has a means to fall back to reporting the distribution
241+
version information if no ``__version__`` attribute is defined. Only standard
242+
library modules, and modules added via means other than Python package
243+
installation would fail to have version information reported in that case.
187244
188245
189246
.. _zerover: https://0ver.org

source/guides/section-build-and-publish.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Building and Publishing
77

88
writing-pyproject-toml
99
distributing-packages-using-setuptools
10-
single-sourcing-package-version
1110
dropping-older-python-versions
1211
packaging-binary-extensions
1312
packaging-namespace-packages
Lines changed: 5 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,8 @@
1-
.. _`Single sourcing the version`:
1+
:orphan:
22

3-
===================================
4-
Single-sourcing the package version
5-
===================================
3+
.. meta::
4+
:http-equiv=refresh: 0; url=../../discussions/single-source-version/
65

7-
.. todo:: Update this page for build backends other than setuptools.
6+
Redirecting stale single-source package version link...
87

9-
There are many techniques to maintain a single source of truth for the version
10-
number of your project:
11-
12-
#. Read the file in :file:`setup.py` and get the version. Example (from `pip setup.py
13-
<https://github.com/pypa/pip/blob/003c7ac/setup.py>`_)::
14-
15-
import codecs
16-
import os.path
17-
18-
def read(rel_path):
19-
here = os.path.abspath(os.path.dirname(__file__))
20-
with codecs.open(os.path.join(here, rel_path), 'r') as fp:
21-
return fp.read()
22-
23-
def get_version(rel_path):
24-
for line in read(rel_path).splitlines():
25-
if line.startswith('__version__'):
26-
delim = '"' if '"' in line else "'"
27-
return line.split(delim)[1]
28-
else:
29-
raise RuntimeError("Unable to find version string.")
30-
31-
setup(
32-
...
33-
version=get_version("package/__init__.py")
34-
...
35-
)
36-
37-
.. note::
38-
39-
As of the release of setuptools 46.4.0, one can accomplish the same
40-
thing by instead placing the following in the project's
41-
:file:`setup.cfg` file (replacing "package" with the import name of the
42-
package):
43-
44-
.. code-block:: ini
45-
46-
[metadata]
47-
version = attr: package.__version__
48-
49-
As of the release of setuptools 61.0.0, one can specify the
50-
version dynamically in the project's :file:`pyproject.toml` file.
51-
52-
.. code-block:: toml
53-
54-
[project]
55-
name = "package"
56-
dynamic = ["version"]
57-
58-
[tool.setuptools.dynamic]
59-
version = {attr = "package.__version__"}
60-
61-
Please be aware that declarative config indicators, including the
62-
``attr:`` directive, are not supported in parameters to
63-
:file:`setup.py`.
64-
65-
#. Use an external build tool that either manages updating both locations, or
66-
offers an API that both locations can use.
67-
68-
Few tools you could use, in no particular order, and not necessarily complete:
69-
`bump2version <https://pypi.org/project/bump2version>`_,
70-
`changes <https://pypi.org/project/changes>`_,
71-
`commitizen <https://pypi.org/project/commitizen>`_,
72-
`zest.releaser <https://pypi.org/project/zest.releaser>`_.
73-
74-
75-
#. Set the value to a ``__version__`` global variable in a dedicated module in
76-
your project (e.g. :file:`version.py`), then have :file:`setup.py` read and
77-
``exec`` the value into a variable.
78-
79-
::
80-
81-
version = {}
82-
with open("...sample/version.py") as fp:
83-
exec(fp.read(), version)
84-
# later on we use: version['__version__']
85-
86-
Example using this technique: `warehouse <https://github.com/pypa/warehouse/blob/64ca42e42d5613c8339b3ec5e1cb7765c6b23083/warehouse/__about__.py>`_.
87-
88-
#. Place the value in a simple ``VERSION`` text file and have both
89-
:file:`setup.py` and the project code read it.
90-
91-
::
92-
93-
with open(os.path.join(mypackage_root_dir, 'VERSION')) as version_file:
94-
version = version_file.read().strip()
95-
96-
An advantage with this technique is that it's not specific to Python. Any
97-
tool can read the version.
98-
99-
.. warning::
100-
101-
With this approach you must make sure that the ``VERSION`` file is included in
102-
all your source and binary distributions (e.g. add ``include VERSION`` to your
103-
:file:`MANIFEST.in`).
104-
105-
#. Set the value in :file:`setup.py`, and have the project code use the
106-
``importlib.metadata`` API to fetch the value at runtime.
107-
(``importlib.metadata`` was introduced in Python 3.8 and is available to
108-
older versions as the ``importlib-metadata`` project.) An installed
109-
project's version can be fetched with the API as follows::
110-
111-
import sys
112-
113-
if sys.version_info >= (3, 8):
114-
from importlib import metadata
115-
else:
116-
import importlib_metadata as metadata
117-
118-
assert metadata.version('pip') == '1.2.0'
119-
120-
Be aware that the ``importlib.metadata`` API only knows about what's in the
121-
installation metadata, which is not necessarily the code that's currently
122-
imported.
123-
124-
If a project uses this method to fetch its version at runtime, then its
125-
``install_requires`` value needs to be edited to install
126-
``importlib-metadata`` on pre-3.8 versions of Python like so::
127-
128-
setup(
129-
...
130-
install_requires=[
131-
...
132-
'importlib-metadata >= 1.0 ; python_version < "3.8"',
133-
...
134-
],
135-
...
136-
)
137-
138-
An older (and less efficient) alternative to ``importlib.metadata`` is the
139-
``pkg_resources`` API provided by ``setuptools``::
140-
141-
import pkg_resources
142-
assert pkg_resources.get_distribution('pip').version == '1.2.0'
143-
144-
If a project uses ``pkg_resources`` to fetch its own version at runtime,
145-
then ``setuptools`` must be added to the project's ``install_requires``
146-
list.
147-
148-
Example using this technique: `setuptools <https://github.com/pypa/setuptools/blob/main/setuptools/version.py>`_.
149-
150-
151-
#. Set the value to ``__version__`` in ``sample/__init__.py`` and import
152-
``sample`` in :file:`setup.py`.
153-
154-
::
155-
156-
import sample
157-
setup(
158-
...
159-
version=sample.__version__
160-
...
161-
)
162-
163-
.. warning::
164-
165-
Although this technique is common, beware that it will fail if
166-
``sample/__init__.py`` imports packages from ``install_requires``
167-
dependencies, which will very likely not be installed yet when
168-
:file:`setup.py` is run.
169-
170-
171-
#. Keep the version number in the tags of a version control system (Git, Mercurial, etc)
172-
instead of in the code, and automatically extract it from there using
173-
`setuptools_scm <https://pypi.org/project/setuptools-scm/>`_.
8+
If the page doesn't automatically refresh, see :ref:`single-source-version`.

source/guides/writing-pyproject-toml.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ This field is required, although it is often marked as dynamic using
163163
dynamic = ["version"]
164164
165165
This allows use cases such as filling the version from a ``__version__``
166-
attribute or a Git tag. Consult :ref:`Single sourcing the version` for more
167-
details.
166+
attribute or a Git tag. Consult the :ref:`single-source-version`
167+
discussion for more details.
168168

169169

170170
Dependencies and requirements

0 commit comments

Comments
 (0)