diff --git a/newsfragments/5331.added.md b/newsfragments/5331.added.md new file mode 100644 index 00000000000..0c1377915d0 --- /dev/null +++ b/newsfragments/5331.added.md @@ -0,0 +1 @@ +Introspection: emit base classes. \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index eedbd625e5c..66e64e161f8 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -120,9 +120,13 @@ fn convert_members<'a>( chunks_by_parent, )?); } - Chunk::Class { name, id } => { - classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?) - } + Chunk::Class { name, id, bases } => classes.push(convert_class( + id, + name, + bases, + chunks_by_id, + chunks_by_parent, + )?), Chunk::Function { name, id: _, @@ -164,6 +168,7 @@ fn convert_members<'a>( fn convert_class( id: &str, name: &str, + bases: &[String], chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { @@ -182,6 +187,7 @@ fn convert_class( ); Ok(Class { name: name.into(), + bases: bases.into(), methods, attributes, }) @@ -404,6 +410,8 @@ enum Chunk { Class { id: String, name: String, + #[serde(default)] + bases: Vec, }, Function { #[serde(default)] diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 9f86bb7e303..b458ba38254 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -11,6 +11,7 @@ pub struct Module { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Class { pub name: String, + pub bases: Vec, pub methods: Vec, pub attributes: Vec, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index baad91dd6e2..fbf90193c22 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -100,7 +100,18 @@ fn module_stubs(module: &Module) -> String { } fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> String { - let mut buffer = format!("class {}:", class.name); + let mut buffer = format!("class {}", class.name); + if !class.bases.is_empty() { + buffer.push('('); + for (i, base) in class.bases.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + buffer.push_str(annotation_stub(base, modules_to_import)); + } + buffer.push(')'); + } + buffer.push(':'); if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); return buffer; diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 868bfe18fb5..ecb9df8bce9 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -20,7 +20,7 @@ use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; use syn::visit_mut::{visit_type_mut, VisitMut}; -use syn::{Attribute, Ident, ReturnType, Type, TypePath}; +use syn::{Attribute, Ident, Path, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -61,19 +61,23 @@ pub fn class_introspection_code( pyo3_crate_path: &PyO3CratePath, ident: &Ident, name: &str, + extends: Option<&Path>, ) -> TokenStream { - IntrospectionNode::Map( - [ - ("type", IntrospectionNode::String("class".into())), - ( - "id", - IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), - ), - ("name", IntrospectionNode::String(name.into())), - ] - .into(), - ) - .emit(pyo3_crate_path) + let mut desc = HashMap::from([ + ("type", IntrospectionNode::String("class".into())), + ( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ), + ("name", IntrospectionNode::String(name.into())), + ]); + if let Some(extends) = extends { + desc.insert( + "bases", + IntrospectionNode::List(vec![IntrospectionNode::BaseType(extends).into()]), + ); + } + IntrospectionNode::Map(desc).emit(pyo3_crate_path) } #[allow(clippy::too_many_arguments)] @@ -345,6 +349,7 @@ enum IntrospectionNode<'a> { IntrospectionId(Option>), InputType { rust_type: Type, nullable: bool }, OutputType { rust_type: Type, is_final: bool }, + BaseType(&'a Path), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -410,6 +415,13 @@ impl IntrospectionNode<'_> { } content.push_str("\""); } + Self::BaseType(path) => { + content.push_str("\""); + content.push_tokens( + quote! { <#path as #pyo3_crate_path::impl_::pyclass::PyClassBaseType>::BASE_NAME.as_bytes() }, + ); + content.push_str("\""); + } Self::Map(map) => { content.push_str("{"); for (i, (key, value)) in map.into_iter().enumerate() { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3ff700074dc..f336c277feb 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -399,7 +399,7 @@ impl FieldPyO3Options { } } -fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow<'a, syn::Ident> { +fn get_class_python_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> Cow<'a, Ident> { args.options .name .as_ref() @@ -2352,12 +2352,14 @@ impl<'a> PyClassImplsBuilder<'a> { }; let pyclass_base_type_impl = attr.options.subclass.map(|subclass| { + let name_with_module = get_class_python_module_and_name(cls, self.attr); quote_spanned! { subclass.span() => impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls { type LayoutAsBase = #pyo3_path::impl_::pycell::PyClassObject; type BaseNativeType = ::BaseNativeType; type Initializer = #pyo3_path::pyclass_init::PyClassInitializer; type PyClassMutability = ::PyClassMutability; + const BASE_NAME: &'static str = #name_with_module; } } }); @@ -2498,9 +2500,14 @@ impl<'a> PyClassImplsBuilder<'a> { #[cfg(feature = "experimental-inspect")] fn impl_introspection(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; - let name = get_class_python_name(self.cls, self.attr).to_string(); let ident = self.cls; - let static_introspection = class_introspection_code(pyo3_path, ident, &name); + let name = get_class_python_name(ident, self.attr).to_string(); + let static_introspection = class_introspection_code( + pyo3_path, + ident, + &name, + self.attr.options.extends.as_ref().map(|attr| &attr.value), + ); let introspection_id = introspection_id_const(); quote! { #static_introspection diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 0480847094e..8d808263cac 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -25,6 +25,7 @@ mod pyo3_pytests { #[pymodule_export] use { comparisons::comparisons, consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions, + subclassing::subclassing, }; // Inserting to sys.modules allows importing submodules nicely from Python @@ -43,7 +44,6 @@ mod pyo3_pytests { m.add_wrapped(wrap_pymodule!(othermod::othermod))?; m.add_wrapped(wrap_pymodule!(path::path))?; m.add_wrapped(wrap_pymodule!(sequence::sequence))?; - m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 0f00e74c19d..561dbc14ab7 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -1,6 +1,8 @@ //! Test for [#220](https://github.com/PyO3/pyo3/issues/220) use pyo3::prelude::*; +#[cfg(not(Py_LIMITED_API))] +use pyo3::types::PyDict; #[pyclass(subclass)] pub struct Subclassable {} @@ -17,8 +19,43 @@ impl Subclassable { } } +#[pyclass(extends = Subclassable)] +pub struct Subclass {} + +#[pymethods] +impl Subclass { + #[new] + fn new() -> (Self, Subclassable) { + (Subclass {}, Subclassable::new()) + } + + fn __str__(&self) -> &'static str { + "Subclass" + } +} + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends = PyDict)] +pub struct SubDict {} + +#[cfg(not(Py_LIMITED_API))] +#[pymethods] +impl SubDict { + #[new] + fn new() -> Self { + Self {} + } + + fn __str__(&self) -> &'static str { + "SubDict" + } +} + #[pymodule(gil_used = false)] -pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +pub mod subclassing { + #[cfg(not(Py_LIMITED_API))] + #[pymodule_export] + use super::SubDict; + #[pymodule_export] + use super::{Subclass, Subclassable}; } diff --git a/pytests/stubs/subclassing.pyi b/pytests/stubs/subclassing.pyi new file mode 100644 index 00000000000..0d6199535ca --- /dev/null +++ b/pytests/stubs/subclassing.pyi @@ -0,0 +1,11 @@ +class SubDict(dict): + def __new__(cls, /) -> None: ... + def __str__(self, /) -> str: ... + +class Subclass(Subclassable): + def __new__(cls, /) -> None: ... + def __str__(self, /) -> str: ... + +class Subclassable: + def __new__(cls, /) -> None: ... + def __str__(self, /) -> str: ... diff --git a/pytests/tests/test_subclassing.py b/pytests/tests/test_subclassing.py index 2cee283dda7..b7e7ec69b9a 100644 --- a/pytests/tests/test_subclassing.py +++ b/pytests/tests/test_subclassing.py @@ -1,4 +1,4 @@ -from pyo3_pytests.subclassing import Subclassable +from pyo3_pytests.subclassing import Subclassable, Subclass class SomeSubClass(Subclassable): @@ -6,7 +6,13 @@ def __str__(self): return "SomeSubclass" -def test_subclassing(): +def test_python_subclassing(): a = SomeSubClass() assert str(a) == "SomeSubclass" assert type(a) is SomeSubClass + + +def test_rust_subclassing(): + a = Subclass() + assert str(a) == "Subclass" + assert type(a) is Subclass diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 402028d39be..e4c804facd2 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -14,6 +14,9 @@ impl<'py> IntoPyObject<'py> for &str { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) @@ -30,6 +33,9 @@ impl<'py> IntoPyObject<'py> for &&str { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -46,6 +52,9 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -62,6 +71,9 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (&**self).into_pyobject(py) @@ -78,6 +90,9 @@ impl<'py> IntoPyObject<'py> for char { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + fn into_pyobject(self, py: Python<'py>) -> Result { let mut bytes = [0u8; 4]; Ok(PyString::new(py, self.encode_utf8(&mut bytes))) @@ -94,6 +109,9 @@ impl<'py> IntoPyObject<'py> for &char { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -110,6 +128,9 @@ impl<'py> IntoPyObject<'py> for String { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "str"; + fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) } @@ -125,6 +146,9 @@ impl<'py> IntoPyObject<'py> for &String { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) diff --git a/src/exceptions.rs b/src/exceptions.rs index d3b9f994dd5..728a18c630e 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -280,7 +280,7 @@ macro_rules! impl_native_exception ( $crate::impl_exception_boilerplate!($name); $crate::pyobject_native_type!($name, $layout, |_py| unsafe { $crate::ffi::$exc_name as *mut $crate::ffi::PyTypeObject } $(, #checkfunction=$checkfunction)?); - $crate::pyobject_subclassable_native_type!($name, $layout); + $crate::pyobject_subclassable_native_type!($name, $layout, stringify!($name)); ); ($name:ident, $exc_name:ident, $doc:expr) => ( impl_native_exception!($name, $exc_name, $doc, $crate::ffi::PyBaseExceptionObject); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1228c2ea758..bd16fdae95b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1139,6 +1139,8 @@ pub trait PyClassBaseType: Sized { type BaseNativeType; type Initializer: PyObjectInit; type PyClassMutability: PyClassMutability; + /// Fully qualified name of the base class including modules + const BASE_NAME: &'static str; } /// Implementation of tp_dealloc for pyclasses without gc diff --git a/src/types/any.rs b/src/types/any.rs index fb3d97b55ce..75d2a10fbc4 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -52,6 +52,7 @@ impl crate::impl_::pyclass::PyClassBaseType for PyAny { type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; + const BASE_NAME: &'static str = "object"; } /// This trait represents the Python APIs which are usable on all Python objects. diff --git a/src/types/complex.rs b/src/types/complex.rs index d2624875733..92f322b7ab0 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -21,7 +21,7 @@ use std::ffi::c_double; #[repr(transparent)] pub struct PyComplex(PyAny); -pyobject_subclassable_native_type!(PyComplex, ffi::PyComplexObject); +pyobject_subclassable_native_type!(PyComplex, ffi::PyComplexObject, "complex"); pyobject_native_type!( PyComplex, diff --git a/src/types/datetime.rs b/src/types/datetime.rs index d519d8a69d0..4fe783775fd 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -231,7 +231,7 @@ pyobject_native_type!( #checkfunction=PyDate_Check ); #[cfg(not(Py_LIMITED_API))] -pyobject_subclassable_native_type!(PyDate, crate::ffi::PyDateTime_Date); +pyobject_subclassable_native_type!(PyDate, crate::ffi::PyDateTime_Date, "datetime.date"); #[cfg(Py_LIMITED_API)] pyobject_native_type_named!(PyDate); @@ -332,7 +332,11 @@ pyobject_native_type!( #checkfunction=PyDateTime_Check ); #[cfg(not(Py_LIMITED_API))] -pyobject_subclassable_native_type!(PyDateTime, crate::ffi::PyDateTime_DateTime); +pyobject_subclassable_native_type!( + PyDateTime, + crate::ffi::PyDateTime_DateTime, + "datetime.datetime" +); #[cfg(Py_LIMITED_API)] pyobject_native_type_named!(PyDateTime); @@ -583,7 +587,7 @@ pyobject_native_type!( #checkfunction=PyTime_Check ); #[cfg(not(Py_LIMITED_API))] -pyobject_subclassable_native_type!(PyTime, crate::ffi::PyDateTime_Time); +pyobject_subclassable_native_type!(PyTime, crate::ffi::PyDateTime_Time, "datetime.time"); #[cfg(Py_LIMITED_API)] pyobject_native_type_named!(PyTime); @@ -769,7 +773,7 @@ pyobject_native_type!( #checkfunction=PyTZInfo_Check ); #[cfg(not(Py_LIMITED_API))] -pyobject_subclassable_native_type!(PyTzInfo, crate::ffi::PyObject); +pyobject_subclassable_native_type!(PyTzInfo, crate::ffi::PyObject, "datetime.tzinfo"); #[cfg(Py_LIMITED_API)] pyobject_native_type_named!(PyTzInfo); @@ -885,7 +889,7 @@ pyobject_native_type!( #checkfunction=PyDelta_Check ); #[cfg(not(Py_LIMITED_API))] -pyobject_subclassable_native_type!(PyDelta, crate::ffi::PyDateTime_Delta); +pyobject_subclassable_native_type!(PyDelta, crate::ffi::PyDateTime_Delta, "datetime.timedelta"); #[cfg(Py_LIMITED_API)] pyobject_native_type_named!(PyDelta); diff --git a/src/types/dict.rs b/src/types/dict.rs index 15d31e06f49..bade224b851 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -16,7 +16,7 @@ use crate::{ffi, BoundObject, IntoPyObject, IntoPyObjectExt, Python}; #[repr(transparent)] pub struct PyDict(PyAny); -pyobject_subclassable_native_type!(PyDict, crate::ffi::PyDictObject); +pyobject_subclassable_native_type!(PyDict, crate::ffi::PyDictObject, "dict"); pyobject_native_type!( PyDict, diff --git a/src/types/float.rs b/src/types/float.rs index 62a7ae40bcb..f4a696f0b17 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -23,7 +23,7 @@ use std::ffi::c_double; #[repr(transparent)] pub struct PyFloat(PyAny); -pyobject_subclassable_native_type!(PyFloat, crate::ffi::PyFloatObject); +pyobject_subclassable_native_type!(PyFloat, crate::ffi::PyFloatObject, "float"); pyobject_native_type!( PyFloat, diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 45787a13b63..e4c57c803fa 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -61,7 +61,7 @@ impl<'py> PyFrozenSetBuilder<'py> { pub struct PyFrozenSet(PyAny); #[cfg(not(any(PyPy, GraalPy)))] -pyobject_subclassable_native_type!(PyFrozenSet, crate::ffi::PySetObject); +pyobject_subclassable_native_type!(PyFrozenSet, crate::ffi::PySetObject, "frozenset"); #[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PyFrozenSet, diff --git a/src/types/mod.rs b/src/types/mod.rs index 666262d75f9..92de1f6bf51 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -184,15 +184,16 @@ macro_rules! pyobject_native_type_core { #[doc(hidden)] #[macro_export] macro_rules! pyobject_subclassable_native_type { - ($name:ty, $layout:path $(;$generics:ident)*) => { + ($name:ty, $layout:path, $python_name:expr $(;$generics:ident)*) => { #[cfg(not(Py_LIMITED_API))] impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { type LayoutAsBase = $crate::impl_::pycell::PyClassObjectBase<$layout>; type BaseNativeType = $name; type Initializer = $crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = $crate::pycell::impl_::ImmutableClass; + const BASE_NAME: &'static str = $python_name; } - } + }; } #[doc(hidden)] diff --git a/src/types/set.rs b/src/types/set.rs index cdc920b0c12..4edfcea153a 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -19,7 +19,7 @@ use std::ptr; pub struct PySet(PyAny); #[cfg(not(any(PyPy, GraalPy)))] -pyobject_subclassable_native_type!(PySet, crate::ffi::PySetObject); +pyobject_subclassable_native_type!(PySet, crate::ffi::PySetObject, "set"); #[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index f008b2cd252..e6482a3b693 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -16,7 +16,11 @@ use super::PyWeakrefMethods; pub struct PyWeakrefReference(PyAny); #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] -pyobject_subclassable_native_type!(PyWeakrefReference, crate::ffi::PyWeakReference); +pyobject_subclassable_native_type!( + PyWeakrefReference, + crate::ffi::PyWeakReference, + "weakref.ref" +); #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] pyobject_native_type!( diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c3b8570d6d3..923e0d93562 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -43,8 +43,12 @@ fn test_compile_errors() { #[cfg(not(feature = "uuid"))] t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); - // output changes with async feature - #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] + // output changes with async and inspection features + #[cfg(all( + Py_LIMITED_API, + feature = "experimental-async", + not(feature = "experimental-inspect") + ))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); #[cfg(not(feature = "experimental-async"))] t.compile_fail("tests/ui/invalid_async.rs"); @@ -80,7 +84,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/abi3_dict.rs"); #[cfg(not(feature = "experimental-inspect"))] t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); - #[cfg(all(not(Py_LIMITED_API), Py_3_11))] + #[cfg(all(not(Py_LIMITED_API), Py_3_11, not(feature = "experimental-inspect")))] t.compile_fail("tests/ui/invalid_base_class.rs"); #[cfg(any(not(Py_3_10), all(not(Py_3_14), Py_LIMITED_API)))] t.compile_fail("tests/ui/immutable_type.rs");