From df93391da9dded85967f931641e3d6f441e67a49 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 01:14:06 -0400 Subject: [PATCH 1/8] Add lax_str and lax_int support for enum values not inherited from str/int --- src/input/input_python.rs | 25 ++++++++++++++++++++++++- tests/validators/test_int.py | 13 +++++++++++++ tests/validators/test_string.py | 15 +++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index cf84c5517..b3767f174 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -255,7 +255,17 @@ impl<'a> Input<'a> for PyAny { || self.is_instance(decimal_type.as_ref(py)).unwrap_or_default() } { Ok(self.str()?.into()) - } else { + } else if let Some(enum_val) = maybe_as_enum(self.py(), self) { + return Ok(enum_val.str()?.into()); + } else { + // let py = self.py(); + // // Enum value not inherited from `str` + // let enum_object = py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py); + // let meta_type = self.get_type().get_type(); + // if meta_type.is(&enum_object) { + // let val = self.getattr(intern!(py, "value"))?; + // return Ok(val.str()?.into()); + // } Err(ValError::new(ErrorTypeDefaults::StringType, self)) } } @@ -339,6 +349,8 @@ impl<'a> Input<'a> for PyAny { decimal_as_int(self.py(), self, decimal) } else if let Ok(float) = self.extract::() { float_as_int(self, float) + } else if let Some(enum_val) = maybe_as_enum(self.py(), self) { + Ok(EitherInt::Py(enum_val)) } else { Err(ValError::new(ErrorTypeDefaults::IntType, self)) } @@ -758,6 +770,17 @@ fn maybe_as_string(v: &PyAny, unicode_error: ErrorType) -> ValResult(py: Python<'a>, v: &'a PyAny) -> Option<&'a PyAny> { + let enum_meta_object = py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py); + let meta_type = v.get_type().get_type(); + if meta_type.is(&enum_meta_object) { + v.getattr(intern!(py, "value")).ok() + } else { + None + } +} + #[cfg(PyPy)] static DICT_KEYS_TYPE: pyo3::once_cell::GILOnceCell> = pyo3::once_cell::GILOnceCell::new(); diff --git a/tests/validators/test_int.py b/tests/validators/test_int.py index 8d5850dc8..dedc2bd93 100644 --- a/tests/validators/test_int.py +++ b/tests/validators/test_int.py @@ -459,3 +459,16 @@ def test_float_subclass() -> None: v_lax = v.validate_python(FloatSubclass(1)) assert v_lax == 1 assert type(v_lax) == int + + +def test_int_subclass_plain_enum() -> None: + v = SchemaValidator({'type': 'int'}) + + from enum import Enum + + class PlainEnum(Enum): + ONE = 1 + + v_lax = v.validate_python(PlainEnum.ONE) + assert v_lax == 1 + assert type(v_lax) == int diff --git a/tests/validators/test_string.py b/tests/validators/test_string.py index acb145a58..523276083 100644 --- a/tests/validators/test_string.py +++ b/tests/validators/test_string.py @@ -249,6 +249,21 @@ def test_lax_subclass(FruitEnum, kwargs): assert repr(p) == "'pear'" +@pytest.mark.parametrize('kwargs', [{}, {'to_lower': True}], ids=repr) +def test_lax_subclass_plain_enum(kwargs): + v = SchemaValidator(core_schema.str_schema(**kwargs)) + + from enum import Enum + + class PlainEnum(Enum): + ONE = 'one' + + p = v.validate_python(PlainEnum.ONE) + assert p == 'one' + assert type(p) is str + assert repr(p) == "'one'" + + def test_subclass_preserved() -> None: class StrSubclass(str): pass From a25c9161a2fac4f7e32a21b9fb795cd0a591bffb Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 01:15:33 -0400 Subject: [PATCH 2/8] ob_type: Fix bug --- src/serializers/ob_type.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serializers/ob_type.rs b/src/serializers/ob_type.rs index fc491f618..36484113e 100644 --- a/src/serializers/ob_type.rs +++ b/src/serializers/ob_type.rs @@ -82,7 +82,7 @@ impl ObTypeLookup { timedelta: PyDelta::new(py, 0, 0, 0, false).unwrap().get_type_ptr() as usize, url: PyUrl::new(lib_url.clone()).into_py(py).as_ref(py).get_type_ptr() as usize, multi_host_url: PyMultiHostUrl::new(lib_url, None).into_py(py).as_ref(py).get_type_ptr() as usize, - enum_object: py.import("enum").unwrap().getattr("Enum").unwrap().to_object(py), + enum_object: py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py), generator_object: py .import("types") .unwrap() From 62c06913c6a65f9c77fc7edc1375f09330d8fb49 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 01:21:25 -0400 Subject: [PATCH 3/8] clean up --- src/input/input_python.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index b3767f174..6e5d1eb3c 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -255,17 +255,9 @@ impl<'a> Input<'a> for PyAny { || self.is_instance(decimal_type.as_ref(py)).unwrap_or_default() } { Ok(self.str()?.into()) - } else if let Some(enum_val) = maybe_as_enum(self.py(), self) { + } else if let Some(enum_val) = maybe_as_enum(self) { return Ok(enum_val.str()?.into()); } else { - // let py = self.py(); - // // Enum value not inherited from `str` - // let enum_object = py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py); - // let meta_type = self.get_type().get_type(); - // if meta_type.is(&enum_object) { - // let val = self.getattr(intern!(py, "value"))?; - // return Ok(val.str()?.into()); - // } Err(ValError::new(ErrorTypeDefaults::StringType, self)) } } @@ -349,7 +341,7 @@ impl<'a> Input<'a> for PyAny { decimal_as_int(self.py(), self, decimal) } else if let Ok(float) = self.extract::() { float_as_int(self, float) - } else if let Some(enum_val) = maybe_as_enum(self.py(), self) { + } else if let Some(enum_val) = maybe_as_enum(self) { Ok(EitherInt::Py(enum_val)) } else { Err(ValError::new(ErrorTypeDefaults::IntType, self)) @@ -771,7 +763,8 @@ fn maybe_as_string(v: &PyAny, unicode_error: ErrorType) -> ValResult(py: Python<'a>, v: &'a PyAny) -> Option<&'a PyAny> { +fn maybe_as_enum<'a>(v: &'a PyAny) -> Option<&'a PyAny> { + let py = v.py(); let enum_meta_object = py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py); let meta_type = v.get_type().get_type(); if meta_type.is(&enum_meta_object) { From fcd9a42c5e1b8f8a24f588b154bcd7879177794a Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 01:56:25 -0400 Subject: [PATCH 4/8] only import enum module once --- src/input/input_python.rs | 7 +++++-- src/input/shared.rs | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index 6e5d1eb3c..e2471409a 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -21,7 +21,10 @@ use super::datetime::{ float_as_duration, float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime, EitherTime, }; -use super::shared::{decimal_as_int, float_as_int, int_as_bool, map_json_err, str_as_bool, str_as_float, str_as_int}; +use super::shared::{ + decimal_as_int, float_as_int, get_enum_meta_object, int_as_bool, map_json_err, str_as_bool, str_as_float, + str_as_int, +}; use super::{ py_string_str, BorrowInput, EitherBytes, EitherFloat, EitherInt, EitherString, EitherTimedelta, GenericArguments, GenericIterable, GenericIterator, GenericMapping, Input, JsonInput, PyArgs, @@ -765,7 +768,7 @@ fn maybe_as_string(v: &PyAny, unicode_error: ErrorType) -> ValResult(v: &'a PyAny) -> Option<&'a PyAny> { let py = v.py(); - let enum_meta_object = py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py); + let enum_meta_object = get_enum_meta_object(py); let meta_type = v.get_type().get_type(); if meta_type.is(&enum_meta_object) { v.getattr(intern!(py, "value")).ok() diff --git a/src/input/shared.rs b/src/input/shared.rs index 1a8e2b61c..20b4cbbf1 100644 --- a/src/input/shared.rs +++ b/src/input/shared.rs @@ -1,11 +1,25 @@ use num_bigint::BigInt; -use pyo3::{intern, PyAny, Python}; +use pyo3::sync::GILOnceCell; +use pyo3::{intern, Py, PyAny, Python, ToPyObject}; use crate::errors::{ErrorType, ErrorTypeDefaults, ValError, ValResult}; use super::parse_json::{JsonArray, JsonInput}; use super::{EitherFloat, EitherInt, Input}; +static ENUM_META_OBJECT: GILOnceCell> = GILOnceCell::new(); + +pub fn get_enum_meta_object(py: Python) -> Py { + ENUM_META_OBJECT + .get_or_init(py, || { + py.import("enum") + .and_then(|enum_module| enum_module.getattr("EnumMeta")) + .unwrap() + .to_object(py) + }) + .clone() +} + pub fn map_json_err<'a>(input: &'a impl Input<'a>, error: serde_json::Error) -> ValError<'a> { ValError::new( ErrorType::JsonInvalid { From 2543f2ae756d5455befbb956822212742b522c80 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 02:02:55 -0400 Subject: [PATCH 5/8] lint --- src/input/input_python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index e2471409a..e42243e40 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -766,7 +766,7 @@ fn maybe_as_string(v: &PyAny, unicode_error: ErrorType) -> ValResult(v: &'a PyAny) -> Option<&'a PyAny> { +fn maybe_as_enum(v: &PyAny) -> Option<&PyAny> { let py = v.py(); let enum_meta_object = get_enum_meta_object(py); let meta_type = v.get_type().get_type(); From df0d8e3a14bd5bb1ff8a6fd393777d915f316c71 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 12 Oct 2023 09:45:08 -0400 Subject: [PATCH 6/8] Fixup --- src/input/input_python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index e42243e40..eb8c553ba 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -259,7 +259,7 @@ impl<'a> Input<'a> for PyAny { } { Ok(self.str()?.into()) } else if let Some(enum_val) = maybe_as_enum(self) { - return Ok(enum_val.str()?.into()); + Ok(enum_val.str()?.into()) } else { Err(ValError::new(ErrorTypeDefaults::StringType, self)) } From 79277fcd0a1393ae467875d444ae1b27bd0d23c1 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Sun, 15 Oct 2023 05:38:50 -0400 Subject: [PATCH 7/8] Use interned strings --- src/input/shared.rs | 4 ++-- src/serializers/ob_type.rs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/input/shared.rs b/src/input/shared.rs index 20b4cbbf1..105da4bcc 100644 --- a/src/input/shared.rs +++ b/src/input/shared.rs @@ -12,8 +12,8 @@ static ENUM_META_OBJECT: GILOnceCell> = GILOnceCell::new(); pub fn get_enum_meta_object(py: Python) -> Py { ENUM_META_OBJECT .get_or_init(py, || { - py.import("enum") - .and_then(|enum_module| enum_module.getattr("EnumMeta")) + py.import(intern!(py, "enum")) + .and_then(|enum_module| enum_module.getattr(intern!(py, "EnumMeta"))) .unwrap() .to_object(py) }) diff --git a/src/serializers/ob_type.rs b/src/serializers/ob_type.rs index 36484113e..e4af8bf6f 100644 --- a/src/serializers/ob_type.rs +++ b/src/serializers/ob_type.rs @@ -82,7 +82,12 @@ impl ObTypeLookup { timedelta: PyDelta::new(py, 0, 0, 0, false).unwrap().get_type_ptr() as usize, url: PyUrl::new(lib_url.clone()).into_py(py).as_ref(py).get_type_ptr() as usize, multi_host_url: PyMultiHostUrl::new(lib_url, None).into_py(py).as_ref(py).get_type_ptr() as usize, - enum_object: py.import("enum").unwrap().getattr("EnumMeta").unwrap().to_object(py), + enum_object: py + .import(intern!(py, "enum")) + .unwrap() + .getattr(intern!(py, "EnumMeta")) + .unwrap() + .to_object(py), generator_object: py .import("types") .unwrap() From 75d22a61bd81134a7864afde8e4189c92814ac42 Mon Sep 17 00:00:00 2001 From: Michael Huang Date: Thu, 26 Oct 2023 11:20:22 -0400 Subject: [PATCH 8/8] Implemented suggested fix for is_enum check on ob_type --- src/serializers/ob_type.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/serializers/ob_type.rs b/src/serializers/ob_type.rs index e4af8bf6f..109aed3bb 100644 --- a/src/serializers/ob_type.rs +++ b/src/serializers/ob_type.rs @@ -82,12 +82,7 @@ impl ObTypeLookup { timedelta: PyDelta::new(py, 0, 0, 0, false).unwrap().get_type_ptr() as usize, url: PyUrl::new(lib_url.clone()).into_py(py).as_ref(py).get_type_ptr() as usize, multi_host_url: PyMultiHostUrl::new(lib_url, None).into_py(py).as_ref(py).get_type_ptr() as usize, - enum_object: py - .import(intern!(py, "enum")) - .unwrap() - .getattr(intern!(py, "EnumMeta")) - .unwrap() - .to_object(py), + enum_object: py.import("enum").unwrap().getattr("Enum").unwrap().to_object(py), generator_object: py .import("types") .unwrap() @@ -264,8 +259,9 @@ impl ObTypeLookup { fn is_enum(&self, op_value: Option<&PyAny>, py_type: &PyType) -> bool { // only test on the type itself, not base types if op_value.is_some() { + let enum_meta_type = self.enum_object.as_ref(py_type.py()).get_type(); let meta_type = py_type.get_type(); - meta_type.is(&self.enum_object) + meta_type.is(enum_meta_type) } else { false }