|
1 |
| -.. _`Single sourcing the version`: |
| 1 | +:orphan: |
2 | 2 |
|
3 |
| -=================================== |
4 |
| -Single-sourcing the package version |
5 |
| -=================================== |
| 3 | +.. meta:: |
| 4 | + :http-equiv=refresh: 0; url=../../discussions/single-source-version/ |
6 | 5 |
|
7 |
| -.. todo:: Update this page for build backends other than setuptools. |
| 6 | +Redirecting stale single-source package version link... |
8 | 7 |
|
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`. |
0 commit comments