Skip to content

Commit

Permalink
[STUDIO] Allow passing widget as an argument to callback (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
ObaraEmmanuel committed Nov 9, 2024
1 parent d10d9c2 commit 9ddf030
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 6 deletions.
3 changes: 2 additions & 1 deletion formation/handlers/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
))
14 changes: 11 additions & 3 deletions formation/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
Expand Down
90 changes: 88 additions & 2 deletions formation/tests/samples/lambda.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"layout": "place"
},
"layout": {
"height": 362,
"height": 453,
"width": 200,
"x": 30,
"y": 30
Expand Down Expand Up @@ -33,7 +33,7 @@
"children": [
{
"attrib": {
"value": "200x329+0+0"
"value": "200x420+0+0"
},
"type": "arg"
}
Expand Down Expand Up @@ -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": "<Button-1>"
},
"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": "<Button-1>"
},
"type": "event"
}
],
"type": "tkinter.Button"
}
],
"type": "tkinter.Tk"
Expand Down
57 changes: 57 additions & 0 deletions formation/tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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("<Button-1>")
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("<Button-1>")
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), {}))
Expand All @@ -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}))

Expand All @@ -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"))
Expand Down
2 changes: 2 additions & 0 deletions formation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, (), {}

Expand Down

0 comments on commit 9ddf030

Please sign in to comment.