biqqles / dataclassy

A fast and flexible reimplementation of data classes
https://pypi.org/project/dataclassy
Mozilla Public License 2.0
81 stars 9 forks source link

Multi-level inheritance causes __get__ to fail on properties #49

Closed kgreav closed 3 years ago

kgreav commented 3 years ago

Hi,

Having a rather subtle issue with properties in inherited classes more than 2 levels deep:

from dataclassy import dataclass

@dataclass(slots=True)
class Foo(object):
    foo_prop: str = "foo_prop_value"

class Bar(Foo):
    bar_prop: str = "bar_prop_value"

class FooBar(Bar):
    pass

get_foo_prop = Foo.foo_prop
get_bar_prop = Bar.bar_prop

print("get bar prop from Bar: ", get_bar_prop.__get__(Bar()))
print("get bar prop from FooBar: ", get_bar_prop.__get__(FooBar()))

print()

print("get foo prop from Foo: ", getattr(Foo(), get_foo_prop.__name__))
print("get foo prop from Bar: ", getattr(Bar(), get_foo_prop.__name__))
print("get foo prop from FooBar: ", getattr(FooBar(), get_foo_prop.__name__))

print()

print("get foo prop from Foo: ", get_foo_prop.__get__(Foo()))
print("get foo prop from Bar: ", get_foo_prop.__get__(Bar()))
print("get foo prop from FooBar: ", get_foo_prop.__get__(FooBar()))

output:

get bar prop from Bar:  bar_prop_value
get bar prop from FooBar:  bar_prop_value

get foo prop from Foo:  foo_prop_value
get foo prop from Bar:  foo_prop_value
get foo prop from FooBar:  foo_prop_value

get foo prop from Foo:  foo_prop_value
get foo prop from Bar:  foo_prop_value
Traceback (most recent call last):
  File "test.py", line 29, in <module>
    print("get foo prop from FooBar: ", get_foo_prop.__get__(FooBar()))
AttributeError: foo_prop

It's odd that get causes an AttributeError on FooBar (2 levels of inheritance) but getattr works fine. I'd expect both to work.

Worth noting this also breaks dataclasses, but seems like something we should fix.

biqqles commented 3 years ago

You're probably aware of this but worth noting since it isn't explicitly stated: this only affects slots=True. And I'm not even sure it's anything to do with dataclassy. Converting the classes into their closest "normal class" equivalents:

class Foo(object):
    __slots__= ('foo_prop', )
    def __init__(self):
        self.foo_prop = "foo_prop_value"

class Bar(Foo):
    __slots__ = ('bar_prop',)
    def __init__(self):
        self.bar_prop = "bar_prop_value"

class FooBar(Bar):
    pass

get_foo_prop = Foo().foo_prop
get_bar_prop = Bar().bar_prop

and then running the print lines from there on raises the same exception. Am I missing something? Do you know why this is happening or if there's a workaround you would like to see? Sorry about the extra-long response time by the way.

kgreav commented 3 years ago

Hm I actually didn't link it back to slots but that makes sense. Well, guess it's a slots bug then. Probably nothing we can do about it in that case other than raise a bug with python. I'll look around and see if this is an issue reported elsewhere. Sorry for wasting your time.

biqqles commented 3 years ago

Absolutely no need to apologise! Python has a few weird low-level edge cases like this (see #36) that are frustrating to work around so at least this situation is highly specific (even if that sadly doesn't help you).