From 955d03913f3659d6858c68c7d35201aa4e0e97d5 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 27 Jun 2025 14:04:38 +0900 Subject: [PATCH 1/3] 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 6b697d1025ce782b455539491f84655ca0494ae8 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 27 Jun 2025 16:48:17 +0900 Subject: [PATCH 2/3] Fix typing.py --- Lib/test/test_typing.py | 4 ---- compiler/codegen/src/compile.rs | 26 ++++++++++++++++++++++---- vm/src/frame.rs | 16 ++++++++++++---- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 72ecc037a8..aa3c13547f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4099,8 +4099,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: @@ -9601,8 +9599,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/compiler/codegen/src/compile.rs b/compiler/codegen/src/compile.rs index 2e7e5a4ad5..71b94cdb96 100644 --- a/compiler/codegen/src/compile.rs +++ b/compiler/codegen/src/compile.rs @@ -1042,14 +1042,32 @@ impl Compiler<'_> { ))); }; let name_string = name.id.to_string(); - if type_params.is_some() { - self.push_symbol_table(); - } - self.compile_expression(value)?; + + // For PEP 695 syntax, we need to compile type_params first + // so that they're available when compiling the value expression if let Some(type_params) = type_params { + self.push_symbol_table(); + + // Compile type params first to define T1, T2, etc. self.compile_type_params(type_params)?; + // Stack now has type_params tuple at top + + // Compile value expression (can now see T1, T2) + self.compile_expression(value)?; + // Stack: [type_params_tuple, value] + + // We need [value, type_params_tuple] for TypeAlias instruction + emit!(self, Instruction::Rotate2); + self.pop_symbol_table(); + } else { + // No type params - push value first, then None (not empty tuple) + self.compile_expression(value)?; + // Push None for type_params (matching CPython) + self.emit_load_const(ConstantData::None); } + + // Push name last self.emit_load_const(ConstantData::Str { value: name_string.clone().into(), }); diff --git a/vm/src/frame.rs b/vm/src/frame.rs index e32a9e0546..eca9cdf331 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -1266,10 +1266,18 @@ impl ExecutingFrame<'_> { } bytecode::Instruction::TypeAlias => { let name = self.pop_value(); - let type_params: PyTupleRef = self - .pop_value() - .downcast() - .map_err(|_| vm.new_type_error("Type params must be a tuple."))?; + let type_params_obj = self.pop_value(); + + // CPython allows None or tuple for type_params + let type_params: PyTupleRef = if vm.is_none(&type_params_obj) { + // If None, use empty tuple (matching CPython's behavior) + vm.ctx.empty_tuple.clone() + } else { + type_params_obj + .downcast() + .map_err(|_| vm.new_type_error("Type params must be a tuple."))? + }; + let value = self.pop_value(); let type_alias = typing::TypeAliasType::new(name, type_params, value); self.push_value(type_alias.into_ref(&vm.ctx).into()); From fd52673815390c520b732fd71efb814372c68386 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 27 Jun 2025 20:01:49 +0900 Subject: [PATCH 3/3] ParamSpec.__repr__ --- vm/src/stdlib/typing.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vm/src/stdlib/typing.rs b/vm/src/stdlib/typing.rs index 25a26015a4..e9cd1cf270 100644 --- a/vm/src/stdlib/typing.rs +++ b/vm/src/stdlib/typing.rs @@ -343,7 +343,7 @@ pub(crate) mod decl { infer_variance: bool, } - #[pyclass(flags(HAS_DICT), with(AsNumber, Constructor))] + #[pyclass(flags(HAS_DICT), with(AsNumber, Constructor, Representable))] impl ParamSpec { #[pymethod] fn __mro_entries__(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult { @@ -555,6 +555,14 @@ pub(crate) mod decl { } } + impl Representable for ParamSpec { + #[inline(always)] + fn repr_str(zelf: &crate::Py, vm: &VirtualMachine) -> PyResult { + let name = zelf.__name__().str(vm)?; + Ok(format!("~{}", name)) + } + } + pub(crate) fn make_paramspec(name: PyObjectRef) -> ParamSpec { ParamSpec { name,