web-dev-qa-db-fra.com

Quelle est la bonne façon de partager la version du package avec setup.py et le package?

Avec distutils, setuptools, etc. une version de package est spécifiée dans setup.py:

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

Je voudrais pouvoir accéder au même numéro de version depuis le package:

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

Je pourrais ajouter __version__ = '1.0.0' à __init__.py de mon package, mais je voudrais également inclure des importations supplémentaires dans mon package pour créer une interface simplifiée vers le package:

# file: __init__.py

from foobar import foo
from foobar.bar import Bar

__version__ = '1.0.0'

et

# file: setup.py

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

Cependant, ces importations supplémentaires peuvent entraîner l'échec de l'installation de foobar si elles importent d'autres packages qui ne sont pas encore installés. Quelle est la bonne façon de partager la version du package avec setup.py et le package?

58
Jace Browning

Définissez la version dans setup.py uniquement, et lisez votre propre version avec pkg_resources , interrogeant efficacement les métadonnées setuptools:

fichier: setup.py

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

fichier: __init__.py

from pkg_resources import get_distribution

__version__ = get_distribution('foobar').version

Pour que cela fonctionne dans tous les cas, où vous pourriez finir par l'exécuter sans l'avoir installé, testez DistributionNotFound et l'emplacement de distribution:

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
72
Martijn Pieters

Je ne crois pas qu'il y ait une réponse canonique à cela, mais ma méthode (soit directement copiée soit légèrement modifiée à partir de ce que j'ai vu à divers autres endroits) est la suivante:

Hiérarchie des dossiers (fichiers pertinents uniquement):

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.
)

... ce qui est moche comme un péché ... mais ça marche, et je l'ai vu ou quelque chose comme ça dans des paquets distribués par des gens que je m'attendrais à mieux connaître s'il y en avait un.

18
Zero Piraeus

Je suis d'accord avec la philosophie de @ stefano-m à propos de:

Avoir la version = "x.y.z" dans la source et l'analyser dans setup.py est certainement la bonne solution, à mon humble avis. Beaucoup mieux que (l'inverse) s'appuyant sur la magie du temps d'exécution.

Et cette réponse est dérivée de celle de @ zero-piraeus réponse . Le point essentiel est "n'utilisez pas les importations dans setup.py, lisez plutôt la version à partir d'un fichier".

J'utilise regex pour analyser le __version__ pour qu'il ne soit pas nécessaire que ce soit la dernière ligne d'un fichier dédié. En fait, je mets toujours la source unique de vérité __version__ à l'intérieur de mon projet __init__.py.

Hiérarchie des dossiers (fichiers pertinents uniquement):

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.
)

... ce qui n'est pas encore idéal ... mais ça marche.

Et en passant, à ce stade, vous pouvez tester votre nouveau jouet de cette manière:

python setup.py --version
1.2.3

PS: Ce officiel Python (et son miroir ) décrit plus d'options. Sa première option utilise également l'expression régulière. (Dépend de l'expression exacte que vous utilisez, elle peut ou non gérer les guillemets à l'intérieur de la chaîne de version. En général, ce n'est pas un gros problème.)

PPS: le correctif dans ADAL Python est maintenant rétroporté dans cette réponse.

12
RayLuo

Mettez __version__ dans your_pkg/__init__.py, et analyser dans setup.py en utilisant 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],
      ...
      )

Si vous utilisez Python <3.4, ​​notez que importlib.util.find_spec n'est pas disponible. De plus, aucun backport de importlib ne peut bien sûr être invoqué pour être disponible pour setup.py. Dans ce cas, utilisez:

import os

file_path = os.path.join(os.path.dirname(__file__), PKG_DIR, '__init__.py')
3
nexcvon

Il existe plusieurs méthodes proposées dans les Guides d'emballage sur python.org.

2
funky-future

Sur la base de réponse acceptée et des commentaires, voici ce que j'ai fini par faire:

fichier: setup.py

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

fichier: __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

Explication:

  • __project__ est le nom du projet à installer qui peut être différent du nom du package

  • VERSION est ce que j'affiche dans mes interfaces de ligne de commande lorsque --version Est demandé

  • les importations supplémentaires (pour l'interface de package simplifiée) ne se produisent que si le projet a effectivement été installé

2
Jace Browning

La réponse acceptée nécessite que le package soit installé. Dans mon cas, j'avais besoin d'extraire les paramètres d'installation (y compris __version__) à partir de la source setup.py. J'ai trouvé une solution directe et simple en parcourant les tests du paquet setuptools . Vous cherchez plus d'informations sur le _setup_stop_after l'attribut m'a conduit à n ancien message de la liste de diffusion qui mentionnait distutils.core.run_setup, ce qui m'amène à les documents réels nécessaires . Après tout cela, voici la solution simple:

fichier 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)

fichier extract.py:

from distutils.core import run_setup
dist = run_setup('./setup.py', stop_after='init')
dist.get_version()
2
ZachP