diff --git a/TermTk/TTkCore/signal.py b/TermTk/TTkCore/signal.py index 67fef876..b69acf32 100644 --- a/TermTk/TTkCore/signal.py +++ b/TermTk/TTkCore/signal.py @@ -58,9 +58,29 @@ Methods __all__ = ['pyTTkSlot', 'pyTTkSignal'] # from typing import TypeVar, TypeVarTuple, Generic, List -from inspect import getfullargspec +from inspect import getfullargspec, iscoroutinefunction from types import LambdaType from threading import Lock +import asyncio + +import importlib.util + +if importlib.util.find_spec('pyodideProxy'): + def _run_coroutines(coros): + for call in coros: + asyncio.create_task(call) +else: + from threading import Thread + import asyncio + + def _async_runner(coros): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(asyncio.gather(*coros)) + loop.close() + + def _run_coroutines(coros): + Thread(target=_async_runner, args=(coros,)).start() def pyTTkSlot(*args): def pyTTkSlot_d(func): @@ -73,7 +93,7 @@ def pyTTkSlot(*args): # class pyTTkSignal(Generic[*Ts]): class pyTTkSignal(): _signals = [] - __slots__ = ('_types', '_connected_slots', '_mutex') + __slots__ = ('_types', '_connected_slots', '_connected_async_slots', '_mutex') def __init__(self, *args, **kwargs) -> None: # ref: http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#PyQt5.QtCore.pyqtSignal @@ -87,6 +107,7 @@ class pyTTkSignal(): # an unbound signal self._types = args self._connected_slots = {} + self._connected_async_slots = {} self._mutex = Lock() pyTTkSignal._signals.append(self) @@ -122,8 +143,12 @@ class pyTTkSignal(): if a!=b and not issubclass(a,b): error = "Decorated slot has no signature compatible: "+slot.__name__+str(slot._TTkslot_attr)+" != signal"+str(self._types) raise TypeError(error) - if slot not in self._connected_slots: - self._connected_slots[slot]=slice(nargs) + if iscoroutinefunction(slot): + if slot not in self._connected_async_slots: + self._connected_async_slots[slot]=slice(nargs) + else: + if slot not in self._connected_slots: + self._connected_slots[slot]=slice(nargs) def disconnect(self, *args, **kwargs) -> None: for slot in args: @@ -137,6 +162,9 @@ class pyTTkSignal(): raise TypeError(error) for slot,sl in self._connected_slots.copy().items(): slot(*args[sl], **kwargs) + if self._connected_async_slots: + coros = [slot(*args[sl], **kwargs) for slot,sl in self._connected_async_slots.copy().items()] + _run_coroutines(coros) self._mutex.release() def clear(self): @@ -150,4 +178,4 @@ class pyTTkSignal(): def forward(self): def _ret(*args, **kwargs) -> None: self.emit(*args, **kwargs) - return _ret + return _ret \ No newline at end of file diff --git a/tests/t.ui/test.ui.034.async.01.py b/tests/t.ui/test.ui.034.async.01.py new file mode 100755 index 00000000..d89f7b8c --- /dev/null +++ b/tests/t.ui/test.ui.034.async.01.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import asyncio +import inspect +import sys, os +import time +from datetime import datetime + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +root = ttk.TTk(layout=(rgl:=ttk.TTkGridLayout())) + +rgl.addWidget(button_add := ttk.TTkButton(maxSize=(8,3), border=True, text="+") , 0, 0) +rgl.addWidget(button_call := ttk.TTkButton(maxSize=(8,3), border=True, text="Call") , 1, 0) +rgl.addWidget(button_call2 := ttk.TTkButton(maxSize=(8,3), border=True, text="Call2", checkable=True) , 2, 0) +button_add.clicked.connect(lambda: num.setText(int(num.text()) + 1)) + +rgl.addWidget(res := ttk.TTkLabel(test='out...'), 0 , 1) +rgl.addWidget(num := ttk.TTkLabel(text='1') , 1 , 1) + +rgl.addWidget(qb := ttk.TTkButton(border=True, maxWidth=8, text="Quit"), 0,2,3,1) +qb.clicked.connect(ttk.TTkHelper.quit) + +rgl.addWidget(ttk.TTkLogViewer(), 3, 0, 1, 3) + + +# normal slot with a bolocking call +@ttk.pyTTkSlot() +def call0(): + now = datetime.now().strftime("[%Y-%m-%d]-[%H:%M:%S]") + res.setText(f"{now} 0 Calling...") + ttk.TTkLog.info(f"{now} 0 Calling...") + time.sleep(1) + res.setText(f"{now} 0 Calling... - DONE") + ttk.TTkLog.info(f"{now} 0 Calling... - DONE") + +# async call wothout slot decorator +async def call1(): + now = datetime.now().strftime("[%Y-%m-%d]-[%H:%M:%S]") + res.setText(f"{now} 1 Calling...") + ttk.TTkLog.info(f"{now} 1 Calling...") + await asyncio.sleep(3) + res.setText(f"{now} 1 Calling... - DONE") + ttk.TTkLog.info(f"{now} 1 Calling... - DONE") + +# async call with slot decorator +@ttk.pyTTkSlot() +async def call2(): + now = datetime.now().strftime("[%Y-%m-%d]-[%H:%M:%S]") + res.setText(f"{now} 2 Calling...") + ttk.TTkLog.info(f"{now} 2 Calling...") + await asyncio.sleep(4) + res.setText(f"{now} 2 Calling... - DONE") + ttk.TTkLog.info(f"{now} 2 Calling... - DONE") + +# async call with slot decorator and arguments +@ttk.pyTTkSlot(bool) +async def call3(val): + now = datetime.now().strftime("[%Y-%m-%d]-[%H:%M:%S]") + res.setText(f"{now} 3 Calling... {val}") + ttk.TTkLog.info(f"{now} 3 Calling... {val}") + await asyncio.sleep(5) + res.setText(f"{now} 3 Calling... {val} - DONE") + ttk.TTkLog.info(f"{now} 3 Calling... {val} - DONE") + +print(inspect.iscoroutinefunction(call0)) +print(inspect.iscoroutinefunction(call1)) +print(inspect.iscoroutinefunction(call2)) +print(inspect.iscoroutinefunction(call3)) + +button_call.clicked.connect(call0) +button_call.clicked.connect(call1) +button_call.clicked.connect(call2) + +# button_call2.toggled.connect(call0) +button_call2.toggled.connect(call1) +button_call2.toggled.connect(call2) +button_call2.toggled.connect(call3) + +root.mainloop() \ No newline at end of file diff --git a/tools/check.import.sh b/tools/check.import.sh index c5ff7a6b..ffc975f8 100755 --- a/tools/check.import.sh +++ b/tools/check.import.sh @@ -11,6 +11,8 @@ __check(){ -e "signal.py:from inspect import getfullargspec" \ -e "signal.py:from types import LambdaType" \ -e "signal.py:from threading import Lock" \ + -e "signal.py:import asyncio" \ + -e "signal.py:import importlib.util" \ -e "colors.py:from .colors_ansi_map" \ -e "log.py:import inspect" \ -e "log.py:import logging" \