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

Add BaseModel.model_validate_strings and TypeAdapter.validate_strings #7552

Merged
merged 2 commits into from Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 22 additions & 0 deletions pydantic/main.py
Expand Up @@ -530,6 +530,28 @@ def model_validate_json(
__tracebackhide__ = True
return cls.__pydantic_validator__.validate_json(json_data, strict=strict, context=context)

@classmethod
def model_validate_strings(
cls: type[Model],
obj: Any,
*,
strict: bool | None = None,
context: dict[str, Any] | None = None,
) -> Model:
"""Validate the given object contains string data against the Pydantic model.

Args:
obj: The object contains string data to validate.
strict: Whether to enforce types strictly.
context: Extra variables to pass to the validator.

Returns:
The validated Pydantic model.
"""
# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
__tracebackhide__ = True
return cls.__pydantic_validator__.validate_strings(obj, strict=strict, context=context)

@classmethod
def __get_pydantic_core_schema__(
cls, __source: type[BaseModel], __handler: _annotated_handlers.GetCoreSchemaHandler
Expand Down
13 changes: 13 additions & 0 deletions pydantic/type_adapter.py
Expand Up @@ -221,6 +221,19 @@ def validate_json(
"""
return self.validator.validate_json(__data, strict=strict, context=context)

def validate_strings(self, __obj: Any, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> T:
"""Validate object contains string data against the model.

Args:
__obj: The object contains string data to validate.
strict: Whether to strictly check types.
context: Additional context to use during validation.

Returns:
The validated object.
"""
return self.validator.validate_strings(__obj, strict=strict, context=context)

def get_default_value(self, *, strict: bool | None = None, context: dict[str, Any] | None = None) -> Some[T] | None:
"""Get the default value for the wrapped type.

Expand Down
13 changes: 12 additions & 1 deletion tests/test_dataclasses.py
Expand Up @@ -6,7 +6,7 @@
import traceback
from collections.abc import Hashable
from dataclasses import InitVar
from datetime import datetime
from datetime import date, datetime
from pathlib import Path
from typing import Any, Callable, ClassVar, Dict, FrozenSet, Generic, List, Optional, Set, TypeVar, Union

Expand Down Expand Up @@ -2593,3 +2593,14 @@ class Foo:

obj = Foo(**{'some-var': 'some_value'})
assert obj.some_var == 'some_value'


def test_validate_strings():
@pydantic.dataclasses.dataclass
class Nested:
d: date

class Model(BaseModel):
n: Nested

assert Model.model_validate_strings({'n': {'d': '2017-01-01'}}).n.d == date(2017, 1, 1)
48 changes: 48 additions & 0 deletions tests/test_main.py
Expand Up @@ -5,6 +5,7 @@
from collections import defaultdict
from copy import deepcopy
from dataclasses import dataclass
from datetime import date, datetime
from enum import Enum
from typing import (
Any,
Expand Down Expand Up @@ -2543,6 +2544,53 @@ class UnrelatedClass:
assert res == ModelFromAttributesFalse(x=1)


@pytest.mark.parametrize(
'field_type,input_value,expected,raises_match,strict',
[
(bool, 'true', True, None, False),
(bool, 'true', True, None, True),
(bool, 'false', False, None, False),
(bool, 'e', ValidationError, 'type=bool_parsing', False),
(int, '1', 1, None, False),
(int, '1', 1, None, True),
(int, 'xxx', ValidationError, 'type=int_parsing', True),
(float, '1.1', 1.1, None, False),
(float, '1.10', 1.1, None, False),
(float, '1.1', 1.1, None, True),
(float, '1.10', 1.1, None, True),
(date, '2017-01-01', date(2017, 1, 1), None, False),
(date, '2017-01-01', date(2017, 1, 1), None, True),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_from_datetime_inexact', False),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_parsing', True),
(date, '2017-01-01T00:00:00', date(2017, 1, 1), None, False),
(date, '2017-01-01T00:00:00', ValidationError, 'type=date_parsing', True),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, False),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, True),
],
ids=repr,
)
def test_model_validate_strings(field_type, input_value, expected, raises_match, strict):
class Model(BaseModel):
x: field_type

if raises_match is not None:
with pytest.raises(expected, match=raises_match):
Model.model_validate_strings({'x': input_value}, strict=strict)
else:
assert Model.model_validate_strings({'x': input_value}, strict=strict).x == expected


@pytest.mark.parametrize('strict', [True, False])
def test_model_validate_strings_dict(strict):
class Model(BaseModel):
x: Dict[int, date]

assert Model.model_validate_strings({'x': {'1': '2017-01-01', '2': '2017-01-02'}}, strict=strict).x == {
1: date(2017, 1, 1),
2: date(2017, 1, 2),
}


def test_model_signature_annotated() -> None:
class Model(BaseModel):
x: Annotated[int, 123]
Expand Down
36 changes: 36 additions & 0 deletions tests/test_root_model.py
@@ -1,4 +1,5 @@
import pickle
from datetime import date, datetime
from typing import Any, Dict, List, Optional, Union

import pytest
Expand Down Expand Up @@ -616,3 +617,38 @@ def test_copy_preserves_equality():

deepcopied = model.__deepcopy__()
assert model == deepcopied


@pytest.mark.parametrize(
'root_type,input_value,expected,raises_match,strict',
[
(bool, 'true', True, None, False),
(bool, 'true', True, None, True),
(bool, 'false', False, None, False),
(bool, 'e', ValidationError, 'type=bool_parsing', False),
(int, '1', 1, None, False),
(int, '1', 1, None, True),
(int, 'xxx', ValidationError, 'type=int_parsing', True),
(float, '1.1', 1.1, None, False),
(float, '1.10', 1.1, None, False),
(float, '1.1', 1.1, None, True),
(float, '1.10', 1.1, None, True),
(date, '2017-01-01', date(2017, 1, 1), None, False),
(date, '2017-01-01', date(2017, 1, 1), None, True),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_from_datetime_inexact', False),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_parsing', True),
(date, '2017-01-01T00:00:00', date(2017, 1, 1), None, False),
(date, '2017-01-01T00:00:00', ValidationError, 'type=date_parsing', True),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, False),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, True),
],
ids=repr,
)
def test_model_validate_strings(root_type, input_value, expected, raises_match, strict):
Model = RootModel[root_type]

if raises_match is not None:
with pytest.raises(expected, match=raises_match):
Model.model_validate_strings(input_value, strict=strict)
else:
assert Model.model_validate_strings(input_value, strict=strict).root == expected
43 changes: 43 additions & 0 deletions tests/test_type_adapter.py
@@ -1,6 +1,7 @@
import json
import sys
from dataclasses import dataclass
from datetime import date, datetime
from typing import Any, Dict, ForwardRef, Generic, List, NamedTuple, Tuple, TypeVar, Union

import pytest
Expand Down Expand Up @@ -266,3 +267,45 @@ class UnrelatedClass:

res = ta.validate_python(UnrelatedClass(), from_attributes=True)
assert res == ModelFromAttributesFalse(x=1)


@pytest.mark.parametrize(
'field_type,input_value,expected,raises_match,strict',
[
(bool, 'true', True, None, False),
(bool, 'true', True, None, True),
(bool, 'false', False, None, False),
(bool, 'e', ValidationError, 'type=bool_parsing', False),
(int, '1', 1, None, False),
(int, '1', 1, None, True),
(int, 'xxx', ValidationError, 'type=int_parsing', True),
(float, '1.1', 1.1, None, False),
(float, '1.10', 1.1, None, False),
(float, '1.1', 1.1, None, True),
(float, '1.10', 1.1, None, True),
(date, '2017-01-01', date(2017, 1, 1), None, False),
(date, '2017-01-01', date(2017, 1, 1), None, True),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_from_datetime_inexact', False),
(date, '2017-01-01T12:13:14.567', ValidationError, 'type=date_parsing', True),
(date, '2017-01-01T00:00:00', date(2017, 1, 1), None, False),
(date, '2017-01-01T00:00:00', ValidationError, 'type=date_parsing', True),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, False),
(datetime, '2017-01-01T12:13:14.567', datetime(2017, 1, 1, 12, 13, 14, 567_000), None, True),
],
ids=repr,
)
def test_validate_strings(field_type, input_value, expected, raises_match, strict):
ta = TypeAdapter(field_type)
if raises_match is not None:
with pytest.raises(expected, match=raises_match):
ta.validate_strings(input_value, strict=strict)
else:
assert ta.validate_strings(input_value, strict=strict) == expected


@pytest.mark.parametrize('strict', [True, False])
def test_validate_strings_dict(strict):
assert TypeAdapter(Dict[int, date]).validate_strings({'1': '2017-01-01', '2': '2017-01-02'}, strict=strict) == {
1: date(2017, 1, 1),
2: date(2017, 1, 2),
}