8000 avoid coercing int subclasses to floats by samuelcolvin · Pull Request #914 · pydantic/pydantic-core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
8000

avoid coercing int subclasses to floats #914

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

Merged
merged 5 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ features = ["pyo3/extension-module"]
[tool.ruff]
line-length = 120
extend-select = ['Q', 'RUF100', 'C90', 'I']
extend-ignore = ['E501']
extend-ignore = [
'E501', # ignore line too long and let black handle it
'E721', # using type() instead of isinstance() - we use this in tests
]
flake8-quotes = {inline-quotes = 'single', multiline-quotes = 'double'}
mccabe = { max-complexity = 13 }
isort = { known-first-party = ['pydantic_core', 'tests'] }
Expand Down
9 changes: 8 additions & 1 deletion src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,9 @@ impl<'a> Input<'a> for PyAny {
if PyBool::is_exact_type_of(self) {
Err(ValError::new(ErrorTypeDefaults::IntType, self))
} else {
Ok(EitherInt::Py(self))
// force to an int to upcast to a pure python int
Copy link
Member Author

Choose a reason for hiding this comment

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

we now do the same in "strict" mode to match lax mode

i would definitely consider reverting this change - substituting consistency between lax and strict for a smaller change of breaking changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I think we want to go the other way and not upcast in both, I already can't remember why we wanted the upcast in lax mode.

Regardless of to upcast or not upcast, I think both modes should behave the same, so we should change one or the other.

Also you might want to consider using BigInt here if the i64 extraction fails.

let int = self.extract::<i64>()?;
Ok(EitherInt::I64(int))
}
} else {
Err(ValError::new(ErrorTypeDefaults::IntType, self))
Expand All @@ -314,7 +316,12 @@ impl<'a> Input<'a> for PyAny {
if PyInt::is_exact_type_of(self) {
Ok(EitherInt::Py(self))
} else if let Some(cow_str) = maybe_as_string(self, ErrorTypeDefaults::IntParsing)? {
// Try strings before subclasses of int as that will be far more common
str_as_int(self, &cow_str)
} else if PyInt::is_type_of(self) {
// force to an int to upcast to a pure python int to maintain current behaviour
let int = self.extract::<i64>()?;
Ok(EitherInt::I64(int))
} else if let Ok(float) = self.extract::<f64>() {
float_as_int(self, float)
} else {
Expand Down
2 changes: 1 addition & 1 deletion tests/serializers/test_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_subclass_bytes(schema_type, input_value, expected_json):

v = s.to_python(input_value, mode='json')
assert v == expected_json
assert type(v) == str # noqa: E721
assert type(v) == str

assert s.to_json(input_value) == json.dumps(expected_json).encode('utf-8')

Expand Down
14 changes: 7 additions & 7 deletions tests/serializers/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ def test_int_to_float():
s = SchemaSerializer(core_schema.float_schema())
v_plain = s.to_python(1)
assert v_plain == 1
assert type(v_plain) == int # noqa: E721
assert type(v_plain) == int

v_plain_subclass = s.to_python(IntSubClass(1))
assert v_plain_subclass == IntSubClass(1)
assert type(v_plain_subclass) == IntSubClass

v_json = s.to_python(1, mode='json')
assert v_json == 1.0
assert type(v_json) == float # noqa: E721
assert type(v_json) == float

v_json_subclass = s.to_python(IntSubClass(1), mode='json')
assert v_json_subclass == 1
assert type(v_json_subclass) == float # noqa: E721
assert type(v_json_subclass) == float

assert s.to_json(1) == b'1.0'
assert s.to_json(IntSubClass(1)) == b'1.0'
Expand All @@ -95,12 +95,12 @@ def test_int_to_float_key():
s = SchemaSerializer(core_schema.dict_schema(core_schema.float_schema(), core_schema.float_schema()))
v_plain = s.to_python({1: 1})
assert v_plain == {1: 1}
assert type(list(v_plain.keys())[0]) == int # noqa: E721
assert type(v_plain[1]) == int # noqa: E721
assert type(list(v_plain.keys())[0]) == int
assert type(v_plain[1]) == int

v_json = s.to_python({1: 1}, mode='json')
assert v_json == {'1': 1.0}
assert type(v_json['1']) == float # noqa: E721
assert type(v_json['1']) == float

assert s.to_json({1: 1}) == b'{"1":1.0}'

Expand Down Expand Up @@ -133,6 +133,6 @@ def test_numpy():

v = s.to_python(numpy.float64(1.0), mode='json')
assert v == 1.0
assert type(v) == float # noqa: E721
assert type(v) == float

assert s.to_json(numpy.float64(1.0)) == b'1.0'
2 changes: 1 addition & 1 deletion tests/serializers/test_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ def test_subclass_str(schema_type, input_value, expected):

v = s.to_python(input_value, mode='json')
assert v == expected
assert type(v) == str # noqa: E721
assert type(v) == str

assert s.to_json(input_value) == json.dumps(expected).encode('utf-8')
32 changes: 31 additions & 1 deletion tests/validators/test_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_int_py_and_json(py_and_json: PyAndJson, input_value, expected):
else:
output = v.validate_test(input_value)
assert output == expected
assert isinstance(output, int)
assert type(output) == int


@pytest.mark.parametrize(
Expand Down Expand Up @@ -405,3 +405,33 @@ def test_string_as_int_with_underscores() -> None:
v.validate_python(edge_case)
with pytest.raises(ValidationError):
v.validate_json(f'"{edge_case}"')


class IntSubclass(int):
pass


def test_int_subclass() -> None:
v = SchemaValidator({'type': 'int'})
v_lax = v.validate_python(IntSubclass(1))
assert v_lax == 1
assert type(v_lax) == int
v_strict = v.validate_python(IntSubclass(1), strict=True)
assert v_strict == 1
assert type(v_strict) == int

assert v.validate_python(IntSubclass(1136885225876639845)) == 1136885225876639845
assert v.validate_python(IntSubclass(1136885225876639845), strict=True) == 1136885225876639845


def test_int_subclass_constraint() -> None:
v = SchemaValidator({'type': 'int', 'gt': 0})
v_lax = v.validate_python(IntSubclass(1))
assert v_lax == 1
assert type(v_lax) == int
v_strict = v.validate_python(IntSubclass(1), strict=True)
assert v_strict == 1
assert type(v_strict) == int

with pytest.raises(ValidationError, match='Input should be greater than 0'):
v.validate_python(IntSubclass(0))
2 changes: 1 addition & 1 deletion tests/validators/test_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_lax_subclass(FruitEnum, kwargs):
assert v.validate_python(b'foobar') == 'foobar'
p = v.validate_python(FruitEnum.pear)
assert p == 'pear'
assert type(p) is str # noqa: E721
assert type(p) is str
assert repr(p) == "'pear'"


Expand Down
0