Skip to content

Commit

Permalink
gh-71592: Add ability to trace Tcl commands executed by Tkinter (GH-1…
Browse files Browse the repository at this point in the history
…18291)

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.
  • Loading branch information
serhiy-storchaka committed May 6, 2024
1 parent 417dd3a commit 1ff626e
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 6 deletions.
19 changes: 15 additions & 4 deletions Lib/tkinter/__init__.py
Expand Up @@ -41,6 +41,7 @@
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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -411,7 +415,6 @@ def __del__(self):
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

Expand Down Expand Up @@ -683,15 +686,13 @@ def destroy(self):
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

def deletecommand(self, name):
"""Internal function.
Delete the Tcl command provided in NAME."""
#print '- Tkinter: deleted command', name
self.tk.deletecommand(name)
try:
self._tclCommands.remove(name)
Expand Down Expand Up @@ -2450,6 +2451,8 @@ def __init__(self, screenName=None, baseName=None, className='Tk',
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:
Expand Down Expand Up @@ -2536,6 +2539,14 @@ def __getattr__(self, attr):
"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,
Expand Down
148 changes: 147 additions & 1 deletion Modules/_tkinter.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -1413,6 +1443,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args)
}
else
{
TRACE(self, ("(O)", args));

objv = Tkapp_CallArgs(args, objStore, &objc);
if (!objv)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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... */
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 1ff626e

Please sign in to comment.