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

Python 如何从setup.py中提取依赖项信息,python,setup.py,Python,Setup.py,我有一个python项目,我们称之为foobar,与所有python项目一样,在项目根目录中有一个setup.py脚本。比如说 福巴 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', package

我有一个python项目,我们称之为
foobar
,与所有python项目一样,在项目根目录中有一个
setup.py
脚本。比如说

  • 福巴
    • 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来解析它,但我认为应该已经有一些工具可以做到这一点。最好的方法是什么

在我看来,您可以使用
mock
来完成这项工作(假设您已经安装了它,并且具备了所有
setup.py
要求……)。这里的想法是模拟
setuptools.setup
并检查调用它的参数。当然,您不需要使用
mock
来实现这一点——如果需要,您可以直接使用monkey-patch
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', [])

与@mgilson的解决方案非常相似,我使用ast,解析setup.py模块,在setup调用之前插入一个mock setup方法,并收集args和kwargs

import ast


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]

这里的答案已经很好了,但是我不得不稍微修改@mgilson的答案才能让它为我工作,因为(显然)在我的源代码树中有错误配置的项目,并且导入了错误的设置。在我的解决方案中,我临时用另一个名称创建setup.py文件的副本,以便导入它,模拟可以截获正确的安装所需数据

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

您可以使用distutils.core的:

这样,就不需要模拟任何东西,而且您可以提取有关项目的更多信息,而不需要模拟setup()调用


请注意,此解决方案可能会有问题,因为显然正在进行积极的工作来反对distutils。有关详细信息,请参阅评论。

是。在过去,
mock
是一个第三方软件包,您必须安装该软件包才能实际使用。这在使用
AttributeError时失败:python 3.7.7下的模块“distutils”没有属性“core”
。我安装了
python3 distutils
python3 distutils extra
,但没有成功。我支持(后面的引擎),但被拒绝了,因为“最好避免使用distutils,我们正试图积极反对它。”来自Paul Gansle,setuptools维护者和Python核心开发人员。@akaihola,你请求拉的那条线真的很有趣,我没有意识到这一点。如果你能按照保罗的建议,添加一个备选答案,那就太好了。几周内我无法仔细查看细节,但我可能会稍后再试。我将更新我的答案,并说明潜在的问题。
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']