diff --git a/extmod/modtyping.c b/extmod/modtyping.c index 5e131b3348793..c7ec3465d1356 100644 --- a/extmod/modtyping.c +++ b/extmod/modtyping.c @@ -25,12 +25,14 @@ */ #include "py/obj.h" +#include "py/runtime.h" #if MICROPY_PY_TYPING // Implement roughly the equivalent of the following minimal Python typing module, meant to support the // typing syntax at runtime but otherwise ignoring any functionality: // +// TYPE_CHECKING = False // class _AnyCall: // def __init__(*args, **kwargs): // pass @@ -59,6 +61,78 @@ typedef struct _mp_obj_any_call_t } mp_obj_any_call_t; static const mp_obj_type_t mp_type_any_call_t; +static const mp_obj_type_t mp_type_typing_alias; + +// Lightweight runtime representation for objects such as typing.List[int]. +// The alias keeps track of the original builtin type and the tuple of +// parameters so that __origin__ and __args__ can be queried at runtime. +typedef struct _mp_obj_typing_alias_t { + mp_obj_base_t base; + mp_obj_t origin; + mp_obj_t args; // tuple or MP_OBJ_NULL when not parametrised +} mp_obj_typing_alias_t; + +// Maps a qstr name to the builtin type that should back the alias. +typedef struct { + qstr name; + const mp_obj_type_t *type; +} typing_alias_spec_t; + +static mp_obj_t typing_alias_from_spec(const typing_alias_spec_t *spec_table, size_t spec_len, qstr attr); + +static mp_obj_t typing_alias_new(mp_obj_t origin, mp_obj_t args) { + mp_obj_typing_alias_t *self = mp_obj_malloc(mp_obj_typing_alias_t, &mp_type_typing_alias); + self->origin = origin; + self->args = args; + return MP_OBJ_FROM_PTR(self); +} + +static void typing_alias_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + // Only handle reads that we recognise: __origin__ and __args__. Anything + // else is delegated back to the VM where it will fall through to the + // generic AnyCall behaviour. + if (dest[0] != MP_OBJ_NULL) { + return; + } + + mp_obj_typing_alias_t *self = MP_OBJ_TO_PTR(self_in); + if (attr == MP_QSTR___origin__) { + dest[0] = self->origin; + } else if (attr == MP_QSTR___args__) { + dest[0] = self->args == MP_OBJ_NULL ? mp_const_empty_tuple : self->args; + } +} + +static mp_obj_t typing_alias_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) { + if (value != MP_OBJ_SENTINEL) { + mp_raise_TypeError(MP_ERROR_TEXT("typing alias does not support assignment")); + } + + mp_obj_typing_alias_t *self = MP_OBJ_TO_PTR(self_in); + mp_obj_t args_obj; + if (mp_obj_is_type(index_in, &mp_type_tuple)) { + args_obj = index_in; + } else { + mp_obj_t items[1] = { index_in }; + args_obj = mp_obj_new_tuple(1, items); + } + + return typing_alias_new(self->origin, args_obj); +} + +static mp_obj_t typing_alias_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_obj_typing_alias_t *self = MP_OBJ_TO_PTR(self_in); + return mp_call_function_n_kw(self->origin, n_args, n_kw, args); +} + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_typing_alias, + MP_QSTR_typing_alias, + MP_TYPE_FLAG_NONE, + attr, typing_alias_attr, + subscr, typing_alias_subscr, + call, typing_alias_call + ); // Can be used both for __new__ and __call__: the latter's prototype is @@ -114,10 +188,39 @@ static void any_call_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } } +// Only a small subset of typing.* names need concrete runtime behaviour. The +// table below lists those names together with the builtin type that should be +// wrapped in a typing alias. Everything else continues to use the extremely +// small AnyCall shim. +static const typing_alias_spec_t typing_container_specs[] = { + { MP_QSTR_type, &mp_type_type }, + { MP_QSTR_Type, &mp_type_type }, + { MP_QSTR_List, &mp_type_list }, + { MP_QSTR_Dict, &mp_type_dict }, + { MP_QSTR_Tuple, &mp_type_tuple }, + { MP_QSTR_Literal, &mp_type_any_call_t }, + #if MICROPY_PY_BUILTINS_SET + { MP_QSTR_Set, &mp_type_set }, + #endif + #if MICROPY_PY_BUILTINS_FROZENSET + { MP_QSTR_FrozenSet, &mp_type_frozenset }, + #endif +}; + void any_call_module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { // Only loading is supported. if (dest[0] == MP_OBJ_NULL) { - dest[0] = MP_OBJ_FROM_PTR(&mp_type_any_call_t); + // First see if this attribute corresponds to a container alias that + // needs a proper __getitem__ implementation. + mp_obj_t alias = typing_alias_from_spec(typing_container_specs, MP_ARRAY_SIZE(typing_container_specs), attr); + if (alias != MP_OBJ_NULL) { + dest[0] = alias; + } else { + // Otherwise fall back to returning the singleton AnyCall object, + // preserving the "typing ignores everything" behaviour used for + // the majority of names. + dest[0] = MP_OBJ_FROM_PTR(&mp_type_any_call_t); + } } } @@ -131,9 +234,21 @@ static MP_DEFINE_CONST_OBJ_TYPE( call, any_call_call ); +// Helper to look up a qstr in the alias specification table and lazily create +// the corresponding typing alias object when a match is found. +static mp_obj_t typing_alias_from_spec(const typing_alias_spec_t *spec_table, size_t spec_len, qstr attr) { + for (size_t i = 0; i < spec_len; ++i) { + if (spec_table[i].name == attr) { + mp_obj_t origin = MP_OBJ_FROM_PTR(spec_table[i].type); + return typing_alias_new(origin, MP_OBJ_NULL); + } + } + return MP_OBJ_NULL; +} static const mp_rom_map_elem_t mp_module_typing_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_typing) }, + { MP_ROM_QSTR(MP_QSTR_TYPE_CHECKING), MP_ROM_FALSE }, }; static MP_DEFINE_CONST_DICT(mp_module_typing_globals, mp_module_typing_globals_table); diff --git a/tests/extmod/typing_pep_0484.py b/tests/extmod/typing_pep_0484.py new file mode 100644 index 0000000000000..c4231a1d9c7d9 --- /dev/null +++ b/tests/extmod/typing_pep_0484.py @@ -0,0 +1,187 @@ +try: + from typing import TYPE_CHECKING +except ImportError: + print("SKIP") + raise SystemExit + +print("# Python 3.5") +print("### PEP 484") + +# https://peps.python.org/topic/typing/ +# https://peps.python.org/pep-0484/ + +# Currently excludes tests using `Generic[T]` due to MicroPython runtime limitations + + +print("Running PEP 484 example-based test") + + +print("Type Definition Syntax") + + +def greeting(name: str) -> str: + return "Hello " + name + + +print("greeting:", greeting("world")) + +from typing import List + +l: List[int] + + +print("Type aliases") + +Url = str + + +def retry(url: Url, retry_count: int) -> None: + print("retry", url, retry_count) + + +retry("http://example", 3) + + +print("Callable example") +from typing import Callable, TypeVar, Union + + +def feeder(get_next_item: Callable[[], str]) -> None: + try: + v = get_next_item() + print("feeder got", v) + except Exception as e: + print("feeder runtime exception:", e) + + +def get_const(): + return "x" + + +feeder(get_const) + + +print("TypeVar constrained example") +AnyStr = TypeVar("AnyStr", str, bytes) + + +def concat(x: AnyStr, y: AnyStr) -> AnyStr: + return x + y + + +print("concat:", concat("a", "b")) + +# Generic user-defined class +from typing import Generic, TypeVar + +T = TypeVar("T") +# FIXME: Crash - inheriting from typing.Generic[T] unsupported at runtime +# try: +# +# class LoggedVar(Generic[T]): +# pass +# +# def __init__(self, value: T, name: str) -> None: +# self.name = name +# self.value = value +# +# def set(self, new: T) -> None: +# self.value = new +# +# def get(self) -> T: +# return self.value +# +# except Exception as e: +# print("-[ ] FIXME: Difference - Generic[T] base class unsupported:", e) + + +# Union/Optional examples +def handle_employee(e: Union[str, None]) -> None: + print("handle_employee called with", e) + + +handle_employee("John") +handle_employee(None) + + +# Any example +def use_map(m: dict) -> None: + print("use_map keys:", list(m.keys())) + + +use_map({"a": 1}) + +# NewType example: at runtime NewType returns identity function +try: + from typing import NewType + + UserId = NewType("UserId", int) + v = UserId(5) + print("NewType UserId runtime:", v, type(v)) +except Exception as e: + print("-[ ] FIXME: Difference or Crash - NewType runtime issue:", e) + +print("TYPE_CHECKING guard") + +from typing import TYPE_CHECKING + +# TYPE_CHECKING guard +if TYPE_CHECKING: + # This block is for type checkers only + pass + print("typing.TYPE_CHECKING is True at runtime. ERROR") +else: + print("typing.TYPE_CHECKING is False at runtime as expected") + + +print("Forward reference example") + + +class Tree: + def __init__(self, left: "Tree" = None, right: "Tree" = None): # type: ignore + self.left = left + self.right = right + + +tr = Tree() +print("Tree forward refs OK") + +# NoReturn example +from typing import NoReturn + + +def stop() -> NoReturn: + raise RuntimeError("stop") + + +try: + stop() +except RuntimeError: + print("stop() raised RuntimeError as expected (NoReturn at runtime)") + +# Overload example (runtime @overload should not be called directly) +from typing import overload + + +@overload +def func(x: int) -> int: + ... + + +@overload +def func(x: str) -> str: + ... + + +def func(x): + return x + + +print("overload func for int:", func(1)) + +# Cast example: at runtime cast returns the value +from typing import cast + +print("cast runtime identity:", cast(str, 123)) + +print("-----") diff --git a/tests/extmod/typing_pep_0526.py b/tests/extmod/typing_pep_0526.py new file mode 100644 index 0000000000000..204dd685d8feb --- /dev/null +++ b/tests/extmod/typing_pep_0526.py @@ -0,0 +1,167 @@ +try: + from typing import TYPE_CHECKING +except Exception: + print("SKIP") + raise SystemExit + +print("# Python 3.6") +print("### PEP 526 - Syntax for variable annotations") + +# https://peps.python.org/pep-0526/ +# Currently excludes tests using `Generic[T]` due to MicroPython runtime limitations + + +print("Specification") + +my_var: int +my_var = 5 # Passes type check. +other_var: int = "a" # Flagged as error by type checker, # type: ignore +# but OK at runtime. + + +print("Global and local variable annotations") +from typing import List, Tuple, Optional + +some_number: int # variable without initial value +some_list: List[int] = [] # variable with initial value + + +sane_world: bool +if 2 + 2 == 4: + sane_world = True +else: + sane_world = False + +# Tuple packing with variable annotation syntax +t: Tuple[int, ...] = (1, 2, 3) +# or +t: Tuple[int, ...] = 1, 2, 3 # This only works in Python 3.8+ + +# Tuple unpacking with variable annotation syntax +header: str +kind: int +body: Optional[List[str]] + +a: int # type: ignore +try: + print(a) # raises NameError # type: ignore + +except NameError: + print("Expected NameError") + + +def f_1(): + a: int + try: + print(a) # raises UnboundLocalError # type: ignore + except UnboundLocalError: + print("Expected UnboundLocalError") + + +a: int # type: ignore +a: str # Static type checker may or may not warn about this. + + +print("Class and instance variable annotations") +from typing import ClassVar, Dict + + +class BasicStarship: + captain: str = "Picard" # instance variable with default + damage: int # instance variable without default + stats: ClassVar[Dict[str, int]] = {} # class variable + + +class Starship_1: + captain = "Picard" + stats = {} + + def __init__(self, damage, captain=None): + self.damage = damage + if captain: + self.captain = captain # Else keep the default + + def hit(self): + Starship.stats["hits"] = Starship.stats.get("hits", 0) + 1 + + +class Starship: + captain: str = "Picard" + damage: int + stats: ClassVar[Dict[str, int]] = {} + + def __init__(self, damage: int, captain: str = None): # type: ignore + self.damage = damage + if captain: + self.captain = captain # Else keep the default + + def hit(self): + Starship.stats["hits"] = Starship.stats.get("hits", 0) + 1 + + +enterprise_d = Starship(3000) +enterprise_d.stats = {} # Flagged as error by a type checker # type: ignore +Starship.stats = {} # This is OK + + +# FIXME: - cpy_diff - User Defined Generic Classes unsupported +# from typing import Generic, TypeVar +# T = TypeVar("T") +# class Box(Generic[T]): +# def __init__(self, content): +# self.content: T = content + + +print("Annotating expressions") + + +class Cls: + pass + + +c = Cls() +c.x: int = 0 # Annotates c.x with int. # type: ignore +c.y: int # Annotates c.y with int.# type: ignore + +d = {} +d["a"]: int = 0 # Annotates d['a'] with int.# type: ignore +d["b"]: int # Annotates d['b'] with int.# type: ignore + +(x): int # Annotates x with int, (x) treated as expression by compiler.# type: ignore +(y): int = 0 # Same situation here. + + +# print("Where annotations aren’t allowed") +# The Examples crash both CPython and MicroPython at runtime. + +print("Runtime Effects of Type Annotations") + + +def f(): + x: NonexistentName # No RUNTIME error. # type: ignore + + +# FIXME: cpy_diff - MicroPython does not raise NameError at runtime +# try: +# x: NonexistentName # Error! +# print("-[ ] FIXME: Expected NameError") +# except NameError: +# print("Expected NameError:") + +# try: + +# class X: +# var: NonexistentName # Error! +# except NameError: +# print("Expected NameError:") + + +# FIXME: cpy_diff - MicroPython does not provide the ``__annotations__`` dict at runtime +# print(__annotations__) +# __annotations__["s"] = str + + +alice: "well done" = "A+" # type: ignore +bob: "what a shame" = "F-" # type: ignore + +print("-----") diff --git a/tests/extmod/typing_pep_0544.py b/tests/extmod/typing_pep_0544.py new file mode 100644 index 0000000000000..6431ba8c53d2d --- /dev/null +++ b/tests/extmod/typing_pep_0544.py @@ -0,0 +1,419 @@ +try: + from typing import TYPE_CHECKING +except Exception: + print("SKIP") + raise SystemExit + +print("# Python 3.8") +print("### PEP 544 - Protocols: Structural subtyping (static duck typing)") + +# https://peps.python.org/topic/typing/ +# Currently excludes tests using `Generic[T]` due to MicroPython runtime limitations + +print("Defining a protocol") +# https://peps.python.org/pep-0544/#defining-a-protocol + +from typing import Protocol, Iterable + + +class SupportsClose(Protocol): + def close(self) -> None: + ... + + +class Resource: + ... + + def close(self) -> None: + self.file.close() # type: ignore + self.lock.release() # type: ignore + + +def close_all(things: Iterable[SupportsClose]) -> None: + for t in things: + t.close() + + +# FIXME: Difference or Crash - Resource requires file and lock attributes +# r = Resource() +# close_all([f, r]) # OK! +# try: +# close_all([1]) # Error: 'int' has no 'close' method +# except Exception: +# print("Expected: 'int' has no 'close' method") + +print("Protocol members") +# https://peps.python.org/pep-0544/#protocol-members + + +from typing import Protocol +from abc import abstractmethod + + +class Example(Protocol): + def first(self) -> int: # This is a protocol member + return 42 + + @abstractmethod + def second(self) -> int: # Method without a default implementation + raise NotImplementedError + + +# --------- + +from typing import Protocol, List + + +class Template(Protocol): + name: str # This is a protocol member + value: int = 0 # This one too (with default) + + def method(self) -> None: + self.temp: List[int] = [] # Error in type checker # type: ignore + + +class Concrete_1: + def __init__(self, name: str, value: int) -> None: + self.name = name + self.value = value + + def method(self) -> None: + return + + +var: Template = Concrete_1("value", 42) # OK + + +print("Explicitly declaring implementation") + + +class PColor(Protocol): + @abstractmethod + def draw(self) -> str: + ... + + def complex_method(self) -> int: + # some complex code here + ... + + +class NiceColor(PColor): + def draw(self) -> str: + return "deep blue" + + +class BadColor(PColor): + def draw(self) -> str: + return super().draw() # Error, no default implementation # type: ignore + + +class ImplicitColor: # Note no 'PColor' base here + def draw(self) -> str: + return "probably gray" + + def complex_method(self) -> int: + # class needs to implement this + ... + + +nice: NiceColor +another: ImplicitColor + + +def represent(c: PColor) -> None: + print(c.draw(), c.complex_method()) + + +# ----------------------------------- + +from typing import Protocol, Tuple + + +class RGB(Protocol): + rgb: Tuple[int, int, int] + + @abstractmethod + def intensity(self) -> int: + return 0 + + +class Point(RGB): + def __init__(self, red: int, green: int, blue: str) -> None: + self.rgb = red, green, blue # Error, 'blue' must be 'int' # type: ignore + + +print("Merging and extending protocols") +# https://peps.python.org/pep-0544/#merging-and-extending-protocols + +from typing import Sized, Protocol + +# FIXME: TypeError: multiple bases have instance lay-out conflict - CRASH +# Is this a MicroPython multiple inheritance limitation? +try: + + class SizedAndClosable_1(Sized, Protocol): + def close(self) -> None: + ... +except Exception as e: + print("-[ ] FIXME: Difference or Crash - multiple bases have instance lay-out conflict:", e) + + +class SupportsClose_2(Protocol): + def close(self) -> None: + ... + + +# FIXME: TypeError: multiple bases have instance lay-out conflict - CRASH +try: + + class SizedAndClosable_2(Sized, SupportsClose_2, Protocol): + pass +except Exception as e: + print("-[ ] FIXME: Difference or Crash - multiple bases have instance lay-out conflict:", e) + +print("Generic protocols") +# https://peps.python.org/pep-0544/#generic-protocols + +# FIXME: Micropython does not support User Defined Generic Classes +# TypeError: 'type' object isn't subscriptable +# from typing import TypeVar, Protocol, Iterator +# T = TypeVar("T") +# class Iterable(Protocol[T]): +# @abstractmethod +# def __iter__(self) -> Iterator[T]: ... + + +print("Recursive protocols") + +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class Traversable(Protocol): + def leaves(self) -> Iterable["Traversable"]: + ... + + +class SimpleTree: + def leaves(self) -> List["SimpleTree"]: + ... + + +root: Traversable = SimpleTree() # OK + + +# FIXME: CPY_DIFF : Micropython does not support User Defined Generic Classes +# TypeError: 'type' object isn't subscriptable +# class Tree(Generic[T]): +# def leaves(self) -> List["Tree[T]"]: ... +# +# def walk(graph: Traversable) -> None: +# ... +# tree: Tree[float] = Tree() +# tree: Tree = Tree() +# walk(tree) # OK, 'Tree[float]' is a subtype of 'Traversable' + + +print("Self-types in protocols") + +C = TypeVar("C", bound="Copyable") # type: ignore + + +class Copyable(Protocol): + def copy(self: C) -> C: + ... + + +class One: + def copy(self) -> "One": + ... + + +T = TypeVar("T", bound="Other") + + +class Other: + def copy(self: T) -> T: + ... + + +c: Copyable +c = One() # OK # type: ignore +c = Other() # Also OK # type: ignore + + +print("Callback protocols") + +from typing import Optional, List, Protocol + + +class Combiner(Protocol): + def __call__(self, *vals: bytes, maxlen: Optional[int] = None) -> List[bytes]: + ... + + +def good_cb(*vals: bytes, maxlen: Optional[int] = None) -> List[bytes]: + ... + + +def bad_cb(*vals: bytes, maxitems: Optional[int]) -> List[bytes]: + ... + + +comb: Combiner = good_cb # OK +comb = bad_cb # Static Typecheck Error! # type: ignore +# Argument 2 has incompatible type because of different name and kind in the callback + +print("Unions and intersections of protocols") + +from typing import Union, Optional, Protocol + + +class Exitable(Protocol): + def exit(self) -> int: + ... + + +class Quittable(Protocol): + def quit(self) -> Optional[int]: + ... + + +def finish(task: Union[Exitable, Quittable]) -> int: + ... + + +class DefaultJob: + ... + + def quit(self) -> int: + return 0 + + +finish(DefaultJob()) # OK + +# --------------- + +from typing import Iterable, Hashable + + +# # class HashableFloats(Iterable[float], Hashable, Protocol): +# FIXME: TypeError: multiple bases have instance lay-out conflict +# class HashableFloats(Iterable, Hashable, Protocol): +# pass +# def cached_func(args: HashableFloats) -> float: ... +# cached_func((1, 2, 3)) # OK, tuple is both hashable and iterable + + +print("Type[] and class objects vs protocols") +from typing import Type + + +class Proto(Protocol): + @abstractmethod + def meth(self) -> int: + ... + + +class Concrete: + def meth(self) -> int: + return 42 + + +def fun(cls: Type[Proto]) -> int: + return cls().meth() # OK + + +fun(Concrete) # OK + +# FIXME: Should Throw: Can't instantiate protocol with abstract methods - +# try: +# fun(Proto) # Error # type: ignore +# print("-[ ] FIXME: Should Throw: Can't instantiate protocol with abstract methods") +# except Exception: +# print("Expected: Can't instantiate protocol with abstract methods") + +# --------------- + +from typing import Any, Protocol + + +class ProtoA(Protocol): + def meth(self, x: int) -> int: + ... + + +class ProtoB(Protocol): + def meth(self, obj: Any, x: int) -> int: + ... + + +class C: + def meth(self, x: int) -> int: + ... + + +a: ProtoA = C # Type check error, signatures don't match! # type: ignore +b: ProtoB = C # OK # type: ignore + + +print("NewType() and type aliases") + +from typing import NewType, Protocol, Iterator + + +class Id(Protocol): + code: int + secrets: Iterator[bytes] + + +UserId = NewType("UserId", Id) # Error, can't provide distinct type # type: ignore + +# ------------------------- + +from typing import TypeVar, Reversible, Iterable, Sized + +# FIXME: cpy_diff : User Defined Generic Classes unsupported +# TypeError: 'type' object isn't subscriptable + +# T = TypeVar("T") +# class SizedIterable_3(Iterable[T], Sized, Protocol): +# pass +# CompatReversible = Union[Reversible[T], SizedIterable_3[T]] + +print("@runtime_checkable decorator and narrowing types by isinstance()") + + +from typing import runtime_checkable, Protocol + +# FIXME: cpy_diff : NotImplementedError: @runtime_checkable decorator unsupported +# @runtime_checkable +# class SupportsClose(Protocol): +# def close(self): ... + + +# assert isinstance(open(__file__), SupportsClose) + + +class Foo(Protocol): + @property + def c(self) -> int: + return 42 # Default value can be provided for property... + + +print("typing.Protocols") +# https://docs.python.org/3/library/typing.html#protocols + +from typing import ( + SupportsInt, + SupportsBytes, + SupportsFloat, + SupportsComplex, + SupportsRound, + SupportsAbs, + SupportsIndex, +) +# TODO: what are sensible tests for these protocols? + +print("-----") diff --git a/tests/extmod/typing_pep_0560.py b/tests/extmod/typing_pep_0560.py new file mode 100644 index 0000000000000..0eb1e07482206 --- /dev/null +++ b/tests/extmod/typing_pep_0560.py @@ -0,0 +1,64 @@ +try: + from typing import TYPE_CHECKING +except Exception: + print("SKIP") + raise SystemExit + +print("# Python 3.7") +print("### PEP 560 - Type Hinting Generics In Standard Collections") + +print("Specification") + + +print("__class_getitem__") + + +class MyList: + def __getitem__(self, index): + return index + 1 + + def __class_getitem__(cls, item): + return f"{cls.__name__}[{item.__name__}]" + + +class MyOtherList(MyList): + pass + + +assert MyList()[0] == 1 + +# FIXME: Difference or Crash - __class_getitem__ not supported +# tests/extmod/typing_pep_0560.py", line 29, in +# TypeError: 'type' object isn't subscriptable +# assert MyList[int] == "MyList[int]" +# assert MyOtherList()[0] == 1 +# assert MyOtherList[int] == "MyOtherList[int]" + + +# ------------------------- + + +class GenericAlias: + def __init__(self, origin, item): + self.origin = origin + self.item = item + + def __mro_entries__(self, bases): + return (self.origin,) + + +class NewList: + def __class_getitem__(cls, item): + return GenericAlias(cls, item) + + +# FIXME: Difference or Crash - __class_getitem__ not supported +# TypeError: 'type' object isn't subscriptable +# class Tokens(NewList[int]): ... + + +# Not sure these make sense to test +# assert Tokens.__bases__ == (NewList,) +# assert Tokens.__orig_bases__ == (NewList[int],) + +print("-----") diff --git a/tests/extmod/typing_pep_0586.py b/tests/extmod/typing_pep_0586.py new file mode 100644 index 0000000000000..9874ede1f3212 --- /dev/null +++ b/tests/extmod/typing_pep_0586.py @@ -0,0 +1,210 @@ +# FIXME: This may break the test if __future__ is not available +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except Exception: + print("SKIP") + raise SystemExit + +print("# Python 3.8") +print("### PEP 586") + +# https://peps.python.org/pep-0586 + +print("Legal parameters for Literal at type check time") +from typing import Literal, Optional + + +class Color: + RED = 1 + GREEN = 2 + BLUE = 3 + + +Literal[26] +Literal[0x1A] # Exactly equivalent to Literal[26] +Literal[-4] +Literal["hello world"] +Literal[b"hello world"] +Literal["hello world"] +Literal[True] +Literal[Color.RED] # Assuming Color is some enum +Literal[None] + +# ---------- + +# FIXME: TypeError: 'type' object isn't subscriptable +ReadOnlyMode = Literal["r", "r+"] +WriteAndTruncateMode = Literal["w", "w+", "wt", "w+t"] +WriteNoTruncateMode = Literal["r+", "r+t"] +AppendMode = Literal["a", "a+", "at", "a+t"] + +# AllModes = Literal[ReadOnlyMode, WriteAndTruncateMode, WriteNoTruncateMode, AppendMode] + +# ---------- +# FIXME: TypeError: 'type' object isn't subscriptable +Literal[Literal[Literal[1, 2, 3], "foo"], 5, None] +# Optional[Literal[1, 2, 3, "foo", 5]] + +print("Parameters at runtime") + + +def my_function(x: Literal[1, 2]) -> int: + return x * 3 + + +x: Literal[1, 2, 3] = 3 +y: Literal[my_function] = my_function # type: ignore + + +print("Using non-Literals in Literal contexts") + + +def expects_str(x: str) -> None: + ... + + +var: Literal["foo"] = "foo" + +# Legal: Literal["foo"] is a subtype of str +expects_str(var) + +# --------------------- + + +def expects_literal(x: Literal["foo"]) -> None: + ... + + +def runner(my_str: str) -> None: + expects_literal(my_str) # type: ignore + + +runner("foo") # type: ignore + +print("Intelligent indexing of structured data") +from typing import Tuple, List, Literal + +a: Literal[0] = 0 +b: Literal[5] = 5 + +some_tuple: Tuple[int, str, List[bool]] = (3, "abc", [True, False]) + +# FIXME: NameError: name 'reveal_type' isn't defined +# reveal_type(some_tuple[a]) # Revealed type is 'int' + +try: + some_tuple[b] # Error: 5 is not a valid index into the tuple # type: ignore +except Exception: + print("Expected: tuple index out of range") + +# ----------------- + + +class Test: + def __init__(self, param: int) -> None: + self.myfield = param + + def mymethod(self, val: int) -> str: + ... + + +a: Literal["myfield"] = "myfield" +b: Literal["mymethod"] = "mymethod" +c: Literal["blah"] = "blah" + +t = Test(24) +# reveal_type(getattr(t, a)) # Revealed type is 'int' +# reveal_type(getattr(t, b)) # Revealed type is 'Callable[[int], str]' + +try: + getattr(t, c) +except AttributeError: + print("Expected: No attribute named 'blah' in Test") + +print("Interactions with overloads") +from typing import overload, IO, Any, Union, Text + +# FIXME: TypeError: 'type' object isn't subscriptable +# _PathType = Union[str, bytes, int] +_PathType = str + + +@overload +def open( + path: _PathType, + mode: Literal["r", "w", "a", "x", "r+", "w+", "a+", "x+"], +) -> IO[Text]: + ... + + +@overload +def open( + path: _PathType, + mode: Literal["rb", "wb", "ab", "xb", "r+b", "w+b", "a+b", "x+b"], +) -> IO[bytes]: + ... + + +# Fallback overload for when the user isn't using literal types +@overload +def open(path: _PathType, mode: str) -> IO[Any]: + pass + + +print("Interactions with generics") +# Fixme: User Defined Generic Classes unsupported + +# from typing import Generic, TypeVar + +# A = TypeVar("A", bound=int) +# B = TypeVar("B", bound=int) +# C = TypeVar("C", bound=int) + + +# requires from __futures__ import annotations +# A simplified definition for Matrix[row, column] +# TypeError: 'type' object isn't subscriptable +# class Matrix(Generic[A, B]): +# class Matrix(Generic[A, B]): +# def __add__(self, other: Matrix[A, B]) -> Matrix[A, B]: ... +# def __matmul__(self, other: Matrix[B, C]) -> Matrix[A, C]: ... +# def transpose(self) -> Matrix[B, A]: ... + + +# foo: Matrix[Literal[2], Literal[3]] = Matrix() +# bar: Matrix[Literal[3], Literal[7]] = Matrix() + +# baz = foo @ bar +# reveal_type(baz) # Revealed type is 'Matrix[Literal[2], Literal[7]]' + + +print("Interactions with enums and exhaustiveness checks") + +# FIXME: enum module not standard in MicroPython +try: + from enum import Enum + + class Status(Enum): + SUCCESS = 0 + INVALID_DATA = 1 + FATAL_ERROR = 2 + + def parse_status(s: Union[str, Status]) -> None: + if s is Status.SUCCESS: + print("Success!") + elif s is Status.INVALID_DATA: + print("The given data is invalid because...") + elif s is Status.FATAL_ERROR: + print("Unexpected fatal error...") + else: + # 's' must be of type 'str' since all other options are exhausted + print("Got custom status: " + s) + +except ImportError: + # print("Skipped enum test, enum module not available") + pass + + +print("-----") diff --git a/tests/extmod/typing_pep_0589.py b/tests/extmod/typing_pep_0589.py new file mode 100644 index 0000000000000..79b58570aef6b --- /dev/null +++ b/tests/extmod/typing_pep_0589.py @@ -0,0 +1,242 @@ +try: + import typing +except ImportError: + print("SKIP") + raise SystemExit + +print("# Python 3.8") +print("### PEP 589") + +# https://peps.python.org/topic/typing/ +# https://peps.python.org/pep-0589/ +# https://typing.python.org/en/latest/spec/typeddict.html#typeddict + + +print("Class-based Syntax") + +from typing import TypedDict +from typing import NotRequired, ReadOnly, Annotated + + +class Movie(TypedDict): + name: str + year: int + + +class EmptyDict(TypedDict): + pass + + +# ------------------------------------------------------------------------ +print("Using TypedDict Types") + +movie: Movie = {"name": "Blade Runner", "year": 1982} + + +def record_movie(movie: Movie) -> None: + ... + + +record_movie({"name": "Blade Runner", "year": 1982}) + +movie: Movie +movie = {"name": "Blade Runner", "year": 1982} + +# ------------------------------------------------------------------------ +print("Totality and optional keys") + +try: + # FIXME cpy_diff: runtime typing does not accept 'total' argument + class MovieTotal(TypedDict, total=True): + name: str + year: int +except TypeError as e: + print( + "-[ ] FIXME: cpy_diff : `total` parameter not supported by runtime TypedDict implementation:", + e, + ) + +try: + + class MoviePartial(TypedDict, total=False): + name: str + year: int +except TypeError as e: + print( + "-[ ] FIXME: cpy_diff : `total` parameter not supported by runtime TypedDict implementation:", + e, + ) + + +mt: MovieTotal = {"name": "Alien", "year": 1979} # type : ignore +mp: MoviePartial = {"name": "Alien"} # year is optional # type : ignore + +assert mt["year"] == 1979 +assert "year" not in mp or isinstance(mp.get("year"), (int, type(None))) + +# ------------------------------------------------------------------------ +print("Inheritance and required/optional mix") + + +class Point2D(TypedDict): + x: int + y: int + + +try: + + class Point3D(Point2D, total=False): + z: int +except TypeError as e: + print( + "FIXME: cpy_diff : `total` parameter not supported by runtime TypedDict implementation for Point3D:", + e, + ) + + +p2: Point2D = {"x": 1, "y": 2} +p3: Point3D = {"x": 1, "y": 2} +assert p2["x"] == 1 +assert "z" not in p3 + +print("Runtime checks: TypedDict cannot be used with isinstance/class checks") +try: + if isinstance(movie, Movie): # type: ignore + pass + print("-[ ] FIXME: TypedDict class allowed in isinstance (unexpected)") +except TypeError: + print("TypedDict class not allowed for isinstance/class checks") + +print("Alternative functional syntax and constructors") + +MovieAlt = TypedDict("MovieAlt", {"name": str, "year": int}) +MovieAlt2 = TypedDict("MovieAlt2", {"name": str, "year": int}, total=False) + +m_alt: MovieAlt = {"name": "Blade Runner", "year": 1982} + +# FIXME: Difference or Crash - calling the functional TypedDict constructor with kwargs +try: + ma = MovieAlt(name="Blade Runner", year=1982) + print(type(ma)) # should be dict at runtime +except TypeError as e: + print( + "-[ ] FIXME: cpy_diff Functional TypedDict constructor call failed at runtime (expected):", + e, + ) + + +print("Inheritance examples") + +try: + + class BookBasedMovie(Movie): + based_on: str +except TypeError as e: + print( + "Inheritance from TypedDicts not supported by runtime implementation for BookBasedMovie:", + e, + ) + +# KNOWN limitation - no multiple inheritance in MicroPython +# class X(TypedDict): +# x: int +# class Y(TypedDict): +# y: str +# try: +# class XYZ(X, Y): +# z: bool +# xyz: XYZ = {"x": 1, "y": "a", "z": True} +# except TypeError as e: +# print("Multiple inheritance for TypedDicts not supported at runtime (XYZ):", e) + +print("Totality and mixing with Required/NotRequired") + + +class _MovieBase(TypedDict): + title: str + + +try: + + class MovieMix(_MovieBase, total=False): + year: int +except TypeError as e: + print( + "FIXME: cpy_diff - total parameter not supported by runtime TypedDict implementation for MovieMix:", + e, + ) + MovieMix = dict # fallback for runtime operations # type: ignore + + +# equivalent to marking year as NotRequired +class MovieMix2(_MovieBase): + year: NotRequired[int] + + +# Do not try to execute known runtime errors: +try: + m1: MovieMix = {} # type: ignore + m2: MovieMix = {"year": 2015} # type: ignore +except TypeError as e: + print("Assigning to MovieMix failed at runtime (expected for missing required fields):", e) + +print("Required/NotRequired with Annotated/ReadOnly examples") + +from typing import NotRequired, ReadOnly, Annotated + + +class Band(TypedDict): + name: str + members: ReadOnly[list[str]] + + +blur: Band = {"name": "blur", "members": []} +blur["name"] = "Blur" +# the following would be a type-checker error (but allowed at runtime): +blur["members"] = ["Daemon Albarn"] # type: ignore +blur["members"].append("Daemon Albarn") + +print("extra_items and closed examples") + +try: + + class MovieExtra(TypedDict, extra_items=int): + name: str + + # FIXME: Difference - constructor with kwargs + extra_ok: MovieExtra = {"name": "BR", "year": 1982} +except TypeError as e: + print("-[ ] FIXME: extra_items not supported by runtime typing implementation:", e) + +try: + + class MovieClosed(TypedDict, closed=True): + name: str + + try: + # FIXME: Difference or Crash - constructor with kwargs + MovieClosed( + name="No Country for Old Men", year=2007 + ) # Should be runtime error per ctor semantics + print("Constructed ClosedMovie with extra item (may be allowed at runtime)") + except TypeError: + print("-[ ] FIXME: Closed Movie rejected extra kwargs at construction") +except TypeError as e: + print("-[ ] FIXME: closed parameter not supported by runtime typing implementation:", e) + +print("Interaction with Mapping and dict conversions") + +try: + # FIXME: + class IntDict(TypedDict, extra_items=int): + pass + + not_required_num_dict: IntDict = {"num": 1, "bar": 2} +except TypeError as e: + print("-[ ] FIXME: extra_items not supported by runtime typing implementation", e) + # Fall back to plain dict to exercise runtime operations + not_required_num_dict = {"num": 1, "bar": 2} +# at runtime this is a dict; operations like clear/popitem are available +not_required_num_dict.clear() + +print("-----") diff --git a/tests/extmod/typing_pep_0591.py b/tests/extmod/typing_pep_0591.py new file mode 100644 index 0000000000000..7699dd932edf5 --- /dev/null +++ b/tests/extmod/typing_pep_0591.py @@ -0,0 +1,111 @@ +try: + from typing import TYPE_CHECKING +except Exception: + print("SKIP") + raise SystemExit + +print("# Python 3.8") +print("### PEP 0591 - Final qualifier for types") +# https://peps.python.org/pep-0591/ + +print("The final decorator") + +from typing import List, Sequence, final + + +@final +class Base_1: + ... + + +try: + + class Derived_1(Base_1): # Error: Cannot inherit from final class "Base" + ... +except Exception: + print("Expected: Cannot inherit from final class 'Base'") + + +# ----------------- + + +class Base_2: + @final + def foo(self) -> None: + ... + + +try: + + class Derived_2(Base_2): + def foo(self) -> None: # Error: Cannot override final attribute "foo" + # (previously declared in base class "Base") + ... +except Exception: + print("Expected: Cannot override final attribute 'foo'") + + +print("The Final annotation") + +from typing import Final + +ID_1: Final[float] = 1 +ID_2: Final = 2 + +print("Semantics and examples") + +from typing import Final + +RATE: Final = 3000 + + +class Base: + DEFAULT_ID: Final = 0 + + +try: + RATE = 300 # Error: can't assign to final attribute +except Exception: + print("Expected: can't assign to final attribute 'RATE'") + +try: + Base.DEFAULT_ID = 1 # Error: can't override a final attribute +except Exception: + print("Expected: can't override a final attribute 'DEFAULT_ID'") + +# ------------------- + +# FIXME: Difference - Final cannot be used with container types +try: + x: List[Final[int]] = [] # Error! + print("-[ ] FIXME: Final cannot be used with container types") +except Exception: + print("Expected: Final cannot be used with container types") + + +try: + + def fun(x: Final[List[int]]) -> None: # Error! + ... + +except Exception: + print("Expected: Final cannot be used for parameters or return types") + +# ------------------- + +x_2: Final = ["a", "b"] +x_2.append("c") # OK + +y: Final[Sequence[str]] = ["a", "b"] +z: Final = ("a", "b") # Also works + + +# ------------------- +from typing import NamedTuple, Final + +X: Final = "x" +Y: Final = "y" +N = NamedTuple("N", [(X, int), (Y, int)]) + + +print("-----") diff --git a/tests/extmod/typing_runtime.py b/tests/extmod/typing_runtime.py index 74980d3760a59..2b5b89cb8d531 100644 --- a/tests/extmod/typing_runtime.py +++ b/tests/extmod/typing_runtime.py @@ -1,18 +1,22 @@ print("Testing runtime aspects of typing module") + try: from typing import TYPE_CHECKING except ImportError: print("SKIP") raise SystemExit +print("# Python 3.5+") +print("### Miscellaneous") + -print("Testing : typing.TYPE_CHECKING - Python 3.5.2") +print("typing.TYPE_CHECKING") if TYPE_CHECKING: from typing_extensions import TypeGuard -print("Testing : typing parameter annotations") -from typing import Any, Dict, List +print("typing parameter annotations") +from typing import Any, Dict, List, Union def add_numbers(a: int, b: int) -> int: @@ -38,7 +42,7 @@ def process_data(data: Dict[str, Any]) -> None: process_data({"key": "value", "number": 42}) -print("Testing : typing.Self - Python 3.11") +print("typing.Self - Python 3.11") from typing import Callable, Self @@ -55,7 +59,7 @@ def cb(x): base.register(cb) -print("Testing : typing@no_type_check decorator") +print("typing@no_type_check decorator") from typing import no_type_check @@ -66,7 +70,7 @@ def quad(r0): print(quad(1)) -print("Testing : typing.Protocol") +print("typing.Protocol") from typing import Protocol @@ -93,7 +97,7 @@ def add(adder: Adder) -> None: add(IntAdder()) add(FloatAdder()) -print("Testing : typing.NewType") +print("typing.NewType") from typing import NewType @@ -105,7 +109,7 @@ def add(adder: Adder) -> None: assert isinstance(some_id, int), "NewType should be instance of the original type" -print("Testing : typing.Any") +print("typing.Any") from typing import Any a: Any = None @@ -138,7 +142,7 @@ def hash_b(item: Any) -> int: print(hash_b(42)) print(hash_b("foo")) -print("Testing : typing.AnyStr") +print("typing.AnyStr") from typing import AnyStr @@ -150,12 +154,12 @@ def concat(a: AnyStr, b: AnyStr) -> AnyStr: concat("foo", "bar") # OK, output has type 'str' concat(b"foo", b"bar") # OK, output has type 'bytes' try: - concat("foo", b"bar") # Error, cannot mix str and bytes + concat("foo", b"bar") # Error, cannot mix str and bytes # type: ignore except TypeError: print("TypeError is expected") -print("Testing : typing.LiteralString") +print("typing.LiteralString") from typing import LiteralString @@ -179,7 +183,7 @@ def caller(arbitrary_string: str, literal_string: LiteralString) -> None: caller(some_str, literal_str) -print("Testing : typing.overload") +print("typing.overload") from typing import overload @@ -201,9 +205,8 @@ def bar(x): print(bar(42)) -print("Testing : typing.Required, NotRequired in TypedDict") - -# Specification: https://typing.readthedocs.io/en/latest/spec/typeddict.html#required-and-notrequired +print("typing.Required, NotRequired in TypedDict") +# https://typing.readthedocs.io/en/latest/spec/typeddict.html#required-and-notrequired from typing import NotRequired, Required, TypedDict @@ -217,7 +220,7 @@ class Movie(TypedDict): m = Movie(title="Life of Brian", year=1979) -print("Testing : typing.TypeVar") +print("typing.TypeVar") from typing import List, TypeVar @@ -235,7 +238,7 @@ def first(container: List[T]) -> T: print(first(list_two)) -print("Testing : typing.Generator") +print("typing.Generator") from typing import Generator @@ -250,7 +253,7 @@ def echo(a: float) -> Generator[int, float, str]: print(v) -print("Testing : typing.NoReturn") +print("typing.NoReturn") from typing import NoReturn @@ -261,7 +264,7 @@ def stop() -> NoReturn: # -print("Testing : typing.Final") +print("typing.Final") from typing import Final @@ -271,7 +274,7 @@ def stop() -> NoReturn: print(CONST) -print("Testing : typing.final") +print("typing.final") from typing import final @@ -299,21 +302,25 @@ class Other(Leaf): # type: ignore # Error reported by type checker other = Other() -print("Testing : typing.TypeVarTuple and typing.Unpack") +print("typing.TypeVarTuple and typing.Unpack") from typing import TypeVarTuple, Unpack Ts = TypeVarTuple("Ts") -tup: tuple[Unpack[Ts]] # Semantically equivalent, and backwards-compatible +tup: tuple[Unpack[Ts]] # Semantically equivalent, and backwards-compatible # type: ignore -print("Testing : typing.Callable, ParamSpec") +print("typing.Callable, ParamSpec") # ParamSpec, 3.11 notation # https://docs.python.org/3/library/typing.html#typing.ParamSpec -# FIXME: from collections.abc import Callable -from typing import Callable +try: + from collections.abc import Callable +except ImportError: + print("- [ ] FIXME: from collections.abc import Callable") + +from typing import Callable # Workaround for test from typing import TypeVar, ParamSpec T = TypeVar("T") @@ -341,8 +348,109 @@ def add_two(x: float, y: float) -> float: assert x == 3, "add_two(1, 2) == 3" -print("Testing : typing.") -print("Testing : typing.") -print("Testing : typing.") -print("Testing : typing.") -print("Testing : typing.") +print("typing.get_origin()") +# https://docs.python.org/3/library/typing.html#typing.get_origin + +from typing import get_origin + +# FIXME: - cpy_diff - get_origin() unsupported, or always returns None +if not get_origin(str) is None: + print("- [ ] FIXME: cpy_diff - get_origin(str) should be None") +# assert get_origin(Dict[str, int]) is dict +# assert get_origin(Union[int, str]) is Union + +print("typing.get_args()") +# https://docs.python.org/3/library/typing.html#typing.get_args +from typing import get_args, Dict, Union + +# FIXME: - cpy_diff - get_args() unsupported, or always returns () +if not get_args(int) == (): + print("- [ ] FIXME: cpy_diff - get_args(int) should be ()") + +# assert get_args(Dict[int, str]) == (int, str), "get_args(Dict[int, str]) should be (int, str)" +# assert get_args(Union[int, str]) == (int, str), "get_args(Union[int, str]) should be (int, str)" + + +print("Subscriptables") + +from typing import ( + AbstractSet, + AsyncContextManager, + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, +) +from typing import ( + Callable, + ChainMap, + Collection, + Container, + ContextManager, + Coroutine, + Counter, + DefaultDict, +) +from typing import ( + Deque, + Dict, + FrozenSet, + Generator, + Generic, + Iterable, + Iterator, + List, + Literal, + Mapping, +) +from typing import ( + MutableMapping, + MutableSequence, + MutableSet, + NamedTuple, + Optional, + OrderedDict, + Self, +) +from typing import Sequence, Set, Tuple, Type, Union + + +t_01: AbstractSet[Any] +t_02: AsyncContextManager[Any] +t_03: AsyncGenerator[Any] +t_04: AsyncIterable[Any] +t_05: AsyncIterator[Any] +t_06: Awaitable[Any] +t_07: Callable[[], Any] +t_08: ChainMap[Any, Any] +t_09: Collection[Any] +t_10: Container[Any] +t_11: ContextManager[Any] +t_12: Coroutine[Any, Any, Any] +t_13: Counter[Any] +t_14: DefaultDict[Any, Any] +t_15: Deque[Any] +t_16: Dict[Any, Any] +t_17: FrozenSet[Any] +t_18: Generator[Any] +# t_19: Generic[Any] +t_20: Iterable[Any] +t_21: Iterator[Any] +t_22: List[Any] +t_23: Literal[1, 2, 3, "a", b"b", True, None] +t_24: Mapping[Any, Any] +t_25: MutableMapping[Any, Any] +t_26: MutableSequence[Any] +t_27: MutableSet[Any] +t_28: NamedTuple +t_29: Optional[Any] +t_30: OrderedDict[Any, Any] +# t_31: Self[Any] +t_32: Sequence[Any] +t_33: Set[Any] +t_34: Tuple[Any] +t_35: Type[Any] +t_36: Union[Any, Any] + + +print("-----")