diff --git a/extmod/extmod.cmake b/extmod/extmod.cmake index 98e8a84608a56..b8f2b36f324fd 100644 --- a/extmod/extmod.cmake +++ b/extmod/extmod.cmake @@ -43,6 +43,7 @@ set(MICROPY_SOURCE_EXTMOD ${MICROPY_EXTMOD_DIR}/modtls_axtls.c ${MICROPY_EXTMOD_DIR}/modtls_mbedtls.c ${MICROPY_EXTMOD_DIR}/modtime.c + ${MICROPY_EXTMOD_DIR}/modtyping.c ${MICROPY_EXTMOD_DIR}/modvfs.c ${MICROPY_EXTMOD_DIR}/modwebsocket.c ${MICROPY_EXTMOD_DIR}/modwebrepl.c diff --git a/extmod/extmod.mk b/extmod/extmod.mk index c132fd89ce887..f7acc0fe0251a 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -43,6 +43,7 @@ SRC_EXTMOD_C += \ extmod/modtls_axtls.c \ extmod/modtls_mbedtls.c \ extmod/modtime.c \ + extmod/modtyping.c \ extmod/moductypes.c \ extmod/modvfs.c \ extmod/modwebrepl.c \ diff --git a/extmod/modtyping.c b/extmod/modtyping.c new file mode 100644 index 0000000000000..c7ec3465d1356 --- /dev/null +++ b/extmod/modtyping.c @@ -0,0 +1,272 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2024 Damien P. George + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#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 +// +// def __call__(self, *args, **kwargs): +// return self +// +// def __getitem__(self, attr): +// return self +// +// _typing_obj = _AnyCall() +// +// def __getattr__(attr): +// return _typing_obj +// +// Note this works together with the micropython compiler itself ignoring type hints, i.e. when encountering +// +// def hello(name: str) -> None: +// pass +// +// both str and None hints are simply ignored. + +typedef struct _mp_obj_any_call_t +{ + mp_obj_base_t base; +} 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 +// (mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) +// so this function works as long as the argument size matches. +static mp_obj_t any_call_new(const mp_obj_type_t *arg0, size_t n_args, size_t n_kw, const mp_obj_t *args) { + #if MICROPY_OBJ_REPR != MICROPY_OBJ_REPR_D + MP_STATIC_ASSERT(sizeof(mp_obj_type_t *) == sizeof(mp_obj_t)); + #endif + // If it's an actual instance we're used for __call__ so return self_in. + if (mp_obj_get_type(MP_OBJ_FROM_PTR(arg0)) == &mp_type_any_call_t) { + return MP_OBJ_FROM_PTR(arg0); + } + // Could also be we're being called as a function/decorator, return the decorated thing then. + // TODO obviously a bit of a hack, plus doesn't work for decorators with arguments. + // Note could test mp_obj_is_fun on the first arg here, then being called as decorator etc that + // is true, but turns out just returning the last argument works in more cases, like + // UserId = typing.NewType("UserId", int) + // assert UserId(1) == 1 + if (n_args != 0) { + return args[n_args - 1]; + } + return mp_obj_malloc(mp_obj_any_call_t, arg0); +} + +#if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D +static mp_obj_t any_call_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) { + if (mp_obj_get_type(self_in) == &mp_type_any_call_t || n_args == 0) { + return self_in; + } + return args[n_args - 1]; +} +#else +#define any_call_call any_call_new +#endif + +#if defined(MICROPY_UNIX_COVERAGE) +void any_call_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest); +#endif + +static mp_obj_t any_call_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) { + return self_in; +} + +// TODO could probably apply same trick as in any_call_new here and merge any_call_module_attr, +// but still have to test if that's worth it code-size wise. +static void any_call_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + // Only loading is supported. + if (dest[0] == MP_OBJ_NULL) { + if (attr != MP_QSTR___str__ && attr != MP_QSTR___repr__) { + dest[0] = self_in; + } + } +} + +// 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) { + // 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); + } + } +} + +static MP_DEFINE_CONST_OBJ_TYPE( + mp_type_any_call_t, + MP_QSTR_any_call, + MP_TYPE_FLAG_NONE, + make_new, any_call_new, + attr, any_call_attr, + subscr, any_call_subscr, + 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); + +const mp_obj_module_t mp_module_typing = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&mp_module_typing_globals, +}; + +// Extensible such that a typing module implemented in Python still has priority. +MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_typing, mp_module_typing); +MP_REGISTER_MODULE_DELEGATION(mp_module_typing, any_call_module_attr); + + +#if MICROPY_PY_TYPING_EXTRA_MODULES +MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_abc, mp_module_typing); +MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR___future__, mp_module_typing); +MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_typing_extensions, mp_module_typing); +#endif + +#endif // MICROPY_PY_TYPING diff --git a/ports/unix/variants/mpconfigvariant_common.h b/ports/unix/variants/mpconfigvariant_common.h index cea0397414325..02cedbb266041 100644 --- a/ports/unix/variants/mpconfigvariant_common.h +++ b/ports/unix/variants/mpconfigvariant_common.h @@ -121,3 +121,9 @@ #define MICROPY_PY_MACHINE (1) #define MICROPY_PY_MACHINE_PULSE (1) #define MICROPY_PY_MACHINE_PIN_BASE (1) + +// Enable "typing" and related modules. +#ifndef MICROPY_PY_TYPING +#define MICROPY_PY_TYPING (1) +#define MICROPY_PY_TYPING_EXTRA_MODULES (1) +#endif diff --git a/ports/windows/mpconfigport.h b/ports/windows/mpconfigport.h index fabc9072d6c70..cfe08024f6fc7 100644 --- a/ports/windows/mpconfigport.h +++ b/ports/windows/mpconfigport.h @@ -143,6 +143,8 @@ #define MICROPY_PY_TIME_TIME_TIME_NS (1) #define MICROPY_PY_TIME_CUSTOM_SLEEP (1) #define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c" +#define MICROPY_PY_TYPING (1) +#define MICROPY_PY_TYPING_EXTRA_MODULES (1) #define MICROPY_PY_ERRNO (1) #define MICROPY_PY_UCTYPES (1) #define MICROPY_PY_DEFLATE (1) diff --git a/ports/windows/msvc/sources.props b/ports/windows/msvc/sources.props index f7c4c6bcac01b..859f8e8670911 100644 --- a/ports/windows/msvc/sources.props +++ b/ports/windows/msvc/sources.props @@ -20,6 +20,7 @@ + diff --git a/py/modcollections.c b/py/modcollections.c index 46326d13eef5a..fa8e96121bb3e 100644 --- a/py/modcollections.c +++ b/py/modcollections.c @@ -28,6 +28,11 @@ #if MICROPY_PY_COLLECTIONS +#if MICROPY_PY_TYPING && MICROPY_PY_TYPING_EXTRA_MODULES && MICROPY_MODULE_BUILTIN_SUBPACKAGES +// Enable importing collections.abc as an alias of the typing module. +extern const mp_obj_module_t mp_module_typing; +#endif + static const mp_rom_map_elem_t mp_module_collections_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_collections) }, #if MICROPY_PY_COLLECTIONS_DEQUE @@ -37,6 +42,9 @@ static const mp_rom_map_elem_t mp_module_collections_globals_table[] = { #if MICROPY_PY_COLLECTIONS_ORDEREDDICT { MP_ROM_QSTR(MP_QSTR_OrderedDict), MP_ROM_PTR(&mp_type_ordereddict) }, #endif + #if MICROPY_PY_TYPING && MICROPY_PY_TYPING_EXTRA_MODULES && MICROPY_MODULE_BUILTIN_SUBPACKAGES + { MP_ROM_QSTR(MP_QSTR_abc), MP_ROM_PTR(&mp_module_typing) }, + #endif }; static MP_DEFINE_CONST_DICT(mp_module_collections_globals, mp_module_collections_globals_table); diff --git a/py/mpconfig.h b/py/mpconfig.h index 34eafa9e5debb..343c0105101d7 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -1614,6 +1614,17 @@ typedef double mp_float_t; #define MICROPY_PY_THREAD_GIL_VM_DIVISOR (32) #endif +// Whether to provide the minimal typing module. +#ifndef MICROPY_PY_TYPING +#define MICROPY_PY_TYPING (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) +#endif + +// Whether to provide the minimal abc and typing_extensions modules. +// They will simply be aliases for the typing module. +#ifndef MICROPY_PY_TYPING_EXTRA_MODULES +#define MICROPY_PY_TYPING_EXTRA_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) +#endif + // Extended modules #ifndef MICROPY_PY_ASYNCIO diff --git a/tests/cpydiff/core_class_metaclass.py b/tests/cpydiff/core_class_metaclass.py new file mode 100644 index 0000000000000..b06849156819d --- /dev/null +++ b/tests/cpydiff/core_class_metaclass.py @@ -0,0 +1,12 @@ +""" +categories: Core,Classes +description: Defining a class with a metaclass is not possible. +cause: Currently not implemented to limit size and complexity of the runtime. +workaround: Use composition or class decorators instead of metaclasses. See https://realpython.com/python-metaclasses/#is-this-really-necessary +""" + +from abc import ABCMeta + + +class MyABC(metaclass=ABCMeta): + pass diff --git a/tests/cpydiff/modules_typing_generics.py b/tests/cpydiff/modules_typing_generics.py new file mode 100644 index 0000000000000..3f0685419b1ee --- /dev/null +++ b/tests/cpydiff/modules_typing_generics.py @@ -0,0 +1,32 @@ +""" +categories: Modules,typing +description: User Defined Generics +cause: Micropython does not implement User Defined Generics +workaround: None +""" + +from typing import Dict, Generic, TypeVar + +T = TypeVar("T") + + +class Registry(Generic[T]): + def __init__(self) -> None: + self._store: Dict[str, T] = {} + + def set_item(self, k: str, v: T) -> None: + self._store[k] = v + + def get_item(self, k: str) -> T: + return self._store[k] + + +family_name_reg = Registry[str]() +family_age_reg = Registry[int]() + +family_name_reg.set_item("husband", "steve") +family_name_reg.set_item("dad", "john") + +family_age_reg.set_item("steve", 30) + +print(repr(family_name_reg.__dict__)) diff --git a/tests/cpydiff/modules_typing_getargs.py b/tests/cpydiff/modules_typing_getargs.py new file mode 100644 index 0000000000000..8998cca67c116 --- /dev/null +++ b/tests/cpydiff/modules_typing_getargs.py @@ -0,0 +1,12 @@ +""" +categories: Modules,typing +description: ``get_args()`` function not fully implemented. +cause: Micropython does not implement all typing features +workaround: None +""" + +from typing import get_args + +# partial implementation of get_args +x = get_args(int) +assert x == (), f"expected () but got {x}" diff --git a/tests/cpydiff/modules_typing_getorigin.py b/tests/cpydiff/modules_typing_getorigin.py new file mode 100644 index 0000000000000..82c5914ecc42c --- /dev/null +++ b/tests/cpydiff/modules_typing_getorigin.py @@ -0,0 +1,13 @@ +""" +categories: Modules,typing +description: ``get_origin()`` function not fully implemented. +cause: Micropython does not implement all typing features from Python 3.8+ +workaround: None +""" +# https://docs.python.org/3/library/typing.html#typing.get_origin + +from typing import Dict, get_origin + +assert get_origin(Dict[str, int]) is dict, "origin Dict cannot be detected" + +assert get_origin(str) is None, "origin str should be None" diff --git a/tests/cpydiff/modules_typing_reveal_type.py b/tests/cpydiff/modules_typing_reveal_type.py new file mode 100644 index 0000000000000..f64957dc897ff --- /dev/null +++ b/tests/cpydiff/modules_typing_reveal_type.py @@ -0,0 +1,24 @@ +""" +categories: Modules,typing +description: ``reveal_type()`` is not implemented. +cause: Micropython does not implement all typing features +workaround: None +""" + +from typing import Self, reveal_type + + +class Foo: + def return_self(self) -> Self: + ... + + +class SubclassOfFoo(Foo): + pass + + +foo = Foo() +sub = SubclassOfFoo() + +reveal_type(foo) +reveal_type(sub) diff --git a/tests/cpydiff/modules_typing_runtime_checkable.py b/tests/cpydiff/modules_typing_runtime_checkable.py new file mode 100644 index 0000000000000..4acf3e12bde75 --- /dev/null +++ b/tests/cpydiff/modules_typing_runtime_checkable.py @@ -0,0 +1,17 @@ +""" +categories: Modules,typing +description: ``runtime_checkable()`` is not implemented. +cause: Micropython does not implement all typing features from Python 3.8+ +workaround: None +""" + +from typing import runtime_checkable, Protocol + + +@runtime_checkable +class SwallowLaden(Protocol): + def __iter__(self): + ... + + +assert isinstance([1, 2, 3], SwallowLaden) diff --git a/tests/cpydiff/modules_typing_typeddict.py b/tests/cpydiff/modules_typing_typeddict.py new file mode 100644 index 0000000000000..6311bc887668b --- /dev/null +++ b/tests/cpydiff/modules_typing_typeddict.py @@ -0,0 +1,24 @@ +""" +categories: Modules,typing +description: ``TypedDict`` class not allowed for instance or class checks. +cause: Micropython does not implement all typing features +workaround: None +""" + +from typing import TypeVar, TypedDict + + +class Movie(TypedDict): + name: str + year: int + + +movie: Movie = {"name": "Blade Runner", "year": 1982} + +try: + if isinstance(movie, Movie): # type: ignore + pass + print("TypedDict class not allowed for instance or class checks") + +except TypeError as e: + print("Handled according to spec") diff --git a/tests/extmod/collections_abc.py b/tests/extmod/collections_abc.py new file mode 100644 index 0000000000000..dd65d90381b39 --- /dev/null +++ b/tests/extmod/collections_abc.py @@ -0,0 +1,88 @@ +print("Testing runtime aspects of collections.abc module") + +try: + import typing +except ImportError: + print("SKIP") + raise SystemExit + + +print("Testing : collections.abc.Mapping, Sequence") + +# FIXME: from collections.abc import Mapping, Sequence +from typing import Mapping, Sequence + + +class Employee: + ... + + +def notify_by_email(employees: Sequence[Employee], overrides: Mapping[str, str]) -> None: + pass + + +notify_by_email([], {}) + + +print("Testing : collections.abc.Callable, Awaitable") + +# from collections.abc import Callable, Awaitable +from typing import Callable, Awaitable + + +def feeder(get_next_item: Callable[[], str]) -> None: + ... # Body + + +def async_query( + on_success: Callable[[int], None], on_error: Callable[[int, Exception], None] +) -> None: + ... # Body + + +async def on_update(value: str) -> None: + ... # Body + + +callback: Callable[[str], Awaitable[None]] = on_update + +# ... + + +def concat(x: str, y: str) -> str: + return x + y + + +x: Callable[..., str] +x = str # OK +x = concat # Also OK + + +print("Testing : collections.abc.Iterable") + +# FIXME: from collections.abc import Iterable +from typing import Iterable +from typing import Protocol + + +class Combiner(Protocol): + def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: + ... + + +def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes: + for item in data: + pass + return b"".join(cb_results(*data)) + + +def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]: + return [val[:maxlen] for val in vals if maxlen is not None] + + +batch_proc([], good_cb) # OK + + +print("Testing : collections.abc.") +print("Testing : collections.abc.") +print("Testing : collections.abc.") diff --git a/tests/extmod/typing_mod_collections_abc.py b/tests/extmod/typing_mod_collections_abc.py new file mode 100644 index 0000000000000..a34537998f0f6 --- /dev/null +++ b/tests/extmod/typing_mod_collections_abc.py @@ -0,0 +1,48 @@ +from math import e + + +try: + import collections.abc +except ImportError: + print("SKIP") + raise SystemExit + +print("# Python 3.3+") +print("### module collections.abc") +# https://peps.python.org/pep-3119/ +# https://docs.python.org/3/library/collections.abc.html#module-collections.abc + +# No test for runtime behaviour + +print("collections.abc") + +from collections.abc import Container +from collections.abc import Hashable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Reversible +from collections.abc import Generator +from collections.abc import Sized +from collections.abc import Callable +from collections.abc import Collection +from collections.abc import Sequence +from collections.abc import MutableSequence + +# from collections.abc import ByteString # Deprecated since version 3.12, +from collections.abc import Set +from collections.abc import MutableSet +from collections.abc import Mapping +from collections.abc import MutableMapping +from collections.abc import MappingView +from collections.abc import KeysView +from collections.abc import ItemsView +from collections.abc import ValuesView +from collections.abc import Awaitable +from collections.abc import Coroutine +from collections.abc import AsyncIterable +from collections.abc import AsyncIterator +from collections.abc import AsyncGenerator +from collections.abc import Buffer + + +print("-----") diff --git a/tests/extmod/typing_mod_typing_extensions.py b/tests/extmod/typing_mod_typing_extensions.py new file mode 100644 index 0000000000000..0a7ced3f0c8f6 --- /dev/null +++ b/tests/extmod/typing_mod_typing_extensions.py @@ -0,0 +1,27 @@ +try: + # typing_extensions MUST exist when typing is available + import typing_extensions +except ImportError: + print("SKIP") + raise SystemExit + +print("# Python 3.5") +print("### module typing_extensions") +# https://typing-extensions.readthedocs.io/ + +# No test for runtime behaviour + +print("Special typing primitives") + +import typing_extensions + +try: + from typing_extensions import * + +except ImportError: + print("- [ ] FIXME: add typing_extensions module") + +# Do not test for all the individual names, just that the module exists +# as the different CPython versions have different subsets of names + +print("-----") diff --git a/tests/extmod/typing_pep_0484.py b/tests/extmod/typing_pep_0484.py new file mode 100644 index 0000000000000..18dd8b17688bc --- /dev/null +++ b/tests/extmod/typing_pep_0484.py @@ -0,0 +1,190 @@ +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 + +if cast(str, 123) == 123: + print("cast runtime works as identity function") +else: + print("- [ ] FIXME: Difference - cast runtime does not work as identity function") + +print("-----") diff --git a/tests/extmod/typing_pep_0526.py b/tests/extmod/typing_pep_0526.py new file mode 100644 index 0000000000000..b8c9a95a7c4e8 --- /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..e2d666d1370be --- /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..14ff0e0d8a622 --- /dev/null +++ b/tests/extmod/typing_pep_0591.py @@ -0,0 +1,110 @@ +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("- [ ] FIXME: 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'") + +# ------------------- + +try: + x: List[Final[int]] = [] # Error! + print("- [ ] FIXME: document cpydiff : 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_pep_3119.py b/tests/extmod/typing_pep_3119.py new file mode 100644 index 0000000000000..0a4f5214e862a --- /dev/null +++ b/tests/extmod/typing_pep_3119.py @@ -0,0 +1,85 @@ +from math import e + + +try: + import abc +except ImportError: + print("SKIP") + raise SystemExit + +print("# Python 3.0+") +print("### module abc") +# https://peps.python.org/pep-3119/ +# https://docs.python.org/3/library/abc.html#module-abc + + +print("PEP 3119 - abc") +try: + from abc import ABC + + class MyABC(ABC): + pass +except Exception: + print("- [ ] FIXME: from abc import ABC") + +# do not test : class MyABC(metaclass=ABCMeta): ... + + +print("@abc.abstractmethod") +try: + from abc import ABC, abstractmethod +except Exception: + print("- [ ] FIXME: from abc import abstractmethod") + raise SystemExit + + +class C1(ABC): + @abstractmethod + def my_abstract_method(self, arg1): + ... + + @classmethod + @abstractmethod + def my_abstract_classmethod(cls, arg2): + ... + + @staticmethod + @abstractmethod + def my_abstract_staticmethod(arg3): + ... + + @property + @abstractmethod + def my_abstract_property(self): + ... + + @my_abstract_property.setter + @abstractmethod + def my_abstract_property(self, val): + ... + + @abstractmethod + def _get_x(self): + ... + + @abstractmethod + def _set_x(self, val): + ... + + x = property(_get_x, _set_x) + + +# do not test deprecated abstractclassmethod, abstractstaticmethod, abstractproperty + + +print("abc.get_cache_token()") + +try: + from abc import get_cache_token + + print(get_cache_token()) +except Exception: + print("- [ ] FIXME: from abc import get_cache_token") + + +print("-----") diff --git a/tests/extmod/typing_runtime.py b/tests/extmod/typing_runtime.py new file mode 100644 index 0000000000000..d47d6dbc3ab4a --- /dev/null +++ b/tests/extmod/typing_runtime.py @@ -0,0 +1,456 @@ +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("typing.TYPE_CHECKING") +if TYPE_CHECKING: + from typing_extensions import TypeGuard + +print("typing parameter annotations") +from typing import Any, Dict, List, Union + + +def add_numbers(a: int, b: int) -> int: + return a + b + + +def greet(name: str) -> None: + print(f"Hello, {name}!") + + +def get_average(numbers: List[float]) -> float: + return sum(numbers) / len(numbers) + + +def process_data(data: Dict[str, Any]) -> None: + # process the data + pass + + +a = add_numbers(3, 5) +greet("Alice") +avg = get_average([1.0, 2.5, 3.5]) +process_data({"key": "value", "number": 42}) + + +print("typing.Self - Python 3.11") +from typing import Callable, Self + + +class BaseClass: + def register(self, callback: Callable[[Self], None]) -> None: + ... + + +def cb(x): + pass + + +base = BaseClass() +base.register(cb) + + +print("typing@no_type_check decorator") +from typing import no_type_check + + +@no_type_check +def quad(r0): + return r0 * 4 + + +print(quad(1)) + +print("typing.Protocol") + +from typing import Protocol + + +class Adder(Protocol): + def add(self, x, y): + ... + + +class IntAdder: + def add(self, x, y): + return x + y + + +class FloatAdder: + def add(self, x, y): + return x + y + + +def add(adder: Adder) -> None: + print(adder.add(2, 3)) + + +add(IntAdder()) +add(FloatAdder()) + +print("typing.NewType") + +from typing import NewType + +UserId = NewType("UserId", int) +some_id = UserId(524313) + +print(some_id) + +assert isinstance(some_id, int), "NewType should be instance of the original type" + + +print("typing.Any") +from typing import Any + +a: Any = None +a = [] # OK +a = 2 # OK + +s: str = "" +s = a # OK + + +def foo(item: Any) -> int: + # Passes type checking; 'item' could be any type, + # and that type might have a 'bar' method + item.bar() + return 42 + + +def hash_b(item: Any) -> int: + try: + # Passes type checking + item.magic() + return foo(item) + except AttributeError: + # just ignore any error for this test + pass + return 21 + ... + + +print(hash_b(42)) +print(hash_b("foo")) + +print("typing.AnyStr") + +from typing import AnyStr + + +def concat(a: AnyStr, b: AnyStr) -> AnyStr: + return a + b + + +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 # type: ignore +except TypeError: + print("TypeError is expected") + + +print("typing.LiteralString") +from typing import LiteralString + + +def run_query(sql: LiteralString) -> None: + ... + + +def caller(arbitrary_string: str, literal_string: LiteralString) -> None: + run_query("SELECT * FROM students") # OK + run_query(literal_string) # OK + run_query("SELECT * FROM " + literal_string) # OK + run_query(arbitrary_string) # type: ignore # type checker error + run_query(f"SELECT * FROM students WHERE name = {arbitrary_string}") # type: ignore # type checker error + + assert isinstance(literal_string, str), "literal_string should be a string" + assert isinstance(arbitrary_string, str), "arbitrary_string should be a string" + + +some_str = "a" * 1000 +literal_str = "drop * from tables" + +caller(some_str, literal_str) + +print("typing.overload") + +from typing import overload + + +@overload +def bar(x: int) -> str: + ... + + +@overload +def bar(x: str) -> int: + ... + + +def bar(x): + return x + + +print(bar(42)) + + +print("typing.Required, NotRequired in TypedDict") +# https://typing.readthedocs.io/en/latest/spec/typeddict.html#required-and-notrequired + +from typing import NotRequired, Required, TypedDict + + +class Movie(TypedDict): + title: Required[str] + year: int + director: NotRequired[str] + + +m = Movie(title="Life of Brian", year=1979) + + +print("typing.TypeVar") + +from typing import List, TypeVar + +T = TypeVar("T") + + +def first(container: List[T]) -> T: + return container[0] + + +list_one: List[str] = ["a", "b", "c"] +print(first(list_one)) + +list_two: List[int] = [1, 2, 3] +print(first(list_two)) + + +print("typing.Generator") + +from typing import Generator + + +def echo(a: float) -> Generator[int, float, str]: + yield int(a) + return "Done" + + +e = echo(2.4) +v = next(e) +print(v) + + +print("typing.NoReturn") + +from typing import NoReturn + + +def stop() -> NoReturn: + raise RuntimeError("no way") + + +# + +print("typing.Final") + +from typing import Final + +CONST: Final = 42 + + +print(CONST) + + +print("typing.final") + +from typing import final + + +class Base: + @final + def done(self) -> None: + ... + + +class Sub(Base): + def done(self) -> None: # type: ignore # Error reported by type checker + ... + + +@final +class Leaf: + ... + + +class Other(Leaf): # type: ignore # Error reported by type checker + ... + + +other = Other() + + +print("typing.TypeVarTuple and typing.Unpack") + +from typing import TypeVarTuple, Unpack + +Ts = TypeVarTuple("Ts") +tup: tuple[Unpack[Ts]] # Semantically equivalent, and backwards-compatible # type: ignore + + +print("typing.Callable, ParamSpec") + +# ParamSpec, 3.11 notation +# https://docs.python.org/3/library/typing.html#typing.ParamSpec + +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") +P = ParamSpec("P") + + +def add_logging(f: Callable[P, T]) -> Callable[P, T]: + """A type-safe decorator to add logging to a function.""" + + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + print(f"{f.__name__} was called") + return f(*args, **kwargs) + + return inner + + +@add_logging +def add_two(x: float, y: float) -> float: + """Add two numbers together.""" + return x + y + + +x = add_two(1, 2) +print(x) +assert x == 3, "add_two(1, 2) == 3" + + +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: document 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: document 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("-----") diff --git a/tests/extmod/typing_syntax.py b/tests/extmod/typing_syntax.py new file mode 100644 index 0000000000000..475b0a0a20a64 --- /dev/null +++ b/tests/extmod/typing_syntax.py @@ -0,0 +1,65 @@ +# This doesn't quite test everything but just serves to verify that basic syntax works, +# which for MicroPython means everything typing-related should be ignored. + +try: + import typing +except ImportError: + print("SKIP") + raise SystemExit + +from typing import List, Tuple, Iterable, NewType, TypeVar, Union, Generic +from typing import Any + +# Available with MICROPY_PY_TYPING_EXTRA_MODULES. +try: + import typing_extensions +except ImportError: + typing_extensions = None + +# Available with MICROPY_PY_TYPING_EXTRA_MODULES and MICROPY_MODULE_BUILTIN_SUBPACKAGES. +try: + import collections.abc + + collections.abc.Sequence +except ImportError: + pass + +import sys + +# If this is available verify it works, and try the other modules as well. +if typing_extensions is not None: + import __future__ + from abc import abstractmethod + + getattr(__future__, "annotations") + + +# if "micropython" in sys.implementation.name: +# # Verify assignment is not possible. +# try: +# typing.a = None +# raise Exception() +# except AttributeError: +# pass +# try: +# typing[0] = None +# raise Exception() +# except TypeError: +# pass +# try: +# List.a = None +# raise Exception() +# except AttributeError: +# pass + + +MyAlias = str +Vector: typing.List[float] +UserId = NewType("UserId", int) +T = TypeVar("T", int, float, complex) + +hintedGlobal: Any = None + + +def func_with_hints(c: int, b: MyAlias, a: Union[int, None], lst: List[float] = [0.0]) -> Any: + pass diff --git a/tests/ports/unix/extra_coverage.py.exp b/tests/ports/unix/extra_coverage.py.exp index 176db8e9f8479..106e702ff68a7 100644 --- a/tests/ports/unix/extra_coverage.py.exp +++ b/tests/ports/unix/extra_coverage.py.exp @@ -50,16 +50,18 @@ RuntimeError: ame__ port -builtins micropython _asyncio _thread -array binascii btree cexample -cmath collections cppexample cryptolib -deflate errno example_package -ffi framebuf gc hashlib -heapq io json machine -math os platform random -re select socket struct -sys termios time tls -uctypes vfs websocket +builtins micropython __future__ _asyncio +_thread abc array binascii +btree cexample cmath collections +cppexample cryptolib deflate errno +example_package ffi framebuf +gc hashlib heapq io +json machine math os +platform random re select +socket struct sys termios +time tls typing +typing_extensions uctypes vfs +websocket me micropython machine math