-
Notifications
You must be signed in to change notification settings - Fork 215
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
[Fix(9559)]
- Validation fails for enum field with decimal type
#1324
base: main
Are you sure you want to change the base?
Changes from all commits
d736610
399245b
36ce3ba
dd32fc0
b327afa
b11d3e8
af0766c
6a393e4
beffb8f
0e23f4a
80308f3
a5e1d3d
00b5346
a6c2a30
5d6986c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ docs/_build/ | |
htmlcov/ | ||
node_modules/ | ||
|
||
.venv | ||
/.benchmarks/ | ||
/.idea/ | ||
/.pytest_cache/ | ||
|
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -79,6 +79,7 @@ pub trait EnumValidateValue: std::fmt::Debug + Clone + Send + Sync { | |||
py: Python<'py>, | ||||
input: &I, | ||||
lookup: &LiteralLookup<PyObject>, | ||||
class: &Py<PyType>, | ||||
strict: bool, | ||||
) -> ValResult<Option<PyObject>>; | ||||
} | ||||
|
@@ -116,7 +117,7 @@ impl<T: EnumValidateValue> Validator for EnumValidator<T> { | |||
}, | ||||
input, | ||||
)); | ||||
} else if let Some(v) = T::validate_value(py, input, &self.lookup, strict)? { | ||||
} else if let Some(v) = T::validate_value(py, input, &self.lookup, &self.class, strict)? { | ||||
state.floor_exactness(Exactness::Lax); | ||||
return Ok(v); | ||||
} else if let Some(ref missing) = self.missing { | ||||
|
@@ -167,6 +168,7 @@ impl EnumValidateValue for PlainEnumValidator { | |||
py: Python<'py>, | ||||
input: &I, | ||||
lookup: &LiteralLookup<PyObject>, | ||||
class: &Py<PyType>, | ||||
strict: bool, | ||||
) -> ValResult<Option<PyObject>> { | ||||
match lookup.validate(py, input)? { | ||||
|
@@ -183,8 +185,14 @@ impl EnumValidateValue for PlainEnumValidator { | |||
} else if py_input.is_instance_of::<PyFloat>() { | ||||
return Ok(lookup.validate_int(py, input, false)?.map(|v| v.clone_ref(py))); | ||||
} | ||||
if py_input.is_instance_of::<PyAny>() { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
if let Ok(res) = class.call1(py, (py_input,)) { | ||||
return Ok(Some(res)); | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
||||
Ok(None) | ||||
} | ||||
} | ||||
|
@@ -201,6 +209,7 @@ impl EnumValidateValue for IntEnumValidator { | |||
py: Python<'py>, | ||||
input: &I, | ||||
lookup: &LiteralLookup<PyObject>, | ||||
_class: &Py<PyType>, | ||||
strict: bool, | ||||
) -> ValResult<Option<PyObject>> { | ||||
Ok(lookup.validate_int(py, input, strict)?.map(|v| v.clone_ref(py))) | ||||
|
@@ -217,6 +226,7 @@ impl EnumValidateValue for StrEnumValidator { | |||
py: Python, | ||||
input: &I, | ||||
lookup: &LiteralLookup<PyObject>, | ||||
_class: &Py<PyType>, | ||||
strict: bool, | ||||
) -> ValResult<Option<PyObject>> { | ||||
Ok(lookup.validate_str(input, strict)?.map(|v| v.clone_ref(py))) | ||||
|
@@ -233,6 +243,7 @@ impl EnumValidateValue for FloatEnumValidator { | |||
py: Python<'py>, | ||||
input: &I, | ||||
lookup: &LiteralLookup<PyObject>, | ||||
_class: &Py<PyType>, | ||||
strict: bool, | ||||
) -> ValResult<Option<PyObject>> { | ||||
Ok(lookup.validate_float(py, input, strict)?.map(|v| v.clone_ref(py))) | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import re | ||
import sys | ||
from decimal import Decimal | ||
from enum import Enum, IntEnum, IntFlag | ||
|
||
import pytest | ||
|
@@ -344,3 +345,130 @@ class ColorEnum(IntEnum): | |
|
||
assert v.validate_python(ColorEnum.GREEN) is ColorEnum.GREEN | ||
assert v.validate_python(1 << 63) is ColorEnum.GREEN | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'value', | ||
[-1, 0, 1], | ||
) | ||
def test_enum_int_validation_should_succeed_for_decimal(value: int): | ||
# GIVEN | ||
class MyEnum(Enum): | ||
VALUE = value | ||
|
||
class MyIntEnum(IntEnum): | ||
VALUE = value | ||
|
||
# WHEN | ||
v = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values())), | ||
default=MyEnum.VALUE, | ||
) | ||
) | ||
|
||
v_int = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyIntEnum, list(MyIntEnum.__members__.values())), | ||
default=MyIntEnum.VALUE, | ||
) | ||
) | ||
|
||
# THEN | ||
assert v.validate_python(Decimal(value)) is MyEnum.VALUE | ||
assert v.validate_python(Decimal(float(value))) is MyEnum.VALUE | ||
|
||
assert v_int.validate_python(Decimal(value)) is MyIntEnum.VALUE | ||
assert v_int.validate_python(Decimal(float(value))) is MyIntEnum.VALUE | ||
|
||
|
||
def test_enum_int_validation_should_succeed_for_custom_type(): | ||
# GIVEN | ||
class AnyWrapper: | ||
def __init__(self, value): | ||
self.value = value | ||
|
||
def __eq__(self, other: object) -> bool: | ||
return self.value == other | ||
|
||
class MyEnum(Enum): | ||
VALUE = 999 | ||
SECOND_VALUE = 1000000 | ||
THIRD_VALUE = 'Py03' | ||
|
||
# WHEN | ||
v = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values())), | ||
default=MyEnum.VALUE, | ||
) | ||
) | ||
|
||
# THEN | ||
assert v.validate_python(AnyWrapper(999)) is MyEnum.VALUE | ||
assert v.validate_python(AnyWrapper(1000000)) is MyEnum.SECOND_VALUE | ||
assert v.validate_python(AnyWrapper('Py03')) is MyEnum.THIRD_VALUE | ||
|
||
|
||
def test_enum_str_validation_should_fail_for_decimal_when_expecting_str_value(): | ||
# GIVEN | ||
class MyEnum(Enum): | ||
VALUE = '1' | ||
|
||
# WHEN | ||
v = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values())), | ||
default=MyEnum.VALUE, | ||
) | ||
) | ||
|
||
# THEN | ||
with pytest.raises(ValidationError): | ||
v.validate_python(Decimal(1)) | ||
|
||
|
||
def test_enum_int_validation_should_fail_for_incorrect_decimal_value(): | ||
# GIVEN | ||
class MyEnum(Enum): | ||
VALUE = 1 | ||
|
||
# WHEN | ||
v = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values())), | ||
default=MyEnum.VALUE, | ||
) | ||
) | ||
|
||
# THEN | ||
with pytest.raises(ValidationError): | ||
v.validate_python(Decimal(2)) | ||
|
||
with pytest.raises(ValidationError): | ||
v.validate_python((1, 2)) | ||
|
||
with pytest.raises(ValidationError): | ||
v.validate_python(Decimal(1.1)) | ||
|
||
|
||
def test_enum_int_validation_should_fail_for_plain_type_without_eq_checking(): | ||
# GIVEN | ||
class MyEnum(Enum): | ||
VALUE = 1 | ||
Comment on lines
+456
to
+458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realise now that we probably also want to test this with e.g. |
||
|
||
class MyClass: | ||
def __init__(self, value): | ||
self.value = value | ||
|
||
# WHEN | ||
v = SchemaValidator( | ||
core_schema.with_default_schema( | ||
schema=core_schema.enum_schema(MyEnum, list(MyEnum.__members__.values())), | ||
default=MyEnum.VALUE, | ||
) | ||
) | ||
|
||
# THEN | ||
with pytest.raises(ValidationError): | ||
v.validate_python(MyClass(1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So now that I look at this new point in the diff, I see that (and I regret forgetting this) that we already call
_missing_
here. But as we observe in the issue and this PR, simply calling_missing_
is not enough because enum__new__
has more complex logic which isn't encapsulated purely by_missing_
.I wonder if there is a case to have a new branch (before or after this one? not sure 🤔) which calls the enum type, with the logic going here instead of in
PlainEnumValidator
. Putting the logic here would also solve the special-cased enums likeIntEnum
, I think.That does beg the question, though: if we add the case of calling the enum type here, do we need logic for
_missing_
at all? My intuition is that we don't, and we should try to phase out the_missing_
logic.