Complete rewrite and removed legacy bash version

This commit is contained in:
2023-09-17 20:26:11 -05:00
parent a7da5d9ed8
commit a7c8e630fa
163 changed files with 27028 additions and 1105 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:`~libs.prompt_toolkit.input.Input` instance.
:param callbacks: :class:`~libs.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:`~libs.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.)
"""

View File

@@ -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:`~libs.prompt_toolkit.eventloop.base.EventLoop`
and :class:`~libs.prompt_toolkit.interface.CommandLineInterface`.
:meth:`~libs.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

View File

@@ -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 libs.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

View File

@@ -0,0 +1,311 @@
from __future__ import unicode_literals
import fcntl
import os
import random
import signal
import threading
import time
from libs.prompt_toolkit.terminal.vt100_input import InputStream
from libs.prompt_toolkit.utils import DummyContext, in_main_thread
from libs.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)
# Handle everything in random order. (To avoid starvation.)
random.shuffle(tasks)
random.shuffle(low_priority_tasks)
# 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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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 = _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:
# 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:
# 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)