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 <damien@micropython.org>
This commit is contained in:
Damien George 2021-04-20 17:11:13 +10:00
parent 5669a60954
commit 3c4bfd1dec
6 changed files with 39 additions and 7 deletions

View File

@ -176,10 +176,6 @@ Exceptions
.. exception:: OSError .. 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:: RuntimeError
.. exception:: StopIteration .. exception:: StopIteration

View File

@ -16,13 +16,13 @@ Constants
Error codes, based on ANSI C/POSIX standard. All error codes start with Error codes, based on ANSI C/POSIX standard. All error codes start with
"E". As mentioned above, inventory of the codes depends on "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:: where ``exc`` is an instance of `OSError`. Usage example::
try: try:
uos.mkdir("my_dir") uos.mkdir("my_dir")
except OSError as exc: except OSError as exc:
if exc.args[0] == uerrno.EEXIST: if exc.errno == uerrno.EEXIST:
print("Directory already exists") print("Directory already exists")
.. data:: errorcode .. data:: errorcode

View File

@ -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) { if (attr == MP_QSTR_args) {
decompress_error_text_maybe(self); decompress_error_text_maybe(self);
dest[0] = MP_OBJ_FROM_PTR(self->args); 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); dest[0] = mp_obj_exception_get_value(self_in);
} }
} }

View File

@ -1,3 +1,5 @@
# test basic properties of exceptions
print(repr(IndexError())) print(repr(IndexError()))
print(str(IndexError())) print(str(IndexError()))
@ -12,3 +14,6 @@ s = StopIteration()
print(s.value) print(s.value)
s = StopIteration(1, 2, 3) s = StopIteration(1, 2, 3)
print(s.value) print(s.value)
print(OSError().errno)
print(OSError(1, "msg").errno)

View File

@ -1,6 +1,10 @@
# test subclassing a native exception
class MyExc(Exception): class MyExc(Exception):
pass pass
e = MyExc(100, "Some error") e = MyExc(100, "Some error")
print(e) print(e)
print(repr(e)) print(repr(e))
@ -20,3 +24,19 @@ try:
raise MyExc("Some error2") raise MyExc("Some error2")
except: except:
print("Caught user exception") 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)

View File

@ -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)