clan-app: Fixing design issue in webviewExecutor
Some checks failed
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-no-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.12-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.12-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-pytest Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.postgresql Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.template-minimal Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-inventory-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.module-clan-vars-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-install-test-ubuntu-22-04 Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.flash Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-eval Build done.
checks / checks-impure (pull_request) Successful in 4m32s

This commit is contained in:
Luis Hebendanz 2024-07-10 17:51:37 +02:00
parent 78ec710611
commit 811126ad10
7 changed files with 117 additions and 128 deletions

View File

@ -0,0 +1,64 @@
import logging
from collections.abc import Callable
from typing import Any, T
from gi.repository import GObject
log = logging.getLogger(__name__)
class _ImplApi:
def __init__(self) -> None:
self._registry: dict[str, Callable[[Any], Any]] = {}
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
name = fn.__name__
if name in self._registry:
raise ValueError(f"Function {name} already registered")
signal_source = fn.__annotations__.get("signal_source", None)
if signal_source is None:
raise ValueError(f"Function {name} must have a 'source' annotation")
# check that class is type WebExecutor
elif not issubclass(signal_source, GObject.Object):
raise ValueError(f"Function {name} must have a 'source' annotation of type GObject")
del fn.__annotations__["signal_source"]
self._registry[name] = fn
return fn
def validate(self,
abstr_methods: dict[str, dict[str, Any]]
) -> dict[str, Callable]:
impl_fns = self._registry
# iterate over the methods and check if all are implemented
for abstr_name, abstr_annotations in abstr_methods.items():
if abstr_name not in impl_fns:
raise NotImplementedError(
f"Abstract method '{abstr_name}' is not implemented"
)
else:
expected_signature = abstr_annotations
got_signature = impl_fns[abstr_name].__annotations__
if expected_signature != got_signature:
log.error(f"Expected signature: {expected_signature}")
log.error(f"Actual signature: {got_signature}")
raise ValueError(
f"Abstract method '{abstr_name}' has different signature than the implementation"
)
return impl_fns
def get(self, name: str) -> Callable[..., T]:
return self._registry.get(name)
ImplApi = _ImplApi()

View File

@ -1,21 +1,24 @@
import gi
gi.require_version("WebKit", "6.0")
from gi.repository import Gio, GLib, Gtk, WebKit
gi.require_version("Gtk", "4.0")
import logging
from clan_cli.api.directory import FileRequest
from gi.repository import Gio, Gtk
from clan_app.api import ImplApi
from clan_app.views.webview import WebExecutor
log = logging.getLogger(__name__)
# Implement the abstract open_file function
def open_file(file_request: FileRequest) -> str | None:
@ImplApi.register
def open_file(signal_source: WebExecutor, file_request: FileRequest) -> str | None:
# Function to handle the response and stop the loop
selected_path = None
def on_file_select(
file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop
) -> None:
def on_file_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
try:
gfile = file_dialog.open_finish(task)
if gfile:
@ -23,12 +26,8 @@ def open_file(file_request: FileRequest) -> str | None:
selected_path = gfile.get_path()
except Exception as e:
print(f"Error getting selected file or directory: {e}")
finally:
main_loop.quit()
def on_folder_select(
file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop
) -> None:
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
try:
gfile = file_dialog.select_folder_finish(task)
if gfile:
@ -36,12 +35,8 @@ def open_file(file_request: FileRequest) -> str | None:
selected_path = gfile.get_path()
except Exception as e:
print(f"Error getting selected directory: {e}")
finally:
main_loop.quit()
def on_save_finish(
file_dialog: Gtk.FileDialog, task: Gio.Task, main_loop: GLib.MainLoop
) -> None:
def on_save_finish(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
try:
gfile = file_dialog.save_finish(task)
if gfile:
@ -49,8 +44,6 @@ def open_file(file_request: FileRequest) -> str | None:
selected_path = gfile.get_path()
except Exception as e:
print(f"Error getting selected file: {e}")
finally:
main_loop.quit()
dialog = Gtk.FileDialog()
@ -81,23 +74,15 @@ def open_file(file_request: FileRequest) -> str | None:
filters.append(file_filters)
dialog.set_filters(filters)
main_loop = GLib.MainLoop()
# if select_folder
if file_request.mode == "select_folder":
dialog.select_folder(
callback=lambda dialog, task: on_folder_select(dialog, task, main_loop),
callback=lambda dialog, task: on_folder_select(dialog, task),
)
elif file_request.mode == "open_file":
dialog.open(
callback=lambda dialog, task: on_file_select(dialog, task, main_loop)
)
dialog.open(callback=lambda dialog, task: on_file_select(dialog, task))
elif file_request.mode == "save":
dialog.save(
callback=lambda dialog, task: on_save_finish(dialog, task, main_loop)
)
# Wait for the user to select a file or directory
main_loop.run() # type: ignore
dialog.save(callback=lambda dialog, task: on_save_finish(dialog, task))
log.debug(f"Selected path: {selected_path}")
return selected_path

View File

@ -13,7 +13,7 @@ gi.require_version("Adw", "1")
from pathlib import Path
from clan_cli.custom_logger import setup_logging
from gi.repository import Adw, Gdk, Gio, Gtk, GLib, GObject
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
from clan_app.components.interfaces import ClanConfig

View File

@ -9,8 +9,6 @@ gi.require_version("Adw", "1")
from gi.repository import Adw
from clan_app.singletons.use_views import ViewStack
log = logging.getLogger(__name__)
@ -47,7 +45,6 @@ class ToastOverlay:
toast.connect("dismissed", lambda toast: self.active_toasts.remove(key))
class WarningToast:
toast: Adw.Toast

View File

@ -1,20 +1,18 @@
import dataclasses
import json
import gi
import logging
import threading
from collections.abc import Callable
from dataclasses import fields, is_dataclass
from pathlib import Path
from threading import Lock
from types import UnionType
from typing import Any, get_args
from typing import Any, ClassVar, get_args
from clan_cli.api import API
from clan_app.api.file import open_file
import gi
from clan_app.api import ImplApi
gi.require_version("WebKit", "6.0")
from gi.repository import Gio, GLib, Gtk, WebKit
from gi.repository import GLib, GObject, WebKit
log = logging.getLogger(__name__)
@ -47,7 +45,6 @@ def dataclass_to_dict(obj: Any) -> Any:
return obj
def is_union_type(type_hint: type) -> bool:
return type(type_hint) is UnionType
@ -100,9 +97,18 @@ def from_dict(t: type, data: dict[str, Any] | None) -> Any:
return None
class WebView:
def __init__(self, content_uri: str, methods: dict[str, Callable]) -> None:
self.method_registry: dict[str, Callable] = methods
class WebExecutor(GObject.Object):
__gsignals__: ClassVar = {
"func_returned": (GObject.SignalFlags.RUN_FIRST, None, []),
}
def __init__(self, content_uri: str, abstr_methods: dict[str, Callable]) -> None:
super().__init__()
self.webview = WebKit.WebView()
@ -119,9 +125,7 @@ class WebView:
self.webview.load_uri(content_uri)
# global mutex lock to ensure functions run sequentially
self.mutex_lock = Lock()
self.queue_size = 0
self.method_registry = ImplApi.validate(abstr_methods)
def on_message_received(
self, user_content_manager: WebKit.UserContentManager, message: Any
@ -133,73 +137,11 @@ class WebView:
log.debug(f"Received message: {payload}")
log.debug(f"Queue size: {self.queue_size} (Wait)")
def threaded_wrapper() -> bool:
"""
Ensures only one function is executed at a time
Wait until there is no other function acquiring the global lock.
Starts a thread with the potentially long running API function within.
"""
if not self.mutex_lock.locked():
thread = threading.Thread(
target=self.threaded_handler,
args=(
handler_fn,
payload.get("data"),
method_name,
),
)
thread.start()
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
GLib.idle_add(
threaded_wrapper,
handler_fn,
payload.get("data"),
method_name,
)
self.queue_size += 1
def threaded_handler(
self,
handler_fn: Callable[
...,
Any,
],
data: dict[str, Any] | None,
method_name: str,
) -> None:
with self.mutex_lock:
log.debug(f"Executing... {method_name}")
log.debug(f"{data}")
if data is None:
result = handler_fn()
else:
reconciled_arguments = {}
op_key = data.pop("op_key", None)
for k, v in data.items():
# Some functions expect to be called with dataclass instances
# But the js api returns dictionaries.
# Introspect the function and create the expected dataclass from dict dynamically
# Depending on the introspected argument_type
arg_class = API.get_method_argtype(method_name, k)
if dataclasses.is_dataclass(arg_class):
reconciled_arguments[k] = from_dict(arg_class, v)
else:
reconciled_arguments[k] = v
r = handler_fn(**reconciled_arguments)
# Parse the result to a serializable dictionary
# Echo back the "op_key" to the js api
result = dataclass_to_dict(r)
result["op_key"] = op_key
serialized = json.dumps(result)
# Use idle_add to queue the response call to js on the main GTK thread
GLib.idle_add(self.return_data_to_js, method_name, serialized)
self.queue_size -= 1
log.debug(f"Done: Remaining queue size: {self.queue_size}")
def return_data_to_js(self, method_name: str, serialized: str) -> bool:
# This function must be run on the main GTK thread to interact with the webview

View File

@ -1,21 +1,16 @@
import logging
import threading
import gi
from clan_cli.api import API
from clan_cli.history.list import list_history
from clan_app.components.interfaces import ClanConfig
from clan_app.singletons.toast import ToastOverlay
from clan_app.singletons.use_views import ViewStack
from clan_app.views.webview import WebView, open_file
from clan_app.views.webview import WebExecutor
gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, GLib
from gi.repository import Adw, Gio
log = logging.getLogger(__name__)
@ -41,10 +36,10 @@ class MainWindow(Adw.ApplicationWindow):
stack_view = ViewStack.use().view
# Override platform specific functions
API.register(open_file)
webview = WebView(methods=API._registry, content_uri=config.content_uri)
webview = WebExecutor(
abstr_methods=API._orig_annotations, content_uri=config.content_uri
)
stack_view.add_named(webview.get_webview(), "webview")
stack_view.set_visible_child_name(config.initial_view)
@ -55,4 +50,3 @@ class MainWindow(Adw.ApplicationWindow):
def on_destroy(self, source: "Adw.ApplicationWindow") -> None:
log.debug("====Destroying Adw.ApplicationWindow===")

View File

@ -52,7 +52,7 @@ def update_wrapper_signature(wrapper: Callable, wrapped: Callable) -> None:
class _MethodRegistry:
def __init__(self) -> None:
self._orig: dict[str, Callable[[Any], Any]] = {}
self._orig_annotations: dict[str, Callable[[Any], Any]] = {}
self._registry: dict[str, Callable[[Any], Any]] = {}
def register_abstract(self, fn: Callable[..., T]) -> Callable[..., T]:
@ -84,7 +84,13 @@ API.register(open_file)
return fn
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._orig[fn.__name__] = fn
if fn.__name__ in self._registry:
raise ValueError(f"Function {fn.__name__} already registered")
if fn.__name__ in self._orig_annotations:
raise ValueError(f"Function {fn.__name__} already registered")
# make copy of original function
self._orig_annotations[fn.__name__] = fn.__annotations__.copy()
@wraps(fn)
def wrapper(
@ -118,6 +124,7 @@ API.register(open_file)
update_wrapper_signature(wrapper, fn)
self._registry[fn.__name__] = wrapper
return fn
def to_json_schema(self) -> dict[str, Any]: