Skip to content

Commit 8177729

Browse files
committed
Add generic call_method() for remote control API
Add call_method() to enable calling any public panel/window method from macros and remote clients. Includes intelligent method resolution and thread-safe execution via signal-based dispatch.
1 parent 352443f commit 8177729

File tree

10 files changed

+491
-12
lines changed

10 files changed

+491
-12
lines changed

.github/copilot-instructions.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,19 @@ proxy.calc("moving_average", sigima.params.MovingAverageParam.create(n=5))
337337
- `proxy.add_signal()`, `proxy.add_image()`: Create objects
338338
- `proxy.calc()`: Run processor methods
339339
- `proxy.get_object()`: Retrieve data
340-
- `proxy.delete_object()`: Remove objects
340+
- `proxy.call_method()`: Call any public panel or window method
341+
342+
**Generic Method Calling**:
343+
```python
344+
# Remove objects from current panel
345+
proxy.call_method("remove_object", force=True)
346+
347+
# Call method on specific panel
348+
proxy.call_method("delete_all_objects", panel="signal")
349+
350+
# Call main window method
351+
panel_name = proxy.call_method("get_current_panel")
352+
```
341353

342354
### 6. Remote Control API
343355

datalab/control/baseproxy.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,50 @@ def calc(self, name: str, param: gds.DataSet | None = None) -> gds.DataSet:
519519
ValueError: unknown function
520520
"""
521521

522+
@abc.abstractmethod
523+
def call_method(
524+
self,
525+
method_name: str,
526+
*args,
527+
panel: str | None = None,
528+
**kwargs,
529+
):
530+
"""Call a public method on a panel or main window.
531+
532+
This generic method allows calling any public method that is not explicitly
533+
exposed in the proxy API. The method resolution follows this order:
534+
535+
1. If panel is specified: call method on that specific panel
536+
2. If panel is None:
537+
a. Try to call method on main window (DLMainWindow)
538+
b. If not found, try to call method on current panel (BaseDataPanel)
539+
540+
This makes it convenient to call panel methods without specifying the panel
541+
parameter when working on the current panel.
542+
543+
Args:
544+
method_name: Name of the method to call
545+
*args: Positional arguments to pass to the method
546+
panel: Panel name ("signal", "image", or None for auto-detection).
547+
Defaults to None.
548+
**kwargs: Keyword arguments to pass to the method
549+
550+
Returns:
551+
The return value of the called method
552+
553+
Raises:
554+
AttributeError: If the method does not exist or is not public
555+
ValueError: If the panel name is invalid
556+
557+
Examples:
558+
>>> # Call remove_object on current panel (auto-detected)
559+
>>> proxy.call_method("remove_object", force=True)
560+
>>> # Call a signal panel method specifically
561+
>>> proxy.call_method("delete_all_objects", panel="signal")
562+
>>> # Call main window method
563+
>>> proxy.call_method("raise_window")
564+
"""
565+
522566

523567
class BaseProxy(AbstractDLControl, metaclass=abc.ABCMeta):
524568
"""Common base class for DataLab proxies
@@ -790,3 +834,32 @@ def import_macro_from_file(self, filename: str) -> None:
790834
filename: Filename.
791835
"""
792836
return self._datalab.import_macro_from_file(filename)
837+
838+
def call_method(
839+
self,
840+
method_name: str,
841+
*args,
842+
panel: str | None = None,
843+
**kwargs,
844+
):
845+
"""Call a public method on a panel or main window.
846+
847+
Method resolution order when panel is None:
848+
1. Try main window (DLMainWindow)
849+
2. If not found, try current panel (BaseDataPanel)
850+
851+
Args:
852+
method_name: Name of the method to call
853+
*args: Positional arguments to pass to the method
854+
panel: Panel name ("signal", "image", or None for auto-detection).
855+
Defaults to None.
856+
**kwargs: Keyword arguments to pass to the method
857+
858+
Returns:
859+
The return value of the called method
860+
861+
Raises:
862+
AttributeError: If the method does not exist or is not public
863+
ValueError: If the panel name is invalid
864+
"""
865+
return self._datalab.call_method(method_name, *args, panel=panel, **kwargs)

datalab/control/remote.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ class RemoteServer(QC.QThread):
8989
SIG_RUN_MACRO = QC.Signal(str)
9090
SIG_STOP_MACRO = QC.Signal(str)
9191
SIG_IMPORT_MACRO_FROM_FILE = QC.Signal(str)
92+
SIG_CALL_METHOD = QC.Signal(str, list, object, dict)
9293

9394
def __init__(self, win: DLMainWindow) -> None:
9495
QC.QThread.__init__(self)
@@ -120,6 +121,7 @@ def __init__(self, win: DLMainWindow) -> None:
120121
self.SIG_RUN_MACRO.connect(win.run_macro)
121122
self.SIG_STOP_MACRO.connect(win.stop_macro)
122123
self.SIG_IMPORT_MACRO_FROM_FILE.connect(win.import_macro_from_file)
124+
self.SIG_CALL_METHOD.connect(win.call_method_slot)
123125

124126
def serve(self) -> None:
125127
"""Start server and serve forever"""
@@ -635,6 +637,38 @@ def import_macro_from_file(self, filename: str) -> None:
635637
"""
636638
self.SIG_IMPORT_MACRO_FROM_FILE.emit(filename)
637639

640+
@remote_call
641+
def call_method(
642+
self,
643+
method_name: str,
644+
call_params: dict,
645+
):
646+
"""Call a public method on a panel or main window.
647+
648+
Method resolution order when panel is None:
649+
1. Try main window (DLMainWindow)
650+
2. If not found, try current panel (BaseDataPanel)
651+
652+
Args:
653+
method_name: Name of the method to call
654+
call_params: Dictionary with keys 'args' (list), 'panel' (str|None),
655+
'kwargs' (dict). Defaults to empty for missing keys.
656+
657+
Returns:
658+
The return value of the called method
659+
660+
Raises:
661+
AttributeError: If the method does not exist or is not public
662+
ValueError: If the panel name is invalid
663+
664+
"""
665+
args = call_params.get("args", [])
666+
panel = call_params.get("panel")
667+
kwargs = call_params.get("kwargs", {})
668+
self.result = None
669+
self.SIG_CALL_METHOD.emit(method_name, args, panel, kwargs)
670+
return self.result
671+
638672

639673
RemoteServer.check_remote_functions()
640674

@@ -1014,3 +1048,39 @@ def add_annotations_from_items(
10141048
items_json = items_to_json(items)
10151049
if items_json is not None:
10161050
self._datalab.add_annotations_from_items(items_json, refresh_plot, panel)
1051+
1052+
def call_method(
1053+
self,
1054+
method_name: str,
1055+
*args,
1056+
panel: str | None = None,
1057+
**kwargs,
1058+
):
1059+
"""Call a public method on a panel or main window.
1060+
1061+
Method resolution order when panel is None:
1062+
1. Try main window (DLMainWindow)
1063+
2. If not found, try current panel (BaseDataPanel)
1064+
1065+
Args:
1066+
method_name: Name of the method to call
1067+
*args: Positional arguments to pass to the method
1068+
panel: Panel name ("signal", "image", or None for auto-detection).
1069+
Defaults to None.
1070+
**kwargs: Keyword arguments to pass to the method
1071+
1072+
Returns:
1073+
The return value of the called method
1074+
1075+
Raises:
1076+
AttributeError: If the method does not exist or is not public
1077+
ValueError: If the panel name is invalid
1078+
"""
1079+
# Convert args/kwargs to single dict for XML-RPC serialization
1080+
# This avoids XML-RPC signature mismatch issues with default parameters
1081+
call_params = {
1082+
"args": list(args) if args else [],
1083+
"panel": panel,
1084+
"kwargs": dict(kwargs) if kwargs else {},
1085+
}
1086+
return self._datalab.call_method(method_name, call_params)

datalab/gui/main.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,109 @@ def delete_metadata(
448448
panel = self.__get_current_basedatapanel()
449449
panel.delete_metadata(refresh_plot, keep_roi)
450450

451+
@remote_controlled
452+
def call_method(
453+
self,
454+
method_name: str,
455+
*args,
456+
panel: Literal["signal", "image"] | None = None,
457+
**kwargs,
458+
):
459+
"""Call a public method on a panel or main window.
460+
461+
This generic method allows calling any public method that is not explicitly
462+
exposed in the proxy API. The method resolution follows this order:
463+
464+
1. If panel is specified: call method on that specific panel
465+
2. If panel is None:
466+
a. Try to call method on main window (DLMainWindow)
467+
b. If not found, try to call method on current panel (BaseDataPanel)
468+
469+
This makes it convenient to call panel methods without specifying the panel
470+
parameter when working on the current panel.
471+
472+
Args:
473+
method_name: Name of the method to call
474+
*args: Positional arguments to pass to the method
475+
panel: Panel name ("signal", "image", or None for auto-detection).
476+
Defaults to None.
477+
**kwargs: Keyword arguments to pass to the method
478+
479+
Returns:
480+
The return value of the called method
481+
482+
Raises:
483+
AttributeError: If the method does not exist or is not public
484+
ValueError: If the panel name is invalid
485+
486+
Examples:
487+
>>> # Call remove_object on current panel (auto-detected)
488+
>>> win.call_method("remove_object", force=True)
489+
>>> # Call a signal panel method specifically
490+
>>> win.call_method("delete_all_objects", panel="signal")
491+
>>> # Call main window method
492+
>>> win.call_method("get_current_panel")
493+
"""
494+
# Security check: only allow public methods (not starting with _)
495+
if method_name.startswith("_"):
496+
raise AttributeError(
497+
f"Cannot call private method '{method_name}' through proxy"
498+
)
499+
500+
# If panel is specified, use that panel directly
501+
if panel is not None:
502+
target = self.__get_datapanel(panel)
503+
if not hasattr(target, method_name):
504+
raise AttributeError(
505+
f"Method '{method_name}' does not exist on {panel} panel"
506+
)
507+
method = getattr(target, method_name)
508+
if not callable(method):
509+
raise AttributeError(f"'{method_name}' is not a callable method")
510+
return method(*args, **kwargs)
511+
512+
# Panel is None: try main window first, then current panel
513+
# Try main window first
514+
if hasattr(self, method_name):
515+
method = getattr(self, method_name)
516+
if callable(method):
517+
return method(*args, **kwargs)
518+
519+
# Method not found on main window, try current panel
520+
current_panel = self.__get_current_basedatapanel()
521+
if hasattr(current_panel, method_name):
522+
method = getattr(current_panel, method_name)
523+
if callable(method):
524+
return method(*args, **kwargs)
525+
526+
# Method not found anywhere
527+
raise AttributeError(
528+
f"Method '{method_name}' does not exist on main window or current panel"
529+
)
530+
531+
def call_method_slot(
532+
self,
533+
method_name: str,
534+
args: list,
535+
panel: Literal["signal", "image"] | None,
536+
kwargs: dict,
537+
) -> None:
538+
"""Slot to call a method from RemoteServer thread in GUI thread.
539+
540+
This slot receives signals from RemoteServer and executes the method in
541+
the GUI thread, avoiding thread-safety issues with Qt widgets and dialogs.
542+
543+
Args:
544+
method_name: Name of the method to call
545+
args: Positional arguments as a list
546+
panel: Panel name or None for auto-detection
547+
kwargs: Keyword arguments as a dict
548+
"""
549+
# Call the method and store result in RemoteServer
550+
result = self.call_method(method_name, *args, panel=panel, **kwargs)
551+
# Store result in RemoteServer for retrieval by XML-RPC thread
552+
self.remote_server.result = result
553+
451554
@remote_controlled
452555
def get_object_shapes(
453556
self,

0 commit comments

Comments
 (0)