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.
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
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
this seems to do it for me.