2025-12-25 upload
This commit is contained in:
55
venv/Lib/site-packages/urwid/event_loop/__init__.py
Normal file
55
venv/Lib/site-packages/urwid/event_loop/__init__.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Package with EventLoop implementations for urwid."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
from .asyncio_loop import AsyncioEventLoop
|
||||
from .main_loop import MainLoop
|
||||
from .select_loop import SelectEventLoop
|
||||
|
||||
__all__ = (
|
||||
"AsyncioEventLoop",
|
||||
"EventLoop",
|
||||
"ExitMainLoop",
|
||||
"MainLoop",
|
||||
"SelectEventLoop",
|
||||
)
|
||||
|
||||
try:
|
||||
from .twisted_loop import TwistedEventLoop
|
||||
|
||||
__all__ += ("TwistedEventLoop",) # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from .tornado_loop import TornadoEventLoop
|
||||
|
||||
__all__ += ("TornadoEventLoop",) # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from .glib_loop import GLibEventLoop
|
||||
|
||||
__all__ += ("GLibEventLoop",) # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from .trio_loop import TrioEventLoop
|
||||
|
||||
__all__ += ("TrioEventLoop",) # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if sys.platform != "win32":
|
||||
# ZMQEventLoop cause interpreter crash on windows
|
||||
try:
|
||||
from .zmq_loop import ZMQEventLoop
|
||||
|
||||
__all__ += ("ZMQEventLoop",) # type: ignore[assignment]
|
||||
except ImportError:
|
||||
pass
|
||||
178
venv/Lib/site-packages/urwid/event_loop/abstract_loop.py
Normal file
178
venv/Lib/site-packages/urwid/event_loop/abstract_loop.py
Normal file
@@ -0,0 +1,178 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Abstract shared code for urwid EventLoop implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import signal
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor, Future
|
||||
from types import FrameType
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_Spec = ParamSpec("_Spec")
|
||||
|
||||
__all__ = ("EventLoop", "ExitMainLoop")
|
||||
|
||||
|
||||
class ExitMainLoop(Exception):
|
||||
"""
|
||||
When this exception is raised within a main loop the main loop
|
||||
will exit cleanly.
|
||||
"""
|
||||
|
||||
|
||||
class EventLoop(abc.ABC):
|
||||
"""
|
||||
Abstract class representing an event loop to be used by :class:`MainLoop`.
|
||||
"""
|
||||
|
||||
__slots__ = ("logger",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> Future[_T] | asyncio.Future[_T]:
|
||||
"""Run callable in executor if supported.
|
||||
|
||||
:param executor: Executor to use for running the function
|
||||
:type executor: concurrent.futures.Executor
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: arguments to function (positional only)
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function (keyword only)
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
(exact future type depends on the event loop type)
|
||||
:rtype: concurrent.futures.Future | asyncio.Future
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> typing.Any:
|
||||
"""
|
||||
Call callback() a given time from now. No parameters are
|
||||
passed to callback.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns a handle that may be passed to remove_alarm()
|
||||
|
||||
seconds -- floating point time to wait before calling callback
|
||||
callback -- function to call from event loop
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def enter_idle(self, callback):
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns a handle that may be passed to remove_idle()
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_alarm(self, handle) -> bool:
|
||||
"""
|
||||
Remove an alarm.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns True if the alarm exists, False otherwise
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_enter_idle(self, handle) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_watch_file(self, handle) -> bool:
|
||||
"""
|
||||
Remove an input file.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns True if the input file exists, False otherwise
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the event loop. Exit the loop when any callback raises
|
||||
an exception. If ExitMainLoop is raised, exit cleanly.
|
||||
|
||||
This method has no default implementation.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]):
|
||||
"""
|
||||
Call callback() when fd has some data to read. No parameters
|
||||
are passed to callback.
|
||||
|
||||
This method has no default implementation.
|
||||
|
||||
Returns a handle that may be passed to remove_watch_file()
|
||||
|
||||
fd -- file descriptor to watch for input
|
||||
callback -- function to call when input is available
|
||||
"""
|
||||
|
||||
def set_signal_handler(
|
||||
self,
|
||||
signum: int,
|
||||
handler: Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers,
|
||||
) -> Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers | None:
|
||||
"""
|
||||
Sets the signal handler for signal signum.
|
||||
|
||||
The default implementation of :meth:`set_signal_handler`
|
||||
is simply a proxy function that calls :func:`signal.signal()`
|
||||
and returns the resulting value.
|
||||
|
||||
signum -- signal number
|
||||
handler -- function (taking signum as its single argument),
|
||||
or `signal.SIG_IGN`, or `signal.SIG_DFL`
|
||||
"""
|
||||
return signal.signal(signum, handler)
|
||||
253
venv/Lib/site-packages/urwid/event_loop/asyncio_loop.py
Normal file
253
venv/Lib/site-packages/urwid/event_loop/asyncio_loop.py
Normal file
@@ -0,0 +1,253 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Asyncio based urwid EventLoop implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
_Spec = ParamSpec("_Spec")
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
__all__ = ("AsyncioEventLoop",)
|
||||
|
||||
|
||||
class AsyncioEventLoop(EventLoop):
|
||||
"""
|
||||
Event loop based on the standard library ``asyncio`` module.
|
||||
|
||||
.. warning::
|
||||
Under Windows, AsyncioEventLoop globally enforces WindowsSelectorEventLoopPolicy
|
||||
as a side-effect of creating a class instance.
|
||||
Original event loop policy is restored in destructor method.
|
||||
|
||||
.. note::
|
||||
If you make any changes to the urwid state outside of it
|
||||
handling input or responding to alarms (for example, from asyncio.Task
|
||||
running in background), and wish the screen to be
|
||||
redrawn, you must call :meth:`MainLoop.draw_screen` method of the
|
||||
main loop manually.
|
||||
|
||||
A good way to do this:
|
||||
asyncio.get_event_loop().call_soon(main_loop.draw_screen)
|
||||
"""
|
||||
|
||||
def __init__(self, *, loop: asyncio.AbstractEventLoop | None = None, **kwargs) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
self._event_loop_policy_altered: bool = False
|
||||
self._original_event_loop_policy: asyncio.AbstractEventLoopPolicy | None = None
|
||||
|
||||
if loop:
|
||||
self._loop: asyncio.AbstractEventLoop = loop
|
||||
else:
|
||||
self._original_event_loop_policy = asyncio.get_event_loop_policy()
|
||||
if sys.platform == "win32" and not isinstance(
|
||||
self._original_event_loop_policy, asyncio.WindowsSelectorEventLoopPolicy
|
||||
):
|
||||
self.logger.debug("Set WindowsSelectorEventLoopPolicy as asyncio event loop policy")
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
self._event_loop_policy_altered = True
|
||||
else:
|
||||
self._event_loop_policy_altered = False
|
||||
|
||||
self._loop = asyncio.get_event_loop()
|
||||
|
||||
else:
|
||||
self._runner: asyncio.Runner | None = None
|
||||
|
||||
if loop:
|
||||
self._loop: asyncio.AbstractEventLoop = loop
|
||||
else:
|
||||
try:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
self._runner = asyncio.Runner(loop_factory=asyncio.SelectorEventLoop)
|
||||
self._loop = self._runner.get_loop()
|
||||
|
||||
self._exc: BaseException | None = None
|
||||
|
||||
self._idle_asyncio_handle: asyncio.TimerHandle | None = None
|
||||
self._idle_handle: int = 0
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
|
||||
def __del__(self) -> None:
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if self._event_loop_policy_altered:
|
||||
asyncio.set_event_loop_policy(self._original_event_loop_policy) # Restore default event loop policy
|
||||
elif self._runner is not None:
|
||||
self._runner.close()
|
||||
|
||||
def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T]:
|
||||
"""
|
||||
Wrap the callback to also call _entering_idle.
|
||||
"""
|
||||
|
||||
@functools.wraps(callback)
|
||||
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T:
|
||||
if not self._idle_asyncio_handle:
|
||||
self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
|
||||
return callback(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def _entering_idle(self) -> None:
|
||||
"""
|
||||
Call all the registered idle callbacks.
|
||||
"""
|
||||
try:
|
||||
for callback in self._idle_callbacks.values():
|
||||
callback()
|
||||
finally:
|
||||
self._idle_asyncio_handle = None
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor | None,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> asyncio.Future[_T]:
|
||||
"""Run callable in executor.
|
||||
|
||||
:param executor: Executor to use for running the function. Default asyncio executor is used if None.
|
||||
:type executor: concurrent.futures.Executor | None
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: arguments to function (positional only)
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function (keyword only)
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
:rtype: asyncio.Future
|
||||
"""
|
||||
return self._loop.run_in_executor(executor, functools.partial(func, *args, **kwargs))
|
||||
|
||||
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> asyncio.TimerHandle:
|
||||
"""
|
||||
Call callback() a given time from now. No parameters are
|
||||
passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_alarm()
|
||||
|
||||
seconds -- time in seconds to wait before calling callback
|
||||
callback -- function to call from event loop
|
||||
"""
|
||||
return self._loop.call_later(seconds, self._also_call_idle(callback))
|
||||
|
||||
def remove_alarm(self, handle) -> bool:
|
||||
"""
|
||||
Remove an alarm.
|
||||
|
||||
Returns True if the alarm exists, False otherwise
|
||||
"""
|
||||
existed = not handle.cancelled()
|
||||
handle.cancel()
|
||||
return existed
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Call callback() when fd has some data to read. No parameters
|
||||
are passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_watch_file()
|
||||
|
||||
fd -- file descriptor to watch for input
|
||||
callback -- function to call when input is available
|
||||
"""
|
||||
self._loop.add_reader(fd, self._also_call_idle(callback))
|
||||
return fd
|
||||
|
||||
def remove_watch_file(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an input file.
|
||||
|
||||
Returns True if the input file exists, False otherwise
|
||||
"""
|
||||
return self._loop.remove_reader(handle)
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
Returns a handle that may be passed to remove_enter_idle()
|
||||
"""
|
||||
# XXX there's no such thing as "idle" in most event loops; this fakes
|
||||
# it by adding extra callback to the timer and file watch callbacks.
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _exception_handler(self, loop: asyncio.AbstractEventLoop, context):
|
||||
if exc := context.get("exception"):
|
||||
loop.stop()
|
||||
|
||||
if self._idle_asyncio_handle:
|
||||
# clean it up to prevent old callbacks
|
||||
# from messing things up if loop is restarted
|
||||
self._idle_asyncio_handle.cancel()
|
||||
self._idle_asyncio_handle = None
|
||||
|
||||
if not isinstance(exc, ExitMainLoop):
|
||||
# Store the exc_info so we can re-raise after the loop stops
|
||||
self._exc = exc
|
||||
else:
|
||||
loop.default_exception_handler(context)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Start the event loop.
|
||||
|
||||
Exit the loop when any callback raises an exception.
|
||||
If ExitMainLoop is raised, exit cleanly.
|
||||
"""
|
||||
self._loop.set_exception_handler(self._exception_handler)
|
||||
self._loop.run_forever()
|
||||
if self._exc:
|
||||
exc = self._exc
|
||||
self._exc = None
|
||||
raise exc.with_traceback(exc.__traceback__)
|
||||
294
venv/Lib/site-packages/urwid/event_loop/glib_loop.py
Normal file
294
venv/Lib/site-packages/urwid/event_loop/glib_loop.py
Normal file
@@ -0,0 +1,294 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""GLib based urwid EventLoop implementation.
|
||||
|
||||
PyGObject library is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import signal
|
||||
import typing
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor, Future
|
||||
from types import FrameType
|
||||
|
||||
from typing_extensions import Literal, ParamSpec
|
||||
|
||||
_Spec = ParamSpec("_Spec")
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
__all__ = ("GLibEventLoop",)
|
||||
|
||||
|
||||
def _ignore_handler(_sig: int, _frame: FrameType | None = None) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class GLibEventLoop(EventLoop):
|
||||
"""
|
||||
Event loop based on GLib.MainLoop
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
self._alarms: list[int] = []
|
||||
self._watch_files: dict[int, int] = {}
|
||||
self._idle_handle: int = 0
|
||||
self._glib_idle_enabled = False # have we called glib.idle_add?
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._loop = GLib.MainLoop()
|
||||
self._exc: BaseException | None = None
|
||||
self._enable_glib_idle()
|
||||
self._signal_handlers: dict[int, int] = {}
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> Future[_T]:
|
||||
"""Run callable in executor.
|
||||
|
||||
:param executor: Executor to use for running the function
|
||||
:type executor: concurrent.futures.Executor
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: positional arguments to function
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
:rtype: concurrent.futures.Future
|
||||
"""
|
||||
return executor.submit(func, *args, **kwargs)
|
||||
|
||||
def alarm(
|
||||
self,
|
||||
seconds: float,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> tuple[int, Callable[[], typing.Any]]:
|
||||
"""
|
||||
Call callback() a given time from now. No parameters are
|
||||
passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_alarm()
|
||||
|
||||
seconds -- floating point time to wait before calling callback
|
||||
callback -- function to call from event loop
|
||||
"""
|
||||
|
||||
@self.handle_exit
|
||||
def ret_false() -> Literal[False]:
|
||||
callback()
|
||||
self._enable_glib_idle()
|
||||
return False
|
||||
|
||||
fd = GLib.timeout_add(int(seconds * 1000), ret_false)
|
||||
self._alarms.append(fd)
|
||||
return (fd, callback)
|
||||
|
||||
def set_signal_handler(
|
||||
self,
|
||||
signum: int,
|
||||
handler: Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers,
|
||||
) -> None:
|
||||
"""
|
||||
Sets the signal handler for signal signum.
|
||||
|
||||
.. WARNING::
|
||||
Because this method uses the `GLib`-specific `unix_signal_add`
|
||||
function, its behaviour is different than `signal.signal().`
|
||||
|
||||
If `signum` is not `SIGHUP`, `SIGINT`, `SIGTERM`, `SIGUSR1`,
|
||||
`SIGUSR2` or `SIGWINCH`, this method performs no actions and
|
||||
immediately returns None.
|
||||
|
||||
Returns None in all cases (unlike :func:`signal.signal()`).
|
||||
..
|
||||
|
||||
signum -- signal number
|
||||
handler -- function (taking signum as its single argument),
|
||||
or `signal.SIG_IGN`, or `signal.SIG_DFL`
|
||||
"""
|
||||
glib_signals = [
|
||||
signal.SIGHUP,
|
||||
signal.SIGINT,
|
||||
signal.SIGTERM,
|
||||
signal.SIGUSR1,
|
||||
signal.SIGUSR2,
|
||||
]
|
||||
|
||||
# GLib supports SIGWINCH as of version 2.54.
|
||||
if not GLib.check_version(2, 54, 0):
|
||||
glib_signals.append(signal.SIGWINCH)
|
||||
|
||||
if signum not in glib_signals:
|
||||
# The GLib event loop supports only the signals listed above
|
||||
return
|
||||
|
||||
if signum in self._signal_handlers:
|
||||
GLib.source_remove(self._signal_handlers.pop(signum))
|
||||
|
||||
if handler == signal.Handlers.SIG_IGN:
|
||||
handler = _ignore_handler
|
||||
elif handler == signal.Handlers.SIG_DFL:
|
||||
return
|
||||
|
||||
def final_handler(signal_number: int):
|
||||
# MyPy False-negative: signal.Handlers casted
|
||||
handler(signal_number, None) # type: ignore[operator]
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
source = GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signum, final_handler, signum)
|
||||
self._signal_handlers[signum] = source
|
||||
|
||||
def remove_alarm(self, handle) -> bool:
|
||||
"""
|
||||
Remove an alarm.
|
||||
|
||||
Returns True if the alarm exists, False otherwise
|
||||
"""
|
||||
try:
|
||||
self._alarms.remove(handle[0])
|
||||
GLib.source_remove(handle[0])
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Call callback() when fd has some data to read. No parameters
|
||||
are passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_watch_file()
|
||||
|
||||
fd -- file descriptor to watch for input
|
||||
callback -- function to call when input is available
|
||||
"""
|
||||
|
||||
@self.handle_exit
|
||||
def io_callback(source, cb_condition) -> Literal[True]:
|
||||
callback()
|
||||
self._enable_glib_idle()
|
||||
return True
|
||||
|
||||
self._watch_files[fd] = GLib.io_add_watch(fd, GLib.IO_IN, io_callback)
|
||||
return fd
|
||||
|
||||
def remove_watch_file(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an input file.
|
||||
|
||||
Returns True if the input file exists, False otherwise
|
||||
"""
|
||||
if handle in self._watch_files:
|
||||
GLib.source_remove(self._watch_files[handle])
|
||||
del self._watch_files[handle]
|
||||
return True
|
||||
return False
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
Returns a handle that may be passed to remove_enter_idle()
|
||||
"""
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def _enable_glib_idle(self) -> None:
|
||||
if self._glib_idle_enabled:
|
||||
return
|
||||
GLib.idle_add(self._glib_idle_callback)
|
||||
self._glib_idle_enabled = True
|
||||
|
||||
def _glib_idle_callback(self):
|
||||
for callback in self._idle_callbacks.values():
|
||||
callback()
|
||||
self._glib_idle_enabled = False
|
||||
return False # ask glib not to call again (or we would be called
|
||||
|
||||
def remove_enter_idle(self, handle) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the event loop. Exit the loop when any callback raises
|
||||
an exception. If ExitMainLoop is raised, exit cleanly.
|
||||
"""
|
||||
try:
|
||||
self._loop.run()
|
||||
finally:
|
||||
if self._loop.is_running():
|
||||
self._loop.quit()
|
||||
if self._exc:
|
||||
# An exception caused us to exit, raise it now
|
||||
exc = self._exc
|
||||
self._exc = None
|
||||
raise exc.with_traceback(exc.__traceback__)
|
||||
|
||||
def handle_exit(self, f: Callable[_Spec, _T]) -> Callable[_Spec, _T | Literal[False]]:
|
||||
"""
|
||||
Decorator that cleanly exits the :class:`GLibEventLoop` if
|
||||
:exc:`ExitMainLoop` is thrown inside of the wrapped function. Store the
|
||||
exception info if some other exception occurs, it will be reraised after
|
||||
the loop quits.
|
||||
|
||||
*f* -- function to be wrapped
|
||||
"""
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | Literal[False]:
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ExitMainLoop:
|
||||
self._loop.quit()
|
||||
except BaseException as exc:
|
||||
self._exc = exc
|
||||
if self._loop.is_running():
|
||||
self._loop.quit()
|
||||
return False
|
||||
|
||||
return wrapper
|
||||
716
venv/Lib/site-packages/urwid/event_loop/main_loop.py
Normal file
716
venv/Lib/site-packages/urwid/event_loop/main_loop.py
Normal file
@@ -0,0 +1,716 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import typing
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
|
||||
from urwid import display, signals
|
||||
from urwid.command_map import Command, command_map
|
||||
from urwid.display.common import INPUT_DESCRIPTORS_CHANGED
|
||||
from urwid.util import StoppingContext, is_mouse_event
|
||||
from urwid.widget import PopUpTarget
|
||||
|
||||
from .abstract_loop import ExitMainLoop
|
||||
from .select_loop import SelectEventLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from urwid.display import BaseScreen
|
||||
from urwid.widget import Widget
|
||||
|
||||
from .abstract_loop import EventLoop
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
|
||||
IS_WINDOWS = sys.platform == "win32"
|
||||
PIPE_BUFFER_READ_SIZE = 4096 # can expect this much on Linux, so try for that
|
||||
|
||||
__all__ = ("CantUseExternalLoop", "MainLoop")
|
||||
|
||||
|
||||
class CantUseExternalLoop(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MainLoop:
|
||||
"""
|
||||
This is the standard main loop implementation for a single interactive
|
||||
session.
|
||||
|
||||
:param widget: the topmost widget used for painting the screen, stored as
|
||||
:attr:`widget` and may be modified. Must be a box widget.
|
||||
:type widget: widget instance
|
||||
|
||||
:param palette: initial palette for screen
|
||||
:type palette: iterable of palette entries
|
||||
|
||||
:param screen: screen to use, default is a new :class:`raw_display.Screen`
|
||||
instance; stored as :attr:`screen`
|
||||
:type screen: display module screen instance
|
||||
|
||||
:param handle_mouse: ``True`` to ask :attr:`.screen` to process mouse events
|
||||
:type handle_mouse: bool
|
||||
|
||||
:param input_filter: a function to filter input before sending it to
|
||||
:attr:`.widget`, called from :meth:`.input_filter`
|
||||
:type input_filter: callable
|
||||
|
||||
:param unhandled_input: a function called when input is not handled by
|
||||
:attr:`.widget`, called from :meth:`.unhandled_input`
|
||||
:type unhandled_input: callable
|
||||
|
||||
:param event_loop: if :attr:`.screen` supports external an event loop it may be
|
||||
given here, default is a new :class:`SelectEventLoop` instance;
|
||||
stored as :attr:`.event_loop`
|
||||
:type event_loop: event loop instance
|
||||
|
||||
:param pop_ups: `True` to wrap :attr:`.widget` with a :class:`PopUpTarget`
|
||||
instance to allow any widget to open a pop-up anywhere on the screen
|
||||
:type pop_ups: boolean
|
||||
|
||||
|
||||
.. attribute:: screen
|
||||
|
||||
The screen object this main loop uses for screen updates and reading input
|
||||
|
||||
.. attribute:: event_loop
|
||||
|
||||
The event loop object this main loop uses for waiting on alarms and IO
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: Widget,
|
||||
palette: Iterable[
|
||||
tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str]
|
||||
] = (),
|
||||
screen: BaseScreen | None = None,
|
||||
handle_mouse: bool = True,
|
||||
input_filter: Callable[[list[str], list[int]], list[str]] | None = None,
|
||||
unhandled_input: Callable[[str | tuple[str, int, int, int]], bool | None] | None = None,
|
||||
event_loop: EventLoop | None = None,
|
||||
pop_ups: bool = False,
|
||||
):
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
self._widget = widget
|
||||
self.handle_mouse = handle_mouse
|
||||
self._pop_ups = False # only initialize placeholder
|
||||
self.pop_ups = pop_ups # triggers property setting side-effect
|
||||
|
||||
if not screen:
|
||||
screen = display.raw.Screen()
|
||||
|
||||
if palette:
|
||||
screen.register_palette(palette)
|
||||
|
||||
self.screen: BaseScreen = screen
|
||||
self.screen_size: tuple[int, int] | None = None
|
||||
|
||||
self._unhandled_input = unhandled_input
|
||||
self._input_filter = input_filter
|
||||
|
||||
if not hasattr(screen, "hook_event_loop") and event_loop is not None:
|
||||
raise NotImplementedError(f"screen object passed {screen!r} does not support external event loops")
|
||||
if event_loop is None:
|
||||
event_loop = SelectEventLoop()
|
||||
self.event_loop: EventLoop = event_loop
|
||||
|
||||
if hasattr(self.screen, "signal_handler_setter"):
|
||||
# Tell the screen what function it must use to set
|
||||
# signal handlers
|
||||
self.screen.signal_handler_setter = self.event_loop.set_signal_handler
|
||||
|
||||
self._watch_pipes: dict[int, tuple[Callable[[], typing.Any], int]] = {}
|
||||
|
||||
@property
|
||||
def widget(self) -> Widget:
|
||||
"""
|
||||
Property for the topmost widget used to draw the screen.
|
||||
This must be a box widget.
|
||||
"""
|
||||
return self._widget
|
||||
|
||||
@widget.setter
|
||||
def widget(self, widget: Widget) -> None:
|
||||
self._widget = widget
|
||||
if self.pop_ups:
|
||||
self._topmost_widget.original_widget = self._widget
|
||||
else:
|
||||
self._topmost_widget = self._widget
|
||||
|
||||
def _set_widget(self, widget: Widget) -> None:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}._set_widget` is deprecated, "
|
||||
f"please use `{self.__class__.__name__}.widget` property."
|
||||
"API will be removed in version 4.0.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.widget = widget
|
||||
|
||||
@property
|
||||
def pop_ups(self) -> bool:
|
||||
return self._pop_ups
|
||||
|
||||
@pop_ups.setter
|
||||
def pop_ups(self, pop_ups: bool) -> None:
|
||||
self._pop_ups = pop_ups
|
||||
if pop_ups:
|
||||
self._topmost_widget = PopUpTarget(self._widget)
|
||||
else:
|
||||
self._topmost_widget = self._widget
|
||||
|
||||
def _set_pop_ups(self, pop_ups: bool) -> None:
|
||||
warnings.warn(
|
||||
f"method `{self.__class__.__name__}._set_pop_ups` is deprecated, "
|
||||
f"please use `{self.__class__.__name__}.pop_ups` property."
|
||||
"API will be removed in version 4.0.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self.pop_ups = pop_ups
|
||||
|
||||
def set_alarm_in(self, sec: float, callback: Callable[[Self, _T], typing.Any], user_data: _T = None):
|
||||
"""
|
||||
Schedule an alarm in *sec* seconds that will call *callback* from the
|
||||
within the :meth:`run` method.
|
||||
|
||||
:param sec: seconds until alarm
|
||||
:type sec: float
|
||||
:param callback: function to call with two parameters: this main loop
|
||||
object and *user_data*
|
||||
:type callback: callable
|
||||
:param user_data: optional user data to pass to the callback
|
||||
:type user_data: object
|
||||
"""
|
||||
self.logger.debug(f"Setting alarm in {sec!r} seconds with callback {callback!r}")
|
||||
|
||||
def cb() -> None:
|
||||
callback(self, user_data)
|
||||
|
||||
return self.event_loop.alarm(sec, cb)
|
||||
|
||||
def set_alarm_at(self, tm: float, callback: Callable[[Self, _T], typing.Any], user_data: _T = None):
|
||||
"""
|
||||
Schedule an alarm at *tm* time that will call *callback* from the
|
||||
within the :meth:`run` function. Returns a handle that may be passed to
|
||||
:meth:`remove_alarm`.
|
||||
|
||||
:param tm: time to call callback e.g. ``time.time() + 5``
|
||||
:type tm: float
|
||||
:param callback: function to call with two parameters: this main loop
|
||||
object and *user_data*
|
||||
:type callback: callable
|
||||
:param user_data: optional user data to pass to the callback
|
||||
:type user_data: object
|
||||
"""
|
||||
sec = tm - time.time()
|
||||
self.logger.debug(f"Setting alarm in {sec!r} seconds with callback {callback!r}")
|
||||
|
||||
def cb() -> None:
|
||||
callback(self, user_data)
|
||||
|
||||
return self.event_loop.alarm(sec, cb)
|
||||
|
||||
def remove_alarm(self, handle) -> bool:
|
||||
"""
|
||||
Remove an alarm. Return ``True`` if *handle* was found, ``False``
|
||||
otherwise.
|
||||
"""
|
||||
return self.event_loop.remove_alarm(handle)
|
||||
|
||||
if not IS_WINDOWS:
|
||||
|
||||
def watch_pipe(self, callback: Callable[[bytes], bool | None]) -> int:
|
||||
"""
|
||||
Create a pipe for use by a subprocess or thread to trigger a callback
|
||||
in the process/thread running the main loop.
|
||||
|
||||
:param callback: function taking one parameter to call from within the process/thread running the main loop
|
||||
:type callback: callable
|
||||
|
||||
This method returns a file descriptor attached to the write end of a pipe.
|
||||
The read end of the pipe is added to the list of files :attr:`event_loop` is watching.
|
||||
When data is written to the pipe the callback function will be called
|
||||
and passed a single value containing data read from the pipe.
|
||||
|
||||
This method may be used any time you want to update widgets from another thread or subprocess.
|
||||
|
||||
Data may be written to the returned file descriptor with ``os.write(fd, data)``.
|
||||
Ensure that data is less than 512 bytes (or 4K on Linux)
|
||||
so that the callback will be triggered just once with the complete value of data passed in.
|
||||
|
||||
If the callback returns ``False`` then the watch will be removed from :attr:`event_loop`
|
||||
and the read end of the pipe will be closed.
|
||||
You are responsible for closing the write end of the pipe with ``os.close(fd)``.
|
||||
"""
|
||||
import fcntl
|
||||
|
||||
pipe_rd, pipe_wr = os.pipe()
|
||||
fcntl.fcntl(pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK)
|
||||
watch_handle = None
|
||||
|
||||
def cb() -> None:
|
||||
data = os.read(pipe_rd, PIPE_BUFFER_READ_SIZE)
|
||||
if callback(data) is False:
|
||||
self.event_loop.remove_watch_file(watch_handle)
|
||||
os.close(pipe_rd)
|
||||
|
||||
watch_handle = self.event_loop.watch_file(pipe_rd, cb)
|
||||
self._watch_pipes[pipe_wr] = (watch_handle, pipe_rd)
|
||||
return pipe_wr
|
||||
|
||||
def remove_watch_pipe(self, write_fd: int) -> bool:
|
||||
"""
|
||||
Close the read end of the pipe and remove the watch created by :meth:`watch_pipe`.
|
||||
|
||||
..note:: You are responsible for closing the write end of the pipe.
|
||||
|
||||
Returns ``True`` if the watch pipe exists, ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
watch_handle, pipe_rd = self._watch_pipes.pop(write_fd)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if not self.event_loop.remove_watch_file(watch_handle):
|
||||
return False
|
||||
os.close(pipe_rd)
|
||||
return True
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]):
|
||||
"""
|
||||
Call *callback* when *fd* has some data to read. No parameters are
|
||||
passed to callback.
|
||||
|
||||
Returns a handle that may be passed to :meth:`remove_watch_file`.
|
||||
"""
|
||||
self.logger.debug(f"Setting watch file descriptor {fd!r} with {callback!r}")
|
||||
return self.event_loop.watch_file(fd, callback)
|
||||
|
||||
def remove_watch_file(self, handle):
|
||||
"""
|
||||
Remove a watch file. Returns ``True`` if the watch file
|
||||
exists, ``False`` otherwise.
|
||||
"""
|
||||
return self.event_loop.remove_watch_file(handle)
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the main loop handling input events and updating the screen. The
|
||||
loop will continue until an :exc:`ExitMainLoop` exception is raised.
|
||||
|
||||
If you would prefer to manage the event loop yourself, don't use this
|
||||
method. Instead, call :meth:`start` before starting the event loop,
|
||||
and :meth:`stop` once it's finished.
|
||||
"""
|
||||
with suppress(ExitMainLoop):
|
||||
self._run()
|
||||
|
||||
def _test_run(self):
|
||||
"""
|
||||
>>> w = _refl("widget") # _refl prints out function calls
|
||||
>>> w.render_rval = "fake canvas" # *_rval is used for return values
|
||||
>>> scr = _refl("screen")
|
||||
>>> scr.get_input_descriptors_rval = [42]
|
||||
>>> scr.get_cols_rows_rval = (20, 10)
|
||||
>>> scr.started = True
|
||||
>>> scr._urwid_signals = {}
|
||||
>>> evl = _refl("event_loop")
|
||||
>>> evl.enter_idle_rval = 1
|
||||
>>> evl.watch_file_rval = 2
|
||||
>>> ml = MainLoop(w, [], scr, event_loop=evl)
|
||||
>>> ml.run() # doctest:+ELLIPSIS
|
||||
screen.start()
|
||||
screen.set_mouse_tracking()
|
||||
screen.unhook_event_loop(...)
|
||||
screen.hook_event_loop(...)
|
||||
event_loop.enter_idle(<bound method MainLoop.entering_idle...>)
|
||||
event_loop.run()
|
||||
event_loop.remove_enter_idle(1)
|
||||
screen.unhook_event_loop(...)
|
||||
screen.stop()
|
||||
>>> ml.draw_screen() # doctest:+ELLIPSIS
|
||||
screen.get_cols_rows()
|
||||
widget.render((20, 10), focus=True)
|
||||
screen.draw_screen((20, 10), 'fake canvas')
|
||||
"""
|
||||
|
||||
def start(self) -> StoppingContext:
|
||||
"""
|
||||
Sets up the main loop, hooking into the event loop where necessary.
|
||||
Starts the :attr:`screen` if it hasn't already been started.
|
||||
|
||||
If you want to control starting and stopping the event loop yourself,
|
||||
you should call this method before starting, and call `stop` once the
|
||||
loop has finished. You may also use this method as a context manager,
|
||||
which will stop the loop automatically at the end of the block:
|
||||
|
||||
with main_loop.start():
|
||||
...
|
||||
|
||||
Note that some event loop implementations don't handle exceptions
|
||||
specially if you manage the event loop yourself. In particular, the
|
||||
Twisted and asyncio loops won't stop automatically when
|
||||
:exc:`ExitMainLoop` (or anything else) is raised.
|
||||
"""
|
||||
|
||||
self.logger.debug(f"Starting event loop {self.event_loop.__class__.__name__!r} to manage display.")
|
||||
|
||||
self.screen.start()
|
||||
|
||||
if self.handle_mouse:
|
||||
self.screen.set_mouse_tracking()
|
||||
|
||||
if not hasattr(self.screen, "hook_event_loop"):
|
||||
raise CantUseExternalLoop(f"Screen {self.screen!r} doesn't support external event loops")
|
||||
|
||||
with suppress(NameError):
|
||||
signals.connect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)
|
||||
|
||||
# watch our input descriptors
|
||||
self._reset_input_descriptors()
|
||||
self.idle_handle = self.event_loop.enter_idle(self.entering_idle)
|
||||
|
||||
# the screen is redrawn automatically after input and alarms,
|
||||
# however, there can be none of those at the start,
|
||||
# so draw the initial screen here unconditionally
|
||||
self.event_loop.alarm(0, self.entering_idle)
|
||||
|
||||
return StoppingContext(self)
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Cleans up any hooks added to the event loop. Only call this if you're
|
||||
managing the event loop yourself, after the loop stops.
|
||||
"""
|
||||
|
||||
self.event_loop.remove_enter_idle(self.idle_handle)
|
||||
del self.idle_handle
|
||||
signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)
|
||||
self.screen.unhook_event_loop(self.event_loop)
|
||||
|
||||
self.screen.stop()
|
||||
|
||||
def _reset_input_descriptors(self) -> None:
|
||||
self.screen.unhook_event_loop(self.event_loop)
|
||||
self.screen.hook_event_loop(self.event_loop, self._update)
|
||||
|
||||
def _run(self) -> None:
|
||||
try:
|
||||
self.start()
|
||||
except CantUseExternalLoop:
|
||||
try:
|
||||
self._run_screen_event_loop()
|
||||
return
|
||||
finally:
|
||||
self.screen.stop()
|
||||
|
||||
try:
|
||||
self.event_loop.run()
|
||||
except:
|
||||
self.screen.stop() # clean up screen control
|
||||
raise
|
||||
self.stop()
|
||||
|
||||
def _update(self, keys: list[str], raw: list[int]) -> None:
|
||||
"""
|
||||
>>> w = _refl("widget")
|
||||
>>> w.selectable_rval = True
|
||||
>>> w.mouse_event_rval = True
|
||||
>>> scr = _refl("screen")
|
||||
>>> scr.get_cols_rows_rval = (15, 5)
|
||||
>>> evl = _refl("event_loop")
|
||||
>>> ml = MainLoop(w, [], scr, event_loop=evl)
|
||||
>>> ml._input_timeout = "old timeout"
|
||||
>>> ml._update(['y'], [121]) # doctest:+ELLIPSIS
|
||||
screen.get_cols_rows()
|
||||
widget.selectable()
|
||||
widget.keypress((15, 5), 'y')
|
||||
>>> ml._update([("mouse press", 1, 5, 4)], [])
|
||||
widget.mouse_event((15, 5), 'mouse press', 1, 5, 4, focus=True)
|
||||
>>> ml._update([], [])
|
||||
"""
|
||||
if keys := self.input_filter(keys, raw):
|
||||
self.process_input(keys)
|
||||
if "window resize" in keys:
|
||||
self.screen_size = None
|
||||
|
||||
def _run_screen_event_loop(self) -> None:
|
||||
"""
|
||||
This method is used when the screen does not support using external event loops.
|
||||
|
||||
The alarms stored in the SelectEventLoop in :attr:`event_loop` are modified by this method.
|
||||
"""
|
||||
# pylint: disable=protected-access # special case for alarms handling
|
||||
self.logger.debug(f"Starting screen {self.screen!r} event loop")
|
||||
|
||||
next_alarm = None
|
||||
|
||||
while True:
|
||||
self.draw_screen()
|
||||
|
||||
if not next_alarm and self.event_loop._alarms:
|
||||
next_alarm = heapq.heappop(self.event_loop._alarms)
|
||||
|
||||
keys: list[str] = []
|
||||
raw: list[int] = []
|
||||
while not keys:
|
||||
if next_alarm:
|
||||
sec = max(0.0, next_alarm[0] - time.time())
|
||||
self.screen.set_input_timeouts(sec)
|
||||
else:
|
||||
self.screen.set_input_timeouts(None)
|
||||
keys, raw = self.screen.get_input(True)
|
||||
if not keys and next_alarm and next_alarm[0] - time.time() <= 0:
|
||||
break
|
||||
|
||||
if keys := self.input_filter(keys, raw):
|
||||
self.process_input(keys)
|
||||
|
||||
while next_alarm:
|
||||
if (next_alarm[0] - time.time()) > 0:
|
||||
break
|
||||
_tm, _tie_break, callback = next_alarm
|
||||
callback()
|
||||
|
||||
if self.event_loop._alarms:
|
||||
next_alarm = heapq.heappop(self.event_loop._alarms)
|
||||
else:
|
||||
next_alarm = None
|
||||
|
||||
if "window resize" in keys:
|
||||
self.screen_size = None
|
||||
|
||||
def _test_run_screen_event_loop(self):
|
||||
"""
|
||||
>>> w = _refl("widget")
|
||||
>>> scr = _refl("screen")
|
||||
>>> scr.get_cols_rows_rval = (10, 5)
|
||||
>>> scr.get_input_rval = [], []
|
||||
>>> ml = MainLoop(w, screen=scr)
|
||||
>>> def stop_now(loop, data):
|
||||
... raise ExitMainLoop()
|
||||
>>> handle = ml.set_alarm_in(0, stop_now)
|
||||
>>> try:
|
||||
... ml._run_screen_event_loop()
|
||||
... except ExitMainLoop:
|
||||
... pass
|
||||
screen.get_cols_rows()
|
||||
widget.render((10, 5), focus=True)
|
||||
screen.draw_screen((10, 5), None)
|
||||
screen.set_input_timeouts(0.0)
|
||||
screen.get_input(True)
|
||||
"""
|
||||
|
||||
def process_input(self, keys: Iterable[str | tuple[str, int, int, int]]) -> bool:
|
||||
"""
|
||||
This method will pass keyboard input and mouse events to :attr:`widget`.
|
||||
This method is called automatically from the :meth:`run` method when
|
||||
there is input, but may also be called to simulate input from the user.
|
||||
|
||||
*keys* is a list of input returned from :attr:`screen`'s get_input()
|
||||
or get_input_nonblocking() methods.
|
||||
|
||||
Returns ``True`` if any key was handled by a widget or the
|
||||
:meth:`unhandled_input` method.
|
||||
"""
|
||||
self.logger.debug(f"Processing input: keys={keys!r}")
|
||||
if not self.screen_size:
|
||||
self.screen_size = self.screen.get_cols_rows()
|
||||
|
||||
something_handled = False
|
||||
|
||||
for key in keys:
|
||||
if key == "window resize":
|
||||
continue
|
||||
|
||||
if isinstance(key, str):
|
||||
if self._topmost_widget.selectable():
|
||||
if handled_key := self._topmost_widget.keypress(self.screen_size, key):
|
||||
key = handled_key # noqa: PLW2901
|
||||
|
||||
else:
|
||||
something_handled = True
|
||||
continue
|
||||
|
||||
elif is_mouse_event(key):
|
||||
event, button, col, row = key
|
||||
if hasattr(self._topmost_widget, "mouse_event") and self._topmost_widget.mouse_event(
|
||||
self.screen_size,
|
||||
event,
|
||||
button,
|
||||
col,
|
||||
row,
|
||||
focus=True,
|
||||
):
|
||||
something_handled = True
|
||||
continue
|
||||
|
||||
else:
|
||||
raise TypeError(f"{key!r} is not str | tuple[str, int, int, int]")
|
||||
|
||||
if key:
|
||||
if command_map[key] == Command.REDRAW_SCREEN:
|
||||
self.screen.clear()
|
||||
something_handled = True
|
||||
else:
|
||||
something_handled |= bool(self.unhandled_input(key))
|
||||
else:
|
||||
something_handled = True
|
||||
|
||||
return something_handled
|
||||
|
||||
def _test_process_input(self):
|
||||
"""
|
||||
>>> w = _refl("widget")
|
||||
>>> w.selectable_rval = True
|
||||
>>> scr = _refl("screen")
|
||||
>>> scr.get_cols_rows_rval = (10, 5)
|
||||
>>> ml = MainLoop(w, [], scr)
|
||||
>>> ml.process_input(['enter', ('mouse drag', 1, 14, 20)])
|
||||
screen.get_cols_rows()
|
||||
widget.selectable()
|
||||
widget.keypress((10, 5), 'enter')
|
||||
widget.mouse_event((10, 5), 'mouse drag', 1, 14, 20, focus=True)
|
||||
True
|
||||
"""
|
||||
|
||||
def input_filter(self, keys: list[str], raw: list[int]) -> list[str]:
|
||||
"""
|
||||
This function is passed each all the input events and raw keystroke
|
||||
values. These values are passed to the *input_filter* function
|
||||
passed to the constructor. That function must return a list of keys to
|
||||
be passed to the widgets to handle. If no *input_filter* was
|
||||
defined this implementation will return all the input events.
|
||||
"""
|
||||
if self._input_filter:
|
||||
return self._input_filter(keys, raw)
|
||||
return keys
|
||||
|
||||
def unhandled_input(self, data: str | tuple[str, int, int, int]) -> bool | None:
|
||||
"""
|
||||
This function is called with any input that was not handled by the
|
||||
widgets, and calls the *unhandled_input* function passed to the
|
||||
constructor. If no *unhandled_input* was defined then the input
|
||||
will be ignored.
|
||||
|
||||
*input* is the keyboard or mouse input.
|
||||
|
||||
The *unhandled_input* function should return ``True`` if it handled
|
||||
the input.
|
||||
"""
|
||||
if self._unhandled_input:
|
||||
return self._unhandled_input(data)
|
||||
return False
|
||||
|
||||
def entering_idle(self) -> None:
|
||||
"""
|
||||
This method is called whenever the event loop is about to enter the
|
||||
idle state. :meth:`draw_screen` is called here to update the
|
||||
screen when anything has changed.
|
||||
"""
|
||||
if self.screen.started:
|
||||
self.draw_screen()
|
||||
else:
|
||||
self.logger.debug(f"No redrawing screen: {self.screen!r} is not started.")
|
||||
|
||||
def draw_screen(self) -> None:
|
||||
"""
|
||||
Render the widgets and paint the screen. This method is called
|
||||
automatically from :meth:`entering_idle`.
|
||||
|
||||
If you modify the widgets displayed outside of handling input or
|
||||
responding to an alarm you will need to call this method yourself
|
||||
to repaint the screen.
|
||||
"""
|
||||
if not self.screen_size:
|
||||
self.screen_size = self.screen.get_cols_rows()
|
||||
self.logger.debug(f"Screen size recalculated: {self.screen_size!r}")
|
||||
|
||||
canvas = self._topmost_widget.render(self.screen_size, focus=True)
|
||||
self.screen.draw_screen(self.screen_size, canvas)
|
||||
|
||||
|
||||
def _refl(name: str, rval=None, loop_exit=False):
|
||||
"""
|
||||
This function is used to test the main loop classes.
|
||||
|
||||
>>> scr = _refl("screen")
|
||||
>>> scr.function("argument")
|
||||
screen.function('argument')
|
||||
>>> scr.callme(when="now")
|
||||
screen.callme(when='now')
|
||||
>>> scr.want_something_rval = 42
|
||||
>>> x = scr.want_something()
|
||||
screen.want_something()
|
||||
>>> x
|
||||
42
|
||||
|
||||
"""
|
||||
|
||||
class Reflect:
|
||||
def __init__(self, name: str, rval=None):
|
||||
self._name = name
|
||||
self._rval = rval
|
||||
|
||||
def __call__(self, *argl, **argd):
|
||||
args = ", ".join([repr(a) for a in argl])
|
||||
if args and argd:
|
||||
args = f"{args}, "
|
||||
args += ", ".join([f"{k}={v!r}" for k, v in argd.items()])
|
||||
print(f"{self._name}({args})")
|
||||
if loop_exit:
|
||||
raise ExitMainLoop()
|
||||
return self._rval
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr.endswith("_rval"):
|
||||
raise AttributeError()
|
||||
# print(self._name+"."+attr)
|
||||
if hasattr(self, f"{attr}_rval"):
|
||||
return Reflect(f"{self._name}.{attr}", getattr(self, f"{attr}_rval"))
|
||||
return Reflect(f"{self._name}.{attr}")
|
||||
|
||||
return Reflect(name)
|
||||
|
||||
|
||||
def _test():
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_test()
|
||||
230
venv/Lib/site-packages/urwid/event_loop/select_loop.py
Normal file
230
venv/Lib/site-packages/urwid/event_loop/select_loop.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Select based urwid EventLoop implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import heapq
|
||||
import logging
|
||||
import selectors
|
||||
import time
|
||||
import typing
|
||||
from contextlib import suppress
|
||||
from itertools import count
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterator
|
||||
from concurrent.futures import Executor, Future
|
||||
|
||||
from typing_extensions import Literal, ParamSpec
|
||||
|
||||
_T = typing.TypeVar("_T")
|
||||
_Spec = ParamSpec("_Spec")
|
||||
|
||||
__all__ = ("SelectEventLoop",)
|
||||
|
||||
|
||||
class SelectEventLoop(EventLoop):
|
||||
"""
|
||||
Event loop based on :func:`selectors.DefaultSelector.select`
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = []
|
||||
self._watch_files: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._idle_handle: int = 0
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._tie_break: Iterator[int] = count()
|
||||
self._did_something: bool = False
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> Future[_T]:
|
||||
"""Run callable in executor.
|
||||
|
||||
:param executor: Executor to use for running the function
|
||||
:type executor: concurrent.futures.Executor
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: positional arguments to function
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
:rtype: concurrent.futures.Future
|
||||
"""
|
||||
return executor.submit(func, *args, **kwargs)
|
||||
|
||||
def alarm(
|
||||
self,
|
||||
seconds: float,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> tuple[float, int, Callable[[], typing.Any]]:
|
||||
"""
|
||||
Call callback() a given time from now. No parameters are
|
||||
passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_alarm()
|
||||
|
||||
seconds -- floating point time to wait before calling callback
|
||||
callback -- function to call from event loop
|
||||
"""
|
||||
tm = time.time() + seconds
|
||||
handle = (tm, next(self._tie_break), callback)
|
||||
heapq.heappush(self._alarms, handle)
|
||||
return handle
|
||||
|
||||
def remove_alarm(self, handle: tuple[float, int, Callable[[], typing.Any]]) -> bool:
|
||||
"""
|
||||
Remove an alarm.
|
||||
|
||||
Returns True if the alarm exists, False otherwise
|
||||
"""
|
||||
try:
|
||||
self._alarms.remove(handle)
|
||||
heapq.heapify(self._alarms)
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Call callback() when fd has some data to read. No parameters
|
||||
are passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_watch_file()
|
||||
|
||||
fd -- file descriptor to watch for input
|
||||
callback -- function to call when input is available
|
||||
"""
|
||||
self._watch_files[fd] = callback
|
||||
return fd
|
||||
|
||||
def remove_watch_file(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an input file.
|
||||
|
||||
Returns True if the input file exists, False otherwise
|
||||
"""
|
||||
if handle in self._watch_files:
|
||||
del self._watch_files[handle]
|
||||
return True
|
||||
return False
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
Returns a handle that may be passed to remove_idle()
|
||||
"""
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _entering_idle(self) -> None:
|
||||
"""
|
||||
Call all the registered idle callbacks.
|
||||
"""
|
||||
for callback in self._idle_callbacks.values():
|
||||
callback()
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the event loop. Exit the loop when any callback raises
|
||||
an exception. If ExitMainLoop is raised, exit cleanly.
|
||||
"""
|
||||
with contextlib.suppress(ExitMainLoop):
|
||||
self._did_something = True
|
||||
while True:
|
||||
with suppress(InterruptedError):
|
||||
self._loop()
|
||||
|
||||
def _loop(self) -> None:
|
||||
"""
|
||||
A single iteration of the event loop
|
||||
"""
|
||||
tm: float | Literal["idle"] | None = None
|
||||
|
||||
with selectors.DefaultSelector() as selector:
|
||||
for fd, callback in self._watch_files.items():
|
||||
selector.register(fd, selectors.EVENT_READ, callback)
|
||||
|
||||
if self._alarms or self._did_something:
|
||||
timeout = 0.0
|
||||
|
||||
if self._alarms:
|
||||
timeout_ = self._alarms[0][0]
|
||||
tm = timeout_
|
||||
timeout = max(timeout, timeout_ - time.time())
|
||||
|
||||
if self._did_something and (not self._alarms or (self._alarms and timeout > 0)):
|
||||
timeout = 0.0
|
||||
tm = "idle"
|
||||
|
||||
self.logger.debug(f"Waiting for input: timeout={timeout!r}")
|
||||
ready = [event for event, _ in selector.select(timeout)]
|
||||
|
||||
elif self._watch_files:
|
||||
self.logger.debug("Waiting for input: timeout")
|
||||
ready = [event for event, _ in selector.select()]
|
||||
else:
|
||||
ready = []
|
||||
|
||||
if not ready:
|
||||
if tm == "idle":
|
||||
self.logger.debug("No input, entering IDLE")
|
||||
self._entering_idle()
|
||||
self._did_something = False
|
||||
elif tm is not None:
|
||||
# must have been a timeout
|
||||
tm, _tie_break, alarm_callback = heapq.heappop(self._alarms)
|
||||
self.logger.debug(f"No input in timeout, calling scheduled {alarm_callback!r}")
|
||||
alarm_callback()
|
||||
self._did_something = True
|
||||
|
||||
self.logger.debug("Processing input")
|
||||
for record in ready:
|
||||
record.data()
|
||||
self._did_something = True
|
||||
213
venv/Lib/site-packages/urwid/event_loop/tornado_loop.py
Normal file
213
venv/Lib/site-packages/urwid/event_loop/tornado_loop.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Tornado IOLoop based urwid EventLoop implementation.
|
||||
|
||||
Tornado library is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import logging
|
||||
import typing
|
||||
from contextlib import suppress
|
||||
|
||||
from tornado import ioloop
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor
|
||||
|
||||
from typing_extensions import Literal, ParamSpec
|
||||
|
||||
_Spec = ParamSpec("_Spec")
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
__all__ = ("TornadoEventLoop",)
|
||||
|
||||
|
||||
class TornadoEventLoop(EventLoop):
|
||||
"""This is an Urwid-specific event loop to plug into its MainLoop.
|
||||
It acts as an adaptor for Tornado's IOLoop which does all
|
||||
heavy lifting except idle-callbacks.
|
||||
"""
|
||||
|
||||
def __init__(self, loop: ioloop.IOLoop | None = None) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
if loop:
|
||||
self._loop: ioloop.IOLoop = loop
|
||||
else:
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
||||
|
||||
self._loop = ioloop.IOLoop.current()
|
||||
|
||||
self._pending_alarms: dict[object, int] = {}
|
||||
self._watch_handles: dict[int, int] = {} # {<watch_handle> : <file_descriptor>}
|
||||
self._max_watch_handle: int = 0
|
||||
self._exc: BaseException | None = None
|
||||
|
||||
self._idle_asyncio_handle: object | None = None
|
||||
self._idle_handle: int = 0
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
|
||||
def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T]:
|
||||
"""
|
||||
Wrap the callback to also call _entering_idle.
|
||||
"""
|
||||
|
||||
@functools.wraps(callback)
|
||||
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T:
|
||||
if not self._idle_asyncio_handle:
|
||||
self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
|
||||
return callback(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
def _entering_idle(self) -> None:
|
||||
"""
|
||||
Call all the registered idle callbacks.
|
||||
"""
|
||||
try:
|
||||
for callback in self._idle_callbacks.values():
|
||||
callback()
|
||||
finally:
|
||||
self._idle_asyncio_handle = None
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> asyncio.Future[_T]:
|
||||
"""Run callable in executor.
|
||||
|
||||
:param executor: Executor to use for running the function
|
||||
:type executor: concurrent.futures.Executor
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: arguments to function (positional only)
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function (keyword only)
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
:rtype: asyncio.Future
|
||||
"""
|
||||
return self._loop.run_in_executor(executor, functools.partial(func, *args, **kwargs))
|
||||
|
||||
def alarm(self, seconds: float, callback: Callable[[], typing.Any]):
|
||||
@self._also_call_idle
|
||||
@functools.wraps(callback)
|
||||
def wrapped() -> None:
|
||||
with suppress(KeyError):
|
||||
del self._pending_alarms[handle]
|
||||
|
||||
self.handle_exit(callback)()
|
||||
|
||||
handle = self._loop.add_timeout(self._loop.time() + seconds, wrapped)
|
||||
self._pending_alarms[handle] = 1
|
||||
return handle
|
||||
|
||||
def remove_alarm(self, handle: object) -> bool:
|
||||
self._loop.remove_timeout(handle)
|
||||
try:
|
||||
del self._pending_alarms[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], _T]) -> int:
|
||||
@self._also_call_idle
|
||||
def handler(_fd: int, _events: int) -> None:
|
||||
self.handle_exit(callback)()
|
||||
|
||||
self._loop.add_handler(fd, handler, ioloop.IOLoop.READ)
|
||||
self._max_watch_handle += 1
|
||||
handle = self._max_watch_handle
|
||||
self._watch_handles[handle] = fd
|
||||
return handle
|
||||
|
||||
def remove_watch_file(self, handle: int) -> bool:
|
||||
if (fd := self._watch_handles.pop(handle, None)) is not None:
|
||||
self._loop.remove_handler(fd)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
Returns a handle that may be passed to remove_idle()
|
||||
"""
|
||||
# XXX there's no such thing as "idle" in most event loops; this fakes
|
||||
# it by adding extra callback to the timer and file watch callbacks.
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def handle_exit(self, f: Callable[_Spec, _T]) -> Callable[_Spec, _T | Literal[False]]:
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | Literal[False]:
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ExitMainLoop:
|
||||
pass # handled later
|
||||
except Exception as exc:
|
||||
self._exc = exc
|
||||
|
||||
if self._idle_asyncio_handle:
|
||||
# clean it up to prevent old callbacks
|
||||
# from messing things up if loop is restarted
|
||||
self._loop.remove_timeout(self._idle_asyncio_handle)
|
||||
self._idle_asyncio_handle = None
|
||||
|
||||
self._loop.stop()
|
||||
return False
|
||||
|
||||
return wrapper
|
||||
|
||||
def run(self) -> None:
|
||||
self._loop.start()
|
||||
if self._exc:
|
||||
exc, self._exc = self._exc, None
|
||||
raise exc.with_traceback(exc.__traceback__)
|
||||
309
venv/Lib/site-packages/urwid/event_loop/trio_loop.py
Normal file
309
venv/Lib/site-packages/urwid/event_loop/trio_loop.py
Normal file
@@ -0,0 +1,309 @@
|
||||
# Urwid main loop code using Python-3.5 features (Trio, Curio, etc)
|
||||
# Copyright (C) 2018 Toshio Kuratomi
|
||||
# Copyright (C) 2019 Tamas Nepusz
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Trio Runner based urwid EventLoop implementation.
|
||||
|
||||
Trio library is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import trio
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup # pylint: disable=redefined-builtin # backport
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import io
|
||||
from collections.abc import Awaitable, Callable, Hashable, Mapping
|
||||
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
_Spec = ParamSpec("_Spec")
|
||||
|
||||
__all__ = ("TrioEventLoop",)
|
||||
|
||||
|
||||
class _TrioIdleCallbackInstrument(trio.abc.Instrument):
|
||||
"""IDLE callbacks emulation helper."""
|
||||
|
||||
__slots__ = ("idle_callbacks",)
|
||||
|
||||
def __init__(self, idle_callbacks: Mapping[Hashable, Callable[[], typing.Any]]):
|
||||
self.idle_callbacks = idle_callbacks
|
||||
|
||||
def before_io_wait(self, timeout: float) -> None:
|
||||
if timeout > 0:
|
||||
for idle_callback in self.idle_callbacks.values():
|
||||
idle_callback()
|
||||
|
||||
|
||||
class TrioEventLoop(EventLoop):
|
||||
"""
|
||||
Event loop based on the ``trio`` module.
|
||||
|
||||
``trio`` is an async library for Python 3.5 and later.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Constructor."""
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
|
||||
self._idle_handle = 0
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._pending_tasks: list[tuple[Callable[_Spec, Awaitable], trio.CancelScope, _Spec.args]] = []
|
||||
|
||||
self._nursery: trio.Nursery | None = None
|
||||
|
||||
self._sleep = trio.sleep
|
||||
self._wait_readable = trio.lowlevel.wait_readable
|
||||
|
||||
def alarm(
|
||||
self,
|
||||
seconds: float,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> trio.CancelScope:
|
||||
"""Calls `callback()` a given time from now.
|
||||
|
||||
:param seconds: time in seconds to wait before calling the callback
|
||||
:type seconds: float
|
||||
:param callback: function to call from the event loop
|
||||
:type callback: Callable[[], typing.Any]
|
||||
:return: a handle that may be passed to `remove_alarm()`
|
||||
:rtype: trio.CancelScope
|
||||
|
||||
No parameters are passed to the callback.
|
||||
"""
|
||||
return self._start_task(self._alarm_task, seconds, callback)
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""Calls `callback()` when the event loop enters the idle state.
|
||||
|
||||
There is no such thing as being idle in a Trio event loop so we
|
||||
simulate it by repeatedly calling `callback()` with a short delay.
|
||||
"""
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def remove_alarm(self, handle: trio.CancelScope) -> bool:
|
||||
"""Removes an alarm.
|
||||
|
||||
Parameters:
|
||||
handle: the handle of the alarm to remove
|
||||
"""
|
||||
return self._cancel_scope(handle)
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""Removes an idle callback.
|
||||
|
||||
Parameters:
|
||||
handle: the handle of the idle callback to remove
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def remove_watch_file(self, handle: trio.CancelScope) -> bool:
|
||||
"""Removes a file descriptor being watched for input.
|
||||
|
||||
Parameters:
|
||||
handle: the handle of the file descriptor callback to remove
|
||||
|
||||
Returns:
|
||||
True if the file descriptor was watched, False otherwise
|
||||
"""
|
||||
return self._cancel_scope(handle)
|
||||
|
||||
def _cancel_scope(self, scope: trio.CancelScope) -> bool:
|
||||
"""Cancels the given Trio cancellation scope.
|
||||
|
||||
Returns:
|
||||
True if the scope was cancelled, False if it was cancelled already
|
||||
before invoking this function
|
||||
"""
|
||||
existed = not scope.cancel_called
|
||||
scope.cancel()
|
||||
return existed
|
||||
|
||||
def run(self) -> None:
|
||||
"""Starts the event loop. Exits the loop when any callback raises an
|
||||
exception. If ExitMainLoop is raised, exits cleanly.
|
||||
"""
|
||||
|
||||
emulate_idle_callbacks = _TrioIdleCallbackInstrument(self._idle_callbacks)
|
||||
|
||||
try:
|
||||
trio.run(self._main_task, instruments=[emulate_idle_callbacks])
|
||||
except BaseException as exc:
|
||||
self._handle_main_loop_exception(exc)
|
||||
|
||||
async def run_async(self) -> None:
|
||||
"""Starts the main loop and blocks asynchronously until the main loop exits.
|
||||
|
||||
This allows one to embed an urwid app in a Trio app even if the Trio event loop is already running.
|
||||
Example::
|
||||
|
||||
with trio.open_nursery() as nursery:
|
||||
event_loop = urwid.TrioEventLoop()
|
||||
|
||||
# [...launch other async tasks in the nursery...]
|
||||
|
||||
loop = urwid.MainLoop(widget, event_loop=event_loop)
|
||||
with loop.start():
|
||||
await event_loop.run_async()
|
||||
|
||||
nursery.cancel_scope.cancel()
|
||||
"""
|
||||
|
||||
emulate_idle_callbacks = _TrioIdleCallbackInstrument(self._idle_callbacks)
|
||||
|
||||
try:
|
||||
trio.lowlevel.add_instrument(emulate_idle_callbacks)
|
||||
try:
|
||||
await self._main_task()
|
||||
finally:
|
||||
trio.lowlevel.remove_instrument(emulate_idle_callbacks)
|
||||
except BaseException as exc:
|
||||
self._handle_main_loop_exception(exc)
|
||||
|
||||
def watch_file(
|
||||
self,
|
||||
fd: int | io.IOBase,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> trio.CancelScope:
|
||||
"""Calls `callback()` when the given file descriptor has some data
|
||||
to read. No parameters are passed to the callback.
|
||||
|
||||
Parameters:
|
||||
fd: file descriptor to watch for input
|
||||
callback: function to call when some input is available
|
||||
|
||||
Returns:
|
||||
a handle that may be passed to `remove_watch_file()`
|
||||
"""
|
||||
return self._start_task(self._watch_task, fd, callback)
|
||||
|
||||
async def _alarm_task(
|
||||
self,
|
||||
scope: trio.CancelScope,
|
||||
seconds: float,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> None:
|
||||
"""Asynchronous task that sleeps for a given number of seconds and then
|
||||
calls the given callback.
|
||||
|
||||
Parameters:
|
||||
scope: the cancellation scope that can be used to cancel the task
|
||||
seconds: the number of seconds to wait
|
||||
callback: the callback to call
|
||||
"""
|
||||
with scope:
|
||||
await self._sleep(seconds)
|
||||
callback()
|
||||
|
||||
def _handle_main_loop_exception(self, exc: BaseException) -> None:
|
||||
"""Handles exceptions raised from the main loop, catching ExitMainLoop
|
||||
instead of letting it propagate through.
|
||||
|
||||
Note that since Trio may collect multiple exceptions from tasks into an ExceptionGroup,
|
||||
we cannot simply use a try..catch clause, we need a helper function like this.
|
||||
"""
|
||||
self._idle_callbacks.clear()
|
||||
if isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
|
||||
exc = exc.exceptions[0]
|
||||
|
||||
if isinstance(exc, ExitMainLoop):
|
||||
return
|
||||
|
||||
raise exc.with_traceback(exc.__traceback__) from None
|
||||
|
||||
async def _main_task(self) -> None:
|
||||
"""Main Trio task that opens a nursery and then sleeps until the user
|
||||
exits the app by raising ExitMainLoop.
|
||||
"""
|
||||
try:
|
||||
async with trio.open_nursery() as self._nursery:
|
||||
self._schedule_pending_tasks()
|
||||
await trio.sleep_forever()
|
||||
finally:
|
||||
self._nursery = None
|
||||
|
||||
def _schedule_pending_tasks(self) -> None:
|
||||
"""Schedules all pending asynchronous tasks that were created before
|
||||
the nursery to be executed on the nursery soon.
|
||||
"""
|
||||
for task, scope, args in self._pending_tasks:
|
||||
self._nursery.start_soon(task, scope, *args)
|
||||
self._pending_tasks.clear()
|
||||
|
||||
def _start_task(
|
||||
self,
|
||||
task: Callable[Concatenate[trio.CancelScope, _Spec], Awaitable],
|
||||
*args: _Spec.args,
|
||||
) -> trio.CancelScope:
|
||||
"""Starts an asynchronous task in the Trio nursery managed by the
|
||||
main loop. If the nursery has not started yet, store a reference to
|
||||
the task and the arguments so we can start the task when the nursery
|
||||
is open.
|
||||
|
||||
Parameters:
|
||||
task: a Trio task to run
|
||||
|
||||
Returns:
|
||||
a cancellation scope for the Trio task
|
||||
"""
|
||||
scope = trio.CancelScope()
|
||||
if self._nursery:
|
||||
self._nursery.start_soon(task, scope, *args)
|
||||
else:
|
||||
self._pending_tasks.append((task, scope, args))
|
||||
return scope
|
||||
|
||||
async def _watch_task(
|
||||
self,
|
||||
scope: trio.CancelScope,
|
||||
fd: int | io.IOBase,
|
||||
callback: Callable[[], typing.Any],
|
||||
) -> None:
|
||||
"""Asynchronous task that watches the given file descriptor and calls
|
||||
the given callback whenever the file descriptor becomes readable.
|
||||
|
||||
Parameters:
|
||||
scope: the cancellation scope that can be used to cancel the task
|
||||
fd: the file descriptor to watch
|
||||
callback: the callback to call
|
||||
"""
|
||||
with scope:
|
||||
# We check for the scope being cancelled before calling
|
||||
# wait_readable because if callback cancels the scope, fd might be
|
||||
# closed and calling wait_readable with a closed fd does not work.
|
||||
while not scope.cancel_called:
|
||||
await self._wait_readable(fd)
|
||||
callback()
|
||||
270
venv/Lib/site-packages/urwid/event_loop/twisted_loop.py
Normal file
270
venv/Lib/site-packages/urwid/event_loop/twisted_loop.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# Urwid main loop code
|
||||
# Copyright (C) 2004-2012 Ian Ward
|
||||
# Copyright (C) 2008 Walter Mundt
|
||||
# Copyright (C) 2009 Andrew Psaltis
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""Twisted Reactor based urwid EventLoop implementation.
|
||||
|
||||
Twisted library is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import sys
|
||||
import typing
|
||||
|
||||
from twisted.internet.abstract import FileDescriptor
|
||||
from twisted.internet.error import AlreadyCalled, AlreadyCancelled
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor, Future
|
||||
|
||||
from twisted.internet.base import DelayedCall, ReactorBase
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
_Spec = ParamSpec("_Spec")
|
||||
_T = typing.TypeVar("_T")
|
||||
|
||||
__all__ = ("TwistedEventLoop",)
|
||||
|
||||
|
||||
class _TwistedInputDescriptor(FileDescriptor):
|
||||
def __init__(self, reactor: ReactorBase, fd: int, cb: Callable[[], typing.Any]) -> None:
|
||||
self._fileno = fd
|
||||
self.cb = cb
|
||||
super().__init__(reactor) # ReactorBase implement full API as required in interfaces
|
||||
|
||||
def fileno(self) -> int:
|
||||
return self._fileno
|
||||
|
||||
def doRead(self):
|
||||
return self.cb()
|
||||
|
||||
def getHost(self):
|
||||
raise NotImplementedError("No network operation expected")
|
||||
|
||||
def getPeer(self):
|
||||
raise NotImplementedError("No network operation expected")
|
||||
|
||||
def writeSomeData(self, data: bytes) -> None:
|
||||
raise NotImplementedError("Reduced functionality: read-only")
|
||||
|
||||
|
||||
class TwistedEventLoop(EventLoop):
|
||||
"""
|
||||
Event loop based on Twisted_
|
||||
"""
|
||||
|
||||
_idle_emulation_delay = 1.0 / 256 # a short time (in seconds)
|
||||
|
||||
def __init__(self, reactor: ReactorBase | None = None, manage_reactor: bool = True) -> None:
|
||||
"""
|
||||
:param reactor: reactor to use
|
||||
:type reactor: :class:`twisted.internet.reactor`.
|
||||
:param: manage_reactor: `True` if you want this event loop to run
|
||||
and stop the reactor.
|
||||
:type manage_reactor: boolean
|
||||
|
||||
.. WARNING::
|
||||
Twisted's reactor doesn't like to be stopped and run again. If you
|
||||
need to stop and run your :class:`MainLoop`, consider setting
|
||||
``manage_reactor=False`` and take care of running/stopping the reactor
|
||||
at the beginning/ending of your program yourself.
|
||||
|
||||
You can also forego using :class:`MainLoop`'s run() entirely, and
|
||||
instead call start() and stop() before and after starting the
|
||||
reactor.
|
||||
|
||||
.. _Twisted: https://twisted.org/
|
||||
"""
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
if reactor is None:
|
||||
import twisted.internet.reactor
|
||||
|
||||
reactor = twisted.internet.reactor
|
||||
self.reactor: ReactorBase = reactor
|
||||
self._watch_files: dict[int, _TwistedInputDescriptor] = {}
|
||||
self._idle_handle: int = 0
|
||||
self._twisted_idle_enabled = False
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._exc: BaseException | None = None
|
||||
self.manage_reactor = manage_reactor
|
||||
self._enable_twisted_idle()
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[..., _T],
|
||||
*args: object,
|
||||
**kwargs: object,
|
||||
) -> Future[_T]:
|
||||
raise NotImplementedError(
|
||||
"Twisted implement it's own ThreadPool executor. Please use native API for call:\n"
|
||||
"'threads.deferToThread(Callable[..., Any], *args, **kwargs)'\n"
|
||||
"And use 'addCallback' api for callbacks:\n"
|
||||
"'threads.deferToThread(Callable[..., T], *args, **kwargs).addCallback(Callable[[T], None])'"
|
||||
)
|
||||
|
||||
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> DelayedCall:
|
||||
"""
|
||||
Call callback() a given time from now. No parameters are
|
||||
passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_alarm()
|
||||
|
||||
seconds -- floating point time to wait before calling callback
|
||||
callback -- function to call from event loop
|
||||
"""
|
||||
handle = self.reactor.callLater(seconds, self.handle_exit(callback))
|
||||
return handle
|
||||
|
||||
def remove_alarm(self, handle: DelayedCall) -> bool:
|
||||
"""
|
||||
Remove an alarm.
|
||||
|
||||
Returns True if the alarm exists, False otherwise
|
||||
"""
|
||||
try:
|
||||
handle.cancel()
|
||||
|
||||
except (AlreadyCancelled, AlreadyCalled):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Call callback() when fd has some data to read. No parameters
|
||||
are passed to callback.
|
||||
|
||||
Returns a handle that may be passed to remove_watch_file()
|
||||
|
||||
fd -- file descriptor to watch for input
|
||||
callback -- function to call when input is available
|
||||
"""
|
||||
ind = _TwistedInputDescriptor(self.reactor, fd, self.handle_exit(callback))
|
||||
self._watch_files[fd] = ind
|
||||
self.reactor.addReader(ind)
|
||||
return fd
|
||||
|
||||
def remove_watch_file(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an input file.
|
||||
|
||||
Returns True if the input file exists, False otherwise
|
||||
"""
|
||||
if handle in self._watch_files:
|
||||
self.reactor.removeReader(self._watch_files[handle])
|
||||
del self._watch_files[handle]
|
||||
return True
|
||||
return False
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a callback for entering idle.
|
||||
|
||||
Returns a handle that may be passed to remove_enter_idle()
|
||||
"""
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def _enable_twisted_idle(self) -> None:
|
||||
"""
|
||||
Twisted's reactors don't have an idle or enter-idle callback
|
||||
so the best we can do for now is to set a timer event in a very
|
||||
short time to approximate an enter-idle callback.
|
||||
|
||||
.. WARNING::
|
||||
This will perform worse than the other event loops until we can find a
|
||||
fix or workaround
|
||||
"""
|
||||
if self._twisted_idle_enabled:
|
||||
return
|
||||
self.reactor.callLater(
|
||||
self._idle_emulation_delay,
|
||||
self.handle_exit(self._twisted_idle_callback, enable_idle=False),
|
||||
)
|
||||
self._twisted_idle_enabled = True
|
||||
|
||||
def _twisted_idle_callback(self) -> None:
|
||||
for callback in self._idle_callbacks.values():
|
||||
callback()
|
||||
self._twisted_idle_enabled = False
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an idle callback.
|
||||
|
||||
Returns True if the handle was removed.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the event loop. Exit the loop when any callback raises
|
||||
an exception. If ExitMainLoop is raised, exit cleanly.
|
||||
"""
|
||||
if not self.manage_reactor:
|
||||
return
|
||||
self.reactor.run()
|
||||
if self._exc:
|
||||
# An exception caused us to exit, raise it now
|
||||
exc = self._exc
|
||||
self._exc = None
|
||||
raise exc.with_traceback(exc.__traceback__)
|
||||
|
||||
def handle_exit(self, f: Callable[_Spec, _T], enable_idle: bool = True) -> Callable[_Spec, _T | None]:
|
||||
"""
|
||||
Decorator that cleanly exits the :class:`TwistedEventLoop` if
|
||||
:class:`ExitMainLoop` is thrown inside of the wrapped function. Store the
|
||||
exception info if some other exception occurs, it will be reraised after
|
||||
the loop quits.
|
||||
|
||||
*f* -- function to be wrapped
|
||||
"""
|
||||
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
|
||||
rval = None
|
||||
try:
|
||||
rval = f(*args, **kwargs)
|
||||
except ExitMainLoop:
|
||||
if self.manage_reactor:
|
||||
self.reactor.stop()
|
||||
except BaseException as exc:
|
||||
print(sys.exc_info())
|
||||
self._exc = exc
|
||||
if self.manage_reactor:
|
||||
self.reactor.crash()
|
||||
if enable_idle:
|
||||
self._enable_twisted_idle()
|
||||
return rval
|
||||
|
||||
return wrapper
|
||||
277
venv/Lib/site-packages/urwid/event_loop/zmq_loop.py
Normal file
277
venv/Lib/site-packages/urwid/event_loop/zmq_loop.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# Urwid main loop code using ZeroMQ queues
|
||||
# Copyright (C) 2019 Dave Jones
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Urwid web site: https://urwid.org/
|
||||
|
||||
"""ZeroMQ based urwid EventLoop implementation.
|
||||
|
||||
`ZeroMQ <https://zeromq.org>`_ library is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import errno
|
||||
import heapq
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
from itertools import count
|
||||
|
||||
import zmq
|
||||
|
||||
from .abstract_loop import EventLoop, ExitMainLoop
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import io
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import Executor, Future
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
ZMQAlarmHandle = typing.TypeVar("ZMQAlarmHandle")
|
||||
_T = typing.TypeVar("_T")
|
||||
_Spec = ParamSpec("_Spec")
|
||||
|
||||
|
||||
class ZMQEventLoop(EventLoop):
|
||||
"""
|
||||
This class is an urwid event loop for `ZeroMQ`_ applications. It is very
|
||||
similar to :class:`SelectEventLoop`, supporting the usual :meth:`alarm`
|
||||
events and file watching (:meth:`watch_file`) capabilities, but also
|
||||
incorporates the ability to watch zmq queues for events
|
||||
(:meth:`watch_queue`).
|
||||
|
||||
.. _ZeroMQ: https://zeromq.org/
|
||||
"""
|
||||
|
||||
_alarm_break = count()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
|
||||
self._did_something = True
|
||||
self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = []
|
||||
self._poller = zmq.Poller()
|
||||
self._queue_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
self._idle_handle = 0
|
||||
self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
|
||||
|
||||
def run_in_executor(
|
||||
self,
|
||||
executor: Executor,
|
||||
func: Callable[_Spec, _T],
|
||||
*args: _Spec.args,
|
||||
**kwargs: _Spec.kwargs,
|
||||
) -> Future[_T]:
|
||||
"""Run callable in executor.
|
||||
|
||||
:param executor: Executor to use for running the function
|
||||
:type executor: concurrent.futures.Executor
|
||||
:param func: function to call
|
||||
:type func: Callable
|
||||
:param args: positional arguments to function
|
||||
:type args: object
|
||||
:param kwargs: keyword arguments to function
|
||||
:type kwargs: object
|
||||
:return: future object for the function call outcome.
|
||||
:rtype: concurrent.futures.Future
|
||||
"""
|
||||
return executor.submit(func, *args, **kwargs)
|
||||
|
||||
def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> ZMQAlarmHandle:
|
||||
"""
|
||||
Call *callback* a given time from now. No parameters are passed to
|
||||
callback. Returns a handle that may be passed to :meth:`remove_alarm`.
|
||||
|
||||
:param float seconds:
|
||||
floating point time to wait before calling callback.
|
||||
|
||||
:param callback:
|
||||
function to call from event loop.
|
||||
"""
|
||||
handle = (time.time() + seconds, next(self._alarm_break), callback)
|
||||
heapq.heappush(self._alarms, handle)
|
||||
return handle
|
||||
|
||||
def remove_alarm(self, handle: ZMQAlarmHandle) -> bool:
|
||||
"""
|
||||
Remove an alarm. Returns ``True`` if the alarm exists, ``False``
|
||||
otherwise.
|
||||
"""
|
||||
try:
|
||||
self._alarms.remove(handle)
|
||||
heapq.heapify(self._alarms)
|
||||
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def watch_queue(
|
||||
self,
|
||||
queue: zmq.Socket,
|
||||
callback: Callable[[], typing.Any],
|
||||
flags: int = zmq.POLLIN,
|
||||
) -> zmq.Socket:
|
||||
"""
|
||||
Call *callback* when zmq *queue* has something to read (when *flags* is
|
||||
set to ``POLLIN``, the default) or is available to write (when *flags*
|
||||
is set to ``POLLOUT``). No parameters are passed to the callback.
|
||||
Returns a handle that may be passed to :meth:`remove_watch_queue`.
|
||||
|
||||
:param queue:
|
||||
The zmq queue to poll.
|
||||
|
||||
:param callback:
|
||||
The function to call when the poll is successful.
|
||||
|
||||
:param int flags:
|
||||
The condition to monitor on the queue (defaults to ``POLLIN``).
|
||||
"""
|
||||
if queue in self._queue_callbacks:
|
||||
raise ValueError(f"already watching {queue!r}")
|
||||
self._poller.register(queue, flags)
|
||||
self._queue_callbacks[queue] = callback
|
||||
return queue
|
||||
|
||||
def watch_file(
|
||||
self,
|
||||
fd: int | io.TextIOWrapper,
|
||||
callback: Callable[[], typing.Any],
|
||||
flags: int = zmq.POLLIN,
|
||||
) -> io.TextIOWrapper:
|
||||
"""
|
||||
Call *callback* when *fd* has some data to read. No parameters are
|
||||
passed to the callback. The *flags* are as for :meth:`watch_queue`.
|
||||
Returns a handle that may be passed to :meth:`remove_watch_file`.
|
||||
|
||||
:param fd:
|
||||
The file-like object, or fileno to monitor.
|
||||
|
||||
:param callback:
|
||||
The function to call when the file has data available.
|
||||
|
||||
:param int flags:
|
||||
The condition to monitor on the file (defaults to ``POLLIN``).
|
||||
"""
|
||||
if isinstance(fd, int):
|
||||
fd = os.fdopen(fd)
|
||||
self._poller.register(fd, flags)
|
||||
self._queue_callbacks[fd.fileno()] = callback
|
||||
return fd
|
||||
|
||||
def remove_watch_queue(self, handle: zmq.Socket) -> bool:
|
||||
"""
|
||||
Remove a queue from background polling. Returns ``True`` if the queue
|
||||
was being monitored, ``False`` otherwise.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
self._poller.unregister(handle)
|
||||
finally:
|
||||
self._queue_callbacks.pop(handle, None)
|
||||
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def remove_watch_file(self, handle: io.TextIOWrapper) -> bool:
|
||||
"""
|
||||
Remove a file from background polling. Returns ``True`` if the file was
|
||||
being monitored, ``False`` otherwise.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
self._poller.unregister(handle)
|
||||
finally:
|
||||
self._queue_callbacks.pop(handle.fileno(), None)
|
||||
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
|
||||
"""
|
||||
Add a *callback* to be executed when the event loop detects it is idle.
|
||||
Returns a handle that may be passed to :meth:`remove_enter_idle`.
|
||||
"""
|
||||
self._idle_handle += 1
|
||||
self._idle_callbacks[self._idle_handle] = callback
|
||||
return self._idle_handle
|
||||
|
||||
def remove_enter_idle(self, handle: int) -> bool:
|
||||
"""
|
||||
Remove an idle callback. Returns ``True`` if *handle* was removed,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
try:
|
||||
del self._idle_callbacks[handle]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _entering_idle(self) -> None:
|
||||
for callback in list(self._idle_callbacks.values()):
|
||||
callback()
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Start the event loop. Exit the loop when any callback raises an
|
||||
exception. If :exc:`ExitMainLoop` is raised, exit cleanly.
|
||||
"""
|
||||
with contextlib.suppress(ExitMainLoop):
|
||||
while True:
|
||||
try:
|
||||
self._loop()
|
||||
except zmq.error.ZMQError as exc: # noqa: PERF203
|
||||
if exc.errno != errno.EINTR:
|
||||
raise
|
||||
|
||||
def _loop(self) -> None:
|
||||
"""
|
||||
A single iteration of the event loop.
|
||||
"""
|
||||
state = "wait" # default state not expecting any action
|
||||
if self._alarms or self._did_something:
|
||||
timeout = 0
|
||||
if self._alarms:
|
||||
state = "alarm"
|
||||
timeout = max(0.0, self._alarms[0][0] - time.time())
|
||||
if self._did_something and (not self._alarms or (self._alarms and timeout > 0)):
|
||||
state = "idle"
|
||||
timeout = 0
|
||||
ready = dict(self._poller.poll(timeout * 1000))
|
||||
else:
|
||||
ready = dict(self._poller.poll())
|
||||
|
||||
if not ready:
|
||||
if state == "idle":
|
||||
self._entering_idle()
|
||||
self._did_something = False
|
||||
elif state == "alarm":
|
||||
_due, _tie_break, callback = heapq.heappop(self._alarms)
|
||||
callback()
|
||||
self._did_something = True
|
||||
|
||||
for queue in ready:
|
||||
self._queue_callbacks[queue]()
|
||||
self._did_something = True
|
||||
Reference in New Issue
Block a user