Подтвердить что ты не робот

Импорт модуля 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, но ничего простого. Я удивлен, что этого не существует.. или, может быть, я ничего не получаю.

Спасибо.

4b9b3361

Ответ 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

Затем вы можете проверить наличие "общего" в том, что вы импортировали. Если этого не существует, вы можете игнорировать его, и если это произойдет, вы точно знаете, что там. Ничего лишнего никогда не будет вызывать, если он не вызвал из одного из ваших предопределенных методов.