Skip to content

Commit

Permalink
prevent int,float,decimal -> str (pydantic#212)
Browse files Browse the repository at this point in the history
* prevent int,float,decimal -> str, fix pydantic#150

* fix tests

* coverage
  • Loading branch information
samuelcolvin committed Aug 2, 2022
1 parent 1e93886 commit 42a465a
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 60 deletions.
2 changes: 0 additions & 2 deletions src/input/input_json.rs
Expand Up @@ -62,8 +62,6 @@ impl<'a> Input<'a> for JsonInput {
fn lax_str(&'a self) -> ValResult<EitherString<'a>> {
match self {
JsonInput::String(s) => Ok(s.as_str().into()),
JsonInput::Int(int) => Ok(int.to_string().into()),
JsonInput::Float(float) => Ok(float.to_string().into()),
_ => Err(ValError::new(ErrorKind::StrType, self)),
}
}
Expand Down
14 changes: 2 additions & 12 deletions src/input/input_python.rs
Expand Up @@ -4,8 +4,8 @@ use std::str::from_utf8;
use pyo3::exceptions::PyAttributeError;
use pyo3::prelude::*;
use pyo3::types::{
PyBool, PyByteArray, PyBytes, PyDate, PyDateTime, PyDelta, PyDict, PyFrozenSet, PyInt, PyList, PyMapping,
PySequence, PySet, PyString, PyTime, PyTuple, PyType,
PyBool, PyByteArray, PyBytes, PyDate, PyDateTime, PyDelta, PyDict, PyFrozenSet, PyList, PyMapping, PySequence,
PySet, PyString, PyTime, PyTuple, PyType,
};
use pyo3::{intern, AsPyPointer};

Expand Down Expand Up @@ -113,16 +113,6 @@ impl<'a> Input<'a> for PyAny {
Err(_) => return Err(ValError::new(ErrorKind::StrUnicode, self)),
};
Ok(str.into())
} else if self.cast_as::<PyBool>().is_ok() {
// do this before int and float parsing as `False` is cast to `0` and we don't want False to
// be returned as a string
Err(ValError::new(ErrorKind::StrType, self))
} else if let Ok(int) = self.cast_as::<PyInt>() {
let int = i64::extract(int)?;
Ok(int.to_string().into())
} else if let Ok(float) = f64::extract(self) {
// don't cast_as here so Decimals are covered - internally f64:extract uses PyFloat_AsDouble
Ok(float.to_string().into())
} else {
Err(ValError::new(ErrorKind::StrType, self))
}
Expand Down
6 changes: 0 additions & 6 deletions src/input/return_enums.rs
Expand Up @@ -227,12 +227,6 @@ impl<'a> EitherString<'a> {
}
}

impl<'a> From<String> for EitherString<'a> {
fn from(data: String) -> Self {
Self::Cow(Cow::Owned(data))
}
}

impl<'a> From<&'a str> for EitherString<'a> {
fn from(data: &'a str) -> Self {
Self::Cow(Cow::Borrowed(data))
Expand Down
2 changes: 1 addition & 1 deletion tests/benchmarks/test_complete_benchmark.py
Expand Up @@ -91,7 +91,7 @@ def test_complete_invalid():
lax_validator = SchemaValidator(lax_schema)
with pytest.raises(ValidationError) as exc_info:
lax_validator.validate_python(input_data_wrong())
assert len(exc_info.value.errors()) == 736
assert len(exc_info.value.errors()) == 738

model = pydantic_model()
if model is None:
Expand Down
2 changes: 1 addition & 1 deletion tests/benchmarks/test_micro_benchmarks.py
Expand Up @@ -415,7 +415,7 @@ def test_frozenset_of_ints_core(benchmark):
benchmark(v.validate_python, frozenset_of_ints)


dict_of_ints_data = ({i: i for i in range(1000)}, {i: str(i) for i in range(1000)})
dict_of_ints_data = ({str(i): i for i in range(1000)}, {str(i): str(i) for i in range(1000)})


@skip_pydantic
Expand Down
12 changes: 7 additions & 5 deletions tests/test_json.py
Expand Up @@ -29,10 +29,12 @@ def test_null():


def test_str():
assert SchemaValidator({'type': 'str'}).validate_json('"foobar"') == 'foobar'
assert SchemaValidator({'type': 'str'}).validate_json('123') == '123'
s = SchemaValidator({'type': 'str'})
assert s.validate_json('"foobar"') == 'foobar'
with pytest.raises(ValidationError, match=r'Input should be a valid string \[kind=str_type,'):
SchemaValidator({'type': 'str'}).validate_json('false')
s.validate_json('false')
with pytest.raises(ValidationError, match=r'Input should be a valid string \[kind=str_type,'):
s.validate_json('123')


@pytest.mark.parametrize(
Expand All @@ -53,8 +55,8 @@ def test_model():
)

# language=json
input_str = '{"field_a": 123, "field_b": 1}'
assert v.validate_json(input_str) == {'field_a': '123', 'field_b': 1}
input_str = '{"field_a": "abc", "field_b": 1}'
assert v.validate_json(input_str) == {'field_a': 'abc', 'field_b': 1}


def test_float_no_remainder():
Expand Down
4 changes: 2 additions & 2 deletions tests/validators/test_dict.py
Expand Up @@ -24,8 +24,8 @@ def test_dict(py_and_json: PyAndJson):
@pytest.mark.parametrize(
'input_value,expected',
[
({'1': 1, '2': 2}, {'1': '1', '2': '2'}),
(OrderedDict(a=1, b=2), {'a': '1', 'b': '2'}),
({'1': b'1', '2': b'2'}, {'1': '1', '2': '2'}),
(OrderedDict(a=b'1', b='2'), {'a': '1', 'b': '2'}),
({}, {}),
('foobar', Err("Input should be a valid dictionary [kind=dict_type, input_value='foobar', input_type=str]")),
([], Err('Input should be a valid dictionary [kind=dict_type,')),
Expand Down
12 changes: 6 additions & 6 deletions tests/validators/test_function.py
Expand Up @@ -168,7 +168,7 @@ def f(input_value, **kwargs):
}
)

assert v.validate_python({'field_a': '123', 'field_b': 321}) == {'field_a': 123, 'field_b': '321 Changed'}
assert v.validate_python({'field_a': '123', 'field_b': b'321'}) == {'field_a': 123, 'field_b': '321 Changed'}
assert f_kwargs == {'data': {'field_a': 123}, 'config': None, 'context': None}


Expand All @@ -192,7 +192,7 @@ def f(input_value, **kwargs):
{'config_choose_priority': 2},
)

assert v.validate_python({'test_field': 321}) == {'test_field': '321 Changed'}
assert v.validate_python({'test_field': b'321'}) == {'test_field': '321 Changed'}
assert f_kwargs == {'data': {}, 'config': {'config_choose_priority': 2}, 'context': None}


Expand All @@ -206,7 +206,7 @@ def f(input_value, **kwargs):

v = SchemaValidator({'type': 'function', 'mode': 'after', 'function': f, 'schema': {'type': 'str'}})

assert v.validate_python(123) == '123 Changed'
assert v.validate_python(b'abc') == 'abc Changed'
assert f_kwargs == {'data': None, 'config': None, 'context': None}


Expand Down Expand Up @@ -241,7 +241,7 @@ def f(input_value, **kwargs):

m = {'field_a': 'test', 'more': 'foobar'}
assert v.validate_python({'field_a': 'test'}) == m
assert v.validate_assignment('field_a', 456, m) == {'field_a': '456', 'more': 'foobar'}
assert v.validate_assignment('field_a', b'abc', m) == {'field_a': 'abc', 'more': 'foobar'}


def test_function_wrong_sig():
Expand Down Expand Up @@ -279,9 +279,9 @@ def __validate__(cls, input_value, **kwargs):
assert isinstance(f, Foobar)
assert f.a == 'foofoo'

f = v.validate_python(1)
f = v.validate_python(b'a')
assert isinstance(f, Foobar)
assert f.a == '11'
assert f.a == 'aa'

with pytest.raises(ValidationError) as exc_info:
v.validate_python(True)
Expand Down
31 changes: 25 additions & 6 deletions tests/validators/test_string.py
Expand Up @@ -13,8 +13,8 @@
'input_value,expected',
[
('foobar', 'foobar'),
(123, '123'),
(123.456, '123.456'),
(123, Err('Input should be a valid string [kind=str_type, input_value=123, input_type=int]')),
(123.456, Err('Input should be a valid string [kind=str_type, input_value=123.456, input_type=float]')),
(False, Err('Input should be a valid string [kind=str_type')),
(True, Err('Input should be a valid string [kind=str_type')),
([], Err('Input should be a valid string [kind=str_type, input_value=[], input_type=list]')),
Expand Down Expand Up @@ -46,8 +46,11 @@ def test_str(py_and_json: PyAndJson, input_value, expected):
),
# null bytes are very annoying, but we can't really block them here
(b'\x00', '\x00'),
(123, '123'),
(Decimal('123'), '123'),
(123, Err('Input should be a valid string [kind=str_type, input_value=123, input_type=int]')),
(
Decimal('123'),
Err("Input should be a valid string [kind=str_type, input_value=Decimal('123'), input_type=Decimal]"),
),
],
)
def test_str_not_json(input_value, expected):
Expand All @@ -62,9 +65,8 @@ def test_str_not_json(input_value, expected):
@pytest.mark.parametrize(
'kwargs,input_value,expected',
[
({}, 123, '123'),
({}, 'abc', 'abc'),
({'strict': True}, 'Foobar', 'Foobar'),
({'strict': True}, 123, Err('Input should be a valid string [kind=str_type, input_value=123, input_type=int]')),
({'to_upper': True}, 'fooBar', 'FOOBAR'),
({'to_lower': True}, 'fooBar', 'foobar'),
({'strip_whitespace': True}, ' foobar ', 'foobar'),
Expand Down Expand Up @@ -93,6 +95,23 @@ def test_constrained_str(py_and_json: PyAndJson, kwargs: Dict[str, Any], input_v
assert v.validate_test(input_value) == expected


@pytest.mark.parametrize(
'kwargs,input_value,expected',
[
({}, b'abc', 'abc'),
({'strict': True}, 'Foobar', 'Foobar'),
({'strict': True}, 123, Err('Input should be a valid string [kind=str_type, input_value=123, input_type=int]')),
],
)
def test_constrained_str_py_only(kwargs: Dict[str, Any], input_value, expected):
v = SchemaValidator({'type': 'str', **kwargs})
if isinstance(expected, Err):
with pytest.raises(ValidationError, match=re.escape(expected.message)):
v.validate_python(input_value)
else:
assert v.validate_python(input_value) == expected


def test_unicode_error():
# `.to_str()` Returns a `UnicodeEncodeError` if the input is not valid unicode (containing unpaired surrogates).
# https://github.com/PyO3/pyo3/blob/6503128442b8f3e767c663a6a8d96376d7fb603d/src/types/string.rs#L477
Expand Down
17 changes: 9 additions & 8 deletions tests/validators/test_tuple.py
Expand Up @@ -51,8 +51,8 @@ def test_any_no_copy():
'mode,items,input_value,expected',
[
('variable', {'type': 'int'}, (1, 2, '33'), (1, 2, 33)),
('variable', {'type': 'str'}, (1, 2, '33'), ('1', '2', '33')),
('positional', [{'type': 'int'}, {'type': 'str'}, {'type': 'float'}], (1, 2, 33), (1, '2', 33.0)),
('variable', {'type': 'str'}, (b'1', b'2', '33'), ('1', '2', '33')),
('positional', [{'type': 'int'}, {'type': 'str'}, {'type': 'float'}], (1, b'a', 33), (1, 'a', 33.0)),
],
)
def test_tuple_strict_passes_with_tuple(mode, items, input_value, expected):
Expand Down Expand Up @@ -227,7 +227,7 @@ def test_union_tuple_list(input_value, expected):
[
((1, 2, 3), (1, 2, 3)),
(('a', 'b', 'c'), ('a', 'b', 'c')),
(('a', 1, 'c'), ('a', '1', 'c')),
(('a', b'a', 'c'), ('a', 'a', 'c')),
(
[5],
Err(
Expand All @@ -251,6 +251,7 @@ def test_union_tuple_list(input_value, expected):
),
),
],
ids=repr,
)
def test_union_tuple_var_len(input_value, expected):
v = SchemaValidator(
Expand All @@ -276,7 +277,6 @@ def test_union_tuple_var_len(input_value, expected):
[
((1, 2, 3), (1, 2, 3)),
(('a', 'b', 'c'), ('a', 'b', 'c')),
(('a', 1, 'c'), ('a', '1', 'c')),
(
[5, '1', 1],
Err(
Expand All @@ -298,6 +298,7 @@ def test_union_tuple_var_len(input_value, expected):
),
),
],
ids=repr,
)
def test_union_tuple_fix_len(input_value, expected):
v = SchemaValidator(
Expand Down Expand Up @@ -349,10 +350,10 @@ def test_tuple_fix_extra():

def test_tuple_fix_extra_any():
v = SchemaValidator({'type': 'tuple', 'mode': 'positional', 'items_schema': ['str'], 'extra_schema': 'any'})
assert v.validate_python([1]) == ('1',)
assert v.validate_python([1, 2]) == ('1', 2)
assert v.validate_python((1, 2)) == ('1', 2)
assert v.validate_python([1, 2, b'3']) == ('1', 2, b'3')
assert v.validate_python([b'1']) == ('1',)
assert v.validate_python([b'1', 2]) == ('1', 2)
assert v.validate_python((b'1', 2)) == ('1', 2)
assert v.validate_python([b'1', 2, b'3']) == ('1', 2, b'3')
with pytest.raises(ValidationError) as exc_info:
v.validate_python([])
assert exc_info.value.errors() == [{'kind': 'missing', 'loc': [0], 'message': 'Field required', 'input_value': []}]
22 changes: 11 additions & 11 deletions tests/validators/test_typed_dict.py
Expand Up @@ -46,7 +46,7 @@ def test_simple():
}
)

assert v.validate_python({'field_a': 123, 'field_b': 1}) == {'field_a': '123', 'field_b': 1}
assert v.validate_python({'field_a': b'abc', 'field_b': 1}) == {'field_a': 'abc', 'field_b': 1}


def test_strict():
Expand Down Expand Up @@ -77,9 +77,9 @@ def test_with_default():
}
)

assert v.validate_python({'field_a': 123}) == ({'field_a': '123', 'field_b': 666}, {'field_a'})
assert v.validate_python({'field_a': 123, 'field_b': 1}) == (
{'field_a': '123', 'field_b': 1},
assert v.validate_python({'field_a': b'abc'}) == ({'field_a': 'abc', 'field_b': 666}, {'field_a'})
assert v.validate_python({'field_a': b'abc', 'field_b': 1}) == (
{'field_a': 'abc', 'field_b': 1},
{'field_b', 'field_a'},
)

Expand All @@ -92,13 +92,13 @@ def test_missing_error():
}
)
with pytest.raises(ValidationError) as exc_info:
v.validate_python({'field_a': 123})
v.validate_python({'field_a': b'abc'})
assert (
str(exc_info.value)
== """\
1 validation error for typed-dict
field_b
Field required [kind=missing, input_value={'field_a': 123}, input_type=dict]"""
Field required [kind=missing, input_value={'field_a': b'abc'}, input_type=dict]"""
)


Expand Down Expand Up @@ -141,7 +141,7 @@ def test_ignore_extra():
}
)

assert v.validate_python({'field_a': 123, 'field_b': 1, 'field_c': 123}) == (
assert v.validate_python({'field_a': b'123', 'field_b': 1, 'field_c': 123}) == (
{'field_a': '123', 'field_b': 1},
{'field_b', 'field_a'},
)
Expand All @@ -158,7 +158,7 @@ def test_forbid_extra():
)

with pytest.raises(ValidationError) as exc_info:
v.validate_python({'field_a': 123, 'field_b': 1})
v.validate_python({'field_a': 'abc', 'field_b': 1})

assert exc_info.value.errors() == [
{'kind': 'extra_forbidden', 'loc': ['field_b'], 'message': 'Extra inputs are not permitted', 'input_value': 1}
Expand All @@ -175,8 +175,8 @@ def test_allow_extra():
}
)

assert v.validate_python({'field_a': 123, 'field_b': (1, 2)}) == (
{'field_a': '123', 'field_b': (1, 2)},
assert v.validate_python({'field_a': b'abc', 'field_b': (1, 2)}) == (
{'field_a': 'abc', 'field_b': (1, 2)},
{'field_a', 'field_b'},
)

Expand Down Expand Up @@ -238,7 +238,7 @@ def test_validate_assignment():

assert v.validate_python({'field_a': 'test'}) == ({'field_a': 'test'}, {'field_a'})

assert v.validate_assignment('field_a', 456, {'field_a': 'test'}) == ({'field_a': '456'}, {'field_a'})
assert v.validate_assignment('field_a', b'abc', {'field_a': 'test'}) == ({'field_a': 'abc'}, {'field_a'})


def test_validate_assignment_functions():
Expand Down

0 comments on commit 42a465a

Please sign in to comment.