1

我想要一个函数,给定name导致 a 的 a NameError,可以识别可以import用来解决它的 Python 包。

这部分相当简单,我已经完成了,但现在我还有一个问题:我想这样做而不引起副作用。这是我现在正在使用的代码:

def necessaryImportFor(name):
    from pkgutil import walk_packages
    for package in walk_packages():
        if package[1] == name:
            return name
        try:
            if hasattr(__import__(package[1]), name):
                return package[1]
        except Exception as e:
            print("Can't check " + package[1] + " on account of a " + e.__class__.__name__ + ": " + str(e))
    print("No possible import satisfies " + name)

问题是这段代码实际上__import__是每个模块。这意味着导入每个模块的每一个副作用都会发生。在测试我的代码时,我发现导入所有模块可能导致的副作用包括:

  • 启动 tkinter 应用程序
  • 请求密码getpass
  • 要求其他inputraw_input
  • 打印信息 ( import this)
  • 打开网站 ( import antigravity)

我考虑的一个可能的解决方案是找到每个模块的路径(如何?在我看来,唯一的方法是通过import模块然后使用inspect它上面的一些方法),然后解析它以找到每个class, def,并且=它本身不在 a classordef中,但这似乎是一个巨大的 PITA,我认为它不适用于用 C/C++ 而不是纯 Python 实现的模块。

另一种可能性是启动一个子 Python 实例,该实例将其输出重定向到devnull那里并在那里执行检查,如果花费的时间太长,则将其杀死。这将解决前四颗子弹,而第五颗子弹是如此特殊,我可以跳过antigravity。但是不得不在这个单一函数中启动数千个 Python 实例似乎有点……繁重且效率低下。

有没有人有我没有考虑过的更好的解决方案?例如,有没有一种简单的方法可以告诉 Python 生成 AST 或其他东西而不实际导入模块?

4

1 回答 1

3

所以我最终编写了一些方法,可以列出源文件中的所有内容,而无需导入源文件。

ast模块似乎没有特别好的文档记录,所以这有点像 PITA 试图弄清楚如何提取所有感兴趣的东西。尽管如此,今天经过大约 6 个小时的反复试验,我还是能够将它放在一起并在我的计算机上的 3000 多个 Python 源文件上运行它,而不会引发任何异常。

def listImportablesFromAST(ast_):
    from ast import (Assign, ClassDef, FunctionDef, Import, ImportFrom, Name,
                     For, Tuple, TryExcept, TryFinally, With)

    if isinstance(ast_, (ClassDef, FunctionDef)):
        return [ast_.name]
    elif isinstance(ast_, (Import, ImportFrom)):
        return [name.asname if name.asname else name.name for name in ast_.names]

    ret = []

    if isinstance(ast_, Assign):
        for target in ast_.targets:
            if isinstance(target, Tuple):
                ret.extend([elt.id for elt in target.elts])
            elif isinstance(target, Name):
                ret.append(target.id)
        return ret

    # These two attributes cover everything of interest from If, Module,
    # and While. They also cover parts of For, TryExcept, TryFinally, and With.
    if hasattr(ast_, 'body') and isinstance(ast_.body, list):
        for innerAST in ast_.body:
            ret.extend(listImportablesFromAST(innerAST))
    if hasattr(ast_, 'orelse'):
        for innerAST in ast_.orelse:
            ret.extend(listImportablesFromAST(innerAST))

    if isinstance(ast_, For):
        target = ast_.target
        if isinstance(target, Tuple):
            ret.extend([elt.id for elt in target.elts])
        else:
            ret.append(target.id)
    elif isinstance(ast_, TryExcept):
        for innerAST in ast_.handlers:
            ret.extend(listImportablesFromAST(innerAST))
    elif isinstance(ast_, TryFinally):
        for innerAST in ast_.finalbody:
            ret.extend(listImportablesFromAST(innerAST))
    elif isinstance(ast_, With):
        if ast_.optional_vars:
            ret.append(ast_.optional_vars.id)
    return ret

def listImportablesFromSource(source, filename = '<Unknown>'):
    from ast import parse
    return listImportablesFromAST(parse(source, filename))

def listImportablesFromSourceFile(filename):
    with open(filename) as f:
        source = f.read()
    return listImportablesFromSource(source, filename)

上面的代码涵盖了名义上的问题:如何在不运行 Python 包的情况下检查它的内容?

但它给你留下了另一个问题:我如何从 Python 包的名称中获取路径?

这是我写的来处理这个问题:

class PathToSourceFileException(Exception):
    pass

class PackageMissingChildException(PathToSourceFileException):
    pass

class PackageMissingInitException(PathToSourceFileException):
    pass

class NotASourceFileException(PathToSourceFileException):
    pass

def pathToSourceFile(name):
    '''
    Given a name, returns the path to the source file, if possible.
    Otherwise raises an ImportError or subclass of PathToSourceFileException.
    '''

    from os.path import dirname, isdir, isfile, join

    if '.' in name:
        parentSource = pathToSourceFile('.'.join(name.split('.')[:-1]))
        path = join(dirname(parentSource), name.split('.')[-1])
        if isdir(path):
            path = join(path, '__init__.py')
            if isfile(path):
                return path
            raise PackageMissingInitException()
        path += '.py'
        if isfile(path):
            return path
        raise PackageMissingChildException()

    from imp import find_module, PKG_DIRECTORY, PY_SOURCE

    f, path, (suffix, mode, type_) = find_module(name)
    if f:
        f.close()
    if type_ == PY_SOURCE:
        return path
    elif type_ == PKG_DIRECTORY:
        path = join(path, '__init__.py')
        if isfile(path):
            return path
        raise PackageMissingInitException()
    raise NotASourceFileException('Name ' + name + ' refers to the file at path ' + path + ' which is not that of a source file.')

一起尝试这两个代码,我有这个功能:

def listImportablesFromName(name, allowImport = False):
    try:
        return listImportablesFromSourceFile(pathToSourceFile(name))
    except PathToSourceFileException:
        if not allowImport:
            raise
        return dir(__import__(name))

最后,这是我在问题中提到的我想要的函数的实现:

def necessaryImportFor(name):
    packageNames = []

    def nameHandler(name):
        packageNames.append(name)

    from pkgutil import walk_packages
    for package in walk_packages(onerror=nameHandler):
        nameHandler(package[1])
    # Suggestion: Sort package names by count of '.', so shallower packages are searched first.
    for package in packageNames:
        # Suggestion: just skip any package that starts with 'test.'
        try:
            if name in listImportablesForName(package):
                return package
        except ImportError:
            pass
        except PathToSourceFileException:
            pass
    return None

这就是我度过星期天的方式。

于 2015-04-26T19:58:42.630 回答