From ad6913dca9c420c141a9463257e426b8b1d778ce Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 28 May 2026 23:36:03 +0200 Subject: [PATCH 1/4] python-stdlib: Add modules to support typing. typing, typing_extensions and collections.abc Signed-off-by: Jos Verlinde --- .../collections-abc/collections/abc.py | 7 ++ python-stdlib/collections-abc/manifest.py | 5 + python-stdlib/typing/manifest.py | 8 ++ python-stdlib/typing/typing.py | 96 +++++++++++++++++++ python-stdlib/typing_extensions/manifest.py | 10 ++ .../typing_extensions/typing_extensions.py | 5 + 6 files changed, 131 insertions(+) create mode 100644 python-stdlib/collections-abc/collections/abc.py create mode 100644 python-stdlib/collections-abc/manifest.py create mode 100644 python-stdlib/typing/manifest.py create mode 100644 python-stdlib/typing/typing.py create mode 100644 python-stdlib/typing_extensions/manifest.py create mode 100644 python-stdlib/typing_extensions/typing_extensions.py diff --git a/python-stdlib/collections-abc/collections/abc.py b/python-stdlib/collections-abc/collections/abc.py new file mode 100644 index 000000000..329ba287d --- /dev/null +++ b/python-stdlib/collections-abc/collections/abc.py @@ -0,0 +1,7 @@ +# collections.abc +# minimal support for runtime typing +# type: ignore +try: + from typing import __Ignore as ABC, __getattr__ as __getattr__ +except: + pass diff --git a/python-stdlib/collections-abc/manifest.py b/python-stdlib/collections-abc/manifest.py new file mode 100644 index 000000000..24376bb5e --- /dev/null +++ b/python-stdlib/collections-abc/manifest.py @@ -0,0 +1,5 @@ +metadata(version="1.26.1") + +# require("collections") +require("typing") +package("collections") diff --git a/python-stdlib/typing/manifest.py b/python-stdlib/typing/manifest.py new file mode 100644 index 000000000..e7866b38b --- /dev/null +++ b/python-stdlib/typing/manifest.py @@ -0,0 +1,8 @@ +# type: ignore + +metadata(version="1.26.1", description="Typing module for MicroPython.") + +# default to opt_level 3 for minimal firmware size +options.defaults(opt_level=3) + +module("typing.py", opt=options.opt_level) diff --git a/python-stdlib/typing/typing.py b/python-stdlib/typing/typing.py new file mode 100644 index 000000000..f51c36758 --- /dev/null +++ b/python-stdlib/typing/typing.py @@ -0,0 +1,96 @@ +""" +This module provides runtime support for type hints. +based on : +- https://github.com/micropython/micropython-lib/pull/584 +- https://github.com/Josverl/micropython-stubs/tree/main/mip +- https://github.com/Josverl/rt_typing + +""" + +# ------------------------------------- +# code reduction by Ignoring type hints +# ------------------------------------- +class __Ignore: + """A class to ignore type hints in code.""" + + def __call__(*keys, **values): + # May need some guardrails here + pass + + def __getitem__(self, key): + # May need some guardrails here + return __ignore + + def __getattr__(self, key): + return self.__dict__.get(key, __ignore) + + +__ignore = __Ignore() +# ----------------- +# typing essentials +# ----------------- +TYPE_CHECKING = False + +def final(value): # ( 6 + bytes) + # decorator to indicate that a method should not be overridden + # https://docs.python.org/3/library/typing.html#typing.final + return value + + +# def overload(arg): # ( 27 bytes) +# # ignore functions signatures with @overload decorator +# return None +overload = __ignore # saves bytes, and is semantically similar + +def NewType(_, value): # (21 bytes) + # https://docs.python.org/3/library/typing.html#newtype + # MicroPython: just use the original type. + return value + +# --------------- +# useful methods +# --------------- + +# https://docs.python.org/3/library/typing.html#typing.cast +# def cast(type, arg): # ( 23 bytes) +# return arg +cast = NewType # saves bytes, and is semantically similar + +# https://docs.python.org/3/library/typing.html#typing.no_type_check +# def no_type_check(arg): # ( 26 bytes) +# # decorator to disable type checking on a function or method +# return arg +no_type_check = final # saves bytes, and is semantically similar + +# ----------------- +# less used methods +# ----------------- + +# def reveal_type(x): # ( 38 bytes) +# # # https://docs.python.org/3/library/typing.html#typing.reveal_type +# return x +# or for smaller size: +reveal_type = final # saves bytes, and is semantically similar + +# The get_origin behaviour is # already implemented by the __getattr__ +# method of __Ignore, which passess the current test suite. + +# def get_origin(type): # ( 23 bytes) +# # https://docs.python.org/3/library/typing.html#typing.get_origin +# # Return None for all unsupported objects. +# return None + + +def get_args(_): # ( 22 bytes) + # https://docs.python.org/3/library/typing.html#typing.get_args + # Python 3.8+ only + return () + + +# ref: https://github.com/micropython/micropython-lib/pull/584#issuecomment-2317690854 +def __getattr__(key): + return __ignore + +# snarky way to alias typing_extensions to typing ( saving 59 bytes) +import sys +sys.modules["typing_extensions"] = sys.modules["typing"] diff --git a/python-stdlib/typing_extensions/manifest.py b/python-stdlib/typing_extensions/manifest.py new file mode 100644 index 000000000..addbec166 --- /dev/null +++ b/python-stdlib/typing_extensions/manifest.py @@ -0,0 +1,10 @@ +metadata(version="1.26.1") + + +# default to opt_level 3 for minimal firmware size +options.defaults(opt_level=3) + +module("typing_extensions.py", opt=options.opt_level) + +require("typing") +package("typing_extensions") diff --git a/python-stdlib/typing_extensions/typing_extensions.py b/python-stdlib/typing_extensions/typing_extensions.py new file mode 100644 index 000000000..ecd1dcab1 --- /dev/null +++ b/python-stdlib/typing_extensions/typing_extensions.py @@ -0,0 +1,5 @@ +# typing_extensions.py +# type: ignore + +from typing import * +from typing import __getattr__ From c9192a2e911333f27d69efc92ef4259c9b4571bc Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 28 May 2026 23:36:03 +0200 Subject: [PATCH 2/4] python-stdlib: Update modules to allow for new typing modules. `__future__`, `abc`, `collection` Signed-off-by: Jos Verlinde --- python-stdlib/__future__/manifest.py | 6 +++-- python-stdlib/abc/abc.py | 12 ++++++++-- python-stdlib/abc/manifest.py | 6 +++-- .../collections/collections/__init__.py | 8 +++++++ python-stdlib/typing/typing.py | 23 +++++++++++++++---- .../typing_extensions/typing_extensions.py | 7 ++++-- 6 files changed, 50 insertions(+), 12 deletions(-) diff --git a/python-stdlib/__future__/manifest.py b/python-stdlib/__future__/manifest.py index e06f3268d..add632c05 100644 --- a/python-stdlib/__future__/manifest.py +++ b/python-stdlib/__future__/manifest.py @@ -1,3 +1,5 @@ -metadata(version="0.1.0") +metadata(version="0.1.1") -module("__future__.py") +options.defaults(opt_level=3) + +module("__future__.py", opt=options.opt_level) diff --git a/python-stdlib/abc/abc.py b/python-stdlib/abc/abc.py index c2c707f62..670e4e5ac 100644 --- a/python-stdlib/abc/abc.py +++ b/python-stdlib/abc/abc.py @@ -1,6 +1,14 @@ +# type: ignore class ABC: pass -def abstractmethod(f): - return f +def abstractmethod(arg): + return arg + +try: + # add functionality if typing module is available + from typing import __getattr__ as __getattr__ + +except: # naked except saves 4 bytes + pass diff --git a/python-stdlib/abc/manifest.py b/python-stdlib/abc/manifest.py index c76312960..277dc3ef9 100644 --- a/python-stdlib/abc/manifest.py +++ b/python-stdlib/abc/manifest.py @@ -1,3 +1,5 @@ -metadata(version="0.1.0") +metadata(version="0.2.0") -module("abc.py") +options.defaults(opt_level=3) + +module("abc.py", opt=options.opt_level) diff --git a/python-stdlib/collections/collections/__init__.py b/python-stdlib/collections/collections/__init__.py index 36dfc1c41..da0df43bc 100644 --- a/python-stdlib/collections/collections/__init__.py +++ b/python-stdlib/collections/collections/__init__.py @@ -6,6 +6,14 @@ from .defaultdict import defaultdict except ImportError: pass +# optional collections.abc typing dummy module +try: + # cannot use relative import here + import collections.abc as abc + import sys + sys.modules['collections.abc'] = abc +except ImportError: + pass class MutableMapping: diff --git a/python-stdlib/typing/typing.py b/python-stdlib/typing/typing.py index f51c36758..b67cc7508 100644 --- a/python-stdlib/typing/typing.py +++ b/python-stdlib/typing/typing.py @@ -30,12 +30,10 @@ def __getattr__(self, key): # typing essentials # ----------------- TYPE_CHECKING = False - -def final(value): # ( 6 + bytes) - # decorator to indicate that a method should not be overridden - # https://docs.python.org/3/library/typing.html#typing.final +def reveal_type(value): return value +override = final = reveal_type # saves bytes, and is semantically similar # def overload(arg): # ( 27 bytes) # # ignore functions signatures with @overload decorator @@ -47,9 +45,13 @@ def NewType(_, value): # (21 bytes) # MicroPython: just use the original type. return value +def TypeVar(key, *types, bound = None, covariant=False, contravariant=False, infer_variance=False): + return key # --------------- # useful methods # --------------- +# is semantically similar +TypeVarTuple = final # https://docs.python.org/3/library/typing.html#typing.cast # def cast(type, arg): # ( 23 bytes) @@ -86,6 +88,18 @@ def get_args(_): # ( 22 bytes) # Python 3.8+ only return () +# https://typing.python.org/en/latest/spec/typeddict.html +# make TypedDict dict-like at runtime. +TypedDict = dict + +class IO: + pass +class TextIO: + pass +class BinaryIO: + pass + +AnyStr=str # ref: https://github.com/micropython/micropython-lib/pull/584#issuecomment-2317690854 def __getattr__(key): @@ -94,3 +108,4 @@ def __getattr__(key): # snarky way to alias typing_extensions to typing ( saving 59 bytes) import sys sys.modules["typing_extensions"] = sys.modules["typing"] +del sys diff --git a/python-stdlib/typing_extensions/typing_extensions.py b/python-stdlib/typing_extensions/typing_extensions.py index ecd1dcab1..361364f9a 100644 --- a/python-stdlib/typing_extensions/typing_extensions.py +++ b/python-stdlib/typing_extensions/typing_extensions.py @@ -1,5 +1,8 @@ # typing_extensions.py # type: ignore +import typing -from typing import * -from typing import __getattr__ +# snarky way to alias typing_extensions to typing as import * wont work +import sys +sys.modules["typing_extensions"] = sys.modules["typing"] +del sys From bca923490c0658b807d7f355996bc5b46854a5a8 Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Mon, 1 Jun 2026 19:56:49 +0200 Subject: [PATCH 3/4] micropython/bundle-typing: Add bundle for typing modules. #2 --- micropython/bundles/bundle-typing/manifest.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 micropython/bundles/bundle-typing/manifest.py diff --git a/micropython/bundles/bundle-typing/manifest.py b/micropython/bundles/bundle-typing/manifest.py new file mode 100644 index 000000000..f0e9120b7 --- /dev/null +++ b/micropython/bundles/bundle-typing/manifest.py @@ -0,0 +1,26 @@ +# type: ignore[all] +# Bundle to simplify including typing support in MicroPython projects. +# This is not a full implementation of the typing module, but balances +# functionality with code size, while ignoring type hints at runtime +# to save resources. +# Usage: +# require("bundle-typing") +# require("bundle-typing", extensions=True) + +metadata( + version="1.0.0", + description="Limited runtime typing support for MicroPython.", +) + +options.defaults(opt_level=3, extensions=False) + +# Primary typing related modules +require("__future__", opt_level=options.opt_level) +require("typing", opt_level=options.opt_level) +require("typing_extensions", opt_level=options.opt_level) + +# Optional typing modules +if options.extensions: + require("collections", opt_level=options.opt_level) + require("collections-abc", opt_level=options.opt_level) + require("abc", opt_level=options.opt_level) From c0a41b79270e8275ae5b644c9a3f40df5b3996f1 Mon Sep 17 00:00:00 2001 From: Jos Verlinde Date: Thu, 28 May 2026 23:36:03 +0200 Subject: [PATCH 4/4] python-stdlib/unittest: Report (Un)Expected failures. The could be submitted as a separate PR, but is included here to allow better reporting of the tests results of different typing variants. Signed-off-by: Jos Verlinde --- python-stdlib/unittest/unittest/__init__.py | 67 ++++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/python-stdlib/unittest/unittest/__init__.py b/python-stdlib/unittest/unittest/__init__.py index 8014e2828..714768e7a 100644 --- a/python-stdlib/unittest/unittest/__init__.py +++ b/python-stdlib/unittest/unittest/__init__.py @@ -228,14 +228,21 @@ def skipUnless(cond, msg): return skip(msg) +class _ExpectedFailure(Exception): + pass + + +class _UnexpectedSuccess(Exception): + pass + + def expectedFailure(test): def test_exp_fail(*args, **kwargs): try: test(*args, **kwargs) - except: - pass - else: - assert False, "unexpected success" + except Exception: + raise _ExpectedFailure + raise _UnexpectedSuccess return test_exp_fail @@ -270,12 +277,29 @@ def run(self, suite: TestSuite): res.printErrors() print("----------------------------------------------------------------------") print("Ran %d tests\n" % res.testsRun) - if res.failuresNum > 0 or res.errorsNum > 0: - print("FAILED (failures=%d, errors=%d)" % (res.failuresNum, res.errorsNum)) + extras = [] + if res.skippedNum > 0: + extras.append("skipped=%d" % res.skippedNum) + if res.expectedFailuresNum > 0: + extras.append("expected failures=%d" % res.expectedFailuresNum) + if res.unexpectedSuccessesNum > 0: + extras.append("unexpected successes=%d" % res.unexpectedSuccessesNum) + if (res.failuresNum + res.errorsNum + res.unexpectedSuccessesNum) > 0: + parts = [ + "failures=%d" % res.failuresNum, + "errors=%d" % res.errorsNum, + ] + if res.unexpectedSuccessesNum > 0: + parts.append("unexpected successes=%d" % res.unexpectedSuccessesNum) + if res.expectedFailuresNum > 0: + parts.append("expected failures=%d" % res.expectedFailuresNum) + if res.skippedNum > 0: + parts.append("skipped=%d" % res.skippedNum) + print("FAILED (%s)" % ", ".join(parts)) else: msg = "OK" - if res.skippedNum > 0: - msg += " (skipped=%d)" % res.skippedNum + if extras: + msg += " (%s)" % ", ".join(extras) print(msg) return res @@ -289,14 +313,22 @@ def __init__(self): self.errorsNum = 0 self.failuresNum = 0 self.skippedNum = 0 + self.expectedFailuresNum = 0 + self.unexpectedSuccessesNum = 0 self.testsRun = 0 self.errors = [] self.failures = [] self.skipped = [] + self.expectedFailures = [] + self.unexpectedSuccesses = [] self._newFailures = 0 def wasSuccessful(self): - return self.errorsNum == 0 and self.failuresNum == 0 + return ( + self.errorsNum == 0 + and self.failuresNum == 0 + and self.unexpectedSuccessesNum == 0 + ) def printErrors(self): if self.errors or self.failures: @@ -325,10 +357,14 @@ def __add__(self, other): self.errorsNum += other.errorsNum self.failuresNum += other.failuresNum self.skippedNum += other.skippedNum + self.expectedFailuresNum += other.expectedFailuresNum + self.unexpectedSuccessesNum += other.unexpectedSuccessesNum self.testsRun += other.testsRun self.errors.extend(other.errors) self.failures.extend(other.failures) self.skipped.extend(other.skipped) + self.expectedFailures.extend(other.expectedFailures) + self.unexpectedSuccesses.extend(other.unexpectedSuccesses) return self @@ -353,6 +389,19 @@ def _handle_test_exception( test_result.skipped.append((current_test, reason)) print(" skipped:", reason) return + elif isinstance(exc, _ExpectedFailure): + test_result.expectedFailuresNum += 1 + test_result.expectedFailures.append((current_test, "")) + if verbose: + print(" expected failure") + return + elif isinstance(exc, _UnexpectedSuccess): + test_result.unexpectedSuccessesNum += 1 + test_result.unexpectedSuccesses.append((current_test, "")) + if verbose: + print(" unexpected success") + test_result._newFailures += 1 + return elif isinstance(exc, AssertionError): test_result.failuresNum += 1 test_result.failures.append((current_test, ex_str))