pax_global_header00006660000000000000000000000064142043162550014514gustar00rootroot0000000000000052 comment=51c34224974d98fb91ffc91e856c691a94b1ab45 gbulb-0.6.3/000077500000000000000000000000001420431625500126155ustar00rootroot00000000000000gbulb-0.6.3/.github/000077500000000000000000000000001420431625500141555ustar00rootroot00000000000000gbulb-0.6.3/.github/labels.toml000066400000000000000000000030341420431625500163140ustar00rootroot00000000000000[bug] color = "dd2525" name = "bug" description = "A confirmed crash or error in behavior." [documentation] color = "8125dd" name = "documentation" description = "The issue relates to documentation." [duplicate] color = "333333" name = "duplicate" description = "A duplicate of an existing ticket." [enhancement] color = "2525dd" name = "enhancement" description = "New features, or improvements on existing features." [first-timers-only] color = "25dd25" name = "first-timers-only" description = "Is this your first time contributing? This could be a good place to start!" [invalid] color = "333333" name = "invalid" description = "The issue is mistaken or incorrect in some way." [linux] color = "25d4dd" name = "linux" description = "The issue relates Linux support." [more-details] color = "f975e6" name = "more-details" description = "More details are needed before the question can be answered." [not-quite-right] color = "f975e6" name = "not-quite-right" description = "The idea or PR has been reviewed, but more work is needed." [question] color = "cc317c" name = "question" description = "Requests for information." [up-for-grabs] color = "25dd25" name = "up-for-grabs" description = "Help wanted!" [windows] color = "25d4dd" name = "windows" description = "The issue relates to Microsoft Windows support." [wontfix] color = "333333" name = "wontfix" description = "The problem described is real, but we've decided against fixing it." [work-in-progress] color = "f975e6" name = "work-in-progress" description = "The PR is a work in progress" gbulb-0.6.3/.github/workflows/000077500000000000000000000000001420431625500162125ustar00rootroot00000000000000gbulb-0.6.3/.github/workflows/ci.yml000066400000000000000000000040751420431625500173360ustar00rootroot00000000000000name: CI on: pull_request: push: branches: - master jobs: beefore: name: Pre-test checks runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: task: - 'flake8' - 'towncrier-check' - 'package' steps: - uses: actions/checkout@v1 - name: Set up Python 3.9 uses: actions/setup-python@v1 with: python-version: '3.9' - name: Install dependencies run: | sudo apt-get install -y python3-dev python3-gi gir1.2-gtk-3.0 libgirepository1.0-dev pkg-config pip install --upgrade pip pip install --upgrade setuptools pip install tox - name: Run pre-test check run: | tox -e ${{ matrix.task }} smoke: name: Smoke test (3.6) needs: beefore runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.6 uses: actions/setup-python@v1 with: python-version: '3.6' - name: Install dependencies run: | sudo apt-get install -y python3-dev python3-gi gir1.2-gtk-3.0 libgirepository1.0-dev pkg-config pip install --upgrade pip pip install --upgrade setuptools pip install tox - name: Test run: | tox -e py python-versions: # Only run this and subsequent steps on branches. # `github.head_ref` only exists on pull requests. if: github.head_ref name: Python compatibility test needs: smoke runs-on: ubuntu-latest strategy: max-parallel: 4 matrix: python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | sudo apt-get install -y python3-dev python3-gi gir1.2-gtk-3.0 libgirepository1.0-dev pkg-config pip install --upgrade pip pip install --upgrade setuptools pip install tox - name: Test run: | tox -e py gbulb-0.6.3/.github/workflows/publish.yml000066400000000000000000000014011420431625500203770ustar00rootroot00000000000000name: Upload Python Package on: release: types: published jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python uses: actions/setup-python@v1 with: python-version: '3.x' - name: Install dependencies run: | sudo apt-get install -y python3-dev python3-gi gir1.2-gtk-3.0 libgirepository1.0-dev pkg-config python -m pip install --upgrade pip python -m pip install --upgrade setuptools python -m pip install tox - name: Build release artefacts run: | tox -e package - name: Publish release env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | tox -e publish gbulb-0.6.3/.github/workflows/release.yml000066400000000000000000000007731420431625500203640ustar00rootroot00000000000000name: Create Release on: push: tags: - 'v*' jobs: build: name: Create Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@master - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} draft: true prerelease: false gbulb-0.6.3/.gitignore000066400000000000000000000005451420431625500146110ustar00rootroot00000000000000*.pyc *~ .*.sw[op] *.egg-info /dist /build docs/_build/ distribute-* .DS_Store .python-version .pytest_cache .coverage coverage.xml venv*/ .vscode/ .eggs/ .tox/ /local /pip-wheel-metadata # IntelliJ Idea family of suites .idea *.iml ## File-based project format: *.ipr *.iws ## mpeltonen/sbt-idea plugin .idea_modules/ # direnv .envrc # Wing IDE *.wp[ur] gbulb-0.6.3/AUTHORS.rst000066400000000000000000000007701420431625500145000ustar00rootroot00000000000000Gbulb was originally created by Anthony Baire in September 2013. It was maintained by Nathan Hoad from 2015? until October 2021. It is currently maintained by Russell Keith-Magee, as part of the BeeWare project. And here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- people who have submitted patches, reported bugs, added translations, helped answer newbie questions, and generally made gbulb that much better: * Jan Lübbe * Krzysztof Kotlenga * montag451 * Brecht De Vlieger gbulb-0.6.3/CHANGELOG.rst000066400000000000000000000061741420431625500146460ustar00rootroot00000000000000Change Log ========== .. towncrier release notes start 0.6.3 (2022-02-20) ------------------ Bugfixes ^^^^^^^^ * Corrected the import of ``InvalidStateError`` to fix an error seen on Python 3.8+. (`#56 `__) * Reverted the fix from #47; that change led to file descriptor leaks. (`#52 `_) 0.6.2 (2021-10-24) ------------------ Features ^^^^^^^^ * Added support for Python 3.10. (`#50 `_) Bugfixes ^^^^^^^^ * Corrects a problem where a socket isn't forgotten and causes 100% CPU load. (`#47 `_) Improved Documentation ^^^^^^^^^^^^^^^^^^^^^^ * (`#49 `_) 0.6.1 (2018-08-09) ------------------ Bug fixes ^^^^^^^^^ * Support for 3.7, for real this time. Thank you Philippe Normand! 0.6.0 (2018-08-06) ------------------ Bug fixes ^^^^^^^^^ * Support for 3.7. Features ^^^^^^^^ * Preliminary Windows support. Please note that using subprocesses is known not to work. Patches welcome. Removals ^^^^^^^^ * Support for 3.4 and below has been dropped. 0.5.3 (2017-01-27) ------------------ Bug fixes ^^^^^^^^^ * 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) ------------------ Bug fixes ^^^^^^^^^ * Fixed a sporadic test hang. 0.5.1 (2017-01-20) ------------------ Bug fixes ^^^^^^^^^ * Fixed breakage on Python versions older than 3.5.3, caused by 0.5.0. Thanks Brecht De Vlieger! 0.5 (2017-01-12) ---------------- Bug fixes ^^^^^^^^^ * Fixed issue with readers and writers not being added to the loop properly as a result of `Python Issue 28369 `__. 0.4 (2016-10-26) ---------------- Bug fixes ^^^^^^^^^ * 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) ---------------- Bug fixes ^^^^^^^^^ * gbulb will no longer occasionally leak memory when used with threads. 0.2 (2016-03-20) ---------------- Features ^^^^^^^^ * ``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 * **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()`` Bug fixes ^^^^^^^^^ * Default signal handling of SIGINT * ``gbulb.wait_signal.cancel()`` now obeys the interface defined by ``asyncio.Future`` 0.1 2013-09-20 --------------- Features ^^^^^^^^ * Initial release gbulb-0.6.3/CONTRIBUTING.md000066400000000000000000000002651420431625500150510ustar00rootroot00000000000000# Contributing BeeWare <3's contributions! Please be aware, BeeWare operates under a Code of Conduct. See [CONTRIBUTING to BeeWare](http://beeware.org/contributing) for details. gbulb-0.6.3/LICENSE000066400000000000000000000011131420431625500136160ustar00rootroot00000000000000Copyright 2015 Nathan Hoad Copyright 2021 Russell Keith-Magee 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.3/MANIFEST.in000066400000000000000000000005211420431625500143510ustar00rootroot00000000000000include AUTHORS.rst include CONTRIBUTING.md include CHANGELOG.rst include LICENSE include README.rst include tox.ini recursive-include changes *.rst recursive-include examples * recursive-exclude examples *.pyc *.pyo recursive-include tests * recursive-exclude tests *.pyc *.pyo recursive-include src * recursive-exclude src *.pyc *.pyo gbulb-0.6.3/README.rst000066400000000000000000000120011420431625500142760ustar00rootroot00000000000000gbulb ===== .. image:: https://img.shields.io/pypi/pyversions/gbulb.svg :target: https://pypi.python.org/pypi/gbulb :alt: Python Versions .. image:: https://img.shields.io/pypi/v/gbulb.svg :target: https://pypi.python.org/pypi/gbulb :alt: PyPI Version .. image:: https://img.shields.io/pypi/status/gbulb.svg :target: https://pypi.python.org/pypi/gbulb :alt: Maturity .. image:: https://img.shields.io/pypi/l/gbulb.svg :target: https://github.com/beeware/gbulb/blob/master/LICENSE :alt: BSD License .. image:: https://github.com/beeware/gbulb/workflows/CI/badge.svg?branch=master :target: https://github.com/beeware/gbulb/actions :alt: Build Status .. image:: https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic :target: https://beeware.org/bee/chat/ :alt: Discord server Gbulb is a Python library that implements a `PEP 3156 `__ interface for the `GLib main event loop `__ 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. Requirements ------------ - python 3.6+ - pygobject - glib - gtk+3 (optional) Usage ----- GLib event loop ~~~~~~~~~~~~~~~ Example usage:: import asyncio, gbulb gbulb.install() asyncio.get_event_loop().run_forever() Gtk+ event loop *(suitable for GTK+ applications)* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Example usage:: import asyncio, gbulb gbulb.install(gtk=True) asyncio.get_event_loop().run_forever() GApplication/GtkApplication event loop ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Example usage:: 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. Community --------- gblub is part of the `BeeWare suite`_. You can talk to the community through: * `@pybeeware on Twitter `__ * `Discord `__ * The gbulb `Github Discussions forum `__ We foster a welcoming and respectful community as described in our `BeeWare Community Code of Conduct`_. Contributing ------------ If you experience problems with gbulb, `log them on GitHub`_. If you want to contribute code, please `fork the code`_ and `submit a pull request`_. .. _BeeWare suite: http://beeware.org .. _BeeWare Community Code of Conduct: http://beeware.org/community/behavior/ .. _log them on Github: https://github.com/beeware/gbulb/issues .. _fork the code: https://github.com/beeware/gbulb .. _submit a pull request: https://github.com/beeware/gbulb/pulls gbulb-0.6.3/changes/000077500000000000000000000000001420431625500142255ustar00rootroot00000000000000gbulb-0.6.3/changes/.gitignore000066400000000000000000000000141420431625500162100ustar00rootroot00000000000000!.gitignore gbulb-0.6.3/changes/template.rst000066400000000000000000000022721420431625500165750ustar00rootroot00000000000000{% if top_line %} {{ top_line }} {{ top_underline * ((top_line)|length)}} {% elif versiondata.name %} {{ versiondata.name }} {{ versiondata.version }} ({{ versiondata.date }}) {{ top_underline * ((versiondata.name + versiondata.version + versiondata.date)|length + 4)}} {% else %} {{ versiondata.version }} ({{ versiondata.date }}) {{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}} {% endif %} {% for section, _ in sections.items() %} {% set underline = underlines[0] %}{% if section %}{{section}} {{ underline * section|length }}{% set underline = underlines[1] %} {% endif %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section]%} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} * {{ text }} ({{ values|join(', ') }}) {% endfor %} {% else %} * {{ sections[section][category]['']|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% else %} {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %}gbulb-0.6.3/examples/000077500000000000000000000000001420431625500144335ustar00rootroot00000000000000gbulb-0.6.3/examples/gtk.py000066400000000000000000000012271420431625500155740ustar00rootroot00000000000000import 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.ensure_future(counter(display)) loop.run_forever() if __name__ == '__main__': main() gbulb-0.6.3/examples/test-gtk.py000077500000000000000000000041331420431625500165530ustar00rootroot00000000000000#!/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.ensure_future(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.3/examples/wait_signal.py000066400000000000000000000015151420431625500173100ustar00rootroot00000000000000import 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.ensure_future(text_watcher(display)) asyncio.ensure_future(counter(display)) loop.run_forever() if __name__ == '__main__': main() gbulb-0.6.3/pyproject.toml000066400000000000000000000006361420431625500155360ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 46.4.0", "wheel >= 0.32.0", ] build-backend = "setuptools.build_meta" [tool.towncrier] directory = "changes" package = "gbulb" package_dir = "src" filename = "CHANGELOG.rst" title_format = "{version} ({project_date})" issue_format = "`#{issue} `__" template = "changes/template.rst" underlines = ["-", "^", "\""] gbulb-0.6.3/setup.cfg000066400000000000000000000035741420431625500144470ustar00rootroot00000000000000[metadata] name = gbulb version = attr: gbulb.__version__ url = https://github.com/beeware/gbulb project_urls = Funding = https://beeware.org/contributing/membership/ Documentation = http://gbulb.readthedocs.io/en/latest/ Tracker = https://github.com/beeware/gbulb/issues Source = https://github.com/beeware/gbulb author = Russell Keith-Magee author_email = russell@keith-magee.com maintainer = Russell Keith-Magee maintainer_email = russell@keith-magee.com 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.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Software Development :: Libraries :: Python Modules license = Apache 2.0 license_file = LICENSE description = GLib event loop for tulip (PEP 3156) long_description = file: README.rst long_description_content_type = text/x-rst; charset=UTF-8 keywords = gtk glib gnome asyncio tulip platforms = linux [options] zip_safe = False packages = find: python_requires = >= 3.6 include_package_data = True package_dir = = src install_requires = pygobject>=3.14.0 [options.packages.find] where = src [aliases] test = pytest [bdist_wheel] universal = 1 [flake8] # https://flake8.readthedocs.org/en/latest/ exclude=\ venv*/*,\ local/*,\ docs/*,\ build/*,\ .eggs/*,\ .tox/* max-complexity = 10 max-line-length = 119 ignore = E121,E123,E126,E226,E24,E704,W503,W504,C901 [isort] skip_glob = docs/conf.py venv* local multi_line_output=3 [tool:pytest] testpaths = tests # need to ensure build directories aren't excluded from recursion norecursedirs = gbulb-0.6.3/setup.py000066400000000000000000000000741420431625500143300ustar00rootroot00000000000000#!/usr/bin/env python from setuptools import setup setup() gbulb-0.6.3/src/000077500000000000000000000000001420431625500134045ustar00rootroot00000000000000gbulb-0.6.3/src/gbulb/000077500000000000000000000000001420431625500144775ustar00rootroot00000000000000gbulb-0.6.3/src/gbulb/__init__.py000066400000000000000000000007311420431625500166110ustar00rootroot00000000000000from .glib_events import * # noqa: F401,F403 from .utils import * # noqa: F401,F403 # __all__ = [ # '__version__', # ] # Examples of valid version strings # __version__ = '1.2.3.dev1' # Development release 1 # __version__ = '1.2.3a1' # Alpha Release 1 # __version__ = '1.2.3b1' # Beta Release 1 # __version__ = '1.2.3rc1' # RC Release 1 # __version__ = '1.2.3' # Final Release # __version__ = '1.2.3.post1' # Post Release 1 __version__ = "0.6.3" gbulb-0.6.3/src/gbulb/glib_events.py000066400000000000000000000774341420431625500173710ustar00rootroot00000000000000"""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, sslproto, tasks, CancelledError try: from gi.repository import GLib, Gio except ImportError: # pragma: no cover GLib = None Gio = None 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: def _create_handle_for_pid(self, pid): return pid def _close_process_handle(self, pid): return 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) async 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: await waiter except Exception as exc: err = exc else: err = None if err is not None: transport.close() await 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 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()) async def accept_coro(future, conn): # Coroutine closing the accept socket if the future is cancelled try: return (await future) except 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) def read_func(channel, nbytes): return 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) def read_func(channel, nbytes): return 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) def write_func(channel, buf): return 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) def write_func(channel, buf): return 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: def read_func(channel, nbytes): return 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. def write_func(channel, buf): return 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 ): loop = self.get_default_loop() else: loop = GLibEventLoop() loop._policy = self return loop 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): loop = GLibEventLoop( context=GLib.main_context_default(), application=self._application ) loop._policy = self return loop gbulb-0.6.3/src/gbulb/gtk.py000066400000000000000000000035441420431625500156440ustar00rootroot00000000000000import 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): loop = GtkEventLoop(application=self._application) loop._policy = self return loop def new_event_loop(self): if not self._default_loop: loop = self.get_default_loop() else: loop = GtkEventLoop() loop._policy = self return loop gbulb-0.6.3/src/gbulb/transports.py000066400000000000000000000316141420431625500172750ustar00rootroot00000000000000import collections import socket import subprocess from asyncio import base_subprocess, transports, CancelledError, InvalidStateError 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 CancelledError: if not self._closing: raise except 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): 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.3/src/gbulb/utils.py000066400000000000000000000035141420431625500162140ustar00rootroot00000000000000import 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.3/tests/000077500000000000000000000000001420431625500137575ustar00rootroot00000000000000gbulb-0.6.3/tests/conftest.py000066400000000000000000000017271420431625500161650ustar00rootroot00000000000000import 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.fixture(scope="function") def glib_loop(glib_policy): loop = glib_policy.new_event_loop() setup_test_loop(loop) yield loop check_loop_failures(loop) loop.close() @pytest.fixture(scope="function") def gtk_loop(gtk_policy): loop = gtk_policy.new_event_loop() setup_test_loop(loop) yield loop check_loop_failures(loop) loop.close() gbulb-0.6.3/tests/docker-images/000077500000000000000000000000001420431625500164715ustar00rootroot00000000000000gbulb-0.6.3/tests/docker-images/Makefile000066400000000000000000000003161420431625500201310ustar00rootroot00000000000000base: 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.3/tests/docker-images/base.Dockerfile000066400000000000000000000003201420431625500213670ustar00rootroot00000000000000FROM 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.3/tests/docker-images/python.Dockerfile000066400000000000000000000021711420431625500220040ustar00rootroot00000000000000FROM 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.3/tests/test_glib_events.py000066400000000000000000000353401420431625500176760ustar00rootroot00000000000000import asyncio import sys import pytest from unittest import mock, skipIf from gi.repository import Gio, GLib 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 async 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") @pytest.mark.filterwarnings('ignore:g_unix_signal_source_new') 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") @pytest.mark.filterwarnings('ignore:g_unix_signal_source_new') 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 async def coro(): glib_loop.call_soon(glib_loop.stop) await 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 = 0.125 s = glib_loop.time() time.sleep(SLEEP_TIME) e = glib_loop.time() diff = e - s assert SLEEP_TIME + 0.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() 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() async def coro(): proc = await asyncio.create_subprocess_exec( "cat", stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, ) proc.stdin.write(b"hey\n") await proc.stdin.drain() proc.stdin.close() out = await proc.stdout.read() assert out == b"hey\n" await 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() async def run(): proc = await asyncio.create_subprocess_exec( "cat", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, ) try: proc.stdin.write(b"test line\n") await proc.stdin.drain() line = await asyncio.wait_for(proc.stdout.readline(), timeout=5) assert line == b"test line\n" proc.stdin.close() line = await asyncio.wait_for(proc.stdout.readline(), timeout=5) assert line == b"" finally: await proc.wait() glib_loop.run_until_complete(run()) def test_sockets(glib_loop): server_done = asyncio.Event() server_done._loop = glib_loop server_success = False async def cb(reader, writer): nonlocal server_success writer.write(b"cool data\n") await writer.drain() print("reading") d = await reader.readline() print("hrm", d) server_success = d == b"thank you\n" writer.close() server_done.set() async def run(): s = await asyncio.start_server(cb, "127.0.0.1", 0) reader, writer = await asyncio.open_connection( "127.0.0.1", s.sockets[0].getsockname()[-1] ) d = await reader.readline() assert d == b"cool data\n" writer.write(b"thank you\n") await writer.drain() writer.close() await server_done.wait() assert server_success glib_loop.run_until_complete(run()) gbulb-0.6.3/tests/test_gtk.py000066400000000000000000000026241420431625500161610ustar00rootroot00000000000000import pytest try: import gi gi.require_version('Gtk', '3.0') 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.3/tests/test_utils.py000066400000000000000000000056231420431625500165360ustar00rootroot00000000000000from unittest import mock import pytest @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.SignalFlags.RUN_LAST, None, (str,)), } t = TestObject() def emitter(): yield t.emit("foo", "frozen brains tell no tales") called = False async def waiter(): nonlocal called r = await wait_signal(t, "foo") assert r == (t, "frozen brains tell no tales") called = True glib_loop.run_until_complete( asyncio.wait([waiter(), emitter()], timeout=1) ) 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.SignalFlags.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") @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) ) 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.3/tox.ini000066400000000000000000000023361420431625500141340ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = flake8,towncrier-check,docs,package,py{36,37,38,39,310},pypy3 skip_missing_interpreters = true [testenv] setenv = PYTHONPATH = {toxinidir}/src deps = pytest pytest-tldr pytest-cov commands = pytest --cov -vv coverage xml [testenv:flake8] skip_install = True deps = flake8 commands = flake8 {posargs} [testenv:towncrier-check] skip_install = True deps = {[testenv:towncrier]deps} commands = python -m towncrier.check [testenv:towncrier] skip_install = True deps = towncrier == 21.9.0 commands = towncrier build {posargs} [testenv:docs] deps = -r{toxinidir}/docs/requirements_docs.txt commands = python setup.py build_sphinx -W [testenv:package] deps = check_manifest wheel twine commands = check-manifest -v python setup.py sdist bdist_wheel python -m twine check dist/* [testenv:publish] deps = wheel twine passenv = TWINE_USERNAME TWINE_PASSWORD commands = python -m twine upload dist/*