Manière standard d'intégrer la version dans un paquet python?
Question
Existe-t-il un moyen standard d'associer une chaîne de version à un package python de telle sorte 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 en dur supplémentaire, car les chaînes mineures / majeures sont déjà spécifiées dans setup.py
. La solution alternative que j’ai trouvée était d’avoir import __version__
dans mon foo/__init__.py
, puis __version__.py
généré par <=>.
La solution
Ce n'est pas directement une réponse à votre question, mais vous devriez envisager de le nommer __version__
, pas version
.
Ceci est presque une quasi-standard. De nombreux modules de la bibliothèque standard utilisent <=> et ceci est également utilisé dans beaucoup de modules tiers , il s’agit donc du quasi standard.
En général, <=> est une chaîne, mais parfois c'est aussi un flottant ou un tuple.
Modifier: comme mentionné par S.Lott (merci!), PEP 8 le dit explicitement:
Version comptable
Si vous devez avoir Subversion, CVS ou RCS crud dans votre fichier source, faites-le comme suit.
__version__ = "$Revision: 63990 $" # $Source$
Ces lignes doivent être incluses après la docstring du module, avant tout autre code, séparé par une ligne vierge en haut et en bas.
Vous devez également vous assurer que le numéro de version est conforme au format décrit dans PEP 440. ( PEP 386 , une version antérieure de cette norme).
Autres conseils
J'utilise un seul et même fichier _version.py
comme " one Cannonical Place " pour stocker les informations de version:
-
Il fournit un
__version__
attribut. -
Il fournit la version standard des métadonnées. Par conséquent, il sera détecté par
pkg_resources
ou par d'autres outils qui analysent les métadonnées du package (EGG-INFO et / ou PKG-INFO, PEP 0345). -
Il n'importe pas votre package (ni quoi que ce soit d'autre) lors de la construction de votre package, ce qui peut entraîner des problèmes dans certaines situations. (Voir les commentaires ci-dessous sur les problèmes que cela peut causer.)
-
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: le & "; un lieu canonique &"; pour stocker le numéro de version est un fichier .py, nommé " _version.py " qui se trouve dans votre package Python, par exemple dans myniftyapp/_version.py
. Ce fichier est un module 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 ainsi 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 la & version; " argument à 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 un exemple de cette technique Je l'utilise depuis des années.
Le code de 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 un exemple de code d'importation la version .
Si vous voyez quelque chose qui ne va pas avec cette approche, merci de me le faire savoir.
Réécrit 2017-05
Après plus de dix ans d'écriture de code Python et de 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 pbr
package 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é 11ème paquet python le plus utilisé et n'a pas atteint ce niveau N'incluez aucune astuce sale: un seul: résoudre un problème d’emballage commun de manière très simple.
yourpackage/version.py
peut faire plus de travail de 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 la manière dont il semble adopter pbr dans un commit, jetez un coup d'oeil swich emballage pour pfr
Vous constaterez probablement que la version n’est pas du tout stockée dans le référentiel. PBR le détecte dans les branches et les tags Git.
Inutile de vous inquiéter de ce qui se passe lorsque vous n'avez pas de référentiel git, car pbr fait & "compiler &"; et mettre en cache la version lorsque vous compressez 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 qui explique également pourquoi:
À l'intérieur yourpackage/__init__.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 setup.py
:
from .version import __version__
À l'intérieur <=>:
exec(open('yourpackage/version.py').read())
setup(
...
version=__version__,
...
Si vous connaissez une autre approche qui semble meilleure, faites-le moi savoir.
Selon le reporté PEP 396 (numéros de version de module) , un moyen proposé de le faire. 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, cette version DEVRAIT être disponible dans l'attribut
__version__
.4) Pour les modules qui résident dans un paquet d'espace de noms, le module DEVRAIT inclure l'attribut <=>. Le paquet d'espace de noms lui-même NE DEVRAIT PAS inclure son propre <=> attribut.
5) La <=> valeur de l'attribut DEVRAIT être une chaîne.
Bien que ce soit probablement trop tard, il existe une alternative un peu 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 lorsqu'ils utilisent __version_info__
, et à ce titre, le stockent comme un tuple d'intes; 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 <=> peuvent être utilisés assez facilement). (D'autre part, 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 sûr, mon point de vue, et j’aimerais bien que les autres nous disent comment utiliser un tuple numérique.
Comme me l'a rappelé Shezi, les comparaisons (lexicales) des chaînes de nombres 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 <=> (ou ce que cela s'appellera) sous forme de tuple de valeurs entières permettrait une comparaison plus efficace des versions.
J'utilise un fichier JSON dans le répertoire dir. Cela répond 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 également d'autres informations dans pkg_info.json
, comme l'auteur. je
souhaite utiliser JSON car je peux automatiser la gestion des métadonnées.
Beaucoup de ces solutions ignorent les git
balises de version, ce qui signifie que vous devez suivre la version à plusieurs endroits (mauvais). J'ai abordé cette question avec les objectifs suivants:
- Dérivez toutes les références de version python d'une balise dans le
git tag
référentiel - Automatisez les étapes
push
/setup.py upload
etmake release
avec une seule commande ne prenant aucune entrée.
Comment ça marche:
-
Dans une commande
origin
, la dernière version marquée du référentiel git est trouvée et incrémentée. La balise est repoussée àMakefile
. -
Le
src/_version.py
enregistre la version danssetup.py
où elle sera lue par_version.py
et également incluse dans la version. Ne pas vérifierpackage.__version__
dans le contrôle de source!
La commande -
release
lit la nouvelle chaîne de version à partir denext_minor_ver
.
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 next_major_ver
cible incrémente toujours le troisième chiffre de la version, mais vous pouvez utiliser versionbump.py
ou my_module/_version.py
pour incrémenter les autres chiffres. Les commandes reposent sur le script my_module/__init__.py
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 __name__ == '__main__':
cli() # pylint: disable=no-value-for-parameter
Ceci explique comment traiter et incrémenter le numéro de version de my_module
.
__ init __. py
Le fichier <=> est importé dans <=>. 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.
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
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, à savoir eitner
-
Intégrez la version dans
setup.py
et faites en sorte queversion.py
génère un module (par exemple, <=>) contenant uniquement les informations de version importées par votre package ou .
-
L'inverse: placez les informations de version dans votre paquet lui-même et importez ce pour définir la version dans <=>
Il convient également de noter que ainsi que __version__
être un semi-std. en python, il en est ainsi __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 la <=> chaîne d'un fichier ou autre.
J'ai aussi vu un autre style:
>>> django.VERSION
(1, 1, 0, 'final', 0)
flèche le gère de manière intéressante.
Dans arrow/__init__.py
:
__version__ = '0.8.0'
VERSION = __version__
Dans setup.py
:
def grep(attrname):
pattern = r"{0}\W*=\W*'([^']+)'".format(attrname)
strval, = re.findall(pattern, file_text)
return strval
setup(
name='arrow',
version=grep('__version__'),
# [...]
)
Pour ce que ça vaut, si vous utilisez NumPy distutils, numpy.distutils.misc_util.Configuration
a un make_svn_version_py()
méthode intégrant le numéro de révision entre package.__svn_version__
dans la variable version
.
- Utilisez un fichier
version.py
uniquement avec__version__ = <VERSION>
param dans le fichier. Dans le fichiersetup.py
, importez le paramètre__version__
et mettez-le dans le fichierversion=__version__
comme ceci:version=<CURRENT_VERSION>
- Une autre méthode consiste à utiliser uniquement un
version=<VERSION>
fichier avec.bumpversion.cfg
- la CURRENT_VERSION est codée en dur.
Etant donné que nous ne souhaitons pas modifier manuellement la version du fichier chaque fois que nous créons un nouveau tag (prêt à publier une nouvelle version du package), nous pouvons utiliser les éléments suivants.
Je recommande vivement le package bumpversion . Je l'utilise depuis des années pour créer une version.
commencez par ajouter [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
à votre git commit
fichier si vous ne l'avez pas déjà.
Vous devez utiliser un court script comme celui-ci chaque fois que vous cognez une version:
bumpversion (patch|minor|major) - choose only one option
git push
git push --tags
Ajoutez ensuite un fichier par référentiel appelé: git reset
:
[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]
Remarque:
- Vous pouvez utiliser le paramètre <=> sous <=> fichier comme suggéré dans d'autres publications et mettre à jour le fichier bumpversion comme ceci: <=>
- Vous devez <=> ou <=> tout ce qui se trouve dans votre rapport, sinon vous obtiendrez une erreur de rapport sale.
- Assurez-vous que votre environnement virtuel inclut le package de bumpversion. Sans cela, il ne fonctionnera pas.
Si vous utilisez CVS (ou RCS) et souhaitez une solution rapide, vous pouvez utiliser:
__version__ = "$Revision: 1.1 $"[11:-2]
__version_info__ = tuple([int(s) for s in __version__.split(".")])
(Bien entendu, le numéro de révision vous sera remplacé par CVS.)
Ceci vous donne une version imprimable et des informations sur la version que vous pouvez utiliser pour vérifier que le module que vous importez a au moins la version attendue:
import my_module
assert my_module.__version_info__ >= (1, 1)