Как реализовать ловушку импорта, которая может изменять исходный код на лету с помощью 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())