Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix sequence like validator with strict True #8977

Merged
merged 10 commits into from Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion pydantic/_internal/_std_types_schema.py
Expand Up @@ -274,7 +274,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
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')}
]


Comment on lines +2380 to +2401
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidhewitt, interesting rust bug here!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to chat with @davidhewitt about this PR tomorrow and will get back to you :).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AHHHHH I can reproduce now. Dumb error on my end :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is actually an issue with the deque schema generation, not with the solution we've implemented :).

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 @@ -2395,6 +2395,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