Как я могу проверить во время выполнения, что модуль Python действителен, не импортируя его?

У меня есть пакет, содержащий подпакеты, только один из которых мне нужно импортировать во время выполнения, но мне нужно проверить их правильность. Вот моя структура папок:

game/
 __init__.py
 game1/
   __init__.py
   constants.py
   ...
 game2/
   __init__.py
   constants.py
   ...

На данный момент код, который запускается при загрузке, делает:

import pkgutil
import game as _game
# Detect the known games
for importer,modname,ispkg in pkgutil.iter_modules(_game.__path__):
    if not ispkg: continue # game support modules are packages
    # Equivalent of "from game import <modname>"
    try:
        module = __import__('game',globals(),locals(),[modname],-1)
    except ImportError:
        deprint(u'Error in game support module:', modname, traceback=True)
        continue
    submod = getattr(module,modname)
    if not hasattr(submod,'fsName') or not hasattr(submod,'exe'): continue
    _allGames[submod.fsName.lower()] = submod

но у этого есть недостаток, заключающийся в том, что все подпакеты импортируются, импортируя другие модули в подпакете (например, Constants.py и т. д.), что составляет несколько мегабайт мусора. Поэтому я хочу заменить этот код проверкой того, что подмодули действительны (они будут импортироваться нормально). Думаю, мне следует как-то использовать eval, но как? Или что мне делать?

ИЗМЕНИТЬ: tldr;

Я ищу эквивалент ядра цикла выше:

    try:
        probaly_eval(game, modname) # fails iff `from game import modname` fails
        # but does _not_ import the module
    except: # I'd rather have a more specific error here but methinks not possible
        deprint(u'Error in game support module:', modname, traceback=True)
        continue

Поэтому мне нужен четкий ответ, существует ли точный эквивалент оператора импорта для проверки ошибок - без импорта модуля. Это мой вопрос, многие отвечавшие и комментирующие отвечали на разные вопросы.


person Mr_and_Mrs_D    schedule 27.01.2017    source источник
comment
что-то вроде: python -m py_compile script.py ?   -  person fedepad    schedule 27.01.2017
comment
Мне нужно сделать это из работающей программы, как указано   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
Да, но вы должны иметь возможность использовать его внутри программы, загружая как модуль.... docs.python.org/2/library/py_compile.html   -  person fedepad    schedule 27.01.2017
comment
@fedepad: это единственный правильный способ? (у меня уже было два ответа, оба удалены...)   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
Вы не можете проверить действительность модуля без его импорта. Вы можете импортировать его в другой интерпретатор Python (см. выше) или в свой собственный (см. importlib). В последнем случае все побочные эффекты, которые модуль может вызвать во время импорта, т.е. установка каких-то обезьяньих исправлений, выполнение произвольного ввода-вывода и т. д. произойдет с вашим интерпретатором, даже если сам модуль не будет включен в ваше пространство имен.   -  person 9000    schedule 27.01.2017
comment
@ 9000 - что, если я предоставлю одноразовый диктофон?   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
@9000 - как насчет этого: stackoverflow.com/a/41897538/281545   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
@Mr_and_Mrs_D: Компиляция - хороший шаг, но она проверяет файл в меньшей степени из-за отсутствия многих статических проверок (во время компиляции) в Python. Вы можете успешно скомпилировать файл, который будет бомбить с AttributeError или ArithmeticError или KeyError и т. д. во время импорта. Простой импорт OTOH не гарантирует, что импортированные функции в любом случае не будут аварийно завершать работу во время выполнения.   -  person 9000    schedule 27.01.2017
comment
@Mr_and_Mrs_D: eval предоставление одноразового словаря сохраняет пространство имен вашего интерпретатора, хорошая идея! OTOH это не мешает оцениваемому модулю выполнять произвольный ввод-вывод, по крайней мере, если вы не очень оборонительны (что сложно). Это зависит от того, сколько саддбоксинга вам нужно, например. импорт кода, загруженного из Интернета, по сравнению с проверкой работоспособности кода, которому вы больше всего доверяете.   -  person 9000    schedule 27.01.2017
comment
@MoinuddinQuadri: почему вы удалили тег eval? Это явно актуально - см. комментарии выше. На самом деле указанный путь компиляции, вероятно, является низшим способом проверки правильности, поскольку он пропустит упомянутые ошибки.   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
@9000 - OTOH mere importing does not guarantee that imported functions will not crash at runtime anyway - лол, конечно - я не ищу волшебного метода, который проверит, что моя программа не содержит ошибок - просто для эквивалента приведенному выше коду - эквивалентный код должен пройти iff выше проходит. vs doing a sanity check for code you mostly trust - см. выше - мне нужен код, эквивалентный приведенному выше try: import except: print 'error'; continue   -  person Mr_and_Mrs_D    schedule 27.01.2017
comment
@9000 - пользовательское время импорта - все еще опасно - просто poc: stackoverflow.com/a/43700205/281545   -  person Mr_and_Mrs_D    schedule 29.04.2017


Ответы (5)


Возможно, вы ищете модули py_compile или compileall.
Вот документация:
https://docs.python.org/2/library/py_compile.html
https://docs.python.org/2/library/compileall.html#module-compileall

Вы можете загрузить нужный модуль в качестве модуля и вызвать его из своей программы.
Например:

import py_compile

try:
    py_compile.compile(your_py_file, doraise=True)
    module_ok = True
except py_compile.PyCompileError:
    module_ok = False
person fedepad    schedule 27.01.2017

Если вы хотите скомпилировать файл без его импорта (в текущем интерпретаторе), вы можете использовать py_compile.compile как:

>>> import py_compile

# valid python file
>>> py_compile.compile('/path/to/valid/python/file.py')

# invalid python file
>>> py_compile.compile('/path/to/in-valid/python/file.txt')
Sorry: TypeError: compile() expected string without null bytes

Код выше записывает ошибку в std.error. Если вы хотите вызвать исключение, вам нужно будет установить doraise как True (по умолчанию False). Следовательно, ваш код будет:

from py_compile import compile, PyCompileError

try:
    compile('/path/to/valid/python/file.py', doraise=True)
    valid_file = True
except PyCompileError:
    valid_file = False

Согласно документам py_compile.compile:

Скомпилируйте исходный файл в байт-код и запишите файл кэша байт-кода. Исходный код загружается из файла с именем file. Байт-код записывается в cfile, который по умолчанию равен файлу + 'c' ("o", если в текущем интерпретаторе включена оптимизация). Если указан dfile, он используется в качестве имени исходного файла в сообщениях об ошибках вместо файла. Если doraise истинно, PyCompileError возникает при возникновении ошибки при компиляции файла. Если doraise равно false (по умолчанию), строка ошибки записывается в sys.stderr, но исключение не возникает.

Убедитесь, что скомпилированный модуль не импортирован (в текущем интерпретаторе):

>>> import py_compile, sys
>>> py_compile.compile('/path/to/main.py')

>>> print [key for key in locals().keys() if isinstance(locals()[key], type(sys)) and not key.startswith('__')]
['py_compile', 'sys']  # main not present
person Anonymous    schedule 27.01.2017
comment
@Mr_and_Mrs_D Это то, что вам нужно? - person Anonymous; 27.01.2017
comment
Вы уверены, что это не добавляет модуль в sys.modules? - person Mr_and_Mrs_D; 27.01.2017
comment
@Mr_and_Mrs_D Проверьте редактирование. Я сделал небольшой тестовый скрипт, чтобы убедиться, что он не импортируется при компиляции. - person Anonymous; 27.01.2017
comment
Проверю на своей установке, когда вернусь - person Mr_and_Mrs_D; 27.01.2017
comment
Это требует, чтобы вы знали разницу между «foo.py» и «foo/__init__», были на 100% уверены, что файл будет выполняться по пути python, все требования импорта соблюдены, а модуль выполняется. например, при импорте не будет простого повышения ValueError() . - person Jonathan Vanasco; 28.01.2017
comment
@JonathanVanasco - как я прокомментировал ваш ответ, у меня есть код в OP, который явно обходит пакеты. Однако компиляция по-прежнему не эквивалентна импорту - см.: Вы можете успешно скомпилировать файл, который будет выдавать ошибку AttributeError, ArithmeticError или KeyError и т. д. во время импорта - акцент мой - person Mr_and_Mrs_D; 28.01.2017

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

Использование pycompile и compileall будет проверять только то, можете ли вы скомпилировать файл Python, а не импортировать модуль. Между ними есть большая разница.

  1. Такой подход означает, что вы знаете фактическую файловую структуру модулей — import foo может представлять /foo.py или /foo/__init__.py.
  2. Такой подход не гарантирует, что модуль находится в pythonpath вашего интерпретатора или является модулем, который загрузит ваш интерпретатор. Все станет сложнее, если у вас есть несколько версий в /site-packages/ или python ищет модуль в одном из многих возможных мест.
  3. То, что ваш файл «компилируется», не означает, что он «запустится». Как пакет он может иметь неудовлетворенные зависимости или даже вызывать ошибки.

Представьте, что это ваш файл Python:

 from makebelieve import nothing
 raise ValueError("ABORT")

Вышеупомянутое будет скомпилировано, но если вы их импортируете... это вызовет ImportError, если у вас не установлен makebelieve, и вызовет ValueError, если вы это сделаете.

Мои предложения:

  1. импортируйте пакет, затем выгрузите модули. чтобы выгрузить их, просто переберите содержимое в sys.modules.keys()​​​. если вы беспокоитесь о загруженных внешних модулях, вы можете переопределить import, чтобы регистрировать загрузку ваших пакетов. Примером этого является ужасный пакет профилирования, который я написал: https://github.com/jvanasco/import_logger [Я забыл, откуда у меня появилась идея переопределить импорт. Может быть, celery?] Как некоторые заметили, выгрузка модулей полностью зависит от интерпретатора, но почти каждый вариант имеет много недостатков.

  2. Используйте подпроцессы для запуска нового интерпретатора через popen. т.е. popen('python', '-m', 'module_name'). Это будет иметь много накладных расходов, если вы сделаете это для каждого необходимого модуля (накладные расходы на каждый интерпретатор и импорт), но вы можете написать файл «.py», который импортирует все, что вам нужно, и просто попытаться запустить его. В любом случае вам придется проанализировать вывод, так как импорт «действительного» пакета может привести к допустимым ошибкам во время выполнения. я не могу вспомнить, наследует ли подпроцесс ваши переменные среды или нет, но я думаю, что да. Подпроцесс представляет собой совершенно новый процесс/интерпретатор операционной системы, поэтому модули будут загружены в память этого недолговечного процесса. Уточненный ответ.

person Jonathan Vanasco    schedule 27.01.2017
comment
Выгрузка модуля полностью зависит от компилятора Python, и вы не можете быть уверены, когда компилятор это сделает. Вы можете использовать delete_module (из созданной мной библиотеки), но и здесь у вас есть чтобы быть уверенным, что импортированный модуль не содержит никаких ссылок и полностью зависит от компилятора, когда он освободит эту память - person Anonymous; 27.01.2017
comment
Выгружать посылки не вариант - везде не рекомендуется. Я ищу не хакерский способ - правильный способ. Если бы я хотел заняться болью/взломом выгрузки пакетов, я бы уже сделал это - нелегко. И загрузка интерпретатора кажется слишком накладной — и я не уверен, что он не оставит модуль в пространстве имен. - person Mr_and_Mrs_D; 27.01.2017
comment
Ну, вы просите сделать хакерскую вещь. я бы посоветовал не использовать параметры компиляции, потому что они требуют, чтобы вы обращались к модулям в виде файлов, что означает, что вам нужно знать разницу между foo.py или foo/__init__.py И эти файлы могут быть вне вашего пути Python. Если вы запустите новый интерпретатор с помощью подпроцесса, модули будут загружены в процесс этого интерпретатора, а не в ваш. - person Jonathan Vanasco; 28.01.2017
comment
Нет, это не так, и даже если это так, мой вопрос в другом. Вы начинаете объяснять подводные камни, которые не применимы - я четко заявляю, что знаю свою структуру папок и даю цикл, который ее пересекает - так почему вы добавили целый абзац с подводными камнями компиляции (это не тот путь, который я считаю, я считаю своего рода eval нужен) ? - person Mr_and_Mrs_D; 28.01.2017

Я считаю, что imp.find_module удовлетворяет по крайней мере некоторым из ваших требований: https://docs.python.org/2/library/imp.html#imp.find_module

Быстрый тест показывает, что он не запускает импорт:

>>> import imp
>>> import sys
>>> len(sys.modules)
47
>>> imp.find_module('email')
(None, 'C:\\Python27\\lib\\email', ('', '', 5))
>>> len(sys.modules)
47
>>> import email
>>> len(sys.modules)
70

Вот пример использования в моем коде (который пытается классифицировать модули): L44" rel="nofollow noreferrer">https://github.com/asottile/aspy.refactor_imports/blob/2b9bf8bd2cf22ef114bcc2eb3e157b99825204e0/aspy/refactor_imports/classify.py#L38-L44

person Anthony Sottile    schedule 28.01.2017

У нас уже был пользовательский импортер (отказ от ответственности: я не писал этот код, я просто текущий сопровождающий), чей load_module:

def load_module(self,fullname):
    if fullname in sys.modules:
        return sys.modules[fullname]
    else: # set to avoid reimporting recursively
        sys.modules[fullname] = imp.new_module(fullname)
    if isinstance(fullname,unicode):
        filename = fullname.replace(u'.',u'\\')
        ext = u'.py'
        initfile = u'__init__'
    else:
        filename = fullname.replace('.','\\')
        ext = '.py'
        initfile = '__init__'
    try:
        if os.path.exists(filename+ext):
            with open(filename+ext,'U') as fp:
                mod = imp.load_source(fullname,filename+ext,fp)
                sys.modules[fullname] = mod
                mod.__loader__ = self
        else:
            mod = sys.modules[fullname]
            mod.__loader__ = self
            mod.__file__ = os.path.join(os.getcwd(),filename)
            mod.__path__ = [filename]
            #init file
            initfile = os.path.join(filename,initfile+ext)
            if os.path.exists(initfile):
                with open(initfile,'U') as fp:
                    code = fp.read()
                exec compile(code, initfile, 'exec') in mod.__dict__
        return mod
    except Exception as e: # wrap in ImportError a la python2 - will keep
        # the original traceback even if import errors nest
        print 'fail', filename+ext
        raise ImportError, u'caused by ' + repr(e), sys.exc_info()[2]

Поэтому я подумал, что могу заменить части, которые обращаются к кешу sys.modules, переопределяемыми методами, которые в моем переопределении оставят этот кеш в покое:

So:

@@ -48,2 +55,2 @@ class UnicodeImporter(object):
-        if fullname in sys.modules:
-            return sys.modules[fullname]
+        if self._check_imported(fullname):
+            return self._get_imported(fullname)
@@ -51 +58 @@ class UnicodeImporter(object):
-            sys.modules[fullname] = imp.new_module(fullname)
+            self._add_to_imported(fullname, imp.new_module(fullname))
@@ -64 +71 @@ class UnicodeImporter(object):
-                    sys.modules[fullname] = mod
+                    self._add_to_imported(fullname, mod)
@@ -67 +74 @@ class UnicodeImporter(object):
-                mod = sys.modules[fullname]
+                mod = self._get_imported(fullname)

и определить:

class FakeUnicodeImporter(UnicodeImporter):

    _modules_to_discard = {}

    def _check_imported(self, fullname):
        return fullname in sys.modules or fullname in self._modules_to_discard

    def _get_imported(self, fullname):
        try:
            return sys.modules[fullname]
        except KeyError:
            return self._modules_to_discard[fullname]

    def _add_to_imported(self, fullname, mod):
        self._modules_to_discard[fullname] = mod

    @classmethod
    def cleanup(cls):
        cls._modules_to_discard.clear()

Затем я добавил импортер в sys.meta_path, и все было готово:

importer = sys.meta_path[0]
try:
    if not hasattr(sys,'frozen'):
        sys.meta_path = [fake_importer()]
    perform_the_imports() # see question
finally:
    fake_importer.cleanup()
    sys.meta_path = [importer]

Верно ? Неправильно!

Traceback (most recent call last):
  File "bash\bush.py", line 74, in __supportedGames
    module = __import__('game',globals(),locals(),[modname],-1)
  File "Wrye Bash Launcher.pyw", line 83, in load_module
    exec compile(code, initfile, 'exec') in mod.__dict__
  File "bash\game\game1\__init__.py", line 29, in <module>
    from .constants import *
ImportError: caused by SystemError("Parent module 'bash.game.game1' not loaded, cannot perform relative import",)

Хм ? В настоящее время я импортирую тот же самый модуль. Что ж, ответ, вероятно, находится в документах по импорту

Если модуль не найден в кеше, выполняется поиск sys.meta_path (спецификацию sys.meta_path можно найти в PEP 302).

Это не совсем так, но я предполагаю, что оператор from .constants import * ищет sys.modules, чтобы проверить, есть ли там родительский модуль, и я не вижу способа обходя это (обратите внимание, что наш пользовательский загрузчик использует встроенный механизм импорта для модулей, mod.__loader__ = self устанавливается постфактум).

Поэтому я обновил свой FakeImporter, чтобы использовать кеш sys.modules, а затем очистил его.

class FakeUnicodeImporter(UnicodeImporter):

    _modules_to_discard = set()

    def _check_imported(self, fullname):
        return fullname in sys.modules or fullname in self._modules_to_discard

    def _add_to_imported(self, fullname, mod):
        super(FakeUnicodeImporter, self)._add_to_imported(fullname, mod)
        self._modules_to_discard.add(fullname)

    @classmethod
    def cleanup(cls):
        for m in cls._modules_to_discard: del sys.modules[m]

Это, однако, взорвалось по-новому - или, скорее, двумя способами:

  • ссылка на игру/пакет содержалась в bash верхнем экземпляре пакета в sys.modules:

    bash\
      __init__.py
      the_code_in_question_is_here.py
      game\
        ...
    

    потому что game импортируется как bash.game. Эта ссылка содержала ссылки на все game1, game2,... подпакеты, поэтому они никогда не удалялись сборщиком мусора.

  • ссылка на другой модуль (brec) удерживалась как bash.brec тем же экземпляром модуля bash. Эта ссылка была импортирована как from .. import brec в game\game1 без активации импорта для обновления SomeClass. Однако в еще одном модуле импорт формы from ...brec import SomeClass действительно инициировал импорт, и еще один экземпляр модуля brec оказался в sys .модули. Этот экземпляр имел необновленный SomeClass и выдал ошибку AttributeError.

Оба были исправлены путем удаления этих ссылок вручную, поэтому gc собрал все модули (для 5 мегабайт оперативной памяти из 75), и from .. import brec действительно вызвал импорт (это from ... import foo против from ...foo import bar требует вопроса).

Мораль этой истории в том, что это возможно, но:

  • пакет и подпакеты должны ссылаться только друг на друга
  • все ссылки на внешние модули/пакеты должны быть удалены из атрибутов пакета верхнего уровня.
  • сама ссылка на пакет должна быть удалена из атрибута пакета верхнего уровня

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


Этот пост был спонсирован отладчиком Pydev - я нашел модуль gc очень полезным для понимания того, что происходит - советы из здесь. Конечно, было много переменных отладчика и других сложных вещей.

введите здесь описание изображения

person Mr_and_Mrs_D    schedule 29.04.2017
comment
У меня есть открытый вопрос о нашем загрузчике здесь: stackoverflow.com/q/41921098/281545 - person Mr_and_Mrs_D; 29.04.2017