diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 44007f2501..606fa1be6e 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -3351,8 +3351,6 @@ def x(self): ... self.assertNotIsSubclass(C, Protocol) self.assertNotIsInstance(C(), Protocol) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_protocols_issubclass_non_callable(self): class C: x = 1 @@ -3412,8 +3410,6 @@ def __init__(self) -> None: ): issubclass(Eggs, Spam) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_no_weird_caching_with_issubclass_after_isinstance_2(self): @runtime_checkable class Spam(Protocol): @@ -3434,8 +3430,6 @@ class Eggs: ... ): issubclass(Eggs, Spam) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_no_weird_caching_with_issubclass_after_isinstance_3(self): @runtime_checkable class Spam(Protocol): @@ -4091,8 +4085,6 @@ class MyChain(typing.ChainMap[str, T]): ... self.assertIs(MyChain[int]().__class__, MyChain) self.assertEqual(MyChain[int]().__orig_class__, MyChain[int]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_all_repr_eq_any(self): objs = (getattr(typing, el) for el in typing.__all__) for obj in objs: @@ -9591,8 +9583,6 @@ def test_all(self): self.assertIn('SupportsBytes', a) self.assertIn('SupportsComplex', a) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_all_exported_names(self): # ensure all dynamically created objects are actualised for name in typing.__all__: diff --git a/Lib/typing.py b/Lib/typing.py index b64a6b6714..a7397356d6 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -220,6 +220,8 @@ def _should_unflatten_callable_args(typ, args): >>> P = ParamSpec('P') >>> collections.abc.Callable[[int, int], str].__args__ == (int, int, str) True + >>> collections.abc.Callable[P, str].__args__ == (P, str) + True As a result, if we need to reconstruct the Callable from its __args__, we need to unflatten it. @@ -263,6 +265,8 @@ def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): >>> P = ParamSpec('P') >>> T = TypeVar('T') + >>> _collect_type_parameters((T, Callable[P, T])) + (~T, ~P) """ # required type parameter cannot appear after parameter with default default_encountered = False @@ -1983,7 +1987,8 @@ def _allow_reckless_class_checks(depth=2): The abc and functools modules indiscriminately call isinstance() and issubclass() on the whole MRO of a user class, which may contain protocols. """ - return _caller(depth) in {'abc', 'functools', None} + # XXX: RUSTPYTHON; https://github.com/python/cpython/pull/136115 + return _caller(depth) in {'abc', '_py_abc', 'functools', None} _PROTO_ALLOWLIST = { @@ -2090,11 +2095,11 @@ def __subclasscheck__(cls, other): and cls.__dict__.get("__subclasshook__") is _proto_hook ): _type_check_issubclass_arg_1(other) - # non_method_attrs = sorted(cls.__non_callable_proto_members__) - # raise TypeError( - # "Protocols with non-method members don't support issubclass()." - # f" Non-method members: {str(non_method_attrs)[1:-1]}." - # ) + non_method_attrs = sorted(cls.__non_callable_proto_members__) + raise TypeError( + "Protocols with non-method members don't support issubclass()." + f" Non-method members: {str(non_method_attrs)[1:-1]}." + ) return _abc_subclasscheck(cls, other) def __instancecheck__(cls, instance): @@ -2526,6 +2531,18 @@ def get_origin(tp): This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar, Annotated, and others. Return None for unsupported types. + + Examples:: + + >>> P = ParamSpec('P') + >>> assert get_origin(Literal[42]) is Literal + >>> assert get_origin(int) is None + >>> assert get_origin(ClassVar[int]) is ClassVar + >>> assert get_origin(Generic) is Generic + >>> assert get_origin(Generic[T]) is Generic + >>> assert get_origin(Union[T, int]) is Union + >>> assert get_origin(List[Tuple[T, T]][int]) is list + >>> assert get_origin(P.args) is P """ if isinstance(tp, _AnnotatedAlias): return Annotated @@ -2548,6 +2565,10 @@ def get_args(tp): >>> T = TypeVar('T') >>> assert get_args(Dict[str, int]) == (str, int) + >>> assert get_args(int) == () + >>> assert get_args(Union[int, Union[T, int], str][int]) == (int, str) + >>> assert get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + >>> assert get_args(Callable[[], T][int]) == ([], int) """ if isinstance(tp, _AnnotatedAlias): return (tp.__origin__,) + tp.__metadata__ @@ -3225,6 +3246,18 @@ def TypedDict(typename, fields=_sentinel, /, *, total=True): associated with a value of a consistent type. This expectation is not checked at runtime. + Usage:: + + >>> class Point2D(TypedDict): + ... x: int + ... y: int + ... label: str + ... + >>> a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + >>> b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + >>> Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + True + The type info can be accessed via the Point2D.__annotations__ dict, and the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. TypedDict supports an additional equivalent form:: @@ -3680,44 +3713,43 @@ def decorator(cls_or_fn): return cls_or_fn return decorator -# TODO: RUSTPYTHON - -# type _Func = Callable[..., Any] - - -# def override[F: _Func](method: F, /) -> F: -# """Indicate that a method is intended to override a method in a base class. -# -# Usage:: -# -# class Base: -# def method(self) -> None: -# pass -# -# class Child(Base): -# @override -# def method(self) -> None: -# super().method() -# -# When this decorator is applied to a method, the type checker will -# validate that it overrides a method or attribute with the same name on a -# base class. This helps prevent bugs that may occur when a base class is -# changed without an equivalent change to a child class. -# -# There is no runtime checking of this property. The decorator attempts to -# set the ``__override__`` attribute to ``True`` on the decorated object to -# allow runtime introspection. -# -# See PEP 698 for details. -# """ -# try: -# method.__override__ = True -# except (AttributeError, TypeError): -# # Skip the attribute silently if it is not writable. -# # AttributeError happens if the object has __slots__ or a -# # read-only property, TypeError if it's a builtin class. -# pass -# return method + +type _Func = Callable[..., Any] + + +def override[F: _Func](method: F, /) -> F: + """Indicate that a method is intended to override a method in a base class. + + Usage:: + + class Base: + def method(self) -> None: + pass + + class Child(Base): + @override + def method(self) -> None: + super().method() + + When this decorator is applied to a method, the type checker will + validate that it overrides a method or attribute with the same name on a + base class. This helps prevent bugs that may occur when a base class is + changed without an equivalent change to a child class. + + There is no runtime checking of this property. The decorator attempts to + set the ``__override__`` attribute to ``True`` on the decorated object to + allow runtime introspection. + + See PEP 698 for details. + """ + try: + method.__override__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return method def is_protocol(tp: type, /) -> bool: @@ -3740,8 +3772,19 @@ def is_protocol(tp: type, /) -> bool: and tp != Protocol ) + def get_protocol_members(tp: type, /) -> frozenset[str]: """Return the set of members defined in a Protocol. + + Example:: + + >>> from typing import Protocol, get_protocol_members + >>> class P(Protocol): + ... def a(self) -> str: ... + ... b: int + >>> get_protocol_members(P) == frozenset({'a', 'b'}) + True + Raise a TypeError for arguments that are not Protocols. """ if not is_protocol(tp): diff --git a/vm/src/protocol/object.rs b/vm/src/protocol/object.rs index 90eb186732..da0abdf1ce 100644 --- a/vm/src/protocol/object.rs +++ b/vm/src/protocol/object.rs @@ -535,9 +535,14 @@ impl PyObject { derived.recursive_issubclass(cls, vm) } + // _PyObject_RealIsInstance + pub(crate) fn real_is_instance(&self, cls: &PyObject, vm: &VirtualMachine) -> PyResult { + self.object_isinstance(cls, vm) + } + /// Real isinstance check without going through __instancecheck__ /// This is equivalent to CPython's _PyObject_RealIsInstance/object_isinstance - pub fn real_is_instance(&self, cls: &PyObject, vm: &VirtualMachine) -> PyResult { + fn object_isinstance(&self, cls: &PyObject, vm: &VirtualMachine) -> PyResult { if let Ok(cls) = cls.try_to_ref::(vm) { // PyType_Check(cls) - cls is a type object let mut retval = self.class().is_subtype(cls); @@ -576,8 +581,12 @@ impl PyObject { /// Determines if `self` is an instance of `cls`, either directly, indirectly or virtually via /// the __instancecheck__ magic method. - // This is object_recursive_isinstance from CPython's Objects/abstract.c pub fn is_instance(&self, cls: &PyObject, vm: &VirtualMachine) -> PyResult { + self.object_recursive_isinstance(cls, vm) + } + + // This is object_recursive_isinstance from CPython's Objects/abstract.c + fn object_recursive_isinstance(&self, cls: &PyObject, vm: &VirtualMachine) -> PyResult { // PyObject_TypeCheck(inst, (PyTypeObject *)cls) // This is an exact check of the type if self.class().is(cls) { @@ -586,29 +595,28 @@ impl PyObject { // PyType_CheckExact(cls) optimization if cls.class().is(vm.ctx.types.type_type) { - // When cls is exactly a type (not a subclass), use real_is_instance + // When cls is exactly a type (not a subclass), use object_isinstance // to avoid going through __instancecheck__ (matches CPython behavior) - return self.real_is_instance(cls, vm); + return self.object_isinstance(cls, vm); } // Check for Union type (e.g., int | str) - CPython checks this before tuple - if cls.class().is(vm.ctx.types.union_type) { + let cls = if cls.class().is(vm.ctx.types.union_type) { // Match CPython's _Py_union_args which directly accesses the args field let union = cls .try_to_ref::(vm) .expect("checked by is"); - let tuple = union.args(); - for typ in tuple.iter() { - if vm.with_recursion("in __instancecheck__", || self.is_instance(typ, vm))? { - return Ok(true); - } - } - } + union.args().as_object() + } else { + cls + }; // Check if cls is a tuple - if let Ok(tuple) = cls.try_to_ref::(vm) { - for typ in tuple { - if vm.with_recursion("in __instancecheck__", || self.is_instance(typ, vm))? { + if let Some(tuple) = cls.downcast_ref::() { + for item in tuple { + if vm.with_recursion("in __instancecheck__", || { + self.object_recursive_isinstance(item, vm) + })? { return Ok(true); } } @@ -624,7 +632,7 @@ impl PyObject { } // Fall back to object_isinstance (without going through __instancecheck__ again) - self.real_is_instance(cls, vm) + self.object_isinstance(cls, vm) } pub fn hash(&self, vm: &VirtualMachine) -> PyResult { diff --git a/vm/src/stdlib/typing.rs b/vm/src/stdlib/typing.rs index 9f0764e81d..5bbae8ae9f 100644 --- a/vm/src/stdlib/typing.rs +++ b/vm/src/stdlib/typing.rs @@ -48,7 +48,10 @@ pub(crate) mod decl { #[pyfunction(name = "override")] pub(crate) fn r#override(func: PyObjectRef, vm: &VirtualMachine) -> PyResult { // Set __override__ attribute to True - func.set_attr("__override__", vm.ctx.true_value.clone(), vm)?; + // Skip the attribute silently if it is not writable. + // AttributeError happens if the object has __slots__ or a + // read-only property, TypeError if it's a builtin class. + let _ = func.set_attr("__override__", vm.ctx.true_value.clone(), vm); Ok(func) }