From 908fbd2ed9d6ac5db067e1bbfd4ecbed8d1b27a8 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Fri, 22 Oct 2021 16:19:33 +0100 Subject: [PATCH] refactor: make event handlers asynchronous The event handlers have been made asynchronous to allow them to perform async I/O as needed. Tasks can also be submitted which are checked for completion so that their result/exception can be retrieved and reported back to the user. --- austin_tui/__main__.py | 20 +++++++-- austin_tui/controllers/austin.py | 5 +-- austin_tui/view/__init__.py | 72 ++++++++++++++++++++++++++++++-- austin_tui/view/austin.py | 27 ++++++++---- austin_tui/widgets/window.py | 4 ++ noxfile.py | 4 +- poetry.lock | 55 ++++++++++++------------ pyproject.toml | 2 +- 8 files changed, 140 insertions(+), 49 deletions(-) diff --git a/austin_tui/__main__.py b/austin_tui/__main__.py index 80fcaa3..e3dfdeb 100644 --- a/austin_tui/__main__.py +++ b/austin_tui/__main__.py @@ -23,7 +23,7 @@ import asyncio import sys from textwrap import wrap -from typing import Any, List, Optional +from typing import Any, Callable, List, Optional from austin import AustinError from austin.aio import AsyncAustin @@ -114,10 +114,15 @@ def on_terminate(self, stats: str) -> None: def on_view_event(self, event: AustinView.Event, data: Any = None) -> None: """View events handler.""" - def _unhandled() -> None: + def _unhandled(_: Any) -> None: raise RuntimeError(f"Unhandled view event: {event}") - {AustinView.Event.QUIT: self.shutdown}.get(event, _unhandled)() + { + AustinView.Event.QUIT: self.on_shutdown, + AustinView.Event.EXCEPTION: self.on_exception, + }.get(event, _unhandled)( + data + ) # type: ignore[operator] async def start(self, args: List[str]) -> None: """Start Austin and catch any exceptions.""" @@ -157,6 +162,15 @@ def shutdown(self) -> None: asyncio.get_event_loop().stop() + def on_shutdown(self, _: Any = None) -> None: + """The shutdown view event handler.""" + self.shutdown() + + def on_exception(self, exc: Exception) -> None: + """The exception view event handler.""" + self.shutdown() + raise exc + def main() -> None: """Main function.""" diff --git a/austin_tui/controllers/austin.py b/austin_tui/controllers/austin.py index a00943f..4ae6bae 100644 --- a/austin_tui/controllers/austin.py +++ b/austin_tui/controllers/austin.py @@ -283,9 +283,6 @@ def _dump_stats() -> None: except IOError as e: self.view.notification.set_text(f"Failed to save stats: {e}") - # TODO: This is very bad because the return value is an awaitable that - # we are not awaiting. If an exception is thrown then we never retrieve - # it. What's worse is that this could potentially mess up the output :(. - asyncio.get_event_loop().run_in_executor(None, _dump_stats) + self.view.submit_task(_dump_stats) return True diff --git a/austin_tui/view/__init__.py b/austin_tui/view/__init__.py index b08e6ac..c9b3bea 100644 --- a/austin_tui/view/__init__.py +++ b/austin_tui/view/__init__.py @@ -22,9 +22,23 @@ from abc import ABC import asyncio +from asyncio.coroutines import coroutine +from asyncio.coroutines import iscoroutine import curses import sys -from typing import Any, Callable, Dict, List, Optional, TextIO, Type +from typing import ( + Any, + Awaitable, + Callable, + Coroutine, + Dict, + Generator, + List, + Optional, + TextIO, + Type, + Union, +) from importlib_resources import files from lxml.etree import _Comment as Comment @@ -101,15 +115,27 @@ def __init__(self, name: str) -> None: def _create_tasks(self) -> None: loop = asyncio.get_event_loop() + event_handlers = set(self._event_handlers.values()) self._tasks = [ loop.create_task(coro()) for coro in ( attr for attr in (getattr(self, name) for name in dir(self)) - if callable(attr) and asyncio.iscoroutinefunction(attr) + if callable(attr) + and asyncio.iscoroutinefunction(attr) + and attr not in event_handlers ) ] + def on_exception(self, exc: Exception) -> None: + """Default task exception callback. + + This simply closes the view and re-raises the exception. Override in + sub-classes with custom logic. + """ + self.close() + raise exc + async def _input_loop(self) -> None: if not self.root_widget: raise RuntimeError("Missing root widget") @@ -120,12 +146,29 @@ async def _input_loop(self) -> None: if not self.root_widget._win: continue + # Handle user input on the root widget try: - if self._event_handlers[self.root_widget._win.getkey()](): + if await self._event_handlers[self.root_widget._win.getkey()](): self.root_widget.refresh() except (KeyError, curses.error): pass + # Retrieve the result of finished tasks + finished_tasks = [] + running_tasks = [] + for task in self._tasks: + if task.done(): + finished_tasks.append(task) + else: + running_tasks.append(task) + self._tasks = running_tasks + + for task in finished_tasks: + try: + task.result() + except Exception as exc: + self.on_exception(exc) + def _build(self, node: Element) -> Widget: _validate_ns(node) widget_class = QName(node).localname @@ -168,6 +211,27 @@ def open(self) -> None: self._create_tasks() + def submit_task( + self, + task: Union[ + asyncio.Task, + Coroutine[None, None, None], + Callable[[], Any], + ], + ) -> None: + """Submit a task to run concurrently in the event loop. + + A task can be an :class:`asyncio.Task` object, a coroutine or a plain + callable object. Any exception thrown within the task can be retrieved + from the ``on_exception`` callback. + """ + if isinstance(task, asyncio.Task): + self._tasks.append(task) + elif iscoroutine(task): + self._tasks.append(asyncio.create_task(task)) # type: ignore[arg-type] + else: + self._tasks.append(asyncio.get_event_loop().run_in_executor(None, task)) # type: ignore[arg-type] + def close(self) -> None: """Close the view.""" if not self._open or not self.root_widget: @@ -202,7 +266,7 @@ def _parse(view_node: Element) -> View: root, *rest = view_node view.root_widget = view._build(root) - view.connect("KEY_RESIZE", view.root_widget.resize) + view.connect("KEY_RESIZE", view.root_widget.on_resize) def _add_children(widget: Container, node: Element) -> None: for child in node: diff --git a/austin_tui/view/austin.py b/austin_tui/view/austin.py index b83a0cb..f6db0e9 100644 --- a/austin_tui/view/austin.py +++ b/austin_tui/view/austin.py @@ -44,6 +44,7 @@ class AustinView(View): class Event(Enum): """Austin View Events.""" + EXCEPTION = 0 QUIT = 1 def __init__( @@ -63,14 +64,22 @@ def __init__( self._stopped = False - def on_quit(self) -> bool: + def on_exception(self, exc: Exception) -> None: + """The on exception Austin view handler.""" + if not self.callback: + raise RuntimeError( + "AustinTUI requires a callback to handle exception events." + ) + self.callback(self.Event.EXCEPTION, exc) + + async def on_quit(self) -> bool: """Handle Quit event.""" if not self.callback: raise RuntimeError("AustinTUI requires a callback to handle quit events.") self.callback(self.Event.QUIT, None) return False - def on_next_thread(self) -> bool: + async def on_next_thread(self) -> bool: """Handle next thread event.""" if self._austin_controller.push(AustinEvent.CHANGE_THREAD, ThreadNav.NEXT): self.table.draw() @@ -78,7 +87,7 @@ def on_next_thread(self) -> bool: return True return False - def on_previous_thread(self) -> bool: + async def on_previous_thread(self) -> bool: """Handle previous thread event.""" if self._austin_controller.push(AustinEvent.CHANGE_THREAD, ThreadNav.PREV): self.table.draw() @@ -86,7 +95,7 @@ def on_previous_thread(self) -> bool: return True return False - def on_full_mode_toggled(self) -> bool: + async def on_full_mode_toggled(self) -> bool: """Handle Full Mode toggle.""" self.full_mode_cmd.toggle() self._austin_controller.push(AustinEvent.TOGGLE_FULL_MODE) @@ -94,31 +103,31 @@ def on_full_mode_toggled(self) -> bool: self.stats_view.refresh() return True - def on_save(self, data: Any = None) -> bool: + async def on_save(self, data: Any = None) -> bool: """Handle Save event.""" self.notification.set_text("Saving collected statistics ...") self.root_widget.refresh() return self._austin_controller.push(AustinEvent.SAVE) - def on_table_up(self, data: Any = None) -> bool: + async def on_table_up(self, data: Any = None) -> bool: """Handle Up Arrow on the table widget.""" self.stats_view.scroll_up() self.stats_view.refresh() return False - def on_table_down(self, data: Any = None) -> bool: + async def on_table_down(self, data: Any = None) -> bool: """Handle Down Arrow on the table widget.""" self.stats_view.scroll_down() self.stats_view.refresh() return False - def on_table_pgup(self, data: Any = None) -> bool: + async def on_table_pgup(self, data: Any = None) -> bool: """Handle Page Up on the table widget.""" self.stats_view.scroll_up(self.table.height - 1) self.stats_view.refresh() return False - def on_table_pgdown(self, data: Any = None) -> bool: + async def on_table_pgdown(self, data: Any = None) -> bool: """Handle Page Down on the table widget.""" self.stats_view.scroll_down(self.table.height - 1) self.stats_view.refresh() diff --git a/austin_tui/widgets/window.py b/austin_tui/widgets/window.py index 4cc713c..560d459 100644 --- a/austin_tui/widgets/window.py +++ b/austin_tui/widgets/window.py @@ -36,6 +36,10 @@ def __init__(self, name: str) -> None: super().__init__(name) self.win = self + async def on_resize(self) -> bool: + """The resize event handler.""" + return self.resize() + def resize(self) -> bool: """Resize the window. diff --git a/noxfile.py b/noxfile.py index 1af2003..31dd51f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -56,13 +56,13 @@ def lint(session): session.run("flake8", *LINT_LOCATIONS, "--exclude", *LINT_EXCLUDES) -@nox.session(python="3.7") +@nox.session(python="3.9") def mypy(session): session.install("mypy") session.run("mypy", "--show-error-codes", *MYPY_LOCATIONS) -@nox.session(python="3.7") +@nox.session(python="3.9") def coverage(session): """Upload coverage data.""" install_with_constraints(session, "coverage[toml]", "codecov") diff --git a/poetry.lock b/poetry.lock index 984b115..d9e6878 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,7 +44,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "austin-python" -version = "1.0.0" +version = "1.1.0" description = "Python wrapper for Austin, the frame stack sampler for CPython" category = "main" optional = false @@ -358,7 +358,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "3.18.1" +version = "3.19.0" description = "Protocol Buffers" category = "main" optional = false @@ -684,7 +684,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "3e2f716e79f80a91d2a112bbccc18bb9db00a71818b8b0fa995ff689c1079fe1" +content-hash = "46c7b0dff8a7955386d18ff125f02d6fff0820edd9cb8dfc47203e126e0bc9c9" [metadata.files] alabaster = [ @@ -704,8 +704,8 @@ attrs = [ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] austin-python = [ - {file = "austin-python-1.0.0.tar.gz", hash = "sha256:d834ad3039b9277b449c573fa937a0729fdf90029ab99aec0c7d11937972f575"}, - {file = "austin_python-1.0.0-py3-none-any.whl", hash = "sha256:6e61f51b64121048b6b5efdf36e83a589aff76fa7c8865a343efc0092ee555c9"}, + {file = "austin-python-1.1.0.tar.gz", hash = "sha256:7202f4abf79ab4fa594127e64a267bc60873d07fd37686e9ebeb0c22db0b68b9"}, + {file = "austin_python-1.1.0-py3-none-any.whl", hash = "sha256:d08d910b246a5e3f9e99306b8f41f4f8b6a19dd1fa94a8349e4282938942f9c2"}, ] babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, @@ -941,27 +941,30 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] protobuf = [ - {file = "protobuf-3.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fa6d1049d5315566f55c04d0b50c0033415144f96a9d25c820dc542fe2bb7f45"}, - {file = "protobuf-3.18.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0e2790c580070cff2921b93d562539ae027064340151c50db6aaf94c33048cd"}, - {file = "protobuf-3.18.1-cp36-cp36m-win32.whl", hash = "sha256:7e2f0677d68ecdd1cfda2abea65873f5bc7c3f5aae199404a3f5c1d1198c1a63"}, - {file = "protobuf-3.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6f714f5de9d40b3bec90ede4a688cce52f637ccdc5403afcda1f67598f4fdcd7"}, - {file = "protobuf-3.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a7be937c319146cc9f2626f0181e6809062c353e1fe449ecd0df374ba1036b2"}, - {file = "protobuf-3.18.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:10544fc7ace885a882623083c24da5b14148c77563acddc3c58d66f6153c09cd"}, - {file = "protobuf-3.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ee8b11e3eb2ed38f12137c3c132270a0b1dd509e317228ac47b67f21a583f1"}, - {file = "protobuf-3.18.1-cp37-cp37m-win32.whl", hash = "sha256:c492c217d3f69f4d2d5619571e52ab98538edbf53caf67e53ea92bd0a3b5670f"}, - {file = "protobuf-3.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3c1644f8a7f19b45c7a4c32278e2a55ae9e7e2f9e5f02d960a61f04a4890d3e6"}, - {file = "protobuf-3.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9ac691f7b24e4371dcd3980e4f5d6c840a2010da37986203053fee995786ec5"}, - {file = "protobuf-3.18.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:93bad12895d8b0ebc66b605c2ef1802311595f881aef032d9f13282b7550e6b2"}, - {file = "protobuf-3.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0851b5b89191e1976d34fa2e8eb8659829dfb45252053224cf9df857fb5f6a45"}, - {file = "protobuf-3.18.1-cp38-cp38-win32.whl", hash = "sha256:09d9268f6f9da81b7657adcf2fb397524c82f20cdf9e0db3ff4e7567977abd67"}, - {file = "protobuf-3.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d6d927774c0ec746fed15a4faff5f44aad0b7a3421fadb6f3ae5ca1f2f8ae26e"}, - {file = "protobuf-3.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d19c9cb805fd2be1d59eee39e152367ee92a30167e77bd06c8819f8f0009a4c"}, - {file = "protobuf-3.18.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:387f621bf7295a331f8c8a6962d097ceddeb85356792888cfa6a5c6bfc6886a4"}, - {file = "protobuf-3.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c1c5d3966c856f60a9d8d62f4455d70c31026422acdd5c228edf22b65b16c38"}, - {file = "protobuf-3.18.1-cp39-cp39-win32.whl", hash = "sha256:f20f803892f2135e8b96dc58c9a0c6a7ad8436794bf8784af229498d939b4c77"}, - {file = "protobuf-3.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:d76201380f41a2d83fb613a4683059d1fcafbe969518b3e409e279a8788fde2f"}, - {file = "protobuf-3.18.1-py2.py3-none-any.whl", hash = "sha256:61ca58e14033ca0dfa484a31d57237c1be3b6013454c7f53876a20fc88dd69b1"}, - {file = "protobuf-3.18.1.tar.gz", hash = "sha256:1c9bb40503751087300dd12ce2e90899d68628977905c76effc48e66d089391e"}, + {file = "protobuf-3.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:01a0645ef3acddfbc90237e1cdfae1086130fc7cb480b5874656193afd657083"}, + {file = "protobuf-3.19.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d3861c9721a90ba83ee0936a9cfcc4fa1c4b4144ac9658fb6f6343b38558e9b4"}, + {file = "protobuf-3.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b64be5d7270cf5e76375bac049846e8a9543a2d4368b69afe78ab725380a7487"}, + {file = "protobuf-3.19.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f6046b9e2feee0dce994493186e8715b4392ed5f50f356280ad9c2f9f93080a"}, + {file = "protobuf-3.19.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac2f8ec942d414609aba0331952ae12bb823e8f424bbb6b8c422f1cef32dc842"}, + {file = "protobuf-3.19.0-cp36-cp36m-win32.whl", hash = "sha256:3fea09aa04ef2f8b01fcc9bb87f19509934f8a35d177c865b8f9ee5c32b60c1b"}, + {file = "protobuf-3.19.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d1f4277d321f60456845ca9b882c4845736f1f5c1c69eb778eba22a97977d8af"}, + {file = "protobuf-3.19.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8488c2276f14f294e890cc1260ab342a13e90cd20dcc03319d2eea258f1fd321"}, + {file = "protobuf-3.19.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:36bf292f44966c67080e535321501717f4f1eba30faef8f2cd4b0c745a027211"}, + {file = "protobuf-3.19.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99af73ae34c93e0e2ace57ea2e70243f34fc015c8c23fd39ee93652e726f7e7"}, + {file = "protobuf-3.19.0-cp37-cp37m-win32.whl", hash = "sha256:f7a031cf8e2fc14acc0ba694f6dff0a01e06b70d817eba6edc72ee6cc20517ac"}, + {file = "protobuf-3.19.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d4ca5f0c7bc8d2e6966ca3bbd85e9ebe7191b6e21f067896d4af6b28ecff29fe"}, + {file = "protobuf-3.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a8a880593015ef2c83f7af797fa4fbf583b2c98b4bd94e46c5b61fee319d84b"}, + {file = "protobuf-3.19.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:6f16925f5c977dd7787973a50c242e60c22b1d1182aba6bec7bd02862579c10f"}, + {file = "protobuf-3.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9097327d277b0aa4a3224e61cd6850aef3269172397715299bcffc9f90293c9"}, + {file = "protobuf-3.19.0-cp38-cp38-win32.whl", hash = "sha256:708d04394a63ee9bdc797938b6e15ed5bf24a1cb37743eb3886fd74a5a67a234"}, + {file = "protobuf-3.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:ee4d07d596357f51316b6ecf1cc1927660e9d5e418385bb1c51fd2496cd9bee7"}, + {file = "protobuf-3.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a77b8fafdeb8f89fee2b7108ae60d8958d72e33478680cc1e05517892ecc46"}, + {file = "protobuf-3.19.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4f93e0f6af796ddd1502225ff8ea25340ced186ca05b601c44d5c88b45ba80a0"}, + {file = "protobuf-3.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:942dd6bc8bd2a3c6a156d8ab0f80bd45313f22b78e1176283270054dcc8ca4c2"}, + {file = "protobuf-3.19.0-cp39-cp39-win32.whl", hash = "sha256:7b3867795708ac88fde8d6f34f0d9a50af56087e41f624bdb2e9ff808ea5dda7"}, + {file = "protobuf-3.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:a74432e9d28a6072a2359a0f49f81eb14dd718e7dbbfb6c0789b456c49e1f130"}, + {file = "protobuf-3.19.0-py2.py3-none-any.whl", hash = "sha256:c96e94d3e523a82caa3e5f74b35dd1c4884199358d01c950d95c341255ff48bc"}, + {file = "protobuf-3.19.0.tar.gz", hash = "sha256:6a1dc6584d24ef86f5b104bcad64fa0fe06ed36e5687f426e0445d363a041d18"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, diff --git a/pyproject.toml b/pyproject.toml index 68d4e3d..aebd5f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ profile = "google" austin-tui = "austin_tui.__main__:main" [tool.poetry.dependencies] -austin-python = {version = "^1.0.0*", allow-prereleases = true} +austin-python = {version = "^1.1.0*", allow-prereleases = true} importlib-resources = "^2.0.1" lxml = "^4.5.1" python = "^3.6"