]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-71592: Add ability to trace Tcl commands executed by Tkinter (GH-118291)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 6 May 2024 17:12:51 +0000 (20:12 +0300)
committerGitHub <noreply@github.com>
Mon, 6 May 2024 17:12:51 +0000 (20:12 +0300)
This is an experimental feature, for internal use.

Setting tkinter._debug = True before creating the root window enables
printing every executed Tcl command (or a Tcl command equivalent to the
used Tcl C API).

This will help to convert a Tkinter example into Tcl script to check
whether the issue is caused by Tkinter or exists in the underlying Tcl/Tk
library.

Lib/tkinter/__init__.py
Modules/_tkinter.c
Modules/clinic/_tkinter.c.h

index 5031085ac3e9279b8f9e97c5c712e60ee9cdeff1..dc6ee9a1b47225784e073505807ed6ee7c5ab0f9 100644 (file)
@@ -41,6 +41,7 @@ from tkinter.constants import *
 import re
 
 wantobjects = 1
+_debug = False  # set to True to print executed Tcl/Tk commands
 
 TkVersion = float(_tkinter.TK_VERSION)
 TclVersion = float(_tkinter.TCL_VERSION)
@@ -69,7 +70,10 @@ def _stringify(value):
         else:
             value = '{%s}' % _join(value)
     else:
-        value = str(value)
+        if isinstance(value, bytes):
+            value = str(value, 'latin1')
+        else:
+            value = str(value)
         if not value:
             value = '{}'
         elif _magic_re.search(value):
@@ -411,7 +415,6 @@ class Variable:
             self._tk.globalunsetvar(self._name)
         if self._tclCommands is not None:
             for name in self._tclCommands:
-                #print '- Tkinter: deleted command', name
                 self._tk.deletecommand(name)
             self._tclCommands = None
 
@@ -683,7 +686,6 @@ class Misc:
         this widget in the Tcl interpreter."""
         if self._tclCommands is not None:
             for name in self._tclCommands:
-                #print '- Tkinter: deleted command', name
                 self.tk.deletecommand(name)
             self._tclCommands = None
 
@@ -691,7 +693,6 @@ class Misc:
         """Internal function.
 
         Delete the Tcl command provided in NAME."""
-        #print '- Tkinter: deleted command', name
         self.tk.deletecommand(name)
         try:
             self._tclCommands.remove(name)
@@ -2450,6 +2451,8 @@ class Tk(Misc, Wm):
                 baseName = baseName + ext
         interactive = False
         self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
+        if _debug:
+            self.tk.settrace(_print_command)
         if useTk:
             self._loadtk()
         if not sys.flags.ignore_environment:
@@ -2536,6 +2539,14 @@ class Tk(Misc, Wm):
         "Delegate attribute access to the interpreter object"
         return getattr(self.tk, attr)
 
+
+def _print_command(cmd, *, file=sys.stderr):
+    # Print executed Tcl/Tk commands.
+    assert isinstance(cmd, tuple)
+    cmd = _join(cmd)
+    print(cmd, file=file)
+
+
 # Ideally, the classes Pack, Place and Grid disappear, the
 # pack/place/grid methods are defined on the Widget class, and
 # everybody uses w.pack_whatever(...) instead of Pack.whatever(w,
index 3d1cff2fa52464addc04bdf073ac98a949988f97..163449a927cf188ae612529c467fa3839e13a7ad 100644 (file)
@@ -306,6 +306,7 @@ typedef struct {
     int threaded; /* True if tcl_platform[threaded] */
     Tcl_ThreadId thread_id;
     int dispatching;
+    PyObject *trace;
     /* We cannot include tclInt.h, as this is internal.
        So we cache interesting types here. */
     const Tcl_ObjType *OldBooleanType;
@@ -570,6 +571,7 @@ Tkapp_New(const char *screenName, const char *className,
                                 TCL_GLOBAL_ONLY) != NULL;
     v->thread_id = Tcl_GetCurrentThread();
     v->dispatching = 0;
+    v->trace = NULL;
 
 #ifndef TCL_THREADS
     if (v->threaded) {
@@ -1306,6 +1308,29 @@ Tkapp_ObjectResult(TkappObject *self)
     return res;
 }
 
+static int
+Tkapp_Trace(TkappObject *self, PyObject *args)
+{
+    if (args == NULL) {
+        return 0;
+    }
+    if (self->trace) {
+        PyObject *res = PyObject_CallObject(self->trace, args);
+        if (res == NULL) {
+            Py_DECREF(args);
+            return 0;
+        }
+        Py_DECREF(res);
+    }
+    Py_DECREF(args);
+    return 1;
+}
+
+#define TRACE(_self, ARGS) do {                 \
+        if ((_self)->trace && !Tkapp_Trace((_self), Py_BuildValue ARGS)) { \
+            return NULL;                        \
+        }                                       \
+    } while (0)
 
 /* Tkapp_CallProc is the event procedure that is executed in the context of
    the Tcl interpreter thread. Initially, it holds the Tcl lock, and doesn't
@@ -1320,7 +1345,12 @@ Tkapp_CallProc(Tcl_Event *evPtr, int flags)
     int objc;
     int i;
     ENTER_PYTHON
-    objv = Tkapp_CallArgs(e->args, objStore, &objc);
+    if (e->self->trace && !Tkapp_Trace(e->self, PyTuple_Pack(1, e->args))) {
+        objv = NULL;
+    }
+    else {
+        objv = Tkapp_CallArgs(e->args, objStore, &objc);
+    }
     if (!objv) {
         *(e->exc) = PyErr_GetRaisedException();
         *(e->res) = NULL;
@@ -1413,6 +1443,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args)
     }
     else
     {
+        TRACE(self, ("(O)", args));
 
         objv = Tkapp_CallArgs(args, objStore, &objc);
         if (!objv)
@@ -1455,6 +1486,8 @@ _tkinter_tkapp_eval_impl(TkappObject *self, const char *script)
     CHECK_STRING_LENGTH(script);
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((ss))", "eval", script));
+
     ENTER_TCL
     err = Tcl_Eval(Tkapp_Interp(self), script);
     ENTER_OVERLAP
@@ -1484,6 +1517,8 @@ _tkinter_tkapp_evalfile_impl(TkappObject *self, const char *fileName)
     CHECK_STRING_LENGTH(fileName);
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((ss))", "source", fileName));
+
     ENTER_TCL
     err = Tcl_EvalFile(Tkapp_Interp(self), fileName);
     ENTER_OVERLAP
@@ -1513,6 +1548,8 @@ _tkinter_tkapp_record_impl(TkappObject *self, const char *script)
     CHECK_STRING_LENGTH(script);
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((ssss))", "history", "add", script, "exec"));
+
     ENTER_TCL
     err = Tcl_RecordAndEval(Tkapp_Interp(self), script, TCL_NO_EVAL);
     ENTER_OVERLAP
@@ -1702,6 +1739,15 @@ SetVar(TkappObject *self, PyObject *args, int flags)
         newval = AsObj(newValue);
         if (newval == NULL)
             return NULL;
+
+        if (flags & TCL_GLOBAL_ONLY) {
+            TRACE((TkappObject *)self, ("((ssssO))", "uplevel", "#0", "set",
+                                        name1, newValue));
+        }
+        else {
+            TRACE((TkappObject *)self, ("((ssO))", "set", name1, newValue));
+        }
+
         ENTER_TCL
         ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, NULL,
                            newval, flags);
@@ -1719,8 +1765,22 @@ SetVar(TkappObject *self, PyObject *args, int flags)
             return NULL;
         CHECK_STRING_LENGTH(name1);
         CHECK_STRING_LENGTH(name2);
+
         /* XXX must hold tcl lock already??? */
         newval = AsObj(newValue);
+        if (((TkappObject *)self)->trace) {
+            if (flags & TCL_GLOBAL_ONLY) {
+                TRACE((TkappObject *)self, ("((sssNO))", "uplevel", "#0", "set",
+                                    PyUnicode_FromFormat("%s(%s)", name1, name2),
+                                    newValue));
+            }
+            else {
+                TRACE((TkappObject *)self, ("((sNO))", "set",
+                                    PyUnicode_FromFormat("%s(%s)", name1, name2),
+                                    newValue));
+            }
+        }
+
         ENTER_TCL
         ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, name2, newval, flags);
         ENTER_OVERLAP
@@ -1807,6 +1867,28 @@ UnsetVar(TkappObject *self, PyObject *args, int flags)
 
     CHECK_STRING_LENGTH(name1);
     CHECK_STRING_LENGTH(name2);
+
+    if (((TkappObject *)self)->trace) {
+        if (flags & TCL_GLOBAL_ONLY) {
+            if (name2) {
+                TRACE((TkappObject *)self, ("((sssN))", "uplevel", "#0", "unset",
+                                PyUnicode_FromFormat("%s(%s)", name1, name2)));
+            }
+            else {
+                TRACE((TkappObject *)self, ("((ssss))", "uplevel", "#0", "unset", name1));
+            }
+        }
+        else {
+            if (name2) {
+                TRACE((TkappObject *)self, ("((sN))", "unset",
+                                PyUnicode_FromFormat("%s(%s)", name1, name2)));
+            }
+            else {
+                TRACE((TkappObject *)self, ("((ss))", "unset", name1));
+            }
+        }
+    }
+
     ENTER_TCL
     code = Tcl_UnsetVar2(Tkapp_Interp(self), name1, name2, flags);
     ENTER_OVERLAP
@@ -1973,6 +2055,8 @@ _tkinter_tkapp_exprstring_impl(TkappObject *self, const char *s)
     CHECK_STRING_LENGTH(s);
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((ss))", "expr", s));
+
     ENTER_TCL
     retval = Tcl_ExprString(Tkapp_Interp(self), s);
     ENTER_OVERLAP
@@ -2003,6 +2087,8 @@ _tkinter_tkapp_exprlong_impl(TkappObject *self, const char *s)
     CHECK_STRING_LENGTH(s);
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((ss))", "expr", s));
+
     ENTER_TCL
     retval = Tcl_ExprLong(Tkapp_Interp(self), s, &v);
     ENTER_OVERLAP
@@ -2032,6 +2118,9 @@ _tkinter_tkapp_exprdouble_impl(TkappObject *self, const char *s)
 
     CHECK_STRING_LENGTH(s);
     CHECK_TCL_APPARTMENT;
+
+    TRACE(self, ("((ss))", "expr", s));
+
     ENTER_TCL
     retval = Tcl_ExprDouble(Tkapp_Interp(self), s, &v);
     ENTER_OVERLAP
@@ -2061,6 +2150,9 @@ _tkinter_tkapp_exprboolean_impl(TkappObject *self, const char *s)
 
     CHECK_STRING_LENGTH(s);
     CHECK_TCL_APPARTMENT;
+
+    TRACE(self, ("((ss))", "expr", s));
+
     ENTER_TCL
     retval = Tcl_ExprBoolean(Tkapp_Interp(self), s, &v);
     ENTER_OVERLAP
@@ -2286,6 +2378,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
         !WaitForMainloop(self))
         return NULL;
 
+    TRACE(self, ("((ss()O))", "proc", name, func));
+
     data = PyMem_NEW(PythonCmd_ClientData, 1);
     if (!data)
         return PyErr_NoMemory();
@@ -2344,6 +2438,8 @@ _tkinter_tkapp_deletecommand_impl(TkappObject *self, const char *name)
 
     CHECK_STRING_LENGTH(name);
 
+    TRACE(self, ("((sss))", "rename", name, ""));
+
     if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
         Tcl_Condition cond = NULL;
         CommandEvent *ev;
@@ -2469,6 +2565,8 @@ _tkinter_tkapp_createfilehandler_impl(TkappObject *self, PyObject *file,
         return NULL;
     }
 
+    TRACE(self, ("((ssiiO))", "#", "createfilehandler", tfile, mask, func));
+
     data = NewFHCD(func, file, tfile);
     if (data == NULL)
         return NULL;
@@ -2500,6 +2598,8 @@ _tkinter_tkapp_deletefilehandler(TkappObject *self, PyObject *file)
     if (tfile < 0)
         return NULL;
 
+    TRACE(self, ("((ssi))", "#", "deletefilehandler", tfile));
+
     DeleteFHCD(tfile);
 
     /* Ought to check for null Tcl_File object... */
@@ -2534,6 +2634,7 @@ _tkinter_tktimertoken_deletetimerhandler_impl(TkttObject *self)
     PyObject *func = v->func;
 
     if (v->token != NULL) {
+        /* TRACE(...) */
         Tcl_DeleteTimerHandler(v->token);
         v->token = NULL;
     }
@@ -2636,6 +2737,8 @@ _tkinter_tkapp_createtimerhandler_impl(TkappObject *self, int milliseconds,
 
     CHECK_TCL_APPARTMENT;
 
+    TRACE(self, ("((siO))", "after", milliseconds, func));
+
     v = Tktt_New(func);
     if (v) {
         v->token = Tcl_CreateTimerHandler(milliseconds, TimerHandler,
@@ -2803,6 +2906,47 @@ Tkapp_WantObjects(PyObject *self, PyObject *args)
     Py_RETURN_NONE;
 }
 
+/*[clinic input]
+_tkinter.tkapp.settrace
+
+    func: object
+    /
+
+Set the tracing function.
+[clinic start generated code]*/
+
+static PyObject *
+_tkinter_tkapp_settrace(TkappObject *self, PyObject *func)
+/*[clinic end generated code: output=847f6ebdf46e84fa input=31b260d46d3d018a]*/
+{
+    if (func == Py_None) {
+        func = NULL;
+    }
+    else {
+        Py_INCREF(func);
+    }
+    Py_XSETREF(self->trace, func);
+    Py_RETURN_NONE;
+}
+
+/*[clinic input]
+_tkinter.tkapp.gettrace
+
+Get the tracing function.
+[clinic start generated code]*/
+
+static PyObject *
+_tkinter_tkapp_gettrace_impl(TkappObject *self)
+/*[clinic end generated code: output=d4e2ba7d63e77bb5 input=ac2aea5be74e8c4c]*/
+{
+    PyObject *func = self->trace;
+    if (!func) {
+        func = Py_None;
+    }
+    Py_INCREF(func);
+    return func;
+}
+
 /*[clinic input]
 _tkinter.tkapp.willdispatch
 
@@ -3038,6 +3182,8 @@ static PyMethodDef Tkapp_methods[] =
 {
     _TKINTER_TKAPP_WILLDISPATCH_METHODDEF
     {"wantobjects",            Tkapp_WantObjects, METH_VARARGS},
+    _TKINTER_TKAPP_SETTRACE_METHODDEF
+    _TKINTER_TKAPP_GETTRACE_METHODDEF
     {"call",                   Tkapp_Call, METH_VARARGS},
     _TKINTER_TKAPP_EVAL_METHODDEF
     _TKINTER_TKAPP_EVALFILE_METHODDEF
index 188bcc773cfc41cc863c8003219dd921956c362f..192c49dba216e25d6007f49ee931a8074285596f 100644 (file)
@@ -622,6 +622,33 @@ _tkinter_tkapp_loadtk(TkappObject *self, PyObject *Py_UNUSED(ignored))
     return _tkinter_tkapp_loadtk_impl(self);
 }
 
+PyDoc_STRVAR(_tkinter_tkapp_settrace__doc__,
+"settrace($self, func, /)\n"
+"--\n"
+"\n"
+"Set the tracing function.");
+
+#define _TKINTER_TKAPP_SETTRACE_METHODDEF    \
+    {"settrace", (PyCFunction)_tkinter_tkapp_settrace, METH_O, _tkinter_tkapp_settrace__doc__},
+
+PyDoc_STRVAR(_tkinter_tkapp_gettrace__doc__,
+"gettrace($self, /)\n"
+"--\n"
+"\n"
+"Get the tracing function.");
+
+#define _TKINTER_TKAPP_GETTRACE_METHODDEF    \
+    {"gettrace", (PyCFunction)_tkinter_tkapp_gettrace, METH_NOARGS, _tkinter_tkapp_gettrace__doc__},
+
+static PyObject *
+_tkinter_tkapp_gettrace_impl(TkappObject *self);
+
+static PyObject *
+_tkinter_tkapp_gettrace(TkappObject *self, PyObject *Py_UNUSED(ignored))
+{
+    return _tkinter_tkapp_gettrace_impl(self);
+}
+
 PyDoc_STRVAR(_tkinter_tkapp_willdispatch__doc__,
 "willdispatch($self, /)\n"
 "--\n"
@@ -861,4 +888,4 @@ exit:
 #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF
     #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF
 #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */
-/*[clinic end generated code: output=d447501ec5aa9447 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=86a515890d48a2ce input=a9049054013a1b77]*/