clan-app: Fix mypy lints and add GenericFnRuntime
This commit is contained in:
@@ -1,11 +1,6 @@
|
|||||||
|
|
||||||
import importlib
|
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
import pkgutil
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from types import ModuleType
|
from typing import Any, ClassVar, Generic, ParamSpec, TypeVar, cast
|
||||||
from typing import Any, ClassVar, Generic, ParamSpec, TypeVar
|
|
||||||
|
|
||||||
from gi.repository import GLib, GObject
|
from gi.repository import GLib, GObject
|
||||||
|
|
||||||
@@ -24,17 +19,23 @@ class GResult(GObject.Object):
|
|||||||
self.method_name = method_name
|
self.method_name = method_name
|
||||||
|
|
||||||
|
|
||||||
B = TypeVar('B')
|
B = TypeVar("B")
|
||||||
P = ParamSpec('P')
|
P = ParamSpec("P")
|
||||||
|
|
||||||
|
|
||||||
class ImplFunc(GObject.Object, Generic[P, B]):
|
class ImplFunc(GObject.Object, Generic[P, B]):
|
||||||
op_key: str | None = None
|
op_key: str | None = None
|
||||||
__gsignals__: ClassVar = {
|
__gsignals__: ClassVar = {
|
||||||
"returns": (GObject.SignalFlags.RUN_FIRST, None, [GObject.Object]),
|
"returns": (GObject.SignalFlags.RUN_FIRST, None, [GResult]),
|
||||||
}
|
}
|
||||||
def returns(self, result: B) -> None:
|
|
||||||
self.emit("returns", GResult(result, self.__class__.__name__, self.op_key))
|
|
||||||
|
|
||||||
def await_result(self, fn: Callable[[GObject.Object, B], None]) -> None:
|
def returns(self, result: B) -> None:
|
||||||
|
method_name = self.__class__.__name__
|
||||||
|
if self.op_key is None:
|
||||||
|
raise ValueError(f"op_key is not set for the function {method_name}")
|
||||||
|
self.emit("returns", GResult(result, method_name, self.op_key))
|
||||||
|
|
||||||
|
def await_result(self, fn: Callable[["ImplFunc[..., Any]", B], None]) -> None:
|
||||||
self.connect("returns", fn)
|
self.connect("returns", fn)
|
||||||
|
|
||||||
def async_run(self, *args: P.args, **kwargs: P.kwargs) -> bool:
|
def async_run(self, *args: P.args, **kwargs: P.kwargs) -> bool:
|
||||||
@@ -52,58 +53,36 @@ class ImplFunc(GObject.Object, Generic[P, B]):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def is_gobject_subclass(obj: object) -> bool:
|
class GObjApi:
|
||||||
return inspect.isclass(obj) and issubclass(obj, ImplFunc) and obj is not ImplFunc
|
def __init__(self, methods: dict[str, Callable[..., Any]]) -> None:
|
||||||
|
self._methods: dict[str, Callable[..., Any]] = methods
|
||||||
def check_module_for_gobject_classes(module: ModuleType, found_classes: list[type[GObject.Object]] | None = None) -> list[type[GObject.Object]]:
|
|
||||||
if found_classes is None:
|
|
||||||
found_classes = []
|
|
||||||
|
|
||||||
for name, obj in inspect.getmembers(module):
|
|
||||||
if is_gobject_subclass(obj):
|
|
||||||
found_classes.append(obj)
|
|
||||||
|
|
||||||
if hasattr(module, '__path__'): # Check if the module has submodules
|
|
||||||
for _, submodule_name, _ in pkgutil.iter_modules(module.__path__, module.__name__ + '.'):
|
|
||||||
submodule = importlib.import_module(submodule_name)
|
|
||||||
check_module_for_gobject_classes(submodule, found_classes)
|
|
||||||
|
|
||||||
return found_classes
|
|
||||||
|
|
||||||
class ImplApi:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._obj_registry: dict[str, type[ImplFunc]] = {}
|
self._obj_registry: dict[str, type[ImplFunc]] = {}
|
||||||
|
|
||||||
def register_all(self, module: ModuleType) -> None:
|
def register_overwrite(self, obj: type[ImplFunc]) -> None:
|
||||||
objects = check_module_for_gobject_classes(module)
|
|
||||||
for obj in objects:
|
|
||||||
self.register(obj)
|
|
||||||
|
|
||||||
def register(self, obj: type[ImplFunc]) -> None:
|
|
||||||
fn_name = obj.__name__
|
fn_name = obj.__name__
|
||||||
|
|
||||||
|
if not isinstance(obj, type(ImplFunc)):
|
||||||
|
raise ValueError(f"Object '{fn_name}' is not an instance of ImplFunc")
|
||||||
|
|
||||||
if fn_name in self._obj_registry:
|
if fn_name in self._obj_registry:
|
||||||
raise ValueError(f"Function '{fn_name}' already registered")
|
raise ValueError(f"Function '{fn_name}' already registered")
|
||||||
self._obj_registry[fn_name] = obj
|
self._obj_registry[fn_name] = obj
|
||||||
|
|
||||||
def validate(self,
|
def check_signature(self, method_annotations: dict[str, dict[str, Any]]) -> None:
|
||||||
abstr_methods: dict[str, dict[str, Any]]
|
overwrite_fns = self._obj_registry
|
||||||
) -> None:
|
|
||||||
impl_fns = self._obj_registry
|
|
||||||
|
|
||||||
# iterate over the methods and check if all are implemented
|
# iterate over the methods and check if all are implemented
|
||||||
for abstr_name, abstr_annotations in abstr_methods.items():
|
for m_name, m_annotations in method_annotations.items():
|
||||||
if abstr_name not in impl_fns:
|
if m_name not in overwrite_fns:
|
||||||
raise NotImplementedError(
|
continue
|
||||||
f"Abstract method '{abstr_name}' is not implemented"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# check if the signature of the abstract method matches the implementation
|
# check if the signature of the abstract method matches the implementation
|
||||||
# abstract signature
|
# abstract signature
|
||||||
values = list(abstr_annotations.values())
|
values = list(m_annotations.values())
|
||||||
expected_signature = (tuple(values[:-1]), values[-1:][0])
|
expected_signature = (tuple(values[:-1]), values[-1:][0])
|
||||||
|
|
||||||
# implementation signature
|
# implementation signature
|
||||||
obj = dict(impl_fns[abstr_name].__dict__)
|
obj = dict(overwrite_fns[m_name].__dict__)
|
||||||
obj_type = obj["__orig_bases__"][0]
|
obj_type = obj["__orig_bases__"][0]
|
||||||
got_signature = obj_type.__args__
|
got_signature = obj_type.__args__
|
||||||
|
|
||||||
@@ -111,12 +90,26 @@ class ImplApi:
|
|||||||
log.error(f"Expected signature: {expected_signature}")
|
log.error(f"Expected signature: {expected_signature}")
|
||||||
log.error(f"Actual signature: {got_signature}")
|
log.error(f"Actual signature: {got_signature}")
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Abstract method '{abstr_name}' has different signature than the implementation"
|
f"Overwritten method '{m_name}' has different signature than the implementation"
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_obj(self, name: str) -> type[ImplFunc] | None:
|
def get_obj(self, name: str) -> type[ImplFunc]:
|
||||||
return self._obj_registry.get(name)
|
result = self._obj_registry.get(name, None)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
plain_method = self._methods.get(name, None)
|
||||||
|
if plain_method is None:
|
||||||
|
raise ValueError(f"Method '{name}' not found in Api")
|
||||||
|
|
||||||
|
class GenericFnRuntime(ImplFunc[..., Any]):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def async_run(self, *args: Any, **kwargs: dict[str, Any]) -> bool:
|
||||||
|
assert plain_method is not None
|
||||||
|
result = plain_method(*args, **kwargs)
|
||||||
|
self.returns(result)
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
return cast(type[ImplFunc], GenericFnRuntime)
|
||||||
|
|||||||
@@ -84,5 +84,3 @@ class open_file(ImplFunc[[FileRequest], str | None]):
|
|||||||
dialog.save(callback=on_save_finish)
|
dialog.save(callback=on_save_finish)
|
||||||
|
|
||||||
return GLib.SOURCE_REMOVE
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import fields, is_dataclass
|
from dataclasses import fields, is_dataclass
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli.api import API
|
from clan_cli.api import MethodRegistry
|
||||||
|
|
||||||
import clan_app
|
from clan_app.api import GObjApi, GResult, ImplFunc
|
||||||
from clan_app.api import GResult, ImplApi, ImplFunc
|
from clan_app.api.file import open_file
|
||||||
from clan_app.components.serializer import dataclass_to_dict, from_dict
|
from clan_app.components.serializer import dataclass_to_dict, from_dict
|
||||||
|
|
||||||
gi.require_version("WebKit", "6.0")
|
gi.require_version("WebKit", "6.0")
|
||||||
@@ -18,19 +17,21 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class WebExecutor(GObject.Object):
|
class WebExecutor(GObject.Object):
|
||||||
def __init__(self, content_uri: str, abstr_methods: dict[str, Callable]) -> None:
|
def __init__(self, content_uri: str, plain_api: MethodRegistry) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.plain_api: MethodRegistry = plain_api
|
||||||
|
self.webview: WebKit.WebView = WebKit.WebView()
|
||||||
|
|
||||||
self.webview = WebKit.WebView()
|
settings: WebKit.Settings = self.webview.get_settings()
|
||||||
|
|
||||||
settings = self.webview.get_settings()
|
|
||||||
# settings.
|
# settings.
|
||||||
settings.set_property("enable-developer-extras", True)
|
settings.set_property("enable-developer-extras", True)
|
||||||
self.webview.set_settings(settings)
|
self.webview.set_settings(settings)
|
||||||
# Fixme. This filtering is incomplete, it only triggers if a user clicks a link
|
# Fixme. This filtering is incomplete, it only triggers if a user clicks a link
|
||||||
self.webview.connect("decide-policy", self.on_decide_policy)
|
self.webview.connect("decide-policy", self.on_decide_policy)
|
||||||
|
|
||||||
self.manager = self.webview.get_user_content_manager()
|
self.manager: WebKit.UserContentManager = (
|
||||||
|
self.webview.get_user_content_manager()
|
||||||
|
)
|
||||||
# Can be called with: window.webkit.messageHandlers.gtk.postMessage("...")
|
# Can be called with: window.webkit.messageHandlers.gtk.postMessage("...")
|
||||||
# Important: it seems postMessage must be given some payload, otherwise it won't trigger the event
|
# Important: it seems postMessage must be given some payload, otherwise it won't trigger the event
|
||||||
self.manager.register_script_message_handler("gtk")
|
self.manager.register_script_message_handler("gtk")
|
||||||
@@ -39,10 +40,10 @@ class WebExecutor(GObject.Object):
|
|||||||
self.webview.load_uri(content_uri)
|
self.webview.load_uri(content_uri)
|
||||||
self.content_uri = content_uri
|
self.content_uri = content_uri
|
||||||
|
|
||||||
self.api = ImplApi()
|
self.api: GObjApi = GObjApi(self.plain_api.functions)
|
||||||
self.api.register_all(clan_app.api)
|
|
||||||
#self.api.validate(abstr_methods)
|
|
||||||
|
|
||||||
|
self.api.register_overwrite(open_file)
|
||||||
|
self.api.check_signature(self.plain_api.annotations)
|
||||||
|
|
||||||
def on_decide_policy(
|
def on_decide_policy(
|
||||||
self,
|
self,
|
||||||
@@ -73,16 +74,13 @@ class WebExecutor(GObject.Object):
|
|||||||
def on_message_received(
|
def on_message_received(
|
||||||
self, user_content_manager: WebKit.UserContentManager, message: Any
|
self, user_content_manager: WebKit.UserContentManager, message: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
json_msg = message.to_json(4)
|
json_msg = message.to_json(4) # 4 is num of indents
|
||||||
log.debug(f"Webview Request: {json_msg}")
|
log.debug(f"Webview Request: {json_msg}")
|
||||||
payload = json.loads(json_msg)
|
payload = json.loads(json_msg)
|
||||||
method_name = payload["method"]
|
method_name = payload["method"]
|
||||||
|
|
||||||
# Get the function gobject from the api
|
# Get the function gobject from the api
|
||||||
function_obj = self.api.get_obj(method_name)
|
function_obj = self.api.get_obj(method_name)
|
||||||
if function_obj is None:
|
|
||||||
log.error(f"Method '{method_name}' not found in api")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create an instance of the function gobject
|
# Create an instance of the function gobject
|
||||||
fn_instance = function_obj()
|
fn_instance = function_obj()
|
||||||
@@ -102,7 +100,7 @@ class WebExecutor(GObject.Object):
|
|||||||
# But the js api returns dictionaries.
|
# But the js api returns dictionaries.
|
||||||
# Introspect the function and create the expected dataclass from dict dynamically
|
# Introspect the function and create the expected dataclass from dict dynamically
|
||||||
# Depending on the introspected argument_type
|
# Depending on the introspected argument_type
|
||||||
arg_class = API.get_method_argtype(method_name, k)
|
arg_class = self.plain_api.get_method_argtype(method_name, k)
|
||||||
if dataclasses.is_dataclass(arg_class):
|
if dataclasses.is_dataclass(arg_class):
|
||||||
reconciled_arguments[k] = from_dict(arg_class, v)
|
reconciled_arguments[k] = from_dict(arg_class, v)
|
||||||
else:
|
else:
|
||||||
@@ -114,7 +112,6 @@ class WebExecutor(GObject.Object):
|
|||||||
op_key,
|
op_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def on_result(self, source: ImplFunc, data: GResult) -> None:
|
def on_result(self, source: ImplFunc, data: GResult) -> None:
|
||||||
result = dict()
|
result = dict()
|
||||||
result["result"] = dataclass_to_dict(data.result)
|
result["result"] = dataclass_to_dict(data.result)
|
||||||
|
|||||||
@@ -36,12 +36,9 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
stack_view = ViewStack.use().view
|
stack_view = ViewStack.use().view
|
||||||
|
|
||||||
|
webexec = WebExecutor(plain_api=API, content_uri=config.content_uri)
|
||||||
|
|
||||||
webview = WebExecutor(
|
stack_view.add_named(webexec.get_webview(), "webview")
|
||||||
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)
|
stack_view.set_visible_child_name(config.initial_view)
|
||||||
|
|
||||||
view.set_content(stack_view)
|
view.set_content(stack_view)
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ disallow_untyped_defs = true
|
|||||||
no_implicit_optional = true
|
no_implicit_optional = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "clan_cli.*"
|
module = "argcomplete.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
line-length = 88
|
line-length = 88
|
||||||
|
|||||||
@@ -50,10 +50,22 @@ def update_wrapper_signature(wrapper: Callable, wrapped: Callable) -> None:
|
|||||||
wrapper.__signature__ = new_sig # type: ignore
|
wrapper.__signature__ = new_sig # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class _MethodRegistry:
|
class MethodRegistry:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._orig_annotations: dict[str, Callable[[Any], Any]] = {}
|
self._orig_annotations: dict[str, dict[str, Any]] = {}
|
||||||
self._registry: dict[str, Callable[[Any], Any]] = {}
|
self._registry: dict[str, Callable[..., Any]] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def annotations(self) -> dict[str, dict[str, Any]]:
|
||||||
|
return self._orig_annotations
|
||||||
|
|
||||||
|
@property
|
||||||
|
def functions(self) -> dict[str, Callable[..., Any]]:
|
||||||
|
return self._registry
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._orig_annotations.clear()
|
||||||
|
self._registry.clear()
|
||||||
|
|
||||||
def register_abstract(self, fn: Callable[..., T]) -> Callable[..., T]:
|
def register_abstract(self, fn: Callable[..., T]) -> Callable[..., T]:
|
||||||
@wraps(fn)
|
@wraps(fn)
|
||||||
@@ -84,7 +96,6 @@ API.register(open_file)
|
|||||||
return fn
|
return fn
|
||||||
|
|
||||||
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
|
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
|
||||||
|
|
||||||
if fn.__name__ in self._registry:
|
if fn.__name__ in self._registry:
|
||||||
raise ValueError(f"Function {fn.__name__} already registered")
|
raise ValueError(f"Function {fn.__name__} already registered")
|
||||||
if fn.__name__ in self._orig_annotations:
|
if fn.__name__ in self._orig_annotations:
|
||||||
@@ -189,4 +200,4 @@ API.register(open_file)
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
API = _MethodRegistry()
|
API = MethodRegistry()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import sys
|
|||||||
from dataclasses import is_dataclass
|
from dataclasses import is_dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.api import API
|
||||||
from clan_cli.api.util import JSchemaTypeError, type_to_dict
|
from clan_cli.api.util import JSchemaTypeError, type_to_dict
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ def test_all_dataclasses() -> None:
|
|||||||
for file, dataclass in dataclasses:
|
for file, dataclass in dataclasses:
|
||||||
print(f"checking dataclass {dataclass} in file: {file}")
|
print(f"checking dataclass {dataclass} in file: {file}")
|
||||||
try:
|
try:
|
||||||
|
API.reset()
|
||||||
dclass = load_dataclass_from_file(file, dataclass, str(cli_path.parent))
|
dclass = load_dataclass_from_file(file, dataclass, str(cli_path.parent))
|
||||||
type_to_dict(dclass)
|
type_to_dict(dclass)
|
||||||
except JSchemaTypeError as e:
|
except JSchemaTypeError as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user