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