简体   繁体   中英

In Python3, can you trigger a function when an attribute is set on a module?

I am trying to debug a problem where a module's attribute has been removed (as in, it is present in the code, but accessing it at runtime, sometimes, produces an AttributeError .)

Which code this is, and which module/attribute it is, is actually unimportant.

What is important is whether I am able to either edit the module , or monkey patch it, so that I can trigger a function I control when the attribute is modified. That function could be a breakpoint, or could just print the stack trace, raise an exception, etc, to help me see where the real error is (it is clearly not at the place trying to access the attribute, after all.)

What I've found out so far:

I have discovered that one can define __getattr__ on modules.

I have tested whether defining __setattr__ works:

import module
def mod_set_attr(self, name, value):
    raise AttributeError(f"{name} is not a mutable attribute")
module.__setattr__ = mod_set_attr
module.foo = "bar"

(no AttributeError is thrown when you try to set an attr).

I have also tried adding properties directly on the module:

@property
def foo(module):
    return "foo"

@foo.setter
def foo_setter(module, value):
    raise AttributeError("you can't mutate attribute foo")

(still, no AttributeError is thrown).

Honestly didn't expect to be answering this myself so soon, but I found a way! You need to monkey patch the module, by wrapping it in a class, as follows.

class ModuleWrapper(object):
    def __init__(self, module_to_wrap):
        self._mod = module_to_wrap

    def __getattr__(self, name):
        if name == "_mod":
            return super().__getattr__(name)
        return getattr(self._mod, name)

    def __setattr__(self, name, value):
        if name == "_mod":
            return super().__setattr__(name, value)
        if name == "foo":
            raise AttributeError("foo is not mutable")
        return setattr(self._mod, name, value)

    def __delattr__(self, name):
        if name == "foo":
            raise AttributeError("foo is not mutable")
        return delattr(self._mod, name)

import sys, module

sys.modules["module"] = ModuleWrapper(module)

(Sorry about the if name == "_mod" spam, but it is required to avoid infinite recursion in the constructor.)

Essentially we are interrupting any call to setattr or delattr on this module from anywhere and raising an AttributeError . In order to ensure that other places import our wrapped version of the module, we add it to sys.modules[module_name] , which is the true monkey patch.

This means we'll be able to find the culprit that's removing the attribute and fix it. 🎉

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM