Skip to content

Commit

Permalink
add dict_key and dict_value
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Aug 1, 2022
1 parent cf82484 commit 24b77cd
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 8 deletions.
41 changes: 41 additions & 0 deletions src/input/_pyo3_dict.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// TODO: remove this file once a new pyo3 version is released
// with https://github.com/PyO3/pyo3/pull/2358

use pyo3::types::PySequence;
use pyo3::{ffi, pyobject_native_type_core, PyAny, PyTryFrom};

/// 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
);

impl PyDictKeys {
pub fn as_sequence(&self) -> &PySequence {
unsafe { PySequence::try_from_unchecked(self) }
}
}

/// 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
);

impl PyDictValues {
pub fn as_sequence(&self) -> &PySequence {
unsafe { PySequence::try_from_unchecked(self) }
}
}
74 changes: 74 additions & 0 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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,6 +273,22 @@ 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(dict_keys) = self.cast_as::<PyDictKeys>() {
Ok(dict_keys.as_sequence().tuple()?.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
Ok(dict_values.as_sequence().tuple()?.into())
} 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())
Expand All @@ -289,6 +307,22 @@ 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(dict_keys) = self.cast_as::<PyDictKeys>() {
Ok(dict_keys.as_sequence().tuple()?.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
Ok(dict_values.as_sequence().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())
Expand All @@ -307,6 +341,26 @@ 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())
} 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(dict_keys) = self.cast_as::<PyDictKeys>() {
Ok(dict_keys.as_sequence().tuple()?.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
Ok(dict_values.as_sequence().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())
Expand All @@ -329,6 +383,26 @@ 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())
} 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(dict_keys) = self.cast_as::<PyDictKeys>() {
Ok(dict_keys.as_sequence().tuple()?.into())
} else if let Ok(dict_values) = self.cast_as::<PyDictValues>() {
Ok(dict_values.as_sequence().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())
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
25 changes: 19 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,31 @@ 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'
),
),
({'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
15 changes: 15 additions & 0 deletions tests/validators/test_list.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 @@ -43,6 +44,20 @@ def test_list_strict():
((1, 2, '3'), [1, 2, 3]),
({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'
),
),
],
)
def test_list_int(input_value, expected):
Expand Down
17 changes: 15 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,23 @@ 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'
),
),
({'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
15 changes: 15 additions & 0 deletions tests/validators/test_tuple.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
import re
from typing import Any, Dict, Type

Expand Down Expand Up @@ -105,6 +106,20 @@ def test_tuple_var_len_kwargs(kwargs: Dict[str, Any], input_value, expected):
[
((1, 2, '3'), (1, 2, 3)),
([1, 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'
),
),
({1, 2, '3'}, Err('Input should be a valid tuple [kind=tuple_type,')),
(frozenset([1, 2, '3']), Err('Input should be a valid tuple [kind=tuple_type,')),
],
Expand Down

0 comments on commit 24b77cd

Please sign in to comment.