Контейнеры импорта пакетов в Python

Я работаю над созданием модуля Python, который отображает API, предоставляемый другим языком/фреймворком в Python. В идеале я бы хотел, чтобы это было представлено как один корневой пакет, который предоставляет вспомогательные методы и который сопоставляет все пространства имен в этой другой структуре с пакетами/модулями Python. Для удобства рассмотрим CLR в качестве примера:

import clr.System.Data
import clr.System.Windows.Forms

Здесь clr - это магический пакет верхнего уровня, который предоставляет пространства имен CLR System.Data и System.Windows.Forms подпакеты/подмодули (насколько я вижу, пакет - это всего лишь модуль с дочерними модулями/пакетами; все еще действительный, чтобы иметь в нем другие типы участников).

Я прочитал PEP-302 и написал простую прототипную программу, которая достигает аналогичного эффекта, установив пользовательский meta_path hook. Сам модуль clr является правильным модулем Python, который при импорте устанавливает __path__ = [] (делает его пакетом, так что import даже пытается искать подмодули вообще) и регистрирует hook. Крюк сам перехватывает любую загрузку пакета, где полное имя пакета начинается с "clr.", динамически создает новый модуль с помощью imp.new_module(), регистрирует его в sys.modules и использует пиксельные пыль и радуги, чтобы заполнить его классами и методами из оригинальный API. Здесь код:

clr.py

import sys
import imp

class MyLoader:
    def load_module(self, fullname):
        try:
            return sys.modules[fullname]
        except KeyError:
            pass
        print("--- load ---")
        print(fullname)
        m = imp.new_module(fullname)
        m.__file__ = "clr:" + fullname
        m.__path__ = []
        m.__loader__ = self
        m.speak = lambda: print("I'm " + fullname)
        sys.modules.setdefault(fullname, m)
        return m

class MyFinder:
    def find_module(self, fullname, path = None):
        print("--- find ---")
        print(fullname)
        print(path)
        if fullname.startswith("clr."):
            return MyLoader()            
        return None

print("--- init ---")
__path__ = []
sys.meta_path.append(MyFinder())

test.py

import clr.Foo.Bar.Baz

clr.Foo.speak()
clr.Foo.Bar.speak()
clr.Foo.Bar.Baz.speak()

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

Однако, мне интересно, что то, что я здесь делаю, слишком велико. В конце концов, я устанавливаю глобальный крючок, который будет вызван для импорта любого модуля, хотя я отфильтровываю те, которые мне не нужны. Есть ли, возможно, какой-то способ установить крючок, который будет вызван только для импорта из моего конкретного пакета, а не других? Или это правильный способ сделать это на Python?

Ответ 1

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

Просто одна небольшая проблема, может быть, вы могли бы использовать sys.path_hooks? Он кажется немного менее "мощным", чем sys.meta_path

sys.path_hooks - список вызываемых вызовов, которые будут проверяться в чтобы определить, могут ли они обрабатывать данный элемент пути. вызываемый вызывается с одним аргументом, элементом пути. Вызываемый должен поднимать ImportError, если он не может обработать элемент пути, и вернуть объект импортера, если он может обрабатывать элемент пути.