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

changes to some coercions #208

Merged
merged 10 commits into from
Aug 3, 2022
40 changes: 40 additions & 0 deletions src/input/_pyo3_dict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// TODO: remove this file once a new pyo3 version is released
// with https://github.com/PyO3/pyo3/pull/2358

use pyo3::{ffi, pyobject_native_type_core, PyAny};

/// Represents a Python `dict_keys`.
#[cfg(not(PyPy))]
#[repr(transparent)]
pub struct PyDictKeys(PyAny);

#[cfg(not(PyPy))]
pyobject_native_type_core!(
PyDictKeys,
ffi::PyDictKeys_Type,
#checkfunction=ffi::PyDictKeys_Check
);

/// Represents a Python `dict_values`.
#[cfg(not(PyPy))]
#[repr(transparent)]
pub struct PyDictValues(PyAny);

#[cfg(not(PyPy))]
pyobject_native_type_core!(
PyDictValues,
ffi::PyDictValues_Type,
#checkfunction=ffi::PyDictValues_Check
);

/// Represents a Python `dict_items`.
#[cfg(not(PyPy))]
#[repr(transparent)]
pub struct PyDictItems(PyAny);

#[cfg(not(PyPy))]
pyobject_native_type_core!(
PyDictItems,
ffi::PyDictItems_Type,
#checkfunction=ffi::PyDictItems_Check
);
Copy link
Member

Choose a reason for hiding this comment

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

should we remove this as I think it's not used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes!

118 changes: 108 additions & 10 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ 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, PyInt, PyIterator, PyList,
PyMapping, PySequence, PySet, PyString, PyTime, PyTuple, PyType,
};
use pyo3::{intern, AsPyPointer};

use crate::errors::{py_err_string, ErrorKind, InputValue, LocItem, ValError, ValResult};

#[cfg(not(PyPy))]
use super::_pyo3_dict::{PyDictKeys, PyDictValues};
use super::datetime::{
bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, date_as_datetime, float_as_datetime,
float_as_duration, float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime,
Expand Down Expand Up @@ -271,15 +273,35 @@ impl<'a> Input<'a> for PyAny {
}
}

#[cfg(not(PyPy))]
fn lax_list(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(set) = self.cast_as::<PySet>() {
Ok(set.into())
} else if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
Ok(tuple.into())
} else if let Ok(dict_keys) = self.cast_as::<PyDictKeys>() {
let tuple = PyTuple::new(self.py(), dict_keys.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
let tuple = PyTuple::new(self.py(), dict_values.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
Copy link
Member

Choose a reason for hiding this comment

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

Looks like we have this lump of code in multiple cases, could you move it into a macro (or parameterised function?) and reuse?

} else {
Err(ValError::new(ErrorKind::ListType, self))
}
}

#[cfg(PyPy)]
fn lax_list(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::ListType, self))
}
Expand All @@ -293,15 +315,35 @@ impl<'a> Input<'a> for PyAny {
}
}

#[cfg(not(PyPy))]
fn lax_tuple(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(set) = self.cast_as::<PySet>() {
Ok(set.into())
} else if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_keys) = self.cast_as::<PyDictKeys>() {
let tuple = PyTuple::new(self.py(), dict_keys.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
let tuple = PyTuple::new(self.py(), dict_values.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::TupleType, self))
}
}

#[cfg(PyPy)]
fn lax_tuple(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::TupleType, self))
}
Expand All @@ -315,6 +357,7 @@ impl<'a> Input<'a> for PyAny {
}
}

#[cfg(not(PyPy))]
fn lax_set(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(set) = self.cast_as::<PySet>() {
Ok(set.into())
Expand All @@ -324,6 +367,33 @@ impl<'a> Input<'a> for PyAny {
Ok(tuple.into())
} else if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_keys) = self.cast_as::<PyDictKeys>() {
let tuple = PyTuple::new(self.py(), dict_keys.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
let tuple = PyTuple::new(self.py(), dict_values.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::SetType, self))
}
}

#[cfg(PyPy)]
fn lax_set(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(set) = self.cast_as::<PySet>() {
Ok(set.into())
} else if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::SetType, self))
}
Expand All @@ -337,6 +407,7 @@ impl<'a> Input<'a> for PyAny {
}
}

#[cfg(not(PyPy))]
fn lax_frozenset(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
Expand All @@ -346,6 +417,33 @@ impl<'a> Input<'a> for PyAny {
Ok(list.into())
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_keys) = self.cast_as::<PyDictKeys>() {
let tuple = PyTuple::new(self.py(), dict_keys.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
let tuple = PyTuple::new(self.py(), dict_values.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::FrozenSetType, self))
}
}

#[cfg(PyPy)]
fn lax_frozenset(&'a self) -> ValResult<GenericListLike<'a>> {
if let Ok(frozen_set) = self.cast_as::<PyFrozenSet>() {
Ok(frozen_set.into())
} else if let Ok(set) = self.cast_as::<PySet>() {
Ok(set.into())
} else if let Ok(list) = self.cast_as::<PyList>() {
Ok(list.into())
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(tuple.into())
} else if let Ok(iterator) = self.cast_as::<PyIterator>() {
let tuple = PyTuple::new(self.py(), iterator.iter()?.flatten().collect::<Vec<_>>());
Ok(tuple.into())
} else {
Err(ValError::new(ErrorKind::FrozenSetType, self))
}
Expand Down
2 changes: 2 additions & 0 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use pyo3::prelude::*;

#[cfg(not(PyPy))]
mod _pyo3_dict;
mod datetime;
mod input_abstract;
mod input_json;
Expand Down
26 changes: 20 additions & 6 deletions tests/validators/test_frozenset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
import re
from typing import Any, Dict

Expand Down Expand Up @@ -63,19 +64,32 @@ def test_frozenset_no_validators_both(py_and_json: PyAndJson, input_value, expec
@pytest.mark.parametrize(
'input_value,expected',
[
({1, 2, 3}, {1, 2, 3}),
({1, 2, 3}, frozenset({1, 2, 3})),
(frozenset(), frozenset()),
([1, 2, 3, 2, 3], {1, 2, 3}),
([1, 2, 3, 2, 3], frozenset({1, 2, 3})),
([], frozenset()),
((1, 2, 3, 2, 3), {1, 2, 3}),
((1, 2, 3, 2, 3), frozenset({1, 2, 3})),
((), frozenset()),
(frozenset([1, 2, 3, 2, 3]), {1, 2, 3}),
(frozenset([1, 2, 3, 2, 3]), frozenset({1, 2, 3})),
pytest.param(
{1: 1, 2: 2, 3: 3}.keys(),
frozenset({1, 2, 3}),
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
pytest.param(
{1: 1, 2: 2, 3: 3}.values(),
frozenset({1, 2, 3}),
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
((x for x in [1, 2, '3']), frozenset({1, 2, 3})),
({'abc'}, Err('0\n Input should be a valid integer')),
({1, 2, 'wrong'}, Err('Input should be a valid integer')),
({1: 2}, Err('1 validation error for frozenset[int]\n Input should be a valid frozenset')),
('abc', Err('Input should be a valid frozenset')),
# Technically correct, but does anyone actually need this? I think needs a new type in pyo3
pytest.param({1: 10, 2: 20, 3: 30}.keys(), {1, 2, 3}, marks=pytest.mark.xfail(raises=ValidationError)),
],
)
def test_frozenset_ints_python(input_value, expected):
Expand Down
50 changes: 32 additions & 18 deletions tests/validators/test_list.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import platform
import re
from typing import Any, Dict

import pytest
from dirty_equals import IsList, IsNonNegative

from pydantic_core import SchemaValidator, ValidationError

Expand Down Expand Up @@ -42,47 +42,61 @@ def test_list_strict():
[
([1, 2, '3'], [1, 2, 3]),
((1, 2, '3'), [1, 2, 3]),
({1, 2, '3'}, IsList(1, 2, 3, check_order=False)),
(frozenset([1, 2, '3']), IsList(1, 2, 3, check_order=False)),
({1, 2, '3'}, Err('Input should be a valid list/array [kind=list_type,')),
(frozenset({1, 2, '3'}), Err('Input should be a valid list/array [kind=list_type,')),
pytest.param(
{1: 1, 2: 2, '3': '3'}.keys(),
[1, 2, 3],
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
pytest.param(
{1: 1, 2: 2, '3': '3'}.values(),
[1, 2, 3],
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
((x for x in [1, 2, '3']), [1, 2, 3]),
],
)
def test_list_int(input_value, expected):
v = SchemaValidator({'type': 'list', 'items_schema': {'type': 'int'}})
assert v.validate_python(input_value) == expected
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


@pytest.mark.parametrize(
'input_value,expected',
[
([], []),
([1, '2', b'3'], [1, '2', b'3']),
(frozenset([1, '2', b'3']), IsList(1, '2', b'3', check_order=False)),
(frozenset([1, '2', b'3']), Err('Input should be a valid list/array [kind=list_type,')),
((), []),
((1, '2', b'3'), [1, '2', b'3']),
({1, '2', b'3'}, IsList(1, '2', b'3', check_order=False)),
({1, '2', b'3'}, Err('Input should be a valid list/array [kind=list_type,')),
],
)
def test_list_any(input_value, expected):
v = SchemaValidator('list')
output = v.validate_python(input_value)
assert output == expected
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


@pytest.mark.parametrize(
'input_value,index',
[
(['wrong'], 0),
(('wrong',), 0),
({'wrong'}, 0),
([1, 2, 3, 'wrong'], 3),
((1, 2, 3, 'wrong', 4), 3),
({1, 2, 'wrong'}, IsNonNegative()),
],
'input_value,index', [(['wrong'], 0), (('wrong',), 0), ([1, 2, 3, 'wrong'], 3), ((1, 2, 3, 'wrong', 4), 3)]
)
def test_list_error(input_value, index):
v = SchemaValidator({'type': 'list', 'items_schema': {'type': 'int'}})
with pytest.raises(ValidationError) as exc_info:
assert v.validate_python(input_value)
v.validate_python(input_value)
assert exc_info.value.errors() == [
{
'kind': 'int_parsing',
Expand Down
18 changes: 16 additions & 2 deletions tests/validators/test_set.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
import re
from typing import Any, Dict

Expand Down Expand Up @@ -62,11 +63,24 @@ def test_frozenset_no_validators_both(py_and_json: PyAndJson, input_value, expec
((1, 2, 3, 2, 3), {1, 2, 3}),
((), set()),
(frozenset([1, 2, 3, 2, 3]), {1, 2, 3}),
pytest.param(
{1: 1, 2: 2, '3': '3'}.keys(),
{1, 2, 3},
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
pytest.param(
{1: 1, 2: 2, '3': '3'}.values(),
{1, 2, 3},
marks=pytest.mark.skipif(
platform.python_implementation() == 'PyPy', reason='dict views not implemented in pyo3 for pypy'
),
),
((x for x in [1, 2, '3']), {1, 2, 3}),
({'abc'}, Err('0\n Input should be a valid integer')),
({1: 2}, Err('1 validation error for set[int]\n Input should be a valid set')),
('abc', Err('Input should be a valid set')),
# Technically correct, but does anyone actually need this? I think needs a new type in pyo3
pytest.param({1: 10, 2: 20, 3: 30}.keys(), {1, 2, 3}, marks=pytest.mark.xfail(raises=ValidationError)),
],
)
def test_set_ints_python(input_value, expected):
Expand Down