traits icon indicating copy to clipboard operation
traits copied to clipboard

HasStrictTraits not respected in multiple inheritance

Open corranwebster opened this issue 3 years ago • 1 comments

Consider the following:

class A(HasTraits):
    pass

class B(HasStrictTraits):
    pass

class C(A, B):
    pass

class D(B, A):
    pass

If we do

>>> c = C()
>>> c.a = 1

there is no error (which, given that we have inherited HasStrictTraits, we would expect to be raised).

On the other hand:

>>> d = D()
>>> d.a = 1
---------------------------------------------------------------------------
TraitError                                Traceback (most recent call last)
<ipython-input-6-90b5c6ae17d6> in <module>()
----> 1 d.a = 1

TraitError: Cannot set the undefined 'a' attribute of a 'D' object.

This behaves as expected.

The expected behaviour is that if a class inherits from HasStrictTraits, which is a subclass of HasTraits, then it should maintain the HasStrictTraits behaviour unless something is done to explicitly turn it off.

This issue is related to #401 and was first raised in https://github.com/enthought/pyface/issues/730 which has a more detailed analysis of the suspected root problem.

corranwebster avatar Oct 12 '20 15:10 corranwebster

This is an analysis of the problem, but it could be wrong in detail:

HasStrictTraits is defined by setting _ = Disallow. This defines a prefix trait that matches anything, and which doesn't let you set a value. So when you try to set a value that doesn't match a specifically defined trait, then this prefix trait gets used. So far, so good.

HasTraits also has a prefix trait defined implicitly here that gives the normal behaviour (ie. behaves like standard Python).

Now, when you create a new subclass of HasTraits, traits is greedy about finding trait definitions, and so it runs the MRO in reverse order, ignoring non-HasTraits classes, building up a dictionary of trait definitions (both for normal and prefix traits), and then stores them in its trait dictionaries. In particular, for prefix traits, this is cls.__prefix_traits__.

So in the above example A.__prefix_traits__[''] == Python and B.__prefix_traits__[''] == Disallow.

But C.__prefix_traits__[''] == A.__prefix_traits__[''] == Python, which gives the standard HasTraits behaviour; and D.__prefix_traits__[''] == B.__prefix_traits__[''] == Disallow and so it acts like HasStrictTraits.

If this is indeed the root cause, then a potential solution is to track which prefix traits are defined in which classes and merge in only those traits, not the whole of the __prefix_traits__ dictionary.

corranwebster avatar Oct 12 '20 15:10 corranwebster