Справочные требования.txt для установки требует kwarg в setuptools setup.py файл?

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

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

вот мой :

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

16 ответов


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


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

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

это не фильтрует [маркеры среды] (https://pip.pypa.io/en/stable/reference/pip_install/#requirement-specifiers) хотя.


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

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
)

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

цель автора пакета при указании зависимостей-сказать: "где бы вы ни устанавливали этот пакет, это другие пакеты, которые вам нужны, чтобы этот пакет работал."

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

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

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

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

Итак, вывод:

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

ссылки:


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

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

import os
from setuptools import setup

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

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

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

.

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


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

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

Е. Г. с пакетами в репозитории под deps/:

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

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

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


большинство других ответов выше не работают с текущей версией 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 с текущим pip. Это все еще, вероятно, не лучший способ сделать это, так как, как сказано выше, pip на самом деле не поддерживает API.


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

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


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

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

Если вы не хотите заставлять своих пользователей устанавливать 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'))
)

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,
    ...
)

ОСТОРОЖНО 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']

Я создал отдельную функцию для этого. Он фактически анализирует весь каталог файлов требований и устанавливает их в 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 <github@trevor.joynson,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

другое возможное решение...

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,
    ...
)

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


следующий интерфейс стал устаревшим в pip 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 != ''
    ]

вот полный Хак (протестирован с pip 9.0.1) на основании ответ Ромена анализ 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)