导入 AST 修改

Importing AST modification

我正在尝试在导入之前动态重写 pywin32 win32com.client 模块,以下似乎有效 - 但我对导入的 6 行代码(在最后一条评论之后)不满意。谁能推荐一种更简洁的方法来创建/导入生成的模块(包)?

import sys, ast, types
import os.path
import importlib.util

import win32com

# import ast
_win32com_client_spec = importlib.util.find_spec("win32com.client")
_win32com_client_ast = ast.parse(open(_win32com_client_spec.origin).read())

(_win32com_client_dispatch_ast,) = (x for x in _win32com_client_ast.body if isinstance(x, ast.FunctionDef) and x.name == 'Dispatch')

# rename Dispatch
_win32com_client_dispatch_ast.name = '_OldDispatch'

# create new Dispatch
_my_dispatch_mod_ast = ast.parse("""
def Dispatch(*args, **kwds):
    base_inst = _OldDispatch(*args, **kwds)
    spec_inst = _OldDispatch(base_inst)
    return spec_inst
""")
(_my_dispatch_ast,) = _my_dispatch_mod_ast.body

# insert new Dispatch in module
_win32com_client_ast.body.insert(_win32com_client_ast.body.index(_win32com_client_dispatch_ast)+1, _my_dispatch_ast)

# import the package
_my_win32com_client_co = compile(_win32com_client_ast, '<patched win32com.client>', 'exec')
_my_win32com_client_mod = types.ModuleType('win32com.client')
_my_win32com_client_mod.__path__ = (os.path.dirname(_win32com_client_spec.origin),)
sys.modules[_my_win32com_client_mod.__name__] = _my_win32com_client_mod
win32com.client = _my_win32com_client_mod
exec(_my_win32com_client_co, _my_win32com_client_mod.__dict__)

进一步阅读文档显示使用 ModuleSpec 对象优于使用 types.ModuleType

参考:https://docs.python.org/3/library/importlib.html#importlib.util.module_from_spec

生成的代码隐式处理包的 __path__,并允许在整个过程中重用规范对象。请注意,如果使用 spec origin 来显示补丁,file 属性应如下所示明确设置。

模块应该在被执行之前被导入,这似乎对于子模块导入工作很重要,就像 gencache 模块中发现的那样。 import win32com.client.CLSISToClass:

import sys, ast
import importlib.util

import win32com

if 'win32com.client' not in sys.modules:
    # import ast
    _win32com_client_spec = importlib.util.find_spec("win32com.client")
    _win32com_client_path = _win32com_client_spec.origin
    _win32com_client_spec.origin = '<patched win32com.client>'
    _win32com_client_ast = ast.parse(open(_win32com_client_path).read())

    # rename Dispatch
    (_win32com_client_dispatch_ast,) = (x for x in _win32com_client_ast.body if isinstance(x, ast.FunctionDef) and x.name == 'Dispatch')
    _win32com_client_dispatch_ast.name = '_OldDispatch'

    # create new Dispatch (disregard line numbers)
    _my_dispatch_mod_ast = ast.parse("""
def Dispatch(*args, **kwds):
    base_inst = _OldDispatch(*args, **kwds)
    spec_inst = _OldDispatch(base_inst)
    return spec_inst
""")
    (_my_dispatch_ast,) = _my_dispatch_mod_ast.body

    # insert new Dispatch in module
    _win32com_client_ast.body.insert(_win32com_client_ast.body.index(_win32com_client_dispatch_ast)+1, _my_dispatch_ast)

    # compile (original path for debugging), create, import and exec the module
    _my_win32com_client_co = compile(_win32com_client_ast, _win32com_client_path, 'exec')
    _my_win32com_client_mod = importlib.util.module_from_spec(_win32com_client_spec)
    _my_win32com_client_mod.__file__ = _win32com_client_path
    win32com.client = sys.modules['win32com.client'] = _my_win32com_client_mod
    try:
        exec(_my_win32com_client_co, _my_win32com_client_mod.__dict__)
    except:
        del win32com.client, sys.modules['win32com.client']
        raise