Patterns
********


Optional error handling
=======================

In some cases it may be desirable to keep exception handling in a
helper that returns "None" on error.  In the past, the project used an
optional "error" argument that defaulted to "True" that indicated that
exceptions should be raised.  Callers could pass "error=False" to
instruct the function to return "None" instead.

With Python's typing annotations, these routines must be annotated as
returning an Optional value.  While the @overload decorator allows us
to associate return types with specific argument types and counts,
there is no way to associate a return type with specific argument
*values*, like "error=False".

A function annotated as returning an "Optional" value affects the
implied types of the variables used to assign the result.  Every
caller of such a routine would need to check the result against "None"
in order to drop the "Optional" annotation from the type.  Even when
we know the function *cannot* return "None" when passed "error=True".

The way we handle this is to have separate functions for each case so
that callers which will never have a "None" value returned do not need
to check it.

Here are a few examples:


Function raises its own exceptions
----------------------------------

   from typing import Optional

   import gdb

   def new_routine(val: gdb.Value) -> str:
       if some_condition:
           raise RuntimeError("something bad happened")

       return val.string()

   def new_routine_safe(val: gdb.Value) -> Optional[str]:
       try:
           return new_routine(val)
       except RuntimeError:
           return None


Function calls functions that raise optional exceptions
-------------------------------------------------------

   from typing import Optional

   import gdb

   def some_existing_routine(val: gdb.Value, error: bool = True) -> Optional[str]:
       if some_condition:
           if error:
               raise RuntimeError("something bad happened")
           return None

       return val.string()

   def new_routine(val: gdb.Value) -> str:
       print("do something")

       ret = some_existing_routine(val)

       # This is required to drop the Optional annotation
       if ret is None:
           raise RuntimeError("some_existing_routine can't return None")
       return ret

   def new_routine_safe(val: gdb.Value) -> Optional[str]:
       return some_existing_routine(val, False)
