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

后端 未结 7 1752
盖世英雄少女心
盖世英雄少女心 2020-12-07 08:42

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

# file: setup.py
...
setup(
name=\'foobar         


        
相关标签:
7条回答
  • 2020-12-07 08:58

    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.

    0 讨论(0)
  • 2020-12-07 09:03

    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
    
    0 讨论(0)
  • 2020-12-07 09:04

    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?

    0 讨论(0)
  • 2020-12-07 09:05

    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.

    0 讨论(0)
  • 2020-12-07 09:05

    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')
    
    0 讨论(0)
  • 2020-12-07 09:06

    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

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