参考 setuptools setup.py 文件中 install_requires kwarg 的 requirements.txt
- 2025-03-18 08:55:00
- admin 原创
- 43
问题描述:
我有一个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:
从表面上看,requirements.txt
和似乎setup.py
是愚蠢的重复,但重要的是要理解,虽然形式相似,但预期功能却非常不同。
在指定依赖项时,软件包作者的目标是说“无论您在何处安装此软件包,这些都是您需要的其他软件包,以便此软件包能够正常工作”。
相比之下,部署作者(可能是不同时间的同一个人)的工作有所不同,他们会说“这是我们收集并测试过的、我现在需要安装的软件包列表”。
软件包作者会针对各种各样的场景编写代码,因为他们将自己的作品发布出来,供别人以他们可能不知道的方式使用,而且他们无法知道哪些软件包会与他们的软件包一起安装。为了成为一个好邻居,并避免与其他软件包发生依赖版本冲突,他们需要尽可能广泛地指定依赖版本。这就是install_requires
insetup.py
所做的。
部署作者编写的部署包目标非常不同,也非常具体:在特定计算机上安装已安装应用程序或服务的单个实例。为了精确控制部署,并确保测试和部署正确的包,部署作者必须指定要安装的每个包的确切版本和源位置,包括依赖项和依赖项的依赖项。使用此规范,部署包可以重复应用于多台计算机,或在测试计算机上进行测试,并且部署作者可以确信每次部署的都是相同的包。这就是 a 的作用requirements.txt
。
因此,您可以看到,虽然它们看起来都像是一个很大的软件包和版本列表,但这两个东西的作用却截然不同。而且很容易混淆并弄错!但正确的思考方式是,它requirements.txt
是对所有各种软件包文件中的要求所提出的“问题”的“答案” setup.py
。它不是手写,而是通常通过告诉 pip 查看setup.py
一组所需软件包中的所有文件,找到一组它认为符合所有要求的软件包,然后在安装它们之后,将该软件包列表“冻结”到一个文本文件中(这就是名称的由来pip freeze
)。
因此,结论是:
setup.py
应声明尽可能宽松的依赖版本,但这些版本仍然可行。它的作用是说明特定包可以与什么一起工作。requirements.txt
是定义整个安装作业的部署清单,不应将其视为与任何一个包绑定。它的作用是声明使部署工作所需的所有必要包的详尽列表。由于这两件事的内容和存在的理由如此不同,因此简单地将其中一件事照搬到另一件事上是不可行的。
参考:
install_requires 与Python 打包用户指南中的 Requirements 文件。
解决方案 2:
更新12/2024:
这在较新版本的 Python 中不起作用,主要是因为该函数已在pip
模块中移动。无论如何,正如库维护者在下面所述,不建议导入和使用此函数,因为它是一个内部函数,可能会更改和移动。
旧答案:
您可以将其翻转并列出依赖项,setup.py
并使用单个字符(一个点.
)来requirements.txt
代替。
或者,即使没有建议,仍然可以requirements.txt
使用以下技巧(已用 测试)来解析文件(如果它没有通过 URL 引用任何外部要求)pip 9.0.1
:
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
)
解决方案 3:
它不能接受文件句柄。install_requires
参数只能是字符串或字符串列表。
当然,您可以读取安装脚本中的文件并将其作为字符串列表传递给install_requires
。
import os
from setuptools import setup
with open('requirements.txt') as f:
required = f.read().splitlines()
setup(...
install_requires=required,
...)
解决方案 4:
需求文件使用扩展的 pip 格式,这仅在您需要setup.py
用更强的约束来补充时才有用,例如指定某些依赖项必须来自的确切 URL,或pip freeze
将整个包集冻结为已知工作的版本的输出。如果您不需要额外的约束,请仅使用setup.py
。如果您觉得无论如何都需要发送requirements.txt
,您可以将其设为一行:
.
它将有效并且准确引用setup.py
同一目录中的内容。
解决方案 5:
虽然这不是问题的确切答案,但我建议阅读 Donald Stufft 的博客文章https://caremad.io/2013/07/setup-vs-requirement/,以便更好地解决这个问题。我一直在使用它,取得了巨大的成功。
简而言之,requirements.txt
不是setup.py
替代方案,而是部署补充。 在 中保留适当的包依赖项抽象setup.py
。 设置一个requirements.txt
或多个 'em 来获取特定版本的包依赖项,以用于开发、测试或生产。
例如,repo 中包含的软件包如下deps/
:
# fetch specific dependencies
--no-index
--find-links deps/
# install package
# NOTE: -e . for editable mode
.
pip 执行包setup.py
并安装 中声明的依赖项的特定版本install_requires
。没有重复,并且两个工件的用途都得到保留。
解决方案 6:
首先,我认为解析requirements.txt
以填充包元数据中的依赖项列表不是一个好主意。requirements.txt
文件和“安装依赖项”列表是两个不同的概念,它们不能互换。应该反过来,包元数据中的依赖项列表应该被视为某种真实来源,并且requirements.txt
应该从那里生成诸如这样的文件。例如使用诸如这样的工具pip-compile
。请参阅此答案底部的注释。
但每个人的需求不同,这会导致不同的工作流程。因此...有 3 种方法可以处理此问题,具体取决于您希望将项目的包元数据写入何处:pyproject.toml
、setup.cfg
或setup.py
。
小心!
如果您坚持要从文件中读取包元数据中的依赖项列表,requirements.txt
那么请确保该requirements.txt
文件包含在“源分发”(sdist)中,否则安装将失败,原因很明显。
这些技术仅适用于简单requirements.txt
文件。请参阅文档页面中的需求解析pkg_resources
,了解所处理内容的详细信息。简而言之,每一行都应该是有效的PEP 508要求。真正特定于pip 的符号不受支持,这将导致失败。
pyproject.toml
[project]
# ...
dynamic = ["dependencies"]
[tool.setuptools.dynamic]
# ...
dependencies = requirements.txt
setup.cfg
从 setuptools 62.6 版本开始,可以写入如下内容setup.cfg
:
[options]
install_requires = file: requirements.txt
setup.py
无需pip即可requirements.txt
从setuptools 脚本解析相对简单的文件。setuptools项目已在其顶级包中包含必要的工具。setup.py
`pkg_resources`
它或多或少看起来像这样:
#!/usr/bin/env python
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,
)
注意:
https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/
https://github.com/pypa/setuptools/issues/1951#issuecomment-1431345869
另请参阅其他答案:https://stackoverflow.com/a/59971236
解决方案 7:
使用parse_requirements
存在问题,因为 pip API 没有公开记录和支持。在 pip 1.6 中,该功能实际上正在移动,因此现有的使用可能会中断。
setup.py
消除和之间的重复的更可靠方法requirements.txt
是指定依赖项setup.py
,然后将其-e .
放入requirements.txt
文件中。一位pip
开发人员提供了一些关于为什么这是更好方法的信息,可在此处找到:https://caremad.io/blog/setup-vs-requirement/
解决方案 8:
上面的大多数其他答案不适用于当前版本的 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。
解决方案 9:
在 Travis 中安装当前包。这样可以避免使用requirements.txt
文件。例如:
language: python
python:
- "2.7"
- "2.6"
install:
- pip install -q -e .
script:
- python runtests.py
解决方案 10:
from pip.req import parse_requirements
对我来说不起作用,我认为它适用于我的 requirements.txt 中的空白行,但这个函数确实有效
def parse_requirements(requirements):
with open(requirements) as f:
return [l.strip('
') for l in f if l.strip('
') and not l.startswith('#')]
reqs = parse_requirements(<requirements_path>)
setup(
...
install_requires=reqs,
...
)
解决方案 11:
以下接口在 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('
') for line in f
] if s != ''
]
解决方案 12:
注意行为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']
解决方案 13:
如果您不想强迫用户安装 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'))
)
解决方案 14:
我为此创建了一个可重复使用的函数。它实际上解析了整个目录的需求文件并将它们设置为 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)
解决方案 15:
另一个可能的解决方案...
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,
...
)
解决方案 16:
对我有用的解决方法
警告:仅在您尚未使用时使用setup.cfg
!!! 这将覆盖setup.cfg
保持
requirements.txt
与requirements-dev.txt
同一水平setup.py
删除以下行
setup.py
install_requires=...
extras_require=...
将其添加到从文件
setup.py
创建和加载需求 -以及setup.cfg
`requirements.txt`requirements-dev.txt
# Read the dependencies from requirements.txt
try:
with open("requirements.txt", "r", encoding="utf8") as f:
requirements = f.read().splitlines()
requirements = [
f" {req.strip()}
" for req in requirements if req.strip()[0] != "#"
]
except FileNotFoundError:
requirements = []
finally:
if requirements:
requirements = ["[options]
", "install_requires =
"] + requirements
with open("setup.cfg", "w", encoding="utf8") as cfg:
cfg.writelines(requirements)
# Read the extra dependencies from requirements_extra.txt
try:
with open("requirements-dev.txt", "r", encoding="utf8") as f:
dev_requirements = f.read().splitlines()
dev_requirements = [
f" {req.strip()}
" for req in dev_requirements if req.strip()[0] != "#"
]
except FileNotFoundError:
dev_requirements = []
finally:
if dev_requirements:
dev_requirements = [
"
[options.extras_require]
",
"dev =
",
] + dev_requirements
with open("setup.cfg", "a", encoding="utf8") as cfg:
cfg.writelines(dev_requirements)
setup.cfg
运行后应该看起来像这样python setup.py sdist
[options]
install_requires =
package_1
...
[options.extras_require]
dev =
package_2
...
解决方案 17:
交叉发布我对这个 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
项目根目录下即可。
解决方案 18:
我这样做了:
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]}')
解决方案 19:
还有一种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
它有几个缺点,所以这不是你应该在公共项目上做的,但它可能足以满足内部/个人项目的要求。
解决方案 20:
这里是一个完整的黑客攻击(用测试pip 9.0.1
),基于Romain的答案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)
扫码咨询,免费领取项目管理大礼包!