What is the correct way to share package version with setup.py and the package?

Question:

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?

Asked By: Jace Browning

||

Answers:

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.

Answered By: Zero Piraeus

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
Answered By: Martijn Pieters

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

Answered By: Jace Browning

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.

Answered By: RayLuo

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')
Answered By: nexcvon

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='[email protected]',
      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()
Answered By: ZachP

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?

Answered By: Christopher Brown

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
Answered By: infosmith

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'
Answered By: Finlay
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.