traits
traits copied to clipboard
Troubles with properties and defaults initializations
In the following example, an AttributeError
is raised complaining that the foo
attribute is None
even though the foo
attribute is supplied at instantiation.
My fuzzy understanding is that the presence of the Property
attempts to connect some traits listener that attempts to access foo
before foo
itself is attached to the Baz
instance.
This is possible to workaround by removing the Property
and setting the bad_prop
via an on_trait_change
. Still, the behavior here is unexpected.
Example
from traits.api import *
class Foo(HasTraits):
x = Int
class Bar(HasTraits):
y = Int
class Baz(HasTraits):
foo = Instance(Foo)
bar = Instance(Bar)
bad_prop = Property(depends_on='bar.y')
def _bar_default(self):
return Bar(y=self.foo.x)
def _get_bad_prop(self):
return self.bar.y + 1
if __name__ == '__main__':
Baz(foo=Foo(x=1))
Traceback
File ".../traits/has_traits.py", line 3566, in _init_trait_listeners
getattr(self, "_init_trait_%s_listener" % data[0])(name, *data)
File ".../traits/has_traits.py", line 3619, in _init_trait_property_listener
self.on_trait_change(notify, pattern, target=self)
File ".../traits/has_traits.py", line 2825, in on_trait_change
listener.register(self)
File ".../traits/traits_listener.py", line 464, in register
value = getattr(self, type)(new, name, False)
File ".../traits/traits_listener.py", line 738, in _register_simple
return next.register(getattr(object, name))
File "nothing.py", line 18, in _bar_default
return Bar(y=self.foo.x)
AttributeError: 'NoneType' object has no attribute 'x'
Agreed that this looks like a bug, but it may not be trivial to fix. If it helps, reviewing and fixing the Traits initialization system is part of the ETS roadmap.
If this is a situation where a Baz
instance is always expected to have a non-None
foo
trait value, and that foo
trait value isn't ever modified for a given Baz
instance, then one pattern is to assign that trait in the __init__
method, to ensure that foo
gets assigned first:
def __init__(self, foo, **traits):
self.foo = foo
super().__init__(**traits)
Removing the "bug" label: if this is a bug, it's a bug at design level. The Traits initialization sequence (as encoded in CHasTraits.__init__
) is essentially in three stages:
- hook up listeners
- assign any traits passed as keywords
- hook up post-init listeners
In this case, we're depending on something that happens in step 2 while hooking up the listeners as part of step 1.
Thank you @notmatthancock and @mdickinson for looking into this issue. This looks like a duplicate of https://github.com/enthought/traits/issues/481, could you confirm please?
The investigation made in #481 suggests that this is due to an extended trait ("bar.y"
as opposed to just "bar"
) being used. The registration of the extended listener for "y"
causes the parent trait to be evaluated in stage 1 (see the three stages Mark listed above).
on_trait_change
avoids the evaluation of default in stage 1 by setting defer
to True
, which causes other problem (see previous attempt to change this flag for fixing #275).
This looks like a duplicate of #481, could you confirm please?
Yep; looks like we're talking about the same underlying issue.
For future reference, this issue does not apply to Property(observe=...)
(since Traits 6.2).
from traits.api import *
class Foo(HasTraits):
x = Int
class Bar(HasTraits):
y = Int
class Baz(HasTraits):
foo = Instance(Foo)
bar = Instance(Bar)
bad_prop = Property(observe='bar.y')
def _bar_default(self):
return Bar(y=self.foo.x)
def _get_bad_prop(self):
return self.bar.y + 1
if __name__ == '__main__':
Baz(foo=Foo(x=1))
runs without errors.