Port to mainline kivy and Python 3.8
This required some copy-paste of code from @matham's branch: https://github.com/kivy/kivy/pull/5241 namely, the stuff in the `utils_async.py` module. I've added all that as a standalone file for now. Update the pipfile to use `kivy`'s master branch (since there seems to be some lingering cython issues in the current release wheels).kivy_mainline_and_py3.8
parent
82416ea144
commit
a2e5d07b2c
5
Pipfile
5
Pipfile
|
@ -7,8 +7,8 @@ name = "pypi"
|
||||||
e1839a8 = {path = ".",editable = true}
|
e1839a8 = {path = ".",editable = true}
|
||||||
trio = "*"
|
trio = "*"
|
||||||
Cython = "*"
|
Cython = "*"
|
||||||
# matham's next-gen async port of kivy
|
# use master branch kivy since wheels seem borked (due to cython stuff)
|
||||||
Kivy = {editable = true,git = "git://github.com/matham/kivy.git",ref = "async-loop"}
|
kivy = {git = "git://github.com/kivy/kivy.git"}
|
||||||
pdbpp = "*"
|
pdbpp = "*"
|
||||||
msgpack = "*"
|
msgpack = "*"
|
||||||
tractor = {git = "git://github.com/goodboy/tractor.git"}
|
tractor = {git = "git://github.com/goodboy/tractor.git"}
|
||||||
|
@ -19,3 +19,4 @@ pyside2 = "*"
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
pdbpp = "*"
|
pdbpp = "*"
|
||||||
|
piker = {editable = true,path = "."}
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
'''Async version of :mod:`kivy.utils`.
|
||||||
|
===========================================
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
if os.environ.get('KIVY_EVENTLOOP', 'default') == 'trio':
|
||||||
|
import trio
|
||||||
|
async_lib = trio
|
||||||
|
else:
|
||||||
|
import asyncio
|
||||||
|
async_lib = asyncio
|
||||||
|
trio = None
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncCallbackQueue(object):
|
||||||
|
'''A class for asynchronously iterating values in a queue and waiting
|
||||||
|
for the queue to be updated with new values through a callback function.
|
||||||
|
|
||||||
|
An instance is an async iterator which for every iteration waits for
|
||||||
|
callbacks to add values to the queue and then returns it.
|
||||||
|
|
||||||
|
:Parameters:
|
||||||
|
|
||||||
|
`filter`: callable or None
|
||||||
|
A callable that is called with :meth:`callback`'s positional
|
||||||
|
arguments. When provided, if it returns false, this call is dropped.
|
||||||
|
`convert`: callable or None
|
||||||
|
A callable that is called with :meth:`callback`'s positional
|
||||||
|
arguments. It is called immediately as opposed to async.
|
||||||
|
If provided, the return value of convert is returned by
|
||||||
|
the iterator rather than the original value. Helpful
|
||||||
|
for callback values that need to be processed immediately.
|
||||||
|
`maxlen`: int or None
|
||||||
|
If None, the callback queue may grow to an arbitrary length.
|
||||||
|
Otherwise, it is bounded to maxlen. Once it's full, when new items
|
||||||
|
are added a corresponding number of oldest items are discarded.
|
||||||
|
`thread_fn`: callable or None
|
||||||
|
If reading from the queue is done with a different thread than
|
||||||
|
writing it, this is the callback that schedules in the read thread.
|
||||||
|
|
||||||
|
.. versionadded:: 1.10.1
|
||||||
|
'''
|
||||||
|
|
||||||
|
quit = False
|
||||||
|
|
||||||
|
def __init__(self, filter=None, convert=None, maxlen=None, thread_fn=None,
|
||||||
|
**kwargs):
|
||||||
|
super(AsyncCallbackQueue, self).__init__(**kwargs)
|
||||||
|
self.filter = filter
|
||||||
|
self.convert = convert
|
||||||
|
self.callback_result = deque(maxlen=maxlen)
|
||||||
|
self.thread_fn = thread_fn
|
||||||
|
self.event = async_lib.Event()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def __aiter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __anext__(self):
|
||||||
|
self.event.clear()
|
||||||
|
while not self.callback_result and not self.quit:
|
||||||
|
await self.event.wait()
|
||||||
|
self.event.clear()
|
||||||
|
|
||||||
|
if self.callback_result:
|
||||||
|
return self.callback_result.popleft()
|
||||||
|
raise StopAsyncIteration
|
||||||
|
|
||||||
|
def _thread_reentry(self, *largs, **kwargs):
|
||||||
|
self.event.set()
|
||||||
|
|
||||||
|
def callback(self, *args):
|
||||||
|
'''This (and only this) function may be executed from another thread
|
||||||
|
because the callback may be bound to code executing from an external
|
||||||
|
thread.
|
||||||
|
'''
|
||||||
|
f = self.filter
|
||||||
|
if self.quit or f and not f(*args):
|
||||||
|
return
|
||||||
|
|
||||||
|
convert = self.convert
|
||||||
|
if convert:
|
||||||
|
args = convert(*args)
|
||||||
|
|
||||||
|
self.callback_result.append(args)
|
||||||
|
|
||||||
|
thread_fn = self.thread_fn
|
||||||
|
if thread_fn is None:
|
||||||
|
self.event.set()
|
||||||
|
else:
|
||||||
|
thread_fn(self._thread_reentry)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
'''Stops the iterator and cleans up.
|
||||||
|
'''
|
||||||
|
self.quit = True
|
||||||
|
self.event.set()
|
||||||
|
|
||||||
|
|
||||||
|
def _report_kivy_back_in_trio_thread_fn(task_container):
|
||||||
|
# This function gets scheduled into the trio run loop to deliver the
|
||||||
|
# thread's result.
|
||||||
|
def do_release_then_return_result():
|
||||||
|
# if canceled, do the cancellation otherwise the result.
|
||||||
|
if task_container[1] is not None:
|
||||||
|
task_container[1]()
|
||||||
|
return task_container[2].unwrap()
|
||||||
|
|
||||||
|
result = trio.hazmat.Result.capture(do_release_then_return_result)
|
||||||
|
trio.hazmat.reschedule(task_container[0], result)
|
||||||
|
|
||||||
|
|
||||||
|
async def trio_run_in_kivy_thread(sync_fn, *args, cancellable=False):
|
||||||
|
'''When canceled, executed work is discarded.
|
||||||
|
'''
|
||||||
|
if trio is None:
|
||||||
|
raise Exception('trio is required but was not found')
|
||||||
|
task_container = [None, ] * 4
|
||||||
|
|
||||||
|
def kivy_thread_callback(*largs):
|
||||||
|
# This is the function that runs in the worker thread to do the actual
|
||||||
|
# work and then schedule the calls to report_back_in_trio_thread_fn
|
||||||
|
if task_container[1] is None:
|
||||||
|
task_container[2] = trio.hazmat.Result.capture(sync_fn, *args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
task_container[3].run_sync_soon(
|
||||||
|
_report_kivy_back_in_trio_thread_fn, task_container)
|
||||||
|
except trio.RunFinishedError:
|
||||||
|
# The entire run finished, so our particular tasks are certainly
|
||||||
|
# long gone - it must have cancelled. Continue eating the queue.
|
||||||
|
raise # pass
|
||||||
|
|
||||||
|
@trio.hazmat.enable_ki_protection
|
||||||
|
async def schedule_kivy_thread():
|
||||||
|
await trio.hazmat.checkpoint_if_cancelled()
|
||||||
|
# Holds a reference to the task that's blocked in this function waiting
|
||||||
|
# for the result as well as to the cancel callback and the result
|
||||||
|
# (when not canceled).
|
||||||
|
task_container[0] = trio.hazmat.current_task()
|
||||||
|
task_container[3] = trio.hazmat.current_trio_token()
|
||||||
|
from kivy.clock import Clock
|
||||||
|
Clock.schedule_once(kivy_thread_callback, 0)
|
||||||
|
|
||||||
|
def abort(raise_cancel):
|
||||||
|
if cancellable:
|
||||||
|
task_container[1] = raise_cancel
|
||||||
|
return trio.hazmat.Abort.FAILED
|
||||||
|
return await trio.hazmat.wait_task_rescheduled(abort)
|
||||||
|
|
||||||
|
return await schedule_kivy_thread()
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncBindQueue(AsyncCallbackQueue):
|
||||||
|
'''A class for asynchronously observing kivy properties and events.
|
||||||
|
Creates an async iterator which for every iteration waits and
|
||||||
|
returns the property or event value for every time the property changes
|
||||||
|
or the event is dispatched.
|
||||||
|
The returned value is identical to the list of values passed to a function
|
||||||
|
bound to the event or property with bind. So at minimum it's a one element
|
||||||
|
(for events) or two element (for properties, instance and value) list.
|
||||||
|
:Parameters:
|
||||||
|
`bound_obj`: :class:`EventDispatcher`
|
||||||
|
The :class:`EventDispatcher` instance that contains the property
|
||||||
|
or event being observed.
|
||||||
|
`bound_name`: str
|
||||||
|
The property or event name to observe.
|
||||||
|
`current`: bool
|
||||||
|
Whether the iterator should return the current value on its
|
||||||
|
first class (True) or wait for the first event/property dispatch
|
||||||
|
before having a value (False). Defaults to True.
|
||||||
|
E.g.::
|
||||||
|
async for x, y in AsyncBindQueue(
|
||||||
|
bound_obj=widget, bound_name='size', convert=lambda x: x[1]):
|
||||||
|
print(value)
|
||||||
|
Or::
|
||||||
|
async for touch in AsyncBindQueue(
|
||||||
|
bound_obj=widget, bound_name='on_touch_down',
|
||||||
|
convert=lambda x: x[0]):
|
||||||
|
print(value)
|
||||||
|
.. versionadded:: 1.10.1
|
||||||
|
'''
|
||||||
|
|
||||||
|
bound_obj = None
|
||||||
|
|
||||||
|
bound_name = ''
|
||||||
|
|
||||||
|
bound_uid = 0
|
||||||
|
|
||||||
|
def __init__(self, bound_obj, bound_name, current=True, **kwargs):
|
||||||
|
super(AsyncBindQueue, self).__init__(**kwargs)
|
||||||
|
self.bound_name = bound_name
|
||||||
|
self.bound_obj = bound_obj
|
||||||
|
|
||||||
|
uid = self.bound_uid = bound_obj.fbind(bound_name, self.callback)
|
||||||
|
if not uid:
|
||||||
|
raise ValueError(
|
||||||
|
'{} is not a recognized property or event of {}'
|
||||||
|
''.format(bound_name, bound_obj))
|
||||||
|
|
||||||
|
if current and not bound_obj.is_event_type(bound_name):
|
||||||
|
args = bound_obj, getattr(bound_obj, bound_name)
|
||||||
|
|
||||||
|
f = self.filter
|
||||||
|
if not f or f(args):
|
||||||
|
convert = self.convert
|
||||||
|
if convert:
|
||||||
|
args = convert(args)
|
||||||
|
self.callback_result.append(args)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
super(AsyncBindQueue, self).stop()
|
||||||
|
if self.bound_uid:
|
||||||
|
self.bound_obj.unbind_uid(self.bound_name, self.bound_uid)
|
||||||
|
self.bound_uid = 0
|
||||||
|
self.bound_obj = None
|
||||||
|
|
||||||
|
|
||||||
|
def async_bind(self, bound_name, current=True, **kwargs):
|
||||||
|
'''A convenience method that returns a :class:`AsyncBindQueue` instance
|
||||||
|
initialized with the function parameters. :class:`AsyncBindQueue`
|
||||||
|
can also be instantiated directly.
|
||||||
|
.. versionadded:: 1.10.1
|
||||||
|
'''
|
||||||
|
return AsyncBindQueue(
|
||||||
|
bound_obj=self, bound_name=bound_name, current=current, **kwargs)
|
|
@ -10,6 +10,8 @@ from kivy.uix.textinput import TextInput
|
||||||
from kivy.uix.scrollview import ScrollView
|
from kivy.uix.scrollview import ScrollView
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
from .kivy.utils_async import async_bind
|
||||||
|
|
||||||
log = get_logger('keyboard')
|
log = get_logger('keyboard')
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ async def handle_input(
|
||||||
|
|
||||||
last_patt = []
|
last_patt = []
|
||||||
kb = Window.request_keyboard(_kb_closed, widget, 'text')
|
kb = Window.request_keyboard(_kb_closed, widget, 'text')
|
||||||
keyq = kb.async_bind('on_key_down')
|
keyq = async_bind(kb, 'on_key_down')
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
async for kb, keycode, text, modifiers in keyq:
|
async for kb, keycode, text, modifiers in keyq:
|
||||||
|
@ -79,7 +81,7 @@ async def handle_input(
|
||||||
|
|
||||||
log.debug("Restarting keyboard handling loop")
|
log.debug("Restarting keyboard handling loop")
|
||||||
# rebind to avoid capturing keys processed by ^ coro
|
# rebind to avoid capturing keys processed by ^ coro
|
||||||
keyq = kb.async_bind('on_key_down')
|
keyq = async_bind(kb, 'on_key_down')
|
||||||
|
|
||||||
log.debug("Exitting keyboard handling loop")
|
log.debug("Exitting keyboard handling loop")
|
||||||
|
|
||||||
|
@ -178,7 +180,7 @@ class SearchBar(TextInput):
|
||||||
self.select_all()
|
self.select_all()
|
||||||
|
|
||||||
# wait for <enter> to close search bar
|
# wait for <enter> to close search bar
|
||||||
await self.async_bind('on_text_validate').__aiter__().__anext__()
|
await async_bind(self, 'on_text_validate').__aiter__().__anext__()
|
||||||
log.debug(f"Seach text is {self.text}")
|
log.debug(f"Seach text is {self.text}")
|
||||||
|
|
||||||
log.debug("Closing search bar")
|
log.debug("Closing search bar")
|
||||||
|
|
Loading…
Reference in New Issue