我可以在抽象语法树中处理导入吗?
Can I handle imports in an Abstract Syntax Tree?
我想解析并检查 config.py
以获取可接纳的节点。
config.py
可以导入其他配置文件,也必须检查。
ast
模块中是否有任何功能可以将 ast.Import
和 ast.ImportFrom
对象解析为 ast.Module
对象?
这是一个代码示例,我正在检查配置文件 (path_to_config
),但我还想检查它导入的任何文件:
with open(path_to_config) as config_file:
ast_tree = ast.parse(config_file.read())
for script_object in ast_tree.body:
if isinstance(script_object, ast.Import):
# Imported file must be checked too
elif isinstance(script_object, ast.ImportFrom):
# Imported file must be checked too
elif not _is_admissible_node(script_object):
raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)
这比您想象的要复杂一点。 from foo import name
是导入 foo
模块和 foo.name
模块中定义的对象的有效方法,因此您可能需要尝试 both表单以查看它们是否解析为文件。 Python 还允许使用别名,其中代码可以导入 foo.bar
,但实际模块实际上定义为 foo._bar_implementation
并作为 foo
包的属性提供。您无法仅通过查看 Import
和 ImportFrom
节点来检测所有这些情况。
如果忽略这些情况,只查看 from
名称,那么每次导入时,您仍然需要将模块名称转换为文件名,然后从文件中解析源代码。
在 Python 2 中,您可以使用 imp.find_module
为模块 (*) 获取打开的文件对象。在解析每个模块时,您希望保留完整的模块名称,因为稍后您将需要它来帮助您找出包相关的导入。 imp.find_module()
无法处理包导入,所以我创建了一个包装函数:
import imp
_package_paths = {}
def find_module(module):
# imp.find_module can't handle package paths, so we need to do this ourselves
# returns an open file object, the filename, and a flag indicating if this
# is a package directory with __init__.py file.
path = None
if '.' in module:
# resolve the package path first
parts = module.split('.')
module = parts.pop()
for i, part in enumerate(parts, 1):
name = '.'.join(parts[:i])
if name in _package_paths:
path = [_package_paths[name]]
else:
_, filename, (_, _, type_) = imp.find_module(part, path)
if type_ is not imp.PKG_DIRECTORY:
# no Python source code for this package, abort search
return None, None
_package_paths[name] = filename
path = [filename]
source, filename, (_, _, type_) = imp.find_module(module, path)
is_package = False
if type_ is imp.PKG_DIRECTORY:
# load __init__ file in package
source, filename, (_, _, type_) = imp.find_module('__init__', [filename])
is_package = True
if type_ is not imp.PY_SOURCE:
return None, None, False
return source, filename, is_package
我还会跟踪您已经导入的模块名称,这样您就不会处理它们两次;使用 spec
对象中的名称来确保您跟踪它们的规范名称。
使用堆栈处理所有模块:
with open(path_to_config) as config_file:
# stack consists of (modulename, ast) tuples
stack = [('', ast.parse(config_file.read()))]
seen = set()
while stack:
modulename, ast_tree = stack.pop()
for script_object in ast_tree.body:
if isinstance(script_object, (ast.Import, ast.ImportFrom)):
names = [a.name for a in script_object.names]
from_names = []
if hasattr(script_object, 'level'): # ImportFrom
from_names = names
name = script_object.module
if script_object.level:
package = modulename.rsplit('.', script_object.level - 1)[0]
if script_object.module:
name = "{}.{}".format(name, script_object.module)
else:
name = package
names = [name]
for name in names:
if name in seen:
continue
seen.add(name)
source, filename, is_package = find_module(name)
if source is None:
continue
if is_package and from_names:
# importing from a package, assume the imported names
# are modules
names += ('{}.{}'.format(name, fn) for fn in from_names)
continue
with source:
module_ast = ast.parse(source.read(), filename)
stack.append((name, module_ast))
elif not _is_admissible_node(script_object):
raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)
在 from foo import bar
导入的情况下,如果 foo
是一个包,则跳过 foo/__init__.py
并假定 bar
将是一个模块。
(*) imp.find_module()
已弃用 Python 3 代码。在 Python 3 上,您将使用 importlib.util.find_spec()
to get the module loader spec, and then use the ModuleSpec.origin
attribute 获取文件名。 importlib.util.find_spec()
知道如何处理包裹。
我想解析并检查 config.py
以获取可接纳的节点。
config.py
可以导入其他配置文件,也必须检查。
ast
模块中是否有任何功能可以将 ast.Import
和 ast.ImportFrom
对象解析为 ast.Module
对象?
这是一个代码示例,我正在检查配置文件 (path_to_config
),但我还想检查它导入的任何文件:
with open(path_to_config) as config_file:
ast_tree = ast.parse(config_file.read())
for script_object in ast_tree.body:
if isinstance(script_object, ast.Import):
# Imported file must be checked too
elif isinstance(script_object, ast.ImportFrom):
# Imported file must be checked too
elif not _is_admissible_node(script_object):
raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)
这比您想象的要复杂一点。 from foo import name
是导入 foo
模块和 foo.name
模块中定义的对象的有效方法,因此您可能需要尝试 both表单以查看它们是否解析为文件。 Python 还允许使用别名,其中代码可以导入 foo.bar
,但实际模块实际上定义为 foo._bar_implementation
并作为 foo
包的属性提供。您无法仅通过查看 Import
和 ImportFrom
节点来检测所有这些情况。
如果忽略这些情况,只查看 from
名称,那么每次导入时,您仍然需要将模块名称转换为文件名,然后从文件中解析源代码。
在 Python 2 中,您可以使用 imp.find_module
为模块 (*) 获取打开的文件对象。在解析每个模块时,您希望保留完整的模块名称,因为稍后您将需要它来帮助您找出包相关的导入。 imp.find_module()
无法处理包导入,所以我创建了一个包装函数:
import imp
_package_paths = {}
def find_module(module):
# imp.find_module can't handle package paths, so we need to do this ourselves
# returns an open file object, the filename, and a flag indicating if this
# is a package directory with __init__.py file.
path = None
if '.' in module:
# resolve the package path first
parts = module.split('.')
module = parts.pop()
for i, part in enumerate(parts, 1):
name = '.'.join(parts[:i])
if name in _package_paths:
path = [_package_paths[name]]
else:
_, filename, (_, _, type_) = imp.find_module(part, path)
if type_ is not imp.PKG_DIRECTORY:
# no Python source code for this package, abort search
return None, None
_package_paths[name] = filename
path = [filename]
source, filename, (_, _, type_) = imp.find_module(module, path)
is_package = False
if type_ is imp.PKG_DIRECTORY:
# load __init__ file in package
source, filename, (_, _, type_) = imp.find_module('__init__', [filename])
is_package = True
if type_ is not imp.PY_SOURCE:
return None, None, False
return source, filename, is_package
我还会跟踪您已经导入的模块名称,这样您就不会处理它们两次;使用 spec
对象中的名称来确保您跟踪它们的规范名称。
使用堆栈处理所有模块:
with open(path_to_config) as config_file:
# stack consists of (modulename, ast) tuples
stack = [('', ast.parse(config_file.read()))]
seen = set()
while stack:
modulename, ast_tree = stack.pop()
for script_object in ast_tree.body:
if isinstance(script_object, (ast.Import, ast.ImportFrom)):
names = [a.name for a in script_object.names]
from_names = []
if hasattr(script_object, 'level'): # ImportFrom
from_names = names
name = script_object.module
if script_object.level:
package = modulename.rsplit('.', script_object.level - 1)[0]
if script_object.module:
name = "{}.{}".format(name, script_object.module)
else:
name = package
names = [name]
for name in names:
if name in seen:
continue
seen.add(name)
source, filename, is_package = find_module(name)
if source is None:
continue
if is_package and from_names:
# importing from a package, assume the imported names
# are modules
names += ('{}.{}'.format(name, fn) for fn in from_names)
continue
with source:
module_ast = ast.parse(source.read(), filename)
stack.append((name, module_ast))
elif not _is_admissible_node(script_object):
raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)
在 from foo import bar
导入的情况下,如果 foo
是一个包,则跳过 foo/__init__.py
并假定 bar
将是一个模块。
(*) imp.find_module()
已弃用 Python 3 代码。在 Python 3 上,您将使用 importlib.util.find_spec()
to get the module loader spec, and then use the ModuleSpec.origin
attribute 获取文件名。 importlib.util.find_spec()
知道如何处理包裹。