Python 覆盖:猴子补丁的案例
Python Overlays : A case for monkey Patching
我正在尝试 wrap/monkey 修补 python 中的模块。我正在尝试开发一种干净的方法来实现它,它不会干扰任何现有代码。
问题
给定一个从 MODULE
导入一些 CLASS
的脚本
from MODULE import CLASS
我想用另一个 _MODULE_
替换 MODULE
。其中 _MODULE_
是原始 MODULE
的补丁。我能看到的最干净的界面如下。
from overlay import MODULE # Switches MODULE for _MODULE
from MODULE import CLASS # Original import now uses _MODULE_
这基本上是猴子修补模块,就像猴子修补 类、函数和方法一样。我相信如果正确地完成这项工作,就可以始终如一地以项目特定的方式修补代码。
实现这个的最佳方法是什么?
>>> import wrapt
>>> @wrapt.when_imported('collections')
... def hook(collections):
... OldOrderedDict = collections.OrderedDict
... class MyOrderedDict(OldOrderedDict):
... def monkey(self):
... print('ook ook')
... collections.OrderedDict = MyOrderedDict
...
>>> from collections import OrderedDict
>>> OrderedDict().monkey()
ook ook
@wim 的回答当然更好,但我已经摆弄了一段时间,这是我最好的 bash。我假定以下 folder/file 结构:
PACKAGE/
overlay.py
_decimal_.py
__main__.py
在 _decimal_.py
中,我包括以下几行
from decimal import *
__version__ = "X.Y"
在 overlay.py
我有 :
import importlib
import inspect
import builtins
import os
from pathlib import Path
modsep = '.'
class OverlayImporter(object):
def __init__(self, *args, path = None, root = None, _import_ = __import__, **kvps):
super().__init__(*args, **kvps)
self.mask = "_{}_"
self.root = Path(root or os.path.dirname(inspect.getmodule(inspect.stack()[1][0]).__file__))
self.mods = self.modules()
# Substitutes Import Functionality
builtins.__import__ = self
self.imp = _import_
self.lom = []
def __call__(self, name, *args) : # (self, *args, *kvps):
# Hooks the import statement
if self.mapToTarget(name) in self.mods.keys() :
if name in self.lom :
return self.imp(name, *args)
self.lom.append(name)
return importlib.import_module(self.mapToTarget(name)) # This is a little black magic as we ignore the args
return self.imp(name, *args)
def mapToTarget(self, name) :
"""Maps request to the overlay module"""
# Converts PACKAGE.MODULE to overlay._PACKAGE_._MODULE_
return modsep.join([self.mask.format(part) for part in name.split(modsep)])
def modules(self) :
""" Lists the overlays implemented within a directory """
ext = '.py'
mod = lambda parts, ext : [part[:-len(ext)] if enum + 1 == len(parts) else part for enum, part in enumerate(parts)]
lst = [(mod(file.relative_to(self.root).parts, ext), file) for file in self.root.rglob('*'+ext)]
return {modsep.join(item[0][:-1]) if item[0][-1] == "__init__" else modsep.join(item[0]) : item[1] for item in lst}
在 __main_.py
之内我有
from overlay import OverlayImporter
OverlayImporter()
import decimal
print(decimal.__version__)
注释主文件中的前两行可在 decimal 的修补版本和未修补版本之间切换。
我正在尝试 wrap/monkey 修补 python 中的模块。我正在尝试开发一种干净的方法来实现它,它不会干扰任何现有代码。
问题
给定一个从 MODULE
CLASS
的脚本
from MODULE import CLASS
我想用另一个 _MODULE_
替换 MODULE
。其中 _MODULE_
是原始 MODULE
的补丁。我能看到的最干净的界面如下。
from overlay import MODULE # Switches MODULE for _MODULE
from MODULE import CLASS # Original import now uses _MODULE_
这基本上是猴子修补模块,就像猴子修补 类、函数和方法一样。我相信如果正确地完成这项工作,就可以始终如一地以项目特定的方式修补代码。
实现这个的最佳方法是什么?
>>> import wrapt
>>> @wrapt.when_imported('collections')
... def hook(collections):
... OldOrderedDict = collections.OrderedDict
... class MyOrderedDict(OldOrderedDict):
... def monkey(self):
... print('ook ook')
... collections.OrderedDict = MyOrderedDict
...
>>> from collections import OrderedDict
>>> OrderedDict().monkey()
ook ook
@wim 的回答当然更好,但我已经摆弄了一段时间,这是我最好的 bash。我假定以下 folder/file 结构:
PACKAGE/
overlay.py
_decimal_.py
__main__.py
在 _decimal_.py
中,我包括以下几行
from decimal import *
__version__ = "X.Y"
在 overlay.py
我有 :
import importlib
import inspect
import builtins
import os
from pathlib import Path
modsep = '.'
class OverlayImporter(object):
def __init__(self, *args, path = None, root = None, _import_ = __import__, **kvps):
super().__init__(*args, **kvps)
self.mask = "_{}_"
self.root = Path(root or os.path.dirname(inspect.getmodule(inspect.stack()[1][0]).__file__))
self.mods = self.modules()
# Substitutes Import Functionality
builtins.__import__ = self
self.imp = _import_
self.lom = []
def __call__(self, name, *args) : # (self, *args, *kvps):
# Hooks the import statement
if self.mapToTarget(name) in self.mods.keys() :
if name in self.lom :
return self.imp(name, *args)
self.lom.append(name)
return importlib.import_module(self.mapToTarget(name)) # This is a little black magic as we ignore the args
return self.imp(name, *args)
def mapToTarget(self, name) :
"""Maps request to the overlay module"""
# Converts PACKAGE.MODULE to overlay._PACKAGE_._MODULE_
return modsep.join([self.mask.format(part) for part in name.split(modsep)])
def modules(self) :
""" Lists the overlays implemented within a directory """
ext = '.py'
mod = lambda parts, ext : [part[:-len(ext)] if enum + 1 == len(parts) else part for enum, part in enumerate(parts)]
lst = [(mod(file.relative_to(self.root).parts, ext), file) for file in self.root.rglob('*'+ext)]
return {modsep.join(item[0][:-1]) if item[0][-1] == "__init__" else modsep.join(item[0]) : item[1] for item in lst}
在 __main_.py
之内我有
from overlay import OverlayImporter
OverlayImporter()
import decimal
print(decimal.__version__)
注释主文件中的前两行可在 decimal 的修补版本和未修补版本之间切换。