Ссылка require.txt для файла install_requires kwarg в файле setuptools setup.py

У меня есть файл requirements.txt, который я использую с Travis-CI. Кажется глупым дублировать требования как в requirements.txt, так и setup.py, поэтому я надеялся передать дескриптор файла в install_requires kwarg в setuptools.setup.

Возможно ли это? Если да, то как мне это сделать?

Вот мой requirements.txt файл:

guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4

Ответ 1

Вы можете перевернуть его и перечислить зависимости в setup.py и иметь один символ - точку . - вместо этого в requirements.txt require.txt.


С другой стороны, даже если не рекомендуется, это еще можно разобрать requirements.txt файл (если он не относится никаких внешних требований по URL) с помощью следующей хака (протестировано с pip 9.0.1):

install_reqs = parse_requirements('requirements.txt', session='hack')

Это не фильтрует маркеры среды, хотя.


В старых версиях pip, а точнее старше 6.0, есть открытый API, который может быть использован для достижения этой цели. Файл требований может содержать комментарии (#) и может включать некоторые другие файлы (--requirement или -r). Таким образом, если вы действительно хотите, чтобы разобрать requirements.txt вы можете использовать пип анализатор:

from pip.req import parse_requirements

# parse_requirements() returns generator of pip.req.InstallRequirement objects
install_reqs = parse_requirements(<requirements_path>)

# reqs is a list of requirement
# e.g. ['django==1.5.1', 'mezzanine==1.4.6']
reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
)

Ответ 2

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

Цель автора пакета при определении зависимостей заключается в том, чтобы сказать "везде, где вы устанавливаете этот пакет, это другие необходимые вам пакеты, чтобы этот пакет работал".

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

Автор пакета пишет для самых разных сценариев, потому что они ставят свою работу там, чтобы использовать их способами, о которых они могут не знать, и не имеют способа узнать, какие пакеты будут установлены вместе с их пакетом. Чтобы быть хорошим соседом и избегать конфликтов версий зависимостей с другими пакетами, им необходимо указать как можно больший диапазон версий зависимостей, которые могут работать. Это то, что делает install_requires в setup.py.

Автор развертывания пишет для совершенно другой, очень конкретной цели: один экземпляр установленного приложения или службы, установленный на конкретном компьютере. Чтобы точно контролировать развертывание и убедиться, что правильные пакеты протестированы и развернуты, автор развертывания должен указать точную версию и исходное местоположение каждого устанавливаемого пакета, включая зависимости и зависимости зависимостей. С помощью этой спецификации развертывание может быть последовательно применено к нескольким машинам или проверено на тестовом компьютере, и автор развертывания может быть уверен, что одни и те же пакеты развертываются каждый раз. Это то, что делает requirements.txt.

Итак, вы можете видеть, что, хотя оба они выглядят как большой список пакетов и версий, эти две вещи имеют очень разные задания. И это определенно легко смешивать это и ошибаться! Но правильный способ подумать об этом заключается в том, что requirements.txt является "ответом" на "вопрос", заданный требованиями во всех различных файлах пакета setup.py. Вместо того, чтобы писать его вручную, он часто генерируется путем указания pip, чтобы посмотреть все файлы setup.py в наборе желаемых пакетов, найти набор пакетов, которые, по его мнению, соответствуют всем требованиям, а затем после их установки, "заморозить" этот список пакетов в текстовый файл (сюда входит название pip freeze).

Итак, вынос:

  • setup.py должен объявлять самые доступные версии зависимостей, которые все еще работоспособны. Его задача - сказать, с чем может работать конкретный пакет.
  • requirements.txt - это манифест развертывания, который определяет полное задание на установку и не должен рассматриваться как связанный с каким-либо одним пакетом. Его задача - объявить исчерпывающий список всех необходимых пакетов для выполнения работы по развертыванию.
  • Поскольку эти две вещи имеют такой различный контент и причины для существования, невозможно просто скопировать один в другой.

Ссылки:

Ответ 3

Он не может взять дескриптор файла. Аргументом install_requires может быть только строка или список строк.

Вы можете, конечно, прочитать свой файл в настройке script и передать его как список строк в install_requires.

import os
from setuptools import setup

with open('requirements.txt') as f:
    required = f.read().splitlines()

setup(...
install_requires=required,
...)

Ответ 4

Файлы требований используют расширенный формат пифа, что полезно только в том случае, если вам нужно дополнить setup.py более сильными ограничениями, например, с указанием точных URL-адресов, из которых должны быть получены некоторые зависимости, или вывода pip freeze в заморозить весь набор пакетов до известных рабочих версий. Если вам не нужны дополнительные ограничения, используйте только setup.py. Если вы чувствуете, что вам действительно нужно отправить requirements.txt, вы можете сделать его одной строкой:

.

Он будет действительным и будет точно соответствовать содержимому setup.py, находящемуся в том же каталоге.

Ответ 5

Не являясь точным ответом на вопрос, я рекомендую опубликовать сообщение Дональда Стаффта на https://caremad.io/2013/07/setup-vs-requirement/ для хорошего решения этой проблемы. Я использовал его с большим успехом.

Короче говоря, requirements.txt не является альтернативой setup.py, а дополнением к развертыванию. Соблюдайте абстрагирование зависимостей пакетов в setup.py. Установите requirements.txt или более из них для получения определенных версий зависимостей пакетов для разработки, тестирования или производства.

например. с пакетами, включенными в репо под deps/:

# fetch specific dependencies
--no-index
--find-links deps/

# install package
# NOTE: -e . for editable mode
.

pip выполняет пакет setup.py и устанавливает конкретные версии зависимостей, объявленные в install_requires. Там нет двуличности и цели обоих артефактов.

Ответ 6

Большинство других ответов выше не работают с текущей версией API-интерфейса pip. Вот правильный * способ сделать это с текущей версией pip (6.0.8 на момент написания статьи, также работал в 7.1.2. Вы можете проверить свою версию с помощью pip -V).

from pip.req import parse_requirements
from pip.download import PipSession

install_reqs = parse_requirements(<requirements_path>, session=PipSession())

reqs = [str(ir.req) for ir in install_reqs]

setup(
    ...
    install_requires=reqs
    ....
)

* Правильно, так как это способ использовать parse_requirements с текущим пипом. По-видимому, это не лучший способ сделать это, так как, как отмечали выше, пип не поддерживает API.

Ответ 7

Использование parse_requirements является проблематичным, поскольку API-интерфейс pip не публикован и не поддерживается. В pip 1.6 эта функция действительно перемещается, поэтому существующие ее возможности могут сломаться.

Более надежный способ устранения дублирования между setup.py и requirements.txt - это конкретные ваши зависимости в setup.py, а затем поместите -e . в ваш requirements.txt файл. Некоторая информация от одного из разработчиков pip о том, почему это лучший способ, доступна здесь: https://caremad.io/blog/setup-vs-requirement/

Ответ 8

Установите текущий пакет в Travis. Это позволяет избежать использования файла requirements.txt. Например:

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py

Ответ 9

Если вы не хотите, чтобы ваши пользователи устанавливали pip, вы можете эмулировать его поведение следующим образом:

import sys

from os import path as p

try:
    from setuptools import setup, find_packages
except ImportError:
    from distutils.core import setup, find_packages


def read(filename, parent=None):
    parent = (parent or __file__)

    try:
        with open(p.join(p.dirname(parent), filename)) as f:
            return f.read()
    except IOError:
        return ''


def parse_requirements(filename, parent=None):
    parent = (parent or __file__)
    filepath = p.join(p.dirname(parent), filename)
    content = read(filename, parent)

    for line_number, line in enumerate(content.splitlines(), 1):
        candidate = line.strip()

        if candidate.startswith('-r'):
            for item in parse_requirements(candidate[2:].strip(), filepath):
                yield item
        else:
            yield candidate

setup(
...
    install_requires=list(parse_requirements('requirements.txt'))
)

Ответ 10

from pip.req import parse_requirements не работал у меня, и я думаю, что это для пустых строк в моем файле требований .txt, но эта функция работает

def parse_requirements(requirements):
    with open(requirements) as f:
        return [l.strip('\n') for l in f if l.strip('\n') and not l.startswith('#')]

reqs = parse_requirements(<requirements_path>)

setup(
    ...
    install_requires=reqs,
    ...
)

Ответ 11

ОСТОРОЖНО! parse_requirements ПОВЕДЕНИЕ!

Обратите внимание, что pip.req.parse_requirements изменит символы подчеркивания на тире. Это меня раздирало несколько дней, прежде чем я обнаружил это. Пример демонстрации:

from pip.req import parse_requirements  # tested with v.1.4.1

reqs = '''
example_with_underscores
example-with-dashes
'''

with open('requirements.txt', 'w') as f:
    f.write(reqs)

req_deps = parse_requirements('requirements.txt')
result = [str(ir.req) for ir in req_deps if ir.req is not None]
print result

производит

['example-with-underscores', 'example-with-dashes']

Ответ 12

В пипе 10 следующий интерфейс устарел:

from pip.req import parse_requirements
from pip.download import PipSession

Поэтому я переключил его на простой анализ текста:

with open('requirements.txt', 'r') as f:
    install_reqs = [
        s for s in [
            line.strip(' \n') for line in f
        ] if not s.startswith('#') and s != ''
    ]

Ответ 13

Я создал функцию многократного использования для этого. Фактически он анализирует весь каталог файлов требований и устанавливает их в extras_require.

Последние всегда доступны здесь: https://gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os

from setuptools import find_packages, setup

try:
    from pip._internal.req import parse_requirements
    from pip._internal.download import PipSession
except ImportError:
    from pip.req import parse_requirements
    from pip.download import PipSession


def setup_requirements(
        patterns=[
            'requirements.txt', 'requirements/*.txt', 'requirements/*.pip'
        ],
        combine=True,
):
    """
    Parse a glob of requirements and return a dictionary of setup() options.
    Create a dictionary that holds your options to setup() and update it using this.
    Pass that as kwargs into setup(), viola

    Any files that are not a standard option name (ie install, tests, setup) are added to extras_require with their
    basename minus ext. An extra key is added to extras_require: 'all', that contains all distinct reqs combined.

    Keep in mind all literally contains 'all' packages in your extras.
    This means if you have conflicting packages across your extras, then you're going to have a bad time.
    (don't use all in these cases.)

    If you're running this for a Docker build, set 'combine=True'.
    This will set 'install_requires' to all distinct reqs combined.

    Example:

    >>> _conf = dict(
    ...     name='mainline',
    ...     version='0.0.1',
    ...     description='Mainline',
    ...     author='Trevor Joynson <[email protected],io>',
    ...     url='https://trevor.joynson.io',
    ...     namespace_packages=['mainline'],
    ...     packages=find_packages(),
    ...     zip_safe=False,
    ...     include_package_data=True,
    ... )
    >>> _conf.update(setup_requirements())
    >>> setup(**_conf)

    :param str pattern: Glob pattern to find requirements files
    :param bool combine: Set True to set install_requires to extras_require['all']
    :return dict: Dictionary of parsed setup() options
    """
    session = PipSession()

    # Handle setuptools insanity
    key_map = {
        'requirements': 'install_requires',
        'install': 'install_requires',
        'tests': 'tests_require',
        'setup': 'setup_requires',
    }
    ret = {v: set() for v in key_map.values()}
    extras = ret['extras_require'] = {}
    all_reqs = set()

    files = [glob.glob(pat) for pat in patterns]
    files = itertools.chain(*files)

    for full_fn in files:
        # Parse
        reqs = {
            str(r.req)
            for r in parse_requirements(full_fn, session=session)
            # Must match env marker, eg:
            #   yarl ; python_version >= '3.0'
            if r.match_markers()
        }
        all_reqs.update(reqs)

        # Add in the right section
        fn = os.path.basename(full_fn)
        barefn, _ = os.path.splitext(fn)
        key = key_map.get(barefn)

        if key:
            ret[key].update(reqs)
            extras[key] = reqs

        extras[barefn] = reqs

    if 'all' not in extras:
        extras['all'] = list(all_reqs)

    if combine:
        extras['install'] = ret['install_requires']
        ret['install_requires'] = list(all_reqs)

    def _listify(dikt):
        ret = {}

        for k, v in dikt.items():
            if isinstance(v, set):
                v = list(v)
            elif isinstance(v, dict):
                v = _listify(v)
            ret[k] = v

        return ret

    ret = _listify(ret)

    return ret

Ответ 14

Еще одно возможное решение...

def gather_requirements(top_path=None):
    """Captures requirements from repo.

    Expected file format is: requirements[-_]<optional-extras>.txt

    For example:

        pip install -e .[foo]

    Would require:

        requirements-foo.txt

        or

        requirements_foo.txt

    """
    from pip.download import PipSession
    from pip.req import parse_requirements
    import re

    session = PipSession()
    top_path = top_path or os.path.realpath(os.getcwd())
    extras = {}
    for filepath in tree(top_path):
        filename = os.path.basename(filepath)
        basename, ext = os.path.splitext(filename)
        if ext == '.txt' and basename.startswith('requirements'):
            if filename == 'requirements.txt':
                extra_name = 'requirements'
            else:
                _, extra_name = re.split(r'[-_]', basename, 1)
            if extra_name:
                reqs = [str(ir.req) for ir in parse_requirements(filepath, session=session)]
                extras.setdefault(extra_name, []).extend(reqs)
    all_reqs = set()
    for key, values in extras.items():
        all_reqs.update(values)
    extras['all'] = list(all_reqs)
    return extras

а затем использовать...

reqs = gather_requirements()
install_reqs = reqs.pop('requirements', [])
test_reqs = reqs.pop('test', [])
...
setup(
    ...
    'install_requires': install_reqs,
    'test_requires': test_reqs,
    'extras_require': reqs,
    ...
)

Ответ 15

Этот простой подход читает файл требований из setup.py. Это вариант ответа Дмитрия С.. Этот ответ совместим только с Python 3. 6+.

Согласно D.S., requirements.txt может задокументировать конкретные требования с определенными номерами версий, тогда как setup.py может задокументировать абстрактные требования со свободными диапазонами версий.

Ниже приведен отрывок из моего setup.py.

import distutils.text_file
from pathlib import Path
from typing import List

def parse_requirements(filename: str) -> List[str]:
    """Return requirements from requirements file."""
    # Ref: https://stackoverflow.com/a/42033122/
    return distutils.text_file.TextFile(filename=Path(__file__).with_name(filename)).readlines()

setup(...
      install_requires=parse_requirements('requirements.txt'),
   ...)

Обратите внимание, что distutils.text_file.TextFile удалит комментарии. Кроме того, по моему опыту, вам, очевидно, не нужно предпринимать каких-либо особых шагов для включения в файл требований.

Ответ 16

Перекрестная публикация моего ответа от этого ТАКОГО вопроса для другого простого решения с проверкой версии в pip.

try:  # for pip >= 10
    from pip._internal.req import parse_requirements
    from pip._internal.download import PipSession
except ImportError:  # for pip <= 9.0.3
    from pip.req import parse_requirements
    from pip.download import PipSession

requirements = parse_requirements(os.path.join(os.path.dirname(__file__), 'requirements.txt'), session=PipSession())

if __name__ == '__main__':
    setup(
        ...
        install_requires=[str(requirement.req) for requirement in requirements],
        ...
    )

Затем просто добавьте все свои требования в requirements.txt в корневой каталог проекта.

Ответ 17

Еще один взломать parse_requirements, который также анализирует маркеры среды в extras_require:

from collections import defaultdict
from pip.req import parse_requirements

requirements = []
extras = defaultdict(list)
for r in parse_requirements('requirements.txt', session='hack'):
    if r.markers:
        extras[':' + str(r.markers)].append(str(r.req))
    else:
        requirements.append(str(r.req))

setup(
    ...,
    install_requires=requirements,
    extras_require=extras
)

Он должен поддерживать как sdist, так и двоичные dists.

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

Ответ 18

Вот полный хак (проверен с pip 9.0.1) на основе Romain answer, который анализирует requirements.txt и фильтрует его в соответствии с текущим маркеры среды:

from pip.req import parse_requirements

requirements = []
for r in parse_requirements('requirements.txt', session='hack'):
    # check markers, such as
    #
    #     rope_py3k    ; python_version >= '3.0'
    #
    if r.match_markers():
        requirements.append(str(r.req))

print(requirements)