Search code examples
pythongetattributehasattr

Detect if a __getattribute__ call was due to hasattr


I'm re-implementing __getattribute__ for a class.

I want to notice any incorrect (meaning failures are expected, of course) failures of providing attributes (because the __getattribute__ implementation turned out quite complex). For that I log a warning if my code was unable to find/provide the attribute just before raising an AttributeError.

I'm aware:

  1. __getattribute__ implementations are encouraged to be as small as simple as possible.
  2. It is considered wrong for a __getattribute__ implementation to behave differently based on how/why it was called.
  3. Code accessing the attribute can just as well try/except instead of using hasattr.

TL;DR: Nevertheless, I'd like to detect whether a call to __getattribute__ was done due to hasattr (verses a "genuine" attempt at accessing the attribute).


Solution

  • This is not possible, even through stack inspection. hasattr produces no frame object in the Python call stack, as it is written in C, and trying to inspect the last Python frame to guess whether it's suspended in the middle of a hasattr call is prone to all kinds of false negatives and false positives.

    If you're absolutely determined to make your best shot at it anyway, the most reliable (but still fragile) kludge I can think of is to monkey-patch builtins.hasattr with a Python function that does produce a Python stack frame:

    import builtins
    import inspect
    import types
    
    _builtin_hasattr = builtins.hasattr
    if not isinstance(_builtin_hasattr, types.BuiltinFunctionType):
        raise Exception('hasattr already patched by someone else!')
    
    def hasattr(obj, name):
        return _builtin_hasattr(obj, name)
    
    builtins.hasattr = hasattr
    
    def probably_called_from_hasattr():
        # Caller's caller's frame.
        frame = inspect.currentframe().f_back.f_back
        return frame.f_code is hasattr.__code__
    

    Calling probably_called_from_hasattr inside __getattribute__ will then test if your __getattribute__ was probably called from hasattr. This avoids any need to assume that the calling code used the name "hasattr", or that use of the name "hasattr" corresponds to this particular __getattribute__ call, or that the hasattr call originated inside Python-level code instead of C.

    The primary sources of fragility here are if someone saved a reference to the real hasattr before the monkey-patch went through, or if someone else monkey-patches hasattr (such as if someone copy-pastes this code into another file in the same program). The isinstance check attempts to catch most cases of someone else monkey-patching hasattr before us, but it's not perfect.

    Additionally, if hasattr on an object written in C triggers attribute access on your object, that will look like your __getattribute__ was called from hasattr. This is the most likely way to get false positives; everything in the previous paragraph would give false negatives. You can protect against that by checking that the entry for obj in the hasattr frame's f_locals is the object it should be.

    Finally, if your __getattribute__ was called from a decorator-created wrapper, subclass __getattribute__, or something similar, that will not count as a call from hasattr, even if the wrapper or override was called from hasattr, even if you want it to count.