¿Cómo puedo obtener la versión definida en setup.py (setuptools) en mi paquete?

Respuestas:

246

Cadena de versión de interrogación de distribución ya instalada

Para recuperar la versión desde el interior de su paquete en tiempo de ejecución (lo que parece estar preguntando realmente), puede usar:

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

Almacene la cadena de versión para usar durante la instalación

Si desea ir al revés (que parece ser lo que otros autores de respuestas aquí parecen haber pensado que estaba preguntando), coloque la cadena de versión en un archivo separado y lea el contenido de ese archivo setup.py.

Puede hacer una versión.py en su paquete con una __version__línea, luego leerla desde setup.py usando execfile('mypackage/version.py'), para que se establezca __version__en el espacio de nombres setup.py.

Si desea una forma mucho más simple que funcione con todas las versiones de Python e incluso con lenguajes que no sean de Python que puedan necesitar acceso a la cadena de versión:

Almacene la cadena de versión como el único contenido de un archivo de texto plano, llamado eg VERSION, y lea ese archivo durante setup.py.

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

El mismo VERSIONarchivo funcionará exactamente igual en cualquier otro programa, incluso los que no sean Python, y solo necesita cambiar la cadena de versión en un lugar para todos los programas.

Advertencia sobre la condición de carrera durante la instalación

Por cierto, NO importe su paquete desde su setup.py como se sugiere en otra respuesta aquí: parecerá funcionar para usted (porque ya tiene instaladas las dependencias de su paquete), pero causará estragos en los nuevos usuarios de su paquete , ya que no podrán instalar su paquete sin instalar primero manualmente las dependencias.

PJ Eby
fuente
1
Esta es simplemente la mejor respuesta aquí: la mejor manera es usar execfile si necesita setup.py para obtener la versión de su paquete.
noveno
2
execfilefunciona muy bien ... pero (lamentablemente) no funciona con Python 3.
medmunds
9
... OK, para Python 2 y 3 usar with open('mypackage/version.py') as f: exec(f.read())en lugar de execfile('mypackage/version.py'). (De stackoverflow.com/a/437857/647002 )
medmunds
55
La parte de wreak havoc no es necesariamente verdadera ... solo causará estragos si su archivo init .py en el paquete tiene dependencias de importación de código (directa o indirectamente).
Pykler
2
Vale la pena mencionar que django realiza una llamada __import__ en su setup.py . ¿'__Import__' vs 'import' hace que esto sea seguro? No parece estar causándoles ningún problema.
user1978019
33

estudio de ejemplo: mymodule

Imagina esta configuración:

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

Luego imagine un escenario habitual en el que tenga dependencias y se setup.pyvea así:

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

Y un ejemplo __init__.py:

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

Y por ejemplo myclasses.py:

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

problema # 1: importar mymoduledurante la configuración

Si sus setup.pyimportaciones, mymoduleentonces durante la configuración , probablemente obtendrá un ImportError. Este es un error muy común cuando su paquete tiene dependencias. Si su paquete no tiene otras dependencias que las incorporadas, puede estar seguro; Sin embargo, esta no es una buena práctica. La razón de esto es que no es a prueba de futuro; diga mañana que su código necesita consumir alguna otra dependencia.

problema # 2: ¿dónde está mi __version__?

Si hardcode __version__de setup.pyentonces puede no coincidir con la versión que le enviará en su módulo. Para ser coherente, lo pondría en un lugar y lo leería desde el mismo lugar cuando lo necesite. Al importusarlo puede obtener el problema # 1.

solución: à la setuptools

Se podría utilizar una combinación de open, execy proporcionar un diccionario para execlas variables add:

# 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__'],
    ...)

Y en mymodule/version.pyexponer la versión:

__version__ = 'some.semantic.version'

De esta manera, la versión se envía con el módulo, y no tiene problemas durante la instalación al intentar importar un módulo que tiene dependencias faltantes (aún no se ha instalado).

dnozay
fuente
2
Esta parece ser la solución más razonable porque se basa solo en dependencias permitidas.
Dogweather
¿El concepto de versión del paquete definido en el archivo setup.py es diferente de la versión ?
variable
16

La mejor técnica es definir __version__en el código de su producto, luego importarlo en setup.py desde allí. Esto le da un valor que puede leer en su módulo en ejecución, y solo tiene un lugar para definirlo.

Los valores en setup.py no están instalados y setup.py no se queda después de la instalación.

Lo que hice (por ejemplo) en coverage.py:

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

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

ACTUALIZACIÓN (2017): coverage.py ya no se importa para obtener la versión. Importar su propio código puede hacer que sea desinstalable, porque su código de producto intentará importar dependencias, que aún no están instaladas, porque setup.py es lo que las instala.

Ned Batchelder
fuente
44
@Evan: No estoy seguro de a qué te refieres con "solo sacar ese valor de la fuente". Si el archivo __version__que contiene está roto de alguna manera, su importación también se romperá. Python no sabe cómo interpretar solo las declaraciones que desea. @pjeby tiene razón: si su módulo necesita importar otros módulos, es posible que aún no estén instalados y será caótico. Esta técnica funciona si tiene cuidado de que la importación no genere una larga cadena de otras importaciones.
Ned Batchelder
1
@Ned Batchelder Estoy bastante seguro de que, si coloca la versión antes de cualquier importación en el archivo fuente y solo a una 'versión de importación del módulo', no lexará más del archivo fuente de lo que necesita. Además, ¿quién lanzará el código roto? Si el paquete necesita dependencias, use setuptools o espere el lanzamiento de distutils2 más adelante en el año.
Evan Plaice
15
@Evan, lo siento, pero te equivocas al importar archivos parciales. Intente poner una declaración de impresión al final de un módulo largo e importe la primera variable definida en el archivo. La declaración de impresión se ejecutará.
Ned Batchelder
23
Me enamoré de esto, pero @pjeby tiene toda la razón: "Por cierto, NO importe su paquete desde su setup.py como se sugiere en otra respuesta aquí: parecerá funcionar para usted (porque ya tiene instaladas las dependencias de su paquete ), pero causará estragos en los nuevos usuarios de su paquete, ya que no podrán instalar su paquete sin instalar primero las dependencias manualmente ". Ned, ¿te importaría agregar una advertencia a tu respuesta?
Dr. Jan-Philip Gehrcke
1
Al igual que @ Jan-PhilipGehrcke al cargar un archivo setup.py como este para PyPI y luego pip install <packagename>, me sale el siguiente error: ImportError: No module named <packagename>. ¡AVISE a sus lectores que no puede ejecutar archivos setup.py como este en entornos donde el paquete no está instalado!
encimeras
14

Su pregunta es un poco vaga, pero creo que lo que está preguntando es cómo especificarla.

Necesita definir __version__así:

__version__ = '1.4.4'

Y luego puede confirmar que setup.py conoce la versión que acaba de especificar:

% ./setup.py --version
1.4.4
jathanism
fuente
9

No estaba contento con estas respuestas ... no quería requerir herramientas de configuración, ni crear un módulo completamente separado para una sola variable, así que se me ocurrió esto.

Para cuando esté seguro de que el módulo principal está en estilo pep8 y seguirá así:

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 desea ser más cuidadoso y usar un analizador real:

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 es algo así como un módulo desechable, así que no es un problema si es un poco feo.


Actualización: Es muy gracioso porque me he alejado de esto en los últimos años y comenzó a usar un archivo separado en el paquete de llamada meta.py. Puse muchos metadatos allí que quizás quiera cambiar con frecuencia. Entonces, no solo por un valor.

Gringo Suave
fuente
+1. Es relativamente simple, mantiene el número de versión en un lugar, no requiere un archivo separado para contenerlo y no impone las dependencias de importación del módulo de Python en setup.py. Ni siquiera me molestaría con el administrador de contexto en un programa tan efímero como setup.py.
ɈsәɹoɈ
Mucho mejor que usar expresiones regulares, gracias. También puede usar ast.get_docstring()con algunos .split('\n')[0].strip()etc. para completar automáticamente descriptiondesde la fuente. Una cosa menos para mantener sincronizado
perdido el
4

Cree un archivo en su árbol de origen, por ejemplo, en su base de datos / su paquete / _version.py. Deje que el archivo contenga una sola línea de código, como esta:

__version__ = "1.1.0-r4704"

Luego, en su setup.py, abra ese archivo y analice el número de versión de esta manera:

verstr = "desconocido"
tratar:
    verstrline = open ('yourpackage / _version.py', "rt"). read ()
excepto EnvironmentError:
    Pase # Bien, no hay un archivo de versión.
más:
    VSRE = r "^ __ versión__ = ['\"] ([^' \ "] *) ['\"] "
    mo = re.search (VSRE, verstrline, re.M)
    si mo:
        verstr = mo.group (1)
    más:
        raise RuntimeError ("no se puede encontrar la versión en su paquete / _version.py")

Finalmente, en yourbasedir/yourpackage/__init__.pyimport _version así:

__version__ = "desconocido"
tratar:
    de _version import __version__
excepto ImportError:
    # Nos estamos ejecutando en un árbol que no tiene un _version.py, por lo que no sabemos cuál es nuestra versión.
    pasar

Un ejemplo de código que hace esto es el paquete "pyutil" que mantengo. (Consulte PyPI o búsqueda de google: stackoverflow no me permite incluir un hipervínculo en esta respuesta).

@pjeby tiene razón en que no debe importar su paquete desde su propio setup.py. Eso funcionará cuando lo pruebe creando un nuevo intérprete de Python y ejecutando setup.py en primer lugar: python setup.pypero hay casos en que no funcionará. Esto se debe a import youpackageque no significa leer el directorio de trabajo actual para un directorio llamado "yourpackage", significa buscar en la actual sys.modulesuna clave "yourpackage" y luego hacer varias cosas si no está allí. Por lo tanto, siempre funciona cuando lo hace python setup.pyporque tiene un nuevo, vacío sys.modules, pero esto no funciona en general.

Por ejemplo, ¿qué sucede si py2exe está ejecutando su setup.py como parte del proceso de empaquetar una aplicación? He visto un caso como este donde py2exe pondría el número de versión incorrecto en un paquete porque el paquete estaba obteniendo su número de versión deimport myownthingen su setup.py, pero anteriormente se había importado una versión diferente de ese paquete durante la ejecución de py2exe. Del mismo modo, ¿qué sucede si setuptools, easy_install, distribution o distutils2 intentan construir su paquete como parte de un proceso de instalación de un paquete diferente que depende del suyo? Luego, si su paquete es importante en el momento en que se está evaluando su setup.py, o si ya hay una versión de su paquete que se importó durante la vida de este intérprete de Python, o si la importación de su paquete requiere la instalación de otros paquetes primero , o tiene efectos secundarios, puede cambiar los resultados. He tenido varias dificultades para tratar de reutilizar los paquetes de Python que causaron problemas para herramientas como py2exe y setuptools porque su setup.py importa el paquete para encontrar su número de versión.

Por cierto, esta técnica funciona muy bien con herramientas para crear automáticamente el yourpackage/_version.pyarchivo para usted, por ejemplo, leyendo su historial de control de revisiones y escribiendo un número de versión basado en la etiqueta más reciente en el historial de control de revisiones. Aquí hay una herramienta que hace eso para darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst y aquí hay un fragmento de código que hace lo mismo para git: http: // github .com / warner / python-ecdsa / blob / 0ed702a9d4057ecf33eea969b8cf280eaccd89a1 / setup.py # L34

Zooko
fuente
3

Esto también debería funcionar, utilizando expresiones regulares y dependiendo de los campos de metadatos para tener un formato como este:

__fieldname__ = 'value'

Use lo siguiente al comienzo de su setup.py:

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

Después de eso, puede usar los metadatos en su script de esta manera:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']
thp
fuente
Como dije anteriormente, solo use str.split :)
Éric Araujo
Oh, Dios, no. ¿Estás analizando Python en Python? Como mínimo, use eval (), child.
Slacy
3
Un consejo deficiente, la evaluación debe evitarse cuando es tan fácil.
Gringo Suave
3

Con una estructura como esta:

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

donde version.py contiene:

__version__ = 'version_string'

Puedes hacer esto en setup.py :

import sys

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

from version import __version__

Esto no causará ningún problema con las dependencias que tenga en su mymodule / __ init__.py

Jahid
fuente
2

Para evitar importar un archivo (y así ejecutar su código), uno podría analizarlo y recuperar el versionatributo del árbol de sintaxis:

# 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

Este código intenta encontrar la asignación __version__o VERSIONen el nivel superior del retorno del módulo es el valor de cadena. El lado derecho puede ser una cadena o una tupla.

Mikhail Edoshin
fuente
3
Parece inteligente y complicado :) Sería más simple tener un archivo _version.py que contenga una tarea y analizarlo con open-read-str.split.
Éric Araujo
Gracias por publicar esto. Creo que es demasiado complejo para esta situación, pero muy útil para comprender cómo se puede abordar el problema. Me gusta que mantiene el número de versión en un lugar, no requiere un archivo separado para contenerlo y no impone las dependencias de importación del módulo de Python en el script de configuración.
ɈsәɹoɈ
2

Hay mil maneras de pelar un gato, aquí está el mío:

# 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.")
Marc Abramowitz
fuente
2

Limpieza https://stackoverflow.com/a/12413800 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
A
fuente
2

Ahora, esto es asqueroso y necesita un poco de refinamiento (incluso puede haber una llamada de miembro descubierta en pkg_resources que me perdí), pero simplemente no veo por qué esto no funciona, ni por qué nadie lo ha sugerido hasta la fecha (Googlear ha no subió esto) ... tenga en cuenta que esto es Python 2.x, y requeriría requerir pkg_resources (suspiro):

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
Mark Gerolimatos
fuente
2

Quisimos poner la meta información sobre nuestro paquete pypackageryen __init__.py, pero no pudo ya que cuenta con dependencias de terceros como PJ Eby ya se ha señalado (véase su respuesta y la advertencia con respecto a la condición de carrera).

Lo resolvimos creando un módulo separado pypackagery_meta.pyque contiene solo la metainformación:

"""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'

luego importó la metainformación en packagery/__init__.py:

# ...

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

# ...

y finalmente lo usé en 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'],
 )

Debe incluir pypackagery_metaen su paquete con py_modulesel argumento de configuración. De lo contrario, no puede importarlo durante la instalación, ya que la distribución empaquetada carecería de él.

marko.ristin
fuente
1

Simple y directo, cree un archivo llamado source/package_name/version.pycon los siguientes contenidos:

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

Luego, en su archivo source/package_name/__init__.py, importa la versión para que otras personas la usen:

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

Ahora puedes poner esto 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()

Probado esto con Python 2.7, 3.3, 3.4, 3.5, 3.6y 3.7en Linux, Windows y Mac OS. Utilicé en mi paquete que tiene pruebas de integración y unidad para todas estas plataformas. Puedes ver los resultados desde .travis.ymly appveyor.ymlaquí:

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

Una versión alternativa está usando el administrador de contexto:

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

También puede usar el codecsmódulo para manejar errores unicode tanto en Python 2.7como en3.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 está escribiendo un módulo Python al 100% en C / C ++ usando Extensiones Python C, puede hacer lo mismo, pero usando C / C ++ en lugar de Python.

En este caso, cree lo siguiente 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'],
            )
        ],
    )

Que lee la versión del archivo version.h:

const char* __version__ = "1.0.12";

Pero, no olvide crear el MANIFEST.inpara incluir el version.harchivo:

include README.md
include LICENSE.txt

recursive-include source *.h

Y está integrado en la aplicación principal con:

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

    ...
}

Referencias

  1. error de archivo abierto de Python
  2. Definir un global en un módulo Python desde una API C
  3. ¿Cómo incluir los datos del paquete con setuptools / distribution?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. ¿Cómo usar los paquetes setuptools y ext_modules con el mismo nombre?
  6. ¿Es posible incluir subdirectorios utilizando dist utils (setup.py) como parte de los datos del paquete?
usuario
fuente
1

Implemente el paquete en el servidor y la convención de nomenclatura de archivos para paquetes de índices:

ejemplo para la conversión de versión dinámica de pip:

  • ganar:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-win-amd64.egg
  • Mac:

    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.egg
    • test_pkg-1.0.0-py3.7-macosx-10.12-x86_64.whl
  • linux:
    • test_pkg-1.0.0-cp36-cp36m-linux_x86_64.whl
from setuptools_scm import get_version

def _get_version():

     dev_version = str(".".join(map(str, str(get_version()).split("+")[0]\
            .split('.')[:-1])))

    return dev_version

Encuentre el ejemplo setup.py llama a la versión de pip dinámica que coincide con git commit

setup(
    version=_get_version(),
    name=NAME,
    description=DESCRIPTION,
    long_description=LONG_DESCRIPTION,
    classifiers=CLASSIFIERS,

# add few more for wheel wheel package ...conversion

)
Narayana Reddy
fuente
0

Estoy usando una variable de entorno como la siguiente

VERSIÓN = 0.0.0 python setup.py sdist bdist_wheel

En setup.py


import os

setup(
    version=os.environ['VERSION'],
    ...
)

Para verificar la coherencia con la versión del empaquetador, estoy usando el siguiente script.

PKG_VERSION=`python -c "import pkg; print(pkg.__version__)"`
if [ $PKG_VERSION == $VERSION ]; then
    python setup.py sdist bdist_wheel
else
    echo "Package version differs from set env variable"
fi
seongjoo
fuente