gh-66410: Do not stringify arguments of Tkinter callback (GH-98592)

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.
This commit is contained in:
Serhiy Storchaka 2024-05-07 15:07:32 +03:00 committed by GitHub
parent b60d4c0d53
commit 65f5e586a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 60 additions and 25 deletions

View file

@ -1859,6 +1859,16 @@ Changes in the Python API
to :c:func:`PyUnstable_Code_GetFirstFree`. to :c:func:`PyUnstable_Code_GetFirstFree`.
(Contributed by Bogdan Romanyuk in :gh:`115781`.) (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 Build Changes
============= =============

View file

@ -106,6 +106,7 @@ class WidgetRedirector:
to *args to accomplish that. For an example, see colorizer.py. to *args to accomplish that. For an example, see colorizer.py.
''' '''
operation = str(operation) # can be a Tcl_Obj
m = self._operations.get(operation) m = self._operations.get(operation)
try: try:
if m: if m:

View file

@ -482,29 +482,36 @@ class TclTest(unittest.TestCase):
return arg return arg
self.interp.createcommand('testfunc', testfunc) self.interp.createcommand('testfunc', testfunc)
self.addCleanup(self.interp.tk.deletecommand, 'testfunc') self.addCleanup(self.interp.tk.deletecommand, 'testfunc')
def check(value, expected=None, *, eq=self.assertEqual): def check(value, expected1=None, expected2=None, *, eq=self.assertEqual):
if expected is None: expected = value
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 nonlocal result
result = None result = None
r = self.interp.call('testfunc', value) r = self.interp.call('testfunc', value)
self.assertIsInstance(result, str) self.assertIsInstance(result, expected_type)
eq(result, expected) eq(result, expected)
self.assertIsInstance(r, str) self.assertIsInstance(r, expected_type)
eq(r, expected) eq(r, expected)
def float_eq(actual, expected): def float_eq(actual, expected):
self.assertAlmostEqual(float(actual), expected, self.assertAlmostEqual(float(actual), expected,
delta=abs(expected) * 1e-10) delta=abs(expected) * 1e-10)
check(True, '1') check(True, '1', 1)
check(False, '0') check(False, '0', 0)
check('string') check('string')
check('string\xbd') check('string\xbd')
check('string\u20ac') check('string\u20ac')
check('string\U0001f4bb') check('string\U0001f4bb')
if sys.platform != 'win32': if sys.platform != 'win32':
check('<\udce2\udc82\udcac>', '<\u20ac>') check('<\udce2\udc82\udcac>', '<\u20ac>', '<\u20ac>')
check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>') check('<\udced\udca0\udcbd\udced\udcb2\udcbb>', '<\U0001f4bb>', '<\U0001f4bb>')
check('') check('')
check(b'string', 'string') check(b'string', 'string')
check(b'string\xe2\x82\xac', 'string\xe2\x82\xac') check(b'string\xe2\x82\xac', 'string\xe2\x82\xac')
@ -526,9 +533,13 @@ class TclTest(unittest.TestCase):
check(float('inf'), eq=float_eq) check(float('inf'), eq=float_eq)
check(-float('inf'), eq=float_eq) check(-float('inf'), eq=float_eq)
# XXX NaN representation can be not parsable by float() # XXX NaN representation can be not parsable by float()
check((), '') check((), '', '')
check((1, (2,), (3, 4), '5 6', ()), '1 2 {3 4} {5 6} {}') check((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} {}',
(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): def test_splitlist(self):
splitlist = self.interp.tk.splitlist splitlist = self.interp.tk.splitlist

View file

@ -40,7 +40,7 @@ TclError = _tkinter.TclError
from tkinter.constants import * from tkinter.constants import *
import re import re
wantobjects = 1 wantobjects = 2
_debug = False # set to True to print executed Tcl/Tk commands _debug = False # set to True to print executed Tcl/Tk commands
TkVersion = float(_tkinter.TK_VERSION) TkVersion = float(_tkinter.TK_VERSION)
@ -1762,7 +1762,10 @@ class Misc:
try: try:
e.type = EventType(T) e.type = EventType(T)
except ValueError: except ValueError:
e.type = T try:
e.type = EventType(str(T)) # can be int
except ValueError:
e.type = T
try: try:
e.widget = self._nametowidget(W) e.widget = self._nametowidget(W)
except KeyError: except KeyError:

View file

@ -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.

View file

@ -2248,7 +2248,7 @@ _tkinter_tkapp_splitlist(TkappObject *self, PyObject *arg)
/* Client data struct */ /* Client data struct */
typedef struct { typedef struct {
PyObject *self; TkappObject *self;
PyObject *func; PyObject *func;
} PythonCmd_ClientData; } PythonCmd_ClientData;
@ -2272,6 +2272,7 @@ PythonCmd(ClientData clientData, Tcl_Interp *interp,
PyObject *args, *res; PyObject *args, *res;
int i; int i;
Tcl_Obj *obj_res; Tcl_Obj *obj_res;
int objargs = data->self->wantobjects >= 2;
ENTER_PYTHON ENTER_PYTHON
@ -2280,7 +2281,8 @@ PythonCmd(ClientData clientData, Tcl_Interp *interp,
return PythonCmd_Error(interp); return PythonCmd_Error(interp);
for (i = 0; i < (objc - 1); i++) { 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) { if (!s) {
Py_DECREF(args); Py_DECREF(args);
return PythonCmd_Error(interp); return PythonCmd_Error(interp);
@ -2383,7 +2385,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
data = PyMem_NEW(PythonCmd_ClientData, 1); data = PyMem_NEW(PythonCmd_ClientData, 1);
if (!data) if (!data)
return PyErr_NoMemory(); return PyErr_NoMemory();
data->self = Py_NewRef(self); Py_INCREF(self);
data->self = self;
data->func = Py_NewRef(func); data->func = Py_NewRef(func);
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
Tcl_Condition cond = NULL; Tcl_Condition cond = NULL;
@ -2897,10 +2900,10 @@ Tkapp_WantObjects(PyObject *self, PyObject *args)
{ {
int wantobjects = -1; int wantobjects = -1;
if (!PyArg_ParseTuple(args, "|p:wantobjects", &wantobjects)) if (!PyArg_ParseTuple(args, "|i:wantobjects", &wantobjects))
return NULL; return NULL;
if (wantobjects == -1) if (wantobjects == -1)
return PyBool_FromLong(((TkappObject*)self)->wantobjects); return PyLong_FromLong(((TkappObject*)self)->wantobjects);
((TkappObject*)self)->wantobjects = wantobjects; ((TkappObject*)self)->wantobjects = wantobjects;
Py_RETURN_NONE; Py_RETURN_NONE;
@ -3086,7 +3089,7 @@ _tkinter.create
baseName: str = "" baseName: str = ""
className: str = "Tk" className: str = "Tk"
interactive: bool = False interactive: bool = False
wantobjects: bool = False wantobjects: int = 0
wantTk: bool = True wantTk: bool = True
if false, then Tk_Init() doesn't get called if false, then Tk_Init() doesn't get called
sync: bool = False sync: bool = False
@ -3102,7 +3105,7 @@ _tkinter_create_impl(PyObject *module, const char *screenName,
const char *baseName, const char *className, const char *baseName, const char *className,
int interactive, int wantobjects, int wantTk, int sync, int interactive, int wantobjects, int wantTk, int sync,
const char *use) 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; /* XXX baseName is not used anymore;
* try getting rid of it. */ * try getting rid of it. */

View file

@ -676,7 +676,7 @@ PyDoc_STRVAR(_tkinter__flatten__doc__,
PyDoc_STRVAR(_tkinter_create__doc__, PyDoc_STRVAR(_tkinter_create__doc__,
"create($module, screenName=None, baseName=\'\', className=\'Tk\',\n" "create($module, screenName=None, baseName=\'\', className=\'Tk\',\n"
" interactive=False, wantobjects=False, wantTk=True, sync=False,\n" " interactive=False, wantobjects=0, wantTk=True, sync=False,\n"
" use=None, /)\n" " use=None, /)\n"
"--\n" "--\n"
"\n" "\n"
@ -777,8 +777,8 @@ _tkinter_create(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
if (nargs < 5) { if (nargs < 5) {
goto skip_optional; goto skip_optional;
} }
wantobjects = PyObject_IsTrue(args[4]); wantobjects = PyLong_AsInt(args[4]);
if (wantobjects < 0) { if (wantobjects == -1 && PyErr_Occurred()) {
goto exit; goto exit;
} }
if (nargs < 6) { if (nargs < 6) {
@ -888,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=86a515890d48a2ce input=a9049054013a1b77]*/ /*[clinic end generated code: output=d90c1a9850c63249 input=a9049054013a1b77]*/