diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 5ced35aadbe6d3..90bb5db7eb38d8 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -790,6 +790,15 @@ distance millimetres, ``p`` for printer's points. For example, 3.5 inches is expressed as ``"3.5i"``. + When a screen distance option is read back (for example with :meth:`!cget`), + a distance with no unit suffix is returned as an :class:`int` or a :class:`float`. + A distance with a unit suffix depends on the screen resolution + and is returned as an opaque Tcl object that can be passed back to Tk. + + .. versionchanged:: next + Screen distances with no unit suffix are returned as an :class:`int` + or a :class:`float`. + font Tk uses a font description such as ``{courier 10 bold}``; in :mod:`!tkinter` this is most naturally passed as a tuple of diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c3653523647170..7a76b3ab109ee7 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -445,6 +445,13 @@ tkinter :meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`. (Contributed by Serhiy Storchaka in :gh:`143990`.) +* Values of several Tcl object types returned by :mod:`tkinter` are now + converted to the corresponding Python type instead of being wrapped in a + :class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and + ``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances + with no unit suffix to :class:`int` or :class:`float`. + (Contributed by Serhiy Storchaka in :gh:`153513`.) + xml --- diff --git a/Lib/test/test_tcl.py b/Lib/test/test_tcl.py index 70731d3222ced9..26be4ee0d7afe4 100644 --- a/Lib/test/test_tcl.py +++ b/Lib/test/test_tcl.py @@ -650,6 +650,22 @@ def testfunc(arg): else: self.assertEqual(a, expected) + def test_return_dict_object(self): + # A dict is returned as a Tcl_Obj to preserve its structure. + tcl = self.interp.tk + a = tcl.call('dict', 'create', 'a', 1, 'b', 2) + if self.wantobjects: + self.assertIsInstance(a, _tkinter.Tcl_Obj) + self.assertEqual(a.typename, 'dict') + self.assertEqual(str(a), 'a 1 b 2') + + def test_return_nsname_object(self): + # An "nsName" object is returned as a str, not wrapped in a Tcl_Obj. + tcl = self.interp.tk + a = tcl.call('namespace', 'current') + self.assertIsInstance(a, str) + self.assertEqual(a, '::') + def test_splitlist(self): splitlist = self.interp.tk.splitlist call = self.interp.tk.call diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 0ebaac22a2971d..92211075ce846a 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -6,7 +6,7 @@ import unittest import weakref import tkinter -from tkinter import TclError +from tkinter import TclError, ttk import enum from test import support from test.support import os_helper @@ -14,7 +14,7 @@ from test.test_tkinter.support import setUpModule # noqa: F401 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, requires_tk, get_tk_patchlevel, - tcl_version) + tcl_version, tk_version) support.requires('gui') @@ -2054,5 +2054,65 @@ def _info_commands(widget, pattern=None): return widget.tk.splitlist(widget.tk.call('info', 'commands', pattern)) +class TclObjTypeTest(AbstractTkTest, unittest.TestCase): + # See FromObj() in Modules/_tkinter.c: Tcl object types are converted to + # appropriate Python types. These conversions only happen in the object + # mode, so skip when it is disabled. + + def setUp(self): + super().setUp() + if not self.wantobjects: + self.skipTest('requires wantobjects') + + def test_enum_option_returns_str(self): + # An "index" object (an enumeration keyword) is returned as a str. + w = ttk.Scale(self.root, orient='horizontal') + value = w.cget('orient') + self.assertIsInstance(value, str) + self.assertEqual(value, 'horizontal') + + def test_enum_option_is_interned(self): + # Equal "index" keywords share a single interned str object. + a = ttk.Scale(self.root, orient='horizontal').cget('orient') + b = ttk.Scale(self.root, orient='horizontal').cget('orient') + self.assertIs(a, b) + + def test_window_option_returns_str(self): + # A "window" object is returned as a str. + label = tkinter.Label(self.root) + w = ttk.LabelFrame(self.root, labelwidget=label) + value = w.cget('labelwidget') + self.assertIsInstance(value, str) + self.assertEqual(value, str(label)) + + def test_variable_option_returns_str(self): + # A "parsedVarName" object is returned as a str. + w = tkinter.Checkbutton(self.root) + self.assertIsInstance(w.cget('variable'), str) + + def test_pixel_option_without_unit_returns_number(self): + # A screen distance with no unit suffix is already in pixels and thus + # screen independent, so it is returned as an int or a float. + w = tkinter.Frame(self.root) + w['borderwidth'] = 3 + self.assertIsInstance(w.cget('borderwidth'), int) + self.assertEqual(w.cget('borderwidth'), 3) + if tk_version >= (9, 0): + # Tk < 9 rounds a fractional screen distance to an integer. + w['borderwidth'] = 2.5 + self.assertIsInstance(w.cget('borderwidth'), float) + self.assertEqual(w.cget('borderwidth'), 2.5) + + @requires_tk(9, 0) + def test_pixel_option_with_unit_is_not_a_number(self): + # A screen distance with an m/c/i/p suffix depends on the screen + # resolution, so it is not converted to a number here. (Tk < 9 resolves + # it eagerly to an integer pixel count when the option is read.) + w = tkinter.Frame(self.root) + w['borderwidth'] = '3m' + self.assertNotIsInstance(w.cget('borderwidth'), (int, float)) + self.assertEqual(str(w.cget('borderwidth')), '3m') + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst b/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst new file mode 100644 index 00000000000000..a374355d2d4c59 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst @@ -0,0 +1,5 @@ +Values of several Tcl object types returned by :mod:`tkinter` are now +converted to the corresponding Python type instead of being wrapped in a +:class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and +``parsedVarName`` objects to :class:`str`, and ``pixel`` screen distances +with no unit suffix to :class:`int` or :class:`float`. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 62a79128d9726c..30185c08eeabc9 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -356,6 +356,15 @@ typedef struct { const Tcl_ObjType *StringType; const Tcl_ObjType *UTF32StringType; const Tcl_ObjType *PixelType; + const Tcl_ObjType *IndexType; + const Tcl_ObjType *ColorType; + const Tcl_ObjType *BorderType; + const Tcl_ObjType *FontType; + const Tcl_ObjType *CursorType; + const Tcl_ObjType *WindowType; + const Tcl_ObjType *NsNameType; + const Tcl_ObjType *ParsedVarNameType; + const Tcl_ObjType *DictType; } TkappObject; #define TkappObject_CAST(op) ((TkappObject *)(op)) @@ -687,11 +696,22 @@ Tkapp_New(const char *screenName, const char *className, Tcl_DecrRefCount(value); } v->WideIntType = Tcl_GetObjType("wideInt"); - v->BignumType = Tcl_GetObjType("bignum"); v->ListType = Tcl_GetObjType("list"); v->StringType = Tcl_GetObjType("string"); v->UTF32StringType = Tcl_GetObjType("utf32string"); + /* These types may not be registered until first used, so these may be NULL; + FromObj() caches them lazily on first encounter. */ + v->BignumType = Tcl_GetObjType("bignum"); v->PixelType = Tcl_GetObjType("pixel"); + v->IndexType = Tcl_GetObjType("index"); + v->ColorType = Tcl_GetObjType("color"); + v->BorderType = Tcl_GetObjType("border"); + v->FontType = Tcl_GetObjType("font"); + v->CursorType = Tcl_GetObjType("cursor"); + v->WindowType = Tcl_GetObjType("window"); + v->NsNameType = Tcl_GetObjType("nsName"); + v->ParsedVarNameType = Tcl_GetObjType("parsedVarName"); + v->DictType = Tcl_GetObjType("dict"); /* Delete the 'exit' command, which can screw things up */ Tcl_DeleteCommand(v->interp, "exit"); @@ -1257,6 +1277,39 @@ fromBignumObj(TkappObject *tkapp, Tcl_Obj *value) return res; } +/* Convert a "pixel" screen distance to a Python object. A value with no unit + suffix is screen independent and is returned as an int or float. A value with + an m/c/i/p suffix needs a Tk_Window to be resolved to pixels, which is not + available here, so it is kept as a Tcl_Obj. Tcl_Get*FromObj() only set the + object's type on success, so a unit-bearing value does not shimmer. */ +static PyObject* +fromPixelObj(TkappObject *tkapp, Tcl_Obj *value) +{ + PyObject *result = fromWideIntObj(tkapp, value); + if (result != NULL || PyErr_Occurred()) { + return result; + } + Tcl_ResetResult(Tkapp_Interp(tkapp)); + double doubleValue; + if (Tcl_GetDoubleFromObj(NULL, value, &doubleValue) == TCL_OK) { + return PyFloat_FromDouble(doubleValue); + } + return newPyTclObject(value); +} + +/* Convert an "index" object to a Python str. Index values are enumeration + keywords from a small fixed set (e.g. "horizontal", "disabled"), so the result + is interned to share one str object per keyword. */ +static PyObject* +fromIndexObj(TkappObject *tkapp, Tcl_Obj *value) +{ + PyObject *result = unicodeFromTclObj(tkapp, value); + if (result != NULL) { + PyUnicode_InternInPlace(&result); + } + return result; +} + static PyObject* FromObj(TkappObject *tkapp, Tcl_Obj *value) { @@ -1327,19 +1380,96 @@ FromObj(TkappObject *tkapp, Tcl_Obj *value) } if (value->typePtr == tkapp->StringType || - value->typePtr == tkapp->UTF32StringType || - value->typePtr == tkapp->PixelType) + value->typePtr == tkapp->UTF32StringType) { return unicodeFromTclObj(tkapp, value); } - if (tkapp->BignumType == NULL && - strcmp(value->typePtr->name, "bignum") == 0) { - /* bignum type is not registered in Tcl */ + if (value->typePtr == tkapp->PixelType) { + return fromPixelObj(tkapp, value); + } + + if (value->typePtr == tkapp->IndexType) { + return fromIndexObj(tkapp, value); + } + + /* A dict is kept as a Tcl_Obj to preserve its structure for _splitdict(). */ + if (value->typePtr == tkapp->DictType) { + return newPyTclObject(value); + } + + /* Resource types are kept as a Tcl_Obj so that Tk can reuse the parsed + X/font resource when the object is passed back to it. */ + if (value->typePtr == tkapp->ColorType || + value->typePtr == tkapp->BorderType || + value->typePtr == tkapp->FontType || + value->typePtr == tkapp->CursorType) { + return newPyTclObject(value); + } + + /* These types are context-bound lookups (a cached window or namespace + resolution, or a parsed variable name) with nothing worth preserving in + a Tcl_Obj, so they are returned as str. */ + if (value->typePtr == tkapp->WindowType || + value->typePtr == tkapp->NsNameType || + value->typePtr == tkapp->ParsedVarNameType) { + return unicodeFromTclObj(tkapp, value); + } + + /* The types below may not be registered until first used, so they are + matched by name and cached on first encounter; the pointer checks above + then catch them. */ + const char *name = value->typePtr->name; + + if (tkapp->BignumType == NULL && strcmp(name, "bignum") == 0) { tkapp->BignumType = value->typePtr; return fromBignumObj(tkapp, value); } + if (tkapp->DictType == NULL && strcmp(name, "dict") == 0) { + tkapp->DictType = value->typePtr; + return newPyTclObject(value); + } + + if (tkapp->PixelType == NULL && strcmp(name, "pixel") == 0) { + tkapp->PixelType = value->typePtr; + return fromPixelObj(tkapp, value); + } + + if (tkapp->IndexType == NULL && strcmp(name, "index") == 0) { + tkapp->IndexType = value->typePtr; + return fromIndexObj(tkapp, value); + } + + if (tkapp->WindowType == NULL && strcmp(name, "window") == 0) { + tkapp->WindowType = value->typePtr; + return unicodeFromTclObj(tkapp, value); + } + + if (tkapp->NsNameType == NULL && strcmp(name, "nsName") == 0) { + tkapp->NsNameType = value->typePtr; + return unicodeFromTclObj(tkapp, value); + } + + if (tkapp->ParsedVarNameType == NULL && strcmp(name, "parsedVarName") == 0) { + tkapp->ParsedVarNameType = value->typePtr; + return unicodeFromTclObj(tkapp, value); + } + + /* Any other type is returned as a Tcl_Obj. Cache the resource types so the + pointer check above catches them next time. */ + if (tkapp->ColorType == NULL && strcmp(name, "color") == 0) { + tkapp->ColorType = value->typePtr; + } + else if (tkapp->BorderType == NULL && strcmp(name, "border") == 0) { + tkapp->BorderType = value->typePtr; + } + else if (tkapp->FontType == NULL && strcmp(name, "font") == 0) { + tkapp->FontType = value->typePtr; + } + else if (tkapp->CursorType == NULL && strcmp(name, "cursor") == 0) { + tkapp->CursorType = value->typePtr; + } return newPyTclObject(value); }