libervia-backend/libervia/frontends/quick_frontend/quick_bridge.py

118 lines
4 KiB
Python

#!/usr/bin/env python3
# Libervia: an XMPP client
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Adapte the legacy callback/errback bridge API to bridges"""
import asyncio
import inspect
from typing import Any, Callable
from twisted.internet import defer
from twisted.python.failure import Failure
from libervia.backend.core.log import getLogger
log = getLogger(__name__)
class QuickBridgeAdapter:
"""Adapt legacy callback/errback to bridges
@param bridge: bridge instance
"""
def __init__(self, bridge: Any) -> None:
self._bridge = bridge
def __getattr__(self, name: str) -> Any:
attr = getattr(self._bridge, name)
if not callable(attr):
return attr
def wrapper(*args: Any, **kwargs: Any) -> Any:
callback = kwargs.pop("callback", None)
errback = kwargs.pop("errback", None)
result = attr(*args, **kwargs)
if inspect.iscoroutine(result):
result = asyncio.ensure_future(result)
if isinstance(result, asyncio.Future):
# async bridges (e.g. AIOBridge) return asyncio futures
result = defer.Deferred.fromFuture(result)
if (callback is None and errback is None) or not hasattr(
result, "addCallback"
):
return result
if callback is not None:
result.addCallback(callback)
if errback is not None:
result.addErrback(errback)
return result
return wrapper
def register_signal(self, name: str, handler: Callable, iface: str = "core") -> None:
"""Register a signal handler, adapting sync handlers to async bridges
@param name: signal name
@param handler: signal handler, sync or async
@param iface: bridge interface ("core" or "plugin")
"""
if not inspect.iscoroutinefunction(handler):
async def async_handler(*args: Any,
_handler: Callable = handler) -> None:
_handler(*args)
handler = async_handler
try:
self._bridge.register_signal(name, handler, iface)
except AttributeError:
log.warning(f"Signal {name} is not available on the bridge")
def bridge_connect(self, callback: Callable | None = None,
errback: Callable | None = None) -> None:
"""Connect to the bridge
@param callback: called when the bridge is connected
@param errback: called with a twisted Failure on connection error
"""
async def _connect() -> None:
try:
await self._bridge.bridge_connect()
except Exception as e:
if errback is not None:
errback(Failure(e))
else:
if callback is not None:
callback()
try:
asyncio.get_running_loop()
except RuntimeError:
# the event loop is not running yet (frontends usually call connect_bridge()
# before starting their loop)
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.call_soon(asyncio.ensure_future, _connect())
else:
asyncio.ensure_future(_connect())