Skip to content

Commit 1ff626e

Browse files
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.
1 parent 417dd3a commit 1ff626e

File tree

3 files changed

+190
-6
lines changed

3 files changed

+190
-6
lines changed

Lib/tkinter/__init__.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import re
4242

4343
wantobjects = 1
44+
_debug = False # set to True to print executed Tcl/Tk commands
4445

4546
TkVersion = float(_tkinter.TK_VERSION)
4647
TclVersion = float(_tkinter.TCL_VERSION)
@@ -69,7 +70,10 @@ def _stringify(value):
6970
else:
7071
value = '{%s}' % _join(value)
7172
else:
72-
value = str(value)
73+
if isinstance(value, bytes):
74+
value = str(value, 'latin1')
75+
else:
76+
value = str(value)
7377
if not value:
7478
value = '{}'
7579
elif _magic_re.search(value):
@@ -411,7 +415,6 @@ def __del__(self):
411415
self._tk.globalunsetvar(self._name)
412416
if self._tclCommands is not None:
413417
for name in self._tclCommands:
414-
#print '- Tkinter: deleted command', name
415418
self._tk.deletecommand(name)
416419
self._tclCommands = None
417420

@@ -683,15 +686,13 @@ def destroy(self):
683686
this widget in the Tcl interpreter."""
684687
if self._tclCommands is not None:
685688
for name in self._tclCommands:
686-
#print '- Tkinter: deleted command', name
687689
self.tk.deletecommand(name)
688690
self._tclCommands = None
689691

690692
def deletecommand(self, name):
691693
"""Internal function.
692694
693695
Delete the Tcl command provided in NAME."""
694-
#print '- Tkinter: deleted command', name
695696
self.tk.deletecommand(name)
696697
try:
697698
self._tclCommands.remove(name)
@@ -2450,6 +2451,8 @@ def __init__(self, screenName=None, baseName=None, className='Tk',
24502451
baseName = baseName + ext
24512452
interactive = False
24522453
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
2454+
if _debug:
2455+
self.tk.settrace(_print_command)
24532456
if useTk:
24542457
self._loadtk()
24552458
if not sys.flags.ignore_environment:
@@ -2536,6 +2539,14 @@ def __getattr__(self, attr):
25362539
"Delegate attribute access to the interpreter object"
25372540
return getattr(self.tk, attr)
25382541

2542+
2543+
def _print_command(cmd, *, file=sys.stderr):
2544+
# Print executed Tcl/Tk commands.
2545+
assert isinstance(cmd, tuple)
2546+
cmd = _join(cmd)
2547+
print(cmd, file=file)
2548+
2549+
25392550
# Ideally, the classes Pack, Place and Grid disappear, the
25402551
# pack/place/grid methods are defined on the Widget class, and
25412552
# everybody uses w.pack_whatever(...) instead of Pack.whatever(w,

Modules/_tkinter.c

+147-1
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ typedef struct {
306306
int threaded; /* True if tcl_platform[threaded] */
307307
Tcl_ThreadId thread_id;
308308
int dispatching;
309+
PyObject *trace;
309310
/* We cannot include tclInt.h, as this is internal.
310311
So we cache interesting types here. */
311312
const Tcl_ObjType *OldBooleanType;
@@ -570,6 +571,7 @@ Tkapp_New(const char *screenName, const char *className,
570571
TCL_GLOBAL_ONLY) != NULL;
571572
v->thread_id = Tcl_GetCurrentThread();
572573
v->dispatching = 0;
574+
v->trace = NULL;
573575

574576
#ifndef TCL_THREADS
575577
if (v->threaded) {
@@ -1306,6 +1308,29 @@ Tkapp_ObjectResult(TkappObject *self)
13061308
return res;
13071309
}
13081310

1311+
static int
1312+
Tkapp_Trace(TkappObject *self, PyObject *args)
1313+
{
1314+
if (args == NULL) {
1315+
return 0;
1316+
}
1317+
if (self->trace) {
1318+
PyObject *res = PyObject_CallObject(self->trace, args);
1319+
if (res == NULL) {
1320+
Py_DECREF(args);
1321+
return 0;
1322+
}
1323+
Py_DECREF(res);
1324+
}
1325+
Py_DECREF(args);
1326+
return 1;
1327+
}
1328+
1329+
#define TRACE(_self, ARGS) do { \
1330+
if ((_self)->trace && !Tkapp_Trace((_self), Py_BuildValue ARGS)) { \
1331+
return NULL; \
1332+
} \
1333+
} while (0)
13091334

13101335
/* Tkapp_CallProc is the event procedure that is executed in the context of
13111336
the Tcl interpreter thread. Initially, it holds the Tcl lock, and doesn't
@@ -1320,7 +1345,12 @@ Tkapp_CallProc(Tcl_Event *evPtr, int flags)
13201345
int objc;
13211346
int i;
13221347
ENTER_PYTHON
1323-
objv = Tkapp_CallArgs(e->args, objStore, &objc);
1348+
if (e->self->trace && !Tkapp_Trace(e->self, PyTuple_Pack(1, e->args))) {
1349+
objv = NULL;
1350+
}
1351+
else {
1352+
objv = Tkapp_CallArgs(e->args, objStore, &objc);
1353+
}
13241354
if (!objv) {
13251355
*(e->exc) = PyErr_GetRaisedException();
13261356
*(e->res) = NULL;
@@ -1413,6 +1443,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args)
14131443
}
14141444
else
14151445
{
1446+
TRACE(self, ("(O)", args));
14161447

14171448
objv = Tkapp_CallArgs(args, objStore, &objc);
14181449
if (!objv)
@@ -1455,6 +1486,8 @@ _tkinter_tkapp_eval_impl(TkappObject *self, const char *script)
14551486
CHECK_STRING_LENGTH(script);
14561487
CHECK_TCL_APPARTMENT;
14571488

1489+
TRACE(self, ("((ss))", "eval", script));
1490+
14581491
ENTER_TCL
14591492
err = Tcl_Eval(Tkapp_Interp(self), script);
14601493
ENTER_OVERLAP
@@ -1484,6 +1517,8 @@ _tkinter_tkapp_evalfile_impl(TkappObject *self, const char *fileName)
14841517
CHECK_STRING_LENGTH(fileName);
14851518
CHECK_TCL_APPARTMENT;
14861519

1520+
TRACE(self, ("((ss))", "source", fileName));
1521+
14871522
ENTER_TCL
14881523
err = Tcl_EvalFile(Tkapp_Interp(self), fileName);
14891524
ENTER_OVERLAP
@@ -1513,6 +1548,8 @@ _tkinter_tkapp_record_impl(TkappObject *self, const char *script)
15131548
CHECK_STRING_LENGTH(script);
15141549
CHECK_TCL_APPARTMENT;
15151550

1551+
TRACE(self, ("((ssss))", "history", "add", script, "exec"));
1552+
15161553
ENTER_TCL
15171554
err = Tcl_RecordAndEval(Tkapp_Interp(self), script, TCL_NO_EVAL);
15181555
ENTER_OVERLAP
@@ -1702,6 +1739,15 @@ SetVar(TkappObject *self, PyObject *args, int flags)
17021739
newval = AsObj(newValue);
17031740
if (newval == NULL)
17041741
return NULL;
1742+
1743+
if (flags & TCL_GLOBAL_ONLY) {
1744+
TRACE((TkappObject *)self, ("((ssssO))", "uplevel", "#0", "set",
1745+
name1, newValue));
1746+
}
1747+
else {
1748+
TRACE((TkappObject *)self, ("((ssO))", "set", name1, newValue));
1749+
}
1750+
17051751
ENTER_TCL
17061752
ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, NULL,
17071753
newval, flags);
@@ -1719,8 +1765,22 @@ SetVar(TkappObject *self, PyObject *args, int flags)
17191765
return NULL;
17201766
CHECK_STRING_LENGTH(name1);
17211767
CHECK_STRING_LENGTH(name2);
1768+
17221769
/* XXX must hold tcl lock already??? */
17231770
newval = AsObj(newValue);
1771+
if (((TkappObject *)self)->trace) {
1772+
if (flags & TCL_GLOBAL_ONLY) {
1773+
TRACE((TkappObject *)self, ("((sssNO))", "uplevel", "#0", "set",
1774+
PyUnicode_FromFormat("%s(%s)", name1, name2),
1775+
newValue));
1776+
}
1777+
else {
1778+
TRACE((TkappObject *)self, ("((sNO))", "set",
1779+
PyUnicode_FromFormat("%s(%s)", name1, name2),
1780+
newValue));
1781+
}
1782+
}
1783+
17241784
ENTER_TCL
17251785
ok = Tcl_SetVar2Ex(Tkapp_Interp(self), name1, name2, newval, flags);
17261786
ENTER_OVERLAP
@@ -1807,6 +1867,28 @@ UnsetVar(TkappObject *self, PyObject *args, int flags)
18071867

18081868
CHECK_STRING_LENGTH(name1);
18091869
CHECK_STRING_LENGTH(name2);
1870+
1871+
if (((TkappObject *)self)->trace) {
1872+
if (flags & TCL_GLOBAL_ONLY) {
1873+
if (name2) {
1874+
TRACE((TkappObject *)self, ("((sssN))", "uplevel", "#0", "unset",
1875+
PyUnicode_FromFormat("%s(%s)", name1, name2)));
1876+
}
1877+
else {
1878+
TRACE((TkappObject *)self, ("((ssss))", "uplevel", "#0", "unset", name1));
1879+
}
1880+
}
1881+
else {
1882+
if (name2) {
1883+
TRACE((TkappObject *)self, ("((sN))", "unset",
1884+
PyUnicode_FromFormat("%s(%s)", name1, name2)));
1885+
}
1886+
else {
1887+
TRACE((TkappObject *)self, ("((ss))", "unset", name1));
1888+
}
1889+
}
1890+
}
1891+
18101892
ENTER_TCL
18111893
code = Tcl_UnsetVar2(Tkapp_Interp(self), name1, name2, flags);
18121894
ENTER_OVERLAP
@@ -1973,6 +2055,8 @@ _tkinter_tkapp_exprstring_impl(TkappObject *self, const char *s)
19732055
CHECK_STRING_LENGTH(s);
19742056
CHECK_TCL_APPARTMENT;
19752057

2058+
TRACE(self, ("((ss))", "expr", s));
2059+
19762060
ENTER_TCL
19772061
retval = Tcl_ExprString(Tkapp_Interp(self), s);
19782062
ENTER_OVERLAP
@@ -2003,6 +2087,8 @@ _tkinter_tkapp_exprlong_impl(TkappObject *self, const char *s)
20032087
CHECK_STRING_LENGTH(s);
20042088
CHECK_TCL_APPARTMENT;
20052089

2090+
TRACE(self, ("((ss))", "expr", s));
2091+
20062092
ENTER_TCL
20072093
retval = Tcl_ExprLong(Tkapp_Interp(self), s, &v);
20082094
ENTER_OVERLAP
@@ -2032,6 +2118,9 @@ _tkinter_tkapp_exprdouble_impl(TkappObject *self, const char *s)
20322118

20332119
CHECK_STRING_LENGTH(s);
20342120
CHECK_TCL_APPARTMENT;
2121+
2122+
TRACE(self, ("((ss))", "expr", s));
2123+
20352124
ENTER_TCL
20362125
retval = Tcl_ExprDouble(Tkapp_Interp(self), s, &v);
20372126
ENTER_OVERLAP
@@ -2061,6 +2150,9 @@ _tkinter_tkapp_exprboolean_impl(TkappObject *self, const char *s)
20612150

20622151
CHECK_STRING_LENGTH(s);
20632152
CHECK_TCL_APPARTMENT;
2153+
2154+
TRACE(self, ("((ss))", "expr", s));
2155+
20642156
ENTER_TCL
20652157
retval = Tcl_ExprBoolean(Tkapp_Interp(self), s, &v);
20662158
ENTER_OVERLAP
@@ -2286,6 +2378,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
22862378
!WaitForMainloop(self))
22872379
return NULL;
22882380

2381+
TRACE(self, ("((ss()O))", "proc", name, func));
2382+
22892383
data = PyMem_NEW(PythonCmd_ClientData, 1);
22902384
if (!data)
22912385
return PyErr_NoMemory();
@@ -2344,6 +2438,8 @@ _tkinter_tkapp_deletecommand_impl(TkappObject *self, const char *name)
23442438

23452439
CHECK_STRING_LENGTH(name);
23462440

2441+
TRACE(self, ("((sss))", "rename", name, ""));
2442+
23472443
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
23482444
Tcl_Condition cond = NULL;
23492445
CommandEvent *ev;
@@ -2469,6 +2565,8 @@ _tkinter_tkapp_createfilehandler_impl(TkappObject *self, PyObject *file,
24692565
return NULL;
24702566
}
24712567

2568+
TRACE(self, ("((ssiiO))", "#", "createfilehandler", tfile, mask, func));
2569+
24722570
data = NewFHCD(func, file, tfile);
24732571
if (data == NULL)
24742572
return NULL;
@@ -2500,6 +2598,8 @@ _tkinter_tkapp_deletefilehandler(TkappObject *self, PyObject *file)
25002598
if (tfile < 0)
25012599
return NULL;
25022600

2601+
TRACE(self, ("((ssi))", "#", "deletefilehandler", tfile));
2602+
25032603
DeleteFHCD(tfile);
25042604

25052605
/* Ought to check for null Tcl_File object... */
@@ -2534,6 +2634,7 @@ _tkinter_tktimertoken_deletetimerhandler_impl(TkttObject *self)
25342634
PyObject *func = v->func;
25352635

25362636
if (v->token != NULL) {
2637+
/* TRACE(...) */
25372638
Tcl_DeleteTimerHandler(v->token);
25382639
v->token = NULL;
25392640
}
@@ -2636,6 +2737,8 @@ _tkinter_tkapp_createtimerhandler_impl(TkappObject *self, int milliseconds,
26362737

26372738
CHECK_TCL_APPARTMENT;
26382739

2740+
TRACE(self, ("((siO))", "after", milliseconds, func));
2741+
26392742
v = Tktt_New(func);
26402743
if (v) {
26412744
v->token = Tcl_CreateTimerHandler(milliseconds, TimerHandler,
@@ -2803,6 +2906,47 @@ Tkapp_WantObjects(PyObject *self, PyObject *args)
28032906
Py_RETURN_NONE;
28042907
}
28052908

2909+
/*[clinic input]
2910+
_tkinter.tkapp.settrace
2911+
2912+
func: object
2913+
/
2914+
2915+
Set the tracing function.
2916+
[clinic start generated code]*/
2917+
2918+
static PyObject *
2919+
_tkinter_tkapp_settrace(TkappObject *self, PyObject *func)
2920+
/*[clinic end generated code: output=847f6ebdf46e84fa input=31b260d46d3d018a]*/
2921+
{
2922+
if (func == Py_None) {
2923+
func = NULL;
2924+
}
2925+
else {
2926+
Py_INCREF(func);
2927+
}
2928+
Py_XSETREF(self->trace, func);
2929+
Py_RETURN_NONE;
2930+
}
2931+
2932+
/*[clinic input]
2933+
_tkinter.tkapp.gettrace
2934+
2935+
Get the tracing function.
2936+
[clinic start generated code]*/
2937+
2938+
static PyObject *
2939+
_tkinter_tkapp_gettrace_impl(TkappObject *self)
2940+
/*[clinic end generated code: output=d4e2ba7d63e77bb5 input=ac2aea5be74e8c4c]*/
2941+
{
2942+
PyObject *func = self->trace;
2943+
if (!func) {
2944+
func = Py_None;
2945+
}
2946+
Py_INCREF(func);
2947+
return func;
2948+
}
2949+
28062950
/*[clinic input]
28072951
_tkinter.tkapp.willdispatch
28082952
@@ -3038,6 +3182,8 @@ static PyMethodDef Tkapp_methods[] =
30383182
{
30393183
_TKINTER_TKAPP_WILLDISPATCH_METHODDEF
30403184
{"wantobjects", Tkapp_WantObjects, METH_VARARGS},
3185+
_TKINTER_TKAPP_SETTRACE_METHODDEF
3186+
_TKINTER_TKAPP_GETTRACE_METHODDEF
30413187
{"call", Tkapp_Call, METH_VARARGS},
30423188
_TKINTER_TKAPP_EVAL_METHODDEF
30433189
_TKINTER_TKAPP_EVALFILE_METHODDEF

0 commit comments

Comments
 (0)