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

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

I have a requirements.txt file that I’m using with Travis-CI. It seems silly to duplicate the requirements in both requirements.txt and setup.py, so I was hoping to pass a file handle to the install_requires kwarg in setuptools.setup.

Is this possible? If so, how should I go about doing it?

Here is my requirements.txt file:

guessit>=0.5.2
tvdb_api>=1.8.2
hachoir-metadata>=1.3.3
hachoir-core>=1.3.3
hachoir-parser>=1.3.4

回答 0

你可以翻转它周围,列表中的依赖关系setup.py,并有单字符-点.-在requirements.txt代替。


另外,即使不建议使用,也可以requirements.txt通过以下技巧(通过进行测试pip 9.0.1)来分析文件(如果它没有通过URL引用任何外部要求):

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

但是,这不会过滤环境标记


在pip的旧版本中(更具体地讲,版本早于6.0),可以使用公共API来实现此目的。需求文件可以包含注释(#),也可以包含其他一些文件(--requirement-r)。因此,如果您确实要解析a 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
)

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
)

回答 1

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

程序包作者在指定依赖项时的目的是说:“无论您在何处安装此程序包,都需要其他程序包,此程序包才能工作。”

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

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

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

因此,您可以看到,尽管它们看起来都像是一个很大的软件包和版本列表,但是这两件事的工作却截然不同。而且混合起来并弄错它肯定很容易!但是考虑这一点的正确方法是,requirements.txt对所有各种setup.py软件包文件中的需求提出的“问题”进行“解答” 。而不是手工编写,它通常是通过告诉pip查看setup.py一组所需软件包中的所有文件,找到它认为符合所有要求的一组软件包,然后在安装后冻结来生成的。 ”将软件包列表转换为文本文件(这就是pip freeze名称的来源)。

因此,外卖:

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

参考文献:

On the face of it, it does seem that requirements.txt and setup.py are silly duplicates, but it’s important to understand that while the form is similar, the intended function is very different.

The goal of a package author, when specifying dependencies, is to say “wherever you install this package, these are the other packages you need, in order for this package to work.”

In contrast, the deployment author (which may be the same person at a different time) has a different job, in that they say “here’s the list of packages that we’ve gathered together and tested and that I now need to install”.

The package author writes for a wide variety of scenarios, because they’re putting their work out there to be used in ways they may not know about, and have no way of knowing what packages will be installed alongside their package. In order to be a good neighbor and avoid dependency version conflicts with other packages, they need to specify as wide a range of dependency versions as can possibly work. This is what install_requires in setup.py does.

The deployment author writes for a very different, very specific goal: a single instance of an installed application or service, installed on a particular computer. In order to precisely control a deployment, and be sure that the right packages are tested and deployed, the deployment author must specify the exact version and source-location of every package to be installed, including dependencies and dependency’s dependencies. With this spec, a deployment can be repeatably applied to several machines, or tested on a test machine, and the deployment author can be confident that the same packages are deployed every time. This is what a requirements.txt does.

So you can see that, while they both look like a big list of packages and versions, these two things have very different jobs. And it’s definitely easy to mix this up and get it wrong! But the right way to think about this is that requirements.txt is an “answer” to the “question” posed by the requirements in all the various setup.py package files. Rather than write it by hand, it’s often generated by telling pip to look at all the setup.py files in a set of desired packages, find a set of packages that it thinks fits all the requirements, and then, after they’re installed, “freeze” that list of packages into a text file (this is where the pip freeze name comes from).

So the takeaway:

  • setup.py should declare the loosest possible dependency versions that are still workable. Its job is to say what a particular package can work with.
  • requirements.txt is a deployment manifest that defines an entire installation job, and shouldn’t be thought of as tied to any one package. Its job is to declare an exhaustive list of all the necessary packages to make a deployment work.
  • Because these two things have such different content and reasons for existing, it’s not feasible to simply copy one into the other.

References:


回答 2

它不能使用文件句柄。该install_requires参数可以仅仅是一个字符串或字符串列表

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

import os
from setuptools import setup

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

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

It can’t take a file handle. The install_requires argument can only be a string or a list of strings.

You can, of course, read your file in the setup script and pass it as a list of strings to install_requires.

import os
from setuptools import setup

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

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

回答 3

需求文件使用扩展的pip格式,仅当您需要setup.py用更严格的约束来补充时(例如,指定某些依赖项必须来自的确切网址,或pip freeze将整个包冻结为已知工作的输出),该文件才有用版本。如果您不需要额外的约束,请仅使用setup.py。如果您觉得确实需要运送requirements.txt,可以将其放在一行中:

.

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

Requirements files use an expanded pip format, which is only useful if you need to complement your setup.py with stronger constraints, for example specifying the exact urls some of the dependencies must come from, or the output of pip freeze to freeze the entire package set to known-working versions. If you don’t need the extra constraints, use only a setup.py. If you feel like you really need to ship a requirements.txt anyway, you can make it a single line:

.

It will be valid and refer exactly to the contents of the setup.py that is in the same directory.


回答 4

虽然不是该问题的确切答案,但我还是推荐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。没有重复性,并且保留了两个工件的目的。

While not an exact answer to the question, I recommend Donald Stufft’s blog post at https://caremad.io/2013/07/setup-vs-requirement/ for a good take on this problem. I’ve been using it to great success.

In short, requirements.txt is not a setup.py alternative, but a deployment complement. Keep an appropriate abstraction of package dependencies in setup.py. Set requirements.txt or more of ’em to fetch specific versions of package dependencies for development, testing, or production.

E.g. with packages included in the repo under deps/:

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

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

pip executes package’s setup.py and installs the specific versions of dependencies declared in install_requires. There’s no duplicity and the purpose of both artifacts is preserved.


回答 5

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

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

Using parse_requirements is problematic because the pip API isn’t publicly documented and supported. In pip 1.6, that function is actually moving, so existing uses of it are likely to break.

A more reliable way to eliminate duplication between setup.py and requirements.txt is to specific your dependencies in setup.py and then put -e . into your requirements.txt file. Some information from one of the pip developers about why that’s a better way to go is available here: https://caremad.io/blog/setup-vs-requirement/


回答 6

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

Most of the other answers above don’t work with the current version of pip’s API. Here is the correct* way to do it with the current version of pip (6.0.8 at the time of writing, also worked in 7.1.2. You can check your version with 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
    ....
)

* Correct, in that it is the way to use parse_requirements with the current pip. It still probably isn’t the best way to do it, since, as posters above said, pip doesn’t really maintain an API.


回答 7

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

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

Install the current package in Travis. This avoids the use of a requirements.txt file. For example:

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

回答 8

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

from pip.req import parse_requirements did not work for me and I think it’s for the blank lines in my requirements.txt, but this function does work

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

回答 9

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

If you don’t want to force your users to install pip, you can emulate its behavior with this:

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

回答 10

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

The following interface became deprecated in pip 10:

from pip.req import parse_requirements
from pip.download import PipSession

So I switched it just to simple text parsing:

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 != ''
    ]

回答 11

这种简单的方法从中读取需求文件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将删除注释。而且,根据我的经验,您显然不需要采取任何特殊步骤将需求文件捆绑在一起。

This simple approach reads the requirements file from setup.py. It is a variation of the answer by Dmitiry S.. This answer is compatible only with Python 3.6+.

Per D.S., requirements.txt can document concrete requirements with specific version numbers, whereas setup.py can document abstract requirements with loose version ranges.

Below is an excerpt of my 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'),
   ...)

Note that distutils.text_file.TextFile will strip comments. Also, per my experience, you apparently do not need to take any special step to bundle in the requirements file.


回答 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']

BEWARE OF parse_requirements BEHAVIOUR!

Please note that pip.req.parse_requirements will change underscores to dashes. This was enraging me for a few days before I discovered it. Example demonstrating:

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

produces

['example-with-underscores', 'example-with-dashes']

回答 13

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

I created a reusable function for this. It actually parses an entire directory of requirements files and sets them to extras_require.

Latest always available here: 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)

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

Another possible solution…

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

and then to use…

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

回答 15

我不建议这样做。正如多次提到的install_requiresrequirements.txt绝对不应该是同一列表。但是,由于涉及pip的专用内部API的周围存在许多误导性答案,因此可能值得寻找更明智的选择…

不需要piprequirements.txtsetuptools 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,
)

I would not recommend doing such a thing. As mentioned multiple times install_requires and requirements.txt are definitely not supposed to be the same list. But since there are a lot of misleading answers all around involving private internal APIs of pip, it might be worth looking at saner alternatives…

There is no need for pip to parse a requirements.txt file from a setuptools setup.py script. The setuptools project already contains all the necessary tools in its top level package pkg_resources.

It could more or less look like this:

#!/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,
)

Notes:


回答 16

这个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放在项目根目录下即可。

Cross posting my answer from this SO question for another simple, pip version proof solution.

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

Then just throw in all your requirements under requirements.txt under project root directory.


回答 17

我这样做:

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

I did this:

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

回答 18

另一个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有几个缺点,因此这不是您应该在公共项目上执行的操作,但对于内部/个人项目来说就足够了。

Yet another parse_requirements hack that also parses environment markers into 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
)

It should support both sdist and binary dists.

As stated by others, parse_requirements has several shortcomings, so this is not what you should do on public projects, but it may suffice for internal/personal projects.


回答 19

这是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)

Here is a complete hack (tested with pip 9.0.1) based on Romain’s answer that parses requirements.txt and filters it according to current 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)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。