pythontypesenthought

Enthought traits.api TraitList() doesn't seem to support an item_validator


In some applications, I've found that Enthought Traits.api is a helpful addition to support static variable types in python.

I'm trying to use the TraitList() item_validator keyword, but using the item_validator keyword threw an error... I tried this...

from traits.api import HasTraits, HasRequiredTraits, TraitList, TraitError
from traits.api import Regex, Enum

def garage_item_validator(item):
    """Validate item adjectives and reject pink or floral items"""
    try:
        if isinstance(item, Tool):
            if item.adjective!="pink" or item.adjective!="floral":
                return item
            else:
                raise ValueError()
    except ValueError():
        raise TraitError(f"Cannot put {item} in the Garage()")

class Tool(HasRequiredTraits):
    name = Regex(regex=r"[Ww]rench|[Ll]awnmower", required=True)
    adjective = Enum(*["brown", "rusty", "pink", "floral"], required=True)

    def __init__(self, name, adjective):
        self.name = name
        self.adjective = adjective

    def __repr__(self):
        return """<Tool: {}>""".format(self.name)

class Garage(HasTraits):
    things = TraitList(Tool, item_validator=garage_item_validator)  # <---- TraitList() doesn't work

    def __init__(self):
        self.things = list()

if __name__=="__main__":
    my_garage = Garage()
    my_garage.things.append(Tool("Lawnmower", "brown"))
    my_garage.things.append(Tool("wrench", "pink"))
    print(my_garage)

This throws: TypeError: __init__() got an unexpected keyword argument 'item_validator' although the TraitList docs clearly say item_validator is supported.

I also tried to use a traits.api List(), but it just silently ignores the item_validator keyword.

What should I use to validate the contents of a traits list?


Solution

  • TraitList isn't a Trait, but rather a subclass of list that performs validation and fires events. It is used internally by the List trait:

    >>> from traits.api import HasStrictTraits, List, Int
    >>> class Example(HasStrictTraits):
    ...     x = List(Int) 
    ... 
    >>> example = Example()
    >>> example.x
    []
    >>> type(example.x)
    <class 'traits.trait_list_object.TraitListObject'>
    >>> type(example.x).mro()
    [<class 'traits.trait_list_object.TraitListObject'>, <class 'traits.trait_list_object.TraitList'>, <class 'list'>, <class 'object'>]
    

    and it gets its item_validator set by the List trait:

    >>> example.x.item_validator
    <bound method TraitListObject._item_validator of []>
    

    So although there is no hook to change this validator, it does use the validator from the Trait used as the trait argument the List (Int in the above example, so the list will only hold integer items).

    >>> example.x.append("five")
    Traceback (most recent call last):
    ...
    traits.trait_errors.TraitError: Each element of the 'x' trait of an Example instance must be an integer, but a value of 'five' <class 'str'> was specified.
    

    So you can achieve your goal by writing a custom trait that validates the way that you want.

    In your example you want the items to be instances of Tool with certain properties for the adjective, so an Instance trait is a good starting point:

    from traits.api import BaseInstance, HasTraits, List
    
    class GarageTool(BaseInstance):
        
        def __init__(self, **metadata):
            super().__init__(klass=Tool, allow_none=False, **metadata)
            
        def validate(self, object, name, value):
            # validate it is an instance of Tool
            value = super().validate(object, name, value)
            if value.adjective in {"pink", "floral"}:
                self.error(object, name, value)
            return value
        
        def info(self):
            # give better error messages
            return "a Tool which is neither pink nor floral"
    
    class Garage(HasTraits):
        things = List(GarageTool())
        
    if __name__ == "__main__":
        my_garage = Garage()
        my_garage.things.append(Tool("Lawnmower", "brown"))
    
        # now, pink correctly fails in garage_item_validator()
        my_garage.things.append(Tool("wrench", "pink")) # <-- pink fails
    

    which gives an error as desired:

    TraitError: Each element of the 'things' trait of a Garage instance must be a Tool which is neither pink nor floral, but a value of <__main__.Tool object at 0x7f899ad89830> <class '__main__.Tool'> was specified.