I'm using it to write reuseable library code that can have both synchronous and asyncio code paths. It is simplified down to something like this:
from abc import ABCMeta, abstractmethod
from typing import Generator
class Helper( metaclass = ABCMeta ):
@abstractmethod
def help( self, con: DatabaseConnection ) -> None:
raise NotImplementedError
@abstractmethod
async def ahelp( self, con: AsyncDataConnection ) -> None:
raise NotImplementedError
class HelperSelect( Helper ):
' logic here to execute a select query against the database '
rows: list[dict[str,Any]] # help() and ahelp() write their results here
def help( self, con: DatabaseConnection ) -> None:
assert False, 'TODO FIXME write database execution logic here'
async def ahelp( self, con: AsyncDataConnection ) -> None:
assert False, 'TODO FIXME write database execution logic here'
def _application_logic() -> Generator[Helper,None,int]:
sql = 'select * from foo'
helper = HelperSelect( sql )
yield helper
# do something with helper.rows
return 0
def do_something( con: DatabaseConnection ):
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
helper.help( con )
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
async def ado_something( con: AsyncDatabaseConnection ):
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
await helper.ahelp( con )
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
Without the use of gen.throw, your stack trace won't show where inside the logic the exception happened, which can be very frustrating to troubleshoot. Using gen.throw() as in the example above fixes that.
The reason for the Helper classes is because I may have a half dozen different kinds of things the logic may need to request that require asyncio besides just database queries.
I took that psuedo code and built a version that you can actually run and see the differences:
from abc import ABCMeta, abstractmethod
import logging
from typing import Any, Generator
class Helper( metaclass = ABCMeta ):
@abstractmethod
def help( self ) -> None:
raise NotImplementedError
class HelperBoom( Helper ):
def help( self ) -> None:
assert False
def _application_logic() -> Generator[Helper,None,int]:
helper = HelperBoom()
yield helper
return 0
def do_something1():
gen = _application_logic()
try:
while True:
helper = next( gen )
helper.help()
except StopIteration as e:
return e.value
def do_something2():
gen = _application_logic()
try:
while True:
helper = next( gen )
try:
helper.help()
except Exception as e:
gen.throw( e )
except StopIteration as e:
return e.value
try:
do_something1()
except Exception:
logging.exception( 'do_something1 failed:' )
try:
do_something2()
except Exception:
logging.exception( 'do_something2 failed:' )
Here's the output, notice that do_something1's stack trace is missing the line in _application_logic, but do_something2's stack trace has that entry
ERROR:root:do_something1 failed:
Traceback (most recent call last):
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 35, in <module>
do_something1()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 23, in do_something1
helper.help()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 12, in help
assert False
AssertionError
ERROR:root:do_something2 failed:
Traceback (most recent call last):
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 39, in <module>
do_something2()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 32, in do_something2
gen.throw( e )
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 16, in _application_logic
yield helper
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 30, in do_something2
helper.help()
File "C:\cvs\itas\incpy\test_helper_exceptions.py", line 12, in help
assert False
AssertionError
throw()
functionality. – Spiro@contextmanager
decorator. – Exceptive