pax_global_header00006660000000000000000000000064133332050640014511gustar00rootroot0000000000000052 comment=39f09afeb83c4fb6361b11d5f71a7fa4498620f0 gbulb-0.6.1/000077500000000000000000000000001333320506400126105ustar00rootroot00000000000000gbulb-0.6.1/.drone.yml000066400000000000000000000003501333320506400145160ustar00rootroot00000000000000matrix: PYTHON_VERSION: - 3.5.5 - 3.6.6 - 3.7.0 pipeline: build: image: nathanhoad/gbulb-python:${PYTHON_VERSION} commands: - python setup.py install - pytest gbulb-0.6.1/.gitignore000066400000000000000000000000151333320506400145740ustar00rootroot00000000000000*.pyc .*.swp gbulb-0.6.1/AUTHORS.rst000066400000000000000000000001241333320506400144640ustar00rootroot00000000000000Anthony Baire Jan Lübbe Krzysztof Kotlenga Nathan Hoad montag451 Brecht De Vlieger gbulb-0.6.1/CHANGELOG.md000066400000000000000000000040761333320506400144300ustar00rootroot00000000000000# Change Log ## [0.6.1] - 2018-08-09 ### Fixed - Support for 3.7, for real this time. Thank you Philippe Normand! ## [0.6.0] - 2018-08-06 ### Fixed - Support for 3.7. ### Added - Preliminary Windows support. Please note that using subprocesses is known not to work. Patches welcome. ### Changed - Support for 3.4 and below has been dropped. ## [0.5.3] - 2017-01-27 ### Fixed - Implemented child watcher setters and getters to allow writing tests with asynctest for code using gbulb. - `gbulb.install` now monkey patches `asyncio.SafeChildWatcher` to `gbulb.glib_events.GLibChildWatcher`, to ensure that any library code that uses it will use the correct child watcher. ## [0.5.2] - 2017-01-21 ### Fixed - Fixed a sporadic test hang. ## [0.5.1] - 2017-01-20 ### Fixed - Fixed breakage on Python versions older than 3.5.3, caused by 0.5.0. Thanks Brecht De Vlieger! ## [0.5] - 2017-01-12 ### Fixed - Fixed issue with readers and writers not being added to the loop properly as a result of http://bugs.python.org/issue28369. ## [0.4] - 2016-10-26 ### Fixed - gbulb will no longer allow you to schedule coroutines with call_at, call_soon and call_later, the same as asyncio. ## [0.3] - 2016-09-13 ### Fixed - gbulb will no longer occasionally leak memory when used with threads. ## [0.2] - 2016-03-20 ### Added - `gbulb.install` to simplify installation of a GLib-based event loop in asyncio - Connecting sockets now works as intended - Implement `call_soon_threadsafe` - Lots of tests ### Changed - **API BREAKAGE** No implicit Gtk import anymore. `GtkEventLoop` and `GtkEventLoopPolicy` have been moved to `gbulb.gtk` - **API BREAKAGE** No more `threads`, `default` or `full` parameters for event loop policy objects. gbulb now does nothing with threads - **API BREAKAGE** `gbulb.get_default_loop` has been removed - Permit running event loops recursively via `.run()` ### Fixed - Default signal handling of SIGINT - `gbulb.wait_signal.cancel()` now obeys the interface defined by `asyncio.Future` ## [0.1] - 2013-09-20 - Initial release gbulb-0.6.1/LICENSE000066400000000000000000000010501333320506400136110ustar00rootroot00000000000000Copyright 2015 Nathan Hoad Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gbulb-0.6.1/MANIFEST.in000066400000000000000000000003151333320506400143450ustar00rootroot00000000000000include AUTHORS.rst include CHANGELOG.md include LICENSE include README.md recursive-include examples * recursive-exclude examples *.pyc *.pyo recursive-include tests * recursive-exclude tests *.pyc *.pyo gbulb-0.6.1/README.md000066400000000000000000000067051333320506400140770ustar00rootroot00000000000000# gbulb - a PEP 3156 event loop based on GLib [![Build Status](http://drone.getoffmalawn.com/api/badges/nathan-hoad/gbulb/status.svg)](http://drone.getoffmalawn.com/nathan-hoad/gbulb) Gbulb is a Python library that implements a [PEP 3156][PEP3156] interface for the [GLib main event loop][glibloop] under UNIX-like systems. As much as possible, except where noted below, it mimics asyncio's interface. If you notice any differences, please report them. Nathan Hoad ## Licence Apache 2.0 ## Homepage [https://github.com/nathan-hoad/gbulb](https://github.com/nathan-hoad/gbulb) ## Requirements - python3.5+ - pygobject - glib - gtk+3 (optional) ## Usage ### GLib event loop import asyncio, gbulb gbulb.install() asyncio.get_event_loop().run_forever() ### Gtk+ event loop *(suitable for GTK+ applications)* import asyncio, gbulb gbulb.install(gtk=True) asyncio.get_event_loop().run_forever() ### GApplication/GtkApplication event loop import asyncio, gbulb gbulb.install(gtk=True) # only necessary if you're using GtkApplication loop = asyncio.get_event_loop() loop.run_forever(application=my_gapplication_object) ### Waiting on a signal asynchronously See examples/wait_signal.py ## Known issues - Windows is not supported, sorry. If you are interested in this, please help me get it working! I don't have Windows so I can't test it. ## Divergences with PEP 3156 In GLib, the concept of event loop is split in two classes: GLib.MainContext and GLib.MainLoop. The event loop is mostly implemented by MainContext. MainLoop is just a wrapper that implements the run() and quit() functions. MainLoop.run() atomically acquires a MainContext and repeatedly calls MainContext.iteration() until MainLoop.quit() is called. A MainContext is not bound to a particular thread, however it cannot be used by multiple threads concurrently. If the context is owned by another thread, then MainLoop.run() will block until the context is released by the other thread. MainLoop.run() may be called recursively by the same thread (this is mainly used for implementing modal dialogs in Gtk). The issue: given a context, GLib provides no ways to know if there is an existing event loop running for that context. It implies the following divergences with PEP 3156: - .run_forever() and .run_until_complete() are not guaranteed to run immediately. If the context is owned by another thread, then they will block until the context is released by the other thread. - .stop() is relevant only when the currently running Glib.MainLoop object was created by this asyncio object (i.e. by calling .run_forever() or .run_until_complete()). The event loop will quit only when it regains control of the context. This can happen in two cases: 1. when multiple event loop are enclosed (by creating new MainLoop objects and calling .run() recursively) 2. when the event loop has not even yet started because it is still trying to acquire the context It would be wiser not to use any recursion at all. GLibEventLoop will actually prevent you from doing that (in accordance with PEP 3156), however GtkEventLoop will allow you to call run() recursively. You should also keep in mind that enclosed loops may be started at any time by third-party code calling GLib's primitives. [PEP3156]: http://www.python.org/dev/peps/pep-3156/ [glibloop]: https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html gbulb-0.6.1/examples/000077500000000000000000000000001333320506400144265ustar00rootroot00000000000000gbulb-0.6.1/examples/gtk.py000066400000000000000000000012151333320506400155640ustar00rootroot00000000000000import asyncio import gbulb from gi.repository import Gtk @asyncio.coroutine def counter(label): i = 0 while True: label.set_text(str(i)) print('incrementing', i) yield from asyncio.sleep(1) i += 1 def main(): gbulb.install(gtk=True) loop = gbulb.get_event_loop() display = Gtk.Entry() vbox = Gtk.VBox() vbox.pack_start(display, True, True, 0) win = Gtk.Window(title='Counter window') win.connect('delete-event', lambda *args: loop.stop()) win.add(vbox) win.show_all() asyncio.async(counter(display)) loop.run_forever() if __name__ == '__main__': main() gbulb-0.6.1/examples/test-gtk.py000077500000000000000000000041231333320506400165450ustar00rootroot00000000000000#!/usr/bin/env python3 from gi.repository import Gtk import asyncio import gbulb import gbulb.gtk class ProgressBarWindow(Gtk.Window): def __init__(self): Gtk.Window.__init__(self, title="ProgressBar Demo") self.set_border_width(10) vbox = Gtk.VBox() self.add(vbox) self.progressbar = Gtk.ProgressBar() vbox.pack_start(self.progressbar, True, True, 0) button = Gtk.Button("Magic button") button.connect("clicked", self.on_magic) vbox.pack_start(button, True, True, 0) self._magic_button = button button = Gtk.Button("Stop button") button.connect("clicked", self.on_stop) vbox.pack_start(button, True, True, 0) self._stop_button = button self._running = False def on_magic(self, button): def coro(): try: yield from gbulb.wait_signal(self._magic_button, "clicked") self.progressbar.set_text("blah blah!") self.progressbar.set_fraction(0.50) yield from asyncio.sleep(1) self.progressbar.set_fraction(0.75) self.progressbar.set_text("pouet pouet!") yield from gbulb.wait_signal(self._magic_button, "clicked") self.progressbar.set_fraction(1.0) self.progressbar.set_text("done!") yield from asyncio.sleep(1) finally: self.progressbar.set_fraction(0.0) self.progressbar.set_show_text(False) self._running = False if not self._running: self.progressbar.set_fraction(0.25) self.progressbar.set_text("do some magic!") self.progressbar.set_show_text(True) self._running = asyncio.async(coro()) def on_stop(self, button): if self._running: self._running.cancel() asyncio.set_event_loop_policy(gbulb.gtk.GtkEventLoopPolicy()) win = ProgressBarWindow() win.connect("delete-event", lambda *args: loop.stop()) win.show_all() loop = asyncio.get_event_loop() loop.run_forever() gbulb-0.6.1/examples/wait_signal.py000066400000000000000000000014741333320506400173070ustar00rootroot00000000000000import asyncio import gbulb from gi.repository import Gtk @asyncio.coroutine def counter(label): i = 0 while True: label.set_text(str(i)) yield from asyncio.sleep(1) i += 1 @asyncio.coroutine def text_watcher(label): while True: yield from gbulb.wait_signal(label, 'changed') print('label changed', label.get_text()) def main(): gbulb.install(gtk=True) loop = gbulb.get_event_loop() display = Gtk.Entry() vbox = Gtk.VBox() vbox.pack_start(display, True, True, 0) win = Gtk.Window(title='Counter window') win.connect('delete-event', lambda *args: loop.stop()) win.add(vbox) win.show_all() asyncio.async(text_watcher(display)) asyncio.async(counter(display)) loop.run_forever() if __name__ == '__main__': main() gbulb-0.6.1/gbulb/000077500000000000000000000000001333320506400137035ustar00rootroot00000000000000gbulb-0.6.1/gbulb/__init__.py000066400000000000000000000000601333320506400160100ustar00rootroot00000000000000from .glib_events import * from .utils import * gbulb-0.6.1/gbulb/glib_events.py000066400000000000000000000762361333320506400165740ustar00rootroot00000000000000"""PEP 3156 event loop based on GLib""" import asyncio import os import signal import socket import sys import threading import weakref from asyncio import constants, events, futures, sslproto, tasks from gi.repository import GLib, Gio from . import transports if hasattr(os, 'set_blocking'): def _set_nonblocking(fd): os.set_blocking(fd, False) elif sys.platform == 'win32': def _set_nonblocking(fd): pass else: import fcntl def _set_nonblocking(fd): flags = fcntl.fcntl(fd, fcntl.F_GETFL) flags = flags | os.O_NONBLOCK fcntl.fcntl(fd, fcntl.F_SETFL, flags) __all__ = ['GLibEventLoop', 'GLibEventLoopPolicy'] # The Windows `asyncio` implementation doesn't actually use this, but # `glib` abstracts so nicely over this that we can use it on any platform if sys.platform == "win32": class AbstractChildWatcher: pass else: from asyncio.unix_events import AbstractChildWatcher class GLibChildWatcher(AbstractChildWatcher): def __init__(self): self._sources = {} self._handles = {} # On windows on has to open a process handle for the given PID number # before it's possible to use GLib's `child_watch_add` on it if sys.platform == "win32": def _create_handle_for_pid(self, pid): import _winapi return _winapi.OpenProcess(0x00100400, 0, pid) def _close_process_handle(self, handle): import _winapi _winapi.CloseHandle(handle) else: _create_handle_for_pid = lambda self, pid: pid _close_process_handle = lambda self, pid: None def attach_loop(self, loop): # just ignored pass def add_child_handler(self, pid, callback, *args): self.remove_child_handler(pid) handle = self._create_handle_for_pid(pid) source = GLib.child_watch_add(0, handle, self.__callback__) self._sources[pid] = source, callback, args, handle self._handles[handle] = pid def remove_child_handler(self, pid): try: source, callback, args, handle = self._sources.pop(pid) assert self._handles.pop(handle) == pid except KeyError: return False self._close_process_handle(handle) GLib.source_remove(source) return True def close(self): for source, callback, args, handle in self._sources.values(): self._close_process_handle(handle) GLib.source_remove(source) self._sources = {} self._handles = {} def __enter__(self): return self def __exit__(self, a, b, c): pass def __callback__(self, handle, status): try: pid = self._handles.pop(handle) source, callback, args, handle = self._sources.pop(pid) except KeyError: return self._close_process_handle(handle) GLib.source_remove(source) if hasattr(os, "WIFSIGNALED") and os.WIFSIGNALED(status): returncode = -os.WTERMSIG(status) elif hasattr(os, "WIFEXITED") and os.WIFEXITED(status): returncode = os.WEXITSTATUS(status) # FIXME: Hack for adjusting invalid status returned by GLIB # Looks like there is a bug in glib or in pygobject if returncode > 128: returncode = 128 - returncode else: returncode = status callback(pid, returncode, *args) class GLibHandle(events.Handle): __slots__ = ('_source', '_repeat', '_context') def __init__(self, *, loop, source, repeat, callback, args, context=None): super().__init__(callback, args, loop) if sys.version_info[:2] >= (3, 7) and context is None: import contextvars context = contextvars.copy_context() self._context = context self._source = source self._repeat = repeat loop._handlers.add(self) source.set_callback(self.__callback__, self) source.attach(loop._context) def cancel(self): super().cancel() self._source.destroy() self._loop._handlers.discard(self) def __callback__(self, ignore_self): # __callback__ is called within the MainContext object, which is # important in case that code includes a `Gtk.main()` or some such. # Otherwise what happens is the loop is started recursively, but the # callbacks don't finish firing, so they can't be rescheduled. self._run() if not self._repeat: self._source.destroy() self._loop._handlers.discard(self) return self._repeat if sys.platform == "win32": class GLibBaseEventLoopPlatformExt: def __init__(self): pass def close(self): pass else: from asyncio import unix_events class GLibBaseEventLoopPlatformExt(unix_events.SelectorEventLoop): """ Semi-hack that allows us to leverage the existing implementation of Unix domain sockets without having to actually implement a selector based event loop. Note that both `__init__` and `close` DO NOT and SHOULD NOT ever call their parent implementation! """ def __init__(self): self._sighandlers = {} def close(self): for sig in list(self._sighandlers): self.remove_signal_handler(sig) def add_signal_handler(self, sig, callback, *args): self.remove_signal_handler(sig) s = GLib.unix_signal_source_new(sig) if s is None: # Show custom error messages for signal that are uncatchable if sig == signal.SIGKILL: raise RuntimeError("cannot catch SIGKILL") elif sig == signal.SIGSTOP: raise RuntimeError("cannot catch SIGSTOP") else: raise ValueError("signal not supported") assert sig not in self._sighandlers self._sighandlers[sig] = GLibHandle( loop=self, source=s, repeat=True, callback=callback, args=args) def remove_signal_handler(self, sig): try: self._sighandlers.pop(sig).cancel() return True except KeyError: return False class _BaseEventLoop(asyncio.BaseEventLoop): """ Extra inheritance step that needs to be inserted so that we only ever indirectly inherit from `asyncio.BaseEventLoop`. This is necessary as the Unix implementation will also indirectly inherit from that class (thereby creating diamond inheritance). Python permits and fully supports diamond inheritance so this is not a problem. However it is, on the other hand, not permitted to inherit from a class both directly *and* indirectly – hence we add this intermediate class to make sure that can never happen (see https://stackoverflow.com/q/29214888 for a minimal example a forbidden inheritance tree) and https://www.python.org/download/releases/2.3/mro/ for some extensive documentation of the allowed inheritance structures in python. """ class GLibBaseEventLoop(_BaseEventLoop, GLibBaseEventLoopPlatformExt): def __init__(self, context=None): self._handlers = set() self._accept_futures = {} self._context = context or GLib.MainContext() self._selector = self self._transports = weakref.WeakValueDictionary() self._readers = {} self._writers = {} self._channels = weakref.WeakValueDictionary() _BaseEventLoop.__init__(self) GLibBaseEventLoopPlatformExt.__init__(self) def close(self): for future in self._accept_futures.values(): future.cancel() self._accept_futures.clear() for s in list(self._handlers): s.cancel() self._handlers.clear() GLibBaseEventLoopPlatformExt.close(self) _BaseEventLoop.close(self) def select(self, timeout=None): self._context.acquire() try: if timeout is None: self._context.iteration(True) elif timeout <= 0: self._context.iteration(False) else: # Schedule fake callback that will trigger an event and cause the loop to terminate # after the given number of seconds handle = GLibHandle( loop=self, source=GLib.Timeout(timeout*1000), repeat=False, callback=lambda: None, args=()) try: self._context.iteration(True) finally: handle.cancel() return () # Available events are dispatched immediately and not returned finally: self._context.release() def _make_socket_transport(self, sock, protocol, waiter=None, *, extra=None, server=None): """Create socket transport.""" return transports.SocketTransport(self, sock, protocol, waiter, extra, server) def _make_ssl_transport(self, rawsock, protocol, sslcontext, waiter=None, *, server_side=False, server_hostname=None, extra=None, server=None, ssl_handshake_timeout=None): """Create SSL transport.""" # sslproto._is_sslproto_available was removed from asyncio, starting from Python 3.7. if hasattr(sslproto, '_is_sslproto_available') and not sslproto._is_sslproto_available(): raise NotImplementedError("Proactor event loop requires Python 3.5" " or newer (ssl.MemoryBIO) to support " "SSL") # Support for the ssl_handshake_timeout keyword argument was added in Python 3.7. extra_protocol_kwargs = {} if sys.version_info[:2] >= (3, 7): extra_protocol_kwargs['ssl_handshake_timeout'] = ssl_handshake_timeout ssl_protocol = sslproto.SSLProtocol(self, protocol, sslcontext, waiter, server_side, server_hostname, **extra_protocol_kwargs) transports.SocketTransport(self, rawsock, ssl_protocol, extra=extra, server=server) return ssl_protocol._app_transport def _make_datagram_transport(self, sock, protocol, address=None, waiter=None, extra=None): """Create datagram transport.""" return transports.DatagramTransport(self, sock, protocol, address, waiter, extra) def _make_read_pipe_transport(self, pipe, protocol, waiter=None, extra=None): """Create read pipe transport.""" channel = self._channel_from_fileobj(pipe) return transports.PipeReadTransport(self, channel, protocol, waiter, extra) def _make_write_pipe_transport(self, pipe, protocol, waiter=None, extra=None): """Create write pipe transport.""" channel = self._channel_from_fileobj(pipe) return transports.PipeWriteTransport(self, channel, protocol, waiter, extra) @asyncio.coroutine def _make_subprocess_transport(self, protocol, args, shell, stdin, stdout, stderr, bufsize, extra=None, **kwargs): """Create subprocess transport.""" with events.get_child_watcher() as watcher: waiter = asyncio.Future(loop=self) transport = transports.SubprocessTransport(self, protocol, args, shell, stdin, stdout, stderr, bufsize, waiter=waiter, extra=extra, **kwargs) watcher.add_child_handler(transport.get_pid(), self._child_watcher_callback, transport) try: yield from waiter except Exception as exc: err = exc else: err = None if err is not None: transport.close() yield from transport._wait() raise err return transport def _child_watcher_callback(self, pid, returncode, transport): self.call_soon_threadsafe(transport._process_exited, returncode) def _write_to_self(self): self._context.wakeup() def _process_events(self, event_list): """Process selector events.""" pass # This is already done in `.select()` def _start_serving(self, protocol_factory, sock, sslcontext=None, server=None, backlog=100, ssl_handshake_timeout=getattr(constants, 'SSL_HANDSHAKE_TIMEOUT', 60.0)): self._transports[sock.fileno()] = server def server_loop(f=None): try: if f is not None: (conn, addr) = f.result() protocol = protocol_factory() if sslcontext is not None: # FIXME: add ssl_handshake_timeout to this call once 3.7 support is merged in. self._make_ssl_transport( conn, protocol, sslcontext, server_side=True, extra={'peername': addr}, server=server) else: self._make_socket_transport( conn, protocol, extra={'peername': addr}, server=server) if self.is_closed(): return f = self.sock_accept(sock) except OSError as exc: if sock.fileno() != -1: self.call_exception_handler({ 'message': 'Accept failed on a socket', 'exception': exc, 'socket': sock, }) sock.close() except futures.CancelledError: sock.close() else: self._accept_futures[sock.fileno()] = f f.add_done_callback(server_loop) self.call_soon(server_loop) def _stop_serving(self, sock): if sock.fileno() in self._accept_futures: self._accept_futures[sock.fileno()].cancel() sock.close() def _check_not_coroutine(self, callback, name): """Check whether the given callback is a coroutine or not.""" from asyncio import coroutines if (coroutines.iscoroutine(callback) or coroutines.iscoroutinefunction(callback)): raise TypeError("coroutines cannot be used with {}()".format(name)) def _ensure_fd_no_transport(self, fd): """Ensure that the given file descriptor is NOT used by any transport. Adding another reader to a fd that is already being waited for causes a hang on Windows.""" try: transport = self._transports[fd] except KeyError: pass else: if not hasattr(transport, "is_closing") or not transport.is_closing(): raise RuntimeError('File descriptor {!r} is used by transport {!r}' .format(fd, transport)) def _channel_from_socket(self, sock): """Create GLib IOChannel for the given file object. This function will cache weak references to `GLib.Channel` objects it previously has created to prevent weird issues that can occur when two GLib channels point to the same underlying socket resource. On windows this will only work for network sockets. """ fd = self._fileobj_to_fd(sock) sock_id = id(sock) try: channel = self._channels[sock_id] except KeyError: if sys.platform == "win32": channel = GLib.IOChannel.win32_new_socket(fd) else: channel = GLib.IOChannel.unix_new(fd) # disabling buffering requires setting the encoding to None channel.set_encoding(None) channel.set_buffered(False) self._channels[sock_id] = channel return channel def _channel_from_fileobj(self, fileobj): """Create GLib IOChannel for the given file object. On windows this will only work for files and pipes returned GLib's C library. """ fd = self._fileobj_to_fd(fileobj) # pipes have been shown to be blocking here, so we'll do someone # else's job for them. _set_nonblocking(fd) if sys.platform == "win32": channel = GLib.IOChannel.win32_new_fd(fd) else: channel = GLib.IOChannel.unix_new(fd) # disabling buffering requires setting the encoding to None channel.set_encoding(None) channel.set_buffered(False) return channel def _fileobj_to_fd(self, fileobj): """Obtain the raw file descriptor number for the given file object.""" if isinstance(fileobj, int): fd = fileobj else: try: fd = int(fileobj.fileno()) except (AttributeError, TypeError, ValueError): raise ValueError("Invalid file object: {!r}".format(fileobj)) if fd < 0: raise ValueError("Invalid file descriptor: {}".format(fd)) return fd def _delayed(self, source, callback=None, *args): """Create a future that will complete after the given GLib Source object has become ready and the data it tracks has been processed.""" future = None def handle_ready(*args): try: if callback: (done, result) = callback(*args) else: (done, result) = (True, None) if done: future.set_result(result) future.handle.cancel() except Exception as error: if not future.cancelled(): future.set_exception(error) future.handle.cancel() # Create future and properly wire up it's cancellation with the # handle's cancellation machinery future = asyncio.Future(loop=self) future.handle = GLibHandle( loop=self, source=source, repeat=True, callback=handle_ready, args=args ) return future def _socket_handle_errors(self, sock): """Raise exceptions for error states (SOL_ERROR) on the given socket object.""" errno = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) if errno != 0: if sys.platform == "win32": msg = socket.errorTab.get(errno, "Error {0}".format(errno)) raise OSError(errno, "[WinError {0}] {1}".format(errno, msg), None, errno) else: raise OSError(errno, os.strerror(errno)) ############################### # Low-level socket operations # ############################### def sock_connect(self, sock, address): # Request connection on socket (it is expected that `sock` is already non-blocking) try: sock.connect(address) except BlockingIOError: pass # Create glib IOChannel for socket and wait for it to become writable channel = self._channel_from_socket(sock) source = GLib.io_create_watch(channel, GLib.IO_OUT) def sock_finish_connect(sock): self._socket_handle_errors(sock) return (True, sock) return self._delayed(source, sock_finish_connect, sock) def sock_accept(self, sock): channel = self._channel_from_socket(sock) source = GLib.io_create_watch(channel, GLib.IO_IN) def sock_connection_received(sock): return (True, sock.accept()) @asyncio.coroutine def accept_coro(future, conn): # Coroutine closing the accept socket if the future is cancelled try: return (yield from future) except futures.CancelledError: sock.close() raise future = self._delayed(source, sock_connection_received, sock) return self.create_task(accept_coro(future, sock)) def sock_recv(self, sock, nbytes, flags=0): channel = self._channel_from_socket(sock) read_func = lambda channel, nbytes: sock.recv(nbytes, flags) return self._channel_read(channel, nbytes, read_func) def sock_recvfrom(self, sock, nbytes, flags=0): channel = self._channel_from_socket(sock) read_func = lambda channel, nbytes: sock.recvfrom(nbytes, flags) return self._channel_read(channel, nbytes, read_func) def sock_sendall(self, sock, buf, flags=0): channel = self._channel_from_socket(sock) write_func = lambda channel, buf: sock.send(buf, flags) return self._channel_write(channel, buf, write_func) def sock_sendallto(self, sock, buf, addr, flags=0): channel = self._channel_from_socket(sock) write_func = lambda channel, buf: sock.sendto(buf, flags, addr) return self._channel_write(channel, buf, write_func) ##################################### # Low-level GLib.Channel operations # ##################################### def _channel_read(self, channel, nbytes, read_func=None): if read_func is None: read_func = lambda channel, nbytes: channel.read(nbytes) source = GLib.io_create_watch(channel, GLib.IO_IN | GLib.IO_HUP) def channel_readable(read_func, channel, nbytes): return (True, read_func(channel, nbytes)) return self._delayed(source, channel_readable, read_func, channel, nbytes) def _channel_write(self, channel, buf, write_func=None): if write_func is None: # note: channel.write doesn't raise BlockingIOError, instead it # returns 0 # gi.overrides.GLib.write has an isinstance(buf, bytes) check, so # we can't give it a bytearray or a memoryview. write_func = lambda channel, buf: channel.write(bytes(buf)) buflen = len(buf) # Fast-path: If there is enough room in the OS buffer all data can be written synchronously try: nbytes = write_func(channel, buf) except BlockingIOError: nbytes = 0 else: if nbytes >= len(buf): # All data was written synchronously in one go result = asyncio.Future(loop=self) result.set_result(nbytes) return result # Chop off the initially transmitted data and store result # as a bytearray for easier future modification buf = bytearray(buf[nbytes:]) # Send the remaining data asynchronously as the socket becomes writable source = GLib.io_create_watch(channel, GLib.IO_OUT) def channel_writable(buflen, write_func, channel, buf): nbytes = write_func(channel, buf) if nbytes >= len(buf): return (True, buflen) else: del buf[0:nbytes] return (False, buflen) return self._delayed(source, channel_writable, buflen, write_func, channel, buf) def add_reader(self, fileobj, callback, *args): fd = self._fileobj_to_fd(fileobj) self._ensure_fd_no_transport(fd) self.remove_reader(fd) channel = self._channel_from_socket(fd) source = GLib.io_create_watch(channel, GLib.IO_IN | GLib.IO_HUP | GLib.IO_ERR | GLib.IO_NVAL) assert fd not in self._readers self._readers[fd] = GLibHandle( loop=self, source=source, repeat=True, callback=callback, args=args) def remove_reader(self, fileobj): fd = self._fileobj_to_fd(fileobj) self._ensure_fd_no_transport(fd) try: self._readers.pop(fd).cancel() return True except KeyError: return False def add_writer(self, fileobj, callback, *args): fd = self._fileobj_to_fd(fileobj) self._ensure_fd_no_transport(fd) self.remove_writer(fd) channel = self._channel_from_socket(fd) source = GLib.io_create_watch(channel, GLib.IO_OUT | GLib.IO_ERR | GLib.IO_NVAL) assert fd not in self._writers self._writers[fd] = GLibHandle( loop=self, source=source, repeat=True, callback=callback, args=args) def remove_writer(self, fileobj): fd = self._fileobj_to_fd(fileobj) self._ensure_fd_no_transport(fd) try: self._writers.pop(fd).cancel() return True except KeyError: return False class GLibEventLoop(GLibBaseEventLoop): def __init__(self, *, context=None, application=None): self._application = application self._running = False self._argv = None super().__init__(context) if application is None: self._mainloop = GLib.MainLoop(self._context) def is_running(self): return self._running def run(self): recursive = self.is_running() if not recursive and hasattr(events, "_get_running_loop") and events._get_running_loop(): raise RuntimeError( 'Cannot run the event loop while another loop is running') if not recursive: self._running = True if hasattr(events, "_set_running_loop"): events._set_running_loop(self) try: if self._application is not None: self._application.run(self._argv) else: self._mainloop.run() finally: if not recursive: self._running = False if hasattr(events, "_set_running_loop"): events._set_running_loop(None) def run_until_complete(self, future, **kw): """Run the event loop until a Future is done. Return the Future's result, or raise its exception. """ def stop(f): self.stop() future = tasks.ensure_future(future, loop=self) future.add_done_callback(stop) try: self.run_forever(**kw) finally: future.remove_done_callback(stop) if not future.done(): raise RuntimeError('Event loop stopped before Future completed.') return future.result() def run_forever(self, application=None, argv=None): """Run the event loop until stop() is called.""" if application is not None: self.set_application(application) if argv is not None: self.set_argv(argv) if self.is_running(): raise RuntimeError( "Recursively calling run_forever is forbidden. " "To recursively run the event loop, call run().") if hasattr(self, '_mainloop') and hasattr(self._mainloop, "_quit_by_sigint"): del self._mainloop._quit_by_sigint try: self.run() finally: self.stop() # Methods scheduling callbacks. All these return Handles. def call_soon(self, callback, *args, context=None): self._check_not_coroutine(callback, 'call_soon') source = GLib.Idle() source.set_priority(GLib.PRIORITY_DEFAULT) return GLibHandle( loop=self, source=source, repeat=False, callback=callback, args=args, context=context, ) call_soon_threadsafe = call_soon def call_later(self, delay, callback, *args, context=None): self._check_not_coroutine(callback, 'call_later') return GLibHandle( loop=self, source=GLib.Timeout(delay*1000) if delay > 0 else GLib.Idle(), repeat=False, callback=callback, args=args, context=context, ) def call_at(self, when, callback, *args, context=None): self._check_not_coroutine(callback, 'call_at') return self.call_later( when - self.time(), callback, *args, context=context) def time(self): return GLib.get_monotonic_time() / 1000000 def stop(self): """Stop the inner-most invocation of the event loop. Typically, this will mean stopping the event loop completely. Note that due to the nature of GLib's main loop, stopping may not be immediate. """ if self._application is not None: self._application.quit() else: self._mainloop.quit() def set_application(self, application): if not isinstance(application, Gio.Application): raise TypeError("application must be a Gio.Application object") if self._application is not None: raise ValueError("application is already set") if self.is_running(): raise RuntimeError("You can't add the application to a loop that's already running.") self._application = application self._policy._application = application del self._mainloop def set_argv(self, argv): """Sets argv to be passed to Gio.Application.run()""" self._argv = argv class GLibEventLoopPolicy(events.AbstractEventLoopPolicy): """Default GLib event loop policy In this policy, each thread has its own event loop. However, we only automatically create an event loop by default for the main thread; other threads by default have no event loop. """ # TODO add a parameter to synchronise with GLib's thread default contexts # (g_main_context_push_thread_default()) def __init__(self, application=None): self._default_loop = None self._application = application self._watcher_lock = threading.Lock() self._watcher = None self._policy = asyncio.DefaultEventLoopPolicy() self._policy.new_event_loop = self.new_event_loop self.get_event_loop = self._policy.get_event_loop self.set_event_loop = self._policy.set_event_loop def get_child_watcher(self): if self._watcher is None: with self._watcher_lock: if self._watcher is None: self._watcher = GLibChildWatcher() return self._watcher def set_child_watcher(self, watcher): """Set a child watcher. Must be an an instance of GLibChildWatcher, as it ties in with GLib appropriately. """ if watcher is not None and not isinstance(watcher, GLibChildWatcher): raise TypeError("Only GLibChildWatcher is supported!") with self._watcher_lock: self._watcher = watcher def new_event_loop(self): """Create a new event loop and return it.""" if not self._default_loop and isinstance(threading.current_thread(), threading._MainThread): l = self.get_default_loop() else: l = GLibEventLoop() l._policy = self return l def get_default_loop(self): """Get the default event loop.""" if not self._default_loop: self._default_loop = self._new_default_loop() return self._default_loop def _new_default_loop(self): l = GLibEventLoop(context=GLib.main_context_default(), application=self._application) l._policy = self return l gbulb-0.6.1/gbulb/gtk.py000066400000000000000000000035141333320506400150450ustar00rootroot00000000000000import threading from gi.repository import GLib, Gtk from .glib_events import GLibEventLoop, GLibEventLoopPolicy __all__ = ['GtkEventLoop', 'GtkEventLoopPolicy'] class GtkEventLoop(GLibEventLoop): """Gtk-based event loop. This loop supports recursion in Gtk, for example for implementing modal windows. """ def __init__(self, **kwargs): self._recursive = 0 self._recurselock = threading.Lock() kwargs['context'] = GLib.main_context_default() super().__init__(**kwargs) def run(self): """Run the event loop until Gtk.main_quit is called. May be called multiple times to recursively start it again. This is useful for implementing asynchronous-like dialogs in code that is otherwise not asynchronous, for example modal dialogs. """ if self.is_running(): with self._recurselock: self._recursive += 1 try: Gtk.main() finally: with self._recurselock: self._recursive -= 1 else: super().run() def stop(self): """Stop the inner-most event loop. If it's also the outer-most event loop, the event loop will stop. """ with self._recurselock: r = self._recursive if r > 0: Gtk.main_quit() else: super().stop() class GtkEventLoopPolicy(GLibEventLoopPolicy): """Gtk-based event loop policy. Use this if you are using Gtk.""" def _new_default_loop(self): l = GtkEventLoop(application=self._application) l._policy = self return l def new_event_loop(self): if not self._default_loop: l = self.get_default_loop() else: l = GtkEventLoop() l._policy = self return l gbulb-0.6.1/gbulb/transports.py000066400000000000000000000313021333320506400164730ustar00rootroot00000000000000import collections import socket import subprocess from asyncio import base_subprocess, futures, transports class BaseTransport(transports.BaseTransport): def __init__(self, loop, sock, protocol, waiter=None, extra=None, server=None): if hasattr(self, '_sock'): return # The joys of multiple inheritance transports.BaseTransport.__init__(self, extra) self._loop = loop self._sock = sock self._protocol = protocol self._server = server self._closing = False self._closing_delayed = False self._closed = False self._cancelable = set() if sock is not None: self._loop._transports[sock.fileno()] = self if self._server is not None: self._server._attach() def transport_async_init(): self._protocol.connection_made(self) if waiter is not None and not waiter.cancelled(): waiter.set_result(None) self._loop.call_soon(transport_async_init) def close(self): self._closing = True if not self._closing_delayed: self._force_close(None) def is_closing(self): return self._closing def set_protocol(self, protocol): self._protocol = protocol def get_protocol(self): return self._protocol def _fatal_error(self, exc, message='Fatal error on pipe transport'): self._loop.call_exception_handler({ 'message': message, 'exception': exc, 'transport': self, 'protocol': self._protocol, }) self._force_close(exc) def _force_close(self, exc): if self._closed: return self._closed = True # Stop all running tasks for cancelable in self._cancelable: cancelable.cancel() self._cancelable.clear() self._loop.call_soon(self._force_close_async, exc) def _force_close_async(self, exc): try: self._protocol.connection_lost(exc) finally: if self._sock is not None: self._sock.close() self._sock = None if self._server is not None: self._server._detach() self._server = None class ReadTransport(BaseTransport, transports.ReadTransport): max_size = 256 * 1024 def __init__(self, *args, **kwargs): BaseTransport.__init__(self, *args, **kwargs) self._paused = False self._read_fut = None self._loop.call_soon(self._loop_reading) def pause_reading(self): if self._closing: raise RuntimeError('Cannot pause_reading() when closing') if self._paused: raise RuntimeError('Already paused') self._paused = True def resume_reading(self): if not self._paused: raise RuntimeError('Not paused') self._paused = False if self._closing: return self._loop.call_soon(self._loop_reading, self._read_fut) def _close_read(self): # Separate method to allow `Transport.close()` to call us without # us delegating to `BaseTransport.close()` if self._read_fut is not None: self._read_fut.cancel() self._read_fut = None def close(self): self._close_read() super().close() def _create_read_future(self, size): return self._loop.sock_recv(self._sock, size) def _submit_read_data(self, data): if data: self._protocol.data_received(data) else: keep_open = self._protocol.eof_received() if not keep_open: self.close() def _loop_reading(self, fut=None): if self._paused: return data = None try: if fut is not None: assert self._read_fut is fut or (self._read_fut is None and self._closing) if self._read_fut in self._cancelable: self._cancelable.remove(self._read_fut) self._read_fut = None data = fut.result() # Deliver data later in "finally" clause if self._closing: # Since `.close()` has been called we ignore any read data data = None return if data == b'': # No need to reschedule on end-of-file return # Reschedule a new read self._read_fut = self._create_read_future(self.max_size) self._cancelable.add(self._read_fut) except ConnectionAbortedError as exc: if not self._closing: self._fatal_error(exc, 'Fatal read error on pipe transport') except ConnectionResetError as exc: self._force_close(exc) except OSError as exc: self._fatal_error(exc, 'Fatal read error on pipe transport') except futures.CancelledError: if not self._closing: raise except futures.InvalidStateError: self._read_fut = fut self._cancelable.add(self._read_fut) else: self._read_fut.add_done_callback(self._loop_reading) finally: if data is not None: self._submit_read_data(data) class WriteTransport(BaseTransport, transports._FlowControlMixin): _buffer_factory = bytearray def __init__(self, loop, *args, **kwargs): transports._FlowControlMixin.__init__(self, None, loop) BaseTransport.__init__(self, loop, *args, **kwargs) self._buffer = self._buffer_factory() self._buffer_empty_callbacks = set() self._write_fut = None self._eof_written = False def abort(self): self._force_close(None) def can_write_eof(self): return True def get_write_buffer_size(self): return len(self._buffer) def _close_write(self): if self._write_fut is not None: self._closing_delayed = True def transport_write_done_callback(): self._closing_delayed = False self.close() self._buffer_empty_callbacks.add(transport_write_done_callback) def close(self): self._close_write() super().close() def write(self, data): if self._eof_written: raise RuntimeError('write_eof() already called') # Ignore empty data sets or requests to write to a dying connection if not data or self._closing: return if self._write_fut is None: # No data is currently buffered or being sent self._loop_writing(data=data) else: self._buffer_add_data(data) self._maybe_pause_protocol() # From _FlowControlMixin def _create_write_future(self, data): return self._loop.sock_sendall(self._sock, data) def _buffer_add_data(self, data): self._buffer.extend(data) def _buffer_pop_data(self): if len(self._buffer) > 0: data = self._buffer self._buffer = bytearray() return data else: return None def _loop_writing(self, fut=None, data=None): try: assert fut is self._write_fut if self._write_fut in self._cancelable: self._cancelable.remove(self._write_fut) self._write_fut = None # Raise possible exception stored in `fut` if fut: fut.result() # Use buffer as next data object if invoked from done callback if data is None: data = self._buffer_pop_data() if not data: if len(self._buffer_empty_callbacks) > 0: for callback in self._buffer_empty_callbacks: callback() self._buffer_empty_callbacks.clear() self._maybe_resume_protocol() else: self._write_fut = self._create_write_future(data) self._cancelable.add(self._write_fut) if not self._write_fut.done(): self._write_fut.add_done_callback(self._loop_writing) self._maybe_pause_protocol() else: self._write_fut.add_done_callback(self._loop_writing) except ConnectionResetError as exc: self._force_close(exc) except OSError as exc: self._fatal_error(exc, 'Fatal write error on pipe transport') def write_eof(self): self.close() class Transport(ReadTransport, WriteTransport): def __init__(self, *args, **kwargs): ReadTransport.__init__(self, *args, **kwargs) WriteTransport.__init__(self, *args, **kwargs) # Set expected extra attributes (available through `.get_extra_info()`) self._extra['socket'] = self._sock try: self._extra['sockname'] = self._sock.getsockname() except (OSError, AttributeError): pass if 'peername' not in self._extra: try: self._extra['peername'] = self._sock.getpeername() except (OSError, AttributeError) as error: pass def close(self): # Need to invoke both the read's and the write's part of the transport `close` function self._close_read() self._close_write() BaseTransport.close(self) class SocketTransport(Transport): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def write_eof(self): if self._closing or self._eof_written: return self._eof_written = True if self._write_fut is None: self._sock.shutdown(socket.SHUT_WR) else: def transport_write_eof_callback(): if not self._closing: self._sock.shutdown(socket.SHUT_WR) self._buffer_empty_callbacks.add(transport_write_eof_callback) class DatagramTransport(Transport, transports.DatagramTransport): _buffer_factory = collections.deque def __init__(self, loop, sock, protocol, address=None, *args, **kwargs): self._address = address super().__init__(loop, sock, protocol, *args, **kwargs) def _create_read_future(self, size): return self._loop.sock_recvfrom(self._sock, size) def _submit_read_data(self, args): (data, addr) = args self._protocol.datagram_received(data, addr) def _create_write_future(self, args): (data, addr) = args if self._address: return self._loop.sock_sendall(self._sock, data) else: return self._loop.sock_sendallto(self._sock, data, addr) def _buffer_add_data(self, args): (data, addr) = args self._buffer.append((bytes(data), addr)) def _buffer_pop_data(self): if len(self._buffer) > 0: return self._buffer.popleft() else: return None def write(self, data, addr=None): if not isinstance(data, (bytes, bytearray, memoryview)): raise TypeError("data argument must be a bytes-like object, " "not {!r}".format(type(data).__name__)) if not data or self.is_closing(): return if self._address and addr not in (None, self._address): raise ValueError("Invalid address: must be None or {0}".format(self._address)) # Do not copy the data yet, as we might be able to send it synchronously super().write((data, addr)) sendto = write class PipeReadTransport(ReadTransport): def __init__(self, loop, channel, protocol, waiter, extra): self._channel = channel self._channel.set_close_on_unref(True) super().__init__(loop, None, protocol, waiter, extra) def _create_read_future(self, size): return self._loop._channel_read(self._channel, size) def _force_close_async(self, exc): try: super()._force_close_async(exc) finally: self._channel.shutdown(True) class PipeWriteTransport(WriteTransport): def __init__(self, loop, channel, protocol, waiter, extra): self._channel = channel self._channel.set_close_on_unref(True) super().__init__(loop, None, protocol, waiter, extra) def _create_write_future(self, data): return self._loop._channel_write(self._channel, data) def _force_close_async(self, exc): try: super()._force_close_async(exc) finally: self._channel.shutdown(True) class SubprocessTransport(base_subprocess.BaseSubprocessTransport): def _start(self, args, shell, stdin, stdout, stderr, bufsize, **kwargs): self._proc = subprocess.Popen( args, shell=shell, stdin=stdin, stdout=stdout, stderr=stderr, bufsize=bufsize, **kwargs) gbulb-0.6.1/gbulb/utils.py000066400000000000000000000035111333320506400154150ustar00rootroot00000000000000import asyncio import weakref __all__ = ['install', 'get_event_loop', 'wait_signal'] def install(gtk=False): """Set the default event loop policy. Call this as early as possible to ensure everything has a reference to the correct event loop. Set ``gtk`` to True if you intend to use Gtk in your application. If ``gtk`` is True and Gtk is not available, will raise `ValueError`. Note that this class performs some monkey patching of asyncio to ensure correct functionality. """ if gtk: from .gtk import GtkEventLoopPolicy policy = GtkEventLoopPolicy() else: from .glib_events import GLibEventLoopPolicy policy = GLibEventLoopPolicy() # There are some libraries that use SafeChildWatcher directly (which is # completely reasonable), so we have to ensure that it is our version. I'm # sorry, I know this isn't great but it's basically the best that we have. from .glib_events import GLibChildWatcher asyncio.SafeChildWatcher = GLibChildWatcher asyncio.set_event_loop_policy(policy) def get_event_loop(): """Alias to asyncio.get_event_loop().""" return asyncio.get_event_loop() class wait_signal(asyncio.Future): """A future for waiting for a given signal to occur.""" def __init__(self, obj, name, *, loop=None): super().__init__(loop=loop) self._obj = weakref.ref(obj, lambda s: self.cancel()) self._hnd = obj.connect(name, self._signal_callback) def _signal_callback(self, *k): obj = self._obj() if obj is not None: obj.disconnect(self._hnd) self.set_result(k) def cancel(self): if self.cancelled(): return False super().cancel() obj = self._obj() if obj is not None: obj.disconnect(self._hnd) return True gbulb-0.6.1/setup.py000066400000000000000000000021021333320506400143150ustar00rootroot00000000000000#!/usr/bin/env python try: from setuptools import setup except ImportError: from distutils.core import setup setup( name='gbulb', version='0.6.1', description='GLib event loop for tulip (PEP 3156)', author='Nathan Hoad', author_email='nathan@getoffmalawn.com', license='Apache 2.0', url='http://github.com/nathan-hoad/gbulb', packages=['gbulb'], long_description="""Gbulb is a python library that implements a PEP 3156 interface for the GLib main event loop. It is designed to be used together with the tulip reference implementation.""", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Software Development :: Libraries :: Python Modules", ], python_requires='>3.5' ) gbulb-0.6.1/tests/000077500000000000000000000000001333320506400137525ustar00rootroot00000000000000gbulb-0.6.1/tests/docker-images/000077500000000000000000000000001333320506400164645ustar00rootroot00000000000000gbulb-0.6.1/tests/docker-images/Makefile000066400000000000000000000003161333320506400201240ustar00rootroot00000000000000base: docker build -t nathanhoad/gbulb-base -f base.Dockerfile .; python: test ${VERSION} docker build -t nathanhoad/gbulb-python:${VERSION} -f python.Dockerfile --build-arg=PYTHON_VERSION=${VERSION} .; gbulb-0.6.1/tests/docker-images/base.Dockerfile000066400000000000000000000003201333320506400213620ustar00rootroot00000000000000FROM centos:7 RUN yum install -y openssl-devel zlib-devel gtk3-devel gobject-introspection-devel libffi-devel bzip2-devel which gcc make git libtool bzip2 RUN git clone https://github.com/yyuu/pyenv ~/.pyenv gbulb-0.6.1/tests/docker-images/python.Dockerfile000066400000000000000000000021711333320506400217770ustar00rootroot00000000000000FROM nathanhoad/gbulb-base ARG PYTHON_VERSION ARG GOBJECT_CHECKSUM=779effa93f4b59cdb72f4ab0128fb3fd82900bf686193b570fd3a8ce63392d54 ARG GOBJECT_BASE_VERSION=3.14 ARG GOBJECT_VERSION=3.14.0 ENV HOME=/root/ ENV PYENV_ROOT=$HOME/.pyenv ENV PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH RUN pyenv install $PYTHON_VERSION RUN pyenv global $PYTHON_VERSION RUN curl -L "https://ftp.gnome.org/pub/GNOME/sources/pygobject/$GOBJECT_BASE_VERSION/pygobject-$GOBJECT_VERSION.tar.xz" -o pygobject.tar.xz RUN echo "$GOBJECT_CHECKSUM pygobject.tar.xz" > pygobject.checksum RUN sha256sum --check pygobject.checksum RUN tar xvf pygobject.tar.xz WORKDIR pygobject-$GOBJECT_VERSION # pygobject enforces c90 in configure, in a "you're not getting past this" kind # of way. From CPython 3.6.0, they (quite reasonably) moved to c99, and # introduced some c++ style comments to really rub it in, which doesn't go well # with gobject's c90. So this gross sed is to get us those wonderous comments. RUN sed -i 's/-std=c90/-std=c99/g' configure RUN ./configure --prefix="$PYENV_ROOT/versions/$PYTHON_VERSION" --enable-cairo=no RUN make install RUN pip install pytest gbulb-0.6.1/tests/test_glib_events.py000066400000000000000000000353601333320506400176730ustar00rootroot00000000000000import asyncio import sys import pytest from unittest import mock, skipIf from gi.repository import Gio, GLib from utils import glib_loop, glib_policy is_windows = (sys.platform == "win32") class TestGLibEventLoopPolicy: def test_set_child_watcher(self, glib_policy): from gbulb.glib_events import GLibChildWatcher with pytest.raises(TypeError): glib_policy.set_child_watcher(5) glib_policy.set_child_watcher(None) assert isinstance(glib_policy.get_child_watcher(), GLibChildWatcher) g = GLibChildWatcher() glib_policy.set_child_watcher(g) assert glib_policy.get_child_watcher() is g def test_new_event_loop(self, glib_policy): a = glib_policy.new_event_loop() b = glib_policy.new_event_loop() assert a == glib_policy.get_default_loop() assert b != glib_policy.get_default_loop() def test_new_event_loop_application(self, glib_policy): a = glib_policy.new_event_loop() a.set_application(Gio.Application()) b = glib_policy.new_event_loop() assert b._application is None class TestGLibHandle: def test_attachment_order(self, glib_loop): call_manager = mock.Mock() from gbulb.glib_events import GLibHandle # stub this out, we don't care if it gets called or not call_manager.loop.get_debug = lambda: True h = GLibHandle( loop=call_manager.loop, source=call_manager.source, repeat=True, callback=call_manager.callback, args=(), ) print(call_manager.mock_calls) expected_calls = [ mock.call.loop._handlers.add(h), mock.call.source.set_callback(h.__callback__, h), mock.call.source.attach(call_manager.loop._context), ] assert call_manager.mock_calls == expected_calls @asyncio.coroutine def no_op_coro(): pass class TestBaseGLibEventLoop: @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_add_signal_handler(self, glib_loop): import os import signal called = False def handler(): nonlocal called called = True glib_loop.stop() glib_loop.add_signal_handler(signal.SIGHUP, handler) assert signal.SIGHUP in glib_loop._sighandlers glib_loop.call_later(0.01, os.kill, os.getpid(), signal.SIGHUP) glib_loop.run_forever() assert called, 'signal handler didnt fire' @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_remove_signal_handler(self, glib_loop): import signal glib_loop.add_signal_handler(signal.SIGHUP, None) assert signal.SIGHUP in glib_loop._sighandlers assert glib_loop.remove_signal_handler(signal.SIGHUP) assert signal.SIGHUP not in glib_loop._sighandlers # FIXME: it'd be great if we could actually try signalling the process @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_remove_signal_handler_unhandled(self, glib_loop): import signal assert not glib_loop.remove_signal_handler(signal.SIGHUP) @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_remove_signal_handler_sigkill(self, glib_loop): import signal with pytest.raises(RuntimeError): glib_loop.add_signal_handler(signal.SIGKILL, None) @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_remove_signal_handler_sigill(self, glib_loop): import signal with pytest.raises(ValueError): glib_loop.add_signal_handler(signal.SIGILL, None) def test_run_until_complete_early_stop(self, glib_loop): import asyncio @asyncio.coroutine def coro(): glib_loop.call_soon(glib_loop.stop) yield from asyncio.sleep(5) with pytest.raises(RuntimeError): glib_loop.run_until_complete(coro()) @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_add_writer(self, glib_loop): import os rfd, wfd = os.pipe() called = False def callback(*args): nonlocal called called = True glib_loop.stop() glib_loop.add_writer(wfd, callback) glib_loop.run_forever() os.close(rfd) os.close(wfd) assert called, 'callback handler didnt fire' @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_add_reader(self, glib_loop): import os rfd, wfd = os.pipe() called = False def callback(*args): nonlocal called called = True glib_loop.stop() glib_loop.add_reader(rfd, callback) os.write(wfd, b'hey') glib_loop.run_forever() os.close(rfd) os.close(wfd) assert called, 'callback handler didnt fire' @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_add_reader_file(self, glib_loop): import os rfd, wfd = os.pipe() f = os.fdopen(rfd, 'r') glib_loop.add_reader(f, None) os.close(rfd) os.close(wfd) @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_add_writer_file(self, glib_loop): import os rfd, wfd = os.pipe() f = os.fdopen(wfd, 'r') glib_loop.add_writer(f, None) os.close(rfd) os.close(wfd) @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_remove_reader(self, glib_loop): import os rfd, wfd = os.pipe() f = os.fdopen(wfd, 'r') glib_loop.add_reader(f, None) os.close(rfd) os.close(wfd) assert glib_loop.remove_reader(f) assert not glib_loop.remove_reader(f.fileno()) @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_remove_writer(self, glib_loop): import os rfd, wfd = os.pipe() f = os.fdopen(wfd, 'r') glib_loop.add_writer(f, None) os.close(rfd) os.close(wfd) assert glib_loop.remove_writer(f) assert not glib_loop.remove_writer(f.fileno()) def test_time(self, glib_loop): import time SLEEP_TIME = .125 s = glib_loop.time() time.sleep(SLEEP_TIME) e = glib_loop.time() diff = e - s assert SLEEP_TIME + .005 >= diff >= SLEEP_TIME def test_call_at(self, glib_loop): called = False def handler(): nonlocal called called = True now = glib_loop.time() glib_loop.stop() print(now, s) assert now - s <= 0.2 s = glib_loop.time() glib_loop.call_at(s+0.1, handler) glib_loop.run_forever() assert called, 'call_at handler didnt fire' def test_call_soon_no_coroutine(self, glib_loop): with pytest.raises(TypeError): glib_loop.call_soon(no_op_coro) def test_call_later_no_coroutine(self, glib_loop): with pytest.raises(TypeError): glib_loop.call_later(1, no_op_coro) def test_call_at_no_coroutine(self, glib_loop): with pytest.raises(TypeError): glib_loop.call_at(1, no_op_coro) def test_call_soon_priority_order(self, glib_loop): items = [] def handler(i): items.append(i) for i in range(10): glib_loop.call_soon(handler, i) glib_loop.call_soon(glib_loop.stop) glib_loop.run_forever() assert items assert items == sorted(items) def test_call_soon_priority(self, glib_loop): h = glib_loop.call_soon(lambda: None) assert h._source.get_priority() == GLib.PRIORITY_DEFAULT h.cancel() @skipIf(is_windows, "Waiting on raw file descriptors only works for sockets on Windows") def test_add_writer_multiple_calls(self, glib_loop): import os rfd, wfd = os.pipe() timeout_occurred = False expected_i = 10 i = 0 def callback(): nonlocal i i += 1 if i == expected_i: glib_loop.stop() def timeout(): nonlocal timeout_occurred timeout_occurred = True glib_loop.stop() try: glib_loop.add_writer(wfd, callback) glib_loop.call_later(0.1, timeout) glib_loop.run_forever() finally: os.close(rfd) os.close(wfd) assert not timeout_occurred assert i == expected_i def test_call_soon_threadsafe(self, glib_loop): called = False def handler(): nonlocal called called = True glib_loop.stop() glib_loop.call_soon_threadsafe(handler) glib_loop.run_forever() assert called, 'call_soon_threadsafe handler didnt fire' class TestGLibEventLoop: def test_run_forever_recursion(self, glib_loop): def play_it_again_sam(): with pytest.raises(RuntimeError): glib_loop.run_forever() h = glib_loop.call_soon(play_it_again_sam) glib_loop.call_soon(glib_loop.stop) glib_loop.run_forever() def test_run_recursion(self, glib_loop): passed = False def first(): assert glib_loop._running glib_loop.call_soon(second) glib_loop.run() assert glib_loop._running def second(): nonlocal passed assert glib_loop._running glib_loop.stop() assert glib_loop._running passed = True assert not glib_loop._running glib_loop.call_soon(first) glib_loop.run() assert not glib_loop._running assert passed def test_run(self, glib_loop): with mock.patch.object(glib_loop, '_mainloop') as ml: glib_loop.run() ml.run.assert_any_call() glib_loop.set_application(Gio.Application()) with mock.patch.object(glib_loop, '_application') as app: glib_loop.run() app.run.assert_any_call(None) def test_stop(self, glib_loop): with mock.patch.object(glib_loop, '_mainloop') as ml: glib_loop.stop() ml.quit.assert_any_call() glib_loop.set_application(Gio.Application()) with mock.patch.object(glib_loop, '_application') as app: glib_loop.stop() app.quit.assert_any_call() def test_set_application(self, glib_loop): assert glib_loop._application is None assert glib_loop._policy._application is None app = Gio.Application() glib_loop.set_application(app) assert glib_loop._application == app assert glib_loop._policy._application == app def test_set_application_invalid_type(self, glib_loop): with pytest.raises(TypeError): glib_loop.set_application(None) def test_set_application_invalid_repeat_calls(self, glib_loop): app = Gio.Application() glib_loop.set_application(app) with pytest.raises(ValueError): glib_loop.set_application(app) def test_set_application_invalid_when_running(self, glib_loop): app = Gio.Application() with pytest.raises(RuntimeError): with mock.patch.object(glib_loop, 'is_running', return_value=True): glib_loop.set_application(app) @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_signal_handling_with_multiple_invocations(glib_loop): import os import signal glib_loop.call_later(0.01, os.kill, os.getpid(), signal.SIGINT) with pytest.raises(KeyboardInterrupt): glib_loop.run_forever() glib_loop.run_until_complete(asyncio.sleep(0)) @skipIf(is_windows, "Unix signal handlers are not supported on Windows") def test_default_signal_handling(glib_loop): import os import signal glib_loop.call_later(0.01, os.kill, os.getpid(), signal.SIGINT) with pytest.raises(KeyboardInterrupt): glib_loop.run_forever() def test_subprocesses_read_after_closure(glib_loop): import asyncio import subprocess # needed to ensure events.get_child_watcher() returns the right object import gbulb gbulb.install() @asyncio.coroutine def coro(): proc = yield from asyncio.create_subprocess_exec( 'cat', stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, loop=glib_loop) proc.stdin.write(b'hey\n') yield from proc.stdin.drain() proc.stdin.close() out = yield from proc.stdout.read() assert out == b'hey\n' yield from proc.wait() glib_loop.run_until_complete(coro()) def test_subprocesses_readline_without_closure(glib_loop): # needed to ensure events.get_child_watcher() returns the right object import gbulb gbulb.install() @asyncio.coroutine def run(): proc = yield from asyncio.create_subprocess_exec( 'cat', stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, loop=glib_loop) try: proc.stdin.write(b'test line\n') yield from proc.stdin.drain() line = yield from asyncio.wait_for( proc.stdout.readline(), timeout=5, loop=glib_loop) assert line == b'test line\n' proc.stdin.close() line = yield from asyncio.wait_for( proc.stdout.readline(), timeout=5, loop=glib_loop) assert line == b'' finally: yield from proc.wait() glib_loop.run_until_complete(run()) def test_sockets(glib_loop): server_done = asyncio.Event(loop=glib_loop) server_success = False @asyncio.coroutine def cb(reader, writer): nonlocal server_success writer.write(b'cool data\n') yield from writer.drain() print('reading') d = yield from reader.readline() print('hrm', d) server_success = d == b'thank you\n' writer.close() server_done.set() @asyncio.coroutine def run(): s = yield from asyncio.start_server(cb, '127.0.0.1', 0, loop=glib_loop) reader, writer = yield from asyncio.open_connection('127.0.0.1', s.sockets[0].getsockname()[-1], loop=glib_loop) d = yield from reader.readline() assert d == b'cool data\n' writer.write(b'thank you\n') yield from writer.drain() writer.close() yield from server_done.wait() assert server_success glib_loop.run_until_complete(run()) gbulb-0.6.1/tests/test_gtk.py000066400000000000000000000026071333320506400161550ustar00rootroot00000000000000import pytest from utils import gtk_loop, gtk_policy try: from gi.repository import Gtk except ImportError: # pragma: no cover Gtk = None @pytest.mark.skipif(not Gtk, reason="Gtk is not available") class TestGtkEventLoopPolicy: def test_new_event_loop(self, gtk_policy): from gbulb.gtk import GtkEventLoop a = gtk_policy.new_event_loop() b = gtk_policy.new_event_loop() assert isinstance(a, GtkEventLoop) assert isinstance(b, GtkEventLoop) assert a != b assert a == gtk_policy.get_default_loop() def test_new_event_loop_application(self, gtk_policy): a = gtk_policy.new_event_loop() a.set_application(Gtk.Application()) b = gtk_policy.new_event_loop() assert b._application is None def test_event_loop_recursion(self, gtk_loop): loop_count = 0 def inner(): nonlocal loop_count i = loop_count print('starting loop', loop_count) loop_count += 1 if loop_count == 10: print('loop {} stopped'.format(i)) gtk_loop.stop() else: gtk_loop.call_soon(inner) gtk_loop.run() print('loop {} stopped'.format(i)) gtk_loop.stop() gtk_loop.call_soon(inner) gtk_loop.run_forever() assert loop_count == 10 gbulb-0.6.1/tests/test_utils.py000066400000000000000000000056751333320506400165400ustar00rootroot00000000000000from unittest import mock import pytest from utils import glib_loop @pytest.mark.parametrize('gtk,gtk_available', [ (False, False), (False, True), (True, False), (True, True), ]) def test_install(gtk, gtk_available): from gbulb import install import sys called = False def set_event_loop_policy(pol): nonlocal called called = True cls_name = pol.__class__.__name__ if gtk: assert cls_name == 'GtkEventLoopPolicy' else: assert cls_name == 'GLibEventLoopPolicy' if gtk and 'gbulb.gtk' in sys.modules: del sys.modules['gbulb.gtk'] mock_repository = mock.Mock() if not gtk_available: del mock_repository.Gtk with mock.patch.dict('sys.modules', {'gi.repository': mock_repository}): with mock.patch('asyncio.set_event_loop_policy', set_event_loop_policy): import_error = gtk and not gtk_available try: install(gtk=gtk) except ImportError: assert import_error else: assert not import_error assert called def test_get_event_loop(): import asyncio import gbulb assert asyncio.get_event_loop() is gbulb.get_event_loop() def test_wait_signal(glib_loop): import asyncio from gi.repository import GObject from gbulb import wait_signal class TestObject(GObject.GObject): __gsignals__ = { 'foo': (GObject.SIGNAL_RUN_LAST, None, (str,)), } t = TestObject() def emitter(): yield t.emit('foo', 'frozen brains tell no tales') called = False @asyncio.coroutine def waiter(): nonlocal called r = yield from wait_signal(t, 'foo', loop=glib_loop) assert r == (t, 'frozen brains tell no tales') called = True glib_loop.run_until_complete(asyncio.wait([waiter(), emitter()], timeout=1, loop=glib_loop)) assert called def test_wait_signal_cancel(glib_loop): import asyncio from gi.repository import GObject from gbulb import wait_signal class TestObject(GObject.GObject): __gsignals__ = { 'foo': (GObject.SIGNAL_RUN_LAST, None, (str,)), } t = TestObject() def emitter(): yield t.emit('foo', 'frozen brains tell no tales') called = False cancelled = False def waiter(): nonlocal cancelled yield r = wait_signal(t, 'foo', loop=glib_loop) @r.add_done_callback def caller(r): nonlocal called called = True r.cancel() assert r.cancelled() cancelled = True glib_loop.run_until_complete(asyncio.wait([waiter(), emitter()], timeout=1, loop=glib_loop)) assert cancelled assert called def test_wait_signal_cancel_state(): from gbulb import wait_signal m = wait_signal(mock.Mock(), 'anything') assert m.cancel() assert not m.cancel() gbulb-0.6.1/tests/utils.py000066400000000000000000000016621333320506400154710ustar00rootroot00000000000000import pytest def fail_test(loop, context): # pragma: no cover loop.test_failure = context def setup_test_loop(loop): loop.set_exception_handler(fail_test) loop.test_failure = None def check_loop_failures(loop): # pragma: no cover if loop.test_failure is not None: pytest.fail('{message}: {exception}'.format(**loop.test_failure)) @pytest.fixture def glib_policy(): from gbulb.glib_events import GLibEventLoopPolicy return GLibEventLoopPolicy() @pytest.fixture def gtk_policy(): from gbulb.gtk import GtkEventLoopPolicy return GtkEventLoopPolicy() @pytest.yield_fixture(scope='function') def glib_loop(): l = glib_policy().new_event_loop() setup_test_loop(l) yield l check_loop_failures(l) l.close() @pytest.yield_fixture(scope='function') def gtk_loop(): l = gtk_policy().new_event_loop() setup_test_loop(l) yield l check_loop_failures(l) l.close()