373

我有一个requirements.txt与 Travis-CI 一起使用的文件。requirements.txt复制and中的要求似乎很愚蠢setup.py,因此我希望将文件句柄传递install_requiressetuptools.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
4

20 回答 20

305

You can flip it around and list the dependencies in setup.py and have a single character — a dot . — in requirements.txt instead.


Alternatively, even if not advised, it is still possible to parse the requirements.txt file (if it doesn't refer any external requirements by URL) with the following hack (tested with pip 9.0.1):

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

This doesn't filter environment markers though.


In old versions of pip, more specifically older than 6.0, there is a public API that can be used to achieve this. A requirement file can contain comments (#) and can include some other files (--requirement or -r). Thus, if you really want to parse a requirements.txt you can use the pip parser:

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
)
于 2013-05-18T13:18:24.863 回答
294

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

包作者在指定依赖项时的目标是说“无论您在何处安装此包,这些都是您需要的其他包,以使该包正常工作。”

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

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

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

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

所以外卖:

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

参考:

于 2015-11-13T04:21:24.610 回答
125

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

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

import os
from setuptools import setup

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

setup(...
install_requires=required,
...)
于 2013-01-18T13:14:47.607 回答
66

需求文件使用扩展的 pip 格式,这仅在您需要setup.py用更强的约束补充您的情况下才有用,例如指定某些依赖项必须来自的确切 url,或者将pip freeze整个包集冻结为已知工作的输出版本。如果您不需要额外的约束,请仅使用setup.py. 如果你觉得你真的需要运送一个requirements.txt,你可以把它写成一行:

.

它将是有效的,并且完全引用setup.py同一目录中的内容。

于 2013-09-29T17:48:42.143 回答
39

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

总之,requirements.txt不是setup.py替代品,而是部署的补充。在setup.py. 设置requirements.txt或更多它们以获取特定版本的包依赖项以用于开发、测试或生产。

例如,包含在 repo 中的软件包deps/

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

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

pip 执行包setup.py并安装在install_requires. 没有重复性,并且保留了两个工件的目的。

于 2014-04-06T18:02:07.993 回答
22

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

消除setup.py和之间重复的一种更可靠的方法requirements.txt是指定您的依赖项setup.py,然后放入-e .您的requirements.txt文件中。pip此处提供了一位开发人员关于为什么这是一种更好的方法的一些信息: https ://caremad.io/blog/setup-vs-requirement/

于 2014-03-26T01:31:31.973 回答
19

上面的大多数其他答案都不适用于当前版本的 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 一起使用的方式。它仍然可能不是最好的方法,因为正如上面的海报所说,pip 并没有真正维护 API。

于 2015-04-15T16:33:09.957 回答
13

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

language: python
python:
  - "2.7"
  - "2.6"
install:
  - pip install -q -e .
script:
  - python runtests.py
于 2013-08-21T16:23:17.003 回答
10

我不建议做这样的事情。正如多次提到的install_requiresrequirements.txt绝对不应该是同一个列表。但是,由于涉及pip的私有内部 API 周围有很多误导性的答案,因此可能值得寻找更明智的替代方案......

pip不需要从setuptools脚本解析requirements.txt文件。setuptools项目已经在其顶层包含了所有必要的工具。 setup.pypkg_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安装依赖项”列表是两个不同的概念,不能互换。

但是,如果您确实编写了一个setup.py读取的安装脚本,请requirements.txt确保该requirements.txt文件包含在“源分发”(sdist)中,否则安装显然会失败。


备注

于 2020-01-29T16:25:41.980 回答
6

这种简单的方法从setup.py. 这是Dmitiry S.答案的变体。此答案仅与 Python 3.6+ 兼容。

根据DSrequirements.txt可以记录具有特定版本号的具体要求,而setup.py可以记录具有松散版本范围的抽象要求。

下面是我的摘录setup.py

import distutils.text_file
from pathlib import Path
from typing import List

def _parse_requirements(filename: str) -> List[str]:
    """Return requirements from requirements file."""
    # Ref: https://stackoverflow.com/a/42033122/
    return distutils.text_file.TextFile(filename=str(Path(__file__).with_name(filename))).readlines()

setup(...
      install_requires=_parse_requirements('requirements.txt'),
   ...)

请注意,这distutils.text_file.TextFile将删除评论。此外,根据我的经验,您显然不需要采取任何特殊步骤来捆绑需求文件。

于 2017-02-03T21:15:40.487 回答
5

以下接口在 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 != ''
    ]
于 2018-05-16T10:28:31.243 回答
4

如果你不想强迫你的用户安装 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'))
)
于 2014-08-08T10:33:52.750 回答
4

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,
    ...
)
于 2013-11-08T19:18:45.327 回答
2

小心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']
于 2014-07-26T18:44:20.577 回答
1

我为此创建了一个可重用的函数。它实际上解析了需求文件的整个目录并将它们设置为 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)
于 2015-12-23T02:12:47.227 回答
0

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

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,
    ...
)
于 2017-07-20T19:12:41.007 回答
-1

交叉发布我从这个 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在项目根目录下输入您的所有要求。

于 2019-07-24T21:53:49.710 回答
-2

另一个parse_requirementshack 也将环境标记解析为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有几个缺点,所以这不是你应该在公共项目上做的,但它可能足以满足内部/个人项目。

于 2016-12-15T19:42:19.937 回答
-2

我这样做了:

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]}')
于 2020-03-24T03:38:46.483 回答
-4

pip 9.0.1这是基于Romain 的答案的完整 hack(用 测试),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)
于 2016-11-10T13:24:56.410 回答