How can I get the version defined in setup.py (setuptools) in my package?

前端 未结 16 741
无人及你
无人及你 2020-11-27 09:47

How could I get the version defined in setup.py from my package (for --version, or other purposes)?

相关标签:
16条回答
  • 2020-11-27 10:34

    To avoid importing a file (and thus executing its code) one could parse it and recover the version attribute from the syntax tree:

    # assuming 'path' holds the path to the file
    
    import ast
    
    with open(path, 'rU') as file:
        t = compile(file.read(), path, 'exec', ast.PyCF_ONLY_AST)
        for node in (n for n in t.body if isinstance(n, ast.Assign)):
            if len(node.targets) == 1:
                name = node.targets[0]
                if isinstance(name, ast.Name) and \
                        name.id in ('__version__', '__version_info__', 'VERSION'):
                    v = node.value
                    if isinstance(v, ast.Str):
                        version = v.s
                        break
                    if isinstance(v, ast.Tuple):
                        r = []
                        for e in v.elts:
                            if isinstance(e, ast.Str):
                                r.append(e.s)
                            elif isinstance(e, ast.Num):
                                r.append(str(e.n))
                        version = '.'.join(r)
                        break
    

    This code tries to find the __version__ or VERSION assignment at the top level of the module return is string value. The right side can be either a string or a tuple.

    0 讨论(0)
  • 2020-11-27 10:35

    example study: mymodule

    Imagine this configuration:

    setup.py
    mymodule/
            / __init__.py
            / version.py
            / myclasses.py
    

    Then imagine some usual scenario where you have dependencies and setup.py looks like:

    setup(...
        install_requires=['dep1','dep2', ...]
        ...)
    

    And an example __init__.py:

    from mymodule.myclasses import *
    from mymodule.version import __version__
    

    And for example myclasses.py:

    # these are not installed on your system.
    # importing mymodule.myclasses would give ImportError
    import dep1
    import dep2
    

    problem #1: importing mymodule during setup

    If your setup.py imports mymodule then during setup you would most likely get an ImportError. This is a very common error when your package has dependencies. If your package does not have other dependencies than the builtins, you may be safe; however this isn't a good practice. The reason for that is that it is not future-proof; say tomorrow your code needs to consume some other dependency.

    problem #2: where's my __version__ ?

    If you hardcode __version__ in setup.py then it may not match the version that you would ship in your module. To be consistent, you would put it in one place and read it from the same place when you need it. Using import you may get the problem #1.

    solution: à la setuptools

    You would use a combination of open, exec and provide a dict for exec to add variables:

    # setup.py
    from setuptools import setup, find_packages
    from distutils.util import convert_path
    
    main_ns = {}
    ver_path = convert_path('mymodule/version.py')
    with open(ver_path) as ver_file:
        exec(ver_file.read(), main_ns)
    
    setup(...,
        version=main_ns['__version__'],
        ...)
    

    And in mymodule/version.py expose the version:

    __version__ = 'some.semantic.version'
    

    This way, the version is shipped with the module, and you do not have issues during setup trying to import a module that has missing dependencies (yet to be installed).

    0 讨论(0)
  • 2020-11-27 10:35

    This should also work, using regular expressions and depending on the metadata fields to have a format like this:

    __fieldname__ = 'value'
    

    Use the following at the beginning of your setup.py:

    import re
    main_py = open('yourmodule.py').read()
    metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", main_py))
    

    After that, you can use the metadata in your script like this:

    print 'Author is:', metadata['author']
    print 'Version is:', metadata['version']
    
    0 讨论(0)
  • 2020-11-27 10:36

    Simple and straight, create a file called source/package_name/version.py with the following contents:

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    __version__ = "2.6.9"
    

    Then, on your file source/package_name/__init__.py, you import the version for other people to use:

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    from .version import __version__
    

    Now, you can put this on setup.py

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    import re
    import sys
    
    try:
        filepath = 'source/package_name/version.py'
        version_file = open( filepath )
        __version__ ,= re.findall( '__version__ = "(.*)"', version_file.read() )
    
    except Exception as error:
        __version__ = "0.0.1"
        sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
    
    finally:
        version_file.close()
    

    Tested this with Python 2.7, 3.3, 3.4, 3.5, 3.6 and 3.7 on Linux, Windows and Mac OS. I used on my package which has Integration and Unit Tests for all theses platforms. You can see the results from .travis.yml and appveyor.yml here:

    1. https://travis-ci.org/evandrocoan/debugtools/builds/527110800
    2. https://ci.appveyor.com/project/evandrocoan/pythondebugtools/builds/24245446

    An alternate version is using context manager:

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    import re
    import sys
    
    try:
        filepath = 'source/package_name/version.py'
    
        with open( filepath ) as file:
            __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
    
    except Exception as error:
        __version__ = "0.0.1"
        sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
    

    You can also be using the codecs module to handle unicode errors both on Python 2.7 and 3.6

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    import re
    import sys
    import codecs
    
    try:
        filepath = 'source/package_name/version.py'
    
        with codecs.open( filepath, 'r', errors='ignore' ) as file:
            __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
    
    except Exception as error:
        __version__ = "0.0.1"
        sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
    

    If you are writing a Python module 100% in C/C++ using Python C Extensions, you can do the same thing, but using C/C++ instead of Python.

    On this case, create the following setup.py:

    #!/usr/bin/env python3
    # -*- coding: UTF-8 -*-
    import re
    import sys
    import codecs
    from setuptools import setup, Extension
    
    try:
        filepath = 'source/version.h'
    
        with codecs.open( filepath, 'r', errors='ignore' ) as file:
            __version__ ,= re.findall( '__version__ = "(.*)"', file.read() )
    
    except Exception as error:
        __version__ = "0.0.1"
        sys.stderr.write( "Warning: Could not open '%s' due %s\n" % ( filepath, error ) )
    
    setup(
            name = 'package_name',
            version = __version__,
    
            package_data = {
                    '': [ '**.txt', '**.md', '**.py', '**.h', '**.hpp', '**.c', '**.cpp' ],
                },
    
            ext_modules = [
                Extension(
                    name = 'package_name',
                    sources = [
                        'source/file.cpp',
                    ],
                    include_dirs = ['source'],
                )
            ],
        )
    

    Which reads the version from the file version.h:

    const char* __version__ = "1.0.12";
    

    But, do not forget to create the MANIFEST.in to include the version.h file:

    include README.md
    include LICENSE.txt
    
    recursive-include source *.h
    

    And it is integrated into the main application with:

    #include <Python.h>
    #include "version.h"
    
    // create the module
    PyMODINIT_FUNC PyInit_package_name(void)
    {
        PyObject* thismodule;
        ...
    
        // https://docs.python.org/3/c-api/arg.html#c.Py_BuildValue
        PyObject_SetAttrString( thismodule, "__version__", Py_BuildValue( "s", __version__ ) );
    
        ...
    }
    

    References:

    1. python open file error
    2. Define a global in a Python module from a C API
    3. How to include package data with setuptools/distribute?
    4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
    5. How to use setuptools packages and ext_modules with the same name?
    6. Is it possible to include subdirectories using dist utils (setup.py) as part of package data?
    0 讨论(0)
  • 2020-11-27 10:39

    Your question is a little vague, but I think what you are asking is how to specify it.

    You need to define __version__ like so:

    __version__ = '1.4.4'
    

    And then you can confirm that setup.py knows about the version you just specified:

    % ./setup.py --version
    1.4.4
    
    0 讨论(0)
  • 2020-11-27 10:40

    Create a file in your source tree, e.g. in yourbasedir/yourpackage/_version.py . Let that file contain only a single line of code, like this:

    __version__ = "1.1.0-r4704"

    Then in your setup.py, open that file and parse out the version number like this:

    verstr = "unknown"
    try:
        verstrline = open('yourpackage/_version.py', "rt").read()
    except EnvironmentError:
        pass # Okay, there is no version file.
    else:
        VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
        mo = re.search(VSRE, verstrline, re.M)
        if mo:
            verstr = mo.group(1)
        else:
            raise RuntimeError("unable to find version in yourpackage/_version.py")
    

    Finally, in yourbasedir/yourpackage/__init__.py import _version like this:

    __version__ = "unknown"
    try:
        from _version import __version__
    except ImportError:
        # We're running in a tree that doesn't have a _version.py, so we don't know what our version is.
        pass
    

    An example of code that does this is the "pyutil" package that I maintain. (See PyPI or google search -- stackoverflow is disallowing me from including a hyperlink to it in this answer.)

    @pjeby is right that you shouldn't import your package from its own setup.py. That will work when you test it by creating a new Python interpreter and executing setup.py in it first thing: python setup.py, but there are cases when it won't work. That's because import youpackage doesn't mean to read the current working directory for a directory named "yourpackage", it means to look in the current sys.modules for a key "yourpackage" and then to do various things if it isn't there. So it always works when you do python setup.py because you have a fresh, empty sys.modules, but this doesn't work in general.

    For example, what if py2exe is executing your setup.py as part of the process of packaging up an application? I've seen a case like this where py2exe would put the wrong version number on a package because the package was getting its version number from import myownthing in its setup.py, but a different version of that package had previously been imported during the py2exe run. Likewise, what if setuptools, easy_install, distribute, or distutils2 is trying to build your package as part of a process of installing a different package that depends on yours? Then whether your package is importable at the time that its setup.py is being evaluated, or whether there is already a version of your package that has been imported during this Python interpreter's life, or whether importing your package requires other packages to be installed first, or has side-effects, can change the results. I've had several struggles with trying to re-use Python packages which caused problems for tools like py2exe and setuptools because their setup.py imports the package itself in order to find its version number.

    By the way, this technique plays nicely with tools to automatically create the yourpackage/_version.py file for you, for example by reading your revision control history and writing out a version number based on the most recent tag in revision control history. Here is a tool that does that for darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst and here is a code snippet which does the same thing for git: http://github.com/warner/python-ecdsa/blob/0ed702a9d4057ecf33eea969b8cf280eaccd89a1/setup.py#L34

    0 讨论(0)
提交回复
热议问题