Шаг 198.
Python: сборник рецептов. Модули и пакеты. Загрузка модулей с удаленного компьютера с использованием хуков импортирования. Обсуждение (окончание)

    На этом шаге мы закончим обсуждение этого вопроса.

    Используя свои знания о том, как обрабатывается sys.path, вы можете инсталлировать кастомную функцию проверки пути, которая ищет шаблоны имен файлов, такие как URL'ы. Например:

>>> def check_url(path):
	if path.startswith('http://'):
		return Finder()
	else:
		raise ImportError()

	
>>> sys.path.append('http://localhost:15000')
>>> sys.path_hooks[0] = check_url
>>> import fib
Looking for fib
# Вывод поисковика!
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    import fib
ModuleNotFoundError: No module named 'fib'
>>> # Обратите внимание на установку поисковика в sys.path_importer_cache
>>> sys.path_importer_cache['http://localhost:15000']
<__main__.Finder object at 0x00000211120E69A0>
>>> 

    Это ключевой механизм работы последней части данного рецепта. По сути, кастомная функция проверки пути была установлена, чтобы искать URL'ы в sys.path. Когда они встречаются, создается новый экземпляр UrlPathFinder и инсталлируется в sys.path_importer_cache. Начиная с данного момента, все инструкции импортирования, которые проходят через эту часть sys.path, будут пытаться использовать ваш кастомный поисковик.

    Работа с пакетами с помощью импортировщика, основанного на путях, достаточно сложна и опирается на возвращаемое значение метода find_loader(). Для простых модулей find_loader() возвращает кортеж (loader, None), где loader - это экземпляр загрузчика, который будет импортировать модуль.

    Для обычного пакета find_loader() возвращает кортеж (loader, path), где loader - это экземпляр загрузчика, который будет импортировать пакет (и выполнять __ init__.py), а path - это список каталогов, которые составят начальное наполнение атрибута пакета __path__. Например, если базовый URL был http://localhost:15000, а пользователь выполнил import grok, возвращенный find_loader() путь будет таким: ['http://localhost:15000/grok'].

    find_loader() должен дополнительно учитывать возможность столкнуться с пакетом пространства имен. Это пакет, где присутствуют валидные имена каталогов пакета, но отсутствуют файлы __init__.py. Для этого случая find_loader() должен возвращать кортеж (None, path), где path - это список каталогов, которые составили бы атрибут пакета __path__, определенный файлом __init__.py. В этом случае механизм импортирования переходит дальше, чтобы проверить следующие каталоги в sys.path. Если будут найдены дополнительные пакеты пространств имен, все получившиеся пути соединяются вместе, чтобы образовать финальный пакет пространств имен. См. 187 шаг, где приведена дополнительная информация о пакетах пространств имен.

    В обработке пакетов есть элемент рекурсии, что не очевидно ни в решении, ни в работе. Все пакеты содержат внутреннюю настройку путей, которая может быть найдена в атрибуте __path__. Например:

>>> import xml.etree.ElementTree
Looking for org
>>> xml.__path__
['C:\\Python38\\lib\\xml']
>>> xml.etree.__path__
['C:\\Python38\\lib\\xml\\etree']
>>> 

    Как было упомянуто, установка __path__ контролируется путем возвращаемого значения метода find_loader(). Однако последующая обработка __path__ также производится функциями из sys.path_hooks. Когда подкомпоненты пакета загружаются, записи в __path__ проверяются функцией handle_url(). Это вызывает создание новых экземпляров UrlPathFinder и добавление их в sys.path_importer_cache.

    Оставшаяся сложная часть этой реализации касается поведения функции handle_url() и ее взаимодействия с используемой внутри функцией _get_links(). Если ваша реализация поисковика использует другие модули (например, urllib.request), существует вероятность того, что эти модули предпримут попытки импортирования в середине выполнения операции поисковика. Это может вызвать рекурсивный цикл выполнения handle_url() и других частей поисковика. Чтобы учесть такую возможность, реализация поддерживает кеш созданных поисковиков (по одному на URL). Это позволяет избежать проблемы с созданием дублирующихся поисковиков. Следующий фрагмент кода удостоверяет, что поисковик не отвечает ни на какие запросы импортирования, пока он находится в процессе получения начального набора ссылок:

# Проверяем кеш ссылок 
if self._links is None:
    self._links = []   # См. обсуждение
    self._links = _get_links(self._baseurl)

    Эта проверка может и не понадобиться вам в других реализациях, но для этого примера, использующего URL, она необходима.

    И наконец, метод invalidate_caches() обоих поисковиков - это вспомогательный метод, который должен очищать внутренние кеши при изменении исходного кода. Этот метод вызывается, когда пользователь вызывает importlib.invalidate_caches(). Вы можете использовать его, если хотите, чтобы импортировщики URL перечитали список ссылок - возможно, чтобы получить доступ к добавленным файлам.

    В сравнении двух подходов (изменении sys.meta_path и использовании хуков пути) помогает "высокоуровневый взгляд". Импортировщики, установленные с использованием sys.meta_path, свободны обрабатывать модули так, как пожелают. Например, они могут загружать модули из базы данных или импортировать их способом, который радикально отличается от стандартного процесса для модулей/пакетов. Эта свобода также означает, что импортировщики должны вести больше "бухгалтерии" и заниматься внутренним управлением. Это объясняет, например, почему реализация UrlMetaFinder требует собственного кеширования ссылок, загрузчиков и прочих деталей. С другой стороны, основанные на путях хуки сильнее привязаны к обработке sys.path. По причине связи с sys.path модули, загружаемые с помощью таких расширений, будут склонны иметь те же возможности, что и обычные модули и пакеты, к которым привыкли программисты.

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

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> import urlimport
>>> urlimport.install_path_hook()
DEBUG:urlimport:Installing handle_url
>>> import fib
DEBUG:urlimport:Handle path? /usr/local/lib/python33.zip. [No]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'fib'
>>> import sys
>>> sys.path.append('http://localhost:15000')
>>> import fib
DEBUG:urlimport:Handle path? http://localhost:15000. [Yes]
DEBUG:urlimport:Getting links from http://localhost:15000
DEBUG:urlimport:links: {'spam.py', 'fib.py', 'grok'}
DEBUG:urlimport:find_loader: 'fib'
DEBUG:urlimport:find_loader: module 'fib' found
DEBUG:urlimport:loader: reading 'http://localhost:15000/fib.py'
DEBUG:urlimport:loader: 'http://localhost:15000/fib.py' loaded
I'm fib
>>>

    И последнее: рекомендуем провести некоторое время в медитации над PEP 302 и документации к importlib.

    На следующем шаге мы рассмотрим применение к модулям изменений при импорте.




Предыдущий шаг Содержание