Skip to content

Commit

Permalink
gh-66410: Do not stringify arguments of Tkinter callback (GH-98592)
Browse files Browse the repository at this point in the history
Callbacks registered in the tkinter module now take arguments as
various Python objects (int, float, bytes, tuple), not just str.
To restore the previous behavior set tkinter module global wantobject to 1
before creating the Tk object or call the wantobject() method of the Tk object
with argument 1.
Calling it with argument 2 restores the current default behavior.
  • Loading branch information
serhiy-storchaka committed May 7, 2024
1 parent b60d4c0 commit 65f5e58
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 25 deletions.
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,16 @@ Changes in the Python API
to :c:func:`PyUnstable_Code_GetFirstFree`.
(Contributed by Bogdan Romanyuk in :gh:`115781`.)

* Callbacks registered in the :mod:`tkinter` module now take arguments as
various Python objects (``int``, ``float``, ``bytes``, ``tuple``),
not just ``str``.
To restore the previous behavior set :mod:`!tkinter` module global
:data:`!wantobject` to ``1`` before creating the
:class:`!Tk` object or call the :meth:`!wantobject`
method of the :class:`!Tk` object with argument ``1``.
Calling it with argument ``2`` restores the current default behavior.
(Contributed by Serhiy Storchaka in :gh:`66410`.)


Build Changes
=============
Expand Down
1 change: 1 addition & 0 deletions Lib/idlelib/redirector.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def dispatch(self, operation, *args):
to *args to accomplish that. For an example, see colorizer.py.
'''
operation = str(operation) # can be a Tcl_Obj
m = self._operations.get(operation)
try:
if m:
Expand Down
35 changes: 23 additions & 12 deletions Lib/test/test_tcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,29 +482,36 @@ def testfunc(arg):
return arg
self.interp.createcommand('testfunc', testfunc)
self.addCleanup(self.interp.tk.deletecommand, 'testfunc')
def check(value, expected=None, *, eq=self.assertEqual):
if expected is None:
expected = value
def check(value, expected1=None, expected2=None, *, eq=self.assertEqual):
expected = value
if self.wantobjects >= 2:
if expected2 is not None:
expected = expected2
expected_type = type(expected)
else:
if expected1 is not None:
expected = expected1
expected_type = str
nonlocal result
result = None
r = self.interp.call('testfunc', value)
self.assertIsInstance(result, str)
self.assertIsInstance(result, expected_type)
eq(result, expected)
self.assertIsInstance(r, str)
self.assertIsInstance(r, expected_type)
eq(r, expected)
def float_eq(actual, expected):
self.assertAlmostEqual(float(actual), expected,
delta=abs(expected) * 1e-10)

check(True, '1')
check(False, '0')
check(True, '1', 1)
check(False, '0', 0)
check('string')
check('string\xbd')
check('string\u20ac')
check('string\U0001f4bb')
if sys.platform != 'win32':
check('<\udce2\udc82\udcac>', '<\u20ac>')
check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>')
check('<\udce2\udc82\udcac>', '<\u20ac>', '<\u20ac>')
check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>', '<\U0001f4bb>')
check('')
check(b'string', 'string')
check(b'string\xe2\x82\xac', 'string\xe2\x82\xac')
Expand All @@ -526,9 +533,13 @@ def float_eq(actual, expected):
check(float('inf'), eq=float_eq)
check(-float('inf'), eq=float_eq)
# XXX NaN representation can be not parsable by float()
check((), '')
check((1, (2,), (3, 4), '5 6', ()), '1 2 {3 4} {5 6} {}')
check([1, [2,], [3, 4], '5 6', []], '1 2 {3 4} {5 6} {}')
check((), '', '')
check((1, (2,), (3, 4), '5 6', ()),
'1 2 {3 4} {5 6} {}',
(1, (2,), (3, 4), '5 6', ''))
check([1, [2,], [3, 4], '5 6', []],
'1 2 {3 4} {5 6} {}',
(1, (2,), (3, 4), '5 6', ''))

def test_splitlist(self):
splitlist = self.interp.tk.splitlist
Expand Down
7 changes: 5 additions & 2 deletions Lib/tkinter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from tkinter.constants import *
import re

wantobjects = 1
wantobjects = 2
_debug = False # set to True to print executed Tcl/Tk commands

TkVersion = float(_tkinter.TK_VERSION)
Expand Down Expand Up @@ -1762,7 +1762,10 @@ def getint_event(s):
try:
e.type = EventType(T)
except ValueError:
e.type = T
try:
e.type = EventType(str(T)) # can be int
except ValueError:
e.type = T
try:
e.widget = self._nametowidget(W)
except KeyError:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Callbacks registered in the :mod:`tkinter` module now take arguments as
various Python objects (``int``, ``float``, ``bytes``, ``tuple``), not just
``str``. To restore the previous behavior set :mod:`!tkinter` module global
:data:`~tkinter.wantobject` to ``1`` before creating the
:class:`~tkinter.Tk` object or call the :meth:`~tkinter.Tk.wantobject`
method of the :class:`!Tk` object with argument ``1``. Calling it with
argument ``2`` restores the current default behavior.
17 changes: 10 additions & 7 deletions Modules/_tkinter.c
Original file line number Diff line number Diff line change
Expand Up @@ -2248,7 +2248,7 @@ _tkinter_tkapp_splitlist(TkappObject *self, PyObject *arg)

/* Client data struct */
typedef struct {
PyObject *self;
TkappObject *self;
PyObject *func;
} PythonCmd_ClientData;

Expand All @@ -2272,6 +2272,7 @@ PythonCmd(ClientData clientData, Tcl_Interp *interp,
PyObject *args, *res;
int i;
Tcl_Obj *obj_res;
int objargs = data->self->wantobjects >= 2;

ENTER_PYTHON

Expand All @@ -2280,7 +2281,8 @@ PythonCmd(ClientData clientData, Tcl_Interp *interp,
return PythonCmd_Error(interp);

for (i = 0; i < (objc - 1); i++) {
PyObject *s = unicodeFromTclObj(objv[i + 1]);
PyObject *s = objargs ? FromObj(data->self, objv[i + 1])
: unicodeFromTclObj(objv[i + 1]);
if (!s) {
Py_DECREF(args);
return PythonCmd_Error(interp);
Expand Down Expand Up @@ -2383,7 +2385,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
data = PyMem_NEW(PythonCmd_ClientData, 1);
if (!data)
return PyErr_NoMemory();
data->self = Py_NewRef(self);
Py_INCREF(self);
data->self = self;
data->func = Py_NewRef(func);
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
Tcl_Condition cond = NULL;
Expand Down Expand Up @@ -2897,10 +2900,10 @@ Tkapp_WantObjects(PyObject *self, PyObject *args)
{

int wantobjects = -1;
if (!PyArg_ParseTuple(args, "|p:wantobjects", &wantobjects))
if (!PyArg_ParseTuple(args, "|i:wantobjects", &wantobjects))
return NULL;
if (wantobjects == -1)
return PyBool_FromLong(((TkappObject*)self)->wantobjects);
return PyLong_FromLong(((TkappObject*)self)->wantobjects);
((TkappObject*)self)->wantobjects = wantobjects;

Py_RETURN_NONE;
Expand Down Expand Up @@ -3086,7 +3089,7 @@ _tkinter.create
baseName: str = ""
className: str = "Tk"
interactive: bool = False
wantobjects: bool = False
wantobjects: int = 0
wantTk: bool = True
if false, then Tk_Init() doesn't get called
sync: bool = False
Expand All @@ -3102,7 +3105,7 @@ _tkinter_create_impl(PyObject *module, const char *screenName,
const char *baseName, const char *className,
int interactive, int wantobjects, int wantTk, int sync,
const char *use)
/*[clinic end generated code: output=e3315607648e6bb4 input=09afef9adea70a19]*/
/*[clinic end generated code: output=e3315607648e6bb4 input=7e382ba431bed537]*/
{
/* XXX baseName is not used anymore;
* try getting rid of it. */
Expand Down
8 changes: 4 additions & 4 deletions Modules/clinic/_tkinter.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 65f5e58

Please sign in to comment.