[Python/Tkinter]How can I fetch the value of data which was set in function "event_generate"
Asked Answered
E

3

10

I'm using Python 2.7.4 and new to Tkinter, and I'm stuck with the following code. I generated an event "test" and set the "data" option with a string, but an error occurred when retrieving it from event.

Error -> AttributeError: Event instance has no attribute 'data'

from Tkinter import *

def handle_it(event):
    # print "event handler"
    print event.data

root = Tk()
root.after(1, lambda: root.event_generate('<<test>>', data="hi there"))
root.bind('<<test>>', handle_it)
root.mainloop()

I can't find the related Python docs for this case, so I referred to the tcl document as below http://www.tcl.tk/man/tcl8.5/TkCmd/event.htm#M14

Does TKinter of Python 2.7 support "data" option? Thanks!

Externalism answered 4/5, 2013 at 2:19 Comment(0)
W
11

No, unfortunately it doesn't. The Tcl interpreter recognizes it as a valid option, but it is one of the missing options that are not included in the Event class, like warp. You can take a look at the line 1188 of the Tkinter source code to see the rest of the missing options.

Williamwilliams answered 4/5, 2013 at 2:35 Comment(3)
However, I checked the fields "Tkinter.TkVersion" and "Tkinter.TclVersion", both of them are 8.5. and it seems only partially support some options refer to tcl.tk/man/tcl8.4/TkCmd/event.htm I'm sorry to hear Python doesn't support those missing options for now, I gogled that someone has filed an issue on python.org as a feature request, hopefully they will fix it bugs.python.org/issue3405Externalism
@Externalism You're welcome. There is a patch but it was posted on 2008. I've added myself to the nosy list of the issue, so if there are any news I'll let you know.Williamwilliams
Is it already supported in later Python versions?Bannock
B
6

Tkinter does not handle properly the data field of event_generate.

Here is a snippet using private API of Tkinter (in fact Tcl...) that allows to read this field. This function only works with literals and I usually pass data a dictionary with literals.

from Tkinter import *

def handle_it(event):
    # print "event handler"
    print event.data

def bind_event_data(widget, sequence, func, add = None):
    def _substitute(*args):
        e = lambda: None #simplest object with __dict__
        e.data = eval(args[0])
        e.widget = widget
        return (e,)

    funcid = widget._register(func, _substitute, needcleanup=1)
    cmd = '{0}if {{"[{1} %d]" == "break"}} break\n'.format('+' if add else '', funcid)
    widget.tk.call('bind', widget._w, sequence, cmd)

root = Tk()

# unfortunately, does not work with my snippet (the data argument is eval-ed)
# you can adapt it to handle raw string.
root.after(100, lambda : root.event_generate('<<test>>', data="hi there"))
# works, but definitely looks too hacky
root.after(100, lambda : root.event_generate('<<test>>', data="'hi there'"))
# the way I typically use it
root.after(100, lambda : root.event_generate('<<test>>', data={"content": "hi there"}))

#should be:
#  root.bind('<<test>>', handle_it)
bind_event_data (root, '<<test>>', handle_it)

root.mainloop()

Note: there seems to be a race condition that prevent the event to be catched with a too small delay in after.

Beene answered 21/4, 2014 at 10:52 Comment(0)
T
0
  1. You can't; this is a bug that's been open since 2008.
  2. The fix was finalized in April 2023; it has not been merged as of CPython 3.8.19, 3.11.9, or 3.12.5.
  3. Enjoy the bug-free, thread-safe, busy-polling-free future, today:
#!/usr/bin/env python3
# -*- filename: tkinter_iss47655_polyfill.py -*-

__all__ = ['check_for_bug', 'patch_bug', 'auto_patch_bug']

import logging
import os
import platform
import sys
import threading
import tkinter

FIXED_IN_CPYTHON_VERSION = (4,)  # FIXME update this when they actually fix it

# https://tkdocs.com/tutorial/eventloop.html
# "If you need to communicate from another thread to the thread running
# Tkinter, keep it as simple as possible. Use event_generate to post a
# virtual event to the Tkinter event queue, and then bind to that event
# in your code."


def auto_patch_bug(module=tkinter):
    # NOTE: check_for_bug and auto_patch_bug MUST be called from the
    # main thread because the tkinter module refuses to allow mainloop
    # on another thread.
    if platform.python_implementation() == 'CPython':
        if sys.version_info >= FIXED_IN_CPYTHON_VERSION:
            logging.debug("No need to polyfill tkinter issue 47655 on this Python runtime.")
        elif sys.version_info >= (3, 8):  # FIXME figure out what the actual lower bound this changeset applies seamlessly on is
            try:
                needs_patch = check_for_bug(module)
            except Exception:
                raise Exception("internal error checking for bug")
            if needs_patch:
                patch_bug(module)
                assert not check_for_bug(module), "Patch failed to apply!"
            else:
                logging.debug("No need to polyfill tkinter issue 47655 on this Python runtime.")
        else:
            raise Exception(f"patch not available on old CPython version {platform.python_version()}")
    else:
        # FIXME support other Python implementations
        raise Exception(f"patch not available yet on {platform.python_implementation()}")


def check_for_bug(module=tkinter):
    data = os.urandom(12).hex()  # NOTE: the associated data is coerced to str!
    root = module.Tk()
    root.withdraw()
    result = None
    def handle_ev(event):
        if hasattr(event, 'user_data'):
            nonlocal result
            result = event.user_data
        event.widget.quit()
    root.bind('<<test>>', handle_ev)
    t = threading.Thread(target=root.event_generate, args=('<<test>>',), kwargs={'data': data})
    t.start()
    root.mainloop()
    if result is None:
        # event never arrived, or was missing the .user_data property
        return True
    else:
        if result != data:
            # event arrived, had the .user_data property, but that property had an unexpected value
            raise RuntimeError(f"Could not validate presence of CPython Bug #47655; unexpected data arrived. (Expected {data}, got {result})")
        # event arrived, had the .user_data property, and that data was valid
        return False


def patch_bug(module=tkinter):
    def _substitute(self, *args):
        """https://github.com/python/cpython/pull/7142"""
        if len(args) != len(self._subst_format): return args
        getboolean = self.tk.getboolean

        getint = self.tk.getint
        def getint_event(s):
            """Tk changed behavior in 8.4.2, returning "??" rather more often."""
            try:
                return getint(s)
            except (ValueError, module.TclError):
                return s

        nsign, b, d, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args
        # Missing: (a, c, m, o, v, B, R)
        e = module.Event()
        # serial field: valid for all events
        # number of button: ButtonPress and ButtonRelease events only
        # detail: for Enter, Leave, FocusIn, FocusOut and ConfigureRequest
        # events certain fixed strings (see tcl/tk documentation)
        # user_data: data string from a virtual event or an empty string
        # height field: Configure, ConfigureRequest, Create,
        # ResizeRequest, and Expose events only
        # keycode field: KeyPress and KeyRelease events only
        # time field: "valid for events that contain a time field"
        # width field: Configure, ConfigureRequest, Create, ResizeRequest,
        # and Expose events only
        # x field: "valid for events that contain an x field"
        # y field: "valid for events that contain a y field"
        # keysym as decimal: KeyPress and KeyRelease events only
        # x_root, y_root fields: ButtonPress, ButtonRelease, KeyPress,
        # KeyRelease, and Motion events
        e.serial = getint(nsign)
        e.num = getint_event(b)
        e.user_data = d
        e.detail = d
        try: e.focus = getboolean(f)
        except module.TclError: pass
        e.height = getint_event(h)
        e.keycode = getint_event(k)
        e.state = getint_event(s)
        e.time = getint_event(t)
        e.width = getint_event(w)
        e.x = getint_event(x)
        e.y = getint_event(y)
        e.char = A
        try: e.send_event = getboolean(E)
        except module.TclError: pass
        e.keysym = K
        e.keysym_num = getint_event(N)
        try:
            e.type = module.EventType(T)
        except ValueError:
            e.type = T
        try:
            e.widget = self._nametowidget(W)
        except KeyError:
            e.widget = W
        e.x_root = getint_event(X)
        e.y_root = getint_event(Y)
        try:
            e.delta = getint(D)
        except (ValueError, module.TclError):
            e.delta = 0
        return (e,)

    _subst_format = ('%#', '%b', '%d', '%f', '%h', '%k',
             '%s', '%t', '%w', '%x', '%y',
             '%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D')

    _subst_format_str = " ".join(_subst_format)

    # FIXME this seems less elegant than just assigning module.Misc,
    # but I can't figure out how to make such an assignment propagate
    # "retroactively" to all the subclasses like Tk and the widgets
    module.Misc._substitute = _substitute
    module.Misc._subst_format = _subst_format
    module.Misc._subst_format_str = _subst_format_str
    logging.debug(f"Patched {module.__name__}.Misc to fix CPython Bug #47655.")


if __name__ == '__main__':
    auto_patch_bug()

Note that tkinter only supports sending str objects as associated data; if you want to send arbitrary Python objects, that's not too hard; just send the id of the object you want to attach to your event, instead:

__all__ = ['receive_object', 'post_object']

import logging
import platform
import threading
import tkinter

"""USAGE:

    # on main thread
    from tkinter_iss47655_polyfill import auto_patch_bug
    auto_patch_bug(tkinter)
    
    def handle(event):
        foo = receive_object(event)
        logging.info("")
    
    root.bind('<<arbitrary_name>>', handle)

    # on worker thread
    foo = object() # could be literally any object
    post_object(root, '<<arbitrary_name>>', foo)

"""

INFLIGHT_OBJECTS = dict()  # NOTE: don't try to use a WeakDict here; they support so few datatypes the juice isn't worth the squeeze
INFLIGHT_OBJECTS_LOCK = threading.Lock()

def receive_object(event):
    if not hasattr(event, 'user_data'):
        if platform.python_implementation() == 'CPython' and '%d' not in event.widget._subst_format:
            raise RuntimeError("Can't recieve data due to CPython Bug #47655. Patch this before using receive_object().")
        raise ValueError("No data found on this event.")
    obj_id = int(event.user_data)
    with INFLIGHT_OBJECTS_LOCK:
        result = INFLIGHT_OBJECTS.pop(obj_id)
    return result


def post_object(widget, sequence, obj, **k):
    obj_id = id(obj)
    with INFLIGHT_OBJECTS_LOCK:
        INFLIGHT_OBJECTS[obj_id] = obj
    widget.event_generate(sequence, data=obj_id, **k)
    widget.after(1000, _sanity_check(widget, obj_id, obj.__class__.__name__))


def _sanity_check(widget, obj_id, obj_type):
    def callback():
        with INFLIGHT_OBJECTS_LOCK:
            problem = (obj_id in INFLIGHT_OBJECTS)
        if problem:
            logging.warning("Object %s has been inflight to %s for over a second! Is there a memory leak, or is the system just slow?", f"#{obj_id}/{obj_type}", repr(widget))
            widget.after(29000, _last_resort(widget, obj_id, obj_type))
    return callback


def _last_resort(widget, obj_id, obj_type):
    def callback():
        try:
            with INFLIGHT_OBJECTS_LOCK:
                del INFLIGHT_OBJECTS[obj_id]
        except KeyError:
            pass
        else:
            logging.error("Object %s was inflight to %s for over 30 seconds! Aborted that inflight object as a last resort to stop leaking memory.", f"#{obj_id}/{obj_type}", repr(widget))
    return callback
Teutonism answered 26/8 at 14:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.