Справочные требования.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
- это манифест развертывания, который определяет все задание установки и не должен рассматриваться как привязанный к одному пакету. Его задача-объявить исчерпывающий список всех необходимых пакетов для работы развертывания. - потому что эти две вещи имеют такое разное содержание и причины существования, что невозможно просто скопировать одну в другую.
ссылки:
- install_requires vs файлы требований из руководства пользователя по упаковке Python.
он не может взять дескриптор файла. 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)