Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 116 additions & 1 deletion extmod/modtyping.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -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);
Expand Down
187 changes: 187 additions & 0 deletions tests/extmod/typing_pep_0484.py
Original file line number Diff line number Diff line change
@@ -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("-----")
Loading
Loading