gh-71592: Add ability to trace Tcl commands executed by Tkinter (GH-118291)

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.
This commit is contained in:
Serhiy Storchaka 2024-05-06 20:12:51 +03:00 committed by GitHub
parent 417dd3aca7
commit 1ff626ebda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 190 additions and 6 deletions

View file

@ -41,6 +41,7 @@ from tkinter.constants import *
import re import re
wantobjects = 1 wantobjects = 1
_debug = False # set to True to print executed Tcl/Tk commands
TkVersion = float(_tkinter.TK_VERSION) TkVersion = float(_tkinter.TK_VERSION)
TclVersion = float(_tkinter.TCL_VERSION) TclVersion = float(_tkinter.TCL_VERSION)
@ -69,7 +70,10 @@ def _stringify(value):
else: else:
value = '{%s}' % _join(value) value = '{%s}' % _join(value)
else: else:
value = str(value) if isinstance(value, bytes):
value = str(value, 'latin1')
else:
value = str(value)
if not value: if not value:
value = '{}' value = '{}'
elif _magic_re.search(value): elif _magic_re.search(value):
@ -411,7 +415,6 @@ class Variable:
self._tk.globalunsetvar(self._name) self._tk.globalunsetvar(self._name)
if self._tclCommands is not None: if self._tclCommands is not None:
for name in self._tclCommands: for name in self._tclCommands:
#print '- Tkinter: deleted command', name
self._tk.deletecommand(name) self._tk.deletecommand(name)
self._tclCommands = None self._tclCommands = None
@ -683,7 +686,6 @@ class Misc:
this widget in the Tcl interpreter.""" this widget in the Tcl interpreter."""
if self._tclCommands is not None: if self._tclCommands is not None:
for name in self._tclCommands: for name in self._tclCommands:
#print '- Tkinter: deleted command', name
self.tk.deletecommand(name) self.tk.deletecommand(name)
self._tclCommands = None self._tclCommands = None
@ -691,7 +693,6 @@ class Misc:
"""Internal function. """Internal function.
Delete the Tcl command provided in NAME.""" Delete the Tcl command provided in NAME."""
#print '- Tkinter: deleted command', name
self.tk.deletecommand(name) self.tk.deletecommand(name)
try: try:
self._tclCommands.remove(name) self._tclCommands.remove(name)
@ -2450,6 +2451,8 @@ class Tk(Misc, Wm):
baseName = baseName + ext baseName = baseName + ext
interactive = False interactive = False
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use) self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
if _debug:
self.tk.settrace(_print_command)
if useTk: if useTk:
self._loadtk() self._loadtk()
if not sys.flags.ignore_environment: if not sys.flags.ignore_environment:
@ -2536,6 +2539,14 @@ class Tk(Misc, Wm):
"Delegate attribute access to the interpreter object" "Delegate attribute access to the interpreter object"
return getattr(self.tk, attr) 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 # Ideally, the classes Pack, Place and Grid disappear, the
# pack/place/grid methods are defined on the Widget class, and # pack/place/grid methods are defined on the Widget class, and
# everybody uses w.pack_whatever(...) instead of Pack.whatever(w, # everybody uses w.pack_whatever(...) instead of Pack.whatever(w,

View file

@ -306,6 +306,7 @@ typedef struct {
int threaded; /* True if tcl_platform[threaded] */ int threaded; /* True if tcl_platform[threaded] */
Tcl_ThreadId thread_id; Tcl_ThreadId thread_id;
int dispatching; int dispatching;
PyObject *trace;
/* We cannot include tclInt.h, as this is internal. /* We cannot include tclInt.h, as this is internal.
So we cache interesting types here. */ So we cache interesting types here. */
const Tcl_ObjType *OldBooleanType; const Tcl_ObjType *OldBooleanType;
@ -570,6 +571,7 @@ Tkapp_New(const char *screenName, const char *className,
TCL_GLOBAL_ONLY) != NULL; TCL_GLOBAL_ONLY) != NULL;
v->thread_id = Tcl_GetCurrentThread(); v->thread_id = Tcl_GetCurrentThread();
v->dispatching = 0; v->dispatching = 0;
v->trace = NULL;
#ifndef TCL_THREADS #ifndef TCL_THREADS
if (v->threaded) { if (v->threaded) {
@ -1306,6 +1308,29 @@ Tkapp_ObjectResult(TkappObject *self)
return res; 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 /* 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 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 objc;
int i; int i;
ENTER_PYTHON 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) { if (!objv) {
*(e->exc) = PyErr_GetRaisedException(); *(e->exc) = PyErr_GetRaisedException();
*(e->res) = NULL; *(e->res) = NULL;
@ -1413,6 +1443,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args)
} }
else else
{ {
TRACE(self, ("(O)", args));
objv = Tkapp_CallArgs(args, objStore, &objc); objv = Tkapp_CallArgs(args, objStore, &objc);
if (!objv) if (!objv)
@ -1455,6 +1486,8 @@ _tkinter_tkapp_eval_impl(TkappObject *self, const char *script)
CHECK_STRING_LENGTH(script); CHECK_STRING_LENGTH(script);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "eval", script));
ENTER_TCL ENTER_TCL
err = Tcl_Eval(Tkapp_Interp(self), script); err = Tcl_Eval(Tkapp_Interp(self), script);
ENTER_OVERLAP ENTER_OVERLAP
@ -1484,6 +1517,8 @@ _tkinter_tkapp_evalfile_impl(TkappObject *self, const char *fileName)
CHECK_STRING_LENGTH(fileName); CHECK_STRING_LENGTH(fileName);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "source", fileName));
ENTER_TCL ENTER_TCL
err = Tcl_EvalFile(Tkapp_Interp(self), fileName); err = Tcl_EvalFile(Tkapp_Interp(self), fileName);
ENTER_OVERLAP ENTER_OVERLAP
@ -1513,6 +1548,8 @@ _tkinter_tkapp_record_impl(TkappObject *self, const char *script)
CHECK_STRING_LENGTH(script); CHECK_STRING_LENGTH(script);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ssss))", "history", "add", script, "exec"));
ENTER_TCL ENTER_TCL
err = Tcl_RecordAndEval(Tkapp_Interp(self), script, TCL_NO_EVAL); err = Tcl_RecordAndEval(Tkapp_Interp(self), script, TCL_NO_EVAL);
ENTER_OVERLAP ENTER_OVERLAP
@ -1702,6 +1739,15 @@ SetVar(TkappObject *self, PyObject *args, int flags)
newval = AsObj(newValue); newval = AsObj(newValue);
if (newval == NULL) if (newval == NULL)
return 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 ENTER_TCL
ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, NULL, ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, NULL,
newval, flags); newval, flags);
@ -1719,8 +1765,22 @@ SetVar(TkappObject *self, PyObject *args, int flags)
return NULL; return NULL;
CHECK_STRING_LENGTH(name1); CHECK_STRING_LENGTH(name1);
CHECK_STRING_LENGTH(name2); CHECK_STRING_LENGTH(name2);
/* XXX must hold tcl lock already??? */ /* XXX must hold tcl lock already??? */
newval = AsObj(newValue); 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 ENTER_TCL
ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, name2, newval, flags); ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, name2, newval, flags);
ENTER_OVERLAP ENTER_OVERLAP
@ -1807,6 +1867,28 @@ UnsetVar(TkappObject *self, PyObject *args, int flags)
CHECK_STRING_LENGTH(name1); CHECK_STRING_LENGTH(name1);
CHECK_STRING_LENGTH(name2); 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 ENTER_TCL
code = Tcl_UnsetVar2(Tkapp_Interp(self), name1, name2, flags); code = Tcl_UnsetVar2(Tkapp_Interp(self), name1, name2, flags);
ENTER_OVERLAP ENTER_OVERLAP
@ -1973,6 +2055,8 @@ _tkinter_tkapp_exprstring_impl(TkappObject *self, const char *s)
CHECK_STRING_LENGTH(s); CHECK_STRING_LENGTH(s);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "expr", s));
ENTER_TCL ENTER_TCL
retval = Tcl_ExprString(Tkapp_Interp(self), s); retval = Tcl_ExprString(Tkapp_Interp(self), s);
ENTER_OVERLAP ENTER_OVERLAP
@ -2003,6 +2087,8 @@ _tkinter_tkapp_exprlong_impl(TkappObject *self, const char *s)
CHECK_STRING_LENGTH(s); CHECK_STRING_LENGTH(s);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "expr", s));
ENTER_TCL ENTER_TCL
retval = Tcl_ExprLong(Tkapp_Interp(self), s, &v); retval = Tcl_ExprLong(Tkapp_Interp(self), s, &v);
ENTER_OVERLAP ENTER_OVERLAP
@ -2032,6 +2118,9 @@ _tkinter_tkapp_exprdouble_impl(TkappObject *self, const char *s)
CHECK_STRING_LENGTH(s); CHECK_STRING_LENGTH(s);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "expr", s));
ENTER_TCL ENTER_TCL
retval = Tcl_ExprDouble(Tkapp_Interp(self), s, &v); retval = Tcl_ExprDouble(Tkapp_Interp(self), s, &v);
ENTER_OVERLAP ENTER_OVERLAP
@ -2061,6 +2150,9 @@ _tkinter_tkapp_exprboolean_impl(TkappObject *self, const char *s)
CHECK_STRING_LENGTH(s); CHECK_STRING_LENGTH(s);
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((ss))", "expr", s));
ENTER_TCL ENTER_TCL
retval = Tcl_ExprBoolean(Tkapp_Interp(self), s, &v); retval = Tcl_ExprBoolean(Tkapp_Interp(self), s, &v);
ENTER_OVERLAP ENTER_OVERLAP
@ -2286,6 +2378,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
!WaitForMainloop(self)) !WaitForMainloop(self))
return NULL; return NULL;
TRACE(self, ("((ss()O))", "proc", name, func));
data = PyMem_NEW(PythonCmd_ClientData, 1); data = PyMem_NEW(PythonCmd_ClientData, 1);
if (!data) if (!data)
return PyErr_NoMemory(); return PyErr_NoMemory();
@ -2344,6 +2438,8 @@ _tkinter_tkapp_deletecommand_impl(TkappObject *self, const char *name)
CHECK_STRING_LENGTH(name); CHECK_STRING_LENGTH(name);
TRACE(self, ("((sss))", "rename", name, ""));
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
Tcl_Condition cond = NULL; Tcl_Condition cond = NULL;
CommandEvent *ev; CommandEvent *ev;
@ -2469,6 +2565,8 @@ _tkinter_tkapp_createfilehandler_impl(TkappObject *self, PyObject *file,
return NULL; return NULL;
} }
TRACE(self, ("((ssiiO))", "#", "createfilehandler", tfile, mask, func));
data = NewFHCD(func, file, tfile); data = NewFHCD(func, file, tfile);
if (data == NULL) if (data == NULL)
return NULL; return NULL;
@ -2500,6 +2598,8 @@ _tkinter_tkapp_deletefilehandler(TkappObject *self, PyObject *file)
if (tfile < 0) if (tfile < 0)
return NULL; return NULL;
TRACE(self, ("((ssi))", "#", "deletefilehandler", tfile));
DeleteFHCD(tfile); DeleteFHCD(tfile);
/* Ought to check for null Tcl_File object... */ /* Ought to check for null Tcl_File object... */
@ -2534,6 +2634,7 @@ _tkinter_tktimertoken_deletetimerhandler_impl(TkttObject *self)
PyObject *func = v->func; PyObject *func = v->func;
if (v->token != NULL) { if (v->token != NULL) {
/* TRACE(...) */
Tcl_DeleteTimerHandler(v->token); Tcl_DeleteTimerHandler(v->token);
v->token = NULL; v->token = NULL;
} }
@ -2636,6 +2737,8 @@ _tkinter_tkapp_createtimerhandler_impl(TkappObject *self, int milliseconds,
CHECK_TCL_APPARTMENT; CHECK_TCL_APPARTMENT;
TRACE(self, ("((siO))", "after", milliseconds, func));
v = Tktt_New(func); v = Tktt_New(func);
if (v) { if (v) {
v->token = Tcl_CreateTimerHandler(milliseconds, TimerHandler, v->token = Tcl_CreateTimerHandler(milliseconds, TimerHandler,
@ -2803,6 +2906,47 @@ Tkapp_WantObjects(PyObject *self, PyObject *args)
Py_RETURN_NONE; 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] /*[clinic input]
_tkinter.tkapp.willdispatch _tkinter.tkapp.willdispatch
@ -3038,6 +3182,8 @@ static PyMethodDef Tkapp_methods[] =
{ {
_TKINTER_TKAPP_WILLDISPATCH_METHODDEF _TKINTER_TKAPP_WILLDISPATCH_METHODDEF
{"wantobjects", Tkapp_WantObjects, METH_VARARGS}, {"wantobjects", Tkapp_WantObjects, METH_VARARGS},
_TKINTER_TKAPP_SETTRACE_METHODDEF
_TKINTER_TKAPP_GETTRACE_METHODDEF
{"call", Tkapp_Call, METH_VARARGS}, {"call", Tkapp_Call, METH_VARARGS},
_TKINTER_TKAPP_EVAL_METHODDEF _TKINTER_TKAPP_EVAL_METHODDEF
_TKINTER_TKAPP_EVALFILE_METHODDEF _TKINTER_TKAPP_EVALFILE_METHODDEF

View file

@ -622,6 +622,33 @@ _tkinter_tkapp_loadtk(TkappObject *self, PyObject *Py_UNUSED(ignored))
return _tkinter_tkapp_loadtk_impl(self); 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__, PyDoc_STRVAR(_tkinter_tkapp_willdispatch__doc__,
"willdispatch($self, /)\n" "willdispatch($self, /)\n"
"--\n" "--\n"
@ -861,4 +888,4 @@ exit:
#ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF
#define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF
#endif /* !defined(_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]*/