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

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

需求文件可以包含注释( # ),并可以包含一些其他文件( --requirement-r )。 因此,如果你真的想parsing一个requirements.txt你应该使用pipparsing器:

 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 # eg ['django==1.5.1', 'mezzanine==1.4.6'] reqs = [str(ir.req) for ir in install_reqs] setup( ... install_requires=reqs ) 

更新:我的答案现在是老的。 皮普没有一个公共的API,所以这不再工作 (例如parse_requirements现在需要一个pip.download.PipSession的实例)。 你可以做相反的事情:在setup.py列出依赖关系,并有一个单一的字符 – 一个点. – 在requirements.txt

UPDATE2:即使没有build议,仍然可以parsingrequirements.txt文件,该文件没有引用任何外部需求的URL与下面的黑客(用pip 9.0.1testing):

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

这不过滤环境标记 。

从表面setup.pyrequirements.txtsetup.py看起来似乎是愚蠢的重复,但重要的是要明白,虽然表单是相似的,但是预期的function是非常不同的。

软件包作者在指定依赖关系时的目标是,“无论你在哪里安装这个软件包,这些软件包都是你需要的其他软件包,以便这个软件包能够正常工作”。

相比之下,部署作者(可能是同一个人在不同的时间)有一个不同的工作,他们说:“这里是我们收集和testing的包列表,现在我需要安装”。

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

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

所以你可以看到,虽然他们都看起来像一个包和版本的大名单,这两件事情有非常不同的工作。 混合起来很容易,搞错了! 但是正确的思路是, requirements.txt是所有各种setup.py包文件中的要求所提出的“问题”的“答案”。 通常是通过告诉pip来查看所有包中的setup.py文件,find一套它认为符合所有要求的包,然后在安装完成之后,将这个包列表“冻结”成一个文本文件(这是pip freeze名称的来源)。

所以外卖:

  • setup.py应该声明仍然可行的最宽松的依赖版本。 它的工作就是说一个特定的软件包可以工作。
  • requirements.txt是定义整个安装作业的部署清单,不应被认为与任何一个包绑定在一起。 它的工作是宣布所有必要的软件包的详尽清单,以进行部署工作。
  • 因为这两样东西存在着不同的内容和原因,所以简单地把它们复制到另一个是不可行的。

参考文献:

  • install_requires vs Python打包用户指南中的需求文件 。

它不能采取文件句柄。 install_requires参数只能是一个string或一个string列表 。

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

 import os from setuptools import setup with open('requirements.txt') as f: required = f.read().splitlines() setup(... install_requires=required, ...) 

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

 . 

这将是有效的,并参照完全相同的目录中的setup.py的内容。

虽然不是问题的确切答案,但我build议Donald Stufft的博文在https://caremad.io/2013/07/setup-vs-requirement/上提供一个很好的解决scheme。; 我一直在使用它取得巨大的成功。

简而言之, requirements.txt不是setup.py备选,而是一个部署补充。 在setup.py保留对包依赖关系的适当抽象。 设置requirements.txt或更多,以便为开发,testing或生产提取特定版本的软件包依赖关系。

例如,包含在回购协议下的软件包中:

 # fetch specific dependencies --no-index --find-links deps/ # install package # NOTE: -e . for editable mode . 

pip执行包的setup.py并安装在install_requires声明的特定版本的依赖关系。 没有重复性,两个工件的目的都被保留了下来。

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

使用parse_requirements是有问题的,因为pip API没有公开logging和支持。 在pip 1.6中,这个函数实际上在移动,所以它的现有用途很可能会被打破。

消除setup.pyrequirements.txt之间重复的更可靠的方法是在setup.py您的依赖项,然后将-e . 到您的requirements.txt文件中。 来自一位pip开发者的关于为什么这是更好的方法的一些信息可以在这里find: https : //caremad.io/blog/setup-vs-requirement/

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

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

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

from pip.req import parse_requirements没有为我工作,我认为这是在我的requirements.txt中的空行,但这个function可以工作

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

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

另一个可能的解

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

我为此创build了一个可重用的函数。 它实际上parsing需求文件的整个目录,并将它们设置为extras_require。

最新始终可用: https : //gist.github.com/akatrevorjay/293c26fefa24a7b812f5

 from setuptools import setup, find_packages from pip.req import parse_requirements from pip.download import PipSession import itertools import glob import os 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. 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.txt': 'install_requires', 'install.txt': 'install_requires', 'tests.txt': 'tests_require', 'setup.txt': 'setup_requires', } ret = {v: [] 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) key = key_map.get(fn) if key: ret[key].extend(reqs) else: # Remove extension, use as extras key key, _ = os.path.splitext(fn) extras[key] = reqs if 'all' not in extras: extras['all'] = list(all_reqs) if combine: extras['install'] = ret['install_requires'] ret['install_requires'] = list(all_reqs) return ret 

这是一个完全的破解(基于pip 9.0.1testing),基于Romain的parsingrequirements.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) 

还有另外一个parse_requirements hack,也将环境标记parsing为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有几个缺点,所以这不是你应该在公共项目上做的,但是对于内部/个人项目来说就足够了。