web-dev-qa-db-fra.com

Méthode standard pour intégrer la version dans python?

Existe-t-il un moyen standard d'associer une chaîne de version à un package python) de manière à ce que je puisse effectuer les opérations suivantes?

import foo
print foo.version

J'imagine qu'il existe un moyen de récupérer ces données sans codage dur supplémentaire, car les chaînes mineures/majeures sont spécifiées dans setup.py déjà. La solution alternative que j'ai trouvée était d'avoir import __version__ dans mon foo/__init__.py puis avoir __version__.py généré par setup.py.

228
Dimitri Tcaciuc

Pas directement une réponse à votre question, mais vous devriez envisager de le nommer __version__, pas version.

C'est presque une quasi-standard. De nombreux modules de la bibliothèque standard utilisent __version__, et ceci est également utilisé dans lots de modules tiers, il s’agit donc d’un quasi standard.

Habituellement, __version__ est une chaîne, mais parfois c'est aussi un float ou un tuple.

Edit: comme mentionné par S.Lott (Merci!), PEP 8 le dit explicitement:

Version de comptabilité

Si vous devez avoir Subversion, CVS ou RCS crud dans votre fichier source, procédez comme suit.

    __version__ = "$Revision: 63990 $"
    # $Source$

Ces lignes doivent être incluses après la docstring du module, avant tout autre code, séparées par une ligne vierge au-dessus et au-dessous.

Vous devez également vous assurer que le numéro de version est conforme au format décrit dans PEP 44 ( PEP 386 une version antérieure de cette norme).

115
oefe

J'utilise un seul fichier _version.py Comme "emplacement unique" pour stocker les informations de version:

  1. Il fournit un attribut __version__.

  2. Il fournit la version standard des métadonnées. Par conséquent, il sera détecté par pkg_resources Ou d'autres outils qui analysent les métadonnées du paquet (Egg-INFO et/ou PKG-INFO, PEP 0345).

  3. Cela n’importera pas votre paquet (ni quoi que ce soit d’autre) lors de la construction de votre paquet, ce qui peut poser problème dans certaines situations. (Voir les commentaires ci-dessous sur les problèmes que cela peut causer.)

  4. Il n'y a qu'un seul endroit où le numéro de version est écrit, il n'y a donc qu'un seul endroit pour le changer lorsque le numéro de version change, et il y a moins de risque de versions incohérentes.

Voici comment cela fonctionne: "un emplacement canonique" pour stocker le numéro de version est un fichier .py, nommé "_version.py", qui se trouve dans votre paquet Python, par exemple dans myniftyapp/_version.py. Ce fichier est un Python, mais votre setup.py ne l’importe pas! (Cela annulerait la fonctionnalité 3.). Au lieu de cela, votre setup.py sait que le contenu de Ce fichier est très simple, quelque chose comme:

__version__ = "3.6.5"

Et donc votre setup.py ouvre le fichier et l’analyse, avec un code comme:

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

Ensuite, votre setup.py transmet cette chaîne en tant que valeur de l'argument "version" à setup(), satisfaisant ainsi la fonctionnalité 2.

Pour satisfaire à la fonctionnalité 1, vous pouvez demander à votre paquet (au moment de l'exécution, pas au moment de l'installation!) D'importer le fichier _version à partir de myniftyapp/__init__.py Comme ceci:

from _version import __version__

Voici n exemple de cette technique que j'utilise depuis des années.

Le code dans cet exemple est un peu plus compliqué, mais l'exemple simplifié que j'ai écrit dans ce commentaire devrait être une implémentation complète.

Voici exemple de code d'importation de la version .

Si vous voyez quelque chose qui ne va pas avec cette approche, s'il vous plaît faites le moi savoir.

101
Zooko

Réécrit 2017-05

Après plus de dix ans d’écriture Python et gestion de divers packages, j’en suis venu à la conclusion que le bricolage n’était peut-être pas la meilleure approche.

J'ai commencé à utiliser le package pbr pour gérer le contrôle de version dans mes packages. Si vous utilisez git en tant que SCM, cela s'intégrera parfaitement dans votre flux de travail, ce qui vous épargnera des semaines de travail (vous serez surpris de la complexité du problème).

À ce jour, pbr est classé n ° 11 le plus utilisé python et atteindre ce niveau ne comportait aucune astuce fâcheuse: un seul: réparer un problème de conditionnement courant dans un moyen très simple.

pbr peut faire plus de la maintenance du paquet, ne se limite pas à la gestion des versions mais ne vous oblige pas à en adopter tous les avantages.

Donc, pour vous donner une idée de l’apparence d’adopter pbr dans un commit, jetez un œil à passage de l’emballage à pbr

Vous auriez probablement constaté que la version n'est pas du tout stockée dans le référentiel. PBR le détecte à partir de branches et de tags Git.

Inutile de vous inquiéter de ce qui se passe lorsque vous n'avez pas de référentiel git car pbr "compile" et met en cache la version lorsque vous mettez en package ou installez les applications. Il n'y a donc aucune dépendance d'exécution à l'égard de git.

Ancienne solution

Voici la meilleure solution que j'ai vue jusqu'à présent et cela explique aussi pourquoi:

À l'intérieur 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'

À l'intérieur yourpackage/__init__.py:

from .version import __version__

À l'intérieur setup.py:

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

Si vous connaissez une autre approche qui semble meilleure, faites-le moi savoir.

93
sorin

Selon le différé PEP 396 (numéros de version de module) , il existe un moyen de procéder. Il décrit, avec justification, une norme (certes optionnelle) pour les modules à suivre. Voici un extrait:

3) Lorsqu'un module (ou package) inclut un numéro de version, la version DEVRAIT être disponible dans le fichier __version__ attribut.

4) Pour les modules qui résident dans un paquet d'espace de noms, le module DEVRAIT inclure le __version__ attribut. Le paquet d'espaces de noms lui-même NE DEVRAIT PAS inclure son propre __version__ attribut.

5) Le __version__ la valeur de l'attribut DEVRAIT être une chaîne.

26
Oddthinking

Bien que ce soit probablement trop tard, il existe une alternative légèrement plus simple à la réponse précédente:

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

(Et il serait assez simple de convertir des portions de numéros de version auto-incrémentées en chaîne à l'aide de str().)

Bien sûr, d'après ce que j'ai vu, les gens ont tendance à utiliser quelque chose comme la version mentionnée précédemment en utilisant __version_info__, Et à ce titre, le stockent sous forme de Tuple of Ints; cependant, je ne vois pas trop l'intérêt de le faire, car je doute que vous puissiez effectuer des opérations mathématiques telles que l'addition et la soustraction de portions de numéros de version à des fins autres que la curiosité ou l'auto-incrémentation (et même dans ce cas, int() et str() peuvent être utilisés assez facilement). (D'un autre côté, il est possible que le code de quelqu'un d'autre attende un tuple numérique plutôt qu'un tuple de chaîne et qu'il échoue ainsi.)

C’est, bien entendu, mon propre point de vue, et j’aimerais volontiers les commentaires des autres sur l’utilisation d’un tuple numérique.


Comme me l'a rappelé Shezi, les comparaisons (lexicales) des chaînes numériques n'ont pas nécessairement le même résultat que les comparaisons numériques directes; des zéros au début seraient nécessaires pour répondre à cela. Donc, en fin de compte, stocker __version_info__ (Ou ce que cela s'appellerait) sous forme de tuple de valeurs entières permettrait des comparaisons de version plus efficaces.

22
JAB

J'utilise un fichier JSON dans le répertoire dir. Cela correspond aux exigences de Zooko.

À l'intérieur pkg_dir/pkg_info.json:

{"version": "0.1.0"}

À l'intérieur 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'],
    ...
    )

À l'intérieur 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']

Je mets aussi d'autres informations dans pkg_info.json, comme auteur. J'aime utiliser JSON car je peux automatiser la gestion des métadonnées.

11
Andy Lee

Beaucoup de ces solutions ignorent ici les balises git version, ce qui signifie que vous devez suivre la version à plusieurs endroits (mauvais). Je l'ai abordé avec les objectifs suivants:

  • Dérivez toutes les références python version à partir d'une balise dans le repo git
  • Automatisez les étapes git tag/Push et setup.py upload Avec une seule commande ne prenant aucune entrée.

Comment ça fonctionne:

  1. À partir d'une commande make release, La dernière version balisée du référentiel git est trouvée et incrémentée. La balise est repoussée à Origin.

  2. La Makefile enregistre la version dans src/_version.py, Où elle sera lue par setup.py Et incluse dans la version. Ne pas cocher _version.py Dans le contrôle de source!

  3. La commande setup.py Lit la nouvelle chaîne de version à partir de package.__version__.

Détails:

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

La cible release incrémente toujours le troisième chiffre de la version, mais vous pouvez utiliser le next_minor_ver Ou next_major_ver Pour incrémenter les autres chiffres. Les commandes reposent sur le script versionbump.py Qui est archivé à la racine du référentiel.

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 __== '__main__':
    cli()  # pylint: disable=no-value-for-parameter

Ceci explique comment traiter et incrémenter le numéro de version de git.

__init__.py

Le fichier my_module/_version.py Est importé dans my_module/__init__.py. Indiquez ici la configuration d'installation statique que vous souhaitez distribuer avec votre module.

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

setup.py

La dernière étape consiste à lire les informations de version à partir du module my_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__'],
    ...
    ...
)

Bien sûr, pour que tout cela fonctionne, vous devez avoir au moins une balise de version dans votre référentiel pour commencer.

git tag -a v0.0.1
10
cmcginty

À noter également que __version__ étant un semi-std. dans python est donc __version_info__ qui est un tuple, dans les cas simples, vous pouvez simplement faire quelque chose comme:

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

... et vous pouvez obtenir le __version__ chaîne d'un fichier, ou autre.

6
James Antill

Il ne semble pas exister de méthode standard pour incorporer une chaîne de version dans un package python. La plupart des packages que j'ai vus utilisent une variante de votre solution, par exemple, eitner

  1. Incorporer la version dans setup.py et ont setup.py générer un module (par exemple version.py) ne contenant que des informations de version importées par votre paquet, ou

  2. L'inverse: mettez les informations de version dans votre paquet lui-même, et importez que pour définir la version dans setup.py

6
dF.

J'ai aussi vu un autre style:

>>> Django.VERSION
(1, 1, 0, 'final', 0)
4
L1ker

flèche le gère de manière intéressante.

Maintenant (depuis 2e5031b )

Dans arrow/__init__.py:

__version__ = 'x.y.z'

Dans setup.py:

from arrow import __version__

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

Avant

Dans arrow/__init__.py:

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

Dans 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__'),
    # [...]
)
2
Anto

Pour ce que ça vaut, si vous utilisez NumPy distutils, numpy.distutils.misc_util.Configuration A une méthode make_svn_version_py() qui intègre le numéro de révision dans package.__svn_version__ Dans le variable version.

0
Matt