在setuptools setup.py文件中引用install_requires kwarg的requirements.txt

我有一个requirements.txt文件,我正在使用Travis-CI。在requirements.txtsetup.py中复制需求似乎很愚蠢,所以我希望将一个文件句柄传递给setuptools.setup中的install_requires kwarg。

这可能吗?如果是,我该怎么做呢?

下面是我的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
204557 次浏览

它不能接受文件句柄。install_requires参数可以只能是字符串或字符串列表. c。

当然,您可以在设置脚本中读取文件,并将其作为字符串列表传递给install_requires

import os
from setuptools import setup


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


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

你可以将其翻转并列出setup.py中的依赖项,并使用单个字符—a . . —在requirements.txt中替换。


或者,即使不建议,仍然可以使用以下hack(使用pip 9.0.1测试)解析requirements.txt文件(如果它没有通过URL引用任何外部需求):

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

不过,它不会过滤环境标志


在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
)

在Travis中安装当前包。这避免了使用requirements.txt文件。 例如:< / p >

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

需求文件使用扩展pip格式,这只在你需要用更强的约束来补充你的setup.py时有用,例如指定某些依赖项必须来自的确切url,或者pip freeze的输出将整个包集冻结为已知的工作版本。如果你不需要额外的约束,只使用setup.py。如果你觉得无论如何都需要传递requirements.txt,你可以将它设置为一行:

.

它将是有效的,并准确地引用同一目录中的setup.py的内容。

from pip.req import parse_requirements没有为我工作,我认为它是为我的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 API没有公开记录和支持。在pip 1.6中,该函数实际上是移动的,因此对它的现有使用可能会中断。

消除setup.pyrequirements.txt之间重复的一个更可靠的方法是在setup.py中指定依赖项,然后将-e .放入requirements.txt文件中。来自pip开发人员的一些关于为什么这是一个更好的方法的信息可以在这里找到:https://caremad.io/blog/setup-vs-requirement/

虽然不是这个问题的确切答案,但我推荐Donald Stufft在https://caremad.io/2013/07/setup-vs-requirement/上的博客文章来解决这个问题。我用它取得了巨大的成功。

简而言之,requirements.txt不是setup.py的替代品,而是部署的补充。在setup.py中保持包依赖关系的适当抽象。设置requirements.txt或更多的`它们来获取用于开发、测试或生产的包依赖项的特定版本。

例如,包含在deps/下的repo中的包:

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


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

pip执行包的setup.py并安装在install_requires中声明的依赖项的特定版本。没有表里不一,这两件文物的用途都被保留了下来。

当心__abc0行为!

请注意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']

如果你不想强迫你的用户安装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'))
)

上面的大多数其他答案都不能与当前版本的pip API一起使用。下面是使用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
....
)

正确,因为这是当前pip使用parse_requirements的方式。这仍然可能不是最好的方法,因为正如上面的帖子所说,pip并没有真正维护API。

从表面上看,requirements.txtsetup.py似乎是愚蠢的副本,但重要的是要理解,虽然形式相似,但预期的函数是非常不同的。

当指定依赖项时,包作者的目标是说“无论你在哪里安装这个包,为了使这个包工作,这些都是你需要的其他包。”

相反,部署作者(在不同的时间可能是同一个人)有不同的工作,因为他们说“这是我们收集和测试的包的列表,我现在需要安装”。

包作者为各种各样的场景编写程序,因为他们将自己的工作以他们可能不知道的方式使用,并且无法知道将在他们的包旁边安装哪些包。为了成为一个好邻居并避免与其他包的依赖版本冲突,它们需要指定尽可能广泛的依赖版本。这就是setup.py中的install_requires所做的。

部署作者为一个非常不同、非常具体的目标编写:安装在特定计算机上的已安装应用程序或服务的单个实例。为了精确地控制部署,并确保测试和部署了正确的包,部署作者必须指定要安装的每个包的确切版本和源位置,包括依赖项和依赖项的依赖项。有了这个规范,一个部署可以重复地应用到多台机器上,或者在一台测试机器上进行测试,并且部署作者可以确信每次都部署了相同的包。这就是requirements.txt的作用。

因此,您可以看到,虽然它们看起来都像一个大的包和版本列表,但这两个东西具有非常不同的功能。而且这绝对很容易混淆和出错!但正确的思考方式是,requirements.txt是对所有不同setup.py包文件中的需求所提出的“问题”的“答案”。它不是手工编写的,它通常是通过告诉pip查看一组所需包中的所有setup.py文件,找到一组它认为符合所有要求的包,然后在安装后,将该包列表“冻结”到一个文本文件中(这就是pip freeze名称的来源)而生成的。

所以结论是:

  • setup.py应该声明仍然可用的最宽松的依赖版本。它的工作是说明一个特定的包可以使用什么。
  • requirements.txt是定义整个安装作业的部署清单,不应该被认为绑定到任何一个包。它的任务是声明所有必要包的详尽列表,以使部署工作。
  • 因为这两种东西有着如此不同的内容和存在的原因,所以简单地将其一复制到另一个是不可行的。

引用:

我为此创建了一个可重用函数。它实际上解析需求文件的整个目录,并将它们设置为extras_require。

最新版本总是可用:https://gist.github.com/akatrevorjay/293c26fefa24a7b812f5

import glob
import itertools
import os


# This is getting ridiculous
try:
from pip._internal.req import parse_requirements
from pip._internal.network.session import PipSession
except ImportError:
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:


>>> import setuptools
>>> _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=setuptools.find_packages(),
...     zip_safe=False,
...     include_package_data=True,
... )
>>> _conf.update(setup_requirements())
>>> # setuptools.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




__all__ = ['setup_requirements']


if __name__ == '__main__':
reqs = setup_requirements()
print(reqs)

下面是一个基于罗曼的回答的完整hack(用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)

另一个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和binary dist。

正如其他人所说,parse_requirements有几个缺点,所以这不是你应该在公共项目上做的,但它可能足以用于内部/个人项目。

另一个可能的解决方案……

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

以下接口在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.split('#', 1)[0].strip(' \t\n') for line in f
] if s != ''
]

交叉张贴我的答案从这个SO问题另一个简单的,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下放入所有需求。

我不建议做这样的事。正如多次提到的,install_requiresrequirements.txt绝对不应该是同一个列表。但是由于有很多关于皮普的私有内部api的误导性答案,可能值得考虑更理智的替代方案…


没有皮普,可以从setuptools setup.py脚本解析一个相对简单的requirements.txt文件。setuptools项目已经在它的顶级pkg_resources中包含了必要的工具。

它或多或少是这样的:

#!/usr/bin/env python3


import pathlib


import pkg_resources
import setuptools


with pathlib.Path('requirements.txt').open() as requirements_txt:
install_requires = [
str(requirement)
for requirement
in pkg_resources.parse_requirements(requirements_txt)
]


setuptools.setup(
install_requires=install_requires,
)

同样,这只适用于简单的requirements.txt文件。请参阅需求解析pkg_resources的文档页面以获得关于所处理内容的详细信息。简而言之,每一行都应该是有效的PEP 508要求。不支持具体到pip的符号,它将导致失败。


警告之词

如前所述,不建议这样做。requirements.txt文件和“安装依赖项”列表;是两个不同的概念,它们是不可互换的。

但是,如果你确实编写了一个读取requirements.txtsetup.py安装脚本,那么请确保requirements.txt文件包含在“源分发”中。(sdist)否则安装将明显失败。


自从setuptools版本62.6以来,就可以在setup.cfg中写这样的东西:

[options]
install_requires = file: requirements.txt

或者在pyproject.toml中:

[project]
dynamic = ["dependencies"]


[tool.setuptools.dynamic]
dependencies = requirements.txt

上述忠告同样适用:

  • 只支持非常简单的文件
  • 该文件必须添加到sdist . conf文件中

此外,它被认为是一个&;贝塔&;现在的特性。


笔记:

我是这样做的:

import re


def requirements(filename):
with open(filename) as f:
ll = f.read().splitlines()
d = {}
for l in ll:
k, v = re.split(r'==|>=', l)
d[k] = v
return d


def packageInfo():
try:
from pip._internal.operations import freeze
except ImportError:
from pip.operations import freeze


d = {}
for kv in freeze.freeze():
k, v = re.split(r'==|>=', kv)
d[k] = v
return d


req = getpackver('requirements.txt')
pkginfo = packageInfo()


for k, v in req.items():
print(f'{k:<16}: {v:<6} -> {pkginfo[k]}')