From d5946d496d92e6e91090e8eac4971d4b513b82ae Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 27 Jun 2025 14:04:38 +0900 Subject: [PATCH 1/5] Update typing.py from CPython 3.13.5 --- Lib/typing.py | 128 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index b64a6b6714..fe939a2e68 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 @@ -2090,11 +2094,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 +2530,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 +2564,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 +3245,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 +3712,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 +3771,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): From 39d091f01c04f3c5668bd9dec34b1c012519ca3f Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 29 Jun 2025 04:11:04 +0900 Subject: [PATCH 2/5] Fix typing --- Lib/test/test_typing.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 44007f2501..ad5d4076de 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4091,8 +4091,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 +9589,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__: From 5a2007fd466028f13697c5a1d409bf627e85a675 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 30 Jun 2025 12:14:02 +0900 Subject: [PATCH 3/5] renames --- vm/src/protocol/object.rs | 40 +++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 { From c0b9694cda9b3bd90dda3cdd3747a42ebbe8d084 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 30 Jun 2025 23:46:54 +0900 Subject: [PATCH 4/5] Patch `typing.py` to let `_py_abc` fallback --- Lib/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index fe939a2e68..a7397356d6 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1987,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 = { From 3f5566da53a35592a1006c0c01075edc956787bd Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 26 Jun 2025 18:33:13 +0900 Subject: [PATCH 5/5] Fix _typing.override --- Lib/test/test_typing.py | 6 ------ vm/src/stdlib/typing.rs | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ad5d4076de..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): 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) }