From da90e64dac0af0f6a32ec2b4b8672495e0b29c63 Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Mon, 27 Nov 2023 10:43:24 +1100 Subject: [PATCH] Add CoreFoundationEventLoop directly to avoid pulling in ALL of PyObjC --- src/corefoundationasyncio/__init__.py | 3 + src/corefoundationasyncio/eventloop.py | 182 ++++++++++++++++++++ typings/corefoundationasyncio/__init__.pyi | 1 + typings/corefoundationasyncio/eventloop.pyi | 4 + 4 files changed, 190 insertions(+) create mode 100644 src/corefoundationasyncio/__init__.py create mode 100644 src/corefoundationasyncio/eventloop.py create mode 100644 typings/corefoundationasyncio/__init__.pyi create mode 100644 typings/corefoundationasyncio/eventloop.pyi diff --git a/src/corefoundationasyncio/__init__.py b/src/corefoundationasyncio/__init__.py new file mode 100644 index 0000000..46bc6b2 --- /dev/null +++ b/src/corefoundationasyncio/__init__.py @@ -0,0 +1,3 @@ +from .eventloop import CoreFoundationEventLoop + +__all__ = ('CoreFoundationEventLoop',) diff --git a/src/corefoundationasyncio/eventloop.py b/src/corefoundationasyncio/eventloop.py new file mode 100644 index 0000000..c375699 --- /dev/null +++ b/src/corefoundationasyncio/eventloop.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# Copied from https://github.com/alberthier/corefoundationasyncio/blob/5061b9b7daa8bcd40d54d58432d84dcc0a339ca6/corefoundationasyncio.py +# This module is copied, rather than simply installed, because the PyPI version of this module depends on *all* of PyObjC even though it only needs pyobjc-framework-Cocoa. +# There's an open PR to fix this: httpsV//github.com/alberthier/corefoundationasyncio/pull/3 + + +import asyncio +import sys +import threading + +from CoreFoundation import ( + CFRunLoopGetCurrent, + CFRunLoopTimerCreate, CFRunLoopAddTimer, CFRunLoopRemoveTimer, CFAbsoluteTimeGetCurrent, + CFFileDescriptorCreate, CFFileDescriptorIsValid, CFFileDescriptorEnableCallBacks, CFFileDescriptorDisableCallBacks, + CFFileDescriptorCreateRunLoopSource, CFRunLoopAddSource, CFRunLoopRemoveSource, + kCFAllocatorDefault, kCFRunLoopDefaultMode, kCFRunLoopCommonModes, + kCFFileDescriptorReadCallBack, kCFFileDescriptorWriteCallBack +) +import PyObjCTools.AppHelper + +class _TimerHandle(asyncio.TimerHandle): + + def __init__(self, when, callback, args, loop, context): + super().__init__(when, callback, args, loop, context) + self.cf_runloop_timer = None + + +class _FDEntry: + + def __init__(self): + self.cf_fd = None + self.cf_source = None + self.callbacks = {} + + +class CoreFoundationEventLoop(asyncio.SelectorEventLoop): + """ + Event loop based on CoreFoundation's CFRunLoop. + This allows integration of Cocoa GUI apps with asyncio + """ + + def __init__(self, console_app = False, *eventloop_args): + self._console_app = console_app + self._eventloop_args = eventloop_args + self._registered_fds = {} + self._runloop = CFRunLoopGetCurrent() + super().__init__() + + # Running and stopping the event loop. + + def run_forever(self): + """Run until stop() is called.""" + self._check_closed() + if self.is_running(): + raise RuntimeError('This event loop is already running') + if asyncio._get_running_loop() is not None: + raise RuntimeError('Cannot run the event loop while another loop is running') + self._set_coroutine_origin_tracking(self._debug) + self._thread_id = threading.get_ident() + + old_agen_hooks = sys.get_asyncgen_hooks() + sys.set_asyncgen_hooks(firstiter=self._asyncgen_firstiter_hook, + finalizer=self._asyncgen_finalizer_hook) + try: + asyncio._set_running_loop(self) + if self._console_app: + PyObjCTools.AppHelper.runConsoleEventLoop(*self._eventloop_args) + else: + PyObjCTools.AppHelper.runEventLoop(*self._eventloop_args) + finally: + self._thread_id = None + asyncio._set_running_loop(None) + self._set_coroutine_origin_tracking(False) + sys.set_asyncgen_hooks(*old_agen_hooks) + + def _process_events(self, event_list): + raise NotImplementedError("Not available in this implementation") + + def _run_once(self): + raise NotImplementedError("Not available in this implementation") + + def stop(self): + PyObjCTools.AppHelper.stopEventLoop() + + # Methods scheduling callbacks. All these return Handles. + + def call_at(self, when, callback, *args, context=None): + self._check_closed() + if self._debug: + self._check_thread() + self._check_callback(callback, 'call_at') + timerHandle = _TimerHandle(when, callback, args, self, context) + if timerHandle._source_traceback: + del timerHandle._source_traceback[-1] + self._add_callback(timerHandle) + return timerHandle + + def _call_soon(self, callback, args, context): + handle = asyncio.Handle(callback, args, self, context) + self._add_callback(handle) + return handle + + def _add_callback(self, handle): + assert isinstance(handle, asyncio.Handle), 'A Handle is required here' + is_timer = isinstance(handle, _TimerHandle) + if handle.cancelled(): + return + def ontimeout(cf_timer, info): + if not handle.cancelled(): + handle._run() + when = handle.when() if is_timer else self.time() + cf_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, when, 0, 0, 0, ontimeout, None) + CFRunLoopAddTimer(self._runloop, cf_timer, kCFRunLoopCommonModes) + if is_timer: + handle.cf_runloop_timer = cf_timer + handle._scheduled = True + + def _timer_handle_cancelled(self, handle): + CFRunLoopRemoveTimer(self._runloop, handle.cf_runloop_timer, kCFRunLoopCommonModes) + + def time(self): + return CFAbsoluteTimeGetCurrent() + + # Ready-based callback registration methods. + # The add_*() methods return None. + # The remove_*() methods return True if something was removed, + # False if there was nothing to delete. + + def _register_fd(self, fd, event, callback, args): + entry = self._registered_fds.get(fd) + if entry is not None: + CFFileDescriptorEnableCallBacks(entry.cf_fd, event) + entry.callbacks[event] = (callback, args) + else: + def fd_callback(file_desc, callback_types, entry): + if callback_types & kCFFileDescriptorReadCallBack: + (callback, args) = entry.callbacks[kCFFileDescriptorReadCallBack] + callback(*args) + if callback_types & kCFFileDescriptorWriteCallBack: + (callback, args) = entry.callbacks[kCFFileDescriptorWriteCallBack] + callback(*args) + if CFFileDescriptorIsValid(file_desc): + CFFileDescriptorEnableCallBacks(file_desc, callback_types) + + entry = _FDEntry() + entry.cf_fd = CFFileDescriptorCreate(kCFAllocatorDefault, fd, 0, fd_callback, entry) + CFFileDescriptorEnableCallBacks(entry.cf_fd, event) + entry.callbacks[event] = (callback, args) + entry.cf_source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, entry.cf_fd, 0) + self._registered_fds[fd] = entry + CFRunLoopAddSource(self._runloop, entry.cf_source, kCFRunLoopDefaultMode) + + def _unregister_fd(self, fd, event): + entry = self._registered_fds.pop(fd, None) + if entry is None: + return False + cb = entry.callbacks.pop(event, None) + if cb is None: + return False + if len(entry.callbacks) != 0: + CFFileDescriptorDisableCallBacks(entry.cf_fd, event) + else: + CFRunLoopRemoveSource(self._runloop, entry.cf_source, kCFRunLoopDefaultMode) + return True + + def _add_reader(self, fd, callback, *args): + self._check_closed() + self._register_fd(fd, kCFFileDescriptorReadCallBack, callback, args) + + def _remove_reader(self, fd): + if self.is_closed(): + return False + return self._unregister_fd(fd, kCFFileDescriptorReadCallBack) + + def _add_writer(self, fd, callback, *args): + self._check_closed() + self._register_fd(fd, kCFFileDescriptorWriteCallBack, callback, args) + + def _remove_writer(self, fd): + if self.is_closed(): + return False + return self._unregister_fd(fd, kCFFileDescriptorWriteCallBack) diff --git a/typings/corefoundationasyncio/__init__.pyi b/typings/corefoundationasyncio/__init__.pyi new file mode 100644 index 0000000..628b119 --- /dev/null +++ b/typings/corefoundationasyncio/__init__.pyi @@ -0,0 +1 @@ +from .eventloop import CoreFoundationEventLoop as CoreFoundationEventLoop diff --git a/typings/corefoundationasyncio/eventloop.pyi b/typings/corefoundationasyncio/eventloop.pyi new file mode 100644 index 0000000..c5f0171 --- /dev/null +++ b/typings/corefoundationasyncio/eventloop.pyi @@ -0,0 +1,4 @@ +import asyncio + +class CoreFoundationEventLoop(asyncio.SelectorEventLoop): + def __init__(self, console_app: bool = ..., *eventloop_args) -> None: ...