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

时间:2013-01-18 13:00:14

标签: python pip setuptools requirements.txt

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

20 个答案:

答案 0 :(得分:213)

您可以翻转它并在setup.py中列出依赖项,并在.中包含一个字符 - 一个点requirements.txt


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

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

但这并不会过滤environment markers


在旧版本的pip中,更具体地说是older than 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
)

答案 1 :(得分:137)

从表面上看,似乎requirements.txtsetup.py似乎是愚蠢的重复,但重要的是要理解虽然形式相似,但预期的功能却大相径庭

包装作者在指定依赖关系时的目标是说&#34;无论您在何处安装此包,这些都是您需要的其他包,以便此包能够正常工作。&#34;

相比之下,部署作者(可能是不同时间的同一个人)有不同的工作,因为他们说&#34;这里是我们收集的包裹列表一起测试,我现在需要安装&#34;。

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

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

所以你可以看到,虽然它们看起来像是一个很大的包和版本列表,但这两个东西的工作却截然不同。并且很容易将其混淆并弄错!但正确的思考方式是requirements.txt是一个回答&#34;问题&#34;问题&#34;由所有各种setup.py包文件中的要求构成。不是手工编写,而是通常通过告诉pip查看一组所需包中的所有setup.py文件,找到它认为符合所有要求的一组包,然后通过,在他们安装完毕后,&#34;冻结&#34;包列表到文本文件中(这是pip freeze名称的来源)。

所以外卖:

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

参考文献:

答案 2 :(得分:83)

它无法获取文件句柄。 install_requires参数可以only be a string or a list of strings

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

import os
from setuptools import setup

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

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

答案 3 :(得分:61)

需求文件使用扩展的pip格式,仅当您需要使用更强的约束补充setup.py时才有用,例如指定某些依赖项必须来自的确切URL,或{{}的输出1}}将整个包集冻结为已知工作版本。如果您不需要额外的约束,请仅使用pip freeze。如果您觉得确实需要发送setup.py,那么您可以将其设为一行:

requirements.txt

它将有效并完全引用同一目录中. 的内容。

答案 4 :(得分:37)

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

简而言之,requirements.txt不是setup.py替代,而是部署补充。在setup.py中保持对包依赖关系的适当抽象。设置requirements.txt或更多的&em; em以获取开发,测试或生产的特定版本的软件包依赖项。

E.g。包含在deps/下的仓库中的包:

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

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

pip执行package setup.py并安装install_requires中声明的特定版本的依赖项。没有两面性,两个工件的目的都得以保留。

答案 5 :(得分: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。

答案 6 :(得分:18)

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

消除setup.pyrequirements.txt之间重复的更可靠方法是在setup.py中具体说明您的依赖关系,然后将-e .放入requirements.txt文件中。其中一位pip开发人员提供了有关为什么这是一种更好的方法的信息,请访问:https://caremad.io/blog/setup-vs-requirement/

答案 7 :(得分:14)

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

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

答案 8 :(得分:5)

如果您不想强制用户安装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'))
)

答案 9 :(得分: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,
    ...
)

答案 10 :(得分:3)

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

答案 11 :(得分:2)

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

答案 12 :(得分:2)

以下界面在第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 != ''
    ]

答案 13 :(得分:1)

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

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

答案 14 :(得分:1)

我不建议您这样做。正如多次提到的,install_requiresrequirements.txt绝对不应该是同一列表。但是,由于围绕着 pip 的私有内部API的答案有很多误导性,因此可能值得寻找更明智的选择...

不需要 pip setuptools requirements.txt脚本中解析setup.py文件。 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,
)

注释

答案 15 :(得分:0)

这个简单的方法从setup.py读取需求文件。它是Dmitiry S.的答案的变体。这个答案只与Python 3.6 +兼容。

D.S.requirements.txt可以记录具体版本号的具体要求,而setup.py可以记录宽松版本范围的抽象要求。

以下是我setup.py的摘录。

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/
    requirements = (Path(__file__).parent / filename).read_text().strip().split('\n')
    requirements = [r.strip() for r in requirements]
    requirements = [r for r in sorted(requirements) if r and not r.startswith('#')]
    return requirements

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

根据我的经验,您显然不需要采取任何特殊步骤来捆绑需求文件。

答案 16 :(得分:0)

将我的答案从this SO question交叉发布到另一个简单的pip版本证明解决方案。

   tag.span class:'btn p-0',
          'data-toggle':"tool-tip", 'data-placement': "top", 'title': "Your limit was reached" do
    tag.button  id: "save-search-btn-disabled",
                class: "btn btn-outline-primary disabled",
                disabled: true,
                style: "display: none" do
      "Save Search"
    end
   end

然后将所有需求都放在项目根目录下的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 requriements], ... ) 下。

答案 17 :(得分:-1)

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

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

答案 18 :(得分:-2)

以下是基于Romain's answer的完整黑客攻击(使用pip 9.0.1进行测试),可解析requirements.txt并根据当前environment markers对其进行过滤:

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)

答案 19 :(得分:-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]}')