Как получить доступ к текстовому файлу в корне проекта из пакета python в каталоге `src/`

Я хочу, чтобы номер версии моего пакета жил в одном месте, где все, что ему нужно, может ссылаться на него.

Я нашел несколько предложений в этом руководстве по Python для одиночной поставки версии пакета и решил попробовать #4, сохранив его в простом текстовом файле в корне моего проекта с именем VERSION,

Вот сокращенная версия дерева каталогов моего проекта (вы можете увидеть полный проект на GitHub):

.
├── MANIFEST.in
├── README.md
├── setup.py
├── VERSION
├── src/
│   └── fluidspaces/
│       ├── __init__.py
│       ├── __main__.py
│       ├── i3_commands.py
│       ├── rofi_commands.py
│       ├── workspace.py
│       └── workspaces.py
└── tests/
    ├── test_workspace.py
    └── test_workspaces.py

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

Но VERSION а также src/fluidspaces/__main__.py не являются братьями и сестрами, и основной модуль не знает путь к корню проекта, поэтому я не могу использовать этот подход.

Гид имел это напоминание:

Предупреждение. При таком подходе вы должны убедиться, что файл VERSION включен во все ваши исходные и двоичные дистрибутивы (например, добавьте include VERSION в ваш файл MANIFEST.in).

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

Чтобы построить, я бегу pip install -U . из корня проекта и внутри virtualenv. Вот папки, которые создаются в <virtualenv>/lib/python3.6/site-packages в следствии:

fluidspaces/
├── i3_commands.py
├── __init__.py
├── __main__.py
├── __pycache__/  # contents snipped
├── rofi_commands.py
├── workspace.py
└── workspaces.py
fluidspaces-0.1.0-py3.6.egg-info/
├── dependency_links.txt
├── entry_points.txt
├── installed-files.txt
├── PKG-INFO
├── SOURCES.txt
└── top_level.txt

Больше моих файлов конфигурации:

MANIFEST.in:

include README.md
include VERSION
graft src
prune tests

setup.py:

#!/usr/bin/env python3

from setuptools import setup, find_packages


def readme():
    '''Get long description from readme file'''
    with open('README.md') as f:
        return f.read()


def version():
    '''Get version from version file'''
    with open('VERSION') as f:
        return f.read().strip()


setup(
    name='fluidspaces',
    version=version(),
    description='Navigate i3wm named containers',
    long_description=readme(),
    author='Peter Henry',
    author_email='me@peterhenry.net',
    url='https://github.com/mosbasik/fluidspaces',
    license='MIT',
    classifiers=[
      'Development Status :: 3 - Alpha',
      'Programming Language :: Python :: 3.6',
    ],
    packages=find_packages('src'),
    include_package_data=True,
    package_dir={'': 'src'},
    package_data={'': ['VERSION']},
    setup_requires=[
        'pytest-runner',
    ],
    tests_require=[
        'pytest',
    ],
    entry_points={
        'console_scripts': [
            'fluidspaces = fluidspaces.__main__:main',
        ],
    },
    python_requires='~=3.6',
)

Я нашел этот вопрос. Есть ли какая-либо функция Python для получения корневого каталога "data_files"? это заставляет меня думать pkg_resources Библиотека - это ответ на мои проблемы, но я не смог понять, как использовать ее в моей ситуации.

У меня были проблемы, потому что большинство примеров, которые я обнаружил, содержат пакеты Python непосредственно в корне проекта, а не изолированы в src/ каталог. Я использую src/ каталог из-за таких рекомендаций:

Другие ручки, которые я нашел и попробовал немного крутить, это package_data, include_package_data, а также data_files Kwargs для setup(), Не знаю, насколько они хороши. Похоже, что между вещами, объявленными с ними, и вещами, объявленными в манифесте, есть некоторое взаимодействие, но я не уверен в деталях.

1 ответ

Обсудили эту проблему с некоторыми людьми на IRC-канале #python на Freenode. Я научился:

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

В конце концов я решил использовать setuptools_scm пакет для получения информации о версии из моих тегов git, а не из файла в моем репозитории (кто-то другой делал это с помощью своего пакета, и его аргументы были убедительными).

В результате я получил свой номер версии в setup.py очень легко:

setup.py:

from setuptools import setup, find_packages

def readme():
    '''Get long description from readme file'''
    with open('README.md') as f:
        return f.read()

setup(
    name='fluidspaces',
    use_scm_version=True,  # use this instead of version
    description='Navigate i3wm named containers',
    long_description=readme(),
    author='Peter Henry',
    author_email='me@peterhenry.net',
    url='https://github.com/mosbasik/fluidspaces',
    license='MIT',
    classifiers=[
      'Development Status :: 3 - Alpha',
      'Programming Language :: Python :: 3.6',
    ],
    packages=find_packages('src'),
    package_dir={'': 'src'},
    setup_requires=[
        'pytest-runner',
        'setuptools_scm',  # require package for setup
    ],
    tests_require=[
        'pytest',
    ],
    entry_points={
        'console_scripts': [
            'fluidspaces = fluidspaces.__main__:main',
        ],
    },
    python_requires='~=3.6',
)

но в итоге мне пришлось иметь жестко заданный путь, указывающий, каким должен быть корень проекта по отношению к коду пакета, а это своего рода то, чего я раньше избегал. Я думаю, что эта проблема в репозитории GitHub setuptools_scm может быть, почему это необходимо.

src / fluidspaces / __ main__.py:

import argparse
from setuptools_scm import get_version  # import this function

def main(args=None):
    # set up command line argument parsing
    parser = argparse.ArgumentParser()
    parser.add_argument('-V', '--version',
                        action='version',
                        version=get_version(root='../..', relative_to=__file__))  # and call it here

Для тех, кто все еще ищет ответ на этот вопрос, ниже я попытаюсь следовать четвертому варианту руководства по выбору версии пакета из одного источника. Стоит отметить, ПОЧЕМУ вы можете выбрать это решение, когда есть другие более простые. Как указано в ссылке, этот подход полезен, когда у вас есть внешние инструменты, которые также могут захотеть легко проверить версию (например, инструменты CI/CD).

Файловое дерево

myproj
├── MANIFEST.in
├── myproj
│   ├── VERSION
│   └── __init__.py
└── setup.py

myproj/ ВЕРСИЯ

1.4.2

MANIFEST.in

include myproj/VERSION

setup.py

with open('myproj/VERSION') as version_file:
    version = version_file.read().strip()

setup(
    ...
    version=version,
    ...
    include_package_data=True,  # needed for the VERSION file
    ...
)

myproj/ __ init__.py

import pkgutil

__name__ = 'myproj'
__version__ = pkgutil.get_data(__name__, 'VERSION').decode()

Стоит отметить, что настройка конфигурации в setup.cfg - хорошая и чистая альтернатива включению всего в функцию установки setup.py. Вместо того, чтобы читать версию в setup.py, а затем включать ее в функцию, вы можете сделать следующее:

setup.cfg

[metadata]
name = my_package
version = attr: myproj.VERSION

В полном примере я решил оставить все в setup.py для упрощения использования одним файлом меньше и неуверенности в том, удаляются ли потенциальные пробелы вокруг версии в файле VERSION решением cfg.

Другие вопросы по тегам