Standard way to embed version into Python package?
Asked Answered
C

25

402

Is there a standard way to associate version string with a Python package in such way that I could do the following?

import foo
print(foo.version)

I would imagine there's some way to retrieve that data without any extra hardcoding, since minor/major strings are specified in setup.py already. Alternative solution that I found was to have import __version__ in my foo/__init__.py and then have __version__.py generated by setup.py.

Coons answered 19/1, 2009 at 18:5 Comment(9)
Version of an installed package can be retrieved from metadata with setuptools, so in many cases putting version only in setup.py is enough. See this question.Gentille
FYI, there are basically 5 common patterns to maintain the single source of truth (at both setup and run time) for the version number.Vladamir
@ionelmc Python's documentation lists 7 different options for single-souring. Doesn't that contradict the concept of a "single source of truth"?Coblenz
@StevenVascellaro not sure what you're asking. There are so many ways listed there because the packaging guide doesn't want to be opinionated.Sternforemost
@Sternforemost ur link is broken. this is the updated one packaging.python.org/guides/single-sourcing-package-versionRenoir
this should be way simpler. Look at all the complicated answers to this simple question. Yikes. If setup.py has a version (which it has to), why doesn't package_name.__version__ just work, and show that version?Centering
setuptools now offers a way to dynamically set version in pyproject.toml: setuptools.pypa.io/en/latest/userguide/…Discounter
O.P. is long gone and, amazingly, nobody answered the actual question in over 14 years (is there a standard way?)Vogt
@Vogt I think that makes the answer "no", then.Ats
R
203

Not directly an answer to your question, but you should consider naming it __version__, not version.

This is almost a quasi-standard. Many modules in the standard library use __version__, and this is also used in lots of 3rd-party modules, so it's the quasi-standard.

Usually, __version__ is a string, but sometimes it's also a float or tuple.

As mentioned by S.Lott (Thank you!), PEP 8 says it explicitly:

Module Level Dunder Names

Module level "dunders" (i.e. names with two leading and two trailing underscores) such as __all__, __author__, __version__, etc. should be placed after the module docstring but before any import statements except from __future__ imports.

You should also make sure that the version number conforms to the format described in PEP 440 (PEP 386 a previous version of this standard).

Reindeer answered 19/1, 2009 at 21:13 Comment(7)
It should be a string, and have a version_info for the tuple version.Theodicy
James: Why __version_info__ specifically? (Which "invents" your own double-underscore-word.) [When James commented, underscores did nothing in comments, now they indicate emphasis, so James really wrote __version_info__ too. ---ed.]Tuchun
You can see something about what version should say at packages.python.org/distribute/… That page is about distribute, but the meaning of the version number is becoming a de-facto standard.Dogmatism
Despite what PEP 8 says, it's highly desirable for the contents of __version__ to follow the rules in PEP 386, which allow version numbers to be reliably compared.Magnuson
Awesome. And then go one step further and make a command line flag (-v, --version) for printing the version. Use 'optparse': docs.python.org/3/library/optparse.html#module-optparseSalazar
Where would you put that version. Considering this is the accepted version, I'd love to see that additional info here.Nanice
Just a note that using __version__ is a rejected PEP396 standard: peps.python.org/pep-0396 Instead, you should just specify the version in your pyproject.toml, or setup.cfg.Provo
R
171

I use a single _version.py file as the "once cannonical place" to store version information:

  1. It provides a __version__ attribute.

  2. It provides the standard metadata version. Therefore it will be detected by pkg_resources or other tools that parse the package metadata (EGG-INFO and/or PKG-INFO, PEP 0345).

  3. It doesn't import your package (or anything else) when building your package, which can cause problems in some situations. (See the comments below about what problems this can cause.)

  4. There is only one place that the version number is written down, so there is only one place to change it when the version number changes, and there is less chance of inconsistent versions.

Here is how it works: the "one canonical place" to store the version number is a .py file, named "_version.py" which is in your Python package, for example in myniftyapp/_version.py. This file is a Python module, but your setup.py doesn't import it! (That would defeat feature 3.) Instead your setup.py knows that the contents of this file is very simple, something like:

__version__ = "3.6.5"

And so your setup.py opens the file and parses it, with code like:

import re
VERSIONFILE="myniftyapp/_version.py"
verstrline = open(VERSIONFILE, "rt").read()
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
mo = re.search(VSRE, verstrline, re.M)
if mo:
    verstr = mo.group(1)
else:
    raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))

Then your setup.py passes that string as the value of the "version" argument to setup(), thus satisfying feature 2.

To satisfy feature 1, you can have your package (at run-time, not at setup time!) import the _version file from myniftyapp/__init__.py like this:

from _version import __version__

Here is an example of this technique that I've been using for years.

The code in that example is a bit more complicated, but the simplified example that I wrote into this comment should be a complete implementation.

Here is example code of importing the version.

If you see anything wrong with this approach, please let me know.

Rabato answered 15/8, 2011 at 22:4 Comment(14)
Could you please describe the problems that motivate #3? Glyph said it had something to do with "setuptools likes to pretend that your code isn't anywhere on the system when your setup.py runs", but the details would help convince me and others.Annulation
@IvanKozik: if your setup.py is being executed in a freshly created Python process and your current working directory is the directory that contains your project, then it will work. This is the common way that programmers run setup.py, so they think it is the only way. But there are other ways! Tools like py2exe run a single Python process and then load and execute the setup.py scripts from one project after another (while packing them all together). Suppose that some of the Python code that ran before your setup.py script imported a different version of your module.Rabato
@IvanKozik I think there can be other problems with importing your package, too. Consider that a tool like setuptools or pip needs to do two things for packages that declare dependencies: 1. build this package, 2. install (first building, if necessary) its dependencies.Rabato
@Iva Now, what order should the tool do this in? It can't (in the setuptools/pip/virtualenv system of today) even know what the deps are until it evaluates your setup.py. Also, if it tried to do full depth-first and do all deps before it does this one, it would get stuck if there were circular deps. But if it tries to build this package before installing the dependencies, then if you import your package from your setup.py, it will not necessarily be able to import its deps, or the right versions of its deps.Rabato
Above I wrote: "if your setup.py is being executed in a freshly created Python process and your current working directory is the directory that contains your project, then it will work" I should have written: "if your setup.py is being executed in a freshly created Python process and your current working directory is the directory that contains your project, and all of its dependencies which it needs when it is imported are already installed, then it will work"Rabato
Could you write file "version.py" from "setup.py" instead of parsing it? That seems simpler.Justen
Great answer. Slight wrinkle for readers to be aware of: Release type (alpha, beta, candidate, final, post, etc) is duplicated in the version as specified above, and the trove classifiers, if you specify them.Justen
Jonathan Hartley: I agree it would be slightly simpler for your "setup.py" to write the "version.py" file instead of parsing it, but it would open up a window for inconsistency, when you've edited your setup.py to have the new version but haven't yet executed setup.py to update the version.py file. Another reason to have the canonical version be in a small separate file is that it makes it easy for other tools, such as tools that read your revision control state, to write the version file.Rabato
Similar approach is to execfile("myniftyapp/_version.py") from within setup.py, rather than trying to parse the version code manually. Suggested in https://mcmap.net/q/50055/-how-can-i-get-the-version-defined-in-setup-py-setuptools-in-my-package -- discussion there may be helpful, too.Salutary
Here's a mostly-complementary tool by my long-time programming partner Brian Warner: blog.mozilla.org/warner/2012/01/31/… Versioneer is a tool to generate the version number from git history. Ideally I would like to use Versioneer to generate the version number, and then use the technique described in my answer here to make that version number available to all the places where it is needed. I'm doing something like that in this project: github.com/zooko/pycryptoppRabato
+1. Any reason not to have a file ´__version__´ which contains only the version (1.2.3) and then read that in setup.py as well as in your package? (No parsing is required in this way...)Tremblay
I add __version_info__ = tuple(map(int, __version__.split('.'))) to that.Phelia
Should be from ._version import __version__ to pull in the _version module from _version.py in the local project and not another one floating around in your environment.Marella
Your first link is broken.Featherstitch
I
141

Rewritten 2017-05

After 13+ years of writing Python code and managing various packages, I came to the conclusion that DIY is maybe not the best approach.

I started using the pbr package for dealing with versioning in my packages. If you are using git as your SCM, this will fit into your workflow like magic, saving your weeks of work (you will be surprised about how complex the issue can be).

As of today, pbr has 12M mongthly downloads, and reaching this level didn't include any dirty tricks. It was only one thing -- fixing a common packaging problem in a very simple way.

pbr can do more of the package maintenance burden, and is not limited to versioning, but it does not force you to adopt all its benefits.

So to give you an idea about how it looks to adopt pbr in one commit have a look switching packaging to pbr

Probably you would observed that the version is not stored at all in the repository. PBR does detect it from Git branches and tags.

No need to worry about what happens when you do not have a git repository because pbr does "compile" and cache the version when you package or install the applications, so there is no runtime dependency on git.

Old solution

Here is the best solution I've seen so far and it also explains why:

Inside yourpackage/version.py:

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '0.12'

Inside yourpackage/__init__.py:

from .version import __version__

Inside setup.py:

exec(open('yourpackage/version.py').read())
setup(
    ...
    version=__version__,
    ...

If you know another approach that seems to be better let me know.

Indeterminable answered 18/4, 2013 at 13:50 Comment(18)
You should replace exec(open('yourpackage/version.py').read()) with execfile('yourpackage/version.py').Aqualung
Err, no. execfile() does not exist in Python 3, so it is better to use exec(open().read()).Aqualung
why not to put "version='0.12' directly to setup.py?Thumping
why not from .version import __version__ in setup.py as well?Butterandeggs
@Butterandeggs Because the package isn't loaded when setup.py is running - try it, you'll get an error (or at least, I did :-))Facelifting
@Facelifting I guess in Python 2.7+ . is in sys.path by default, so as long as version module doesn't have dependencies it is a way to go.Gentille
@Gentille Yes, but the . directory is the directory with setup.py, not the directory with the version module...Facelifting
If you run python setup.py install from a directory other than the package root, the relative lookup for version.py will failOrebro
works perfectly, but added absolute path to make more robust:here = os.path.abspath(os.path.dirname(__file__)) then exec(open(os.path.join(here, 'yourpackage/version.py')).read()). Also, didn't find a need for the relative import.Reggiereggis
The link to pbr results in a bad gateway.Bilection
Thank you for helping us switch to pbr (Python Build Reasonableness)! It looks very promising! It will help get us on the way to making the ecosystem safer and more robust via text files like setup.cfg, rather than DIY code running in setup.py, which has made the PyPI, pip install and requirements management jobs so much harder.Syphilis
Can you provide an example of how to implement pbr for versioning?Coblenz
pbr, no doubt, is a great tool, but you failed to address the question. How can you access the current version or the installed package via bpr.Cuckooflower
pbr is a horrible solution. It's a package written by the openstack community for openstack problems, and it introduces a whole host of opinions and assumptions that can screw you up big time if you're not aware of them. If you don't write code for openstack, there is a high probability that it introduces more problems than it solves.Workmanlike
You don't need pbr to access version information, just get it from module.__version__ -- in fact what pbr does is automates creation of this at build time and its maintenance when running from git. If you use git and tags for version it would be a waste of time not to benefit form it. True that pbr originated on openstack, but it has nothing specific on that (its popularity has nothing to do with openstack). I hope that packaging on python will evolve so pbr would not be needed.Indeterminable
this is one straightforward answer answer which just works! I think this should be the accepted answer; used it with attribution: github.com/mie-lab/trackintel/pull/323#issue-720511616Brianabriand
Using what you call "the old solution", I store a __version__ variable in the __init__.py file in the root folder of the package, and read it in setup.cfg using "version = attr: mypackage.__version__" (as suggested in the 1st option here packaging.python.org/en/latest/guides/…). However, I also need to be able to read the version in run-time, but without installing the package (while developing). Does it make sense to import the root folder (after adding it to sys.path) and use mypackage.__version__?Dung
@ItamarKatz: no need to fiddle with sys.path. Any module in a package can import attribs from the package's __init__.py by doing from . import __version__Thetisa
S
33

Per the deferred [STOP PRESS: rejected] PEP 396 (Module Version Numbers), there is a proposed way to do this. It describes, with rationale, an (admittedly optional) standard for modules to follow. Here's a snippet:

  1. When a module (or package) includes a version number, the version SHOULD be available in the __version__ attribute.
  1. For modules which live inside a namespace package, the module SHOULD include the __version__ attribute. The namespace package itself SHOULD NOT include its own __version__ attribute.
  1. The __version__ attribute's value SHOULD be a string.
Steading answered 11/4, 2013 at 15:16 Comment(10)
That PEP is not accepted/standardized, but deferred (due to lack of interest). Therefore it's a bit misleading to state that "there is a standard way" specified by it.Hummocky
@weaver: Oh my! I learnt something new. I didn't know that was something I needed to check for.Steading
Edited to note it isn't a standard. Now I feel embarrassed, because I have raised feature requests on projects asking them to follow this "standard".Steading
Perhaps you should take over the standardization work on that PEP, since you seem interested :)Hummocky
This would work for versioning an individual module, but I'm not sure it would apply to versioning a full project.Coblenz
@StevenVascellaro: "Full project" is a bit vague. This question is about packages. If your project is a web-site, embedded system, phone app, desktop app, command-line, etc., you will need a different system - and a different question.Steading
"the version SHOULD be available in the version attribute" (etc.) does not mean this attr is hard coded. It could get it dynamically too. It just has to be "available". Which is a GoodThing (TM)Leoni
@nerdoc: This reads like you are pushing back against this answer, but I was addressing the question of how do you find out the version of a package you are using, not how it should be calculated.Steading
@Steading 7 years later PEP 396 was rejected.Kahaleel
Thanks for the update, @Spartan. I note they point to newer approaches.Steading
S
31

There is a slightly simpler alternative to some of the other answers:

__version_info__ = ('1', '2', '3')
__version__ = '.'.join(__version_info__)

(And it would be fairly simple to convert auto-incrementing portions of version numbers to a string using str().)

Of course, from what I've seen, people tend to use something like the previously-mentioned version when using __version_info__, and as such store it as a tuple of ints; however, I don't quite see the point in doing so, as I doubt there are situations where you would perform mathematical operations such as addition and subtraction on portions of version numbers for any purpose besides curiosity or auto-incrementation (and even then, int() and str() can be used fairly easily). (On the other hand, there is the possibility of someone else's code expecting a numerical tuple rather than a string tuple and thus failing.)

This is, of course, my own view, and I would gladly like others' input on using a numerical tuple.


As shezi reminded me, (lexical) comparisons of number strings do not necessarily have the same result as direct numerical comparisons; leading zeroes would be required to provide for that. So in the end, storing __version_info__ (or whatever it would be called) as a tuple of integer values would allow for more efficient version comparisons.

Stunning answered 15/7, 2009 at 14:29 Comment(12)
nice (+1), but wouldn't you prefer numbers instead of strings? e.g. __version_info__ = (1,2,3)Moment
Comparison of strings can become dangerous when version numbers exceed 9, since for example '10' < '2'.Kurman
In which case you could simply use int() to do proper value comparisons, or just do what orip suggested and store the values as integers.Stunning
I do this as well but slightly tweaked to address ints.. __version_info__ = (0, 1, 0) __version__ = '.'.join(map(str, __version_info__))Nunnery
Problem with __version__ = '.'.join(__version_info__) is that __version_info__ = ('1', '2', 'beta') will become 1.2.beta, not 1.2beta or 1.2 betaOverbite
@nagisa: I don't see any problem with that. It's not what most people would expect, but it's still very easily parsed.Stunning
I think the problem with this approach is where to put the lines of code declaring the __version__. If they are in setup.py then your program can't import them from within its package. Perhaps this isn't a problem for you, in which case, fine. If they go within your program, then your setup.py can import them, but it shouldn't, since setup.py gets run during install when your program's dependencies are not yet installed (setup.py is used to determine what the dependencies are.) Hence Zooko's answer: manually parse the value out of a product source file, without importing the product packageJusten
Whether you set <tt>__version__</tt> directly as a string or using this method (and parsing <tt>__version_info__</tt> in setup.py) is orthogonal to the other advantages of Zooko's answer.Magnuson
Using numbers can help with comparisons: Opera had a big problem when they were the first browser to reach version 10. Read the full report here: dev.opera.com/articles/view/opera-ua-string-changesAldous
@shezi: That's an issue with substring parsing rather than any comparison issue, but you do bring up a good point: a lexical comparison of integers stored as strings won't necessarily be the same as the numerical comparison of the two values. (e.g. 2 < 10 evaluates to True, but '2' < '10' evaluates to False).Stunning
This version_info trick is at least not in sync with the corresponding PEP. It also has issues what @Overbite mentioned.Windlass
This doesn't specify where the version should be stored. What file should this be located in? Should this be in a file imported by setup.py?Coblenz
S
17

Many of these solutions here ignore git version tags which still means you have to track version in multiple places (bad). I approached this with the following goals:

  • Derive all python version references from a tag in the git repo
  • Automate git tag/push and setup.py upload steps with a single command that takes no inputs.

How it works:

  1. From a make release command, the last tagged version in the git repo is found and incremented. The tag is pushed back to origin.

  2. The Makefile stores the version in src/_version.py where it will be read by setup.py and also included in the release. Do not check _version.py into source control!

  3. setup.py command reads the new version string from package.__version__.

Details:

Makefile

# remove optional 'v' and trailing hash "v1.0-N-HASH" -> "v1.0-N"
git_describe_ver = $(shell git describe --tags | sed -E -e 's/^v//' -e 's/(.*)-.*/\1/')
git_tag_ver      = $(shell git describe --abbrev=0)
next_patch_ver = $(shell python versionbump.py --patch $(call git_tag_ver))
next_minor_ver = $(shell python versionbump.py --minor $(call git_tag_ver))
next_major_ver = $(shell python versionbump.py --major $(call git_tag_ver))

.PHONY: ${MODULE}/_version.py
${MODULE}/_version.py:
    echo '__version__ = "$(call git_describe_ver)"' > $@

.PHONY: release
release: test lint mypy
    git tag -a $(call next_patch_ver)
    $(MAKE) ${MODULE}/_version.py
    python setup.py check sdist upload # (legacy "upload" method)
    # twine upload dist/*  (preferred method)
    git push origin master --tags

The release target always increments the 3rd version digit, but you can use the next_minor_ver or next_major_ver to increment the other digits. The commands rely on the versionbump.py script that is checked into the root of the repo

versionbump.py

"""An auto-increment tool for version strings."""

import sys
import unittest

import click
from click.testing import CliRunner  # type: ignore

__version__ = '0.1'

MIN_DIGITS = 2
MAX_DIGITS = 3


@click.command()
@click.argument('version')
@click.option('--major', 'bump_idx', flag_value=0, help='Increment major number.')
@click.option('--minor', 'bump_idx', flag_value=1, help='Increment minor number.')
@click.option('--patch', 'bump_idx', flag_value=2, default=True, help='Increment patch number.')
def cli(version: str, bump_idx: int) -> None:
    """Bumps a MAJOR.MINOR.PATCH version string at the specified index location or 'patch' digit. An
    optional 'v' prefix is allowed and will be included in the output if found."""
    prefix = version[0] if version[0].isalpha() else ''
    digits = version.lower().lstrip('v').split('.')

    if len(digits) > MAX_DIGITS:
        click.secho('ERROR: Too many digits', fg='red', err=True)
        sys.exit(1)

    digits = (digits + ['0'] * MAX_DIGITS)[:MAX_DIGITS]  # Extend total digits to max.
    digits[bump_idx] = str(int(digits[bump_idx]) + 1)  # Increment the desired digit.

    # Zero rightmost digits after bump position.
    for i in range(bump_idx + 1, MAX_DIGITS):
        digits[i] = '0'
    digits = digits[:max(MIN_DIGITS, bump_idx + 1)]  # Trim rightmost digits.
    click.echo(prefix + '.'.join(digits), nl=False)


if __name__ == '__main__':
    cli()  # pylint: disable=no-value-for-parameter

This does the heavy lifting how to process and increment the version number from git.

__init__.py

The my_module/_version.py file is imported into my_module/__init__.py. Put any static install config here that you want distributed with your module.

from ._version import __version__
__author__ = ''
__email__ = ''

setup.py

The last step is to read the version info from the my_module module.

from setuptools import setup, find_packages

pkg_vars  = {}

with open("{MODULE}/_version.py") as fp:
    exec(fp.read(), pkg_vars)

setup(
    version=pkg_vars['__version__'],
    ...
    ...
)

Of course, for all of this to work you'll have to have at least one version tag in your repo to start.

git tag -a v0.0.1
Swallowtail answered 13/8, 2017 at 2:2 Comment(8)
indeed - this whole thread forgets that a VCS is very important in this discussion. just an obs: version incrementing should remain a manual process, so the preferred way would be 1. manually create and push a tag 2. let VCS tools discover that tag and store it where needed (wow - this SO editing interface is really crippling me - I had to edit this a dozen times just to deal with newlines AND IT STILL DOESN'T WORK !@#$%^&*)Lidalidah
No need to use versionbump.py when we have an awesome bumpversion package for python.Curettage
@Curettage I looked at versionbump. The docs are not very clear, and it doesn't handle tagging very well. In my testing it seems to get into states that cause it to crash: subprocess.CalledProcessError: Command '['git', 'commit', '-F', '/var/folders/rl/tjyk4hns7kndnx035p26wg692g_7t8/T/tmppishngbo']' returned non-zero exit status 1.Swallowtail
Another tweak is to use PBR to handle some of the versioning. You can use the same process to tag the repo and then PBR will correctly inject the new version in the setup.py command.Swallowtail
@Swallowtail Sorry for the delayed reply, please check my answer: https://mcmap.net/q/49869/-standard-way-to-embed-version-into-python-packageCurettage
Why shouldn't _version.py be tracked with version control?Coblenz
@StevenVascellaro This complicates the release process. Now you have to make sure that you're tags and commits match the value in _version.py. Using a single version tag is cleaner IMO and means you can create a release directly from github UI.Swallowtail
seems bumpversion is not miantained. You could use the newer bump2versionBudweis
B
16

I use a JSON file in the package dir. This fits Zooko's requirements.

Inside pkg_dir/pkg_info.json:

{"version": "0.1.0"}

Inside setup.py:

from distutils.core import setup
import json

with open('pkg_dir/pkg_info.json') as fp:
    _info = json.load(fp)

setup(
    version=_info['version'],
    ...
    )

Inside pkg_dir/__init__.py:

import json
from os.path import dirname

with open(dirname(__file__) + '/pkg_info.json') as fp:
    _info = json.load(fp)

__version__ = _info['version']

I also put other information in pkg_info.json, like author. I like to use JSON because I can automate management of metadata.

Bibb answered 25/1, 2014 at 21:15 Comment(1)
Could you elaborate how use use the json for automating metadata management? Thanks!Demicanton
G
13

Lots of work toward uniform versioning and in support of conventions has been completed since this question was first asked. Palatable options are now detailed in the Python Packaging User Guide. Also noteworthy is that version number schemes are relatively strict in Python per PEP 440, and so keeping things sane is critical if your package will be released to the Cheese Shop.

Here's a shortened breakdown of versioning options:

  1. Read the file in setup.py (setuptools) and get the version.
  2. Use an external build tool (to update both __init__.py as well as source control), e.g. bump2version, changes or zest.releaser.
  3. Set the value to a __version__ global variable in a specific module.
  4. Place the value in a simple VERSION text file for both setup.py and code to read.
  5. Set the value via a setup.py release, and use importlib.metadata to pick it up at runtime. (Warning, there are pre-3.8 and post-3.8 versions.)
  6. Set the value to __version__ in sample/__init__.py and import sample in setup.py.
  7. Use setuptools_scm to extract versioning from source control so that it's the canonical reference, not code.

NOTE that (7) might be the most modern approach (build metadata is independent of code, published by automation). Also NOTE that if setup is used for package release that a simple python3 setup.py --version will report the version directly.

Genesa answered 22/5, 2020 at 17:3 Comment(0)
T
7

Also worth noting is that as well as __version__ being a semi-std. in python so is __version_info__ which is a tuple, in the simple cases you can just do something like:

__version__ = '1.2.3'
__version_info__ = tuple([ int(num) for num in __version__.split('.')])

...and you can get the __version__ string from a file, or whatever.

Theodicy answered 21/1, 2009 at 19:44 Comment(5)
Do you have any references/links regarding the origin of this usage of __version_info__?Proper
Well it's the same mapping as sys.version to sys.version_info. So: docs.python.org/library/sys.html#sys.version_infoTheodicy
It is easier to do the mapping in the other direction (__version_info__ = (1, 2, 3) and __version__ = '.'.join(map(str, __version_info__))).Dowel
@EOL - __version__ = '.'.join(str(i) for i in __version_info__) - slightly longer but more pythonic.Vassili
I am not sure that what you propose is clearly more pythonic, as it contains a dummy variable which is not really needed and whose meaning is a little hard to express (i has no meaning, version_num is a little long and ambiguous…). I even take the existence of map() in Python as a strong hint that it should be used here, as what we need to do here is the typical use case of map() (use with an existing function)—I don't see many other reasonable uses.Dowel
I
6

There doesn't seem to be a standard way to embed a version string in a python package. Most packages I've seen use some variant of your solution, i.e. eitner

  1. Embed the version in setup.py and have setup.py generate a module (e.g. version.py) containing only version info, that's imported by your package, or

  2. The reverse: put the version info in your package itself, and import that to set the version in setup.py

Isosceles answered 19/1, 2009 at 18:49 Comment(5)
I like your recommandation, but how to generate this module from setup.py?Indeterminable
I like the idea of option (1), it seems simpler than Zooko's answer of parsing the version number from a file. But you can't ensure that version.py is created when a dev just clones your repo. Unless you check in version.py, which opens up the small wrinkle that you might change the version number in setup.py, commit, release, and then have to (slash forget to) commit the change to version.py.Justen
Presumably you could generate the module using something like """with open("mypackage/version.py", "w") as fp: fp.write("__version__ == '%s'\n" % (__version__,))"""Justen
I think option 2. is susceptible to failing during install as noted in comments to JAB's answer.Justen
What about that? You put __version__ = '0.0.1'" (where the version is a string, of course) in the __init__.py" of the main package of your software. Then go for the point 2: In the setup you do from package.__init__ import __version__ as v, and then you set the variable v as the version of your setup.py. Then import mypack as my, print my.__version__ will print the version. The version will be stored in only one place, accessible from the whole code, in a file that does not import anything else related to the software.Henn
K
6

arrow handles it in an interesting way.

Now (since 2e5031b)

In arrow/__init__.py:

__version__ = 'x.y.z'

In setup.py:

from arrow import __version__

setup(
    name='arrow',
    version=__version__,
    # [...]
)

Before

In arrow/__init__.py:

__version__ = 'x.y.z'
VERSION = __version__

In setup.py:

def grep(attrname):
    pattern = r"{0}\W*=\W*'([^']+)'".format(attrname)
    strval, = re.findall(pattern, file_text)
    return strval

file_text = read(fpath('arrow/__init__.py'))

setup(
    name='arrow',
    version=grep('__version__'),
    # [...]
)
Katmandu answered 26/10, 2016 at 12:59 Comment(3)
what is file_text?Bantling
the updated solution is actually harmful. When setup.py is running, it will not necessarily see the version of the package from the local file path. It might revert to a previously installed version, e.g. from running pip install -e . on a development branch or something when testing. setup.py absolutely should not rely on importing the package it is in the process of installing in order to determine parameters for the deployment. Yikes.Bantling
Yes I don't know why arrow decided to regress to this solution. Moreover the commit message says "Updated setup.py with modern Python standards"... 🤷Katmandu
B
5

I also saw another style:

>>> django.VERSION
(1, 1, 0, 'final', 0)
Binal answered 11/10, 2009 at 17:32 Comment(4)
Yes, I saw too. BTW every answer takes other style so now I don't know what style is a "standard". Looking for mentioned PEPs...Hargis
Another way seen; Mongo's Python client uses plain version, without the underscores. So this works; $ python >>> import pymongo >>> pymongo.version '2.7'Cotta
Implementing .VERSION doesn't mean that you don't have to implement __version__.Erzurum
Does this require implementing django in the project?Coblenz
A
5

Using setuptools and pbr

There is not a standard way to manage version, but the standard way to manage your packages is setuptools.

The best solution I've found overall for managing version is to use setuptools with the pbr extension. This is now my standard way of managing version.

Setting up your project for full packaging may be overkill for simple projects, but if you need to manage version, you are probably at the right level to just set everything up. Doing so also makes your package releasable at PyPi so everyone can download and use it with Pip.

PBR moves most metadata out of the setup.py tools and into a setup.cfg file that is then used as a source for most metadata, which can include version. This allows the metadata to be packaged into an executable using something like pyinstaller if needed (if so, you will probably need this info), and separates the metadata from the other package management/setup scripts. You can directly update the version string in setup.cfg manually, and it will be pulled into the *.egg-info folder when building your package releases. Your scripts can then access the version from the metadata using various methods (these processes are outlined in sections below).

When using Git for VCS/SCM, this setup is even better, as it will pull in a lot of the metadata from Git so that your repo can be your primary source of truth for some of the metadata, including version, authors, changelogs, etc. For version specifically, it will create a version string for the current commit based on git tags in the repo.

As PBR will pull version, author, changelog and other info directly from your git repo, so some of the metadata in setup.cfg can be left out and auto generated whenever a distribution is created for your package (using setup.py)



Get the current version in real-time

setuptools will pull the latest info in real-time using setup.py:

python setup.py --version

This will pull the latest version either from the setup.cfg file, or from the git repo, based on the latest commit that was made and tags that exist in the repo. This command doesn't update the version in a distribution though.



Updating the version metadata

When you create a distribution with setup.py (i.e. py setup.py sdist, for example), then all the current info will be extracted and stored in the distribution. This essentially runs the setup.py --version command and then stores that version info into the package.egg-info folder in a set of files that store distribution metadata.

Note on process to update version meta-data:

If you are not using pbr to pull version data from git, then just update your setup.cfg directly with new version info (easy enough, but make sure this is a standard part of your release process).

If you are using git, and you don't need to create a source or binary distribution (using python setup.py sdist or one of the python setup.py bdist_xxx commands) the simplest way to update the git repo info into your <mypackage>.egg-info metadata folder is to just run the python setup.py install command. This will run all the PBR functions related to pulling metadata from the git repo and update your local .egg-info folder, install script executables for any entry-points you have defined, and other functions you can see from the output when you run this command.

Note that the .egg-info folder is generally excluded from being stored in the git repo itself in standard Python .gitignore files (such as from Gitignore.IO), as it can be generated from your source. If it is excluded, make sure you have a standard "release process" to get the metadata updated locally before release, and any package you upload to PyPi.org or otherwise distribute must include this data to have the correct version. If you want the Git repo to contain this info, you can exclude specific files from being ignored (i.e. add !*.egg-info/PKG_INFO to .gitignore)



Accessing the version from a script

You can access the metadata from the current build within Python scripts in the package itself. For version, for example, there are several ways to do this I have found so far:

## This one is a new built-in as of Python 3.8.0 should become the standard
from importlib.metadata import version

v0 = version("mypackage")
print('v0 {}'.format(v0))

## I don't like this one because the version method is hidden
import pkg_resources  # part of setuptools

v1 = pkg_resources.require("mypackage")[0].version
print('v1 {}'.format(v1))

# Probably best for pre v3.8.0 - the output without .version is just a longer string with
# both the package name, a space, and the version string
import pkg_resources  # part of setuptools

v2 = pkg_resources.get_distribution('mypackage').version
print('v2 {}'.format(v2))

## This one seems to be slower, and with pyinstaller makes the exe a lot bigger
from pbr.version import VersionInfo

v3 = VersionInfo('mypackage').release_string()
print('v3 {}'.format(v3))

You can put one of these directly in your __init__.py for the package to extract the version info as follows, similar to some other answers:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version
Aspirant answered 24/10, 2019 at 21:41 Comment(0)
D
5

Using setuptools and pyproject.toml

Setuptools now offers a way to dynamically get version in pyproject.toml

Reproducing the example here, you can create something like the following in your pyproject.toml

# ...
[project]
name = "my_package"
dynamic = ["version"]
# ...
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
Discounter answered 21/11, 2022 at 5:50 Comment(0)
E
4

After several hours of trying to find the simplest reliable solution, here are the parts:

create a version.py file INSIDE the folder of your package "/mypackage":

# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = '1.2.7'

in setup.py:

exec(open('mypackage/version.py').read())
setup(
    name='mypackage',
    version=__version__,

in the main folder init.py:

from .version import __version__

The exec() function runs the script outside of any imports, since setup.py is run before the module can be imported. You still only need to manage the version number in one file in one place, but unfortunately it is not in setup.py. (that's the downside, but having no import bugs is the upside)

Ensile answered 31/3, 2020 at 14:4 Comment(0)
S
4

pbr with bump2version

This solution was derived from this article.

The use case - python GUI package distributed via PyInstaller. Needs to show version info.

Here is the structure of the project packagex

packagex
├── packagex
│   ├── __init__.py
│   ├── main.py
│   └── _version.py
├── packagex.spec
├── LICENSE
├── README.md
├── .bumpversion.cfg
├── requirements.txt
├── setup.cfg
└── setup.py

where setup.py is

# setup.py
import os

import setuptools

about = {}
with open("packagex/_version.py") as f:
    exec(f.read(), about)

os.environ["PBR_VERSION"] = about["__version__"]

setuptools.setup(
    setup_requires=["pbr"],
    pbr=True,
    version=about["__version__"],
)

packagex/_version.py contains just

__version__ = "0.0.1"

and packagex/__init__.py

from ._version import __version__

and for .bumpversion.cfg

[bumpversion]
current_version = 0.0.1
commit = False
tag = False
message = Bump version to v{new_version}
tag_message = v{new_version}


[bumpversion:file:packagex/_version.py]

And here is a GitHub project with this setup.

Shutz answered 19/1, 2021 at 17:20 Comment(0)
V
3

No, there isn't a standard way to embed the version string in a Python package so that it's accessible as an attribute. A standard was proposed in PEP 396 – Module Version Numbers, but that PEP has been rejected in 2021.

What has been standardized:

A couple of examples of non-standard approaches are a VERSION tuple:

>>> import django
>>> django.VERSION
(4, 2, 4, 'final', 0)

or a __version__ string:

>>> import requests
>>> requests.__version__
'2.31.0'

Increasingly, you'll find that many projects don't store a version attribute at all. Regardless of whether a project keeps a version attribute burnt into the source code, the recommended way to retrieve a package version reliably is by using stdlib importlib.metadata:

>>> from importlib.metadata import version
>>> version("django")
'4.2.4'
>>> version("requests")
'2.31.0'

If you've historically provided a version attribute for your package, and you wish to remove it but need to allow a deprecation period for backwards-compatibility reasons, you may use a module level __getattr__ fallback:

# myproj/__init__.py
import warnings
from importlib.metadata import version

def __getattr__(name: str): 
    if name == "__version__":
        warnings.warn(
            f"Accessing myproj.__version__ is deprecated and will be "
            "removed in a future release. Use importlib.metadata directly.",
            DeprecationWarning,
            stacklevel=2,
        )
        return version("myproj")
    raise AttributeError(f"module {__name__} has no attribute {name}")

This is the approach used by python-attrs 23.1.0 for example.

$ PYTHONWARNINGS=always python3 -c 'from attrs import __version__'
<string>:1: DeprecationWarning: Accessing attrs.__version__ is deprecated and will be removed in a future release. Use importlib.metadata directly to query for attrs's packaging metadata.

Don't bother using the module __getattr__ approach unless you're deprecating an existing version attribute from an old project. For new projects, my recommendation is not to provide a version attribute in the source code in the first place, so that the single source of truth for the version string is in the package metadata.

Vogt answered 14/8, 2023 at 15:22 Comment(0)
M
1

I prefer to read the package version from installation environment. This is my src/foo/_version.py:

from pkg_resources import get_distribution                                        
                                                                                  
__version__ = get_distribution('foo').version

Makesure foo is always already installed, that's why a src/ layer is required to prevent foo imported without installation.

In the setup.py, I use setuptools-scm to generate the version automatically.


Update in 2022.7.5:

There is another way, which is my faviourate now. Use setuptools-scm to generate a _version.py file.

setup(
    ...
    use_scm_version={
        'write_to':
        'src/foo/_version.py',
        'write_to_template':
        '"""Generated version file."""\n'
        '__version__ = "{version}"\n',
    },
)
Mohair answered 24/11, 2020 at 9:22 Comment(1)
This works, there are other tools but for a simple solution it's good.Kinnikinnick
A
1

Seeing so many answers is a clean sign that there is no standard way.

So here's another: Poetry and importlib

I use the poetry-dynamic-versioning plugin to set the version on poetry build.

Then in the __init__.py of my package I have:

from importlib.metadata import version

__version__ = version(__name__)

Of course this requires to have a proper package structure and build process.

Azine answered 31/8, 2023 at 9:58 Comment(2)
This is a very nice solution! However, it's better to wrap the version extraction in a try-catch to avoid a ModuleNotFound error when the module is not installed, e.g. when running tests and __name__ is just the name of the folder in your project (src/myproject): Python try: __version__ = version(__name__) except ModuleNotFoundError as err: __version__ = f"No version available for {__name__}" Abbotson
You don't need to import from importlib if you use poetry-dynamic-versioning, its default behaviour is to inject anywhere you declare __version__ = "0.0.0" with the current release version! github.com/mtkennerly/…Flosi
D
0
  1. Create a file named by _version.txt in the same folder as __init__.py and write version as a single line:
0.8.2
  1. Read this infomation from file _version.txt in __init__.py:
    import os 
    def get_version():
        with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), "_version.txt")) as f:
            return f.read().strip() 
    __version__ = get_version()
Denominationalism answered 12/3, 2021 at 13:46 Comment(0)
P
0

Add __version__="0.0.1" to your main __init__.py:

import .submodule

__version__ = '0.0.1'

If your library is called mylib, this is the single source of truth for your version number which you can directly access by mylib.__version__. But you still need to add it to your setup.py file.

In your setup.py, you need to read the __init__.py as a text file:

import re
from setuptools import setup, find_packages

with open("mylib/__init__.py") as f:
    version= re.findall("__version__.*(\d.\d.\d).*", f.read())[0]

setup(
        name="mylib",
        version=version,
        packages=find_packages()
    )

There are different ways to read the __version__ variable from __init__.py as a text file. Check this one as an example: Single-sourcing the package version

Please keep in mind that you need the avoid the following points:

  1. Do not import __version__ from __init__.py because this loads all sub-modules with their dependencies that are not available at the setup time and will fail.
  2. Do not store __version__ in a separate file (e.g. version.py) if you are going to import it from your main directory (i.e. where your __init__.py is located) because it will automatically trigger __init__.py which will lead to the same issue mentioned in point 1.
Practitioner answered 31/8, 2023 at 12:47 Comment(0)
C
-1
  1. Use a version.py file only with __version__ = <VERSION> param in the file. In the setup.py file import the __version__ param and put it's value in the setup.py file like this: version=__version__
  2. Another way is to use just a setup.py file with version=<CURRENT_VERSION> - the CURRENT_VERSION is hardcoded.

Since we don't want to manually change the version in the file every time we create a new tag (ready to release a new package version), we can use the following..

I highly recommend bumpversion package. I've been using it for years to bump a version.

start by adding version=<VERSION> to your setup.py file if you don't have it already.

You should use a short script like this every time you bump a version:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

Then add one file per repo called: .bumpversion.cfg:

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

Note:

  • You can use __version__ parameter under version.py file like it was suggested in other posts and update the bumpversion file like this: [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
  • You must git commit or git reset everything in your repo, otherwise you'll get a dirty repo error.
  • Make sure that your virtual environment includes the package of bumpversion, without it it will not work.
Curettage answered 7/2, 2018 at 13:17 Comment(3)
@Swallowtail Sorry for the delay, please check my answer ^^^ - note that you must git commit or git reset everything in your repo and make sure that your virtual environment includes the package of bumpversion, without it it will not work. Use the latest version.Curettage
I'm a bit unclear about what solution is being suggested here. Are you recommending manually tracking version with version.py, or tracking it with bumpversion?Coblenz
@StevenVascellaro I'm suggesting using bumpversion, never use manual versioning. What I tried to explain is that you can direct bumpversion to update either the version in setup.py file or better yet use it to update the version.py file. It's more common practice to update the version.py file and take the __version__ param value into the setup.py file. My solution is used in production and it's a common practice. Note: just to be clear, using bumpversion as part of a script is the best solution, put it in your CI and it will be automatic operation.Curettage
C
-1

If your project uses git or mercurial as its SCM, I would recommend the following:

  1. Configure your __init__.py as shown below so that it always sets the __version__ attribute correctly - both in development mode, non-pip development mode (python path updated by IDE), or production mode (pip install)

    try:
        # -- Distribution mode --
        # import from _version.py generated by setuptools_scm during release
        from ._version import version as __version__
    except ImportError:
        # -- Source mode --
        # use setuptools_scm to get the current version from src using git
        from setuptools_scm import get_version as _gv
        from os import path as _path
        __version__ = _gv(_path.join(_path.dirname(__file__), _path.pardir))
    
  2. then, choose a way to generate the _version.py file that is required in production (the one imported on line 4 above).

    a. I personally do this in setup.py:

    setup(
      ...
      use_scm_version={'write_to': '%s/_version.py' % <pkgname>}
    )
    

    b. Alternately if you prefer to configure your project using a pyproject.toml or setup.cfg file, you can check the documentation in setuptools_scm to find how this is done. For example with pyproject.toml it is like this:

    [tool.setuptools_scm]
    write_to = "<pkgname>/_version.py"
    

    c. Finally, an alternative is to execute a script manually when you wish to create releases. This script must run after git-tagging your project and before publishing it.

    from setuptools_scm import get_version
    get_version('.', write_to='<pkg_name>/_version.py')
    

    you can for example run this as a single commandline in your continuous integration process:

    > python -c "from setuptools_scm import get_version;get_version('.', write_to='<pkg_name>/_version.py')"
    

This pattern (1+2a) has worked successfully for me for dozens of published packages over the past years (for example makefun), so I can warmly recommend it.

Note: I originally provided the above tip in a separate web page here.

EDIT: setuptools_scm's documentation states that write_to is now deprecated (but still supported at the time of writing). They recommend to use version_file instead.

Cellarer answered 21/2, 2022 at 20:10 Comment(0)
A
-3

If you use CVS (or RCS) and want a quick solution, you can use:

__version__ = "$Revision: 1.1 $"[11:-2]
__version_info__ = tuple([int(s) for s in __version__.split(".")])

(Of course, the revision number will be substituted for you by CVS.)

This gives you a print-friendly version and a version info that you can use to check that the module you are importing has at least the expected version:

import my_module
assert my_module.__version_info__ >= (1, 1)
Ablepsia answered 2/6, 2017 at 10:11 Comment(1)
What file are you recommending saving saving __version__ to? How would one increment the version number with this solution?Coblenz
S
-4

For what it's worth, if you're using NumPy distutils, numpy.distutils.misc_util.Configuration has a make_svn_version_py() method that embeds the revision number inside package.__svn_version__ in the variable version .

Shorn answered 19/1, 2009 at 18:42 Comment(2)
Can you provide more details or an example of how this would work?Coblenz
Hmm. In 2020, this is (was it always?) meant for FORTRAN. Package "numpy.distutils is part of NumPy extending standard Python distutils to deal with Fortran sources."Genesa

© 2022 - 2025 — McMap. All rights reserved.