Как реализовать ловушку импорта, которая может изменять исходный код на лету с помощью importlib?

Использование устаревшего модуля impЯ могу написать пользовательский хук импорта, который изменяет исходный код модуля на лету, перед импортом / выполнением Python. Учитывая исходный код в виде строки с именем source ниже необходим код, необходимый для создания модуля:

module = imp.new_module(name)
sys.modules[name] = module
exec(source, module.__dict__)

поскольку imp устарела, я хотел бы сделать что-то подобное с importlib, [Редактировать: есть другие imp методы, которые необходимо заменить для создания пользовательского хука импорта - поэтому я ищу ответ не просто на замену приведенного выше кода.]

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

Я создал минимальный пример, чтобы продемонстрировать это; см. файл readme для деталей.

2 ответа

Решение

find_module а также load_module оба устарели. Вам нужно будет переключиться на find_spec а также (create_module а также exec_module) модуль соответственно. Увидеть importlib документация для деталей.

Вам также нужно будет проверить, хотите ли вы использовать MetaPathFinder или PathEntryFinder так как система их вызова различна. Таким образом, мета-путь поиска идет первым и может переопределить встроенные модули, тогда как поиск входа пути работает специально для модулей, найденных в sys.path,

Следующее является очень основным импортером, который пытается заменить всю импортную технику. Это показывает, как использовать функции (find_spec, create_module, а также exec_module).

import sys
import os.path

from importlib.abc import Loader, MetaPathFinder
from importlib.util import spec_from_file_location

class MyMetaFinder(MetaPathFinder):
    def find_spec(self, fullname, path, target=None):
        if path is None or path == "":
            path = [os.getcwd()] # top level import -- 
        if "." in fullname:
            *parents, name = fullname.split(".")
        else:
            name = fullname
        for entry in path:
            if os.path.isdir(os.path.join(entry, name)):
                # this module has child modules
                filename = os.path.join(entry, name, "__init__.py")
                submodule_locations = [os.path.join(entry, name)]
            else:
                filename = os.path.join(entry, name + ".py")
                submodule_locations = None
            if not os.path.exists(filename):
                continue

            return spec_from_file_location(fullname, filename, loader=MyLoader(filename),
                submodule_search_locations=submodule_locations)

        return None # we don't know how to import this

class MyLoader(Loader):
    def __init__(self, filename):
        self.filename = filename

    def create_module(self, spec):
        return None # use default module creation semantics

    def exec_module(self, module):
        with open(self.filename) as f:
            data = f.read()

        # manipulate data some way...

        exec(data, vars(module))

def install():
    """Inserts the finder into the import machinery"""
    sys.meta_path.insert(0, MyMetaFinder())

Далее следует чуть более деликатная версия, которая пытается повторно использовать больше импортного оборудования. Таким образом, вам нужно только определить, как получить источник модуля.

import sys
from os.path import isdir
from importlib import invalidate_caches
from importlib.abc import SourceLoader
from importlib.machinery import FileFinder


class MyLoader(SourceLoader):
    def __init__(self, fullname, path):
        self.fullname = fullname
        self.path = path

    def get_filename(self, fullname):
        return self.path

    def get_data(self, filename):
        """exec_module is already defined for us, we just have to provide a way
        of getting the source code of the module"""
        with open(filename) as f:
            data = f.read()
        # do something with data ...
        # eg. ignore it... return "print('hello world')"
        return data


loader_details = MyLoader, [".py"]

def install():
    # insert the path hook ahead of other path hooks
    sys.path_hooks.insert(0, FileFinder.path_hook(loader_details))
    # clear any loaders that might already be in use by the FileFinder
    sys.path_importer_cache.clear()
    invalidate_caches()

Смотрите также этот замечательный проект https://pypi.org/project/importhook/

      pip install importhook
      import importhook

# Setup hook to be called any time the `socket` module is imported and loaded into module cache
@importhook.on_import('socket')
def on_socket_import(socket):
    new_socket = importhook.copy_module(socket)
    setattr(new_socket, 'gethostname', lambda: 'patched-hostname')
    return new_socket

# Import module
import socket

# Prints: 'patched-hostname'
print(socket.gethostname())
Другие вопросы по тегам