traits
traits copied to clipboard
Calling `hasattr`/`getattr` creates trait if missing.
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?