traitlets icon indicating copy to clipboard operation
traitlets copied to clipboard

Traitlets validates default even if overrided

Open maxnoe opened this issue 5 years ago • 3 comments

To make a required trait, I thought I could use:

from traitlets import HasTraits, Unicode

class Foo(HasTraits): 
    bar = Unicode(default_value=None, allow_none=False, required=True) 

As expected, this raises a trait error, if Foo is instantiated without arguments:

f = Foo()

But: this also raises, when bar is provided:

f = Foo(bar='test')

This error is raised:

TraitError                                Traceback (most recent call last)
<ipython-input-8-a25b82aa7a35> in <module>
----> 1 Foo(bar='test')

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in __new__(cls, *args, **kwargs)
    956         else:
    957             inst = new_meth(cls, *args, **kwargs)
--> 958         inst.setup_instance(*args, **kwargs)
    959         return inst
    960 

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
    984         self._trait_notifiers = {}
    985         self._trait_validators = {}
--> 986         super(HasTraits, self).setup_instance(*args, **kwargs)
    987 
    988     def __init__(self, *args, **kwargs):

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in setup_instance(self, *args, **kwargs)
    975             else:
    976                 if isinstance(value, BaseDescriptor):
--> 977                     value.instance_init(self)
    978 
    979 

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in instance_init(self, obj)
    520             if (self._dynamic_default_callable(obj) is None) \
    521                     and (self.default_value is not Undefined):
--> 522                 v = self._validate(obj, self.default_value)
    523                 if self.name is not None:
    524                     obj._trait_values[self.name] = v

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in _validate(self, obj, value)
    589             return value
    590         if hasattr(self, 'validate'):
--> 591             value = self.validate(obj, value)
    592         if obj._cross_validation_lock is False:
    593             value = self._cross_validate(obj, value)

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in validate(self, obj, value)
   2052                 msg = "Could not decode {!r} for unicode trait '{}' of {} instance."
   2053                 raise TraitError(msg.format(value, self.name, class_of(obj)))
-> 2054         self.error(obj, value)
   2055 
   2056 

/usr/lib/python3.8/site-packages/traitlets/traitlets.py in error(self, obj, value)
    623             e = "The '%s' trait must be %s, but a value of %r was specified." \
    624                 % (self.name, self.info(), repr_type(value))
--> 625         raise TraitError(e)
    626 
    627     def get_metadata(self, key, default=None):

TraitError: The 'bar' trait of a Foo instance must be a unicode string, but a value of None <class 'NoneType'> was specified.

maxnoe avatar Apr 18 '20 09:04 maxnoe

There's definitely some performance benefit to be gained by reducing default value validation if it will be overwritten anyway. With that said, I think the deeper problem here is that there's not an easy way to define a trait that has no default value. The above solution could be confusing to users since it doesn't really explain that they need to specify the value upon instantiating the class.

rmorshea avatar Jul 06 '20 08:07 rmorshea

@rmorshea

In the actual codebase, we would use something like this:

from traitlets import HasTraits, Unicode

class Foo(HasTraits): 
    bar = Unicode(default_value=None, allow_none=False, required=True) 

    def __init__(bar, **kwargs):
        super().__init__(bar=bar, **kwargs)

To make that point clear.

maxnoe avatar Jul 13 '20 10:07 maxnoe

Just noting, that the behavior with the current release of traitlets here has changed, however in a for me even more suprising way.

Now, the class instantiation works, and the error is only raised when accessing the value:

from traitlets import HasTraits, Unicode

class Foo(HasTraits): 
    bar = Unicode(default_value=None, allow_none=False)


foo = Foo()  # does not raise, but should?
foo.bar  # now it raises the "expected str" error

maxnoe avatar Mar 30 '22 17:03 maxnoe