Skip to content

Commit

Permalink
Add BaseModel.model_validate_strings and `TypeAdapter.validate_stri…
Browse files Browse the repository at this point in the history
…ngs`
  • Loading branch information
hramezani committed Sep 21, 2023
1 parent 5b86145 commit 69240a5
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 1 deletion.
25 changes: 25 additions & 0 deletions pydantic/main.py
Expand Up @@ -530,6 +530,31 @@ 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.
Raises:
ValueError: If `json_data` is not a JSON string.
"""
# `__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:
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
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):
if raises_match is not None:
print(TypeAdapter(field_type).core_schema)
with pytest.raises(expected, match=raises_match):
TypeAdapter(field_type).validate_strings(input_value, strict=strict)
else:
TypeAdapter(field_type).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),
}

0 comments on commit 69240a5

Please sign in to comment.