Skip to content

Commit 0dab63d

Browse files
committed
support BigInt
1 parent 4d8f3c4 commit 0dab63d

File tree

10 files changed

+154
-63
lines changed

10 files changed

+154
-63
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ url = "2.3.1"
4040
# idna is already required by url, added here to be explicit
4141
idna = "0.3.0"
4242
base64 = "0.13.1"
43+
num-bigint = "0.4.3"
4344

4445
[lib]
4546
name = "_pydantic_core"
@@ -50,7 +51,7 @@ crate-type = ["cdylib", "rlib"]
5051
extension-module = ["pyo3/extension-module"]
5152
# required for cargo bench
5253
auto-initialize = ["pyo3/auto-initialize"]
53-
default = ["mimalloc", "mimalloc/local_dynamic_tls", "pyo3/generate-import-lib"]
54+
default = ["mimalloc", "mimalloc/local_dynamic_tls", "pyo3/generate-import-lib", "pyo3/num-bigint"]
5455

5556
[profile.release]
5657
lto = "fat"

pydantic_core/core_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3772,8 +3772,8 @@ def definition_reference_schema(
37723772
'bool_parsing',
37733773
'int_type',
37743774
'int_parsing',
3775+
'int_parsing_size',
37753776
'int_from_float',
3776-
'int_overflow',
37773777
'float_type',
37783778
'float_parsing',
37793779
'bytes_type',

src/errors/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ pub enum ErrorType {
174174
// int errors
175175
IntType,
176176
IntParsing,
177+
IntParsingSize,
177178
IntFromFloat,
178-
IntOverflow,
179179
// ---------------------
180180
// float errors
181181
FloatType,
@@ -489,7 +489,7 @@ impl ErrorType {
489489
Self::IntType => "Input should be a valid integer",
490490
Self::IntParsing => "Input should be a valid integer, unable to parse string as an integer",
491491
Self::IntFromFloat => "Input should be a valid integer, got a number with a fractional part",
492-
Self::IntOverflow => "Input integer too large to convert to 64-bit integer",
492+
Self::IntParsingSize => "Unable to parse input string as an integer, exceed maximum size",
493493
Self::FloatType => "Input should be a valid number",
494494
Self::FloatParsing => "Input should be a valid number, unable to parse string as an number",
495495
Self::BytesType => "Input should be a valid bytes",

src/input/input_json.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ impl<'a> Input<'a> for JsonInput {
107107
JsonInput::String(s) => str_as_bool(self, s),
108108
JsonInput::Int(int) => int_as_bool(self, *int),
109109
JsonInput::Float(float) => match float_as_int(self, *float) {
110-
Ok(int) => int_as_bool(self, int),
110+
Ok(int) => int.as_bool().ok_or_else(|| ValError::new(ErrorType::BoolParsing, self)),
111111
_ => Err(ValError::new(ErrorType::BoolType, self)),
112112
},
113113
_ => Err(ValError::new(ErrorType::BoolType, self)),
@@ -122,18 +122,17 @@ impl<'a> Input<'a> for JsonInput {
122122
}
123123
}
124124
fn lax_int(&'a self) -> ValResult<EitherInt<'a>> {
125-
let int_result = match self {
125+
match self {
126126
JsonInput::Bool(b) => match *b {
127-
true => Ok(1),
128-
false => Ok(0),
127+
true => Ok(EitherInt::I64(1)),
128+
false => Ok(EitherInt::I64(0)),
129129
},
130-
JsonInput::Int(i) => Ok(*i),
131-
JsonInput::Uint(u) => return Ok(EitherInt::U64(*u)),
130+
JsonInput::Int(i) => Ok(EitherInt::I64(*i)),
131+
JsonInput::Uint(u) => Ok(EitherInt::U64(*u)),
132132
JsonInput::Float(f) => float_as_int(self, *f),
133133
JsonInput::String(str) => str_as_int(self, str),
134134
_ => Err(ValError::new(ErrorType::IntType, self)),
135-
};
136-
int_result.map(EitherInt::I64)
135+
}
137136
}
138137

139138
fn ultra_strict_float(&self) -> ValResult<f64> {

src/input/input_python.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ impl<'a> Input<'a> for PyAny {
260260
int_as_bool(self, int)
261261
} else if let Ok(float) = self.extract::<f64>() {
262262
match float_as_int(self, float) {
263-
Ok(int) => int_as_bool(self, int),
263+
Ok(int) => int.as_bool().ok_or_else(|| ValError::new(ErrorType::BoolParsing, self)),
264264
_ => Err(ValError::new(ErrorType::BoolType, self)),
265265
}
266266
} else {
@@ -287,10 +287,9 @@ impl<'a> Input<'a> for PyAny {
287287
if PyInt::is_exact_type_of(self) {
288288
Ok(EitherInt::Py(self))
289289
} else if let Some(cow_str) = maybe_as_string(self, ErrorType::IntParsing)? {
290-
let int = str_as_int(self, &cow_str)?;
291-
Ok(EitherInt::I64(int))
290+
str_as_int(self, &cow_str)
292291
} else if let Ok(float) = self.extract::<f64>() {
293-
Ok(EitherInt::I64(float_as_int(self, float)?))
292+
float_as_int(self, float)
294293
} else {
295294
Err(ValError::new(ErrorType::IntType, self))
296295
}

src/input/return_enums.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::borrow::Cow;
22
use std::slice::Iter as SliceIter;
33

4+
use num_bigint::BigInt;
5+
46
use pyo3::prelude::*;
57
use pyo3::types::iter::PyDictIterator;
68
use pyo3::types::{
@@ -822,6 +824,7 @@ impl<'a> IntoPy<PyObject> for EitherBytes<'a> {
822824
pub enum EitherInt<'a> {
823825
I64(i64),
824826
U64(u64),
827+
BigInt(BigInt),
825828
Py(&'a PyAny),
826829
}
827830

@@ -831,9 +834,41 @@ impl<'a> EitherInt<'a> {
831834
EitherInt::I64(i) => Ok(i),
832835
EitherInt::U64(u) => match i64::try_from(u) {
833836
Ok(u) => Ok(u),
834-
Err(_) => Err(ValError::new(ErrorType::IntOverflow, u.into_py(py).into_ref(py))),
837+
Err(_) => Err(ValError::new(ErrorType::IntParsingSize, u.into_py(py).into_ref(py))),
838+
},
839+
EitherInt::BigInt(u) => match i64::try_from(u) {
840+
Ok(u) => Ok(u),
841+
Err(e) => Err(ValError::new(
842+
ErrorType::IntParsingSize,
843+
e.into_original().into_py(py).into_ref(py),
844+
)),
845+
},
846+
EitherInt::Py(i) => i.extract().map_err(|_| ValError::new(ErrorType::IntParsingSize, i)),
847+
}
848+
}
849+
850+
pub fn as_bool(&self) -> Option<bool> {
851+
match self {
852+
EitherInt::I64(i) => match i {
853+
0 => Some(false),
854+
1 => Some(true),
855+
_ => None,
856+
},
857+
EitherInt::U64(u) => match u {
858+
0 => Some(false),
859+
1 => Some(true),
860+
_ => None,
861+
},
862+
EitherInt::BigInt(i) => match u8::try_from(i) {
863+
Ok(0) => Some(false),
864+
Ok(1) => Some(true),
865+
_ => None,
866+
},
867+
EitherInt::Py(i) => match i.extract::<u8>() {
868+
Ok(0) => Some(false),
869+
Ok(1) => Some(true),
870+
_ => None,
835871
},
836-
EitherInt::Py(i) => i.extract().map_err(|_| ValError::new(ErrorType::IntOverflow, i)),
837872
}
838873
}
839874
}
@@ -843,6 +878,7 @@ impl<'a> IntoPy<PyObject> for EitherInt<'a> {
843878
match self {
844879
Self::I64(int) => int.into_py(py),
845880
Self::U64(int) => int.into_py(py),
881+
Self::BigInt(int) => int.into_py(py),
846882
Self::Py(int) => int.into_py(py),
847883
}
848884
}

src/input/shared.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use num_bigint::BigInt;
2+
13
use crate::errors::{ErrorType, ValError, ValResult};
4+
use crate::input::EitherInt;
25

36
use super::Input;
47

@@ -43,12 +46,20 @@ pub fn int_as_bool<'a>(input: &'a impl Input<'a>, int: i64) -> ValResult<'a, boo
4346
}
4447
}
4548

46-
pub fn str_as_int<'s, 'l>(input: &'s impl Input<'s>, str: &'l str) -> ValResult<'s, i64> {
47-
if let Ok(i) = str.parse::<i64>() {
48-
Ok(i)
49-
} else if let Some(s) = strip_decimal_zeros(str) {
50-
if let Ok(i) = s.parse::<i64>() {
51-
Ok(i)
49+
/// parse a string as an int
50+
///
51+
/// max length of the input is 4300, see
52+
/// https://docs.python.org/3/whatsnew/3.11.html#other-cpython-implementation-changes and
53+
/// https://github.com/python/cpython/issues/95778 for more info in that length bound
54+
pub fn str_as_int<'s, 'l>(input: &'s impl Input<'s>, str: &'l str) -> ValResult<'s, EitherInt<'s>> {
55+
let len = str.len();
56+
if len > 4300 {
57+
Err(ValError::new(ErrorType::IntParsing, input))
58+
} else if let Some(int) = _parse_str(input, str, len) {
59+
Ok(int)
60+
} else if let Some(str_stripped) = strip_decimal_zeros(str) {
61+
if let Some(int) = _parse_str(input, str_stripped, len) {
62+
Ok(int)
5263
} else {
5364
Err(ValError::new(ErrorType::IntParsing, input))
5465
}
@@ -57,6 +68,19 @@ pub fn str_as_int<'s, 'l>(input: &'s impl Input<'s>, str: &'l str) -> ValResult<
5768
}
5869
}
5970

71+
/// parse a string as an int, `input` is required here to get lifetimes to match up
72+
///
73+
fn _parse_str<'s, 'l>(_input: &'s impl Input<'s>, str: &'l str, len: usize) -> Option<EitherInt<'s>> {
74+
if len < 19 {
75+
if let Ok(i) = str.parse::<i64>() {
76+
return Some(EitherInt::I64(i));
77+
}
78+
} else if let Ok(i) = str.parse::<BigInt>() {
79+
return Some(EitherInt::BigInt(i));
80+
}
81+
None
82+
}
83+
6084
/// we don't want to parse as f64 then call `float_as_int` as it can loose precision for large ints, therefore
6185
/// we strip `.0+` manually instead, then parse as i64
6286
fn strip_decimal_zeros(s: &str) -> Option<&str> {
@@ -68,14 +92,14 @@ fn strip_decimal_zeros(s: &str) -> Option<&str> {
6892
None
6993
}
7094

71-
pub fn float_as_int<'a>(input: &'a impl Input<'a>, float: f64) -> ValResult<'a, i64> {
95+
pub fn float_as_int<'a>(input: &'a impl Input<'a>, float: f64) -> ValResult<'a, EitherInt<'a>> {
7296
if float == f64::INFINITY || float == f64::NEG_INFINITY || float.is_nan() {
7397
Err(ValError::new(ErrorType::FiniteNumber, input))
7498
} else if float % 1.0 != 0.0 {
7599
Err(ValError::new(ErrorType::IntFromFloat, input))
76-
} else if float > i64::MAX as f64 || float < i64::MIN as f64 {
77-
Err(ValError::new(ErrorType::IntOverflow, input))
100+
} else if (i64::MIN as f64) < float && float < (i64::MAX as f64) {
101+
Ok(EitherInt::I64(float as i64))
78102
} else {
79-
Ok(float as i64)
103+
Err(ValError::new(ErrorType::IntParsingSize, input))
80104
}
81105
}

tests/test_errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ def f(input_value, info):
224224
('bool_parsing', 'Input should be a valid boolean, unable to interpret input', None),
225225
('int_type', 'Input should be a valid integer', None),
226226
('int_parsing', 'Input should be a valid integer, unable to parse string as an integer', None),
227+
('int_parsing_size', 'Unable to parse input string as an integer, exceed maximum size', None),
227228
('int_from_float', 'Input should be a valid integer, got a number with a fractional part', None),
228-
('int_overflow', 'Input integer too large to convert to 64-bit integer', None),
229229
('multiple_of', 'Input should be a multiple of 42.1', {'multiple_of': 42.1}),
230230
('greater_than', 'Input should be greater than 42.1', {'gt': 42.1}),
231231
('greater_than_equal', 'Input should be greater than or equal to 42.1', {'ge': 42.1}),

0 commit comments

Comments
 (0)