Manière standard d'incorporer la version dans un package python?

265

Existe-t-il un moyen standard d'associer une chaîne de version à un package python de telle manière que je puisse faire ce qui suit?

import foo
print foo.version

J'imagine qu'il existe un moyen de récupérer ces données sans aucun codage supplémentaire, car les chaînes mineures / majeures sont setup.pydéjà spécifiées dans . Une solution alternative que j'ai trouvée devait avoir import __version__dans mon foo/__init__.pyet ensuite être __version__.pygénérée par setup.py.

Dimitri Tcaciuc
la source
7
Pour info, il y a un très bon aperçu sur: packaging.python.org/en/latest/…
ionelmc
1
La version d'un paquet installé peut être récupérée à partir des métadonnées avec setuptools , donc dans de nombreux cas, mettre uniquement la version dans setup.pyest suffisant. Voir cette question .
saaj
2
Pour info, il existe essentiellement 5 modèles communs pour maintenir la source unique de vérité (à la fois lors de la configuration et de l'exécution) pour le numéro de version.
KF Lin
La documentation de @ionelmc Python répertorie 7 options différentes pour la source unique . Cela ne contredit-il pas le concept d'une " source unique de vérité "?
Stevoisiak
@StevenVascellaro pas sûr de ce que vous demandez. Il y a tellement de façons énumérées ici parce que le guide d'emballage ne veut pas être exprimé.
ionelmc

Réponses:

136

Pas directement une réponse à votre question, mais vous devriez envisager de la nommer __version__, pasversion .

Il s'agit presque d'un quasi-standard. De nombreux modules dans la bibliothèque standard utilisent __version__, et cela est également utilisé en lots modules tiers, c'est donc le quasi-standard.

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

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

Noms de donjon au niveau du module

Niveau du module « dunders » ( à savoir les noms avec deux grands et deux underscores de suivi) , tels que __all__, __author__, __version__, etc. doivent être placés après le module docstring mais avant toute déclaration à l'importation , à l' exception de l' __future__importation.

Vous devez également vous assurer que le numéro de version est conforme au format décrit dans PEP 440 ( PEP 386 une version précédente de cette norme).

oefe
la source
9
Il doit s'agir d'une chaîne et avoir une version_info pour la version de tuple.
James Antill
James: Pourquoi __version_info__spécifiquement? (Qui "invente" votre propre double-soulignement.) [Lorsque James a commenté, les soulignés n'ont rien fait dans les commentaires, maintenant ils indiquent l'accent, donc James a vraiment écrit __version_info__aussi. --- ed.]
Vous pouvez voir quelque chose sur la version à dire sur packages.python.org/distribute/… Cette page concerne la distribution, mais la signification du numéro de version devient une norme de facto.
sienkiew
2
Droite. Il semble que ces PPE se contredisent. Eh bien, PEP 8 dit "si" et "crud", donc il n'approuve pas vraiment l'expansion des mots clés VCS. De plus, si vous passez à un autre VCS, vous perdrez les informations de révision. Par conséquent, je suggère d'utiliser des informations de version conformes à PEP 386/440 intégrées dans un fichier source unique, au moins pour les projets plus importants.
oefe
2
Où mettriez-vous cette version . Étant donné qu'il s'agit de la version acceptée, j'aimerais voir ces informations supplémentaires ici.
darkgaze
120

J'utilise un seul _version.pyfichier comme "lieu une fois canonique" pour stocker les informations de version:

  1. Il fournit un __version__attribut.

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

  3. Il n'importe pas votre package (ou toute autre chose) lors de la construction de votre package, ce qui peut provoquer des problèmes 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, donc il n'y a qu'un seul endroit pour le changer lorsque le numéro de version change, et il y a moins de chance de versions incohérentes.

Voici comment cela fonctionne: le "seul endroit 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 donc votre setup.py ouvre le fichier et l'analyse, avec du 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 package (au moment de l'exécution, pas au moment de l'installation!) D'importer le fichier _version myniftyapp/__init__.pycomme ceci:

from _version import __version__

Voici un exemple de cette technique que j'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 de la version .

Si vous voyez quelque chose de mal avec cette approche, faites-le moi savoir.

Zooko
la source
8
Pourriez-vous décrire les problèmes qui motivent # 3? Glyph a dit que cela avait quelque chose à voir avec "setuptools aime prétendre que votre code n'est nulle part sur le système lorsque votre setup.py s'exécute", mais les détails aideraient à me convaincre, moi et les autres.
Ivan Kozik
2
@Iva Maintenant, dans quel ordre l'outil doit-il procéder? Il ne peut pas (dans le setuptools / pip / virtualenv système d'aujourd'hui) , même savoir ce que les DEPS sont jusqu'à ce qu'il évalue votre setup.py. De plus, s'il essayait de faire la profondeur en premier et de faire tous les dépôts avant de faire celui-ci, il resterait coincé s'il y avait des dépôts circulaires. Mais s'il essaie de construire ce paquet avant d'installer les dépendances, alors si vous importez votre paquet depuis votre setup.py, il ne pourra pas nécessairement importer ses deps, ou les bonnes versions de ses deps.
Zooko
3
Pourriez-vous écrire le fichier "version.py" de "setup.py" au lieu de l'analyser? Cela semble plus simple.
Jonathan Hartley
3
Jonathan Hartley: Je suis d'accord qu'il serait un peu plus simple pour votre "setup.py" d'écrire le fichier "version.py" au lieu de l'analyser, mais cela ouvrirait une fenêtre pour l'incohérence, lorsque vous avez édité votre setup.py pour avoir la nouvelle version mais pas encore exécuté setup.py pour mettre à jour le fichier version.py. Une autre raison pour que la version canonique soit dans un petit fichier séparé est qu'elle permet aux autres outils, tels que les outils qui lisent votre état de contrôle de révision, d'écrire le fichier de version.
Zooko
3
Une approche similaire consiste à execfile("myniftyapp/_version.py")partir de setup.py, plutôt que d'essayer d'analyser le code de version manuellement. Suggéré dans stackoverflow.com/a/2073599/647002 - une discussion peut également être utile.
medmunds
97

Réécrit 2017-05

Après plus de dix ans d'écriture de code Python et de gestion de divers packages, je suis arrivé à la conclusion que le bricolage n'est peut-être pas la meilleure approche.

J'ai commencé à utiliser le pbrpackage pour gérer les versions dans mes packages. Si vous utilisez git comme SCM, cela s'intégrera à votre flux de travail comme par magie, économisant ainsi vos semaines de travail (vous serez surpris de la complexité du problème).

À ce jour, pbr est classé n ° 11 des packages python les plus utilisés et atteindre ce niveau n'incluait aucun truc sale: n'en était qu'un: résoudre un problème de packaging commun de manière très simple.

pbr peut faire plus de la charge de maintenance des packages, ne se limite pas au versioning mais ne vous oblige pas à adopter tous ses avantages.

Donc, pour vous donner une idée de la façon dont il semble adopter pbr en un seul commit, jetez un œil à l' emballage pbr

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

Pas besoin de vous soucier 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 empaquetez ou installez les applications, il n'y a donc pas de dépendance d'exécution sur git.

Ancienne solution

Voici la meilleure solution que j'ai vue jusqu'à présent et elle explique également 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.

Sorin
la source
12
Euh, non. execfile () n'existe pas en Python 3, il est donc préférable d'utiliser exec (open (). read ()).
Christophe Vu-Brugier
4
pourquoi pas from .version import __version__dans setup.py aussi?
Aprillion
4
@Aprillion Parce que le paquet n'est pas chargé lorsqu'il setup.pyest en cours d'exécution - essayez-le, vous obtiendrez une erreur (ou du moins, je l'ai fait :-))
darthbith
3
Le lien vers pbr entraîne une mauvaise passerelle.
MERose
4
pbr , sans aucun doute, est un excellent outil, mais vous n'avez pas répondu à la question. Comment pouvez-vous accéder à la version actuelle ou au package installé via bpr .
nad2000
29

Selon le PEP 396 différé (numéros de version de module) , il existe une méthode proposée pour le faire. Il décrit, avec justification, une norme (certes facultative) 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 l' __version__attribut.

4) Pour les modules qui vivent à l'intérieur d'un paquet d'espace de noms, le module DEVRAIT inclure l' __version__attribut. Le package d'espace de noms lui-même NE DEVRAIT PAS inclure son propre __version__attribut.

5) La __version__valeur de l'attribut DEVRAIT être une chaîne.

Pensée étrange
la source
13
Ce PEP n'est pas accepté / standardisé, mais différé (par manque d'intérêt). Par conséquent, il est un peu trompeur d'affirmer qu '" il existe un moyen standard " spécifié par celui-ci.
tisserand
@weaver: Oh mon Dieu! J'ai appris quelque chose de nouveau. Je ne savais pas que c'était quelque chose que je devais vérifier.
Oddthinking
4
Modifié pour noter que ce n'est pas une norme. Maintenant, je me sens gêné, car j'ai soulevé des demandes de fonctionnalités sur des projets leur demandant de suivre cette "norme".
Oddthinking
1
Peut-être devriez-vous reprendre le travail de normalisation sur ce PEP, car vous semblez intéressé :)
tisserand
Cela fonctionnerait pour le versionnage d'un module individuel, mais je ne suis pas sûr que cela s'appliquerait au versioning d'un projet complet.
Stevoisiak
21

Bien que ce soit probablement beaucoup 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 à incrémentation automatique en une chaîne à l'aide 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 lors de l'utilisation __version_info__, et en tant que telle, à la stocker comme un tuple d'ints; cependant, je ne vois pas vraiment l'intérêt de le faire, car je doute qu'il y ait des situations où vous effectueriez des opérations mathématiques telles que l'addition et la soustraction sur des parties de numéros de version à des fins autres que la curiosité ou l'incrémentation automatique (et même alors, int()et str()peut être utilisé assez facilement). (D'un autre côté, il est possible que le code de quelqu'un d'autre s'attende à un tuple numérique plutôt qu'à un tuple de chaîne et échoue donc.)

Ceci est, bien sûr, mon propre point de vue, et j'aimerais volontiers que les autres me disent comment utiliser un tuple numérique.


Comme Shezi me l'a rappelé, les comparaisons (lexicales) de chaînes de nombres n'ont pas nécessairement le même résultat que les comparaisons numériques directes; les zéros en tête seraient tenus de le prévoir. Ainsi, au final, le stockage __version_info__(ou son nom) sous la forme d'un tuple de valeurs entières permettrait des comparaisons de versions plus efficaces.

COUP
la source
12
bien (+1), mais ne préférez-vous pas les nombres plutôt que les chaînes? par exemple__version_info__ = (1,2,3)
orip
3
La comparaison de chaînes peut devenir dangereuse lorsque les numéros de version dépassent 9, par exemple «10» <«2».
D Coetzee
13
Je le fais aussi mais légèrement modifié pour aborder les ints .. __version_info__ = (0, 1, 0) __version__ = '.'.join(map(str, __version_info__))
rh0dium
2
Le problème __version__ = '.'.join(__version_info__)est que __version_info__ = ('1', '2', 'beta')cela deviendra 1.2.beta, pas 1.2betaou1.2 beta
nagisa
4
Je pense que le problème avec cette approche est de savoir où mettre les lignes de code déclarant la __version__. S'ils se trouvent dans setup.py, votre programme ne peut pas les importer depuis son package. Ce n'est peut-être pas un problème pour vous, auquel cas, très bien. S'ils vont dans votre programme, votre setup.py peut les importer, mais cela ne devrait pas, car setup.py est exécuté pendant l'installation lorsque les dépendances de votre programme ne sont pas encore installées (setup.py est utilisé pour déterminer quelles sont les dépendances) .) D'où la réponse de Zooko: analyser manuellement la valeur d'un fichier source de produit, sans importer le package de produit
Jonathan Hartley
11

Beaucoup de ces solutions ignorent les gitbalises de 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 de version python à partir d'une balise dans le git référentiel
  • Automatisez git tag/ pushet les setup.py uploadétapes avec une seule commande qui ne prend aucune entrée.

Comment ça fonctionne:

  1. À partir d'une make releasecommande, la dernière version balisée du dépôt git est trouvée et incrémentée. La balise est repoussée à origin.

  2. Le Makefilestocke la version dans src/_version.pylaquelle elle sera lue setup.pyet également incluse dans la version. Ne vérifiez pas le contrôle de _version.pysource!

  3. setup.pycommande lit la nouvelle chaîne de version 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 releasecible incrémente toujours le 3e chiffre de la version, mais vous pouvez utiliser le next_minor_verou next_major_verpour incrémenter les autres chiffres. Les commandes reposent surversionbump.py script qui est archivé à la racine du dépôt

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

Cela fait le gros du travail comment traiter et incrémenter le numéro de version de git .

__init__.py

Le my_module/_version.pyfichier est importé dans my_module/__init__.py. Mettez ici toute 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 du my_modulemodule.

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 devrez avoir au moins une balise de version dans votre référentiel pour commencer.

git tag -a v0.0.1
cmcginty
la source
1
en effet - tout ce fil oublie qu'un VCS est très important dans cette discussion. juste un obs: l'incrémentation de version devrait rester un processus manuel, donc la façon préférée serait 1. de créer et de pousser une balise manuellement 2. de laisser les outils VCS découvrir cette balise et la stocker là où c'est nécessaire (wow - cette interface d'édition SO me paralyse vraiment - J'ai dû le modifier une douzaine de fois juste pour gérer les nouvelles lignes ET CELA NE FONCTIONNE PAS ENCORE! @ # $% ^ & *)
axd
Pas besoin d'utiliser versionbump.pyquand nous avons un package bumpversion génial pour python.
Oran
@Oran J'ai regardé versionbump. Les documents ne sont pas très clairs et ne gèrent pas très bien le balisage. Dans mes tests, il semble entrer dans des états qui provoquent son plantage: subprocess.CalledProcessError: Command '[' git ',' commit ',' -F ',' / var / folder / rl / tjyk4hns7kndnx035p26wg692g_7t8 / T / tmppishngbo '] 'a retourné le statut de sortie non nul 1.
cmcginty
1
Pourquoi ne pas _version.pyêtre suivi avec le contrôle de version?
Stevoisiak
1
@StevenVascellaro Cela complique le processus de publication. Vous devez maintenant vous assurer que vos balises et vos validations correspondent à la valeur de _version.py. L'utilisation d'une seule balise de version est plus propre IMO et signifie que vous pouvez créer une version directement à partir de l'interface utilisateur de github.
cmcginty
10

J'utilise un fichier JSON dans le répertoire du package. 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']

J'ai également mis d'autres informations pkg_info.json, comme l'auteur. J'aime utiliser JSON car je peux automatiser la gestion des métadonnées.

Andy Lee
la source
Pourriez-vous expliquer comment utiliser le json pour automatiser la gestion des métadonnées? Merci!
ryanjdillon
6

Il convient également de noter que, en plus d' __version__être semi-standard. en python est donc __version_info__ce 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 __version__chaîne à partir d'un fichier, ou autre chose.

James Antill
la source
1
Avez-vous des références / liens concernant l'origine de cette utilisation de __version_info__?
Craig McQueen
3
Eh bien, c'est le même mappage que sys.version vers sys.version_info. Donc: docs.python.org/library/sys.html#sys.version_info
James Antill
7
Il est plus facile de faire le mappage dans l'autre sens ( __version_info__ = (1, 2, 3)et __version__ = '.'.join(map(str, __version_info__))).
Eric O Lebigot
2
@EOL - __version__ = '.'.join(str(i) for i in __version_info__)- légèrement plus long mais plus pythonique.
ArtOfWarfare
2
Je ne suis pas sûr que ce que vous proposez soit clairement plus pythonique, car il contient une variable fictive qui n'est pas vraiment nécessaire et dont le sens est un peu difficile à exprimer ( in'a pas de sens, version_numest un peu long et ambigu…). Je prends même l'existence de map()Python comme un indice fort qu'il devrait être utilisé ici, car ce que nous devons faire ici est le cas d'utilisation typique de map()(utilisation avec une fonction existante) - je ne vois pas beaucoup d'autres utilisations raisonnables.
Eric O Lebigot
5

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, c'est-à-dire eitner

  1. Incorporez la version setup.pyet setup.pygénérez un module (par exemple version.py) contenant uniquement les informations de version, qui sont importées par votre package, ou

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

dF.
la source
J'aime votre recommandation, mais comment générer ce module à partir de setup.py?
sorin
1
J'aime l'idée de l'option (1), elle semble plus simple que la réponse de Zooko consistant à analyser le numéro de version à partir d'un fichier. Mais vous ne pouvez pas vous assurer que version.py est créé lorsqu'un développeur clone simplement votre dépôt. Sauf si vous archivez version.py, ce qui ouvre la petite ride que vous pouvez modifier le numéro de version dans setup.py, valider, libérer, puis devoir (slash oublier de) valider la modification dans version.py.
Jonathan Hartley
Vraisemblablement, vous pourriez générer le module en utilisant quelque chose comme "" "avec open (" mypackage / version.py "," w ") comme fp: fp.write (" __ version__ == '% s' \ n "% (__version__,) ) "" "
Jonathan Hartley
1
Je pense que l'option 2. est susceptible d'échouer pendant l'installation, comme indiqué dans les commentaires de la réponse de JAB.
Jonathan Hartley
Que dire de cela? Vous mettez __version__ = '0.0.1' "(où la version est une chaîne, bien sûr) dans le __init__.py" du package principal de votre logiciel. Ensuite, allez au point 2: dans la configuration que vous faites à partir du package .__ init__ import __version__ en tant que v, puis définissez la variable v comme version de votre setup.py. Ensuite, importez mypack en tant que my, imprimez ma .__ version__ imprimera la version. La version sera stockée en un seul endroit, accessible à partir de tout le code, dans un fichier qui n'importera rien d'autre lié au logiciel.
SeF
5

la flèche le gère d'une 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__'),
    # [...]
)
Anto
la source
c'est quoi file_text?
ely
2
la solution mise à jour est réellement nuisible. Lorsque setup.py est en cours d'exécution, il ne verra pas nécessairement la version du package à partir du chemin du fichier local. Il peut revenir à une version précédemment installée, par exemple en s'exécutant pip install -e .sur une branche de développement ou quelque chose lors des tests. setup.py ne doit absolument pas s'appuyer sur l'importation du package qu'il est en train d'installer pour déterminer les paramètres du déploiement. Oui.
ely
Oui, je ne sais pas pourquoi arrow a décidé de revenir à cette solution. De plus, le message de validation dit "Setup.py mis à jour avec les standards Python modernes " ... 🤷
Anto
4

J'ai aussi vu un autre style:

>>> django.VERSION
(1, 1, 0, 'final', 0)
L1ker
la source
1
Oui, j'ai vu aussi. BTW chaque réponse prend un autre style alors maintenant je ne sais pas quel style est un "standard". Vous recherchez les
PPE
Une autre façon de voir; Le client Python de Mongo utilise une version simple, sans les traits de soulignement. Donc ça marche; $ python >>> import pymongo >>> pymongo.version '2.7'
AnneTheAgile
L'implémentation .VERSIONne signifie pas que vous n'avez pas à l'implémenter __version__.
Acumenus
Cela nécessite-t-il une mise djangoen œuvre dans le projet?
Stevoisiak
3

En utilisant setuptools etpbr

Il n'y a pas de façon standard de gérer la version, mais la façon standard de gérer vos packages est setuptools .

La meilleure solution que j'ai trouvée dans l'ensemble pour gérer la version est d'utiliser setuptools avec l' pbrextension. C'est maintenant ma façon standard de gérer la version.

La configuration de votre projet pour un emballage complet peut être exagérée pour des projets simples, mais si vous avez besoin de gérer la version, vous êtes probablement au bon niveau pour tout configurer. Cela rend également votre package libérable sur PyPi afin que tout le monde puisse le télécharger et l'utiliser avec Pip.

PBR déplace la plupart des métadonnées hors des setup.pyoutils et dans un setup.cfgfichier qui est ensuite utilisé comme source pour la plupart des métadonnées, qui peuvent inclure la version. Cela permet aux métadonnées d'être empaquetées dans un exécutable en utilisant quelque chose comme pyinstallersi nécessaire (si c'est le cas, vous aurez probablement besoin de ces informations ), et sépare les métadonnées des autres scripts de gestion / configuration de package. Vous pouvez directement mettre à jour la chaîne de version danssetup.cfg manuellement, et elle sera tirée dans le*.egg-info dossier lors de la création des versions de votre package. Vos scripts peuvent ensuite accéder à la version à partir des métadonnées en utilisant différentes méthodes (ces processus sont décrits dans les sections ci-dessous).

Lorsque vous utilisez Git pour VCS / SCM, cette configuration est encore meilleure, car elle extrait de nombreuses métadonnées de Git afin que votre référentiel puisse être votre principale source de vérité pour certaines des métadonnées, y compris la version, les auteurs, les journaux des modifications, etc. Pour la version en particulier, il créera une chaîne de version pour la validation actuelle basée sur les balises git dans le référentiel.

Comme PBR extraira la version, l'auteur, le journal des modifications et d'autres informations directement à partir de votre dépôt git, certaines des métadonnées setup.cfgpeuvent être omises et générées automatiquement chaque fois qu'une distribution est créée pour votre package (en utilisant setup.py)

Version actuelle en temps réel

setuptoolstirera les dernières informations en temps réel en utilisant setup.py:

python setup.py --version

Cela extraira la dernière version du setup.cfgfichier ou du dépôt git, en fonction de la dernière validation effectuée et des balises qui existent dans le dépôt. Cette commande ne met cependant pas à jour la version dans une distribution.

Mise à jour de la version

Lorsque vous créez une distribution avec setup.py(c'est-à py setup.py sdist- dire , par exemple), toutes les informations actuelles seront extraites et stockées dans la distribution. Cela exécute essentiellement la setup.py --versioncommande, puis stocke ces informations de version dans le package.egg-infodossier dans un ensemble de fichiers qui stockent les métadonnées de distribution.

Remarque sur le processus de mise à jour des métadonnées de version:

Si vous n'utilisez pas pbr pour extraire les données de version de git, mettez simplement à jour votre setup.cfg directement avec les nouvelles informations de version (assez facile, mais assurez-vous qu'il s'agit d'une partie standard de votre processus de publication).

Si vous utilisez git et que vous n'avez pas besoin de créer une distribution source ou binaire (en utilisant l' python setup.py sdistune des python setup.py bdist_xxxcommandes), la façon la plus simple de mettre à jour les informations de dépôt git dans votre <mypackage>.egg-infodossier de métadonnées consiste simplement à exécuter la python setup.py installcommande. Cela exécutera toutes les fonctions PBR liées à l'extraction des métadonnées du référentiel git et mettra à jour votre .egg-infodossier local , installera les exécutables de script pour tous les points d'entrée que vous avez définis et d'autres fonctions que vous pouvez voir à partir de la sortie lorsque vous exécutez cette commande.

Notez que le .egg-infodossier est généralement exclu du stockage dans le référentiel git lui-même dans des .gitignorefichiers Python standard (tels que Gitignore.IO ), car il peut être généré à partir de votre source. S'il est exclu, assurez-vous que vous disposez d'un «processus de publication» standard pour obtenir les métadonnées mises à jour localement avant la publication, et tout package que vous téléchargez sur PyPi.org ou distribuez d'une autre manière doit inclure ces données pour avoir la version correcte. Si vous souhaitez que le référentiel Git contienne ces informations, vous pouvez exclure certains fichiers spécifiques d'être ignorés (c'est- !*.egg-info/PKG_INFOà- dire les ajouter à .gitignore)

Accéder à la version à partir d'un script

Vous pouvez accéder aux métadonnées à partir de la version actuelle dans les scripts Python du package lui-même. Pour la version, par exemple, il y a plusieurs façons de le faire jusqu'à présent:

## This one is a new built-in as of Python 3.8.0 should become the standard
from importlib-metadata import version

v0 = version("mypackage")
print('v0 {}'.format(v0))

## I don't like this one because the version method is hidden
import pkg_resources  # part of setuptools

v1 = pkg_resources.require("mypackage")[0].version
print('v1 {}'.format(v1))

# Probably best for pre v3.8.0 - the output without .version is just a longer string with
# both the package name, a space, and the version string
import pkg_resources  # part of setuptools

v2 = pkg_resources.get_distribution('mypackage').version
print('v2 {}'.format(v2))

## This one seems to be slower, and with pyinstaller makes the exe a lot bigger
from pbr.version import VersionInfo

v3 = VersionInfo('mypackage').release_string()
print('v3 {}'.format(v3))

Vous pouvez en mettre un directement dans votre __init__.pypackage pour extraire les informations de version comme suit, de la même manière que pour certaines autres réponses:

__all__ = (
    '__version__',
    'my_package_name'
)

import pkg_resources  # part of setuptools

__version__ = pkg_resources.get_distribution("mypackage").version
LightCC
la source
Reformaté par vote négatif pour répondre directement à la question maintenant.
LightCC
1

Après plusieurs heures à essayer de trouver la solution fiable la plus simple, voici les pièces:

créez un fichier version.py DANS le dossier de votre package "/ mypackage":

# 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__ = '1.2.7'

dans setup.py:

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

dans le dossier principal init .py:

from .version import __version__

La exec()fonction exécute le script en dehors de toute importation, car setup.py est exécuté avant que le module puisse être importé. Il vous suffit de gérer le numéro de version dans un fichier en un seul endroit, mais malheureusement ce n'est pas dans setup.py. (c'est l'inconvénient, mais l'absence de bogues d'importation est l'avantage)

Marc Maxmeister
la source
1

Beaucoup de travail vers un versionnage uniforme et à l'appui des conventions a été accompli depuis la première question posée . Les options de goût sont maintenant détaillées dans le Guide de l'utilisateur de l'emballage Python . Il convient également de noter que les schémas de numéros de version sont relativement stricts en Python selon PEP 440 , et donc garder les choses saines est essentiel si votre package sera publié dans la boutique de fromage .

Voici une ventilation raccourcie des options de version:

  1. Lisez le fichier dans setup.py( setuptools ) et obtenez la version.
  2. Utilisez un outil de construction externe (pour mettre à jour les deux __init__.pyainsi que le contrôle des sources), par exemple bump2version , changes ou zest.releaser .
  3. Définissez la valeur sur __version__ variable globale dans un module spécifique.
  4. Placez la valeur dans un fichier texte VERSION simple pour setup.py et le code à lire.
  5. Définissez la valeur via une setup.pyversion et utilisez importlib.metadata pour la récupérer lors de l'exécution. (Attention, il existe des versions pré-3.8 et post-3.8.)
  6. Définissez la valeur sur __version__in sample/__init__.pyet importez l'échantillon dans setup.py.
  7. Utilisez setuptools_scm pour extraire le contrôle de version du contrôle de source afin qu'il soit la référence canonique, pas le code.

NOTEZ que (7) pourrait être l' approche la plus moderne (les métadonnées de construction sont indépendantes du code, publiées par l'automatisation). En outre NOTE que si l' installation est utilisée pour la libération du package simple python3 setup.py --versionrapportera directement la version.

ici
la source
-1

Pour ce que ça vaut, si vous utilisez des distutils NumPy, numpy.distutils.misc_util.Configurationa une make_svn_version_py()méthode qui incorpore le numéro de révision à l'intérieur package.__svn_version__de la variable version.

Mat
la source
Pouvez-vous fournir plus de détails ou un exemple de la façon dont cela fonctionnerait?
Stevoisiak
Hmm. En 2020, c'est (était-ce toujours?) Destiné à FORTRAN . Le paquet "numpy.distutils fait partie de NumPy étendant les distutils Python standard pour traiter les sources Fortran."
ingyhere
-1
  1. Utilisez un version.pyfichier uniquement avec __version__ = <VERSION>param dans le fichier. Dans le setup.pyfichier, importez le __version__paramètre et mettez sa valeur dans le setup.pyfichier comme ceci: version=__version__
  2. Une autre façon consiste à utiliser uniquement un setup.pyfichier avec version=<CURRENT_VERSION>- la CURRENT_VERSION est codée en dur.

Puisque nous ne voulons pas changer manuellement la version dans le fichier chaque fois que nous créons une nouvelle balise (prête à publier une nouvelle version de package), nous pouvons utiliser ce qui suit.

Je recommande fortement le package bumpversion . Je l'utilise depuis des années pour bump une version.

commencez par ajouter version=<VERSION>à votre setup.pyfichier si vous ne l'avez pas déjà.

Vous devriez utiliser un court script comme celui-ci chaque fois que vous augmentez une version:

bumpversion (patch|minor|major) - choose only one option
git push
git push --tags

Ajoutez ensuite un fichier par dépôt appelé .bumpversion.cfg::

[bumpversion]
current_version = <CURRENT_TAG>
commit = True
tag = True
tag_name = {new_version}
[bumpversion:file:<RELATIVE_PATH_TO_SETUP_FILE>]

Remarque:

  • Vous pouvez utiliser __version__ paramètre sous le version.pyfichier comme cela a été suggéré dans d'autres articles et mettre à jour le fichier bumpversion comme ceci: [bumpversion:file:<RELATIVE_PATH_TO_VERSION_FILE>]
  • Vous devez git commit ougit reset tout dans votre repo, sinon vous obtiendrez une erreur de repo sale.
  • Assurez-vous que votre environnement virtuel inclut le package de bumpversion, sans quoi il ne fonctionnera pas.
Oran
la source
@cmcginty Désolé pour le retard, veuillez vérifier ma réponse ^^^ - notez que vous devez git commit ou git réinitialiser tout dans votre référentiel et assurez-vous que votre environnement virtuel inclut le package de bumpversion, sans cela cela ne fonctionnera pas. Utilisez la dernière version.
Oran
Je ne sais pas trop quelle solution est suggérée ici. Recommandez-vous le suivi manuel de la version avec version.pyou le suivi avec bumpversion?
Stevoisiak
@StevenVascellaro Je suggère d'utiliser la version bump, n'utilisez jamais de version manuelle. Ce que j'ai essayé d'expliquer, c'est que vous pouvez demander à bumpversion de mettre à jour la version dans le fichier setup.py ou mieux encore de l'utiliser pour mettre à jour le fichier version.py. Il est plus courant de mettre à jour le fichier version.py et de prendre la __version__valeur param dans le fichier setup.py. Ma solution est utilisée en production et c'est une pratique courante. Remarque: juste pour être clair, utiliser bumpversion dans le cadre d'un script est la meilleure solution, mettez-le dans votre CI et ce sera un fonctionnement automatique.
Oran
-3

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 sûr, le numéro de révision vous sera remplacé par CVS.)

Cela vous donne une version imprimable et des informations de 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)
Martin Ibert
la source
Dans quel fichier recommandez-vous d'enregistrer l'enregistrement __version__? Comment incrémenter le numéro de version avec cette solution?
Stevoisiak