From 3c4bfd1dec28ffbefc6379dedeaa24feaa2ef373 Mon Sep 17 00:00:00 2001 From: Damien George Date: Tue, 20 Apr 2021 17:11:13 +1000 Subject: [PATCH] py/objexcept: Support errno attribute on OSError exceptions. This commit adds the errno attribute to exceptions, so code can retrieve errno codes from an OSError using exc.errno. The implementation here simply lets `errno` (and the existing `value`) attributes work on any exception instance (they both alias args[0]). This is for efficiency and to keep code size down. The pros and cons of this are: Pros: - more compatible with CPython, less difference to document and learn - OSError().errno will correctly return None, whereas the current way of doing it via OSError().args[0] will raise an IndexError - it reduces code size on most bare-metal ports (because they already have the errno qstr) - for Python code that uses exc.errno the generated bytecode is 2 bytes smaller and more efficient to execute (compared with exc.args[0]); so bytecode loaded to RAM saves 2 bytes RAM for each use of this attribute, and bytecode that is frozen saves 2 bytes flash/ROM for each use - it's easier/shorter to type, and saves 2 bytes of space in .py files that use it (for each use) Cons: - increases code size by 4-8 bytes on minimal ports that don't already have the `errno` qstr - all exceptions now have .errno and .value attributes (a cpydiff test is added to address this) See also #2407. Signed-off-by: Damien George --- docs/library/builtins.rst | 4 ---- docs/library/uerrno.rst | 4 ++-- py/objexcept.c | 4 +++- tests/basics/exception1.py | 5 +++++ tests/basics/subclass_native3.py | 20 ++++++++++++++++++++ tests/cpydiff/types_exception_attrs.py | 9 +++++++++ 6 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 tests/cpydiff/types_exception_attrs.py diff --git a/docs/library/builtins.rst b/docs/library/builtins.rst index 365248dc76..3029cd9e3b 100644 --- a/docs/library/builtins.rst +++ b/docs/library/builtins.rst @@ -176,10 +176,6 @@ Exceptions .. exception:: OSError - |see_cpython| `python:OSError`. MicroPython doesn't implement ``errno`` - attribute, instead use the standard way to access exception arguments: - ``exc.args[0]``. - .. exception:: RuntimeError .. exception:: StopIteration diff --git a/docs/library/uerrno.rst b/docs/library/uerrno.rst index def01362f1..1d60c80e11 100644 --- a/docs/library/uerrno.rst +++ b/docs/library/uerrno.rst @@ -16,13 +16,13 @@ Constants Error codes, based on ANSI C/POSIX standard. All error codes start with "E". As mentioned above, inventory of the codes depends on - :term:`MicroPython port`. Errors are usually accessible as ``exc.args[0]`` + :term:`MicroPython port`. Errors are usually accessible as ``exc.errno`` where ``exc`` is an instance of `OSError`. Usage example:: try: uos.mkdir("my_dir") except OSError as exc: - if exc.args[0] == uerrno.EEXIST: + if exc.errno == uerrno.EEXIST: print("Directory already exists") .. data:: errorcode diff --git a/py/objexcept.c b/py/objexcept.c index 885032c3e3..f6bffec383 100644 --- a/py/objexcept.c +++ b/py/objexcept.c @@ -261,7 +261,9 @@ void mp_obj_exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { if (attr == MP_QSTR_args) { decompress_error_text_maybe(self); dest[0] = MP_OBJ_FROM_PTR(self->args); - } else if (self->base.type == &mp_type_StopIteration && attr == MP_QSTR_value) { + } else if (attr == MP_QSTR_value || attr == MP_QSTR_errno) { + // These are aliases for args[0]: .value for StopIteration and .errno for OSError. + // For efficiency let these attributes apply to all exception instances. dest[0] = mp_obj_exception_get_value(self_in); } } diff --git a/tests/basics/exception1.py b/tests/basics/exception1.py index d83764cb93..a0ca8a74e2 100644 --- a/tests/basics/exception1.py +++ b/tests/basics/exception1.py @@ -1,3 +1,5 @@ +# test basic properties of exceptions + print(repr(IndexError())) print(str(IndexError())) @@ -12,3 +14,6 @@ s = StopIteration() print(s.value) s = StopIteration(1, 2, 3) print(s.value) + +print(OSError().errno) +print(OSError(1, "msg").errno) diff --git a/tests/basics/subclass_native3.py b/tests/basics/subclass_native3.py index 6745b77bb2..ac5aabfed7 100644 --- a/tests/basics/subclass_native3.py +++ b/tests/basics/subclass_native3.py @@ -1,6 +1,10 @@ +# test subclassing a native exception + + class MyExc(Exception): pass + e = MyExc(100, "Some error") print(e) print(repr(e)) @@ -20,3 +24,19 @@ try: raise MyExc("Some error2") except: print("Caught user exception") + + +class MyStopIteration(StopIteration): + pass + + +print(MyStopIteration().value) +print(MyStopIteration(1).value) + + +class MyOSError(OSError): + pass + + +print(MyOSError().errno) +print(MyOSError(1, "msg").errno) diff --git a/tests/cpydiff/types_exception_attrs.py b/tests/cpydiff/types_exception_attrs.py new file mode 100644 index 0000000000..ad72b62a61 --- /dev/null +++ b/tests/cpydiff/types_exception_attrs.py @@ -0,0 +1,9 @@ +""" +categories: Types,Exception +description: All exceptions have readable ``value`` and ``errno`` attributes, not just ``StopIteration`` and ``OSError``. +cause: MicroPython is optimised to reduce code size. +workaround: Only use ``value`` on ``StopIteration`` exceptions, and ``errno`` on ``OSError`` exceptions. Do not use or rely on these attributes on other exceptions. +""" +e = Exception(1) +print(e.value) +print(e.errno)