参考要求.txt在安装程序工具 setup.py 文件中install_requires kwarg



我有一个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

从表面上看,requirements.txtsetup.py似乎是愚蠢的重复,但重要的是要明白,虽然形式相似,但预期的功能却大不相同。

在指定依赖项时,包作者的目标是说"无论你在哪里安装这个包,这些是你需要的其他包,以便这个包工作。

相比之下,部署作者(可能是不同时间的同一个人(有不同的工作,因为他们说"这是我们收集并测试的软件包列表,我现在需要安装"。

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

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

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

所以要点:

  • setup.py应该声明仍然可行的最松散的依赖项版本。 它的工作是说一个特定的包可以使用什么。
  • requirements.txt是定义整个安装作业的部署清单,不应将其视为绑定到任何一个包。 它的工作是声明使部署工作所需的所有软件包的详尽列表。
  • 因为这两件事的内容和存在的理由如此不同,所以简单地将一个复制到另一个是不可行的。

引用:

  • Python 打包用户指南中的install_requires与要求文件。

您可以翻转它并在setup.py中列出依赖项,并在requirements.txt中使用单个字符(点.(。


或者,即使不建议,仍然可以使用以下技巧(使用 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
)

它不能采用文件句柄。install_requires参数只能是一个字符串或字符串列表。

当然,您可以在安装脚本中读取文件,并将其作为字符串列表传递给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的内容。

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

简而言之,requirements.txt不是setup.py替代方案,而是部署补充。在 setup.py 中保留包依赖项的适当抽象。设置 requirements.txt 个或更多 'em 以获取特定版本的包依赖项以进行开发、测试或生产。

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

# fetch specific dependencies
--no-index
--find-links deps/
# install package
# NOTE: -e . for editable mode
.

pip 执行包的setup.py并安装install_requires中声明的特定版本的依赖项。没有重复性,两个文物的目的都被保留了下来。

首先,我认为解析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

可以从安装程序工具setup.py脚本中解析相对简单的requirements.txt文件,而无需 pipsetuptools 项目已在其顶级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://caremad.io/posts/2013/07/setup-vs-requirement/
  • https://setuptools.pypa.io/en/latest/history.html#v62-6-0
  • 另请参阅另一个答案:https://stackoverflow.com/a/59971236

使用 parse_requirements 是有问题的,因为 pip API 没有公开记录和支持。 在 pip 1.6 中,该函数实际上是在移动的,因此它的现有使用可能会中断。

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

上面的大多数其他答案都不适用于当前版本的 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
    ....
)

* 正确,因为这是将parse_requirements与当前点一起使用的方式。它可能仍然不是最好的方法,因为正如上面的海报所说,pip 并没有真正维护 API。

在 Travis 中安装当前软件包。这样可以避免使用 requirements.txt 文件。例如:

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py
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,
    ...
)

以下接口在 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(' tn') for line in f
        ] if s != ''
    ]

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']
<</div> div class="one_answers">

如果你不想强迫你的用户安装 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'))
)
<</div> div class="one_answers">

我为此创建了一个可重用的函数。它实际上解析了需求文件的整个目录,并将它们设置为 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)

另一种可能的解决方案...

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

交叉发布我从这个 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下提出所有要求即可。

另一个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 和二进制 dist。

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

我这样做了:

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]}')

这是一个基于 Romain 答案的完整黑客(用 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)

最新更新