Skip to content

Commit ea51142

Browse files
committed
update fraction, add fraction validator tests and adjust to pass the tests
1 parent e8f1385 commit ea51142

File tree

7 files changed

+408
-57
lines changed

7 files changed

+408
-57
lines changed

python/pydantic_core/core_schema.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,10 @@ def decimal_schema(
812812

813813
class FractionSchema(TypedDict, total=False):
814814
type: Required[Literal['decimal']]
815-
le: Decimal
816-
ge: Decimal
817-
lt: Decimal
818-
gt: Decimal
815+
le: Fraction
816+
ge: Fraction
817+
lt: Fraction
818+
gt: Fraction
819819
strict: bool
820820
ref: str
821821
metadata: dict[str, Any]

src/errors/types.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,8 +582,8 @@ impl ErrorType {
582582
Self::DecimalMaxDigits {..} => "Decimal input should have no more than {max_digits} digit{expected_plural} in total",
583583
Self::DecimalMaxPlaces {..} => "Decimal input should have no more than {decimal_places} decimal place{expected_plural}",
584584
Self::DecimalWholeDigits {..} => "Decimal input should have no more than {whole_digits} digit{expected_plural} before the decimal point",
585-
Self::FractionParsing {..} => "Fraction input should be a tuple of two integers, a string or a Fraction object",
586-
Self::FractionType {..} => "Fraction input should be a tuple of two integers, or a string or Fraction object",
585+
Self::FractionParsing {..} => "Input should be a valid fraction",
586+
Self::FractionType {..} => "Fraction input should be an integer, float, string or Fraction object",
587587
Self::ComplexType {..} => "Input should be a valid python complex object, a number, or a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex",
588588
Self::ComplexStrParsing {..} => "Input should be a valid complex string following the rules at https://docs.python.org/3/library/functions.html#complex",
589589
}

src/input/input_json.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,13 @@ impl<'py, 'data> Input<'py> for JsonValue<'data> {
202202

203203
fn validate_fraction(&self, _strict: bool, py: Python<'py>) -> ValMatch<Bound<'py, PyAny>> {
204204
match self {
205+
JsonValue::Float(f) => {
206+
create_fraction(&PyString::new(py, &f.to_string()), self).map(ValidationMatch::strict)
207+
}
205208
JsonValue::Str(..) | JsonValue::Int(..) | JsonValue::BigInt(..) => {
206209
create_fraction(&self.into_pyobject(py)?, self).map(ValidationMatch::strict)
207210
}
208-
_ => Err(ValError::new(ErrorTypeDefaults::DecimalType, self)),
211+
_ => Err(ValError::new(ErrorTypeDefaults::FractionType, self)),
209212
}
210213
}
211214

src/input/input_python.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,13 +338,26 @@ impl<'py> Input<'py> for Bound<'py, PyAny> {
338338
fn validate_fraction(&self, strict: bool, py: Python<'py>) -> ValMatch<Bound<'py, PyAny>> {
339339
let fraction_type = get_fraction_type(py);
340340

341-
// Fast path for existing decimal objects
341+
// Fast path for existing fraction objects
342342
if self.is_exact_instance(fraction_type) {
343343
return Ok(ValidationMatch::exact(self.to_owned().clone()));
344344
}
345345

346-
if !strict && self.is_instance_of::<PyString>() {
347-
return create_fraction(self, self).map(ValidationMatch::lax);
346+
// Check for fraction subclasses
347+
if self.is_instance(fraction_type)? {
348+
return Ok(ValidationMatch::lax(self.to_owned().clone()));
349+
}
350+
351+
if !strict {
352+
if self.is_instance_of::<PyString>() || (self.is_instance_of::<PyInt>() && !self.is_instance_of::<PyBool>())
353+
{
354+
// Checking isinstance for str / int / bool is fast compared to fraction / float
355+
return create_fraction(self, self).map(ValidationMatch::lax);
356+
}
357+
358+
if self.is_instance_of::<PyFloat>() {
359+
return create_fraction(self.str()?.as_any(), self).map(ValidationMatch::lax);
360+
}
348361
}
349362

350363
let error_type = if strict {

src/validators/decimal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ fn validate_as_decimal(
3737
) -> PyResult<Option<Py<PyAny>>> {
3838
match schema.get_item(key)? {
3939
Some(value) => match value.validate_decimal(false, py) {
40-
Ok(v) => Ok(Some(v.into_inner().unbind())),
40+
Ok(v) => { return Ok(Some(v.into_inner().unbind()))},
4141
Err(_) => Err(PyValueError::new_err(format!(
4242
"'{key}' must be coercible to a Decimal instance",
4343
))),

src/validators/fraction.rs

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use std::sync::Arc;
33
use pyo3::exceptions::{PyTypeError, PyValueError};
44
use pyo3::intern;
55
use pyo3::sync::PyOnceLock;
6-
use pyo3::types::{PyDict, PyString, PyType};
6+
use pyo3::types::{IntoPyDict, PyDict, PyString, PyType};
77
use pyo3::{prelude::*, PyTypeInfo};
88

99
use crate::build_tools::is_strict;
1010
use crate::errors::ErrorTypeDefaults;
1111
use crate::errors::ValResult;
12-
use crate::errors::{ToErrorValue, ValError};
12+
use crate::errors::{ToErrorValue, ValError, Number, ErrorType};
1313
use crate::input::Input;
1414

1515
use super::{BuildValidator, CombinedValidator, DefinitionsBuilder, ValidationState, Validator};
@@ -92,50 +92,50 @@ impl Validator for FractionValidator {
9292
// }
9393
// };
9494

95-
// if let Some(le) = &self.le {
96-
// if is_nan()? || !decimal.le(le)? {
97-
// return Err(ValError::new(
98-
// ErrorType::LessThanEqual {
99-
// le: Number::String(le.to_string()),
100-
// context: Some([("le", le)].into_py_dict(py)?.into()),
101-
// },
102-
// input,
103-
// ));
104-
// }
105-
// }
106-
// if let Some(lt) = &self.lt {
107-
// if is_nan()? || !decimal.lt(lt)? {
108-
// return Err(ValError::new(
109-
// ErrorType::LessThan {
110-
// lt: Number::String(lt.to_string()),
111-
// context: Some([("lt", lt)].into_py_dict(py)?.into()),
112-
// },
113-
// input,
114-
// ));
115-
// }
116-
// }
117-
// if let Some(ge) = &self.ge {
118-
// if is_nan()? || !decimal.ge(ge)? {
119-
// return Err(ValError::new(
120-
// ErrorType::GreaterThanEqual {
121-
// ge: Number::String(ge.to_string()),
122-
// context: Some([("ge", ge)].into_py_dict(py)?.into()),
123-
// },
124-
// input,
125-
// ));
126-
// }
127-
// }
128-
// if let Some(gt) = &self.gt {
129-
// if is_nan()? || !decimal.gt(gt)? {
130-
// return Err(ValError::new(
131-
// ErrorType::GreaterThan {
132-
// gt: Number::String(gt.to_string()),
133-
// context: Some([("gt", gt)].into_py_dict(py)?.into()),
134-
// },
135-
// input,
136-
// ));
137-
// }
138-
// }
95+
if let Some(le) = &self.le {
96+
if !fraction.le(le)? {
97+
return Err(ValError::new(
98+
ErrorType::LessThanEqual {
99+
le: Number::String(le.to_string()),
100+
context: Some([("le", le)].into_py_dict(py)?.into()),
101+
},
102+
input,
103+
));
104+
}
105+
}
106+
if let Some(lt) = &self.lt {
107+
if !fraction.lt(lt)? {
108+
return Err(ValError::new(
109+
ErrorType::LessThan {
110+
lt: Number::String(lt.to_string()),
111+
context: Some([("lt", lt)].into_py_dict(py)?.into()),
112+
},
113+
input,
114+
));
115+
}
116+
}
117+
if let Some(ge) = &self.ge {
118+
if !fraction.ge(ge)? {
119+
return Err(ValError::new(
120+
ErrorType::GreaterThanEqual {
121+
ge: Number::String(ge.to_string()),
122+
context: Some([("ge", ge)].into_py_dict(py)?.into()),
123+
},
124+
input,
125+
));
126+
}
127+
}
128+
if let Some(gt) = &self.gt {
129+
if !fraction.gt(gt)? {
130+
return Err(ValError::new(
131+
ErrorType::GreaterThan {
132+
gt: Number::String(gt.to_string()),
133+
context: Some([("gt", gt)].into_py_dict(py)?.into()),
134+
},
135+
input,
136+
));
137+
}
138+
}
139139

140140
Ok(fraction.into())
141141
}

0 commit comments

Comments
 (0)