]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-153513: Return str and numbers for more Tcl object types in _tkinter (GH-153514)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 10 Jul 2026 14:29:53 +0000 (17:29 +0300)
committerGitHub <noreply@github.com>
Fri, 10 Jul 2026 14:29:53 +0000 (17:29 +0300)
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>
Doc/library/tkinter.rst
Doc/whatsnew/3.16.rst
Lib/test/test_tcl.py
Lib/test/test_tkinter/test_misc.py
Misc/NEWS.d/next/Library/2026-07-10-12-33-30.gh-issue-153513.GxrPuY.rst [new file with mode: 0644]
Modules/_tkinter.c

index 5ced35aadbe6d3097e9b7a56c11a8e6fd1cdf1c1..90bb5db7eb38d8330576aff014c2d7ea78344c34 100644 (file)
@@ -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
index 5e8abc7034bea00c0bd826ebbc2b9f6601f20cae..9e1070e8efdaa57cf60c8c90835d3b293a949e2a 100644 (file)
@@ -454,6 +454,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
 ---
 
index 70731d3222ced94126b386669980998fd9d32641..26be4ee0d7afe445b9ee30fc0ef5b11a8f1d7cb5 100644 (file)
@@ -650,6 +650,22 @@ class TclTest(unittest.TestCase):
         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
index 0ebaac22a2971dd855ec7bddeb7fcfe38fe997e6..92211075ce846aeaca6629326b59dfbaa46cf36d 100644 (file)
@@ -6,7 +6,7 @@ import textwrap
 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.support.script_helper import assert_python_ok
 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 (file)
index 0000000..a374355
--- /dev/null
@@ -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`.
index 62a79128d9726caff46f7fe69fe3ce7bcfe03373..30185c08eeabc9b8acc3df4ec491058363acab11 100644 (file)
@@ -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);
 }