Skip to content

Commit

Permalink
fix sequence like validator with strict True (#8977)
Browse files Browse the repository at this point in the history
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
  • Loading branch information
andresliszt and sydney-runkle committed Mar 26, 2024
1 parent 426402a commit af3d335
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 2 deletions.
7 changes: 5 additions & 2 deletions pydantic/_internal/_std_types_schema.py
Expand Up @@ -281,7 +281,7 @@ class SequenceValidator:
item_source_type: type[Any]
min_length: int | None = None
max_length: int | None = None
strict: bool = False
strict: bool | None = None

def serialize_sequence_via_list(
self, v: Any, handler: core_schema.SerializerFunctionWrapHandler, info: core_schema.SerializationInfo
Expand Down Expand Up @@ -333,7 +333,10 @@ def __get_pydantic_core_schema__(self, source_type: Any, handler: GetCoreSchemaH
else:
coerce_instance_wrap = partial(core_schema.no_info_after_validator_function, self.mapped_origin)

constrained_schema = core_schema.list_schema(items_schema, **metadata)
# we have to use a lax list schema here, because we need to validate the deque's
# items via a list schema, but it's ok if the deque itself is not a list (same for Counter)
metadata_with_strict_override = {**metadata, 'strict': False}
constrained_schema = core_schema.list_schema(items_schema, **metadata_with_strict_override)

check_instance = core_schema.json_or_python_schema(
json_schema=core_schema.list_schema(),
Expand Down
22 changes: 22 additions & 0 deletions tests/test_main.py
Expand Up @@ -2377,6 +2377,28 @@ class StrictModel(BaseModel):
]


@pytest.mark.xfail(
reason='strict=True in model_validate_json does not overwrite strict=False given in ConfigDict'
'See issue: https://github.com/pydantic/pydantic/issues/8930'
)
def test_model_validate_list_strict() -> None:
# FIXME: This change must be implemented in pydantic-core. The argument strict=True
# in model_validate_json method is not overwriting the one set with ConfigDict(strict=False)
# for sequence like types. See: https://github.com/pydantic/pydantic/issues/8930

class LaxModel(BaseModel):
x: List[str]
model_config = ConfigDict(strict=False)

assert LaxModel.model_validate_json(json.dumps({'x': ('a', 'b', 'c')}), strict=None) == LaxModel(x=('a', 'b', 'c'))
assert LaxModel.model_validate_json(json.dumps({'x': ('a', 'b', 'c')}), strict=False) == LaxModel(x=('a', 'b', 'c'))
with pytest.raises(ValidationError) as exc_info:
LaxModel.model_validate_json(json.dumps({'x': ('a', 'b', 'c')}), strict=True)
assert exc_info.value.errors(include_url=False) == [
{'type': 'list_type', 'loc': ('x',), 'msg': 'Input should be a valid list', 'input': ('a', 'b', 'c')}
]


def test_model_validate_json_strict() -> None:
class LaxModel(BaseModel):
x: int
Expand Down
120 changes: 120 additions & 0 deletions tests/test_types.py
Expand Up @@ -2424,6 +2424,126 @@ def test_sequence_strict():
assert TypeAdapter(Sequence[int]).validate_python((), strict=True) == ()


def test_list_strict() -> None:
class LaxModel(BaseModel):
v: List[int]

model_config = ConfigDict(strict=False)

class StrictModel(BaseModel):
v: List[int]

model_config = ConfigDict(strict=True)

assert LaxModel(v=(1, 2)).v == [1, 2]
assert LaxModel(v=('1', 2)).v == [1, 2]
# Tuple should be rejected
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=(1, 2))
assert exc_info.value.errors(include_url=False) == [
{'type': 'list_type', 'loc': ('v',), 'msg': 'Input should be a valid list', 'input': (1, 2)}
]
# Strict in each list item
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=['1', 2])
assert exc_info.value.errors(include_url=False) == [
{'type': 'int_type', 'loc': ('v', 0), 'msg': 'Input should be a valid integer', 'input': '1'}
]


def test_set_strict() -> None:
class LaxModel(BaseModel):
v: Set[int]

model_config = ConfigDict(strict=False)

class StrictModel(BaseModel):
v: Set[int]

model_config = ConfigDict(strict=True)

assert LaxModel(v=(1, 2)).v == {1, 2}
assert LaxModel(v=('1', 2)).v == {1, 2}
# Tuple should be rejected
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=(1, 2))
assert exc_info.value.errors(include_url=False) == [
{
'type': 'set_type',
'loc': ('v',),
'msg': 'Input should be a valid set',
'input': (1, 2),
}
]
# Strict in each set item
with pytest.raises(ValidationError) as exc_info:
StrictModel(v={'1', 2})
err_info = exc_info.value.errors(include_url=False)
# Sets are not ordered
del err_info[0]['loc']
assert err_info == [{'type': 'int_type', 'msg': 'Input should be a valid integer', 'input': '1'}]


def test_frozenset_strict() -> None:
class LaxModel(BaseModel):
v: FrozenSet[int]

model_config = ConfigDict(strict=False)

class StrictModel(BaseModel):
v: FrozenSet[int]

model_config = ConfigDict(strict=True)

assert LaxModel(v=(1, 2)).v == frozenset((1, 2))
assert LaxModel(v=('1', 2)).v == frozenset((1, 2))
# Tuple should be rejected
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=(1, 2))
assert exc_info.value.errors(include_url=False) == [
{
'type': 'frozen_set_type',
'loc': ('v',),
'msg': 'Input should be a valid frozenset',
'input': (1, 2),
}
]
# Strict in each set item
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=frozenset(('1', 2)))
err_info = exc_info.value.errors(include_url=False)
# Sets are not ordered
del err_info[0]['loc']
assert err_info == [{'type': 'int_type', 'msg': 'Input should be a valid integer', 'input': '1'}]


def test_tuple_strict() -> None:
class LaxModel(BaseModel):
v: Tuple[int, int]

model_config = ConfigDict(strict=False)

class StrictModel(BaseModel):
v: Tuple[int, int]

model_config = ConfigDict(strict=True)

assert LaxModel(v=[1, 2]).v == (1, 2)
assert LaxModel(v=['1', 2]).v == (1, 2)
# List should be rejected
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=[1, 2])
assert exc_info.value.errors(include_url=False) == [
{'type': 'tuple_type', 'loc': ('v',), 'msg': 'Input should be a valid tuple', 'input': [1, 2]}
]
# Strict in each list item
with pytest.raises(ValidationError) as exc_info:
StrictModel(v=('1', 2))
assert exc_info.value.errors(include_url=False) == [
{'type': 'int_type', 'loc': ('v', 0), 'msg': 'Input should be a valid integer', 'input': '1'}
]


def test_int_validation():
class Model(BaseModel):
a: PositiveInt = None
Expand Down
14 changes: 14 additions & 0 deletions tests/test_validate_call.py
Expand Up @@ -496,6 +496,20 @@ def foo(a: int, b: EggBox):
]


def test_config_strict():
@validate_call(config=dict(strict=True))
def foo(a: int, b: List[str]):
return f'{a}, {b[0]}'

assert foo(1, ['bar', 'foobar']) == '1, bar'
with pytest.raises(ValidationError) as exc_info:
foo('foo', ('bar', 'foobar'))
assert exc_info.value.errors(include_url=False) == [
{'type': 'int_type', 'loc': (0,), 'msg': 'Input should be a valid integer', 'input': 'foo'},
{'type': 'list_type', 'loc': (1,), 'msg': 'Input should be a valid list', 'input': ('bar', 'foobar')},
]


def test_annotated_use_of_alias():
@validate_call
def foo(a: Annotated[int, Field(alias='b')], c: Annotated[int, Field()], d: Annotated[int, Field(alias='')]):
Expand Down

0 comments on commit af3d335

Please sign in to comment.