Como posso obter a versão definida em setup.py (setuptools) no meu pacote?

Respostas:

246

Interrogar a sequência de versões da distribuição já instalada

Para recuperar a versão de dentro do seu pacote em tempo de execução (o que sua pergunta parece realmente estar perguntando), você pode usar:

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

Armazene a sequência de versões para uso durante a instalação

Se você quiser seguir o caminho inverso (que parece ser o que outros autores de respostas aqui parecem ter pensado que você estava perguntando), coloque a string da versão em um arquivo separado e leia o conteúdo desse arquivo setup.py.

Você pode criar um version.py no seu pacote com uma __version__linha e, em seguida, lê-lo em setup.py usando execfile('mypackage/version.py'), para que seja definido __version__no namespace setup.py.

Se você deseja uma maneira muito mais simples que funcione com todas as versões do Python e até mesmo com as linguagens que não sejam do Python, que podem precisar de acesso à string da versão:

Armazene a string da versão como o único conteúdo de um arquivo de texto sem formatação, nomeado por exemplo VERSION, e leia esse arquivo durante setup.py.

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

O mesmo VERSIONarquivo funcionará exatamente como em qualquer outro programa, mesmo que não seja em Python, e você só precisará alterar a string da versão em um local para todos os programas.

Aviso sobre a condição de corrida durante a instalação

A propósito, NÃO importe seu pacote do setup.py, como sugerido em outra resposta aqui: ele parecerá funcionar para você (porque você já possui as dependências do pacote instaladas), mas causará estragos nos novos usuários do seu pacote. , pois eles não poderão instalar seu pacote sem instalar manualmente as dependências primeiro.

PJ Eby
fonte
1
Esta é simplesmente a melhor resposta aqui: a melhor maneira é usar o execfile se você precisar do setup.py para obter a versão do seu pacote.
trinth 21/09/12
2
execfilefunciona muito bem ... mas (infelizmente) não funciona com o Python 3.
medmunds
9
... OK, para Python 2 e 3 usar with open('mypackage/version.py') as f: exec(f.read())no lugar de execfile('mypackage/version.py'). (From stackoverflow.com/a/437857/647002 )
medmunds
5
A parte de causar estragos não é necessariamente verdadeira ... apenas causará estragos se o arquivo init .py do pacote tiver dependências de importação de código (direta ou indiretamente).
precisa saber é o seguinte
2
Parece mencionar que o django realiza uma chamada __import__ no arquivo setup.py . '__Import__' vs 'import' torna isso seguro? Não parece estar causando problemas.
user1978019
33

exemplo de estudo: mymodule

Imagine esta configuração:

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

Imagine um cenário comum em que você tenha dependências e se setup.pypareça com:

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

E um exemplo __init__.py:

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

E por exemplo myclasses.py:

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

problema 1: importar mymoduledurante a instalação

Se suas setup.pyimportações mymodule, durante a instalação, você provavelmente obterá um ImportError. Este é um erro muito comum quando o seu pacote tem dependências. Se o seu pacote não tiver outras dependências além das internas, você pode estar seguro; no entanto, isso não é uma boa prática. A razão para isso é que não é à prova de futuro; digamos amanhã que seu código precisa consumir alguma outra dependência.

problema 2: onde está o meu __version__?

Se você codificar __version__em setup.pyseguida, ele pode não coincidir com a versão que seria lançado em seu módulo. Para ser consistente, você o colocaria em um só lugar e o leria do mesmo lugar quando necessário. Usando importvocê pode obter o problema nº 1.

solução: à la setuptools

Você usaria uma combinação de open, exece fornecer um dicionário para execas variáveis 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__'],
    ...)

E em mymodule/version.pyexpor a versão:

__version__ = 'some.semantic.version'

Dessa forma, a versão é fornecida com o módulo e você não apresenta problemas durante a instalação ao tentar importar um módulo que possui dependências ausentes (ainda a ser instalado).

dnozay
fonte
2
Essa parece ser a solução mais razoável, pois depende apenas de dependências permitidas.
Dogweather
O conceito de versão do pacote definido no arquivo setup.py é diferente da versão ?
variável
16

A melhor técnica é definir __version__no código do produto e importá-lo para o setup.py a partir daí. Isso fornece um valor que você pode ler no seu módulo em execução e tem apenas um lugar para defini-lo.

Os valores em setup.py não estão instalados e o setup.py não permanece após a instalação.

O que eu fiz (por exemplo) em composition.py:

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

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

UPDATE (2017): o cover.py não se importa mais para obter a versão. A importação do seu próprio código pode torná-lo desinstalável, porque o código do produto tentará importar dependências, que ainda não estão instaladas, porque o setup.py é o que as instala.

Ned Batchelder
fonte
4
@Evan: Não tenho certeza do que você está recebendo "apenas puxe esse valor da fonte". Se o arquivo contido __version__nele estiver quebrado de alguma forma, sua importação também será interrompida. Python não sabe como interpretar apenas as declarações que você deseja. @pjeby está certo: se o seu módulo precisar importar outros módulos, eles ainda não podem estar instalados e serão caóticos. Essa técnica funciona se você for cuidadoso para que a importação não cause uma longa cadeia de outras importações.
N / Batchelder
1
@ Ned Batchelder Tenho certeza de que, se você colocar a versão antes de qualquer importação no arquivo de origem e apenas para uma 'versão de importação do módulo', ele não lexará mais do arquivo de origem do que precisa. Além disso, quem vai liberar código quebrado? Se o pacote precisar de dependências, use setuptools ou aguarde o lançamento do distutils2 mais tarde no ano.
precisa
15
@Evan, desculpe, mas você está errado ao importar arquivos parciais. Tente colocar uma declaração de impressão no final de um módulo longo e importe a primeira variável definida no arquivo. A declaração de impressão será executada.
N / Batchelder
23
Eu me apaixonei por isso, mas @pjeby está totalmente certo: "A propósito, NÃO importe seu pacote do setup.py conforme sugerido em outra resposta aqui: ele parecerá funcionar para você (porque você já possui as dependências do pacote instaladas) ), mas causará estragos nos novos usuários do seu pacote, pois eles não poderão instalar o pacote sem antes instalar manualmente as dependências. " Ned, você se importaria de adicionar um aviso à sua resposta?
Dr. Jan-Philip Gehrcke
1
Como @ Jan-PhilipGehrcke quando eu carregar um arquivo setup.py como este para PyPI e, em seguida pip install <packagename>, eu recebo o seguinte erro: ImportError: No module named <packagename>. AVISO aos seus leitores que você não pode executar arquivos setup.py como este em ambientes onde o pacote ainda não está instalado!
hobs
14

Sua pergunta é um pouco vaga, mas acho que o que você está perguntando é como especificá-la.

Você precisa definir __version__assim:

__version__ = '1.4.4'

E então você pode confirmar que o setup.py conhece a versão que você acabou de especificar:

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

Eu não estava feliz com essas respostas ... não queria exigir ferramentas de configuração, nem criar um módulo inteiro para uma única variável, então eu vim com elas.

Para quando você tiver certeza de que o módulo principal está no estilo pep8 e permanecerá assim:

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

Se você deseja ser mais cuidadoso e usar um analisador 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

O setup.py é um módulo descartável, portanto não é um problema se for um pouco feio.


Atualização: engraçado o bastante, eu me afastei disso nos últimos anos e comecei a usar um arquivo separado no pacote chamado meta.py. Coloquei muitos metadados lá que talvez eu queira mudar com frequência. Portanto, não apenas por um valor.

Gringo Suave
fonte
+1. É relativamente simples, mantém o número da versão em um só lugar, não requer um arquivo separado para contê-lo e não impõe as dependências de importação do módulo python no setup.py. Eu nem me incomodaria com o gerenciador de contexto em um programa de curta duração como o setup.py.
ʇsәɹoɈ
Muito melhor do que usar regex, obrigado. Você também pode usar ast.get_docstring()alguns .split('\n')[0].strip()etc para preencher automaticamente a descriptionpartir da fonte. Menos uma coisa para manter em sincronia
perdida
4

Crie um arquivo na sua árvore de fontes, por exemplo, em yourbasedir / yourpackage / _version.py. Deixe esse arquivo conter apenas uma única linha de código, como esta:

__version__ = "1.1.0-r4704"

Em seguida, em seu setup.py, abra esse arquivo e analise o número da versão assim:

verstr = "desconhecido"
experimentar:
    verstrline = open ('yourpackage / _version.py', "rt"). read ()
exceto EnvironmentError:
    passe # Ok, não há arquivo de versão.
outro:
    VSRE = r "^ __ version__ = ['\"] ([^' \ "] *) ['\"] "
    mo = re.search (VSRE, verstrline, re.M)
    se mo:
        verstr = mo.group (1)
    outro:
        raise RuntimeError ("incapaz de encontrar a versão em yourpackage / _version.py")

Finalmente, na yourbasedir/yourpackage/__init__.pyimportação _version assim:

__version__ = "desconhecido"
experimentar:
    de _version import __version__
exceto ImportError:
    # Estamos rodando em uma árvore que não possui _version.py, por isso não sabemos qual é a nossa versão.
    passar

Um exemplo de código que faz isso é o pacote "pyutil" que eu mantenho. (Consulte PyPI ou pesquisa no google - o stackoverflow está me impedindo de incluir um hiperlink para ele nesta resposta.)

@pjeby está certo em não importar seu pacote de seu próprio setup.py. Isso funcionará quando você testá-lo, criando um novo interpretador Python e executando o setup.py nele: primeiro python setup.py, mas há casos em que ele não funciona. Isso porque import youpackagenão significa ler o diretório de trabalho atual para um diretório chamado "yourpackage", significa procurar no atual sys.modulespor uma chave "yourpackage" e, em seguida, fazer várias coisas se não estiver lá. Por isso, sempre funciona quando você faz, python setup.pyporque você tem um novo, vazio sys.modules, mas isso não funciona em geral.

Por exemplo, e se o py2exe estiver executando o setup.py como parte do processo de empacotamento de um aplicativo? Eu já vi um caso como este em que py2exe colocaria o número da versão incorreta em um pacote porque o pacote estava obtendo o número da versão emimport myownthingem seu setup.py, mas uma versão diferente desse pacote havia sido importada anteriormente durante a execução do py2exe. Da mesma forma, e se setuptools, easy_install, distribuir ou distutils2 estiver tentando criar seu pacote como parte de um processo de instalação de um pacote diferente que depende do seu? Então, se o seu pacote é importável no momento em que seu setup.py está sendo avaliado, ou se já existe uma versão do seu pacote que foi importada durante a vida deste intérprete Python, ou se a importação do seu pacote exige que outros pacotes sejam instalados primeiro , ou tem efeitos colaterais, pode alterar os resultados. Eu tive várias dificuldades ao tentar reutilizar pacotes Python, o que causou problemas para ferramentas como py2exe e setuptools, porque o setup.py importa o próprio pacote para encontrar o número da versão.

A propósito, essa técnica funciona bem com ferramentas para criar automaticamente o yourpackage/_version.pyarquivo para você, por exemplo, lendo seu histórico de controle de revisões e escrevendo um número de versão com base na tag mais recente do histórico de controle de revisões. Aqui está uma ferramenta que faz isso para darcs: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst e aqui está um trecho de código que faz a mesma coisa para o git: http: // github .com / warner / python-ecdsa / blob / 0ed702a9d4057ecf33eea969b8cf280eaccd89a1 / setup.py # L34

Zooko
fonte
3

Isso também deve funcionar, usando expressões regulares e dependendo dos campos de metadados para ter um formato como este:

__fieldname__ = 'value'

Use o seguinte no início de seu setup.py:

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

Depois disso, você pode usar os metadados no seu script assim:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']
thp
fonte
Como eu disse acima, é só usar str.split :)
Éric Araujo
Oh Deus não. Você está analisando Python em Python? No mínimo, use eval (), child.
slacy
3
Má orientação do conselho, eval deve ser evitado quando tão fácil.
Gringo Suave
3

Com uma estrutura como esta:

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

onde version.py contém:

__version__ = 'version_string'

Você pode fazer isso em setup.py :

import sys

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

from version import __version__

Isso não causará nenhum problema com quaisquer dependências que você tenha no seu mymodule / __ init__.py

Jahid
fonte
2

Para evitar importar um arquivo (e, portanto, executar seu código), é possível analisá-lo e recuperar o versionatributo da árvore de sintaxe:

# 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 tenta encontrar a __version__ou a VERSIONatribuição no nível superior do retorno do módulo é o valor da string. O lado direito pode ser uma string ou uma tupla.

Mikhail Edoshin
fonte
3
Isso parece inteligente e complicado :) Seria mais simples ter um arquivo _version.py contendo uma atribuição e analisá-la com open-read-str.split.
Éric Araujo
Obrigado por postar isso. Eu acho que é muito complexo para esta situação, mas muito útil para entender como alguém pode abordar o problema. Gosto que ele mantenha o número da versão em um único local, não exija um arquivo separado para contê-lo e não imponha as dependências de importação do módulo python no script de instalação.
ʇsәɹoɈ
2

Existem mil maneiras de esfolar um gato - eis as minhas:

# 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
fonte
2

Limpando https://stackoverflow.com/a/12413800 do @ 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
AT
fonte
2

Agora, isso é grosseiro e precisa ser aprimorado (pode até haver uma chamada de membro descoberta em pkg_resources que eu perdi), mas eu simplesmente não vejo por que isso não funciona, nem por que ninguém sugeriu isso até agora (pesquisar no Google não aumentou isso) ... observe que este é o Python 2.xe requereria o pkg_resources (sigh):

import pkg_resources

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

Queríamos colocar as informações meta sobre o nosso pacote pypackageryno __init__.py, mas não podia, uma vez que tem dependências de terceiros como PJ Eby já apontado (ver sua resposta e a advertência sobre a condição de corrida).

Resolvemos isso criando um módulo separado pypackagery_meta.pyque contém apenas as meta informações:

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

depois importou as meta informações em packagery/__init__.py:

# ...

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

# ...

e finalmente o usou em 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'],
 )

Você deve incluir pypackagery_metano seu pacote o py_modulesargumento de instalação. Caso contrário, você não poderá importá-lo na instalação, pois a distribuição empacotada não possui.

marko.ristin
fonte
1

Simples e direto, crie um arquivo chamado source/package_name/version.pycom o seguinte conteúdo:

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

Em seguida, no seu arquivo source/package_name/__init__.py, você importa a versão para outras pessoas usarem:

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

Agora, você pode colocar isso 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()

Testado isso com Python 2.7, 3.3, 3.4, 3.5, 3.6e 3.7no Linux, Windows e Mac OS. Eu usei no meu pacote que possui testes de integração e unidade para todas as plataformas de teses. Você pode ver os resultados de .travis.ymle appveyor.ymlaqui:

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

Uma versão alternativa está usando o gerenciador 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 ) )

Você também pode usar o codecsmódulo para lidar com erros unicode no Python 2.7e no3.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 ) )

Se você estiver escrevendo um módulo Python 100% em C / C ++ usando extensões Python C, poderá fazer a mesma coisa, mas usando C / C ++ em vez de Python.

Nesse caso, crie o seguinte 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 lê a versão do arquivo version.h:

const char* __version__ = "1.0.12";

Mas, não esqueça de criar MANIFEST.ino version.harquivo para incluir :

include README.md
include LICENSE.txt

recursive-include source *.h

E é integrado ao aplicativo principal com:

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

    ...
}

Referências:

  1. erro de arquivo aberto python
  2. Definir um global em um módulo Python a partir de uma API C
  3. Como incluir dados do pacote com setuptools / distribut?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. Como usar os pacotes setuptools e ext_modules com o mesmo nome?
  6. É possível incluir subdiretórios usando dist utils (setup.py) como parte dos dados do pacote?
do utilizador
fonte
1

implante pacote no servidor e convenção de nomenclatura de arquivos para pacotes de índices:

exemplo para conversão de versão dinâmica pip:

  • ganhar:

    • 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

Encontre o exemplo setup.py chama a versão do pip dinâmico correspondente ao 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
fonte
0

Eu estou usando uma variável de ambiente como abaixo

VERSION = 0.0.0 python setup.py sdist bdist_wheel

Em setup.py


import os

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

Para verificação de consistência com a versão do empacotador, estou usando o script abaixo.

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
fonte