Импорт модуля python без его выполнения
В контексте сложного приложения мне нужно импортировать пользовательские "скрипты". В идеале script имел бы
def init():
blah
def execute():
more blah
def cleanup():
yadda
поэтому я просто
import imp
fname, path, desc = imp.find_module(userscript)
foo = imp.load_module(userscript, fname, path, desc)
foo.init()
Однако, как мы все знаем, пользователь script выполняется , как только load_module
запускается.
Это означает, что script может быть примерно таким:
def init():
blah
yadda
приведя к тому, что часть yadda
вызывается, как только я import
script.
Мне нужен способ:
- сначала проверьте, имеет ли он init(), execute() и cleanup()
- если они существуют, все хорошо
- если они не существуют, жалуйтесь
- не запускайте какой-либо другой код или, по крайней мере, пока я не знаю, нет init()
Обычно я принуждаю использовать тот же старый трюк if __name__ == '__main__'
, но я мало контролирую предоставляемый пользователем script, поэтому я ищу относительно безболезненное решение. Я видел всевозможные сложные трюки, включая разбор script, но ничего простого. Я удивлен, что этого не существует.. или, может быть, я ничего не получаю.
Спасибо.
Ответы
Ответ 1
Моя попытка с помощью модуля ast:
import ast
# which syntax elements are allowed at module level?
whitelist = [
# docstring
lambda x: isinstance(x, ast.Expr) \
and isinstance(x.value, ast.Str),
# import
lambda x: isinstance(x, ast.Import),
# class
lambda x: isinstance(x, ast.ClassDef),
# function
lambda x: isinstance(x, ast.FunctionDef),
]
def validate(source, required_functions):
tree = ast.parse(source)
functions = set()
required_functions = set(required_functions)
for item in tree.body:
if isinstance(item, ast.FunctionDef):
functions.add(item.name)
continue
if all(not checker(item) for checker in whitelist):
return False
# at least the required functions must be there
return len(required_functions - functions) == 0
if __name__ == "__main__":
required_funcs = [ "init", "execute", "cleanup" ]
with open("/tmp/test.py", "rb") as f:
print("yay!" if validate(f.read(), required_funcs) else "d'oh!")
Ответ 2
Здесь более простая (и более наивная) альтернатива методу AST:
import sys
from imp import find_module, new_module, PY_SOURCE
EXPECTED = ("init", "execute", "cleanup")
def import_script(name):
fileobj, path, description = find_module(name)
if description[2] != PY_SOURCE:
raise ImportError("no source file found")
code = compile(fileobj.read(), path, "exec")
expected = list(EXPECTED)
for const in code.co_consts:
if isinstance(const, type(code)) and const.co_name in expected:
expected.remove(const.co_name)
if expected:
raise ImportError("missing expected function: {}".format(expected))
module = new_module(name)
exec(code, module.__dict__)
sys.modules[name] = module
return module
Имейте в виду, что это очень прямой способ сделать это и обойти расширения для механизмов импорта Python.
Ответ 3
Мне прежде всего не нужны некоторые функции, а класс, соответствующий указанному интерфейсу, используя модуль abc
, или zope.interface
. Это заставляет изготовителя модуля поставлять нужные функции.
Во-вторых, я бы не стал искать код на уровне модуля. Это проблема разработчиков модулей, если он это делает. Это слишком много работает без фактической выгоды.
Если вы беспокоитесь о проблемах с безопасностью, вам все равно нужно изолировать код.
Ответ 4
Не уверен, что вы рассмотрите этот элегантный, но он несколько умный в том смысле, что он распознает, когда def init
являются токенами, а не просто частью сложной многострочной строки:
'''
def init does not define init...
'''
Он не распознает, когда init
определяется сложными альтернативными способами, такими как
init = lambda ...
или
codestr='def i'+'nit ...'
exec(codestr)
Единственный способ справиться со всеми такими случаями - запустить код (например, в песочнице или импортировать) и проверить результат.
import tokenize
import token
import io
import collections
userscript = '''\
def init():
blah
"""
def execute():
more blah
"""
yadda
'''
class Token(object):
def __init__(self, tok):
toknum, tokval, (srow, scol), (erow, ecol), line = tok
self.toknum = toknum
self.tokname = token.tok_name[toknum]
self.tokval = tokval
self.srow = srow
self.scol = scol
self.erow = erow
self.ecol = ecol
self.line = line
class Validator(object):
def __init__(self, codestr):
self.codestr = codestr
self.toks = collections.deque(maxlen = 2)
self.names = set()
def validate(self):
tokens = tokenize.generate_tokens(io.StringIO(self.codestr).readline)
self.toks.append(Token(next(tokens)))
for tok in tokens:
self.toks.append(Token(tok))
if (self.toks[0].tokname == 'NAME' # First token is a name
and self.toks[0].scol == 0 # First token starts at col 0
and self.toks[0].tokval == 'def' # First token is 'def'
and self.toks[1].tokname == 'NAME' # Next token is a name
):
self.names.add(self.toks[1].tokval)
delta = set(['init', 'cleanup', 'execute']) - self.names
if delta:
raise ValueError('{n} not defined'.format(n = ' and '.join(delta)))
v = Validator(userscript)
v.validate()
дает
ValueError: execute and cleanup not defined
Ответ 5
Одним очень простым решением может быть проверка первых символов каждой строки кода: разрешено только:
-
def init():
-
def execute():
-
def cleanup():
- строки, начинающиеся с 4 пробелов
- [необязательно]: строки, начинающиеся с
#
Это очень примитивно, но оно соответствует вашим требованиям...
Обновление. Через секунду, хотя я понял, что это не так просто. Рассмотрим, например, этот фрагмент кода:
def init():
v = """abc
def
ghi"""
print(v)
Это означает, что вам понадобится более сложный алгоритм анализа кода... так что забудьте о моем решении...
Ответ 6
Решение от 1 до 3 (а не часть yadda) заключается в том, чтобы передать "generic_class.py" все необходимые вам методы. Итак,
class Generic(object):
def __init__(self):
return
def execute(self):
return
# etc
Затем вы можете проверить наличие "общего" в том, что вы импортировали. Если этого не существует, вы можете игнорировать его, и если это произойдет, вы точно знаете, что там. Ничего лишнего никогда не будет вызывать, если он не вызвал из одного из ваших предопределенных методов.