add `bound` method variants for `PyTypeInfo`

This commit is contained in:
David Hewitt 2024-01-30 10:31:34 +00:00
parent 9bb001108b
commit 367eeaeeab
20 changed files with 144 additions and 75 deletions

View File

@ -388,7 +388,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
# fn main() -> PyResult<()> {
# Python::with_gil(|py| -> PyResult<()> {
# let globals = PyModule::import(py, "__main__")?.dict();
# globals.set_item("Number", Number::type_object(py))?;
# globals.set_item("Number", Number::type_object_bound(py))?;
#
# py.run(SCRIPT, Some(globals), None)?;
# Ok(())

View File

@ -210,6 +210,7 @@ To minimise breakage of code using the GIL-Refs API, the `Bound<T>` smart pointe
For example, the following APIs have gained updated variants:
- `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc.
- `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below)
- The `PyTypeInfo` trait has had new `_bound` methods added to accept / return `Bound<T>`.
Because the new `Bound<T>` API brings ownership out of the PyO3 framework and into user code, there are a few places where user code is expected to need to adjust while switching to the new API:
- Code will need to add the occasional `&` to borrow the new smart pointer as `&Bound<T>` to pass these types around (or use `.clone()` at the very small cost of increasing the Python reference count)
@ -245,6 +246,8 @@ impl<'py> FromPyObject<'py> for MyType {
The expectation is that in 0.22 `extract_bound` will have the default implementation removed and in 0.23 `extract` will be removed.
## from 0.19.* to 0.20
### Drop support for older technologies
@ -656,7 +659,7 @@ To migrate, update trait bounds and imports from `PyTypeObject` to `PyTypeInfo`.
Before:
```rust,compile_fail
```rust,ignore
use pyo3::Python;
use pyo3::type_object::PyTypeObject;
use pyo3::types::PyType;
@ -668,7 +671,7 @@ fn get_type_object<T: PyTypeObject>(py: Python<'_>) -> &PyType {
After
```rust
```rust,ignore
use pyo3::{Python, PyTypeInfo};
use pyo3::types::PyType;
@ -995,13 +998,13 @@ makes it possible to interact with Python exception objects.
The new types also have names starting with the "Py" prefix. For example, before:
```rust,compile_fail
```rust,ignore
let err: PyErr = TypeError::py_err("error message");
```
After:
```rust,compile_fail
```rust,ignore
# use pyo3::{PyErr, PyResult, Python, type_object::PyTypeObject};
# use pyo3::exceptions::{PyBaseException, PyTypeError};
# Python::with_gil(|py| -> PyResult<()> {

View File

@ -35,8 +35,7 @@
//! ```
use crate::exceptions::PyValueError;
use crate::sync::GILOnceCell;
use crate::types::any::PyAnyMethods;
use crate::types::PyType;
use crate::types::{any::PyAnyMethods, PyType};
use crate::{
intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject,
};
@ -51,7 +50,7 @@ impl ToPyObject for Tz {
.unwrap()
.call1((self.name(),))
.unwrap()
.into()
.unbind()
}
}

View File

@ -36,7 +36,7 @@ impl ToPyObject for Ipv4Addr {
.expect("failed to load ipaddress.IPv4Address")
.call1((u32::from_be_bytes(self.octets()),))
.expect("failed to construct ipaddress.IPv4Address")
.to_object(py)
.unbind()
}
}
@ -48,7 +48,7 @@ impl ToPyObject for Ipv6Addr {
.expect("failed to load ipaddress.IPv6Address")
.call1((u128::from_be_bytes(self.octets()),))
.expect("failed to construct ipaddress.IPv6Address")
.to_object(py)
.unbind()
}
}

View File

@ -159,7 +159,7 @@ impl PyErr {
{
PyErr::from_state(PyErrState::Lazy(Box::new(move |py| {
PyErrStateLazyFnOutput {
ptype: T::type_object(py).into(),
ptype: T::type_object_bound(py).into(),
pvalue: args.arguments(py),
}
})))
@ -540,7 +540,7 @@ impl PyErr {
where
T: PyTypeInfo,
{
self.is_instance(py, T::type_object(py))
self.is_instance(py, T::type_object_bound(py).as_gil_ref())
}
/// Writes the error back to the Python interpreter's global state.
@ -1077,18 +1077,24 @@ mod tests {
fn test_pyerr_matches() {
Python::with_gil(|py| {
let err = PyErr::new::<PyValueError, _>("foo");
assert!(err.matches(py, PyValueError::type_object(py)));
assert!(err.matches(py, PyValueError::type_object_bound(py)));
assert!(err.matches(
py,
(PyValueError::type_object(py), PyTypeError::type_object(py))
(
PyValueError::type_object_bound(py),
PyTypeError::type_object_bound(py)
)
));
assert!(!err.matches(py, PyTypeError::type_object(py)));
assert!(!err.matches(py, PyTypeError::type_object_bound(py)));
// String is not a valid exception class, so we should get a TypeError
let err: PyErr = PyErr::from_type(crate::types::PyString::type_object(py), "foo");
assert!(err.matches(py, PyTypeError::type_object(py)));
let err: PyErr = PyErr::from_type(
crate::types::PyString::type_object_bound(py).as_gil_ref(),
"foo",
);
assert!(err.matches(py, PyTypeError::type_object_bound(py)));
})
}

View File

@ -685,7 +685,7 @@ impl<'py> Python<'py> {
where
T: PyTypeInfo,
{
T::type_object(self)
T::type_object_bound(self).into_gil_ref()
}
/// Imports the Python module with the specified name.

View File

@ -207,7 +207,7 @@ use crate::{
type_object::get_tp_free,
PyTypeInfo,
};
use crate::{ffi, IntoPy, PyErr, PyNativeType, PyObject, PyResult, PyTypeCheck, Python};
use crate::{ffi, Bound, IntoPy, PyErr, PyNativeType, PyObject, PyResult, PyTypeCheck, Python};
use std::cell::UnsafeCell;
use std::fmt;
use std::mem::ManuallyDrop;
@ -553,7 +553,7 @@ where
{
const NAME: &'static str = <T as PyTypeCheck>::NAME;
fn type_check(object: &PyAny) -> bool {
fn type_check(object: &Bound<'_, PyAny>) -> bool {
<T as PyTypeCheck>::type_check(object)
}
}

View File

@ -1,5 +1,5 @@
//! Synchronization mechanisms based on the Python GIL.
use crate::{instance::Bound, types::PyString, types::PyType, Py, PyResult, PyVisit, Python};
use crate::{types::PyString, types::PyType, Bound, Py, PyResult, PyVisit, Python};
use std::cell::UnsafeCell;
/// Value with concurrent access protected by the GIL.
@ -196,9 +196,9 @@ impl GILOnceCell<Py<PyType>> {
py: Python<'py>,
module_name: &str,
attr_name: &str,
) -> PyResult<&'py PyType> {
) -> PyResult<&Bound<'py, PyType>> {
self.get_or_try_init(py, || py.import(module_name)?.getattr(attr_name)?.extract())
.map(|ty| ty.as_ref(py))
.map(|ty| ty.bind(py))
}
}

View File

@ -138,11 +138,11 @@ mod inner {
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
$crate::tests::common::CatchWarnings::enter($py, |w| {
$body;
let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+];
let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object_bound($py), $message)),+];
assert_eq!(w.len(), expected_warnings.len());
for (warning, (category, message)) in w.iter().zip(expected_warnings) {
assert!(warning.getattr("category").unwrap().is(category));
assert!(warning.getattr("category").unwrap().is(&category));
assert_eq!(
warning.getattr("message").unwrap().str().unwrap().to_string_lossy(),
message

View File

@ -1,7 +1,9 @@
//! Python type object information
use crate::ffi_ptr_ext::FfiPtrExt;
use crate::types::any::PyAnyMethods;
use crate::types::{PyAny, PyType};
use crate::{ffi, PyNativeType, Python};
use crate::{ffi, Bound, PyNativeType, Python};
/// `T: PyLayout<U>` represents that `T` is a concrete representation of `U` in the Python heap.
/// E.g., `PyCell` is a concrete representation of all `pyclass`es, and `ffi::PyObject`
@ -64,19 +66,71 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef {
/// Returns the safe abstraction over the type object.
#[inline]
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyTypeInfo::type_object` will be replaced by `PyTypeInfo::type_object_bound` in a future PyO3 version"
)
)]
fn type_object(py: Python<'_>) -> &PyType {
// This isn't implemented in terms of `type_object_bound` because this just borrowed the
// object, for legacy reasons.
unsafe { py.from_borrowed_ptr(Self::type_object_raw(py) as _) }
}
/// Returns the safe abstraction over the type object.
#[inline]
fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> {
// Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme
// edge case, but arbitrary Python code _could_ change the __class__ of an object and cause
// the type object to be freed.
//
// By making `Bound` we assume ownership which is then safe against races.
unsafe {
Self::type_object_raw(py)
.cast::<ffi::PyObject>()
.assume_borrowed_unchecked(py)
.to_owned()
.downcast_into_unchecked()
}
}
/// Checks if `object` is an instance of this type or a subclass of this type.
#[inline]
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyTypeInfo::is_type_of` will be replaced by `PyTypeInfo::is_type_of_bound` in a future PyO3 version"
)
)]
fn is_type_of(object: &PyAny) -> bool {
Self::is_type_of_bound(&object.as_borrowed())
}
/// Checks if `object` is an instance of this type or a subclass of this type.
#[inline]
fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 }
}
/// Checks if `object` is an instance of this type.
#[inline]
#[cfg_attr(
not(feature = "gil-refs"),
deprecated(
since = "0.21.0",
note = "`PyTypeInfo::is_exact_type_of` will be replaced by `PyTypeInfo::is_exact_type_of_bound` in a future PyO3 version"
)
)]
fn is_exact_type_of(object: &PyAny) -> bool {
Self::is_exact_type_of_bound(&object.as_borrowed())
}
/// Checks if `object` is an instance of this type.
#[inline]
fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) }
}
}
@ -89,7 +143,7 @@ pub trait PyTypeCheck: HasPyGilRef {
/// Checks if `object` is an instance of `Self`, which may include a subtype.
///
/// This should be equivalent to the Python expression `isinstance(object, Self)`.
fn type_check(object: &PyAny) -> bool;
fn type_check(object: &Bound<'_, PyAny>) -> bool;
}
impl<T> PyTypeCheck for T
@ -99,8 +153,8 @@ where
const NAME: &'static str = <T as PyTypeInfo>::NAME;
#[inline]
fn type_check(object: &PyAny) -> bool {
<T as PyTypeInfo>::is_type_of(object)
fn type_check(object: &Bound<'_, PyAny>) -> bool {
T::is_type_of_bound(object)
}
}

View File

@ -733,7 +733,7 @@ impl PyAny {
where
T: PyTypeCheck<AsRefTarget = T>,
{
if T::type_check(self) {
if T::type_check(&self.as_borrowed()) {
// Safety: type_check is responsible for ensuring that the type is correct
Ok(unsafe { self.downcast_unchecked() })
} else {
@ -776,7 +776,7 @@ impl PyAny {
where
T: PyTypeInfo<AsRefTarget = T>,
{
if T::is_exact_type_of(self) {
if T::is_exact_type_of_bound(&self.as_borrowed()) {
// Safety: type_check is responsible for ensuring that the type is correct
Ok(unsafe { self.downcast_unchecked() })
} else {
@ -2100,7 +2100,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
where
T: PyTypeCheck,
{
if T::type_check(self.as_gil_ref()) {
if T::type_check(self) {
// Safety: type_check is responsible for ensuring that the type is correct
Ok(unsafe { self.downcast_unchecked() })
} else {
@ -2113,7 +2113,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
where
T: PyTypeCheck,
{
if T::type_check(self.as_gil_ref()) {
if T::type_check(&self) {
// Safety: type_check is responsible for ensuring that the type is correct
Ok(unsafe { self.downcast_into_unchecked() })
} else {
@ -2218,12 +2218,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
#[inline]
fn is_instance_of<T: PyTypeInfo>(&self) -> bool {
T::is_type_of(self.as_gil_ref())
T::is_type_of_bound(self)
}
#[inline]
fn is_exact_instance_of<T: PyTypeInfo>(&self) -> bool {
T::is_exact_type_of(self.as_gil_ref())
T::is_exact_type_of_bound(self)
}
fn contains<V>(&self, value: V) -> PyResult<bool>

View File

@ -1,4 +1,7 @@
use crate::{ffi, ffi_ptr_ext::FfiPtrExt, Borrowed, PyAny, PyTypeInfo, Python};
use crate::{
ffi, ffi_ptr_ext::FfiPtrExt, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyTypeInfo,
Python,
};
/// Represents the Python `Ellipsis` object.
#[repr(transparent)]
@ -38,14 +41,14 @@ unsafe impl PyTypeInfo for PyEllipsis {
}
#[inline]
fn is_type_of(object: &PyAny) -> bool {
fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
// ellipsis is not usable as a base type
Self::is_exact_type_of(object)
Self::is_exact_type_of_bound(object)
}
#[inline]
fn is_exact_type_of(object: &PyAny) -> bool {
object.is(Self::get_bound(object.py()).as_ref())
fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
object.is(&**Self::get_bound(object.py()))
}
}
@ -68,7 +71,7 @@ mod tests {
Python::with_gil(|py| {
assert!(PyEllipsis::get_bound(py)
.get_type()
.is(PyEllipsis::type_object(py)));
.is(&PyEllipsis::type_object_bound(py)));
})
}

View File

@ -114,7 +114,7 @@ impl<'py> Borrowed<'_, 'py, PyIterator> {
impl PyTypeCheck for PyIterator {
const NAME: &'static str = "Iterator";
fn type_check(object: &PyAny) -> bool {
fn type_check(object: &Bound<'_, PyAny>) -> bool {
unsafe { ffi::PyIter_Check(object.as_ptr()) != 0 }
}
}

View File

@ -97,7 +97,7 @@ impl PyMapping {
/// library). This is equvalent to `collections.abc.Mapping.register(T)` in Python.
/// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`.
pub fn register<T: PyTypeInfo>(py: Python<'_>) -> PyResult<()> {
let ty = T::type_object(py);
let ty = T::type_object_bound(py);
get_mapping_abc(py)?.call_method1("register", (ty,))?;
Ok(())
}
@ -232,7 +232,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> {
}
}
fn get_mapping_abc(py: Python<'_>) -> PyResult<&PyType> {
fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
static MAPPING_ABC: GILOnceCell<Py<PyType>> = GILOnceCell::new();
MAPPING_ABC.get_or_try_init_type_ref(py, "collections.abc", "Mapping")
@ -242,10 +242,10 @@ impl PyTypeCheck for PyMapping {
const NAME: &'static str = "Mapping";
#[inline]
fn type_check(object: &PyAny) -> bool {
fn type_check(object: &Bound<'_, PyAny>) -> bool {
// Using `is_instance` for `collections.abc.Mapping` is slow, so provide
// optimized case dict as a well-known mapping
PyDict::is_type_of(object)
PyDict::is_type_of_bound(object)
|| get_mapping_abc(object.py())
.and_then(|abc| object.is_instance(abc))
.unwrap_or_else(|err| {
@ -263,7 +263,7 @@ impl<'v> crate::PyTryFrom<'v> for PyMapping {
fn try_from<V: Into<&'v PyAny>>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> {
let value = value.into();
if PyMapping::type_check(value) {
if PyMapping::type_check(&value.as_borrowed()) {
unsafe { return Ok(value.downcast_unchecked()) }
}

View File

@ -207,9 +207,9 @@ macro_rules! pyobject_native_type_info(
$(
#[inline]
fn is_type_of(ptr: &$crate::PyAny) -> bool {
fn is_type_of_bound(obj: &$crate::Bound<'_, $crate::PyAny>) -> bool {
#[allow(unused_unsafe)]
unsafe { $checkfunction(ptr.as_ptr()) > 0 }
unsafe { $checkfunction(obj.as_ptr()) > 0 }
}
)?
}

View File

@ -1,5 +1,8 @@
use crate::ffi_ptr_ext::FfiPtrExt;
use crate::{ffi, Borrowed, IntoPy, PyAny, PyObject, PyTypeInfo, Python, ToPyObject};
use crate::{
ffi, types::any::PyAnyMethods, Borrowed, Bound, IntoPy, PyAny, PyObject, PyTypeInfo, Python,
ToPyObject,
};
/// Represents the Python `None` object.
#[repr(transparent)]
@ -40,15 +43,14 @@ unsafe impl PyTypeInfo for PyNone {
}
#[inline]
fn is_type_of(object: &PyAny) -> bool {
fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
// NoneType is not usable as a base type
Self::is_exact_type_of(object)
Self::is_exact_type_of_bound(object)
}
#[inline]
fn is_exact_type_of(object: &PyAny) -> bool {
let none = Self::get_bound(object.py());
object.is(none.as_ref())
fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
object.is(&**Self::get_bound(object.py()))
}
}
@ -82,7 +84,9 @@ mod tests {
#[test]
fn test_none_type_object_consistent() {
Python::with_gil(|py| {
assert!(PyNone::get_bound(py).get_type().is(PyNone::type_object(py)));
assert!(PyNone::get_bound(py)
.get_type()
.is(&PyNone::type_object_bound(py)));
})
}

View File

@ -1,4 +1,7 @@
use crate::{ffi, ffi_ptr_ext::FfiPtrExt, Borrowed, PyAny, PyTypeInfo, Python};
use crate::{
ffi, ffi_ptr_ext::FfiPtrExt, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyTypeInfo,
Python,
};
/// Represents the Python `NotImplemented` object.
#[repr(transparent)]
@ -41,14 +44,14 @@ unsafe impl PyTypeInfo for PyNotImplemented {
}
#[inline]
fn is_type_of(object: &PyAny) -> bool {
fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
// NotImplementedType is not usable as a base type
Self::is_exact_type_of(object)
Self::is_exact_type_of_bound(object)
}
#[inline]
fn is_exact_type_of(object: &PyAny) -> bool {
object.is(Self::get_bound(object.py()).as_ref())
fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool {
object.is(&**Self::get_bound(object.py()))
}
}
@ -71,7 +74,7 @@ mod tests {
Python::with_gil(|py| {
assert!(PyNotImplemented::get_bound(py)
.get_type()
.is(PyNotImplemented::type_object(py)));
.is(&PyNotImplemented::type_object_bound(py)));
})
}

View File

@ -72,8 +72,7 @@ impl PySuper {
ty: &Bound<'py, PyType>,
obj: &Bound<'py, PyAny>,
) -> PyResult<Bound<'py, PySuper>> {
PySuper::type_object(ty.py())
.as_borrowed()
PySuper::type_object_bound(ty.py())
.call1((ty, obj))
.map(|any| {
// Safety: super() always returns instance of super

View File

@ -8,11 +8,9 @@ use crate::internal_tricks::get_ssize_index;
use crate::py_result_ext::PyResultExt;
use crate::sync::GILOnceCell;
use crate::type_object::PyTypeInfo;
use crate::types::{PyAny, PyList, PyString, PyTuple, PyType};
use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType};
use crate::{ffi, FromPyObject, Py, PyNativeType, PyTypeCheck, Python, ToPyObject};
use super::any::PyAnyMethods;
/// Represents a reference to a Python object supporting the sequence protocol.
#[repr(transparent)]
pub struct PySequence(PyAny);
@ -182,7 +180,7 @@ impl PySequence {
/// library). This is equvalent to `collections.abc.Sequence.register(T)` in Python.
/// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`.
pub fn register<T: PyTypeInfo>(py: Python<'_>) -> PyResult<()> {
let ty = T::type_object(py);
let ty = T::type_object_bound(py);
get_sequence_abc(py)?.call_method1("register", (ty,))?;
Ok(())
}
@ -517,7 +515,7 @@ where
Ok(v)
}
fn get_sequence_abc(py: Python<'_>) -> PyResult<&PyType> {
fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
static SEQUENCE_ABC: GILOnceCell<Py<PyType>> = GILOnceCell::new();
SEQUENCE_ABC.get_or_try_init_type_ref(py, "collections.abc", "Sequence")
@ -527,11 +525,11 @@ impl PyTypeCheck for PySequence {
const NAME: &'static str = "Sequence";
#[inline]
fn type_check(object: &PyAny) -> bool {
fn type_check(object: &Bound<'_, PyAny>) -> bool {
// Using `is_instance` for `collections.abc.Sequence` is slow, so provide
// optimized cases for list and tuples as common well-known sequences
PyList::is_type_of(object)
|| PyTuple::is_type_of(object)
PyList::is_type_of_bound(object)
|| PyTuple::is_type_of_bound(object)
|| get_sequence_abc(object.py())
.and_then(|abc| object.is_instance(abc))
.unwrap_or_else(|err| {
@ -549,7 +547,7 @@ impl<'v> crate::PyTryFrom<'v> for PySequence {
fn try_from<V: Into<&'v PyAny>>(value: V) -> Result<&'v PySequence, PyDowncastError<'v>> {
let value = value.into();
if PySequence::type_check(value) {
if PySequence::type_check(&value.as_borrowed()) {
unsafe { return Ok(value.downcast_unchecked::<PySequence>()) }
}

View File

@ -14,7 +14,7 @@ impl PyType {
/// Creates a new type object.
#[inline]
pub fn new<T: PyTypeInfo>(py: Python<'_>) -> &PyType {
T::type_object(py)
T::type_object_bound(py).into_gil_ref()
}
/// Retrieves the underlying FFI pointer associated with this Python object.
@ -104,7 +104,7 @@ impl PyType {
where
T: PyTypeInfo,
{
self.is_subclass(T::type_object(self.py()))
self.is_subclass(T::type_object_bound(self.py()).as_gil_ref())
}
}