Как я могу получить версию, определенную в setup.py (setuptools) в моем пакете?


Ответы:


246

Опросить строку версии уже установленного дистрибутива

Чтобы получить версию из вашего пакета во время выполнения (что, по-видимому, действительно задает ваш вопрос), вы можете использовать:

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

Сохранить строку версии для использования во время установки

Если вы хотите пойти по другому пути (что, по-видимому, и есть то, о чем другие авторы ответов думали, что вы спрашивали об этом), поместите строку версии в отдельный файл и прочитайте содержимое этого файла setup.py.

Вы можете сделать version.py в вашем пакете со __version__строкой, а затем прочитать его из setup.py, используя его execfile('mypackage/version.py'), чтобы он устанавливался __version__в пространстве имен setup.py.

Если вы хотите гораздо более простой способ, который будет работать со всеми версиями Python и даже с не-Python языками, которым может потребоваться доступ к строке версии:

Сохраните строку версии как единственное содержимое простого текстового файла с именем eg VERSIONи прочитайте этот файл во время setup.py.

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

Этот же VERSIONфайл будет работать точно так же, как и в любой другой программе, даже не в Python, и вам нужно всего лишь изменить строку версии в одном месте для всех программ.

Предупреждение о состоянии гонки во время установки

Между прочим, НЕ импортируйте ваш пакет из вашего setup.py, как это предлагается в другом ответе здесь: он будет работать для вас (потому что у вас уже установлены зависимости вашего пакета), но он нанесет ущерб новым пользователям вашего пакета , так как они не смогут установить ваш пакет без предварительной ручной установки зависимостей.


1
Это просто лучший ответ: лучший способ - использовать execfile, если вам нужен файл setup.py для получения версии из вашего пакета.
Тринадцатое

2
execfileработает очень хорошо ... но (к сожалению) не работает с Python 3.
medmunds

9
... ОК, для Python 2 и 3 используйте with open('mypackage/version.py') as f: exec(f.read())вместо execfile('mypackage/version.py'). (Из stackoverflow.com/a/437857/647002 )
medmunds

5
Часть wreak havoc не обязательно имеет значение true ... она будет наносить ущерб только в том случае, если ваш файл init .py в пакете имеет зависимости импорта кода (прямо или косвенно).
Пиклер

2
Кажется, стоит упомянуть, что django выполняет вызов __import__ в их файле setup.py . '__Import__' против 'import' делает это безопасным? Похоже, это не вызывает у них никаких проблем.
user1978019

33

пример исследования: mymodule

Представьте себе эту конфигурацию:

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

Затем представьте себе обычный сценарий, в котором у вас есть зависимости и он setup.pyвыглядит следующим образом:

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

И пример __init__.py:

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

И например myclasses.py:

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

проблема № 1: импорт mymoduleво время установки

Если ваш setup.pyимпорт mymoduleто во время настройки вы, скорее всего, получите ImportError. Это очень распространенная ошибка, когда ваш пакет имеет зависимости. Если ваш пакет не имеет других зависимостей, кроме встроенных, вы можете быть в безопасности; Однако это не очень хорошая практика. Причина этого заключается в том, что это не будущее; скажем завтра ваш код должен потреблять некоторые другие зависимости.

проблема № 2: где мой __version__?

Если вы жестко __version__вsetup.py то оно может не совпадать с версией , что вы грузили бы в вашем модуле. Чтобы быть последовательным, вы должны поместить его в одном месте и читать его из того же места, когда вам это нужно. Использование importвы можете получить проблему # 1.

решение: а ля setuptools

Вы могли бы использовать комбинацию open, execи обеспечить Dict для того execчтобы добавить переменные:

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

И в mymodule/version.pyвыставляю версию:

__version__ = 'some.semantic.version'

Таким образом, версия поставляется вместе с модулем, и у вас не возникает проблем во время установки, когда вы пытаетесь импортировать модуль с отсутствующими зависимостями (еще не установлен).


2
Это кажется наиболее разумным решением, поскольку оно опирается только на разрешенные зависимости.
Dogweather

Отличается ли концепция версии пакета, определенной в файле setup.py, от версии ?
переменная

16

Лучше всего определить __version__код вашего продукта, а затем импортировать его в setup.py оттуда. Это дает вам значение, которое вы можете прочитать в своем работающем модуле, и у вас есть только одно место для его определения.

Значения в setup.py не установлены, и setup.py не остается после установки.

Что я сделал (например) в cover.py:

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

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

ОБНОВЛЕНИЕ (2017): cover.py больше не импортирует себя, чтобы получить версию. Импорт собственного кода может сделать его неустранимым, потому что код вашего продукта будет пытаться импортировать зависимости, которые еще не установлены, потому что именно их устанавливает файл setup.py.


4
@Evan: я не уверен, что вы получаете примерно "только получить это значение из источника". Если файл, содержащийся __version__в нем, каким-то образом поврежден, то ваш импорт также будет поврежден. Python не знает, как интерпретировать только те выражения, которые вы хотите. @pjeby прав: если вашему модулю нужно импортировать другие модули, они могут быть еще не установлены, и это будет хаотично. Этот метод работает, если вы осторожны, что при импорте не возникает длинная цепочка других импортов.
Нед Бэтчелдер

1
@Ned Batchelder Я уверен, что если вы поместите версию перед любым импортом в исходном файле и только в «из версии импорта модуля», это не изменит исходный файл больше, чем нужно. Кроме того, кто собирается выпускать неработающий код? Если пакету нужны зависимости, используйте setuptools или дождитесь выпуска distutils2 позже в этом году.
Эван Плейс

15
@ Эван, извини, но ты ошибаешься при импорте частичных файлов. Попробуйте поместить оператор print в конец длинного модуля и импортируйте первую переменную, определенную в файле. Оператор печати будет выполнен.
Нед Бэтчелдер

23
Я согласился с этим, но @pjeby совершенно прав: «Кстати, НЕ импортируйте ваш пакет из вашего setup.py, как предложено в другом ответе здесь: он будет работать для вас (поскольку у вас уже установлены зависимости вашего пакета). ), но это нанесет ущерб новым пользователям вашего пакета, поскольку они не смогут установить ваш пакет без предварительной установки зависимостей вручную. " Нед, не могли бы вы добавить предупреждение к своему ответу?
Доктор Ян-Филипп Герке

1
Например, @ Jan-PhilipGehrcke, когда я загружаю такой файл setup.py, как этот, в PyPI, а затем pip install <packagename>я получаю следующую ошибку:ImportError: No module named <packagename> . Пожалуйста, ПРЕДУПРЕЖДАЙТЕ своим читателям, что вы не можете запускать файлы setup.py, подобные этим, в тех средах, где пакет еще не установлен!
варенье

14

Ваш вопрос немного расплывчат, но я думаю, что вы спрашиваете, как его задать.

Вам нужно определить __version__так:

__version__ = '1.4.4'

И тогда вы можете подтвердить, что setup.py знает о версии, которую вы только что указали:

% ./setup.py --version
1.4.4

9

Я не был доволен этими ответами ... не хотел требовать setuptools или создавать отдельный модуль для одной переменной, поэтому я придумал это.

Если вы уверены, что основной модуль выполнен в стиле pep8 и останется таким:

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

Если вы хотите быть очень осторожным и использовать настоящий парсер:

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 является своего рода одноразовым модулем, поэтому не проблема, если он немного уродлив.


Обновление: достаточно забавно, я отошел от этого в последние годы и начал использовать отдельный файл в пакете под названием meta.py. Я поместил там много метаданных, которые я мог бы захотеть часто менять. Таким образом, не только для одного значения.


+1. Это относительно просто, хранит номер версии в одном месте, не требует отдельного файла для его хранения и не накладывает зависимости импорта модуля python на setup.py. Я даже не стал бы беспокоиться о диспетчере контекста в программе, столь же недолгой, как setup.py.
ɈsәɹoɈ

Гораздо приятнее, чем с помощью регулярных выражений, спасибо. Вы также можете использовать ast.get_docstring()с некоторыми и .split('\n')[0].strip()т. Д. Для автоматического заполнения из descriptionисточника. Одна вещь меньше , чтобы держать в синхронизации
потерял

4

Создайте файл в вашем исходном дереве, например, в yourbasedir / yourpackage / _version.py. Пусть этот файл содержит только одну строку кода, например:

__version__ = "1.1.0-r4704"

Затем в вашем файле setup.py откройте этот файл и проанализируйте номер версии следующим образом:

verstr = "неизвестно"
пытаться:
    verstrline = open ('yourpackage / _version.py', "rt"). read ()
кроме EnvironmentError:
    pass # Хорошо, нет файла версии.
еще:
    VSRE = r "^ __ version__ = ['\"] ([^' \ "] *) ['\"] "
    mo = re.search (VSRE, verstrline, re.M)
    если мо:
        verstr = mo.group (1)
    еще:
        поднять RuntimeError («невозможно найти версию в вашем пакете / _version.py»)

Наконец, в yourbasedir/yourpackage/__init__.py_version импорта вот так:

__version__ = "неизвестно"
пытаться:
    из _version import __version__
кроме ImportError:
    # Мы работаем в дереве, у которого нет _version.py, поэтому мы не знаем, какая у нас версия.
    проходить

Примером кода, который делает это, является пакет "pyutil", который я поддерживаю. (См. PyPI или поиск в Google - stackoverflow запрещает мне включать гиперссылку на него в этом ответе.)

@pjeby прав, что вы не должны импортировать свой пакет из его собственного setup.py. Это сработает, когда вы протестируете его, создав новый интерпретатор Python и выполнив в нем setup.py python setup.py, но в некоторых случаях это не сработает. Это потому, import youpackageчто не означает чтение текущего рабочего каталога для каталога с именем «yourpackage», это означает поиск в текущем sys.modulesключе «yourpackage» и затем выполнение различных действий, если его там нет. Так что это всегда работает, когда вы делаете, python setup.pyпотому что у вас свежий, пустой sys.modules, но это не работает в целом.

Например, что если py2exe выполняет ваш setup.py как часть процесса упаковки приложения? Я видел такой случай, когда py2exe поместил бы неправильный номер версии в пакет, потому что пакет получал свой номер версии отimport myownthingв его setup.py, но другая версия этого пакета была ранее импортирована во время запуска py2exe. Аналогичным образом, что если setuptools, easy_install, distribution или distutils2 пытаются собрать ваш пакет как часть процесса установки другого пакета, который зависит от вашего? Затем, будет ли ваш пакет импортируемым во время оценки его setup.py, или уже существует версия вашего пакета, которая была импортирована в течение жизни этого интерпретатора Python, или для импорта вашего пакета сначала должны быть установлены другие пакеты. или имеет побочные эффекты, может изменить результаты. У меня было несколько попыток повторно использовать пакеты Python, что вызывало проблемы для таких инструментов, как py2exe и setuptools, потому что их setup.py импортирует сам пакет, чтобы найти его номер версии.

Кстати, эта техника прекрасно работает с инструментами для автоматического создания yourpackage/_version.pyфайла для вас, например, читая историю контроля версий и записывая номер версии на основе самого последнего тега в истории контроля версий. Вот инструмент, который делает это для дарков: http://tahoe-lafs.org/trac/darcsver/browser/trunk/README.rst, а также фрагмент кода, который делает то же самое для git: http: // github .com / Уорнер / питон-ECDSA / BLOB / 0ed702a9d4057ecf33eea969b8cf280eaccd89a1 / setup.py # L34


3

Это также должно работать, используя регулярные выражения и в зависимости от полей метаданных иметь такой формат:

__fieldname__ = 'value'

Используйте следующее в начале вашего setup.py:

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

После этого вы можете использовать метаданные в вашем скрипте следующим образом:

print 'Author is:', metadata['author']
print 'Version is:', metadata['version']

Как я уже говорил выше, просто используйте str.split :)
Эрик Араужо

О боже, нет. Вы анализируете Python в Python? По крайней мере, используйте eval (), child.
рабство

3
Плохой совет, рабство, следует избегать, когда так легко.
Гринго Суаве

3

С такой структурой:

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

где version.py содержит:

__version__ = 'version_string'

Вы можете сделать это в setup.py :

import sys

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

from version import __version__

Это не вызовет никаких проблем с зависимостями, которые у вас есть в вашем mymodule / __ init__.py


2

Чтобы избежать импорта файла (и, следовательно, выполнения его кода), можно проанализировать его и восстановить versionатрибут из синтаксического дерева:

# 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

Этот код пытается найти присваивание __version__or VERSIONна верхнем уровне модуля, возвращающее строковое значение. Правая сторона может быть либо строкой, либо кортежем.


3
Это выглядит умно и сложно :) Было бы проще иметь файл _version.py, содержащий одно назначение, и проанализировать его с помощью open-read-str.split.
Эрик Араужо

Спасибо за публикацию этого. Я думаю, что это слишком сложно для этой ситуации, но очень полезно для понимания того, как можно подойти к проблеме. Мне нравится, что он хранит номер версии в одном месте, не требует отдельного файла для его хранения и не накладывает зависимости импорта модуля python на сценарий установки.
ɈsәɹoɈ

2

Есть тысяча способов снять шкуру с кошки - вот мой:

# 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.")

2

Очистка https://stackoverflow.com/a/12413800 от @ 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

2

Теперь это грубо и нуждается в некоторой доработке (возможно, в pkg_resources я пропустил непокрытый вызов члена, но я просто не понимаю, почему это не работает, и почему никто не предложил его на сегодняшний день (поиск в Google не включил это) ... обратите внимание, что это Python 2.x, и для него потребуется 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

2

Мы хотели поставить мета информацию о нашем пакете pypackageryв __init__.py, но не мог , так как он имеет зависимости сторонних , как PJ Эби уже отмечали (см его ответ и предупреждение относительно состояния гонки).

Мы решили это, создав отдельный модуль, pypackagery_meta.pyкоторый содержит только метаинформацию:

"""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__ = 'marko.ristin@gmail.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2018 Parquery AG'

затем импортировал метаинформацию в packagery/__init__.py:

# ...

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

# ...

и, наконец, использовал его в 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'],
 )

Вы должны включить pypackagery_metaв свой пакет сpy_modules аргументом настройки. В противном случае вы не сможете импортировать его после установки, так как в упакованном дистрибутиве его не будет.


1

Просто и понятно, создайте файл source/package_name/version.pyс таким содержимым:

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

Затем в своем файле source/package_name/__init__.pyвы импортируете версию для использования другими людьми:

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

Теперь вы можете надеть это 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()

Испытано это с Python 2.7, 3.3, 3.4, 3.5, 3.6и 3.7на Linux, Windows и Mac OS. Я использовал пакет, в котором есть Интеграция и Модульные тесты для всех этих платформ. Вы можете увидеть результаты .travis.ymlи appveyor.ymlздесь:

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

Альтернативная версия использует менеджер контекста:

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

Вы также можете использовать codecsмодуль для обработки ошибок Unicode как на Python, так 2.7и на Python.3.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 ) )

Если вы пишете модуль Python на 100% на C / C ++ с использованием расширений Python C, вы можете сделать то же самое, но с использованием C / C ++ вместо Python.

В этом случае создайте следующее 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'],
            )
        ],
    )

Который читает версию из файла version.h:

const char* __version__ = "1.0.12";

Но не забудьте создать файл MANIFEST.inдля включения version.h:

include README.md
include LICENSE.txt

recursive-include source *.h

И он интегрирован в основное приложение с:

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

    ...
}

Ссылки:

  1. Ошибка открытого файла Python
  2. Определить глобальный в модуле Python из C API
  3. Как включить данные пакета с помощью setuptools / distribution?
  4. https://github.com/lark-parser/lark/blob/master/setup.py#L4
  5. Как использовать пакеты setuptools и ext_modules с тем же именем?
  6. Можно ли включить подкаталоги, используя dist utils (setup.py) как часть данных пакета?

1

развернуть пакет на сервере и соглашение об именах файлов для пакетов индексов:

пример преобразования динамической версии в пипсах:

  • выиграть:

    • test_pkg-1.0.0-cp36-cp36m-win_amd64.whl
    • test_pkg-1.0.0-py3.6-беспроигрышная amd64.egg
  • макинтош:

    • 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

Найдите образец setup.py вызывает динамическое соответствие версии пипа из git commit

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

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

)

0

Я использую переменную среды, как показано ниже

ВЕРСИЯ = 0.0.0 python setup.py sdist bdist_wheel

В setup.py


import os

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

Для проверки согласованности с версией упаковщика я использую приведенный ниже скрипт.

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.