如何从setup.py提取依赖信息

13 投票
6 回答
3881 浏览
提问于 2025-04-18 09:54

我有一个Python项目,我们叫它foobar,在这个项目的根目录下有一个setup.py脚本,就像所有Python项目一样。比如说:

  • foobar
    • setup.py

这个setup.py文件的内容是:

from ez_setup import use_setuptools
use_setuptools()

from setuptools import setup, find_packages
setup(
    name='foobar',
    version='0.0.0',
    packages=find_packages(),
    install_requires=[
        'spam==1.2.3',
        'eggs>=4.5.6',
    ],
)

我需要用Python从这个setup.py文件中获取依赖信息。我想要的部分是

[
    'spam==1.2.3',
    'eggs>=4.5.6',
]

在上面的例子中。我不想安装这个包,我只需要依赖信息。当然,我可以用正则表达式来解析它,但那样会很麻烦。我也可以用Python的AST来解析,但我觉得应该已经有一些工具可以做到这一点。那有什么好的方法呢?

6 个回答

1

使用 distutils.core 中的 run_setup 是个不错的选择,但有些 setup.py 文件在运行 setup(...) 之前会做一些额外的操作,所以要理解这些代码,似乎只能通过解析代码来解决。

import ast
import sys

path = sys.argv[1]
parsed = ast.parse(open(path).read())
for node in parsed.body:
    if not isinstance(node, ast.Expr):
        continue
    if not isinstance(node.value, ast.Call):
        continue
    if node.value.func.id != "setup":
        continue
    for keyword in node.value.keywords:
        if keyword.arg == "install_requires":
            requirements = ast.literal_eval(keyword.value)
            print("\n".join(requirements))
1

这里已经有很棒的回答了,但我需要稍微修改一下@mgilson的回答,才能让我这边正常工作,因为我的源代码树里似乎有一些配置不正确的项目,导致错误的设置被导入。在我的解决方案中,我暂时用另一个名字创建了一个setup.py文件的副本,这样我就可以导入它,让模拟(mock)能够拦截到正确的install_requires数据。

import sys 
import mock
import setuptools
import tempfile
import os

def import_and_extract(parent_dir):
    sys.path.insert(0, parent_dir)
    with tempfile.NamedTemporaryFile(prefix="setup_temp_", mode='w', dir=parent_dir, suffix='.py') as temp_fh:
        with open(os.path.join(parent_dir, "setup.py"), 'r') as setup_fh:
            temp_fh.write(setup_fh.read()) 
            temp_fh.flush()
        try:
            with mock.patch.object(setuptools, 'setup') as mock_setup:
                module_name = os.path.basename(temp_fh.name).split(".")[0]
                __import__(module_name)
        finally:
            # need to blow away the pyc
            try:
                os.remove("%sc"%temp_fh.name)
            except:
                print >> sys.stderr, ("Failed to remove %sc"%temp_fh.name)
        args, kwargs = mock_setup.call_args
        return sorted(kwargs.get('install_requires', []))


if __name__ == "__main__":
    if len(sys.argv) > 1:
        thedir = sys.argv[1]
        if not os.path.isdir(thedir):
            thedir = os.path.dirname(thedir)
        for d in import_and_extract(thedir):
            print d
    else:   
        print >> sys.stderr, ("syntax: %s directory"%sys.argv[0])
1

这个想法跟@mgilson的解决方案很像。我使用了一个叫做ast的工具,解析setup.py这个模块。在调用setup之前,我插入了一个假的setup方法,然后收集它的参数和关键字参数。

import ast
import textwrap


def parse_setup(setup_filename):
    """Parse setup.py and return args and keywords args to its setup
    function call

    """
    mock_setup = textwrap.dedent('''\
    def setup(*args, **kwargs):
        __setup_calls__.append((args, kwargs))
    ''')
    parsed_mock_setup = ast.parse(mock_setup, filename=setup_filename)
    with open(setup_filename, 'rt') as setup_file:
        parsed = ast.parse(setup_file.read())
        for index, node in enumerate(parsed.body[:]):
            if (
                not isinstance(node, ast.Expr) or
                not isinstance(node.value, ast.Call) or
                node.value.func.id != 'setup'
            ):
                continue
            parsed.body[index:index] = parsed_mock_setup.body
            break

    fixed = ast.fix_missing_locations(parsed)
    codeobj = compile(fixed, setup_filename, 'exec')
    local_vars = {}
    global_vars = {'__setup_calls__': []}
    exec(codeobj, global_vars, local_vars)
    return global_vars['__setup_calls__'][0]
15

我觉得你可以用 mock 来完成这个工作(前提是你已经安装了它,并且满足所有 setup.py 的要求)。这里的想法就是模拟 setuptools.setup,然后检查它被调用时传入了什么参数。当然,实际上你并不一定需要 mock 来做到这一点——如果你愿意的话,也可以直接修改 setuptools

import mock  # or `from unittest import mock` for python3.3+.
import setuptools

with mock.patch.object(setuptools, 'setup') as mock_setup:
    import setup  # This is setup.py which calls setuptools.setup

# called arguments are in `mock_setup.call_args`
args, kwargs = mock_setup.call_args
print kwargs.get('install_requires', [])
9

你可以使用distutils.core里的 run_setup 方法:

from distutils.core import run_setup

result = run_setup("./setup.py", stop_after="init")
result.install_requires
['spam==1.2.3', 'eggs>=4.5.6']

这样就不需要模拟任何东西了,而且你可能能获取到比模拟setup()调用更多的项目信息。

不过要注意,这个方法可能会有问题,因为目前似乎正在进行一些工作来逐步淘汰distutils。具体情况可以查看评论。

撰写回答