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) 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-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/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/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..b67cc7508 --- /dev/null +++ b/python-stdlib/typing/typing.py @@ -0,0 +1,111 @@ +""" +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 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 +# 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 + +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) +# 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 () + +# 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): + return __ignore + +# 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/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..361364f9a --- /dev/null +++ b/python-stdlib/typing_extensions/typing_extensions.py @@ -0,0 +1,8 @@ +# typing_extensions.py +# type: ignore +import typing + +# snarky way to alias typing_extensions to typing as import * wont work +import sys +sys.modules["typing_extensions"] = sys.modules["typing"] +del sys 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))