diff --git a/newsfragments/6237.fixed.md b/newsfragments/6237.fixed.md new file mode 100644 index 00000000000..3898a39cb48 --- /dev/null +++ b/newsfragments/6237.fixed.md @@ -0,0 +1 @@ +Emit parametrized type hints for standalone `PyList`, `PyDict`, and `PyTuple` so generated stubs pass `mypy --strict`. diff --git a/pytests/stubs/datetime.pyi b/pytests/stubs/datetime.pyi index 312fd11f683..c20f07840d0 100644 --- a/pytests/stubs/datetime.pyi +++ b/pytests/stubs/datetime.pyi @@ -1,5 +1,5 @@ from datetime import date, datetime, time, timedelta, tzinfo -from typing import final +from typing import Any, final @final class TzClass(tzinfo): @@ -10,13 +10,13 @@ class TzClass(tzinfo): def date_from_timestamp(timestamp: float) -> date: ... def datetime_from_timestamp(ts: float, tz: tzinfo | None = None) -> datetime: ... -def get_date_tuple(d: date) -> tuple: ... -def get_datetime_tuple(dt: datetime) -> tuple: ... -def get_datetime_tuple_fold(dt: datetime) -> tuple: ... +def get_date_tuple(d: date) -> tuple[Any, ...]: ... +def get_datetime_tuple(dt: datetime) -> tuple[Any, ...]: ... +def get_datetime_tuple_fold(dt: datetime) -> tuple[Any, ...]: ... def get_datetime_tzinfo(dt: datetime) -> tzinfo | None: ... -def get_delta_tuple(delta: timedelta) -> tuple: ... -def get_time_tuple(dt: time) -> tuple: ... -def get_time_tuple_fold(dt: time) -> tuple: ... +def get_delta_tuple(delta: timedelta) -> tuple[Any, ...]: ... +def get_time_tuple(dt: time) -> tuple[Any, ...]: ... +def get_time_tuple_fold(dt: time) -> tuple[Any, ...]: ... def get_time_tzinfo(dt: time) -> tzinfo | None: ... def make_date(year: int, month: int, day: int) -> date: ... def make_datetime( diff --git a/pytests/stubs/dict_iter.pyi b/pytests/stubs/dict_iter.pyi index 92aee6e79ad..27bdd6786a0 100644 --- a/pytests/stubs/dict_iter.pyi +++ b/pytests/stubs/dict_iter.pyi @@ -1,6 +1,6 @@ -from typing import final +from typing import Any, final @final class DictSize: def __new__(cls, /, expected: int) -> DictSize: ... - def iter_dict(self, /, dict: dict) -> int: ... + def iter_dict(self, /, dict: dict[Any, Any]) -> int: ... diff --git a/pytests/stubs/misc.pyi b/pytests/stubs/misc.pyi index 797c6a74520..c78ca21a15b 100644 --- a/pytests/stubs/misc.pyi +++ b/pytests/stubs/misc.pyi @@ -2,7 +2,7 @@ from typing import Any def accepts_bool(val: bool) -> bool: ... def detach_during_finalization() -> Any: ... -def get_item_and_run_callback(dict: dict, callback: Any) -> None: ... +def get_item_and_run_callback(dict: dict[Any, Any], callback: Any) -> None: ... def get_type_fully_qualified_name(obj: Any) -> str: ... def hammer_attaching_in_thread() -> Any: ... def issue_219() -> None: ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 322f2642339..57c212d7571 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,6 +1,6 @@ from typing import Any -def args_kwargs(*args, **kwargs) -> tuple[tuple, dict | None]: ... +def args_kwargs(*args, **kwargs) -> tuple[tuple[Any, ...], dict[Any, Any] | None]: ... def many_keyword_arguments( *, ant: Any | None = None, @@ -27,13 +27,13 @@ def simple( ) -> tuple[Any, Any | None, Any | None]: ... def simple_args( a: Any, b: Any | None = None, *args, c: Any | None = None -) -> tuple[Any, Any | None, tuple, Any | None]: ... +) -> tuple[Any, Any | None, tuple[Any, ...], Any | None]: ... def simple_args_kwargs( a: Any, b: Any | None = None, *args, c: Any | None = None, **kwargs -) -> tuple[Any, Any | None, tuple, Any | None, dict | None]: ... +) -> tuple[Any, Any | None, tuple[Any, ...], Any | None, dict[Any, Any] | None]: ... def simple_kwargs( a: Any, b: Any | None = None, c: Any | None = None, **kwargs -) -> tuple[Any, Any | None, Any | None, dict | None]: ... +) -> tuple[Any, Any | None, Any | None, dict[Any, Any] | None]: ... async def with_async() -> None: ... def with_typed_args( a: bool = False, b: int = 0, c: float = 0.0, d: str = "" diff --git a/src/conversion.rs b/src/conversion.rs index 212c6c0ef29..39651855268 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -118,7 +118,7 @@ impl<'py, T: PyTypeCheck> IntoPyObject<'py> for Bound<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self) @@ -131,7 +131,7 @@ impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &'a Bound<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.as_borrowed()) @@ -144,7 +144,7 @@ impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for Borrowed<'a, 'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self) @@ -157,7 +157,7 @@ impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &Borrowed<'a, 'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(*self) @@ -170,7 +170,7 @@ impl<'py, T: PyTypeCheck> IntoPyObject<'py> for Py { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, py: Python<'py>) -> Result { Ok(self.into_bound(py)) @@ -183,7 +183,7 @@ impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &'a Py { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const OUTPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; fn into_pyobject(self, py: Python<'py>) -> Result { Ok(self.bind_borrowed(py)) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 361a427bb56..a9460c1e339 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -102,7 +102,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const INPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; #[inline] fn extract( diff --git a/src/instance.rs b/src/instance.rs index b57e5dd07ee..f56218b2906 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2315,7 +2315,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const INPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { @@ -2330,7 +2330,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: PyStaticExpr = T::TYPE_HINT; + const INPUT_TYPE: PyStaticExpr = T::STANDALONE_TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { diff --git a/src/type_object.rs b/src/type_object.rs index 017c9c2028f..1b7785033ae 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -60,9 +60,23 @@ pub unsafe trait PyTypeInfo: Sized { const MODULE: Option<&'static str>; /// Provides the full python type as a type hint. + /// + /// This is also used as a building block for parametrized type hints, e.g. `Vec` is + /// hinted as `Self::TYPE_HINT[T::OUTPUT_TYPE]`, so it must stay unparametrized (e.g. `list` + /// rather than `list[Any]`). #[cfg(feature = "experimental-inspect")] const TYPE_HINT: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete"); + /// The type hint used for `Bound<'_, Self>` and `Py` when no more specific + /// parametrization is known. + /// + /// Defaults to [`TYPE_HINT`](Self::TYPE_HINT). Generic native container types like + /// [`PyList`](crate::types::PyList) override this to the fully parametrized form (e.g. + /// `list[Any]`) so that generated stubs pass `mypy --strict`, while keeping `TYPE_HINT` + /// itself unparametrized for reuse as described above. + #[cfg(feature = "experimental-inspect")] + const STANDALONE_TYPE_HINT: PyStaticExpr = Self::TYPE_HINT; + /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -113,6 +127,11 @@ pub unsafe trait PyTypeCheck { #[cfg(feature = "experimental-inspect")] const TYPE_HINT: PyStaticExpr; + /// The type hint used for `Bound<'_, Self>` and `Py` when no more specific + /// parametrization is known. Defaults to [`TYPE_HINT`](Self::TYPE_HINT). + #[cfg(feature = "experimental-inspect")] + const STANDALONE_TYPE_HINT: PyStaticExpr = Self::TYPE_HINT; + /// Checks if `object` is an instance of `Self`, which may include a subtype. /// /// This should be equivalent to the Python expression `isinstance(object, Self)`. @@ -131,6 +150,9 @@ where #[cfg(feature = "experimental-inspect")] const TYPE_HINT: PyStaticExpr = ::TYPE_HINT; + #[cfg(feature = "experimental-inspect")] + const STANDALONE_TYPE_HINT: PyStaticExpr = ::STANDALONE_TYPE_HINT; + #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { T::is_type_of(object) @@ -141,3 +163,75 @@ where T::type_object(py).into_any() } } + +#[cfg(all(test, feature = "experimental-inspect"))] +mod tests { + use super::*; + use crate::platform::prelude::*; + use crate::types::{PyDict, PyList, PyTuple}; + use crate::{Bound, FromPyObject, IntoPyObject, Py}; + + #[test] + fn container_standalone_type_hints_are_parametrized() { + assert_eq!( + ::STANDALONE_TYPE_HINT.to_string(), + "builtins.list[typing.Any]" + ); + assert_eq!( + ::STANDALONE_TYPE_HINT.to_string(), + "builtins.dict[typing.Any, typing.Any]" + ); + assert_eq!( + ::STANDALONE_TYPE_HINT.to_string(), + "builtins.tuple[typing.Any, ...]" + ); + } + + #[test] + fn container_type_hints_stay_unparametrized_for_composition() { + assert_eq!( + ::TYPE_HINT.to_string(), + "builtins.list" + ); + assert_eq!( + ::TYPE_HINT.to_string(), + "builtins.dict" + ); + assert_eq!( + ::TYPE_HINT.to_string(), + "builtins.tuple" + ); + } + + #[test] + fn container_bound_and_py_use_standalone_type_hints() { + macro_rules! assert_container { + ($ty:ty, $expected:expr) => { + assert_eq!( + <$ty as PyTypeInfo>::STANDALONE_TYPE_HINT.to_string(), + $expected + ); + assert_eq!( + as FromPyObject<'_, '_>>::INPUT_TYPE.to_string(), + $expected + ); + assert_eq!( + as FromPyObject<'_, '_>>::INPUT_TYPE.to_string(), + $expected + ); + assert_eq!( + as IntoPyObject<'_>>::OUTPUT_TYPE.to_string(), + $expected + ); + assert_eq!( + as IntoPyObject<'_>>::OUTPUT_TYPE.to_string(), + $expected + ); + }; + } + + assert_container!(PyList, "builtins.list[typing.Any]"); + assert_container!(PyDict, "builtins.dict[typing.Any, typing.Any]"); + assert_container!(PyTuple, "builtins.tuple[typing.Any, ...]"); + } +} diff --git a/src/types/dict.rs b/src/types/dict.rs index 4df1a99132a..33a89ea6c32 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -1,6 +1,8 @@ use crate::err::{self, PyErr, PyResult}; use crate::ffi::Py_ssize_t; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::type_hint_subscript; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::{PyAny, PyList, PyMapping}; @@ -32,7 +34,8 @@ pyobject_native_type!( pyobject_native_static_type_object!(ffi::PyDict_Type), "builtins", "dict", - #checkfunction=ffi::PyDict_Check + #checkfunction=ffi::PyDict_Check, + #standalone_type_hint=type_hint_subscript!(PyDict::TYPE_HINT, PyAny::TYPE_HINT, PyAny::TYPE_HINT) ); #[cfg(RustPython)] @@ -44,7 +47,8 @@ pyobject_native_type_core!( }, "builtins", "dict", - #checkfunction=ffi::PyDict_Check + #checkfunction=ffi::PyDict_Check, + #standalone_type_hint=type_hint_subscript!(PyDict::TYPE_HINT, PyAny::TYPE_HINT, PyAny::TYPE_HINT) ); /// Represents a Python `dict_keys`. diff --git a/src/types/list.rs b/src/types/list.rs index 65a7c8c6c82..fbee8f4783e 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,6 +1,8 @@ use crate::err::{self, PyResult}; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::type_hint_subscript; use crate::internal_tricks::get_ssize_index; #[allow(unused_imports, reason = "used to build docs")] use crate::platform::prelude::*; @@ -32,7 +34,8 @@ pyobject_native_type_core!( PyList, pyobject_native_static_type_object!(ffi::PyList_Type), "builtins", "list", - #checkfunction=ffi::PyList_Check + #checkfunction=ffi::PyList_Check, + #standalone_type_hint=type_hint_subscript!(PyList::TYPE_HINT, PyAny::TYPE_HINT) ); #[cfg(RustPython)] @@ -44,7 +47,8 @@ pyobject_native_type_core!( }, "builtins", "list", - #checkfunction=ffi::PyList_Check + #checkfunction=ffi::PyList_Check, + #standalone_type_hint=type_hint_subscript!(PyList::TYPE_HINT, PyAny::TYPE_HINT) ); #[cfg(Py_3_12)] diff --git a/src/types/mod.rs b/src/types/mod.rs index 970dc80fa8e..7c1cc94994b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -145,7 +145,7 @@ macro_rules! pyobject_native_static_type_object( #[doc(hidden)] #[macro_export] macro_rules! pyobject_type_info_type_hint( - ($module:expr, $name:expr) => {}; + ($module:expr, $name:expr $(, #standalone_type_hint=$standalone_type_hint:expr)?) => {}; ); #[cfg(feature = "experimental-inspect")] @@ -155,6 +155,10 @@ macro_rules! pyobject_type_info_type_hint( ($module:expr, $name:expr) => { const TYPE_HINT: $crate::inspect::PyStaticExpr = $crate::type_hint_identifier!($module, $name); }; + ($module:expr, $name:expr, #standalone_type_hint=$standalone_type_hint:expr) => { + const TYPE_HINT: $crate::inspect::PyStaticExpr = $crate::type_hint_identifier!($module, $name); + const STANDALONE_TYPE_HINT: $crate::inspect::PyStaticExpr = $standalone_type_hint; + }; ); /// Implements the `PyTypeInfo` trait for a native Python type. @@ -167,12 +171,12 @@ macro_rules! pyobject_type_info_type_hint( #[doc(hidden)] #[macro_export] macro_rules! pyobject_native_type_info( - ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr, $module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { + ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr, $module:expr $(, #checkfunction=$checkfunction:path)? $(, #standalone_type_hint=$standalone_type_hint:expr)? $(;$generics:ident)*) => { // SAFETY: macro caller has upheld the safety contracts unsafe impl<$($generics,)*> $crate::type_object::PyTypeInfo for $name { const NAME: &'static str = stringify!($name); const MODULE: ::core::option::Option<&'static str> = $module; - $crate::pyobject_type_info_type_hint!($type_hint_module, $type_hint_name); + $crate::pyobject_type_info_type_hint!($type_hint_module, $type_hint_name $(, #standalone_type_hint=$standalone_type_hint)?); #[inline] #[allow(clippy::redundant_closure_call)] @@ -205,15 +209,15 @@ macro_rules! pyobject_native_type_info( #[doc(hidden)] #[macro_export] macro_rules! pyobject_native_type_core { - ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr, #module=$module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { + ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr, #module=$module:expr $(, #checkfunction=$checkfunction:path)? $(, #standalone_type_hint=$standalone_type_hint:expr)? $(;$generics:ident)*) => { $crate::pyobject_native_type_named!($name $(;$generics)*); - $crate::pyobject_native_type_info!($name, $typeobject, $type_hint_module, $type_hint_name, $module $(, #checkfunction=$checkfunction)? $(;$generics)*); + $crate::pyobject_native_type_info!($name, $typeobject, $type_hint_module, $type_hint_name, $module $(, #checkfunction=$checkfunction)? $(, #standalone_type_hint=$standalone_type_hint)? $(;$generics)*); }; ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr, #module=$module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { $crate::pyobject_native_type_core!($name, $typeobject, $type_hint_module, $type_hint_name, #module=$module $(, #checkfunction=$checkfunction)? $(;$generics)*); }; - ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { - $crate::pyobject_native_type_core!($name, $typeobject, $type_hint_module, $type_hint_name, #module=::core::option::Option::Some("builtins") $(, #checkfunction=$checkfunction)? $(;$generics)*); + ($name:ty, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr $(, #checkfunction=$checkfunction:path)? $(, #standalone_type_hint=$standalone_type_hint:expr)? $(;$generics:ident)*) => { + $crate::pyobject_native_type_core!($name, $typeobject, $type_hint_module, $type_hint_name, #module=::core::option::Option::Some("builtins") $(, #checkfunction=$checkfunction)? $(, #standalone_type_hint=$standalone_type_hint)? $(;$generics)*); }; } @@ -256,8 +260,8 @@ macro_rules! pyobject_native_type_sized { #[doc(hidden)] #[macro_export] macro_rules! pyobject_native_type { - ($name:ty, $layout:path, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr $(, #module=$module:expr)? $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { - $crate::pyobject_native_type_core!($name, $typeobject, $type_hint_module, $type_hint_name $(, #module=$module)? $(, #checkfunction=$checkfunction)? $(;$generics)*); + ($name:ty, $layout:path, $typeobject:expr, $type_hint_module:expr, $type_hint_name:expr $(, #module=$module:expr)? $(, #checkfunction=$checkfunction:path)? $(, #standalone_type_hint=$standalone_type_hint:expr)? $(;$generics:ident)*) => { + $crate::pyobject_native_type_core!($name, $typeobject, $type_hint_module, $type_hint_name $(, #module=$module)? $(, #checkfunction=$checkfunction)? $(, #standalone_type_hint=$standalone_type_hint)? $(;$generics)*); // To prevent inheriting native types with ABI3 #[cfg(not(Py_LIMITED_API))] $crate::pyobject_native_type_sized!($name, $layout $(;$generics)*); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index d81fb55650e..2ba4aa1ab08 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,7 +1,7 @@ use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] -use crate::inspect::{type_hint_subscript, PyStaticExpr}; +use crate::inspect::{type_hint_subscript, PyStaticConstant, PyStaticExpr}; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; #[allow(unused_imports, reason = "used to build docs")] @@ -92,7 +92,13 @@ fn try_new_from_iter<'py>( pub struct PyTuple(PyAny); #[cfg(not(RustPython))] -pyobject_native_type_core!(PyTuple, pyobject_native_static_type_object!(ffi::PyTuple_Type), "builtins", "tuple", #checkfunction=ffi::PyTuple_Check); +pyobject_native_type_core!( + PyTuple, + pyobject_native_static_type_object!(ffi::PyTuple_Type), + "builtins", "tuple", + #checkfunction=ffi::PyTuple_Check, + #standalone_type_hint=type_hint_subscript!(PyTuple::TYPE_HINT, PyAny::TYPE_HINT, PyStaticExpr::Constant { value: PyStaticConstant::Ellipsis }) +); #[cfg(RustPython)] pyobject_native_type_core!( @@ -103,7 +109,8 @@ pyobject_native_type_core!( }, "builtins", "tuple", - #checkfunction=ffi::PyTuple_Check + #checkfunction=ffi::PyTuple_Check, + #standalone_type_hint=type_hint_subscript!(PyTuple::TYPE_HINT, PyAny::TYPE_HINT, PyStaticExpr::Constant { value: PyStaticConstant::Ellipsis }) ); impl PyTuple {