在setuptools的setup.py文件的install_requires kwarg参考req

2019-07-18 09:53发布

我有一个requirements.txt我使用与特拉维斯-CI文件。 看来愚蠢复制中都要求requirements.txtsetup.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

Answer 1:

您可以翻转过来,并列出依赖setup.py ,并有一个字符-一个点. -在requirements.txt代替。


另外,即使不建议,仍然可以解析requirements.txt文件与下面的技巧(与测试(如果它不是指通过URL任何外部要求) pip 9.0.1 ):

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

这不过滤环境标志虽然。


在旧版本的点子,更具体年龄大于6.0 ,则存在可用于实现这一公共API。 一个需求文件可以包含注释( # ),并可以包括一些其他文件( --requirement-r )。 因此,如果你真的想解析requirements.txt可以使用画中画解析器:

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
)


Answer 2:

在它面前,它似乎是requirements.txtsetup.py是愚蠢的重复,而是要明白,虽然形式是相似的,预定的功能有很大的不同是很重要的。

一个包的作者的目标,指定的依赖的时候,是说“只要你安装这个包,这些都是你需要的其他包,为了这个包的工作。”

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

该软件包作者写道用于各种各样的场景,因为他们把自己的工作,在那里,他们中可能不知道的方式使用,并没有知道什么包将沿着他们的包安装的方式。 为了成为一个好邻居,并避免与其他包的依赖版本冲突,他们需要指定宽范围的依赖版本的都不可能工作的。 这是install_requiressetup.py一样。

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

所以你可以看到,虽然他们都像包和版本的大名单中,这两个东西有非常不同的工作。 而这绝对是容易混淆这件事,并把它错了! 但是想想这个正确的方法是, requirements.txt是一个“答案”在所有不同的要求提出的“问题” setup.py包文件。 而不是手工写它,它经常被告知点子来看待所有生成的setup.py文件在一组所需的包,找到了一套自认为包适合所有的要求,然后,正在安装后,“冻结”的包到一个文本文件中的该列表(这是那里的pip freeze名字来自)。

所以外卖:

  • setup.py应申报仍在可行的宽松可能依赖版本。 它的任务是说什么特定的软件包可以工作。
  • requirements.txt是一个部署清单,它定义了整个安装工作,而不应被认为是依赖于任何一个包。 它的任务是宣布所有必需的包的详尽清单,使部署工作。
  • 因为这两件事情有如此不同的内容和理由现有的,这是不可行简单地复制到彼此。

参考文献:

  • install_requires VS文件要求从Python包装用户指南。


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


Answer 4:

要求文件使用扩展PIP格式,如果你需要补充你这是唯一有用的setup.py具有更强的约束,例如指定的确切网址一些依赖关系必须来自或输出pip freeze ,冻结整个包设置为正常工作的版本。 如果你不需要额外的限制,只能使用一个setup.py 。 如果你觉得你真的需要推出一款requirements.txt反正,你可以把它一行:

.

这将是有效的,准确指的是内容setup.py是在同一个目录。



Answer 5:

虽然没有一个确切的答案的问题,我建议唐纳德Stufft的在博客文章https://caremad.io/2013/07/setup-vs-requirement/为好取这个问题。 我一直在使用它取得了巨大成功。

总之, requirements.txt不是setup.py选择,但部署的补充。 保持软件包的依赖关系的适当抽象的setup.py 。 设置requirements.txt以上“时间的获取用于开发,测试或生产软件包的依赖关系的特定版本。

例如,使用包列在第回购deps/

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

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

PIP封装执行的setup.py并安装在声明的依赖性的特定版本install_requires 。 有没有口是心非两者文物的目的被保留。



Answer 6:

大多数其他的答案以上不符合PIP的API的当前版本。 这是正确的方式*与当前版本的点子来做到这一点(在写作,在7.1.2还曾时间6.0.8。您可以检查与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。



Answer 7:

使用parse_requirements是有问题的,因为PIP API不公开记录和支持。 在PIP 1.6,该功能实际上是移动的,所以它的现有用途很可能打破。

消除之间的重复更可靠的方式setup.pyrequirements.txt是在特定的依赖setup.py ,然后把-e . 到您的requirements.txt文件。 从一个有些信息pip为什么这是一个更好的方式去开发人员可以在这里找到: https://caremad.io/blog/setup-vs-requirement/



Answer 8:

安装当前包特拉维斯。 这避免了使用的requirements.txt文件。 例如:

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


Answer 9:

如果您不想强制用户安装点子,你可以模仿它的这种行为:

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


Answer 10:

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


Answer 11:

谨防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']


Answer 12:

下面的接口被废弃在点子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 != ''
    ]


Answer 13:

我创建了一个可重复使用的功能这一点。 它实际上解析的需求文件的整个目录,将其设为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


Answer 14:

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

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


Answer 15:

这个简单的方法读取要求从文件setup.py 。 这是答案的变化Dmitiry S. 。 这个答案只有与Python 3.6或更高版本兼容。

每DS , 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'),
   ...)

按我的经验,你显然不需要采取任何特步在需求文件捆绑。



Answer 16:

另一个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有几个缺点,所以这不是你应该在公共项目做什么,但它可能就足够了内部/个人项目。



Answer 17:

下面是一个完整的黑客(与测试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)


文章来源: Reference requirements.txt for the install_requires kwarg in setuptools setup.py file