Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/6237.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Emit parametrized type hints for standalone `PyList`, `PyDict`, and `PyTuple` so generated stubs pass `mypy --strict`.
14 changes: 7 additions & 7 deletions pytests/stubs/datetime.pyi
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions pytests/stubs/dict_iter.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
2 changes: 1 addition & 1 deletion pytests/stubs/misc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
8 changes: 4 additions & 4 deletions pytests/stubs/pyfunctions.pyi
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = ""
Expand Down
12 changes: 6 additions & 6 deletions src/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self::Output, Self::Error> {
Ok(self)
Expand All @@ -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<Self::Output, Self::Error> {
Ok(self.as_borrowed())
Expand All @@ -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<Self::Output, Self::Error> {
Ok(self)
Expand All @@ -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<Self::Output, Self::Error> {
Ok(*self)
Expand All @@ -170,7 +170,7 @@ impl<'py, T: PyTypeCheck> IntoPyObject<'py> for 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<Self::Output, Self::Error> {
Ok(self.into_bound(py))
Expand All @@ -183,7 +183,7 @@ impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &'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<Self::Output, Self::Error> {
Ok(self.bind_borrowed(py))
Expand Down
2 changes: 1 addition & 1 deletion src/impl_/extract_argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Self::Error> {
Expand All @@ -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<Self, Self::Error> {
Expand Down
94 changes: 94 additions & 0 deletions src/type_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` 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<Self>` 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;

Expand Down Expand Up @@ -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<Self>` 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)`.
Expand All @@ -131,6 +150,9 @@ where
#[cfg(feature = "experimental-inspect")]
const TYPE_HINT: PyStaticExpr = <T as PyTypeInfo>::TYPE_HINT;

#[cfg(feature = "experimental-inspect")]
const STANDALONE_TYPE_HINT: PyStaticExpr = <T as PyTypeInfo>::STANDALONE_TYPE_HINT;

#[inline]
fn type_check(object: &Bound<'_, PyAny>) -> bool {
T::is_type_of(object)
Expand All @@ -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!(
<PyList as PyTypeInfo>::STANDALONE_TYPE_HINT.to_string(),
"builtins.list[typing.Any]"
);
assert_eq!(
<PyDict as PyTypeInfo>::STANDALONE_TYPE_HINT.to_string(),
"builtins.dict[typing.Any, typing.Any]"
);
assert_eq!(
<PyTuple as PyTypeInfo>::STANDALONE_TYPE_HINT.to_string(),
"builtins.tuple[typing.Any, ...]"
);
}

#[test]
fn container_type_hints_stay_unparametrized_for_composition() {
assert_eq!(
<PyList as PyTypeInfo>::TYPE_HINT.to_string(),
"builtins.list"
);
assert_eq!(
<PyDict as PyTypeInfo>::TYPE_HINT.to_string(),
"builtins.dict"
);
assert_eq!(
<PyTuple as PyTypeInfo>::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!(
<Bound<'_, $ty> as FromPyObject<'_, '_>>::INPUT_TYPE.to_string(),
$expected
);
assert_eq!(
<Py<$ty> as FromPyObject<'_, '_>>::INPUT_TYPE.to_string(),
$expected
);
assert_eq!(
<Bound<'_, $ty> as IntoPyObject<'_>>::OUTPUT_TYPE.to_string(),
$expected
);
assert_eq!(
<Py<$ty> 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, ...]");
}
}
8 changes: 6 additions & 2 deletions src/types/dict.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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)]
Expand All @@ -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`.
Expand Down
8 changes: 6 additions & 2 deletions src/types/list.rs
Original file line number Diff line number Diff line change
@@ -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::*;
Expand Down Expand Up @@ -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)]
Expand All @@ -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)]
Expand Down
22 changes: 13 additions & 9 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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.
Expand All @@ -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)]
Expand Down Expand Up @@ -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)*);
};
}

Expand Down Expand Up @@ -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)*);
Expand Down
Loading