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`.
(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
=============

View file

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

View file

@ -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')
@ -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

View file

@ -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)
@ -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:

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 */
typedef struct {
PyObject *self;
TkappObject *self;
PyObject *func;
} PythonCmd_ClientData;
@ -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
@ -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);
@ -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;
@ -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;
@ -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
@ -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. */

View file

@ -676,7 +676,7 @@ PyDoc_STRVAR(_tkinter__flatten__doc__,
PyDoc_STRVAR(_tkinter_create__doc__,
"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"
"--\n"
"\n"
@ -777,8 +777,8 @@ _tkinter_create(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
if (nargs < 5) {
goto skip_optional;
}
wantobjects = PyObject_IsTrue(args[4]);
if (wantobjects < 0) {
wantobjects = PyLong_AsInt(args[4]);
if (wantobjects == -1 && PyErr_Occurred()) {
goto exit;
}
if (nargs < 6) {
@ -888,4 +888,4 @@ exit:
#ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF
#define _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]*/