Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Property and __getattr__ compatibility issue with AttributeError

I just encountered an unexpected behavior. This is a simple class with a __getattr__ method and a property attribute with a typo inside:

class A(object):
    def __getattr__(self, attr):
        if not attr.startswith("ignore_"):
            raise AttributeError(attr)

    @property
    def prop(self):
        return self.some_typo

a = A() # Instantiating
a.ignore_this # This is ignored
a.prop # This raises an Attribute Error

This is the expected outcome (the one I get if __getattr__ is commented):

AttributeError: 'A' object has no attribute 'some_typo'

And this is what I get:

AttributeError: prop

I know this has to do with__getattr__ catching the AttributeError but is there a nice and clean workaround for this issue? Because I can assure you, this is a debug nightmare...

like image 693
Vincent Avatar asked Aug 04 '14 09:08

Vincent


2 Answers

You can just raise a better exception message:

class A(object):
  def __getattr__(self, attr):
    if not attr.startswith("ignore_"):
      raise AttributeError("%r object has not attribute %r" % (self.__class__.__name__, attr))

  @property
  def prop(self):
    return self.some_typo

a=A()
a.ignore_this
a.prop

EDIT: calling __getattribute__ from object base class solves the problem

class A(object):
  def __getattr__(self, attr):
    if not attr.startswith("ignore_"):
      return self.__getattribute__(attr)

  @property
  def prop(self):
    return self.some_typo
like image 100
mguijarr Avatar answered Sep 23 '22 22:09

mguijarr


As mentioned by @asmeurer, the solution by @mguijarr calls prop twice. When prop first runs, it raises an AttributeError which triggers __getattr__. Then self.__getattribute__(attr) triggers prop again, finally resulting in the desired exception.

BETTER ANSWER:

Here we are better off replacing __getattribute__ instead of __getattr__. It gives us more control since __getattribute__ is invoked on all attribute access. In contrast, __getattr__ is only called when there has already been an AttributeError, and it doesn't give us access to that original error.

class A(object):
    def __getattribute__(self, attr):
        try:
            return super().__getattribute__(attr)
        except AttributeError as e:
            if not attr.startswith("ignore_"):
                raise e

    @property
    def prop(self):
        print("hi")
        return self.some_typo

To explain, since A subclasses object in this case, super().__getattribute__(attr) is equivalent to object.__getattribute__(self, attr). That reads a's underlying object attribute, avoiding the infinite recursion had we instead used self.__getattribute__(attr).

In case of AttributeError, we have full control to either fail or reraise, and reraising gives a sensible error message.

like image 1
Ben Mares Avatar answered Sep 20 '22 22:09

Ben Mares