参考 setuptools setup.py 文件中 install_requires kwarg 的 requirements.txt

2025-03-18 08:55:00
admin
原创
44
摘要:问题描述:我有一个requirements.txt与 Travis-CI 一起使用的文件。在和中重复要求似乎很愚蠢requirements.txt,setup.py所以我希望将文件句柄传递给install_requires中的 kwarg setuptools.setup。这可能吗?如果可以,我该怎么做?这是...

问题描述:

我有一个requirements.txt与 Travis-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

解决方案 1:

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

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

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

软件包作者会针对各种各样的场景编写代码,因为他们将自己的作品发布出来,供别人以他们可能不知道的方式使用,而且他们无法知道哪些软件包会与他们的软件包一起安装。为了成为一个好邻居,并避免与其他软件包发生依赖版本冲突,他们需要尽可能广泛地指定依赖版本。这就是install_requiresinsetup.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.tomlsetup.cfgsetup.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.txtsetuptools 脚本解析相对简单的文件。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,
)

注意

解决方案 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

  1. 保持requirements.txtrequirements-dev.txt同一水平setup.py

  2. 删除以下行setup.py

install_requires=...
extras_require=...
  1. 将其添加到从文件 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_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 和二进制 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)
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2482  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1533  
  PLM(产品生命周期管理)项目对于企业优化产品研发流程、提升产品质量以及增强市场竞争力具有至关重要的意义。然而,在项目推进过程中,范围蔓延是一个常见且棘手的问题,它可能导致项目进度延迟、成本超支以及质量下降等一系列不良后果。因此,有效避免PLM项目范围蔓延成为项目成功的关键因素之一。以下将详细阐述三大管控策略,助力企业...
plm系统   0  
  PLM(产品生命周期管理)项目管理在企业产品研发与管理过程中扮演着至关重要的角色。随着市场竞争的加剧和产品复杂度的提升,PLM项目面临着诸多风险。准确量化风险优先级并采取有效措施应对,是确保项目成功的关键。五维评估矩阵作为一种有效的风险评估工具,能帮助项目管理者全面、系统地评估风险,为决策提供有力支持。五维评估矩阵概述...
免费plm软件   0  
  引言PLM(产品生命周期管理)开发流程对于企业产品的全生命周期管控至关重要。它涵盖了从产品概念设计到退役的各个阶段,直接影响着产品质量、开发周期以及企业的市场竞争力。在当今快速发展的科技环境下,客户对产品质量的要求日益提高,市场竞争也愈发激烈,这就使得优化PLM开发流程成为企业的必然选择。缺陷管理工具和六西格玛方法作为...
plm产品全生命周期管理   0  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用