Python中的懒惰monkeypatch

时间:2018-01-15 05:53:01

标签: python python-import

我正在为live coding in Python编写一个工具,如果用户导入它,我想要monkeypatch matplotlib。虽然导入速度有点慢,所以我不想导入它并在用户不导入它时进行monkeypatch。

我通读了导入钩子documentation,并且我在Python 3中使用了它。

# scratch.py
from importlib import import_module
import sys

class PatchedModuleFinder(object):
    def find_module(self, fullname, path=None):
        if fullname not in ('matplotlib', 'matplotlib.pyplot'):
            return None
        is_after = False
        for finder in sys.meta_path:
            if not is_after:
                is_after = finder is self
                continue
            loader = finder.find_module(fullname, path)
            if loader is not None:
                return PatchedMatplotlibLoader(fullname, loader)


class PatchedMatplotlibLoader(object):
    def __init__(self, fullname, main_loader):
        self.fullname = fullname
        self.main_loader = main_loader

    def load_module(self, fullname):
        if self.main_loader is not None:
            module = self.main_loader.load_module(fullname)
        else:
            module = import_module(fullname)
            PatchedModuleFinder.is_desperate = False
        if fullname == 'matplotlib':
            module.use('Agg')
        elif fullname == 'matplotlib.pyplot':
            module.show = self.mock_show
        return module

    def mock_show(self, *args, **kwargs):
        print('mock_show:', args, kwargs)


sys.meta_path.insert(0, PatchedModuleFinder())

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

plt.plot([1, 2, 5])
plt.show()

这适用于Python 3,但不适用于Python 2.7:

$ .tox/py36/bin/python scratch.py
mock_show: () {}
$ .tox/py27/bin/python scratch.py
$ 

我的工具在Python 2.7和Python 3.x上都运行,那么我怎样才能在两个版本中懒洋洋地对一个模块进行monkeypatch?

1 个答案:

答案 0 :(得分:1)

这很难看,但确实有效。希望有人能提出更好的选择。

# scratch.py
from importlib import import_module
import sys

class PatchedModuleFinder(object):
    is_desperate = False

    def find_module(self, fullname, path=None):
        if fullname not in ('matplotlib', 'matplotlib.pyplot'):
            return None
        is_after = False
        for finder in sys.meta_path:
            if not is_after:
                is_after = finder is self
                continue
            loader = finder.find_module(fullname, path)
            if loader is not None:
                return PatchedMatplotlibLoader(fullname, loader)
        if sys.version_info < (3, 0) and not PatchedModuleFinder.is_desperate:
            # Didn't find anyone to load the module, get desperate.
            PatchedModuleFinder.is_desperate = True
            return PatchedMatplotlibLoader(fullname, None)


class PatchedMatplotlibLoader(object):
    def __init__(self, fullname, main_loader):
        self.fullname = fullname
        self.main_loader = main_loader
        self.plt = None

    def load_module(self, fullname):
        if self.main_loader is not None:
            module = self.main_loader.load_module(fullname)
        else:
            module = import_module(fullname)
            PatchedModuleFinder.is_desperate = False
        if fullname == 'matplotlib':
            module.use('Agg')
        elif fullname == 'matplotlib.pyplot':
            self.plt = module
            module.show = self.mock_show
        return module

    def mock_show(self, *args, **kwargs):
        print('mock_show:', args, kwargs)


sys.meta_path.insert(0, PatchedModuleFinder())

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

plt.plot([1, 2, 5])
plt.show('x', 23, foo='bar')

现在它适用于:

$ .tox/py36/bin/python scratch.py
mock_show: ('x', 23) {'foo': 'bar'}
$ .tox/py27/bin/python scratch.py
('mock_show:', ('x', 23), {'foo': 'bar'})