84

With distutils, setuptools, etc. a package version is specified in setup.py:

# file: setup.py
...
setup(
name='foobar',
version='1.0.0',
# other attributes
)

I would like to be able to access the same version number from within the package:

>>> import foobar
>>> foobar.__version__
'1.0.0'

I could add __version__ = '1.0.0' to my package's __init__.py, but I would also like to include additional imports in my package to create a simplified interface to the package:

# file: __init__.py

from foobar import foo
from foobar.bar import Bar

__version__ = '1.0.0'

and

# file: setup.py

from foobar import __version__
...
setup(
name='foobar',
version=__version__,
# other attributes
)

However, these additional imports can cause the installation of foobar to fail if they import other packages that are not yet installed. What is the correct way to share package version with setup.py and the package?

Jace Browning
  • 11,699
  • 10
  • 66
  • 90
  • 5
    To maintain a single source of truth for the version number, there are basically [5 common patterns](https://milkr.io/kfei/5-common-patterns-to-version-your-Python-package) you can do. – KF Lin Jun 22 '16 at 09:07
  • 1
    I have relevant answer here https://stackoverflow.com/a/45656438/64313 – cmcginty Aug 13 '17 at 02:12
  • 2
    Also see https://packaging.python.org/guides/single-sourcing-package-version/ – djvg Oct 06 '21 at 08:27
  • [This answer](https://stackoverflow.com/a/61960231/2641825) summarizes different options "to maintain a single source of truth for the version number of your project", including a link to the [python packaging user guide](https://packaging.python.org/en/latest/guides/single-sourcing-package-version/). – Paul Rougieux May 31 '22 at 11:19

9 Answers9

97

Set the version in setup.py only, and read your own version with pkg_resources, effectively querying the setuptools metadata:

file: setup.py

setup(
    name='foobar',
    version='1.0.0',
    # other attributes
)

file: __init__.py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

To make this work in all cases, where you could end up running this without having installed it, test for DistributionNotFound and the distribution location:

from pkg_resources import get_distribution, DistributionNotFound
import os.path

try:
    _dist = get_distribution('foobar')
    # Normalize case for Windows systems
    dist_loc = os.path.normcase(_dist.location)
    here = os.path.normcase(__file__)
    if not here.startswith(os.path.join(dist_loc, 'foobar')):
        # not installed, but there is another version that *is*
        raise DistributionNotFound
except DistributionNotFound:
    __version__ = 'Please install this project with setup.py'
else:
    __version__ = _dist.version
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • 2
    If this actually works reliably, it's much more elegant than my answer ... which makes me wonder why I haven't seen it elsewhere. Does anyone know whether [this](https://github.com/pypa/pip/issues/401) is a real concern? If it does report the wrong version number, its elegance is neither here nor there ... – Zero Piraeus Jul 14 '13 at 12:46
  • 1
    @MartijnPieters: If I understand correctly, this will work for `setup.py install`, `setup.py develop`, but not running directly from source (which probably doesn't matter too much)? – Jace Browning Jul 14 '13 at 13:44
  • @JaceBrowning: Indeed, this does require that metadata has been generated. You can detect that case (catch the exception or check that `get_distribution()` returned the *current* package (verify the path)) and set the version to 'please run setup.py' or similar. – Martijn Pieters Jul 14 '13 at 14:05
  • @MartijnPieters: Thanks! I ended up catching the exception: http://stackoverflow.com/a/17640285/429533 – Jace Browning Jul 14 '13 at 14:30
  • 1
    then why does authors of `GitPython` using totally different approach by saving version in a file named VERSION? – Ciasto piekarz Feb 24 '14 at 18:07
  • 1
    @san: Because everyone is free to do things differently? I cannot answer that, ask the authors of GitPython. – Martijn Pieters Feb 24 '14 at 18:08
  • Cool, So doing that way doesn't seem wrong either, python gives the flexibility to do things differently until there is a standard set. – Ciasto piekarz Feb 24 '14 at 18:35
  • 21
    I don't like this solution: `__version__` is resolved at run-time instead of at build-time as it should. IMHO I prefer having a static `__version__` inside the source tree, and read it at build-time with some code in `setup.py` like in the [answer](http://stackoverflow.com/a/17626524/1499402) below. – Stefano M Dec 30 '14 at 10:30
  • @StefanoM: There is no 'build-time' here; reading `__version__` from another file is **still reading the version at run-time**. The only difference is how it is being read, by Python as source code or manually. – Martijn Pieters Dec 30 '14 at 10:33
  • 3
    I mean the difference between `__version__ = "x.y.z"` (which is parsed once by `setup.py` at build-time) and `__version__ = some_weird_function()` which is evaluated at run-time to recover info present only in `setup.py` and in `foobar.egg-info`. – Stefano M Dec 30 '14 at 10:47
  • 1
    @StefanoM: both are still read by Python code at run-time, all you did is change where it is stored. – Martijn Pieters Dec 30 '14 at 12:02
  • 3
    Agreed: my wording is not correct, as Python is an interpreted language. However it is important to remark the difference between a possible failure at build-time (if `setup.py` is not able to parse `__version__ = 'x.y.z'`) or at run time (if `get_distribution('foobar')` fails to recover the correct info.) Your approach of course has many advantages, like the possibility of changing version number at build-time: `python setup.py build --tag-date`. What has to go into `__version__`: something burnt into the source tree, or some metadata computed at build-time and recovered at run-time? – Stefano M Dec 30 '14 at 12:36
  • @StefanoM: I always make it part of the source, updating after a release (so the revision control reflects the next version being built). – Martijn Pieters Dec 30 '14 at 12:39
  • 9
    I have to agree with @StefanoM that this solution is sub-optimal. The reason I find it misleading is that if you have both an installed and a development version on your system, it will always show the installed one, no matter which one is actually imported. – greschd Apr 22 '15 at 13:25
  • 2
    @greschd: your dev version should be installed with `setup.py develop` though. – Martijn Pieters Apr 22 '15 at 13:30
  • @MartijnPieters wouldn't that overwrite an installed version? I need to run a stable build of my package alongside the development version. – greschd Apr 22 '15 at 13:39
  • 2
    @greschd Always use a virtualenv! You rarely need to install packages globally anymore. – Martijn Pieters Apr 22 '15 at 13:40
  • 2
    This approach may be sensitive to whether `python setup.py install` or `pip install` was used. It has been observed to fail in the former case. May be related to [`pip` issue 3045](https://github.com/pypa/pip/issues/3045). – 0 _ Nov 29 '15 at 08:34
  • `dist_loc = os.path.realpath( dist_loc )` and `here = os.path.realpath( here )` should be added. Otherwise, if there is symbolic link, the `if not ...` statement will be true. In my test, the `dist_loc` is already the realpath, but `here` is not. I'm not sure whether this is true all the time, so adding two `os.path.realpath()` will guarantee that the issue be absolutely solved. – zijuexiansheng May 30 '17 at 23:02
  • 2
    Nice, but unfortunately it wakes the hobgoblin by violating the current [PEP8 recommendation](https://www.python.org/dev/peps/pep-0008/#module-level-dunder-names): `__version__, etc. should be placed after the module docstring but before any import statements except from __future__ imports.` (Here we have to import pkg_resources before defining `__version__`) – AXO Feb 09 '18 at 12:12
  • 1
    @AXO: PEP8 is a recommendation, not a requirement, and you *can't* reverse the order here, because the value is set from a function that needs importing first. That's an excellent reason to ignore the recommendation here. – Martijn Pieters Feb 09 '18 at 12:55
  • 1
    –1 This adds a *runtime* dependency on `pkg_resources` (setuptools), which your distribution may not have had otherwise. For something small like getting the version number, that's overkill. – wim Feb 13 '18 at 16:41
  • 1
    @wim: with setuptools being the norm, pkg_resources is already installed for 99.9% of popular projects. – Martijn Pieters Feb 13 '18 at 16:42
  • 1
    Nonetheless, it's a 3rd-party dep. Even `pip` vendors it. The stdlib thing is still `distutils`, And I would argue that `setuptools` is not the norm for runtime (`install_requires`) though it may be the norm for `setup_requires` for the majority of popular projects. – wim Feb 13 '18 at 16:44
  • How does this answer differ from https://stackoverflow.com/a/2073599/9769953 ? Both are essentially the same, just a different use of `pkg_resources` functions. Is there a preference? – 9769953 Mar 21 '23 at 11:24
25

I don't believe there's a canonical answer to this, but my method (either directly copied or slightly tweaked from what I've seen in various other places) is as follows:

Folder heirarchy (relevant files only):

package_root/
 |- main_package/
 |   |- __init__.py
 |   `- _version.py
 `- setup.py

main_package/_version.py:

"""Version information."""

# The following line *must* be the last in the module, exactly as formatted:
__version__ = "1.0.0"

main_package/__init__.py:

"""Something nice and descriptive."""

from main_package.some_module import some_function_or_class
# ... etc.
from main_package._version import __version__

__all__ = (
    some_function_or_class,
    # ... etc.
)

setup.py:

from setuptools import setup

setup(
    version=open("main_package/_version.py").readlines()[-1].split()[-1].strip("\"'"),
    # ... etc.
)

... which is ugly as sin ... but it works, and I've seen it or something like it in packages distributed by people who I'd expect to know a better way if there were one.

Zero Piraeus
  • 56,143
  • 27
  • 150
  • 160
  • 7
    **tl;dr: Don't use imports in setup.py, read the version from a file.** I'll have to think about this for a while to decide if I like the approach... – Jace Browning Jul 13 '13 at 17:07
  • 2
    @JaceBrowning yeah, that's a fair summary ... I suspect any solution would have to be a variant of this, since it's importing the package in setup.py that causes problems. – Zero Piraeus Jul 13 '13 at 20:16
  • I wonder if `setuptools` or `distutils` has a function to do this more gracefully? – Jace Browning Jul 13 '13 at 20:31
  • 11
    Having `__version__ = "x.y.z"` in the source and parsing it within `setup.py` is *definitely* the correct solution, IMHO. Much better that relying on run time magic. – Stefano M Dec 30 '14 at 10:22
  • And of course you can `import re` in your `setup.py`, if you'd like something more elegant or robust. – Stefano M Dec 30 '14 at 10:37
  • 1
    Another way of getting `__version__` defined in `setup.py` is to use `pkg_resources.resource_string` and `exec`. For example: `version_info = {}; version_txt = resource_string('my_package', 'foo.py'); exec(version_txt, version_info); print(version_info['__version__']` – MPlanchard Jan 06 '16 at 20:55
  • a nicer and more robust way of reaing the version is `import ast; __version__ = ast.parse(open("my_module/version.py").read()).body[0].value.s' – wotanii Dec 11 '19 at 13:17
  • "Having `__version__ = "x.y.z"` in the source and parsing it within setup.py is definitely the correct solution": I disagree. It might be a (technically) "nicer" solution, but the version information is meta-data *about* the package, and should thus not be stored *inside* the package. The whole `mypackage.__version__` is a (necessary) convenience for the user, but the version should be defined and updated outside of the package, then (optionally) added later to package when installed. Similar arguments can be made for the license, copyright, author information etc. – 9769953 Mar 21 '23 at 11:31
20

I agree with @stefano-m 's philosophy about:

Having version = "x.y.z" in the source and parsing it within setup.py is definitely the correct solution, IMHO. Much better than (the other way around) relying on run time magic.

And this answer is derived from @zero-piraeus 's answer. The whole point is "don't use imports in setup.py, instead, read the version from a file".

I use regex to parse the __version__ so that it does not need to be the last line of a dedicated file at all. In fact, I still put the single-source-of-truth __version__ inside my project's __init__.py.

Folder heirarchy (relevant files only):

package_root/
 |- main_package/
 |   `- __init__.py
 `- setup.py

main_package/__init__.py:

# You can have other dependency if you really need to
from main_package.some_module import some_function_or_class

# Define your version number in the way you mother told you,
# which is so straightforward that even your grandma will understand.
__version__ = "1.2.3"

__all__ = (
    some_function_or_class,
    # ... etc.
)

setup.py:

from setuptools import setup
import re, io

__version__ = re.search(
    r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]',  # It excludes inline comment too
    io.open('main_package/__init__.py', encoding='utf_8_sig').read()
    ).group(1)
# The beautiful part is, I don't even need to check exceptions here.
# If something messes up, let the build process fail noisy, BEFORE my release!

setup(
    version=__version__,
    # ... etc.
)

... which is still not ideal ... but it works.

And by the way, at this point you can test your new toy in this way:

python setup.py --version
1.2.3

PS: This official Python packaging document (and its mirror) describes more options. Its first option is also using regex. (Depends on the exact regex you use, it may or may not handle quotation marks inside version string. Generally not a big issue though.)

PPS: The fix in ADAL Python is now backported into this answer.

RayLuo
  • 17,257
  • 6
  • 88
  • 73
  • 1
    Your argument on not importing any package from setup.py and parsing the version manually is so very true. – Alex Sep 29 '16 at 21:20
  • 3
    This seems the most elegant approach to me too. Thanks – Bede Constantinides Nov 15 '16 at 17:12
  • 2
    That link seems some mirror of the (likely) official Python guide here: https://packaging.python.org/single_source_version/ – ibic Dec 30 '16 at 02:58
  • @A-B-B Yeah I know. Trying to use descriptive function name to reveal the intention, right? Which is generally a good idea in most cases. But no need to be dogmatic here. Because the pattern of assigning a value to a well-known variable `__version__ = blah blah blah` already reveals the intention clearly: some version value will be assigned to this variable. I would say it is just personal preference here. – RayLuo Feb 22 '17 at 04:51
  • Thanks @ibic. I've updated the answer with the official link you provided. – RayLuo May 22 '18 at 18:16
  • @RayLuo nice approach! Is there a reason for using `utf_8_sig` instead of standard utf-8 encoding? – oLen May 12 '20 at 14:03
  • @oLen the reason for `utf_8_sig`: https://github.com/AzureAD/azure-activedirectory-library-for-python/pull/91#discussion_r120240712 – RayLuo May 13 '20 at 08:37
9

setuptools 46.4.0 added basic abstract syntax tree analysis support so that the setup.cfg attr: directive works without having to import your package's dependencies. This makes it possible to have a single source of truth of the package version thereby antiquating much of the solutions in previous answers posted prior to the release of setupstools 46.4.0.

It's now possible to avoid passing version to the setuptools.setup function in setup.py if __version__ is initialized in yourpackage.__init__.py and the following metadata is added to the setup.cfg file of your package. With this configuration the setuptools.setup function will automatically parse the package version from yourpackage.__init__.py and you're free to import __version__.py where needed in your application.

Example

setup.py without version passed to setup

from setuptools import setup

setup(
    name="yourpackage"
)

yourpackage.____init__.py

__version__ = '0.2.0'

setup.cfg

[metadata]
version = attr: package.__version__

some module in your app

from yourpackage import __version__ as expected_version
from pkg_distribution import get_distribution

installed_version = get_distribution("yourpackage").version

assert expected_version != installed_version
Exelian
  • 5,749
  • 1
  • 30
  • 49
datasmith
  • 704
  • 8
  • 14
3

Put __version__ in your_pkg/__init__.py, and parse in setup.py using ast:

import ast
import importlib.util

from pkg_resources import safe_name

PKG_DIR = 'my_pkg'

def find_version():
    """Return value of __version__.

    Reference: https://stackoverflow.com/a/42269185/
    """
    file_path = importlib.util.find_spec(PKG_DIR).origin
    with open(file_path) as file_obj:
        root_node = ast.parse(file_obj.read())
    for node in ast.walk(root_node):
        if isinstance(node, ast.Assign):
            if len(node.targets) == 1 and node.targets[0].id == "__version__":
                return node.value.s
    raise RuntimeError("Unable to find version string.")

setup(name=safe_name(PKG_DIR),
      version=find_version(),
      packages=[PKG_DIR],
      ...
      )

If using Python < 3.4, note that importlib.util.find_spec is not available. Moreover, any backport of importlib of course cannot be relied upon to be available to setup.py. In this case, use:

import os

file_path = os.path.join(os.path.dirname(__file__), PKG_DIR, '__init__.py')
Asclepius
  • 57,944
  • 17
  • 167
  • 143
alex
  • 67
  • 4
3

The accepted answer requires that the package has been installed. In my case, I needed to extract the installation params (including __version__) from the source setup.py. I found a direct and simple solution while looking through the tests of the setuptools package. Looking for more info on the _setup_stop_after attribute lead me to an old mailing list post which mentioned distutils.core.run_setup, which lead me to the actual docs needed. After all that, here's the simple solution:

file setup.py:

from setuptools import setup

setup(name='funniest',
      version='0.1',
      description='The funniest joke in the world',
      url='http://github.com/storborg/funniest',
      author='Flying Circus',
      author_email='flyingcircus@example.com',
      license='MIT',
      packages=['funniest'],
      zip_safe=False)

file extract.py:

from distutils.core import run_setup
dist = run_setup('./setup.py', stop_after='init')
dist.get_version()
ZachP
  • 631
  • 6
  • 11
  • Which file to you run to build distributable? – variable Oct 12 '19 at 04:59
  • You pass the path to setup.py to run_setup, which starts to install the package from setup.py, except stop_after=init causes it to stop before actually installing anything. – ZachP Oct 13 '19 at 19:42
  • This is great. I want exactly what seems to be the concern of @ZachP . I want the version that is supposed to be in the `setuptools` method, `setup`. This seems the only answer that lets you get the version from the `version` variable in the `setup` method without having everything installed - precisely because "`stop_after=init` causes it stop before actually installing anything." The actual installation might have happened and might not have happened. I would note that, somewhere, you should use `__version__ = dist.get_version()`, likely in the main package `__init__.py`. That worked for me. – bballdave025 Dec 05 '19 at 00:08
2

It seems like setuptools do not recommend using pkg_resources anymore.

A newer solution using the recommended importlib.metadata, working in Python 3.8+:

>>> from importlib.metadata import version  
>>> version('wheel')  
'0.32.3'
Finlay
  • 311
  • 3
  • 11
1

Based on the accepted answer and comments, this is what I ended up doing:

file: setup.py

setup(
    name='foobar',
    version='1.0.0',
    # other attributes
)

file: __init__.py

from pkg_resources import get_distribution, DistributionNotFound

__project__ = 'foobar'
__version__ = None  # required for initial installation

try:
    __version__ = get_distribution(__project__).version
except DistributionNotFound:
    VERSION = __project__ + '-' + '(local)'
else:
    VERSION = __project__ + '-' + __version__
    from foobar import foo
    from foobar.bar import Bar

Explanation:

  • __project__ is the name of the project to install which may be different than the name of the package

  • VERSION is what I display in my command-line interfaces when --version is requested

  • the additional imports (for the simplified package interface) only occur if the project has actually been installed

Community
  • 1
  • 1
Jace Browning
  • 11,699
  • 10
  • 66
  • 90
  • 1
    FWIW, I no longer structure my packages this way because I don't like the idea of always having to run code in `__init__.py`. I am now "reading" from the package during setup: https://github.com/jacebrowning/template-python-demo/blob/8e8991138ad6fba7f91deb4c716cd80283c116f7/setup.py#L21-L29 – Jace Browning Sep 24 '16 at 00:55
-1

Very late, I know. But this is working for me.

module/version.py:

__version__ = "1.0.2"

if __name__ == "__main__":
    print(__version__)

module/__init__.py:

from . import version
__version__ = version.__version__

setup.py:

import subprocess

out = subprocess.Popen(['python', 'module/version.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
stdout,stderr = out.communicate()
version = str(stdout)

Main advantage for me is that it requires no hand-crafted parsing or regex, or manifest.in entries. It is also fairly Pythonic, seems to work in all cases (pip -e, etc), and can easily be extended to share docstrings etc by using argparse in version.py. Can anyone see issues with this approach?

Christopher Brown
  • 537
  • 1
  • 5
  • 14