Add CoreFoundationEventLoop directly to avoid pulling in ALL of PyObjC

This commit is contained in:
Danielle McLean 2023-11-27 10:43:24 +11:00
parent dae3e49783
commit da90e64dac
Signed by: 00dani
GPG key ID: 52C059C3B22A753E
4 changed files with 190 additions and 0 deletions

View file

@ -0,0 +1,3 @@
from .eventloop import CoreFoundationEventLoop
__all__ = ('CoreFoundationEventLoop',)

View file

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

View file

@ -0,0 +1 @@
from .eventloop import CoreFoundationEventLoop as CoreFoundationEventLoop

View file

@ -0,0 +1,4 @@
import asyncio
class CoreFoundationEventLoop(asyncio.SelectorEventLoop):
def __init__(self, console_app: bool = ..., *eventloop_args) -> None: ...