Customize your module objects by using this recipe!
All you have to do is import the module containing this code, register the custom module class you want, and stick __moduleclass__ the module that you want customized. The example should explain it all.
This recipe uses recipe 577740.
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 | """moduleclass module"""
import modulehacker # from ActiveState recipe #577740
class Module:
def __init__(self, module):
attrs = dict(self.__dict__)
for attr in module.__dict__:
if attr in attrs:
continue
setattr(self, attr, getattr(module, attr))
_moduleclasses = {}
def register(cls):
_moduleclasses[cls.__name__] = cls
return cls
class Hacker(modulehacker.Hacker):
def hack(self, module):
name = getattr(module, "__moduleclass__", None)
if not name:
return module # untouched
cls = _moduleclasses.get(name)
if not cls:
raise ImportError("Cannot use an unregistered module class")
newmodule = cls(module)
newmodule.__moduleclass__ = cls
return newmodule
modulehacker.register(Hacker())
|
Example 1
<mymodule.py>
import moduleclass
@moduleclass.register
class MyModule(moduleclass.Module):
COUNT = 0
def __init__(self, module=None):
super().__init__(module)
self._toggle = False
self.__class__.COUNT += 1
@property
def TOGGLED(self):
self._toggle = not self._toggle
return self._toggle
<somemodule.py>
__moduleclass__ = "MyModule"
NAME = "X"
<othermodule.py>
__moduleclass__ = "MyModule"
NAME = "X"
<__main__>
import mymodule
print(mymodule.MyModule.COUNT)
# 0
import somemodule
print(somemodule.__moduleclass__)
# '<class 'mymodule.MyModule'>
print(somemodule.TOGGLED)
# True
print(somemodule.TOGGLED)
# False
print(mymodule.MyModule.COUNT)
# 1
import othermodule
print(mymodule.MyModule.COUNT)
# 2
Example 2
<singleton.py>
import moduleclass
@moduleclass.register
class Singleton(moduleclass.Module):
INSTANCE = None
def __new__(cls, *args):
if not cls.INSTANCE:
cls.INSTANCE = super().__new__(cls, *args)
return cls.INSTANCE
<somemodule.py>
__moduleclass__ = "Singleton"
NAME = "X"
<othermodule.py>
__moduleclass__ = "Singleton"
NAME = "Y"
<__main__>
import singletonmodule
import somemodule
print(somemodule.NAME)
# 'X'
import othermodule
print(somemodule is othermodule)
# True
print(othermodule.NAME)
# 'X'
print(somemodule.NAME)
# 'X'
somemodule.IMPORTANT = True
print(othermodule.IMPORTANT)
# True
print(somemodule.IMPORTANT)
# True
Background
By default, when you import a module in Python, it checks sys.modules to see if the module (by name) is already there. If it is, then it binds the name from your import to that module object. If it is not there, then a new module object is created from the builtin module type.
As a result, all the modules in sys.modules (and therefore all your modules) will be of this same time. However, there are times when you want modules to behave differently than normal, like when you want to generate deprecation warnings on module attributes.
You can certainly create your own objects and stick them directly into sys.modules. Any object can go in there. You can also write PEP 302 import hooks to customize import behavior, and thereby the type of the module objects that get created during import.
The Approach
This recipe looks at a middle-ground between the two. It provides an import hook that will simply look for a __moduleclass__ in each module that gets imported, and use that to create the module object if found. It will default to the builtin module object.
The __moduleclass__ mechanism is analogous to metaclasses in Python. In 2.x you would define __metaclass__ in the class body (3.x puts it in the class header) and __build_class__ would use that to generate a new type object, your class. So here, instead of new type objects, we are generating new module objects.
More Details
The approach of this recipe has a couple of tricky spots:
- What should be bound to __moduleclass__?
- How to access the module in order to extract the __moduleclass__ value?
For the first, you'll get a NameError if __moduleclass__ is not a name the interpreter can identify. So we can tackle this several different ways:
- add the moduleclass to the __builtin__ module,
- extract the moduleclass before compiling the module,
- assign a string literal (for the name) to __moduleclass__.
For simplicity sake we are going to just use a string and a moduleclass registration mechanism.
The other tricky spot also has a couple of different ways we can address it:
- pre-import the module, extract the __moduleclass__, and then replace the other module object in sys.modules,
- go find the file and extract it before compiling.
The first one has the added benefit of resulting in the proper import whether or not the module has __moduleclass__. The second solution could entail finding the normally matching finder, pulling the source using the found loader (if possible) and parsing that. In that case the load_module method has to take care of compilation, unlike the first solution.
Notes
One cool approach to hacking modules is exocet (also see the related blog posts).
Another interesting topic to investigate is making a __build_module__ that is analogous to the builtin __build_class__. The builtin __import__ does this already, but you can't pass the module class like you can pass the metaclass for __build_class__. As an aside, __build_function__ could be cool too, but is mostly pointless.