Skip to content

Commit 3bd09b7

Browse files
gh-153513: Return str and numbers for more Tcl object types in _tkinter
FromObj() now returns str for the "index", "window", "nsName" and "parsedVarName" object types, whose internal representation is only a context-bound lookup with nothing worth preserving in a Tcl_Obj. An "index" value is one of a small fixed set of enumeration keywords, so its str is interned. A "pixel" screen distance with no unit suffix is screen independent and is now returned as an int or float. A distance with an m/c/i/p suffix needs a Tk_Window to be resolved and is kept as a Tcl_Obj, as are the "dict" and the resource types "color", "border", "font" and "cursor". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6c389c4 commit 3bd09b7

6 files changed

Lines changed: 235 additions & 8 deletions

File tree

Doc/library/tkinter.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,15 @@ distance
790790
millimetres, ``p`` for printer's points. For example, 3.5 inches is expressed
791791
as ``"3.5i"``.
792792

793+
When a screen distance option is read back (for example with :meth:`!cget`),
794+
a distance with no unit suffix is returned as an :class:`int` or a :class:`float`.
795+
A distance with a unit suffix depends on the screen resolution
796+
and is returned as an opaque Tcl object that can be passed back to Tk.
797+
798+
.. versionchanged:: next
799+
Screen distances with no unit suffix are returned as an :class:`int`
800+
or a :class:`float`.
801+
793802
font
794803
Tk uses a font description such as ``{courier 10 bold}``; in
795804
:mod:`!tkinter` this is most naturally passed as a tuple of

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ tkinter
445445
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
446446
(Contributed by Serhiy Storchaka in :gh:`143990`.)
447447

448+
* Values of several Tcl object types returned by :mod:`tkinter` are now
449+
converted to the corresponding Python type instead of being wrapped in a
450+
:class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and
451+
``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances
452+
with no unit suffix to :class:`int` or :class:`float`.
453+
(Contributed by Serhiy Storchaka in :gh:`153513`.)
454+
448455
xml
449456
---
450457

Lib/test/test_tcl.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,22 @@ def testfunc(arg):
650650
else:
651651
self.assertEqual(a, expected)
652652

653+
def test_return_dict_object(self):
654+
# A dict is returned as a Tcl_Obj to preserve its structure.
655+
tcl = self.interp.tk
656+
a = tcl.call('dict', 'create', 'a', 1, 'b', 2)
657+
if self.wantobjects:
658+
self.assertIsInstance(a, _tkinter.Tcl_Obj)
659+
self.assertEqual(a.typename, 'dict')
660+
self.assertEqual(str(a), 'a 1 b 2')
661+
662+
def test_return_nsname_object(self):
663+
# An "nsName" object is returned as a str, not wrapped in a Tcl_Obj.
664+
tcl = self.interp.tk
665+
a = tcl.call('namespace', 'current')
666+
self.assertIsInstance(a, str)
667+
self.assertEqual(a, '::')
668+
653669
def test_splitlist(self):
654670
splitlist = self.interp.tk.splitlist
655671
call = self.interp.tk.call

Lib/test/test_tkinter/test_misc.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import unittest
77
import weakref
88
import tkinter
9-
from tkinter import TclError
9+
from tkinter import TclError, ttk
1010
import enum
1111
from test import support
1212
from test.support import os_helper
1313
from test.support.script_helper import assert_python_ok
1414
from test.test_tkinter.support import setUpModule # noqa: F401
1515
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1616
requires_tk, get_tk_patchlevel,
17-
tcl_version)
17+
tcl_version, tk_version)
1818

1919
support.requires('gui')
2020

@@ -2054,5 +2054,65 @@ def _info_commands(widget, pattern=None):
20542054
return widget.tk.splitlist(widget.tk.call('info', 'commands', pattern))
20552055

20562056

2057+
class TclObjTypeTest(AbstractTkTest, unittest.TestCase):
2058+
# See FromObj() in Modules/_tkinter.c: Tcl object types are converted to
2059+
# appropriate Python types. These conversions only happen in the object
2060+
# mode, so skip when it is disabled.
2061+
2062+
def setUp(self):
2063+
super().setUp()
2064+
if not self.wantobjects:
2065+
self.skipTest('requires wantobjects')
2066+
2067+
def test_enum_option_returns_str(self):
2068+
# An "index" object (an enumeration keyword) is returned as a str.
2069+
w = ttk.Scale(self.root, orient='horizontal')
2070+
value = w.cget('orient')
2071+
self.assertIsInstance(value, str)
2072+
self.assertEqual(value, 'horizontal')
2073+
2074+
def test_enum_option_is_interned(self):
2075+
# Equal "index" keywords share a single interned str object.
2076+
a = ttk.Scale(self.root, orient='horizontal').cget('orient')
2077+
b = ttk.Scale(self.root, orient='horizontal').cget('orient')
2078+
self.assertIs(a, b)
2079+
2080+
def test_window_option_returns_str(self):
2081+
# A "window" object is returned as a str.
2082+
label = tkinter.Label(self.root)
2083+
w = ttk.LabelFrame(self.root, labelwidget=label)
2084+
value = w.cget('labelwidget')
2085+
self.assertIsInstance(value, str)
2086+
self.assertEqual(value, str(label))
2087+
2088+
def test_variable_option_returns_str(self):
2089+
# A "parsedVarName" object is returned as a str.
2090+
w = tkinter.Checkbutton(self.root)
2091+
self.assertIsInstance(w.cget('variable'), str)
2092+
2093+
def test_pixel_option_without_unit_returns_number(self):
2094+
# A screen distance with no unit suffix is already in pixels and thus
2095+
# screen independent, so it is returned as an int or a float.
2096+
w = tkinter.Frame(self.root)
2097+
w['borderwidth'] = 3
2098+
self.assertIsInstance(w.cget('borderwidth'), int)
2099+
self.assertEqual(w.cget('borderwidth'), 3)
2100+
if tk_version >= (9, 0):
2101+
# Tk < 9 rounds a fractional screen distance to an integer.
2102+
w['borderwidth'] = 2.5
2103+
self.assertIsInstance(w.cget('borderwidth'), float)
2104+
self.assertEqual(w.cget('borderwidth'), 2.5)
2105+
2106+
@requires_tk(9, 0)
2107+
def test_pixel_option_with_unit_is_not_a_number(self):
2108+
# A screen distance with an m/c/i/p suffix depends on the screen
2109+
# resolution, so it is not converted to a number here. (Tk < 9 resolves
2110+
# it eagerly to an integer pixel count when the option is read.)
2111+
w = tkinter.Frame(self.root)
2112+
w['borderwidth'] = '3m'
2113+
self.assertNotIsInstance(w.cget('borderwidth'), (int, float))
2114+
self.assertEqual(str(w.cget('borderwidth')), '3m')
2115+
2116+
20572117
if __name__ == "__main__":
20582118
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Values of several Tcl object types returned by :mod:`tkinter` are now
2+
converted to the corresponding Python type instead of being wrapped in a
3+
:class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and
4+
``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances
5+
with no unit suffix to :class:`int` or :class:`float`.

Modules/_tkinter.c

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,15 @@ typedef struct {
356356
const Tcl_ObjType *StringType;
357357
const Tcl_ObjType *UTF32StringType;
358358
const Tcl_ObjType *PixelType;
359+
const Tcl_ObjType *IndexType;
360+
const Tcl_ObjType *ColorType;
361+
const Tcl_ObjType *BorderType;
362+
const Tcl_ObjType *FontType;
363+
const Tcl_ObjType *CursorType;
364+
const Tcl_ObjType *WindowType;
365+
const Tcl_ObjType *NsNameType;
366+
const Tcl_ObjType *ParsedVarNameType;
367+
const Tcl_ObjType *DictType;
359368
} TkappObject;
360369

361370
#define TkappObject_CAST(op) ((TkappObject *)(op))
@@ -687,11 +696,22 @@ Tkapp_New(const char *screenName, const char *className,
687696
Tcl_DecrRefCount(value);
688697
}
689698
v->WideIntType = Tcl_GetObjType("wideInt");
690-
v->BignumType = Tcl_GetObjType("bignum");
691699
v->ListType = Tcl_GetObjType("list");
692700
v->StringType = Tcl_GetObjType("string");
693701
v->UTF32StringType = Tcl_GetObjType("utf32string");
702+
/* These types may not be registered until first used, so these may be NULL;
703+
FromObj() caches them lazily on first encounter. */
704+
v->BignumType = Tcl_GetObjType("bignum");
694705
v->PixelType = Tcl_GetObjType("pixel");
706+
v->IndexType = Tcl_GetObjType("index");
707+
v->ColorType = Tcl_GetObjType("color");
708+
v->BorderType = Tcl_GetObjType("border");
709+
v->FontType = Tcl_GetObjType("font");
710+
v->CursorType = Tcl_GetObjType("cursor");
711+
v->WindowType = Tcl_GetObjType("window");
712+
v->NsNameType = Tcl_GetObjType("nsName");
713+
v->ParsedVarNameType = Tcl_GetObjType("parsedVarName");
714+
v->DictType = Tcl_GetObjType("dict");
695715

696716
/* Delete the 'exit' command, which can screw things up */
697717
Tcl_DeleteCommand(v->interp, "exit");
@@ -1257,6 +1277,39 @@ fromBignumObj(TkappObject *tkapp, Tcl_Obj *value)
12571277
return res;
12581278
}
12591279

1280+
/* Convert a "pixel" screen distance to a Python object. A value with no unit
1281+
suffix is screen independent and is returned as an int or float. A value with
1282+
an m/c/i/p suffix needs a Tk_Window to be resolved to pixels, which is not
1283+
available here, so it is kept as a Tcl_Obj. Tcl_Get*FromObj() only set the
1284+
object's type on success, so a unit-bearing value does not shimmer. */
1285+
static PyObject*
1286+
fromPixelObj(TkappObject *tkapp, Tcl_Obj *value)
1287+
{
1288+
PyObject *result = fromWideIntObj(tkapp, value);
1289+
if (result != NULL || PyErr_Occurred()) {
1290+
return result;
1291+
}
1292+
Tcl_ResetResult(Tkapp_Interp(tkapp));
1293+
double doubleValue;
1294+
if (Tcl_GetDoubleFromObj(NULL, value, &doubleValue) == TCL_OK) {
1295+
return PyFloat_FromDouble(doubleValue);
1296+
}
1297+
return newPyTclObject(value);
1298+
}
1299+
1300+
/* Convert an "index" object to a Python str. Index values are enumeration
1301+
keywords from a small fixed set (e.g. "horizontal", "disabled"), so the result
1302+
is interned to share one str object per keyword. */
1303+
static PyObject*
1304+
fromIndexObj(TkappObject *tkapp, Tcl_Obj *value)
1305+
{
1306+
PyObject *result = unicodeFromTclObj(tkapp, value);
1307+
if (result != NULL) {
1308+
PyUnicode_InternInPlace(&result);
1309+
}
1310+
return result;
1311+
}
1312+
12601313
static PyObject*
12611314
FromObj(TkappObject *tkapp, Tcl_Obj *value)
12621315
{
@@ -1327,19 +1380,96 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value)
13271380
}
13281381

13291382
if (value->typePtr == tkapp->StringType ||
1330-
value->typePtr == tkapp->UTF32StringType ||
1331-
value->typePtr == tkapp->PixelType)
1383+
value->typePtr == tkapp->UTF32StringType)
13321384
{
13331385
return unicodeFromTclObj(tkapp, value);
13341386
}
13351387

1336-
if (tkapp->BignumType == NULL &&
1337-
strcmp(value->typePtr->name, "bignum") == 0) {
1338-
/* bignum type is not registered in Tcl */
1388+
if (value->typePtr == tkapp->PixelType) {
1389+
return fromPixelObj(tkapp, value);
1390+
}
1391+
1392+
if (value->typePtr == tkapp->IndexType) {
1393+
return fromIndexObj(tkapp, value);
1394+
}
1395+
1396+
/* A dict is kept as a Tcl_Obj to preserve its structure for _splitdict(). */
1397+
if (value->typePtr == tkapp->DictType) {
1398+
return newPyTclObject(value);
1399+
}
1400+
1401+
/* Resource types are kept as a Tcl_Obj so that Tk can reuse the parsed
1402+
X/font resource when the object is passed back to it. */
1403+
if (value->typePtr == tkapp->ColorType ||
1404+
value->typePtr == tkapp->BorderType ||
1405+
value->typePtr == tkapp->FontType ||
1406+
value->typePtr == tkapp->CursorType) {
1407+
return newPyTclObject(value);
1408+
}
1409+
1410+
/* These types are context-bound lookups (a cached window or namespace
1411+
resolution, or a parsed variable name) with nothing worth preserving in
1412+
a Tcl_Obj, so they are returned as str. */
1413+
if (value->typePtr == tkapp->WindowType ||
1414+
value->typePtr == tkapp->NsNameType ||
1415+
value->typePtr == tkapp->ParsedVarNameType) {
1416+
return unicodeFromTclObj(tkapp, value);
1417+
}
1418+
1419+
/* The types below may not be registered until first used, so they are
1420+
matched by name and cached on first encounter; the pointer checks above
1421+
then catch them. */
1422+
const char *name = value->typePtr->name;
1423+
1424+
if (tkapp->BignumType == NULL && strcmp(name, "bignum") == 0) {
13391425
tkapp->BignumType = value->typePtr;
13401426
return fromBignumObj(tkapp, value);
13411427
}
13421428

1429+
if (tkapp->DictType == NULL && strcmp(name, "dict") == 0) {
1430+
tkapp->DictType = value->typePtr;
1431+
return newPyTclObject(value);
1432+
}
1433+
1434+
if (tkapp->PixelType == NULL && strcmp(name, "pixel") == 0) {
1435+
tkapp->PixelType = value->typePtr;
1436+
return fromPixelObj(tkapp, value);
1437+
}
1438+
1439+
if (tkapp->IndexType == NULL && strcmp(name, "index") == 0) {
1440+
tkapp->IndexType = value->typePtr;
1441+
return fromIndexObj(tkapp, value);
1442+
}
1443+
1444+
if (tkapp->WindowType == NULL && strcmp(name, "window") == 0) {
1445+
tkapp->WindowType = value->typePtr;
1446+
return unicodeFromTclObj(tkapp, value);
1447+
}
1448+
1449+
if (tkapp->NsNameType == NULL && strcmp(name, "nsName") == 0) {
1450+
tkapp->NsNameType = value->typePtr;
1451+
return unicodeFromTclObj(tkapp, value);
1452+
}
1453+
1454+
if (tkapp->ParsedVarNameType == NULL && strcmp(name, "parsedVarName") == 0) {
1455+
tkapp->ParsedVarNameType = value->typePtr;
1456+
return unicodeFromTclObj(tkapp, value);
1457+
}
1458+
1459+
/* Any other type is returned as a Tcl_Obj. Cache the resource types so the
1460+
pointer check above catches them next time. */
1461+
if (tkapp->ColorType == NULL && strcmp(name, "color") == 0) {
1462+
tkapp->ColorType = value->typePtr;
1463+
}
1464+
else if (tkapp->BorderType == NULL && strcmp(name, "border") == 0) {
1465+
tkapp->BorderType = value->typePtr;
1466+
}
1467+
else if (tkapp->FontType == NULL && strcmp(name, "font") == 0) {
1468+
tkapp->FontType = value->typePtr;
1469+
}
1470+
else if (tkapp->CursorType == NULL && strcmp(name, "cursor") == 0) {
1471+
tkapp->CursorType = value->typePtr;
1472+
}
13431473
return newPyTclObject(value);
13441474
}
13451475

0 commit comments

Comments
 (0)