From 9ddf0303980cf271bbedf5c34eaed40232607abb Mon Sep 17 00:00:00 2001 From: ObaraEmmanuel Date: Sat, 9 Nov 2024 23:42:34 +0300 Subject: [PATCH] [STUDIO] Allow passing widget as an argument to callback (#48) --- formation/handlers/command.py | 3 +- formation/loader.py | 14 ++++- formation/tests/samples/lambda.json | 90 ++++++++++++++++++++++++++++- formation/tests/test_bindings.py | 57 ++++++++++++++++++ formation/utils.py | 2 + 5 files changed, 160 insertions(+), 6 deletions(-) diff --git a/formation/handlers/command.py b/formation/handlers/command.py index ab8f97e..b3954b6 100644 --- a/formation/handlers/command.py +++ b/formation/handlers/command.py @@ -18,5 +18,6 @@ def handle(widget, config, **kwargs): builder._command_map.append(( prop, props[prop], - kwargs.get("handle_method") + kwargs.get("handle_method"), + widget )) diff --git a/formation/loader.py b/formation/loader.py index d1f2176..bdf1f83 100644 --- a/formation/loader.py +++ b/formation/loader.py @@ -460,12 +460,16 @@ def on_keypress(self, event): for widget, events in self._event_map.items(): for event in events: handler_string = event.get("handler") - parsed = callback_parse(handler_string) + parsed = list(callback_parse(handler_string)) if parsed is None: logger.warning("Callback string '%s' is malformed", handler_string) continue # parsed[0] is the function name. + # starting with "::" means the widget is the first argument + if handler_string.startswith("::"): + parsed[1] = (widget, *parsed[1]) + handler = callback_map.get(parsed[0]) if handler is not None: # parsed[1] is function args/ parsed[2] is function kwargs. @@ -478,11 +482,15 @@ def on_keypress(self, event): else: logger.warning("Callback '%s' not found", parsed[0]) - for prop, val, handle_method in self._command_map: - parsed = callback_parse(val) + for prop, val, handle_method, widget in self._command_map: + parsed = list(callback_parse(val)) if parsed is None: logger.warning("Callback string '%s' is malformed", val) continue + # starting with "::" means the widget is the first argument after the event object + if val.startswith("::"): + parsed[1] = (widget, *parsed[1]) + handler = callback_map.get(parsed[0]) if handle_method is None: raise ValueError("Handle method is None, unable to apply binding") diff --git a/formation/tests/samples/lambda.json b/formation/tests/samples/lambda.json index 7f5d466..7a1a296 100644 --- a/formation/tests/samples/lambda.json +++ b/formation/tests/samples/lambda.json @@ -4,7 +4,7 @@ "layout": "place" }, "layout": { - "height": 362, + "height": 453, "width": 200, "x": 30, "y": 30 @@ -33,7 +33,7 @@ "children": [ { "attrib": { - "value": "200x329+0+0" + "value": "200x420+0+0" }, "type": "arg" } @@ -261,6 +261,92 @@ } ], "type": "tkinter.ttk.Frame" + }, + { + "attrib": { + "attr": { + "command": "::no_arg_widget", + "text": "button_b" + }, + "layout": { + "bordermode": "outside", + "height": "25", + "width": "80", + "x": 99, + "y": 6 + }, + "name": "bb" + }, + "type": "tkinter.Button" + }, + { + "attrib": { + "attr": { + "command": "::single_arg_widget(2)", + "text": "button_a" + }, + "layout": { + "bordermode": "outside", + "height": "25", + "width": "80", + "x": 6, + "y": 7 + }, + "name": "ba" + }, + "type": "tkinter.Button" + }, + { + "attrib": { + "attr": { + "text": "button_d" + }, + "layout": { + "bordermode": "outside", + "height": "25", + "width": "80", + "x": 107, + "y": 387 + }, + "name": "bd" + }, + "children": [ + { + "attrib": { + "add": "False", + "handler": "::no_arg_widget()", + "sequence": "" + }, + "type": "event" + } + ], + "type": "tkinter.Button" + }, + { + "attrib": { + "attr": { + "text": "button_c" + }, + "layout": { + "bordermode": "outside", + "height": "25", + "width": "80", + "x": 14, + "y": 388 + }, + "name": "bc" + }, + "children": [ + { + "attrib": { + "add": "False", + "handler": "::dual_arg_widget(1,2)", + "sequence": "" + }, + "type": "event" + } + ], + "type": "tkinter.Button" } ], "type": "tkinter.Tk" diff --git a/formation/tests/test_bindings.py b/formation/tests/test_bindings.py index cf6d12a..535074f 100644 --- a/formation/tests/test_bindings.py +++ b/formation/tests/test_bindings.py @@ -101,6 +101,15 @@ def kwarg(self, **kw): def mixed_arg(self, a, b, **kw): self.args = (a, b, kw) + def no_arg_widget(self, w): + self.args = w + + def single_arg_widget(self, w, a): + self.args = (w, a) + + def dual_arg_widget(self, w, a, b): + self.args = (w, a, b) + def test_single_arg(self): widgets = ("b1", "b2", "b3", "b4") @@ -132,6 +141,18 @@ def test_mixed_arg(self): widget.invoke() self.assertEqual(self.args, (6, "yes", {"text": "seven", "num": 5})) + def test_no_arg_widget(self): + self.args = None + widget = self.builder.bb + widget.invoke() + self.assertEqual(self.args, widget) + + def test_single_arg_widget(self): + self.args = None + widget = self.builder.ba + widget.invoke() + self.assertEqual(self.args, (widget, 2)) + class LambdaBinding(unittest.TestCase): @@ -160,6 +181,18 @@ def mixed_arg(self, e, a, b, **kw): self.args = (a, b, kw) self.event = e + def no_arg_widget(self, e, w): + self.args = w + self.event = e + + def dual_arg_widget(self, e, w, a, b): + self.args = (w, a, b) + self.event = e + + def single_arg_widget(self, e, w, a): + self.args = (w, a) + self.event = e + def test_single_arg(self): widgets = ("b1", "b2", "b3", "b4") @@ -202,11 +235,30 @@ def test_mixed_arg(self): self.assertEqual(self.args, (6, "yes", {"text": "seven", "num": 5})) self.assertIsInstance(self.event, tkinter.Event) + def test_no_arg_widget(self): + self.args = None + self.event = None + widget = self.builder.bd + widget.update() + widget.event_generate("") + self.assertEqual(self.args, widget) + self.assertIsInstance(self.event, tkinter.Event) + + def test_dual_arg_widget(self): + self.args = None + self.event = None + widget = self.builder.bc + widget.update() + widget.event_generate("") + self.assertEqual(self.args, (widget, 1, 2)) + self.assertIsInstance(self.event, tkinter.Event) + class CallbackParseTest(unittest.TestCase): def test_args(self): self.assertEqual(callback_parse("func(2)"), ("func", (2,), {})) + self.assertEqual(callback_parse("::func(2)"), ("func", (2,), {})) self.assertEqual(callback_parse("func(2, 3)"), ("func", (2, 3), {})) self.assertEqual(callback_parse("func(2, '3', 4)"), ("func", (2, '3', 4), {})) self.assertEqual(callback_parse("func(2, \"3\", 4)"), ("func", (2, '3', 4), {})) @@ -216,15 +268,18 @@ def test_keyword_rejection(self): self.assertIsNone(callback_parse("while()")) self.assertIsNone(callback_parse("True(45)")) self.assertIsNone(callback_parse("True")) + self.assertIsNone(callback_parse("::True")) def test_kwargs(self): self.assertEqual(callback_parse("func(a=2)"), ("func", (), {"a": 2})) self.assertEqual(callback_parse("func(a= 2, b=3)"), ("func", (), {"a": 2, "b": 3})) + self.assertEqual(callback_parse("::func(a= 2, b=3)"), ("func", (), {"a": 2, "b": 3})) self.assertEqual(callback_parse("func(a=2, b='3', c=4)"), ("func", (), {"a": 2, "b": '3', "c": 4})) self.assertEqual(callback_parse("func(a= 2, b=\"3\", c=4)"), ("func", (), {"a": 2, "b": '3', "c": 4})) def test_arg_eval(self): self.assertEqual(callback_parse("func(2+3)"), ("func", (5,), {})) + self.assertEqual(callback_parse("::func(2+3)"), ("func", (5,), {})) self.assertEqual(callback_parse("func(2, '3'+'4')"), ("func", (2, '34'), {})) self.assertEqual(callback_parse("func(bool(1), arg= float('4.556'))"), ("func", (True,), {'arg': 4.556})) @@ -234,6 +289,8 @@ def test_no_args(self): def test_format_fail(self): self.assertIsNone(callback_parse("func(2+3")) + self.assertIsNone(callback_parse(":func(2+3")) + self.assertIsNone(callback_parse("::::func(2+3")) self.assertIsNone(callback_parse("func(2, '3'+'4'")) self.assertIsNone(callback_parse("34func(bool(1), arg= float('4.556'))")) self.assertIsNone(callback_parse("func(2+3)r34")) diff --git a/formation/utils.py b/formation/utils.py index 8b5f156..b01746f 100644 --- a/formation/utils.py +++ b/formation/utils.py @@ -170,6 +170,8 @@ def callback_parse(command: str): just ``funcname`` :return: A tuple containing (funcname, args, kwargs) or None if parsing was unsuccessful """ + if command.startswith("::"): + command = command[2:] if command.isidentifier() and not keyword.iskeyword(command): return command, (), {}