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

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

Ответ 1

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

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

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, которые могут нуждаться в доступе к строке версии:

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

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

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

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

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

Ответ 2

пример исследования: 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: где my __version__?

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

решение: à la 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'

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

Ответ 3

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

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

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

# coverage/__init__.py
__version__ = "3.2"


# setup.py
from coverage import __version__

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

UPDATE (2017): cover.py больше не импортирует себя, чтобы получить версию. Импорт собственного кода может привести к его удалению, поскольку код продукта будет пытаться импортировать зависимости, которые еще не установлены, поскольку setup.py - это то, что их устанавливает.

Ответ 4

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

Вам нужно определить __version__ следующим образом:

__version__ = '1.4.4'

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

% ./setup.py --version
1.4.4

Ответ 5

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

Ответ 6

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

__version__ = "1.1.0-r4704"

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

verstr = "unknown"
try:
    verstrline = open('yourpackage/_version.py', "rt").read()
except EnvironmentError:
    pass # Okay, there is no version file.
else:
    VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
    mo = re.search(VSRE, verstrline, re.M)
    if mo:
        verstr = mo.group(1)
    else:
        raise RuntimeError("unable to find version in yourpackage/_version.py")

Наконец, в yourbasedir/yourpackage/__init__.py import _version вот так:

__version__ = "unknown"
try:
    from _version import __version__
except ImportError:
    # We're running in a tree that doesn't have a _version.py, so we don't know what our version is.
    pass

Пример кода, который делает это пакет "pyutil", который я поддерживаю. (См. PyPI или google search - 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 run. Точно так же, если setuptools, easy_install, distribute или distutils2 пытается создать ваш пакет как часть процесса установки другого пакета, который зависит от вашего? Затем, независимо от того, импортирован ли ваш пакет в момент его оценки setup.py или существует ли уже версия вашего пакета, которая была импортирована в течение этого срока службы интерпретатора Python, или если импорт вашего пакета требует установки первых пакетов, или имеет побочные эффекты, может изменить результаты. У меня было несколько попыток повторного использования пакетов Python, которые вызвали проблемы с такими инструментами, как py2exe и setuptools, потому что их setup.py импортирует сам пакет, чтобы найти его номер версии.

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

Ответ 7

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

__fieldname__ = 'value'

Используйте следующее в начале настройки .py:

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

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

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

Ответ 8

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

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

Ответ 9

Чтобы избежать импорта файла (и, таким образом, его код), можно проанализировать его и восстановить атрибут 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__ или version на верхнем уровне возврата модуля - это строковое значение. Правая сторона может быть либо строкой, либо кортежем.

Ответ 10

Там тысячи способов кожи кошки - здесь моя:

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

Ответ 11

Очистка fooobar.com/questions/24509/... от @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

Ответ 12

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

Мы решили это, создав отдельный модуль 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__ = '[email protected]'
__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 установки py_modules. В противном случае вы не сможете импортировать его после установки, так как в упакованном дистрибутиве его не будет.

Ответ 13

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

Ответ 14

Просто и понятно, создайте файл с именем 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 и в 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) как часть данных пакета?