Check Contents of Python Package 没有 运行 吗?
Check Contents of Python Package without Running it?
我想要一个函数,给定一个 name
导致 NameError
,可以识别 Python 个可以 import
ed 来解决它的包。
那部分相当简单,我已经完成了,但是现在我有一个额外的问题:我想在不产生副作用的情况下完成它。这是我现在使用的代码:
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
请求密码
- 请求其他
input
或 raw_input
- 正在打印消息(
import this
)
- 打开网站(
import antigravity
)
我考虑过的一个可能的解决方案是找到每个模块的路径(如何?在我看来,唯一的方法是 import
访问模块,然后使用 inspect
),然后解析它以找到不在 class
或 def
中的每个 class
、def
和 =
,但这似乎是一个巨大的 PITA,我认为它不适用于在 C/C++ 而不是纯 Python.
中实现的模块
另一种可能性是启动子 Python 实例,它的输出重定向到 devnull
并在那里执行检查,如果花费的时间太长则将其杀死。这将解决前四个项目符号,而第五个项目符号是一个特例,我可以跳过 antigravity
。但是必须在这个单一函数中启动数千个 Python 实例似乎有点……繁重且效率低下。
有没有人有我没有考虑过的更好的解决方案?例如,是否有一种简单的方法可以告诉 Python 生成 AST 或其他内容而不实际导入模块?
所以我最终编写了一些方法,可以列出源文件中的所有内容,而无需导入源文件。
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
这就是我度过周日的方式。
我想要一个函数,给定一个 name
导致 NameError
,可以识别 Python 个可以 import
ed 来解决它的包。
那部分相当简单,我已经完成了,但是现在我有一个额外的问题:我想在不产生副作用的情况下完成它。这是我现在使用的代码:
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
请求密码
- 请求其他
input
或raw_input
- 正在打印消息(
import this
) - 打开网站(
import antigravity
)
我考虑过的一个可能的解决方案是找到每个模块的路径(如何?在我看来,唯一的方法是 import
访问模块,然后使用 inspect
),然后解析它以找到不在 class
或 def
中的每个 class
、def
和 =
,但这似乎是一个巨大的 PITA,我认为它不适用于在 C/C++ 而不是纯 Python.
另一种可能性是启动子 Python 实例,它的输出重定向到 devnull
并在那里执行检查,如果花费的时间太长则将其杀死。这将解决前四个项目符号,而第五个项目符号是一个特例,我可以跳过 antigravity
。但是必须在这个单一函数中启动数千个 Python 实例似乎有点……繁重且效率低下。
有没有人有我没有考虑过的更好的解决方案?例如,是否有一种简单的方法可以告诉 Python 生成 AST 或其他内容而不实际导入模块?
所以我最终编写了一些方法,可以列出源文件中的所有内容,而无需导入源文件。
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
这就是我度过周日的方式。