2017-02-01 3 views
3

Надеюсь, следующий вопрос не слишком длинный. Но в противном случае я не могу объяснить проблему и то, что хочу:Как использовать sys.path_hooks для индивидуальной загрузки модулей?

Ученый от How to use importlib to import modules from arbitrary sources? (мой вопрос вчера) Я написал specfic-загрузчик для нового типа файла (.xxx). (На самом деле xxx - это зашифрованная версия pyc для защиты кода от кражи).

Я хотел бы только добавить крюк импорта для нового типа файла "xxx", не затрагивая другие типы (.py, .pyc, .pyd) любым способом.

Теперь грузчик ModuleLoader, наследующий от mportlib.machinery.SourcelessFileLoader.

Использование sys.path_hooks загрузчик должен быть добавлен в качестве крючка:

myFinder = importlib.machinery.FileFinder 
loader_details = (ModuleLoader, ['.xxx']) 
sys.path_hooks.append(myFinder.path_hook(loader_details)) 

Примечание: Это срабатывает один раз по телефону modloader.activateLoader()

При загрузке модуль с именем test (который является test.xxx) я получаю:

>>> import modloader 
>>> modloader.activateLoader() 
>>> import test 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
ImportError: No module named 'test' 
>>> 

Однако, когда я удалить содержимое sys.path_hooks перед добавлением крюк:

sys.path_hooks = [] 
sys.path.insert(0, '.') # current directory 
sys.path_hooks.append(myFinder.path_hook(loader_details)) 

это работает:

>>> modloader.activateLoader() 
>>> import test 
using xxx class 

in xxxLoader exec_module 
in xxxLoader get_code: .\test.xxx 
ANALYZING ... 

GENERATE CODE OBJECT ... 

    2   0 LOAD_CONST    0 
       3 LOAD_CONST    1 ('foo2') 
       6 MAKE_FUNCTION   0 
       9 STORE_NAME    0 (foo2) 
      12 LOAD_CONST    2 (None) 
      15 RETURN_VALUE 
>>>>>> test 
<module 'test' from '.\\test.xxx'> 

модуль импортируется корректно после преобразования содержимого файлов в объект кода.

Однако я не могу загрузить один и тот же модуль из пакета:import pack.test

Примечание: __init__.py, конечно, как пустой файл в директории пакета.

>>> import pack.test 
Traceback (most recent call last): 
    File "<frozen importlib._bootstrap>", line 2218, in _find_and_load_unlocked 
AttributeError: 'module' object has no attribute '__path__' 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
ImportError: No module named 'pack.test'; 'pack' is not a package 
>>> 

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

>>> import pack.testpy 
Traceback (most recent call last): 
    File "<frozen importlib._bootstrap>", line 2218, in _find_and_load_unlocked 
AttributeError: 'module' object has no attribute '__path__' 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
ImportError: No module named 'pack.testpy'; 'pack' is not a package 
>>> 

Для моего понимания sys.path_hooks проходятся, пока последняя запись не пробовала , Итак, почему первый вариант (без удаления sys.path_hooks) не распознает новое расширение «xxx», а второй вариант (удаление sys.path_hooks)? Похоже, что механизм генерирует исключение, а не переходит к следующей записи, когда запись sys.path_hooks не может распознать «xxx».

И почему вторая версия работает для ру, ВПГ и ххх модулей в текущем каталоге, но не работает в пакете pack? Я бы ожидал, что py и pyc даже не работают в текущем каталоге, потому что sys.path_hooks содержит только крючок для «xxx» ...

+0

Не использовать python 3, так что не могу помочь здесь - как бы резюмировать ваши вопросы, это помогло бы другому ответу. Может быть, попробуйте 'sys.path_hooks.insert (0, myFinder.path_hook (loader_details))'? Вопросы остаются –

ответ

4

Короткий ответ заключается в том, что по умолчанию PathFinder в sys.meta_path не предназначен для добавления новых расширений файлов и импортеров на тех же путях, которые он уже поддерживает. Но есть еще надежда!

Быстрый Разбивка

sys.path_hooks потребляется importlib._bootstrap_external.PathFinder класса.

При импорте каждая запись в sys.meta_path предлагается найти подходящую спецификацию для запрошенного модуля. В частности, PathFinder берет содержимое sys.path и передает его на заводские функции в sys.path_hooks. Каждая фабричная функция имеет шанс либо поднять ImportError (в основном на фабрике, говорящей «нет, я не поддерживаю эту запись пути»), либо возвратить экземпляр finder для этого пути. Первый успешно возвращенный искатель затем кэшируется в sys.path_importer_cache. С этого момента PathFinder будет запрашивать только те кэшированные поисковые экземпляры, если они могут предоставить запрошенный модуль.

Если вы посмотрите на содержимое sys.path_importer_cache, вы увидите, что все записи в каталоге от sys.path были сопоставлены с экземплярами FileFinder. Записи без каталогов (zip-файлы и т. Д.) Будут сопоставлены с другими искателями.

Таким образом, если вы добавили новый завод, созданный с помощью FileFinder.path_hook, до sys.path_hooks, ваш завод будет вызываться, только если предыдущий крючок FileFinder не принял путь. Это маловероятно, так как FileFinder будет работать с любым существующим каталогом.

В качестве альтернативы, если вы введете новую фабрику в sys.path_hooks перед существующими фабриками, крюк по умолчанию будет использоваться только в том случае, если ваш новый завод не принимает путь. И опять же, поскольку FileFinder настолько либеральна, что он примет, это приведет к использованию только вашего загрузчика, как вы уже заметили.

делая работу

Таким образом, вы можете либо попытаться настроить, что существующий завод также поддерживают ваше расширение файла и импортер (что трудно, как импортеры и расширение струнных кортежей проводятся в затворе), или сделайте то, что я закончил, добавив новый мета-поиск пути.

Так, например. от моего собственного проекта,


import sys 

from importlib.abc import FileLoader 
from importlib.machinery import FileFinder, PathFinder 
from os import getcwd 
from os.path import basename 

from sibilant.module import prep_module, exec_module 


SOURCE_SUFFIXES = [".lspy", ".sibilant"] 


_path_importer_cache = {} 
_path_hooks = [] 


class SibilantPathFinder(PathFinder): 
    """ 
    An overridden PathFinder which will hunt for sibilant files in 
    sys.path. Uses storage in this module to avoid conflicts with the 
    original PathFinder 
    """ 


    @classmethod 
    def invalidate_caches(cls): 
     for finder in _path_importer_cache.values(): 
      if hasattr(finder, 'invalidate_caches'): 
       finder.invalidate_caches() 


    @classmethod 
    def _path_hooks(cls, path): 
     for hook in _path_hooks: 
      try: 
       return hook(path) 
      except ImportError: 
       continue 
     else: 
      return None 


    @classmethod 
    def _path_importer_cache(cls, path): 
     if path == '': 
      try: 
       path = getcwd() 
      except FileNotFoundError: 
       # Don't cache the failure as the cwd can easily change to 
       # a valid directory later on. 
       return None 
     try: 
      finder = _path_importer_cache[path] 
     except KeyError: 
      finder = cls._path_hooks(path) 
      _path_importer_cache[path] = finder 
     return finder 


class SibilantSourceFileLoader(FileLoader): 


    def create_module(self, spec): 
     return None 


    def get_source(self, fullname): 
     return self.get_data(self.get_filename(fullname)).decode("utf8") 


    def exec_module(self, module): 
     name = module.__name__ 
     source = self.get_source(name) 
     filename = basename(self.get_filename(name)) 

     prep_module(module) 
     exec_module(module, source, filename=filename) 


def _get_lspy_file_loader(): 
    return (SibilantSourceFileLoader, SOURCE_SUFFIXES) 


def _get_lspy_path_hook(): 
    return FileFinder.path_hook(_get_lspy_file_loader()) 


def _install(): 
    done = False 

    def install(): 
     nonlocal done 
     if not done: 
      _path_hooks.append(_get_lspy_path_hook()) 
      sys.meta_path.append(SibilantPathFinder) 
      done = True 

    return install 


_install = _install() 
_install() 

SibilantPathFinder переопределяет PathFinder и заменяет только те методы, которые ссылаются на sys.path_hook и sys.path_importer_cache с подобными реализациями, которые вместо того, чтобы смотреть в _path_hook и _path_importer_cache, которые являются локальными для этого модуля.

Во время импорта существующий PathFinder попытается найти соответствующий модуль. Если это не удастся, тогда мой инъецированный SibilantPathFinder перевернет sys.path и попытается найти совпадение с одним из моих собственных расширений файлов.

Выяснения Больше Из

Я кончался вникая в источник для _bootstrap_external модуля https://github.com/python/cpython/blob/master/Lib/importlib/_bootstrap_external.py

_install функции и метода PathFinder.find_spec являются лучшими отправными точками, чтобы увидеть, почему вещи работают так, как они делать.

0

@ obriencj анализ ситуации правильный. Но я придумал другое решение этой проблемы, которое не требует ввода ничего в sys.meta_path. Вместо этого он устанавливает специальный крючок в sys.path_hooks, который действует почти как нечто среднее между PathFinder в sys.meta_path и крючками в sys.path_hooks, где вместо того, чтобы просто использовать первый крючок, который говорит «Я могу справиться с этим путем!». он пытается все соответствующие крючки в порядке, до тех пор, пока не найдет, что на самом деле возвращает полезный ModuleSpec от способа его find_spec:

@PathEntryFinder.register 
class MetaFileFinder: 
    """ 
    A 'middleware', if you will, between the PathFinder sys.meta_path hook, 
    and sys.path_hooks hooks--particularly FileFinder. 

    The hook returned by FileFinder.path_hook is rather 'promiscuous' in that 
    it will handle *any* directory. So if one wants to insert another 
    FileFinder.path_hook into sys.path_hooks, that will totally take over 
    importing for any directory, and previous path hooks will be ignored. 

    This class provides its own sys.path_hooks hook as follows: If inserted 
    on sys.path_hooks (it should be inserted early so that it can supersede 
    anything else). Its find_spec method then calls each hook on 
    sys.path_hooks after itself and, for each hook that can handle the given 
    sys.path entry, it calls the hook to create a finder, and calls that 
    finder's find_spec. So each sys.path_hooks entry is tried until a spec is 
    found or all finders are exhausted. 
    """ 

    class hook: 
     """ 
     Use this little internal class rather than a function with a closure 
     or a classmethod or anything like that so that it's easier to 
     identify our hook and skip over it while processing sys.path_hooks. 
     """ 

     def __init__(self, basepath=None): 
      self.basepath = os.path.abspath(basepath) 

     def __call__(self, path): 
      if not os.path.isdir(path): 
       raise ImportError('only directories are supported', path=path) 
      elif not self.handles(path): 
       raise ImportError(
        'only directories under {} are supported'.format(
         self.basepath), path=path) 

      return MetaFileFinder(path) 

     def handles(self, path): 
      """ 
      Return whether this hook will handle the given path, depending on 
      what its basepath is. 
      """ 

      path = os.path.abspath(path) 

      return (self.basepath is None or 
        os.path.commonpath([self.basepath, path]) == self.basepath) 

    def __init__(self, path): 
     self.path = path 
     self._finder_cache = {} 

    def __repr__(self): 
     return '{}({!r})'.format(self.__class__.__name__, self.path) 

    def find_spec(self, fullname, target=None): 
     if not sys.path_hooks: 
      return None 

     last = len(sys.path_hooks) - 1 

     for idx, hook in enumerate(sys.path_hooks): 
      if isinstance(hook, self.__class__.hook): 
       continue 

      finder = None 
      try: 
       if hook in self._finder_cache: 
        finder = self._finder_cache[hook] 
        if finder is None: 
         # We've tried this finder before and got an ImportError 
         continue 
      except TypeError: 
       # The hook is unhashable 
       pass 

      if finder is None: 
       try: 
        finder = hook(self.path) 
       except ImportError: 
        pass 

      try: 
       self._finder_cache[hook] = finder 
      except TypeError: 
       # The hook is unhashable for some reason so we don't bother 
       # caching it 
       pass 

      if finder is not None: 
       spec = finder.find_spec(fullname, target) 
       if (spec is not None and 
         (spec.loader is not None or idx == last)): 
        # If no __init__.<suffix> was found by any Finder, 
        # we may be importing a namespace package (which 
        # FileFinder.find_spec returns in this case). But we 
        # only want to return the namespace ModuleSpec if we've 
        # exhausted every other finder first. 
        return spec 

     # Module spec not found through any of the finders 
     return None 

    def invalidate_caches(self): 
     for finder in self._finder_cache.values(): 
      finder.invalidate_caches() 

    @classmethod 
    def install(cls, basepath=None): 
     """ 
     Install the MetaFileFinder in the front sys.path_hooks, so that 
     it can support any existing sys.path_hooks and any that might 
     be appended later. 

     If given, only support paths under and including basepath. In this 
     case it's not necessary to invalidate the entire 
     sys.path_importer_cache, but only any existing entries under basepath. 
     """ 

     if basepath is not None: 
      basepath = os.path.abspath(basepath) 

     hook = cls.hook(basepath) 
     sys.path_hooks.insert(0, hook) 
     if basepath is None: 
      sys.path_importer_cache.clear() 
     else: 
      for path in list(sys.path_importer_cache): 
       if hook.handles(path): 
        del sys.path_importer_cache[path] 

Это еще, удручает, гораздо больше, чем усложнение должно быть необходимым. Мне кажется, что на Python 2, прежде чем система импорта будет переписываться, было намного проще сделать это, поскольку меньше поддержки встроенных типов модулей (.py и т. Д.) Было построено поверх самих импортных крючков, поэтому было труднее разбить импорт обычных модулей, добавив крючки для импорта новых типов модулей. Я собираюсь начать обсуждение идей python, чтобы убедиться, что мы не сможем улучшить эту ситуацию.

Смежные вопросы