web-dev-qa-db-fra.com

Comment puis-je obtenir la version définie dans setup.py (setuptools) dans mon package?

Comment pourrais-je obtenir la version définie dans setup.py de mon paquet (pour --version, ou à d’autres fins)?

135
elmarco

Chaîne de version d'interrogation de la distribution déjà installée

Pour récupérer la version de votre package au moment de l'exécution (ce que votre question semble réellement demander), vous pouvez utiliser:

import pkg_resources  # part of setuptools
version = pkg_resources.require("MyProject")[0].version

Stocker la chaîne de version à utiliser lors de l'installation

Si vous voulez aller dans l'autre sens (ce qui semble être ce que d'autres auteurs de la réponse semblent avoir pensé que vous posiez la question), placez la chaîne de version dans un fichier séparé et lisez le contenu de ce fichier dans setup.py.

Vous pouvez créer un fichier version.py dans votre paquet avec une ligne __version__, Puis le lire à partir de setup.py en utilisant execfile('mypackage/version.py'), de manière à définir __version__ Dans la configuration. py espace de noms.

Si vous voulez un moyen beaucoup plus simple qui fonctionnera avec toutes les versions de Python) et même les langages non Python pouvant nécessiter un accès à la chaîne de version:

Stockez la chaîne de version en tant que contenu unique d’un fichier texte brut, nommé par exemple. VERSION et lisez ce fichier pendant setup.py.

version_file = open(os.path.join(mypackage_root_dir, 'VERSION'))
version = version_file.read().strip()

Le même fichier VERSION fonctionnera alors exactement de la même manière dans tout autre programme, même non-Python, et il vous suffira de changer la chaîne de version en un seul endroit pour tous les programmes.

Avertissement sur les conditions de concurrence pendant l'installation

En passant, NE PAS importer votre paquet depuis votre fichier setup.py comme suggéré dans une autre réponse: cela semblera fonctionner pour vous (parce que les dépendances de votre paquet sont déjà installées), mais cela fera des ravages chez les nouveaux utilisateurs de votre paquet. , car ils ne pourront pas installer votre paquet sans installer d'abord manuellement les dépendances.

226
PJ Eby

exemple d'étude: mymodule

Imaginez cette configuration:

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

Puis imaginez un scénario habituel dans lequel vous avez des dépendances et où setup.py Ressemble à ceci:

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

Et un exemple __init__.py:

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

Et par exemple myclasses.py:

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

problème n ° 1: importer mymodule lors de l'installation

Si votre setup.py Importe mymodule alors pendant l'installation, vous obtiendrez probablement un ImportError. C'est une erreur très courante lorsque votre paquet a des dépendances. Si votre paquet n'a pas d'autres dépendances que les commandes intégrées, vous pouvez être en sécurité; Cependant, ce n'est pas une bonne pratique. La raison en est que cela n’est pas à l’avenir; dites demain que votre code doit consommer une autre dépendance.

problème n ° 2: où est mon __version__?

Si vous codez en dur __version__ Dans setup.py, Il se peut que sa version ne corresponde pas à celle que vous auriez livrée dans votre module. Pour être cohérent, vous le placeriez au même endroit et le liriez au même endroit quand vous en auriez besoin. En utilisant import, vous pouvez obtenir le problème n ° 1.

solution: à la setuptools

Vous utiliseriez une combinaison de open, exec et fournissez un dict pour exec afin d'ajouter des 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__'],
    ...)

Et dans mymodule/version.py Exposer la version:

__version__ = 'some.semantic.version'

De cette manière, la version est livrée avec le module et vous ne rencontrez aucun problème lors de l'installation lorsque vous essayez d'importer un module comportant des dépendances manquantes (à installer).

30
dnozay

La meilleure technique consiste à définir __version__ dans votre code produit, puis importez-le dans setup.py à partir de là. Cela vous donne une valeur que vous pouvez lire dans votre module en cours d’exécution et n’a qu’un seul emplacement pour la définir.

Les valeurs de setup.py ne sont pas installées et setup.py ne reste pas après l'installation.

Qu'est-ce que j'ai fait (par exemple) dans coverage.py:

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

setup(
    name = 'coverage',
    version = __version__,
    ...
    )

UPDATE (2017): coverage.py ne s'importe plus pour obtenir la version. L'importation de votre propre code peut le rendre désinstallable, car votre code produit essaiera d'importer des dépendances, qui ne sont pas encore installées, car c'est setup.py qui les installe.

14
Ned Batchelder

Votre question est un peu vague, mais je pense que ce que vous demandez, c'est comment la spécifier.

Vous devez définir __version__ ainsi:

__version__ = '1.4.4'

Et vous pourrez alors confirmer que setup.py connaît la version que vous venez de spécifier:

% ./setup.py --version
1.4.4
13
jathanism

Je n'étais pas satisfait de ces réponses ... je ne voulais pas exiger de setuptools, ni créer un module séparé pour une seule variable, alors je les ai conçues.

Pour quand vous êtes sûr que le module principal est dans le style pep8 et le restera:

version = '0.30.unknown'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            _, _, version = line.replace("'", '').split()
            break

Si vous souhaitez être particulièrement prudent et utiliser un véritable analyseur syntaxique:

import ast
version = '0.30.unknown2'
with file('mypkg/mymod.py') as f:
    for line in f:
        if line.startswith('__version__'):
            version = ast.parse(line).body[0].value.s
            break

setup.py est un peu un module jetable, donc ce n’est pas un problème s’il est un peu laid.

7
Gringo Suave

Créez un fichier dans votre arborescence source, par exemple. dans yourbasedir/yourpackage/_version.py. Laissez ce fichier ne contenir qu'une seule ligne de code, comme ceci:

__version__ = "1.1.0-r4704"

Ensuite, dans votre setup.py, ouvrez ce fichier et analysez le numéro de version comme ceci:

 verstr = "unknown" 
 essayez: 
 verstrline = open ('yourpackage/_version.py', "rt"). read () 
 sauf EnvironmentError: 
 pass # OK, il n’existe pas de fichier de version. 
 sinon: 
 VSRE = r "^ __ version__ = ['\"] ([^'\"] *) ['\"]" 
 mo = re.search (VSRE, verstrline, re.M) 
 si mo: 
 verstr = mo.group (1) 
 sinon : 
 raise RuntimeError ("impossible de trouver la version dans votre package/_version.py") 

Enfin, dans yourbasedir/yourpackage/__init__.py, Importez la version _ comme ceci:

 __ version__ = "unknown" 
 essayez: 
 à partir de _version import __version __ 
 sauf ImportError: 
 # Nous courons dans un arbre qui ne fonctionne pas. t ont une _version.py, nous ne savons donc pas quelle est notre version. 
 pass 

Un exemple de code qui fait cela est le paquet "pyutil" que je gère. (Voir PyPI ou recherche Google - stackoverflow ne m'autorise pas à inclure un lien hypertexte dans cette réponse.)

@pjeby a raison de ne pas importer votre paquet depuis son propre fichier setup.py. Cela fonctionnera si vous le testez en créant un nouvel interpréteur Python et en exécutant setup.py: python setup.py, Mais il y a des cas où cela ne fonctionnera pas. C’est parce que import youpackage Ne veut pas dire lire le répertoire de travail actuel d’un répertoire nommé "yourpackage", cela signifie chercher dans le sys.modules Actuel une clé "yourpackage", puis faire divers Cela fonctionne toujours quand vous faites python setup.py parce que vous avez un nouveau sys.modules vide, mais cela ne fonctionne pas en général.

Par exemple, que se passe-t-il si py2exe exécute votre fichier setup.py dans le cadre du processus de création d’une application? J'ai vu un cas comme celui-ci où py2exe mettrait le mauvais numéro de version sur un paquet car le paquet recevait son numéro de version de import myownthing Dans son fichier setup.py, mais une version différente de ce paquet avait été précédemment importé lors de l'exécution de py2exe. De même, que se passe-t-il si setuptools, easy_install, distribution ou distutils2 essaie de construire votre paquet dans le cadre d’un processus d’installation d’un autre paquet qui dépend du vôtre? Ensuite, que votre paquet soit importable au moment de l’évaluation de son fichier setup.py ou qu’une version de votre paquet ait déjà été importée au cours de cette Python, ou votre package requiert l'installation préalable d'autres packages, ou bien ses effets secondaires peuvent modifier les résultats. Plusieurs tentatives pour réutiliser Python qui posaient des problèmes pour des outils tels que py2exe et setuptools car leur fichier setup.py importe le paquet lui-même afin de trouver son numéro de version.

En passant, cette technique fonctionne bien avec les outils permettant de créer automatiquement le fichier yourpackage/_version.py, Par exemple en lisant votre historique de contrôle de révision et en écrivant un numéro de version basé sur la balise la plus récente de l'historique de contrôle de révision. Voici un outil qui fait cela pour darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst et voici un extrait de code qui fait la même chose pour git: http://github.com/warner/python-ecdsa/blob/0ed702a9d4057ecf33eea969b8cf280eaccd89a1/setup.py#L34

4
Zooko

Avec une structure comme celle-ci:

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

version.py contient:

__version__ = 'version_string'

Vous pouvez le faire dans setup.py:

import sys

sys.path[0:0] = ['mymodule']

from version import __version__

Cela ne posera aucun problème avec les dépendances que vous avez dans votre mymodule/__ init __. Py

3
Jahid

Cela devrait également fonctionner, en utilisant des expressions régulières et en fonction des champs de métadonnées pour avoir un format comme celui-ci:

__field= 'value'

Utilisez ce qui suit au début de votre setup.py:

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

Après cela, vous pouvez utiliser les métadonnées de votre script comme ceci:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']
3
thp

Pour éviter d'importer un fichier (et donc d'exécuter son code), on pourrait l'analyser et récupérer l'attribut version à partir de l'arbre de syntaxe:

# 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

Ce code tente de trouver le __version__ ou VERSION L'affectation au niveau supérieur du retour du module est une valeur chaîne. Le côté droit peut être une chaîne ou un tuple.

2
Mikhail Edoshin

Il y a mille façons de peler un chat - voici le mien:

# Copied from (and hacked):
# https://github.com/pypa/virtualenv/blob/develop/setup.py#L42
def get_version(filename):
    import os
    import re

    here = os.path.dirname(os.path.abspath(__file__))
    f = open(os.path.join(here, filename))
    version_file = f.read()
    f.close()
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")
2
Marc Abramowitz

Nettoyage https://stackoverflow.com/a/124138 de @ gringo-suave:

from itertools import ifilter
from os import path
from ast import parse

with open(path.join('package_name', '__init__.py')) as f:
    __version__ = parse(next(ifilter(lambda line: line.startswith('__version__'),
                                     f))).body[0].value.s
2
A T

Nous voulions mettre les méta-informations sur notre paquet pypackagery dans __init__.py, mais n'a pas pu, car il existe des dépendances tierces, comme l'a déjà souligné PJ Eby (voir sa réponse et l'avertissement relatif à la situation de concurrence critique).

Nous l'avons résolu en créant un module séparé pypackagery_meta.py qui contient seulement la méta-information:

"""Define meta information about pypackagery package."""

__title__ = 'pypackagery'
__description__ = ('Package a subset of a monorepo and '
                   'determine the dependent packages.')
__url__ = 'https://github.com/Parquery/pypackagery'
__version__ = '1.0.0'
__author__ = 'Marko Ristin'
__author_email__ = '[email protected]'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Parquery AG'

puis importé la méta-information dans packagery/__init__.py:

# ...

from pypackagery_meta import __title__, __description__, __url__, \
    __version__, __author__, __author_email__, \
    __license__, __copyright__

# ...

et finalement utilisé dans setup.py:

import pypackagery_meta

setup(
    name=pypackagery_meta.__title__,
    version=pypackagery_meta.__version__,
    description=pypackagery_meta.__description__,
    long_description=long_description,
    url=pypackagery_meta.__url__,
    author=pypackagery_meta.__author__,
    author_email=pypackagery_meta.__author_email__,
    # ...
    py_modules=['packagery', 'pypackagery_meta'],
 )

Vous devez inclure pypackagery_meta dans votre paquet avec py_modules argument de configuration. Sinon, vous ne pourrez pas l'importer lors de l'installation car il en manquerait dans la distribution empaquetée.

2
marko.ristin

Maintenant, c’est brutal et a besoin d’être peaufiné (il se peut même qu’un appel de membre non couvert dans pkg_resources que j’ai manqué), mais je ne vois tout simplement pas pourquoi cela ne fonctionne pas, ni pourquoi personne ne l’a suggéré à ce jour (googler around has a pas vu cela) ... notez qu'il s'agit de Python 2.x, et nécessiterait l'exigence de pkg_resources (sigh):

import pkg_resources

version_string = None
try:
    if pkg_resources.working_set is not None:
        disto_obj = pkg_resources.working_set.by_key.get('<my pkg name>', None)
        # (I like adding ", None" to gets)
        if disto_obj is not None:
            version_string = disto_obj.version
except Exception:
    # Do something
    pass
1
Mark Gerolimatos

Simple et direct, créez un fichier nommé source/package_name/version.py avec le contenu suivant:

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

Ensuite, sur votre dossier source/package_name/__init__.py, vous importez la version à l’intention des autres utilisateurs:

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

Maintenant, vous pouvez mettre ceci sur 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()

Testé cela avec Python 2.7, 3.3, 3.4, 3.5, 3.6 et 3.7 sur Linux, Windows et Mac OS. J'ai utilisé sur mon paquet qui a l'intégration et les tests unitaires pour toutes ces plateformes. Vous pouvez voir les résultats de .travis.yml et appveyor.yml ici:

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

Une autre version utilise le gestionnaire de contexte:

#!/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 ) )

Vous pouvez également utiliser le module codecs pour gérer les erreurs Unicode à la fois sur Python 2.7 et 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 ) )

Si vous écrivez un Python à 100% en C/C++ à l'aide de Python C, vous pouvez faire la même chose, mais en utilisant C/C++ à la place de Python.

Dans ce cas, créez le 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'],
            )
        ],
    )

Ce qui lit la version du fichier version.h:

const char* __version__ = "1.0.12";

Mais n'oubliez pas de créer le MANIFEST.in pour inclure le version.h fichier:

include README.md
include LICENSE.txt

recursive-include source *.h

Et il est intégré à l'application principale avec:

#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__ ) );

    ...
}

Les références:

  1. erreur de fichier ouvert python
  2. Définit un global dans un Python à partir d'une API C)
  3. Comment inclure des données de paquet avec setuptools/distrib??
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. Comment utiliser les paquets setuptools et ext_modules avec le même nom?
  6. Est-il possible d'inclure des sous-répertoires en utilisant dist utils (setup.py) dans les données du paquet?
0
user