Welcome, guest | Sign In | My Account | Store | Cart

This recipe uses the PEP 302 import hooks to expose all imported modules to devious behavior.

Simply put, the module is imported like normal and then passed to a hacker object that gets to do whatever it wants to the module. Then the return value from the hack call is put into sys.modules.

Recipe 577741 and recipe 577742 are more concrete examples of using this recipe.

Python, 33 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"""modulehacker module"""

import sys
import importlib

_hackers = []
def register(obj):
    _hackers.append(obj)

class Hacker:
    def hack(self, module):
        return module

class Loader:
    def __init__(self):
        self.module = None
    
    def find_module(self, name, path):
        sys.meta_path.remove(self)
        self.module = importlib.import_module(name)
        sys.meta_path.insert(0, self)
        return self
    
    def load_module(self, name):
        if not self.module:
            raise ImportError("Unable to load module.")
        module = self.module
        for hacker in _hackers:
            module = hacker.hack(module)
        sys.modules[name] = module
        return module

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

Example 1

Sets a __module__ attribute on each module, bound to itself.

<hacker.py>

import modulehacker

class Hacker(modulehacker.Hacker):
    def hack(self, module):
        module.__module__ = module
        return module

modulehacker.register(Hacker())

<somemodule.py>

NAME = "X"

<main.py>

import hacker
import somemodule

print(somemodule.__module__)
# somemodule

Example 2

Tack dynamically generated notes to each module's docstring.

<docadd.py>

import modulehacker

class Hacker(modulehacker.Hacker):
    def __init__(self, notegen):
        self.notegen = notegen
    def hack(self, module):
        note = self.notegen(module)
        if not module.__doc__:
            module.__doc__ = note
        elif module.__doc__.endswith("\n"):
            module.__doc__ += note
        else:
            module.__doc__ += "\n" + note
        return module

<somemodule.py>

ID = "X"

<othermodule.py>

ID = "Y"
class Test: pass

<main.py>

import sys

def order(module):
    return "import order: %s" % len(sys.modules)
def classes(module):
    return "classes: " + ", ".join(
            attr for attr in module.__dict__
            if isinstance(getattr(module, attr), type)
            )

import modulehacker
import docadd
modulehacker.register(docadd.Hacker(order))
modulehacker.register(docadd.Hacker(classes))
import somemodule
import othermodule

print(somemodule.__doc__)
# import order: ...
# classes: 

print(somemodule.__doc__)
# import order: ... + 1
# classes: Test

1 comment

Robin Becker 7 years, 6 months ago  # | flag

When I tried this raw in a sitecustomize.py I found that there's a bug in the implementation of find_module. Presumably the intention is to have the import inject the Loader permanently into sys.meta_path. However, any failing import will leave the Loader out of sys.meta_path. To fix this I used this at line 20

sys.meta_path.remove(self)
try:
    self.module = importlib.import_module(name)
finally:
    sys.meta_path.insert(0, self)

this seems to do it for me.