diff --git a/changes/2320-PrettyWood.md b/changes/2320-PrettyWood.md new file mode 100644 index 0000000000..b3bd818aea --- /dev/null +++ b/changes/2320-PrettyWood.md @@ -0,0 +1 @@ +fix: allow `None` for type `Optional[conset / conlist]` diff --git a/pydantic/types.py b/pydantic/types.py index 846e9ee310..1c2b2da838 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -106,7 +106,6 @@ if TYPE_CHECKING: from .dataclasses import Dataclass # noqa: F401 - from .fields import ModelField from .main import BaseConfig, BaseModel # noqa: F401 from .typing import CallableGenerator @@ -187,8 +186,8 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) @classmethod - def list_length_validator(cls, v: 'Optional[List[T]]', field: 'ModelField') -> 'Optional[List[T]]': - if v is None and not field.required: + def list_length_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]': + if v is None: return None v = list_validator(v) @@ -229,7 +228,10 @@ def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items) @classmethod - def set_length_validator(cls, v: 'Optional[Set[T]]', field: 'ModelField') -> 'Optional[Set[T]]': + def set_length_validator(cls, v: 'Optional[Set[T]]') -> 'Optional[Set[T]]': + if v is None: + return None + v = set_validator(v) v_len = len(v) diff --git a/tests/test_types.py b/tests/test_types.py index f019a7f5aa..66bb6b8627 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -172,6 +172,34 @@ class ConListModelMin(BaseModel): ] +def test_constrained_list_optional(): + class Model(BaseModel): + req: Optional[conlist(str, min_items=1)] = ... + opt: Optional[conlist(str, min_items=1)] + + assert Model(req=None).dict() == {'req': None, 'opt': None} + assert Model(req=None, opt=None).dict() == {'req': None, 'opt': None} + + with pytest.raises(ValidationError) as exc_info: + Model(req=[], opt=[]) + assert exc_info.value.errors() == [ + { + 'loc': ('req',), + 'msg': 'ensure this value has at least 1 items', + 'type': 'value_error.list.min_items', + 'ctx': {'limit_value': 1}, + }, + { + 'loc': ('opt',), + 'msg': 'ensure this value has at least 1 items', + 'type': 'value_error.list.min_items', + 'ctx': {'limit_value': 1}, + }, + ] + + assert Model(req=['a'], opt=['a']).dict() == {'req': ['a'], 'opt': ['a']} + + def test_constrained_list_constraints(): class ConListModelBoth(BaseModel): v: conlist(int, min_items=7, max_items=11) @@ -323,6 +351,34 @@ class ConSetModelMin(BaseModel): ] +def test_constrained_set_optional(): + class Model(BaseModel): + req: Optional[conset(str, min_items=1)] = ... + opt: Optional[conset(str, min_items=1)] + + assert Model(req=None).dict() == {'req': None, 'opt': None} + assert Model(req=None, opt=None).dict() == {'req': None, 'opt': None} + + with pytest.raises(ValidationError) as exc_info: + Model(req=set(), opt=set()) + assert exc_info.value.errors() == [ + { + 'loc': ('req',), + 'msg': 'ensure this value has at least 1 items', + 'type': 'value_error.set.min_items', + 'ctx': {'limit_value': 1}, + }, + { + 'loc': ('opt',), + 'msg': 'ensure this value has at least 1 items', + 'type': 'value_error.set.min_items', + 'ctx': {'limit_value': 1}, + }, + ] + + assert Model(req={'a'}, opt={'a'}).dict() == {'req': {'a'}, 'opt': {'a'}} + + def test_constrained_set_constraints(): class ConSetModelBoth(BaseModel): v: conset(int, min_items=7, max_items=11)