__setattr__
is a special method:
For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.
Python doesn't document anywhere exactly which special methods skip the instance dictionary, and when they do so—in fact, it's really up to each implementation—but (at least in CPython and PyPy), __setattr__
always goes straight to the type.
Note that PEP 562 adds a special case for __getattr__
and __dir__
on ModuleType
to Python 3.7, but does not include __setattr__
.
So, assigning sys.__setattr__
has no effect.
This is also why '__setattr__' in dir(sys)
is False—just as it is for anything that isn't a type. The dir
function doesn't return attributes found on the class (or inherited from a base class). If you want to check for an attribute, you normally use hasattr(sys, '__setattr__')
—or, better, just try to access the attribute (because that works even with, e.g., dynamic attributes created by custom __getattribute__
).
Also, this means that the place to set your breakpoint would be types.ModuleType.__setattr__
(or type(sys).__setattr__
, which is the same place). But that isn't going to work in CPython, because that's a C function slot on a builtin type (actually just inherited from object.__setattr__
), not a Python method.
There are two traditional ways around this. Neither of them is guaranteed to work with builtin modules; from a quick test, with CPython 3.7, the first one works but the second doesn't. But try them both on your own Python implementation/version.
First, you can just create a subclass:
class HookedModuleType(types.ModuleType):
def __setattr__(self, name, value):
print(f'{self.__name__}.__setattr__({name}, {value})')
return super().__setattr__(name, value)
… and then re-type the module:
mymodule.__class__ = HookedModuleType
Alternatively, since ModuleType
doesn't override the default behavior of __setattr__
, it just inherits it from object
, which means that all it does is set self.__dict__[name] = value
. So, you can write a dict
that intercepts __setitem__
and get the same effect:
class HookedDict(dict):
def __setitem__(self, key, value):
print(f'{self._name}.__setitem__({key}, {value})')
return super().__setitem__(key, value)
mymodule.__dict__ = HookedDict(mymodule.__dict__)
mymodule.__dict__._name = mymodule.__name__
If neither of these works, you have to create a slightly more complicated class that proxies to an actual module object:
class ModuleProxy(object):
def __init__(self, module):
object.__setattr__(self, '_module', module)
def __getattr__(self, name):
return getattr(self._module, name)
def __delattr__(self, name):
delattr(self._module, name)
def __setattr__(self, name, value):
print(f'{self._module.__name__}.__setattr__({name}, {value})')
setattr(self._module, name, value)
… and then replace the module with a proxy to it:
sys = sys.modules['sys'] = ModuleProxy(sys)
This one is a bit easier to get wrong, and may cause some weird behavior in a few edge cases, but it doesn't rely on any non-guaranteed behavior, and it seems to work in CPython 3.7, 3.6, and 2.7 and PyPy 5.10/3.5 and 5.10/2.7 (obviously 2.7 requires changing the f-string to a format
call).