diff --git a/Cargo.lock b/Cargo.lock index 323594fad..00c838ef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mimalloc" version = "0.1.29" @@ -209,13 +218,14 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6302e85060011447471887705bb7838f14aba43fcb06957d823739a496b3dc" +checksum = "12f72538a0230791398a0986a6518ebd88abc3fded89007b506ed072acc831e1" dependencies = [ "cfg-if", "indoc", "libc", + "memoffset", "parking_lot", "pyo3-build-config", "pyo3-ffi", @@ -225,9 +235,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b65b546c35d8a3b1b2f0ddbac7c6a569d759f357f2b9df884f5d6b719152c8" +checksum = "fc4cf18c20f4f09995f3554e6bcf9b09bd5e4d6b67c562fdfaafa644526ba479" dependencies = [ "once_cell", "target-lexicon", @@ -235,9 +245,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c275a07127c1aca33031a563e384ffdd485aee34ef131116fcd58e3430d1742b" +checksum = "a41877f28d8ebd600b6aa21a17b40c3b0fc4dfe73a27b6e81ab3d895e401b0e9" dependencies = [ "libc", "pyo3-build-config", @@ -245,9 +255,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284fc4485bfbcc9850a6d661d627783f18d19c2ab55880b021671c4ba83e90f7" +checksum = "2e81c8d4bcc2f216dc1b665412df35e46d12ee8d3d046b381aad05f1fcf30547" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -257,9 +267,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bda0f58f73f5c5429693c96ed57f7abdb38fdfc28ae06da4101a257adb7faf" +checksum = "85752a767ee19399a78272cc2ab625cd7d373b2e112b4b13db28de71fa892784" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1ecbe03b3..6913df363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/pydantic/pydantic-core.git" readme = "README.md" [dependencies] -pyo3 = "0.16.5" +pyo3 = "0.17.1" regex = "1.5.5" strum = { version = "0.24", features = ["derive"] } strum_macros = "0.24" @@ -43,4 +43,4 @@ panic = "abort" [build-dependencies] version_check = "0.9.4" # used where logic has to be version/distrobution specific, e.g. pypy -pyo3-build-config = "0.16.5" +pyo3-build-config = "0.17.1" diff --git a/src/errors/kinds.rs b/src/errors/kinds.rs index 9bc556b5a..1a52e7b78 100644 --- a/src/errors/kinds.rs +++ b/src/errors/kinds.rs @@ -366,7 +366,7 @@ macro_rules! py_dict { ($py:ident, $($value:expr),* $(,)?) => {{ let dict = PyDict::new($py); $( - dict.set_item(stringify!($value), $value.into_py($py))?; + dict.set_item::<&str, Py>(stringify!($value), $value.into_py($py))?; )* Ok(Some(dict.into_py($py))) }}; diff --git a/src/input/_pyo3_dict.rs b/src/input/_pyo3_dict.rs deleted file mode 100644 index 3bc2db2a6..000000000 --- a/src/input/_pyo3_dict.rs +++ /dev/null @@ -1,39 +0,0 @@ -// TODO: remove this file once a new pyo3 version is released -// with https://github.com/PyO3/pyo3/pull/2358 - -use pyo3::{ffi, pyobject_native_type_core, PyAny}; - -/// Represents a Python `dict_keys`. -#[cfg(not(PyPy))] -#[repr(transparent)] -pub struct PyDictKeys(PyAny); - -#[cfg(not(PyPy))] -pyobject_native_type_core!( - PyDictKeys, - ffi::PyDictKeys_Type, - #checkfunction=ffi::PyDictKeys_Check -); - -/// Represents a Python `dict_values`. -#[cfg(not(PyPy))] -#[repr(transparent)] -pub struct PyDictValues(PyAny); - -#[cfg(not(PyPy))] -pyobject_native_type_core!( - PyDictValues, - ffi::PyDictValues_Type, - #checkfunction=ffi::PyDictValues_Check -); - -/// Represents a Python `dict_items`. -#[cfg(not(PyPy))] -pub struct PyDictItems(PyAny, PyAny); - -#[cfg(not(PyPy))] -pyobject_native_type_core!( - PyDictItems, - ffi::PyDictItems_Type, - #checkfunction=ffi::PyDictItems_Check -); diff --git a/src/input/datetime.rs b/src/input/datetime.rs index c8e639372..31f5ad175 100644 --- a/src/input/datetime.rs +++ b/src/input/datetime.rs @@ -219,15 +219,22 @@ impl<'a> EitherDateTime<'a> { pub fn try_into_py(self, py: Python<'a>) -> PyResult { let dt = match self { - Self::Raw(datetime) => { - let tz: Option = match datetime.offset { - Some(offset) => { - let tz_info = TzInfo::new(offset); - Some(Py::new(py, tz_info)?.to_object(py)) - } - None => None, - }; - PyDateTime::new( + Self::Raw(datetime) => match datetime.offset { + Some(offset) => { + let tz_info = TzInfo::new(offset); + PyDateTime::new( + py, + datetime.date.year as i32, + datetime.date.month, + datetime.date.day, + datetime.time.hour, + datetime.time.minute, + datetime.time.second, + datetime.time.microsecond, + Some(Py::new(py, tz_info)?.to_object(py).extract(py)?), + )? + } + None => PyDateTime::new( py, datetime.date.year as i32, datetime.date.month, @@ -236,9 +243,9 @@ impl<'a> EitherDateTime<'a> { datetime.time.minute, datetime.time.second, datetime.time.microsecond, - tz.as_ref(), - )? - } + None, + )?, + }, Self::Py(dt) => dt, }; Ok(dt.into_py(py)) diff --git a/src/input/input_python.rs b/src/input/input_python.rs index d1d7e2ed8..df38eb728 100644 --- a/src/input/input_python.rs +++ b/src/input/input_python.rs @@ -7,12 +7,12 @@ use pyo3::types::{ PyBool, PyByteArray, PyBytes, PyDate, PyDateTime, PyDelta, PyDict, PyFrozenSet, PyIterator, PyList, PyMapping, PySequence, PySet, PyString, PyTime, PyTuple, PyType, }; +#[cfg(not(PyPy))] +use pyo3::types::{PyDictItems, PyDictKeys, PyDictValues}; use pyo3::{intern, AsPyPointer}; use crate::errors::{py_err_string, ErrorKind, InputValue, LocItem, ValError, ValResult}; -#[cfg(not(PyPy))] -use super::_pyo3_dict::{PyDictItems, PyDictKeys, PyDictValues}; use super::datetime::{ bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, date_as_datetime, float_as_datetime, float_as_duration, float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime, diff --git a/src/input/mod.rs b/src/input/mod.rs index 31e1cca7b..61b539c67 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1,7 +1,5 @@ use pyo3::prelude::*; -#[cfg(not(PyPy))] -mod _pyo3_dict; mod datetime; mod input_abstract; mod input_json; diff --git a/src/lookup_key.rs b/src/lookup_key.rs index 03ce75f32..e3e48f348 100644 --- a/src/lookup_key.rs +++ b/src/lookup_key.rs @@ -126,13 +126,13 @@ impl LookupKey { pub fn py_get_attr<'data, 's>(&'s self, obj: &'data PyAny) -> PyResult> { match self { - LookupKey::Simple(key, py_key) => match py_get_attrs(obj, &py_key)? { + LookupKey::Simple(key, py_key) => match py_get_attrs(obj, py_key)? { Some(value) => Ok(Some((key, value))), None => Ok(None), }, - LookupKey::Choice(key1, key2, py_key1, py_key2) => match py_get_attrs(obj, &py_key1)? { + LookupKey::Choice(key1, key2, py_key1, py_key2) => match py_get_attrs(obj, py_key1)? { Some(value) => Ok(Some((key1, value))), - None => match py_get_attrs(obj, &py_key2)? { + None => match py_get_attrs(obj, py_key2)? { Some(value) => Ok(Some((key2, value))), None => Ok(None), }, @@ -302,11 +302,8 @@ impl PathItem { /// wrapper around `getattr` that returns `Ok(None)` for attribute errors, but returns other errors /// We dont check `try_from_attributes` because that check was performed on the top level object before we got here -fn py_get_attrs(obj: &PyAny, attr_name: N) -> PyResult> -where - N: ToPyObject, -{ - match obj.getattr(attr_name) { +fn py_get_attrs<'a, 'b>(obj: &'a PyAny, attr_name: &'b Py) -> PyResult> { + match obj.getattr(attr_name.extract::<&PyString>(obj.py())?) { Ok(attr) => Ok(Some(attr)), Err(err) => { if err.get_type(obj.py()).is_subclass_of::()? { diff --git a/src/validators/typed_dict.rs b/src/validators/typed_dict.rs index 7b3dc874a..f46953c5f 100644 --- a/src/validators/typed_dict.rs +++ b/src/validators/typed_dict.rs @@ -1,8 +1,12 @@ use std::borrow::Cow; +use pyo3::intern; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyFunction, PyList, PySet, PyString}; -use pyo3::{intern, PyTypeInfo}; +#[cfg(not(PyPy))] +use pyo3::types::PyFunction; +use pyo3::types::{PyDict, PyList, PySet, PyString}; +#[cfg(not(PyPy))] +use pyo3::PyTypeInfo; use ahash::AHashSet; @@ -500,23 +504,22 @@ impl<'a> Iterator for AttributesIterator<'a> { .to_string_lossy(); if !name_cow.as_ref().starts_with('_') { // getattr is most likely to fail due to an exception in a @property, skip - if let Ok(attr) = self.object.getattr(name) { + if let Ok(attr) = self.object.getattr(name_cow.as_ref()) { // we don't want bound methods to be included, is there a better way to check? // ref https://stackoverflow.com/a/18955425/949890 let is_bound = matches!(attr.hasattr(intern!(attr.py(), "__self__")), Ok(true)); // the PyFunction::is_type_of(attr) catches `staticmethod`, but also any other function, // I think that's better than including static methods in the yielded attributes, // if someone really wants fields, they can use an explicit field, or a function to modify input + #[cfg(not(PyPy))] if !is_bound && !PyFunction::is_type_of(attr) { - // MASSIVE HACK! PyFunction::is_type_of(attr) doesn't detect staticmethod on PyPy, - // is_instance_of:: crashes with a null pointer, hence this hack, see - // https://github.com/pydantic/pydantic-core/pull/161#discussion_r917257635 - #[cfg(PyPy)] - if attr.get_type().to_string() != "" { - return Some((name, attr)); - } - - #[cfg(not(PyPy))] + return Some((name, attr)); + } + // MASSIVE HACK! PyFunction doesn't exist for PyPy, + // is_instance_of:: crashes with a null pointer, hence this hack, see + // https://github.com/pydantic/pydantic-core/pull/161#discussion_r917257635 + #[cfg(PyPy)] + if !is_bound && attr.get_type().to_string() != "" { return Some((name, attr)); } } diff --git a/tests/validators/test_dict.py b/tests/validators/test_dict.py index 331e55d9f..9cccbb5f6 100644 --- a/tests/validators/test_dict.py +++ b/tests/validators/test_dict.py @@ -183,10 +183,10 @@ def __len__(self): 'loc': [], 'message': ( 'Unable to convert mapping to a dictionary, error: ' - 'ValueError: Expected tuple of length 2, but got tuple of length 1.' + 'ValueError: expected tuple of length 2, but got tuple of length 1' ), 'input_value': HasRepr(IsStr(regex='.+BadMapping object at.+')), - 'context': {'error': 'ValueError: Expected tuple of length 2, but got tuple of length 1.'}, + 'context': {'error': 'ValueError: expected tuple of length 2, but got tuple of length 1'}, } ]