Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/tkinter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_tcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 62 additions & 2 deletions Lib/test/test_tkinter/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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
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')

Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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`.
142 changes: 136 additions & 6 deletions Modules/_tkinter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}

Expand Down
Loading