diff --git a/source/index.rst b/source/index.rst index 70933489a..b56ac57cd 100644 --- a/source/index.rst +++ b/source/index.rst @@ -19,6 +19,7 @@ This guide is maintained on `github current installing distributing + single_source_version additional projects glossary diff --git a/source/single_source_version.rst b/source/single_source_version.rst index 0a80cf3b0..3eea6af50 100644 --- a/source/single_source_version.rst +++ b/source/single_source_version.rst @@ -5,113 +5,46 @@ Single-sourcing the Project Version =================================== :Page Status: Complete -:Last Reviewed: 2015-09-08 +:Last Reviewed: 2024-?? +One of the challenges in building packages is that the version string can be required in multiple places. -There are a few techniques to store the version in your project code without duplicating the value stored in -``setup.py``: +* It needs to be specified when building the package (e.g. in :file:`pyproject.toml`) + - That will assure that it is properly assigned in the distribution file name, and in the installed package metadata. -#. Read the file in ``setup.py`` and parse the version with a regex. Example ( - from `pip setup.py `_):: +* A package may set a top level ``__version__`` attribute to provide runtime access to the version of the imported package. If this is done, the value of ``__version__`` 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 `. - def read(*names, **kwargs): - with io.open( - os.path.join(os.path.dirname(__file__), *names), - encoding=kwargs.get("encoding", "utf8") - ) as fp: - return fp.read() +In any case, The installed distribution version should be accessible using ``importlib.metadata.version("distribution_name")``. - def find_version(*file_paths): - version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") +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. - setup( - ... - version=find_version("package", "__init__.py") - ... - ) +In general, the options are: - .. note:: +1) If the code is in a version control system (VCS), e.g. Git, then the version can be extracted from the VCS. - This technique has the disadvantage of having to deal with complexities of regular expressions. +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. -#. Use an external build tool that either manages updating both locations, or - offers an API that both locations can use. +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 the :file:`__init__.py`, and the build system can extract it at build time. - Few tools you could use, in no particular order, and not necessarily complete: - `bumpversion `_, - `changes `_, `zest.releaser `_. +Consult your build system's documentation for their recommended method. -#. Set the value to a ``__version__`` global variable in a dedicated module in - your project (e.g. ``version.py``), then have ``setup.py`` read and ``exec`` the - value into a variable. +.. _Build system version handling: - Using ``execfile``: +Build System Version Handling +----------------------------- - :: +The following are links to some build system's documentation for handling version strings. - execfile('...sample/version.py') - # now we have a `__version__` variable - # later on we use: __version__ +* `Flit `_ - Using ``exec``: +* `Hatchling `_ - :: +* `PDM `_ - version = {} - with open("...sample/version.py") as fp: - exec(fp.read(), version) - # later on we use: version['__version__'] +* `Setuptools `_ - Example using this technique: `warehouse `_. + - `setuptools_scm `_ -#. Place the value in a simple ``VERSION`` text file and have both ``setup.py`` - and the project code read it. - :: - with open(os.path.join(mypackage_root_dir, 'VERSION')) as version_file: - version = version_file.read().strip() - - An advantage with this technique is that it's not specific to Python. Any - tool can read the version. - - .. warning:: - - With this approach you must make sure that the ``VERSION`` file is included in - all your source and binary distributions. - -#. Set the value in ``setup.py``, and have the project code use the - ``pkg_resources`` API. - - :: - - import pkg_resources - assert pkg_resources.get_distribution('pip').version == '1.2.0' - - Be aware that the ``pkg_resources`` API only knows about what's in the - installation metadata, which is not necessarily the code that's currently - imported. - - -#. Set the value to ``__version__`` in ``sample/__init__.py`` and import - ``sample`` in ``setup.py``. - - :: - - import sample - setup( - ... - version=sample.__version__ - ... - ) - - Although this technique is common, beware that it will fail if - ``sample/__init__.py`` imports packages from ``install_requires`` - dependencies, which will very likely not be installed yet when ``setup.py`` - is run.