Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling hasattr/getattr creates trait if missing. #1753

Open
felixkol opened this issue Jul 27, 2023 · 2 comments
Open

Calling hasattr/getattr creates trait if missing. #1753

felixkol opened this issue Jul 27, 2023 · 2 comments

Comments

@felixkol
Copy link

felixkol commented Jul 27, 2023

I have observed some weird behavior when calling hasattr with a nonexistent attribute.
Assume the following as a minimal example:

class AClass(ta.HasTraits):
    pass


def observer(val):
    print(f"observed: {val}")

inst = AClass()
inst.observe(observer, "*")

print(f"trait_names: {inst.trait_names()}")  
print(f"hasattr('a'): {hasattr(inst, 'a')}")
print(f"trait_names: {inst.trait_names()}")
print(f"is 'a' in dir(inst)?: {'a' in dir(inst)}")
print(f"class traits of inst: {inst.__class_traits__.keys()}")

Yields:

trait_names: ['trait_added', 'trait_modified']
observed: TraitChangeEvent(object=<__main__.AClass object at 0x7f4031f304a0>, name='trait_added', old=<undefined>, new='a')
hasattr: False
trait_names: ['trait_added', 'trait_modified', 'a']
is 'a' in dir(inst)?: True
class traits of inst: dict_keys(['trait_added', 'trait_modified', 'a'])

As one can see, after calling hasattr a new trait is added for the missing attribute 'a', it appears in the list of trait_names as well, but calling getattr now would cause an AttributeError.

Furthermore, using a different expression in the observer changes this behavior:

...
inst.observe(observer, "trait_added")
...
trait_names: ['trait_added', 'trait_modified']
observed: TraitChangeEvent(object=<__main__.AClass object at 0x7f2068cf1450>, name='trait_added', old=<undefined>, new='a')
hasattr: False
trait_names: ['trait_added', 'trait_modified']
is 'a' in dir(inst)?: False
class traits of inst: dict_keys(['trait_added', 'trait_modified', 'a'])

Now, the name of the missing attribute isn't added to the 'trait_names', but the attribute is still created as a class trait.

To my understanding, getting a value should not have such side effects, especially if getting the now appearing attribute in trait_names causes an error. hasattr and trait_names should not contradict each other, but the dependents of the observation expression makes this even less deterministic.

Am I using traits wrong at this point, is this intended behavior or is it a bug?

@mdickinson
Copy link
Member

Thanks for the report. It's not exactly intended behaviour (it's clearly not desirable), but it is behaviour that's fairly deeply baked in, and hard to change without breaking existing code (and unfortunately there's a lot of existing Traits-using code out there). Existing issues #358 and #58 are related.

Slightly simpler reproducer:

>>> class A(HasStrictTraits):
...     bar = Int()
... 
>>> a = A()
>>> "foo" in a.__class_traits__
False
>>> "foo" in a.__class_traits__
False
>>> a.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute 'foo'
>>> "foo" in a.__class_traits__
True

@felixkol
Copy link
Author

Wouldn't it be possible to split getting and setting prefix traits?

On getting, a value would be returned, when it has been set or the prefix trait has a default value. On the latter case, the prefixed trait could be created.
Setting would work just as it is currently.

Then no trait would exist where getting raises an exception.

Would this break the current functionality of prefix traits?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants