Skip to content

Commit a32e6d6

Browse files
committed
extmod: Implement minimal typing and related modules.
Signed-off-by: stijn <stijn@ignitron.net>
1 parent 338df1a commit a32e6d6

File tree

10 files changed

+276
-10
lines changed

10 files changed

+276
-10
lines changed

extmod/extmod.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ set(MICROPY_SOURCE_EXTMOD
4343
${MICROPY_EXTMOD_DIR}/modtls_axtls.c
4444
${MICROPY_EXTMOD_DIR}/modtls_mbedtls.c
4545
${MICROPY_EXTMOD_DIR}/modtime.c
46+
${MICROPY_EXTMOD_DIR}/modtyping.c
4647
${MICROPY_EXTMOD_DIR}/modvfs.c
4748
${MICROPY_EXTMOD_DIR}/modwebsocket.c
4849
${MICROPY_EXTMOD_DIR}/modwebrepl.c

extmod/extmod.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ SRC_EXTMOD_C += \
4343
extmod/modtls_axtls.c \
4444
extmod/modtls_mbedtls.c \
4545
extmod/modtime.c \
46+
extmod/modtyping.c \
4647
extmod/moductypes.c \
4748
extmod/modvfs.c \
4849
extmod/modwebrepl.c \

extmod/modtyping.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* This file is part of the MicroPython project, http://micropython.org/
3+
*
4+
* The MIT License (MIT)
5+
*
6+
* Copyright (c) 2024 Damien P. George
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
#include "py/obj.h"
28+
29+
#if MICROPY_PY_TYPING
30+
31+
// Implement roughly the equivalent of the following minimal Python typing module, meant to support the
32+
// typing syntax at runtime but otherwise ignoring any functionality:
33+
//
34+
// class _AnyCall:
35+
// def __init__(*args, **kwargs):
36+
// pass
37+
//
38+
// def __call__(self, *args, **kwargs):
39+
// return self
40+
//
41+
// def __getitem__(self, attr):
42+
// return self
43+
//
44+
// _typing_obj = _AnyCall()
45+
//
46+
// def __getattr__(attr):
47+
// return _typing_obj
48+
//
49+
// Note this works together with the micropython compiler itself ignoring type hints, i.e. when encountering
50+
//
51+
// def hello(name: str) -> None:
52+
// pass
53+
//
54+
// both str and None hints are simply ignored.
55+
56+
typedef struct _mp_obj_any_call_t
57+
{
58+
mp_obj_base_t base;
59+
} mp_obj_any_call_t;
60+
61+
extern const mp_obj_type_t mp_type_any_call_t;
62+
63+
64+
// Can be used both for __new__ and __call__: the latter's prototype is
65+
// (mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args)
66+
// so this function works as long as the argument size matches.
67+
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) {
68+
#if MICROPY_OBJ_REPR != MICROPY_OBJ_REPR_D
69+
MP_STATIC_ASSERT(sizeof(mp_obj_type_t *) == sizeof(mp_obj_t));
70+
#endif
71+
// If it's an actual instance we're used for __call__ so return self_in.
72+
if (mp_obj_get_type(MP_OBJ_FROM_PTR(arg0)) == &mp_type_any_call_t) {
73+
return MP_OBJ_FROM_PTR(arg0);
74+
}
75+
// Could also be we're being called as a function/decorator, return the decorated thing then.
76+
// TODO obviously a bit of a hack, plus doesn't work for decorators with arguments.
77+
// Note could test mp_obj_is_fun on the first arg here, then being called as decorator etc that
78+
// is true, but turns out just returning the last argument works in more cases, like
79+
// UserId = typing.NewType("UserId", int)
80+
// assert UserId(1) == 1
81+
if (n_args != 0) {
82+
return args[n_args - 1];
83+
}
84+
return mp_obj_malloc(mp_obj_any_call_t, arg0);
85+
}
86+
87+
#if MICROPY_OBJ_REPR == MICROPY_OBJ_REPR_D
88+
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) {
89+
if (mp_obj_get_type(self_in) == &mp_type_any_call_t || n_args == 0) {
90+
return self_in;
91+
}
92+
return args[n_args - 1];
93+
}
94+
#else
95+
#define any_call_call any_call_new
96+
#endif
97+
98+
#if defined(MICROPY_UNIX_COVERAGE)
99+
void any_call_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
100+
#endif
101+
102+
static mp_obj_t any_call_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) {
103+
return self_in;
104+
}
105+
106+
// TODO could probably apply same trick as in any_call_new here and merge any_call_module_attr,
107+
// but still have to test if that's worth it code-size wise.
108+
static void any_call_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
109+
// Only loading is supported.
110+
if (dest[0] == MP_OBJ_NULL) {
111+
if (attr != MP_QSTR___str__ && attr != MP_QSTR___repr__) {
112+
dest[0] = self_in;
113+
}
114+
}
115+
}
116+
117+
void any_call_module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
118+
// Only loading is supported.
119+
if (dest[0] == MP_OBJ_NULL) {
120+
dest[0] = MP_OBJ_FROM_PTR(&mp_type_any_call_t);
121+
}
122+
}
123+
124+
static MP_DEFINE_CONST_OBJ_TYPE(
125+
mp_type_any_call_t,
126+
MP_QSTR_any_call,
127+
MP_TYPE_FLAG_NONE,
128+
make_new, any_call_new,
129+
attr, any_call_attr,
130+
subscr, any_call_subscr,
131+
call, any_call_call
132+
);
133+
134+
135+
static const mp_rom_map_elem_t mp_module_typing_globals_table[] = {
136+
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_typing) },
137+
};
138+
139+
static MP_DEFINE_CONST_DICT(mp_module_typing_globals, mp_module_typing_globals_table);
140+
141+
const mp_obj_module_t mp_module_typing = {
142+
.base = { &mp_type_module },
143+
.globals = (mp_obj_dict_t *)&mp_module_typing_globals,
144+
};
145+
146+
// Extensible such that a typing module implemented in Python still has priority.
147+
MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_typing, mp_module_typing);
148+
MP_REGISTER_MODULE_DELEGATION(mp_module_typing, any_call_module_attr);
149+
150+
151+
#if MICROPY_PY_TYPING_EXTRA_MODULES
152+
MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_abc, mp_module_typing);
153+
MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR___future__, mp_module_typing);
154+
MP_REGISTER_EXTENSIBLE_MODULE(MP_QSTR_typing_extensions, mp_module_typing);
155+
#endif
156+
157+
#endif // MICROPY_PY_TYPING

ports/unix/variants/mpconfigvariant_common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,7 @@
121121
#define MICROPY_PY_MACHINE (1)
122122
#define MICROPY_PY_MACHINE_PULSE (1)
123123
#define MICROPY_PY_MACHINE_PIN_BASE (1)
124+
125+
// Enable "typing" and related modules.
126+
#define MICROPY_PY_TYPING (1)
127+
#define MICROPY_PY_TYPING_EXTRA_MODULES (1)

ports/windows/mpconfigport.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@
143143
#define MICROPY_PY_TIME_TIME_TIME_NS (1)
144144
#define MICROPY_PY_TIME_CUSTOM_SLEEP (1)
145145
#define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c"
146+
#define MICROPY_PY_TYPING (1)
147+
#define MICROPY_PY_TYPING_EXTRA_MODULES (1)
146148
#define MICROPY_PY_ERRNO (1)
147149
#define MICROPY_PY_UCTYPES (1)
148150
#define MICROPY_PY_DEFLATE (1)

ports/windows/msvc/sources.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<PyExtModSource Include="$(PyBaseDir)extmod\modre.c" />
2121
<PyExtModSource Include="$(PyBaseDir)extmod\modselect.c" />
2222
<PyExtModSource Include="$(PyBaseDir)extmod\modtime.c" />
23+
<PyExtModSource Include="$(PyBaseDir)extmod\modtyping.c" />
2324
<PyExtModSource Include="$(PyBaseDir)extmod\virtpin.c" />
2425
<PyExtModSource Include="$(PyBaseDir)extmod\vfs.c" />
2526
<PyExtModSource Include="$(PyBaseDir)extmod\vfs_posix.c" />

py/modcollections.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828

2929
#if MICROPY_PY_COLLECTIONS
3030

31+
#if MICROPY_PY_TYPING && MICROPY_PY_TYPING_EXTRA_MODULES && MICROPY_MODULE_BUILTIN_SUBPACKAGES
32+
// Enable importing collections.abc as an alias of the typing module.
33+
extern const mp_obj_module_t mp_module_typing;
34+
#endif
35+
3136
static const mp_rom_map_elem_t mp_module_collections_globals_table[] = {
3237
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_collections) },
3338
#if MICROPY_PY_COLLECTIONS_DEQUE
@@ -37,6 +42,9 @@ static const mp_rom_map_elem_t mp_module_collections_globals_table[] = {
3742
#if MICROPY_PY_COLLECTIONS_ORDEREDDICT
3843
{ MP_ROM_QSTR(MP_QSTR_OrderedDict), MP_ROM_PTR(&mp_type_ordereddict) },
3944
#endif
45+
#if MICROPY_PY_TYPING && MICROPY_PY_TYPING_EXTRA_MODULES && MICROPY_MODULE_BUILTIN_SUBPACKAGES
46+
{ MP_ROM_QSTR(MP_QSTR_abc), MP_ROM_PTR(&mp_module_typing) },
47+
#endif
4048
};
4149

4250
static MP_DEFINE_CONST_DICT(mp_module_collections_globals, mp_module_collections_globals_table);

py/mpconfig.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,17 @@ typedef double mp_float_t;
16141614
#define MICROPY_PY_THREAD_GIL_VM_DIVISOR (32)
16151615
#endif
16161616

1617+
// Whether to provide the minimal typing module.
1618+
#ifndef MICROPY_PY_TYPING
1619+
#define MICROPY_PY_TYPING (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
1620+
#endif
1621+
1622+
// Whether to provide the minimal abc and typing_extensions modules.
1623+
// They will simply be aliases for the typing module.
1624+
#ifndef MICROPY_PY_TYPING_EXTRA_MODULES
1625+
#define MICROPY_PY_TYPING_EXTRA_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
1626+
#endif
1627+
16171628
// Extended modules
16181629

16191630
#ifndef MICROPY_PY_ASYNCIO

tests/extmod/typing_syntax.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# This doesn't quite test everything but just serves to verify that basic syntax works,
2+
# which for MicroPython means everything typing-related should be ignored.
3+
4+
try:
5+
import typing
6+
except ImportError:
7+
print("SKIP")
8+
raise SystemExit
9+
10+
from typing import List, Tuple, Iterable, NewType, TypeVar, Union, Generic
11+
12+
# Available with MICROPY_PY_TYPING_EXTRA_MODULES.
13+
try:
14+
import typing_extensions
15+
except ImportError:
16+
from typing import Any
17+
18+
typing_extensions = None
19+
20+
# Available with MICROPY_PY_TYPING_EXTRA_MODULES and MICROPY_MODULE_BUILTIN_SUBPACKAGES.
21+
try:
22+
import collections.abc
23+
24+
collections.abc.Sequence
25+
except ImportError:
26+
pass
27+
28+
import sys
29+
30+
# If this is available verify it works, and try the other modules as well.
31+
if typing_extensions is not None:
32+
from typing_extensions import Any
33+
import __future__
34+
from abc import abstractmethod
35+
36+
getattr(__future__, "annotations")
37+
38+
39+
if "micropython" in sys.implementation.name:
40+
# Verify assignment is not possible.
41+
try:
42+
typing.a = None
43+
raise Exception()
44+
except AttributeError:
45+
pass
46+
try:
47+
typing[0] = None
48+
raise Exception()
49+
except TypeError:
50+
pass
51+
try:
52+
List.a = None
53+
raise Exception()
54+
except AttributeError:
55+
pass
56+
57+
58+
MyAlias = str
59+
Vector: typing.List[float]
60+
Nested = Iterable[Tuple[MyAlias, ...]]
61+
UserId = NewType("UserId", int)
62+
T = TypeVar("T", int, float, complex)
63+
64+
hintedGlobal: Any = None
65+
66+
67+
def func_with_hints(c: int, b: MyAlias, a: Union[int, None], lst: List[float] = [0.0]) -> Any:
68+
pass
69+
70+
71+
class ClassWithHints(Generic[T]):
72+
a: int = 0
73+
74+
def foo(self, other: int) -> None:
75+
self.typed_thing: List[T] = []
76+
77+
78+
class Bar(ClassWithHints[Any]):
79+
pass

tests/ports/unix/extra_coverage.py.exp

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ RuntimeError:
5050
ame__
5151
port
5252

53-
builtins micropython _asyncio _thread
54-
array binascii btree cexample
55-
cmath collections cppexample cryptolib
56-
deflate errno example_package
57-
ffi framebuf gc hashlib
58-
heapq io json machine
59-
math os platform random
60-
re select socket struct
61-
sys termios time tls
62-
uctypes vfs websocket
53+
builtins micropython __future__ _asyncio
54+
_thread abc array binascii
55+
btree cexample cmath collections
56+
cppexample cryptolib deflate errno
57+
example_package ffi framebuf
58+
gc hashlib heapq io
59+
json machine math os
60+
platform random re select
61+
socket struct sys termios
62+
time tls typing
63+
typing_extensions uctypes vfs
64+
websocket
6365
me
6466

6567
micropython machine math

0 commit comments

Comments
 (0)