Ana Arduengo
Ana Arduengo

Reputation: 3

Custom list with overwritten built-in function for Pydantic

I want to implement a custom list. Basically, it should behave as a normal list except when using the __contains__ function. If the list is empty we consider that it contains everything.

I then want to use this custom list in pydantic models that inherit from BaseModel.

The class is implemented like so:

class ListAllIfEmpty(list): 
    """
    Normal list. If the list is empty we will consider it contains
    everything
    """
    def __contains__(self, item): 
        if not self: 
            return True
        else: 
            return super().__contains__(item)

When I try to use it as an attribute in a BaseModel and give it a default value, I get an error about generating a schema for ListAllIfEmpty. I get the suggestion of setting arbitrary_types_allowed to true. If I do so, pydantic stops doing the validation of the object. Still, I would like Pydantic to do all of the checks it does with a normal python built-in list. The class that has an attribute of ListAllIfEmpty type looks like this:

class A(BaseModel):
    value1: str
    value2: ListAllIfEmpty[int] = Field(default_factory=lambda: [])

And the error message is the following:

E   pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for [...].ListAllIfEmpty[typing.Annotated[datetime.date, BeforeValidator(func=<function validate_date at 0x000001EB701040E0>), PlainSerializer(func=<function format_date at 0x000001EB701053A0>, return_type=PydanticUndefined, when_used='unless-none')]]. Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.
E
E   If you got this error by calling handler(<some type>) within `__get_pydantic_core_schema__` then you likely need to call `handler.generate_schema(<some type>)` since we do not call `__get_pydantic_core_schema__` on `<some type>` otherwise to avoid infinite recursion.

I've seen that using RootModel might help. Still, I am not fully understanding how this works from the documentation. I have also seen some people mentioning "Annotated" but I have only seen people using it to add validation information. I would also like to keep the behaviour of "ListAllIfEmpty" the same as a normal list, aka, being able to instantiate it like a = [1,2,3].

Upvotes: 0

Views: 330

Answers (1)

HaX.Alvin
HaX.Alvin

Reputation: 46

You should make ListAllIfEmpty inheritance RootModel, and set root to any type you want.
After that, you can custom your __contains__ or other method for root.

class ListAllIfEmpty(RootModel): 
    """
    Normal list. If the list is empty we will consider it contains everything
    """
    root: list[int] = Field(default_factory=list)
    def __contains__(self, item):
        if not self.root:
            return True
        else: 
            return item in self.root
    
    def __getitem__(self, item):
        return self.root[item]
    
    def __iter__(self):
        return iter(self.root)


class A(BaseModel):
    value1: str
    value2: ListAllIfEmpty = Field(default_factory=ListAllIfEmpty)

I test it on these operations, is this what you want?

a = A(value1="test", value2=ListAllIfEmpty())
print(1 in a.value2) # True

b = A(value1="test", value2=list())
print(1 in b.value2) # True

c = A(value1="test", value2=[1, 2, 3])
print(1 in c.value2) # True
print(4 in c.value2) # False

d = A(value1="test")
print(1 in d.value2) # True

A(value1="test", value2=[1,"2",3]) # Pass
A(value1="test", value2=[1,"bad",3]) # Fail

Upvotes: 1

Related Questions