diff options
author | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 |
---|---|---|
committer | Devtools Arcadia <[email protected]> | 2022-02-07 18:08:42 +0300 |
commit | 1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch) | |
tree | e26c9fed0de5d9873cce7e00bc214573dc2195b7 /contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop |
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop')
12 files changed, 1277 insertions, 0 deletions
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/__init__.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/__init__.py diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_base.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_base.py new file mode 100644 index 00000000000..ace2b8db497 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_base.py @@ -0,0 +1,46 @@ +""" +Eventloop for integration with Python3 asyncio. + +Note that we can't use "yield from", because the package should be installable +under Python 2.6 as well, and it should contain syntactically valid Python 2.6 +code. +""" +from __future__ import unicode_literals + +__all__ = ( + 'AsyncioTimeout', +) + + +class AsyncioTimeout(object): + """ + Call the `timeout` function when the timeout expires. + Every call of the `reset` method, resets the timeout and starts a new + timer. + """ + def __init__(self, timeout, callback, loop): + self.timeout = timeout + self.callback = callback + self.loop = loop + + self.counter = 0 + self.running = True + + def reset(self): + """ + Reset the timeout. Starts a new timer. + """ + self.counter += 1 + local_counter = self.counter + + def timer_timeout(): + if self.counter == local_counter and self.running: + self.callback() + + self.loop.call_later(self.timeout, timer_timeout) + + def stop(self): + """ + Ignore timeout. Don't call the callback anymore. + """ + self.running = False diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_posix.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_posix.py new file mode 100644 index 00000000000..426ed96f67d --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_posix.py @@ -0,0 +1,113 @@ +""" +Posix asyncio event loop. +""" +from __future__ import unicode_literals + +from ..terminal.vt100_input import InputStream +from .asyncio_base import AsyncioTimeout +from .base import EventLoop, INPUT_TIMEOUT +from .callbacks import EventLoopCallbacks +from .posix_utils import PosixStdinReader + +import asyncio +import signal + +__all__ = ( + 'PosixAsyncioEventLoop', +) + + +class PosixAsyncioEventLoop(EventLoop): + def __init__(self, loop=None): + self.loop = loop or asyncio.get_event_loop() + self.closed = False + + self._stopped_f = asyncio.Future(loop=self.loop) + + @asyncio.coroutine + def run_as_coroutine(self, stdin, callbacks): + """ + The input 'event loop'. + """ + assert isinstance(callbacks, EventLoopCallbacks) + + # Create reader class. + stdin_reader = PosixStdinReader(stdin.fileno()) + + if self.closed: + raise Exception('Event loop already closed.') + + inputstream = InputStream(callbacks.feed_key) + + try: + # Create a new Future every time. + self._stopped_f = asyncio.Future(loop=self.loop) + + # Handle input timouts + def timeout_handler(): + """ + When no input has been received for INPUT_TIMEOUT seconds, + flush the input stream and fire the timeout event. + """ + inputstream.flush() + + callbacks.input_timeout() + + timeout = AsyncioTimeout(INPUT_TIMEOUT, timeout_handler, self.loop) + + # Catch sigwinch + def received_winch(): + self.call_from_executor(callbacks.terminal_size_changed) + + self.loop.add_signal_handler(signal.SIGWINCH, received_winch) + + # Read input data. + def stdin_ready(): + data = stdin_reader.read() + inputstream.feed(data) + timeout.reset() + + # Quit when the input stream was closed. + if stdin_reader.closed: + self.stop() + + self.loop.add_reader(stdin.fileno(), stdin_ready) + + # Block this coroutine until stop() has been called. + for f in self._stopped_f: + yield f + + finally: + # Clean up. + self.loop.remove_reader(stdin.fileno()) + self.loop.remove_signal_handler(signal.SIGWINCH) + + # Don't trigger any timeout events anymore. + timeout.stop() + + def stop(self): + # Trigger the 'Stop' future. + self._stopped_f.set_result(True) + + def close(self): + # Note: we should not close the asyncio loop itself, because that one + # was not created here. + self.closed = True + + def run_in_executor(self, callback): + self.loop.run_in_executor(None, callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + """ + self.loop.call_soon_threadsafe(callback) + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + self.loop.add_reader(fd, callback) + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + self.loop.remove_reader(fd) diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_win32.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_win32.py new file mode 100644 index 00000000000..45f5f526798 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/asyncio_win32.py @@ -0,0 +1,83 @@ +""" +Win32 asyncio event loop. + +Windows notes: +- Somehow it doesn't seem to work with the 'ProactorEventLoop'. +""" +from __future__ import unicode_literals + +from .base import EventLoop, INPUT_TIMEOUT +from ..terminal.win32_input import ConsoleInputReader +from .callbacks import EventLoopCallbacks +from .asyncio_base import AsyncioTimeout + +import asyncio + +__all__ = ( + 'Win32AsyncioEventLoop', +) + + +class Win32AsyncioEventLoop(EventLoop): + def __init__(self, loop=None): + self._console_input_reader = ConsoleInputReader() + self.running = False + self.closed = False + self.loop = loop or asyncio.get_event_loop() + + @asyncio.coroutine + def run_as_coroutine(self, stdin, callbacks): + """ + The input 'event loop'. + """ + # Note: We cannot use "yield from", because this package also + # installs on Python 2. + assert isinstance(callbacks, EventLoopCallbacks) + + if self.closed: + raise Exception('Event loop already closed.') + + timeout = AsyncioTimeout(INPUT_TIMEOUT, callbacks.input_timeout, self.loop) + self.running = True + + try: + while self.running: + timeout.reset() + + # Get keys + try: + g = iter(self.loop.run_in_executor(None, self._console_input_reader.read)) + while True: + yield next(g) + except StopIteration as e: + keys = e.args[0] + + # Feed keys to input processor. + for k in keys: + callbacks.feed_key(k) + finally: + timeout.stop() + + def stop(self): + self.running = False + + def close(self): + # Note: we should not close the asyncio loop itself, because that one + # was not created here. + self.closed = True + + self._console_input_reader.close() + + def run_in_executor(self, callback): + self.loop.run_in_executor(None, callback) + + def call_from_executor(self, callback, _max_postpone_until=None): + self.loop.call_soon_threadsafe(callback) + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + self.loop.add_reader(fd, callback) + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + self.loop.remove_reader(fd) diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/base.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/base.py new file mode 100644 index 00000000000..db86face668 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/base.py @@ -0,0 +1,85 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'EventLoop', + 'INPUT_TIMEOUT', +) + + +#: When to trigger the `onInputTimeout` event. +INPUT_TIMEOUT = .5 + + +class EventLoop(with_metaclass(ABCMeta, object)): + """ + Eventloop interface. + """ + def run(self, stdin, callbacks): + """ + Run the eventloop until stop() is called. Report all + input/timeout/terminal-resize events to the callbacks. + + :param stdin: :class:`~prompt_toolkit.input.Input` instance. + :param callbacks: :class:`~prompt_toolkit.eventloop.callbacks.EventLoopCallbacks` instance. + """ + raise NotImplementedError("This eventloop doesn't implement synchronous 'run()'.") + + def run_as_coroutine(self, stdin, callbacks): + """ + Similar to `run`, but this is a coroutine. (For asyncio integration.) + """ + raise NotImplementedError("This eventloop doesn't implement 'run_as_coroutine()'.") + + @abstractmethod + def stop(self): + """ + Stop the `run` call. (Normally called by + :class:`~prompt_toolkit.interface.CommandLineInterface`, when a result + is available, or Abort/Quit has been called.) + """ + + @abstractmethod + def close(self): + """ + Clean up of resources. Eventloop cannot be reused a second time after + this call. + """ + + @abstractmethod + def add_reader(self, fd, callback): + """ + Start watching the file descriptor for read availability and then call + the callback. + """ + + @abstractmethod + def remove_reader(self, fd): + """ + Stop watching the file descriptor for read availability. + """ + + @abstractmethod + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. (This is + recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + + @abstractmethod + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. Similar to Twisted's + ``callFromThread``. + + :param _max_postpone_until: `None` or `time.time` value. For interal + use. If the eventloop is saturated, consider this task to be low + priority and postpone maximum until this timestamp. (For instance, + repaint is done using low priority.) + + Note: In the past, this used to be a datetime.datetime instance, + but apparently, executing `time.time` is more efficient: it + does fewer system calls. (It doesn't read /etc/localtime.) + """ diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/callbacks.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/callbacks.py new file mode 100644 index 00000000000..04adab6fd49 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/callbacks.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals +from abc import ABCMeta, abstractmethod +from six import with_metaclass + +__all__ = ( + 'EventLoopCallbacks', +) + + +class EventLoopCallbacks(with_metaclass(ABCMeta, object)): + """ + This is the glue between the :class:`~prompt_toolkit.eventloop.base.EventLoop` + and :class:`~prompt_toolkit.interface.CommandLineInterface`. + + :meth:`~prompt_toolkit.eventloop.base.EventLoop.run` takes an + :class:`.EventLoopCallbacks` instance and operates on that one, driving the + interface. + """ + @abstractmethod + def terminal_size_changed(self): + pass + + @abstractmethod + def input_timeout(self): + pass + + @abstractmethod + def feed_key(self, key): + pass diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/inputhook.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/inputhook.py new file mode 100644 index 00000000000..bab1f4c003a --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/inputhook.py @@ -0,0 +1,107 @@ +""" +Similar to `PyOS_InputHook` of the Python API. Some eventloops can have an +inputhook to allow easy integration with other event loops. + +When the eventloop of prompt-toolkit is idle, it can call such a hook. This +hook can call another eventloop that runs for a short while, for instance to +keep a graphical user interface responsive. + +It's the responsibility of this hook to exit when there is input ready. +There are two ways to detect when input is ready: + +- Call the `input_is_ready` method periodically. Quit when this returns `True`. + +- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor + becomes readable. (But don't read from it.) + + Note that this is not the same as checking for `sys.stdin.fileno()`. The + eventloop of prompt-toolkit allows thread-based executors, for example for + asynchronous autocompletion. When the completion for instance is ready, we + also want prompt-toolkit to gain control again in order to display that. + +An alternative to using input hooks, is to create a custom `EventLoop` class that +controls everything. +""" +from __future__ import unicode_literals +import os +import threading +from prompt_toolkit.utils import is_windows +from .select import select_fds + +__all__ = ( + 'InputHookContext', +) + + +class InputHookContext(object): + """ + Given as a parameter to the inputhook. + """ + def __init__(self, inputhook): + assert callable(inputhook) + + self.inputhook = inputhook + self._input_is_ready = None + + self._r, self._w = os.pipe() + + def input_is_ready(self): + """ + Return True when the input is ready. + """ + return self._input_is_ready(wait=False) + + def fileno(self): + """ + File descriptor that will become ready when the event loop needs to go on. + """ + return self._r + + def call_inputhook(self, input_is_ready_func): + """ + Call the inputhook. (Called by a prompt-toolkit eventloop.) + """ + self._input_is_ready = input_is_ready_func + + # Start thread that activates this pipe when there is input to process. + def thread(): + input_is_ready_func(wait=True) + os.write(self._w, b'x') + + threading.Thread(target=thread).start() + + # Call inputhook. + self.inputhook(self) + + # Flush the read end of the pipe. + try: + # Before calling 'os.read', call select.select. This is required + # when the gevent monkey patch has been applied. 'os.read' is never + # monkey patched and won't be cooperative, so that would block all + # other select() calls otherwise. + # See: http://www.gevent.org/gevent.os.html + + # Note: On Windows, this is apparently not an issue. + # However, if we would ever want to add a select call, it + # should use `windll.kernel32.WaitForMultipleObjects`, + # because `select.select` can't wait for a pipe on Windows. + if not is_windows(): + select_fds([self._r], timeout=None) + + os.read(self._r, 1024) + except OSError: + # This happens when the window resizes and a SIGWINCH was received. + # We get 'Error: [Errno 4] Interrupted system call' + # Just ignore. + pass + self._input_is_ready = None + + def close(self): + """ + Clean up resources. + """ + if self._r: + os.close(self._r) + os.close(self._w) + + self._r = self._w = None diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix.py new file mode 100644 index 00000000000..f631dbd8915 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix.py @@ -0,0 +1,306 @@ +from __future__ import unicode_literals +import fcntl +import os +import signal +import threading +import time + +from prompt_toolkit.terminal.vt100_input import InputStream +from prompt_toolkit.utils import DummyContext, in_main_thread +from prompt_toolkit.input import Input +from .base import EventLoop, INPUT_TIMEOUT +from .callbacks import EventLoopCallbacks +from .inputhook import InputHookContext +from .posix_utils import PosixStdinReader +from .utils import TimeIt +from .select import AutoSelector, Selector, fd_to_int + +__all__ = ( + 'PosixEventLoop', +) + +_now = time.time + + +class PosixEventLoop(EventLoop): + """ + Event loop for posix systems (Linux, Mac os X). + """ + def __init__(self, inputhook=None, selector=AutoSelector): + assert inputhook is None or callable(inputhook) + assert issubclass(selector, Selector) + + self.running = False + self.closed = False + self._running = False + self._callbacks = None + + self._calls_from_executor = [] + self._read_fds = {} # Maps fd to handler. + self.selector = selector() + + # Create a pipe for inter thread communication. + self._schedule_pipe = os.pipe() + fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK) + + # Create inputhook context. + self._inputhook_context = InputHookContext(inputhook) if inputhook else None + + def run(self, stdin, callbacks): + """ + The input 'event loop'. + """ + assert isinstance(stdin, Input) + assert isinstance(callbacks, EventLoopCallbacks) + assert not self._running + + if self.closed: + raise Exception('Event loop already closed.') + + self._running = True + self._callbacks = callbacks + + inputstream = InputStream(callbacks.feed_key) + current_timeout = [INPUT_TIMEOUT] # Nonlocal + + # Create reader class. + stdin_reader = PosixStdinReader(stdin.fileno()) + + # Only attach SIGWINCH signal handler in main thread. + # (It's not possible to attach signal handlers in other threads. In + # that case we should rely on a the main thread to call this manually + # instead.) + if in_main_thread(): + ctx = call_on_sigwinch(self.received_winch) + else: + ctx = DummyContext() + + def read_from_stdin(): + " Read user input. " + # Feed input text. + data = stdin_reader.read() + inputstream.feed(data) + + # Set timeout again. + current_timeout[0] = INPUT_TIMEOUT + + # Quit when the input stream was closed. + if stdin_reader.closed: + self.stop() + + self.add_reader(stdin, read_from_stdin) + self.add_reader(self._schedule_pipe[0], None) + + with ctx: + while self._running: + # Call inputhook. + if self._inputhook_context: + with TimeIt() as inputhook_timer: + def ready(wait): + " True when there is input ready. The inputhook should return control. " + return self._ready_for_reading(current_timeout[0] if wait else 0) != [] + self._inputhook_context.call_inputhook(ready) + inputhook_duration = inputhook_timer.duration + else: + inputhook_duration = 0 + + # Calculate remaining timeout. (The inputhook consumed some of the time.) + if current_timeout[0] is None: + remaining_timeout = None + else: + remaining_timeout = max(0, current_timeout[0] - inputhook_duration) + + # Wait until input is ready. + fds = self._ready_for_reading(remaining_timeout) + + # When any of the FDs are ready. Call the appropriate callback. + if fds: + # Create lists of high/low priority tasks. The main reason + # for this is to allow painting the UI to happen as soon as + # possible, but when there are many events happening, we + # don't want to call the UI renderer 1000x per second. If + # the eventloop is completely saturated with many CPU + # intensive tasks (like processing input/output), we say + # that drawing the UI can be postponed a little, to make + # CPU available. This will be a low priority task in that + # case. + tasks = [] + low_priority_tasks = [] + now = None # Lazy load time. (Fewer system calls.) + + for fd in fds: + # For the 'call_from_executor' fd, put each pending + # item on either the high or low priority queue. + if fd == self._schedule_pipe[0]: + for c, max_postpone_until in self._calls_from_executor: + if max_postpone_until is None: + # Execute now. + tasks.append(c) + else: + # Execute soon, if `max_postpone_until` is in the future. + now = now or _now() + if max_postpone_until < now: + tasks.append(c) + else: + low_priority_tasks.append((c, max_postpone_until)) + self._calls_from_executor = [] + + # Flush all the pipe content. + os.read(self._schedule_pipe[0], 1024) + else: + handler = self._read_fds.get(fd) + if handler: + tasks.append(handler) + + # When there are high priority tasks, run all these. + # Schedule low priority tasks for the next iteration. + if tasks: + for t in tasks: + t() + + # Postpone low priority tasks. + for t, max_postpone_until in low_priority_tasks: + self.call_from_executor(t, _max_postpone_until=max_postpone_until) + else: + # Currently there are only low priority tasks -> run them right now. + for t, _ in low_priority_tasks: + t() + + else: + # Flush all pending keys on a timeout. (This is most + # important to flush the vt100 'Escape' key early when + # nothing else follows.) + inputstream.flush() + + # Fire input timeout event. + callbacks.input_timeout() + current_timeout[0] = None + + self.remove_reader(stdin) + self.remove_reader(self._schedule_pipe[0]) + + self._callbacks = None + + def _ready_for_reading(self, timeout=None): + """ + Return the file descriptors that are ready for reading. + """ + fds = self.selector.select(timeout) + return fds + + def received_winch(self): + """ + Notify the event loop that SIGWINCH has been received + """ + # Process signal asynchronously, because this handler can write to the + # output, and doing this inside the signal handler causes easily + # reentrant calls, giving runtime errors.. + + # Furthur, this has to be thread safe. When the CommandLineInterface + # runs not in the main thread, this function still has to be called + # from the main thread. (The only place where we can install signal + # handlers.) + def process_winch(): + if self._callbacks: + self._callbacks.terminal_size_changed() + + self.call_from_executor(process_winch) + + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. + (This is recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + # Wait until the main thread is idle. + # We start the thread by using `call_from_executor`. The event loop + # favours processing input over `calls_from_executor`, so the thread + # will not start until there is no more input to process and the main + # thread becomes idle for an instant. This is good, because Python + # threading favours CPU over I/O -- an autocompletion thread in the + # background would cause a significantly slow down of the main thread. + # It is mostly noticable when pasting large portions of text while + # having real time autocompletion while typing on. + def start_executor(): + threading.Thread(target=callback).start() + self.call_from_executor(start_executor) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + + :param _max_postpone_until: `None` or `time.time` value. For interal + use. If the eventloop is saturated, consider this task to be low + priority and postpone maximum until this timestamp. (For instance, + repaint is done using low priority.) + """ + assert _max_postpone_until is None or isinstance(_max_postpone_until, float) + self._calls_from_executor.append((callback, _max_postpone_until)) + + if self._schedule_pipe: + try: + os.write(self._schedule_pipe[1], b'x') + except (AttributeError, IndexError, OSError): + # Handle race condition. We're in a different thread. + # - `_schedule_pipe` could have become None in the meantime. + # - We catch `OSError` (actually BrokenPipeError), because the + # main thread could have closed the pipe already. + pass + + def stop(self): + """ + Stop the event loop. + """ + self._running = False + + def close(self): + self.closed = True + + # Close pipes. + schedule_pipe = self._schedule_pipe + self._schedule_pipe = None + + if schedule_pipe: + os.close(schedule_pipe[0]) + os.close(schedule_pipe[1]) + + if self._inputhook_context: + self._inputhook_context.close() + + def add_reader(self, fd, callback): + " Add read file descriptor to the event loop. " + fd = fd_to_int(fd) + self._read_fds[fd] = callback + self.selector.register(fd) + + def remove_reader(self, fd): + " Remove read file descriptor from the event loop. " + fd = fd_to_int(fd) + + if fd in self._read_fds: + del self._read_fds[fd] + + self.selector.unregister(fd) + + +class call_on_sigwinch(object): + """ + Context manager which Installs a SIGWINCH callback. + (This signal occurs when the terminal size changes.) + """ + def __init__(self, callback): + self.callback = callback + self.previous_callback = None + + def __enter__(self): + self.previous_callback = signal.signal(signal.SIGWINCH, lambda *a: self.callback()) + + def __exit__(self, *a, **kw): + if self.previous_callback is None: + # Normally, `signal.signal` should never return `None`. + # For some reason it happens here: + # https://github.com/jonathanslenders/python-prompt-toolkit/pull/174 + signal.signal(signal.SIGWINCH, 0) + else: + signal.signal(signal.SIGWINCH, self.previous_callback) diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix_utils.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix_utils.py new file mode 100644 index 00000000000..320df438ca2 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/posix_utils.py @@ -0,0 +1,82 @@ +from __future__ import unicode_literals + +from codecs import getincrementaldecoder +import os +import six + +__all__ = ( + 'PosixStdinReader', +) + + +class PosixStdinReader(object): + """ + Wrapper around stdin which reads (nonblocking) the next available 1024 + bytes and decodes it. + + Note that you can't be sure that the input file is closed if the ``read`` + function returns an empty string. When ``errors=ignore`` is passed, + ``read`` can return an empty string if all malformed input was replaced by + an empty string. (We can't block here and wait for more input.) So, because + of that, check the ``closed`` attribute, to be sure that the file has been + closed. + + :param stdin_fd: File descriptor from which we read. + :param errors: Can be 'ignore', 'strict' or 'replace'. + On Python3, this can be 'surrogateescape', which is the default. + + 'surrogateescape' is preferred, because this allows us to transfer + unrecognised bytes to the key bindings. Some terminals, like lxterminal + and Guake, use the 'Mxx' notation to send mouse events, where each 'x' + can be any possible byte. + """ + # By default, we want to 'ignore' errors here. The input stream can be full + # of junk. One occurrence of this that I had was when using iTerm2 on OS X, + # with "Option as Meta" checked (You should choose "Option as +Esc".) + + def __init__(self, stdin_fd, + errors=('ignore' if six.PY2 else 'surrogateescape')): + assert isinstance(stdin_fd, int) + self.stdin_fd = stdin_fd + self.errors = errors + + # Create incremental decoder for decoding stdin. + # We can not just do `os.read(stdin.fileno(), 1024).decode('utf-8')`, because + # it could be that we are in the middle of a utf-8 byte sequence. + self._stdin_decoder_cls = getincrementaldecoder('utf-8') + self._stdin_decoder = self._stdin_decoder_cls(errors=errors) + + #: True when there is nothing anymore to read. + self.closed = False + + def read(self, count=1024): + # By default we choose a rather small chunk size, because reading + # big amounts of input at once, causes the event loop to process + # all these key bindings also at once without going back to the + # loop. This will make the application feel unresponsive. + """ + Read the input and return it as a string. + + Return the text. Note that this can return an empty string, even when + the input stream was not yet closed. This means that something went + wrong during the decoding. + """ + if self.closed: + return b'' + + # Note: the following works better than wrapping `self.stdin` like + # `codecs.getreader('utf-8')(stdin)` and doing `read(1)`. + # Somehow that causes some latency when the escape + # character is pressed. (Especially on combination with the `select`.) + try: + data = os.read(self.stdin_fd, count) + + # Nothing more to read, stream is closed. + if data == b'': + self.closed = True + return '' + except OSError: + # In case of SIGWINCH + data = b'' + + return self._stdin_decoder.decode(data) diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/select.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/select.py new file mode 100644 index 00000000000..f678f84c557 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/select.py @@ -0,0 +1,216 @@ +""" +Selectors for the Posix event loop. +""" +from __future__ import unicode_literals, absolute_import +import sys +import abc +import errno +import select +import six + +__all__ = ( + 'AutoSelector', + 'PollSelector', + 'SelectSelector', + 'Selector', + 'fd_to_int', +) + +def fd_to_int(fd): + assert isinstance(fd, int) or hasattr(fd, 'fileno') + + if isinstance(fd, int): + return fd + else: + return fd.fileno() + + +class Selector(six.with_metaclass(abc.ABCMeta, object)): + @abc.abstractmethod + def register(self, fd): + assert isinstance(fd, int) + + @abc.abstractmethod + def unregister(self, fd): + assert isinstance(fd, int) + + @abc.abstractmethod + def select(self, timeout): + pass + + @abc.abstractmethod + def close(self): + pass + + +class AutoSelector(Selector): + def __init__(self): + self._fds = [] + + self._select_selector = SelectSelector() + self._selectors = [self._select_selector] + + # When 'select.poll' exists, create a PollSelector. + if hasattr(select, 'poll'): + self._poll_selector = PollSelector() + self._selectors.append(self._poll_selector) + else: + self._poll_selector = None + + # Use of the 'select' module, that was introduced in Python3.4. We don't + # use it before 3.5 however, because this is the point where this module + # retries interrupted system calls. + if sys.version_info >= (3, 5): + self._py3_selector = Python3Selector() + self._selectors.append(self._py3_selector) + else: + self._py3_selector = None + + def register(self, fd): + assert isinstance(fd, int) + + self._fds.append(fd) + + for sel in self._selectors: + sel.register(fd) + + def unregister(self, fd): + assert isinstance(fd, int) + + self._fds.remove(fd) + + for sel in self._selectors: + sel.unregister(fd) + + def select(self, timeout): + # Try Python 3 selector first. + if self._py3_selector: + try: + return self._py3_selector.select(timeout) + except PermissionError: + # We had a situation (in pypager) where epoll raised a + # PermissionError when a local file descriptor was registered, + # however poll and select worked fine. So, in that case, just + # try using select below. + pass + + try: + # Prefer 'select.select', if we don't have much file descriptors. + # This is more universal. + return self._select_selector.select(timeout) + except ValueError: + # When we have more than 1024 open file descriptors, we'll always + # get a "ValueError: filedescriptor out of range in select()" for + # 'select'. In this case, try, using 'poll' instead. + if self._poll_selector is not None: + return self._poll_selector.select(timeout) + else: + raise + + def close(self): + for sel in self._selectors: + sel.close() + + +class Python3Selector(Selector): + """ + Use of the Python3 'selectors' module. + + NOTE: Only use on Python 3.5 or newer! + """ + def __init__(self): + assert sys.version_info >= (3, 5) + + import selectors # Inline import: Python3 only! + self._sel = selectors.DefaultSelector() + + def register(self, fd): + assert isinstance(fd, int) + import selectors # Inline import: Python3 only! + self._sel.register(fd, selectors.EVENT_READ, None) + + def unregister(self, fd): + assert isinstance(fd, int) + self._sel.unregister(fd) + + def select(self, timeout): + events = self._sel.select(timeout=timeout) + return [key.fileobj for key, mask in events] + + def close(self): + self._sel.close() + + +class PollSelector(Selector): + def __init__(self): + self._poll = select.poll() + + def register(self, fd): + assert isinstance(fd, int) + self._poll.register(fd, select.POLLIN) + + def unregister(self, fd): + assert isinstance(fd, int) + + def select(self, timeout): + tuples = self._poll.poll(timeout) # Returns (fd, event) tuples. + return [t[0] for t in tuples] + + def close(self): + pass # XXX + + +class SelectSelector(Selector): + """ + Wrapper around select.select. + + When the SIGWINCH signal is handled, other system calls, like select + are aborted in Python. This wrapper will retry the system call. + """ + def __init__(self): + self._fds = [] + + def register(self, fd): + self._fds.append(fd) + + def unregister(self, fd): + self._fds.remove(fd) + + def select(self, timeout): + while True: + try: + return select.select(self._fds, [], [], timeout)[0] + except select.error as e: + # Retry select call when EINTR + if e.args and e.args[0] == errno.EINTR: + continue + else: + raise + + def close(self): + pass + + +def select_fds(read_fds, timeout, selector=AutoSelector): + """ + Wait for a list of file descriptors (`read_fds`) to become ready for + reading. This chooses the most appropriate select-tool for use in + prompt-toolkit. + """ + # Map to ensure that we return the objects that were passed in originally. + # Whether they are a fd integer or an object that has a fileno(). + # (The 'poll' implementation for instance, returns always integers.) + fd_map = dict((fd_to_int(fd), fd) for fd in read_fds) + + # Wait, using selector. + sel = selector() + try: + for fd in read_fds: + sel.register(fd) + + result = sel.select(timeout) + + if result is not None: + return [fd_map[fd_to_int(fd)] for fd in result] + finally: + sel.close() diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/utils.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/utils.py new file mode 100644 index 00000000000..ff3a4cfd697 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/utils.py @@ -0,0 +1,23 @@ +from __future__ import unicode_literals +import time + +__all__ = ( + 'TimeIt', +) + + +class TimeIt(object): + """ + Context manager that times the duration of the code body. + The `duration` attribute will contain the execution time in seconds. + """ + def __init__(self): + self.duration = None + + def __enter__(self): + self.start = time.time() + return self + + def __exit__(self, *args): + self.end = time.time() + self.duration = self.end - self.start diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/win32.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/win32.py new file mode 100644 index 00000000000..18e356f0882 --- /dev/null +++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/eventloop/win32.py @@ -0,0 +1,187 @@ +""" +Win32 event loop. + +Windows notes: + - Somehow it doesn't seem to work with the 'ProactorEventLoop'. +""" +from __future__ import unicode_literals + +from ..terminal.win32_input import ConsoleInputReader +from ..win32_types import SECURITY_ATTRIBUTES +from .base import EventLoop, INPUT_TIMEOUT +from .inputhook import InputHookContext +from .utils import TimeIt + +from ctypes import windll, pointer +from ctypes.wintypes import DWORD, BOOL, HANDLE + +import msvcrt +import threading + +__all__ = ( + 'Win32EventLoop', +) + +WAIT_TIMEOUT = 0x00000102 +INPUT_TIMEOUT_MS = int(1000 * INPUT_TIMEOUT) + + +class Win32EventLoop(EventLoop): + """ + Event loop for Windows systems. + + :param recognize_paste: When True, try to discover paste actions and turn + the event into a BracketedPaste. + """ + def __init__(self, inputhook=None, recognize_paste=True): + assert inputhook is None or callable(inputhook) + + self._event = HANDLE(_create_event()) + self._console_input_reader = ConsoleInputReader(recognize_paste=recognize_paste) + self._calls_from_executor = [] + + self.closed = False + self._running = False + + # Additional readers. + self._read_fds = {} # Maps fd to handler. + + # Create inputhook context. + self._inputhook_context = InputHookContext(inputhook) if inputhook else None + + def run(self, stdin, callbacks): + if self.closed: + raise Exception('Event loop already closed.') + + current_timeout = INPUT_TIMEOUT_MS + self._running = True + + while self._running: + # Call inputhook. + with TimeIt() as inputhook_timer: + if self._inputhook_context: + def ready(wait): + " True when there is input ready. The inputhook should return control. " + return bool(self._ready_for_reading(current_timeout if wait else 0)) + self._inputhook_context.call_inputhook(ready) + + # Calculate remaining timeout. (The inputhook consumed some of the time.) + if current_timeout == -1: + remaining_timeout = -1 + else: + remaining_timeout = max(0, current_timeout - int(1000 * inputhook_timer.duration)) + + # Wait for the next event. + handle = self._ready_for_reading(remaining_timeout) + + if handle == self._console_input_reader.handle.value: + # When stdin is ready, read input and reset timeout timer. + keys = self._console_input_reader.read() + for k in keys: + callbacks.feed_key(k) + current_timeout = INPUT_TIMEOUT_MS + + elif handle == self._event.value: + # When the Windows Event has been trigger, process the messages in the queue. + windll.kernel32.ResetEvent(self._event) + self._process_queued_calls_from_executor() + + elif handle in self._read_fds: + callback = self._read_fds[handle] + callback() + else: + # Fire input timeout event. + callbacks.input_timeout() + current_timeout = -1 + + def _ready_for_reading(self, timeout=None): + """ + Return the handle that is ready for reading or `None` on timeout. + """ + handles = [self._event, self._console_input_reader.handle] + handles.extend(self._read_fds.keys()) + return _wait_for_handles(handles, timeout) + + def stop(self): + self._running = False + + def close(self): + self.closed = True + + # Clean up Event object. + windll.kernel32.CloseHandle(self._event) + + if self._inputhook_context: + self._inputhook_context.close() + + self._console_input_reader.close() + + def run_in_executor(self, callback): + """ + Run a long running function in a background thread. + (This is recommended for code that could block the event loop.) + Similar to Twisted's ``deferToThread``. + """ + # Wait until the main thread is idle for an instant before starting the + # executor. (Like in eventloop/posix.py, we start the executor using + # `call_from_executor`.) + def start_executor(): + threading.Thread(target=callback).start() + self.call_from_executor(start_executor) + + def call_from_executor(self, callback, _max_postpone_until=None): + """ + Call this function in the main event loop. + Similar to Twisted's ``callFromThread``. + """ + # Append to list of pending callbacks. + self._calls_from_executor.append(callback) + + # Set Windows event. + windll.kernel32.SetEvent(self._event) + + def _process_queued_calls_from_executor(self): + # Process calls from executor. + calls_from_executor, self._calls_from_executor = self._calls_from_executor, [] + for c in calls_from_executor: + c() + + def add_reader(self, fd, callback): + " Start watching the file descriptor for read availability. " + h = msvcrt.get_osfhandle(fd) + self._read_fds[h] = callback + + def remove_reader(self, fd): + " Stop watching the file descriptor for read availability. " + h = msvcrt.get_osfhandle(fd) + if h in self._read_fds: + del self._read_fds[h] + + +def _wait_for_handles(handles, timeout=-1): + """ + Waits for multiple handles. (Similar to 'select') Returns the handle which is ready. + Returns `None` on timeout. + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v=vs.85).aspx + """ + arrtype = HANDLE * len(handles) + handle_array = arrtype(*handles) + + ret = windll.kernel32.WaitForMultipleObjects( + len(handle_array), handle_array, BOOL(False), DWORD(timeout)) + + if ret == WAIT_TIMEOUT: + return None + else: + h = handle_array[ret] + return h + + +def _create_event(): + """ + Creates a Win32 unnamed Event . + + http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx + """ + return windll.kernel32.CreateEventA(pointer(SECURITY_ATTRIBUTES()), BOOL(True), BOOL(False), None) |