Skip to content

Commit dd844a2

Browse files
bpo-42328: Fix tkinter.ttk.Style.map(). (pythonGH-23300)
The function accepts now the representation of the default state as empty sequence (as returned by Style.map()). The structure of the result is now the same on all platform and does not depend on the value of wantobjects.
1 parent 313467e commit dd844a2

File tree

4 files changed

+111
-28
lines changed

4 files changed

+111
-28
lines changed

Lib/tkinter/test/test_ttk/test_functions.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def test_format_mapdict(self):
137137
result = ttk._format_mapdict(opts)
138138
self.assertEqual(result, ('-üñíćódè', 'á vãl'))
139139

140+
self.assertEqual(ttk._format_mapdict({'opt': [('value',)]}),
141+
('-opt', '{} value'))
142+
140143
# empty states
141144
valid = {'opt': [('', '', 'hi')]}
142145
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi'))
@@ -159,10 +162,6 @@ def test_format_mapdict(self):
159162
opts = {'a': None}
160163
self.assertRaises(TypeError, ttk._format_mapdict, opts)
161164

162-
# items in the value must have size >= 2
163-
self.assertRaises(IndexError, ttk._format_mapdict,
164-
{'a': [('invalid', )]})
165-
166165

167166
def test_format_elemcreate(self):
168167
self.assertTrue(ttk._format_elemcreate(None), (None, ()))

Lib/tkinter/test/test_ttk/test_style.py

+85-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
import unittest
22
import tkinter
33
from tkinter import ttk
4+
from test import support
45
from test.support import requires, run_unittest
56
from tkinter.test.support import AbstractTkTest
67

78
requires('gui')
89

10+
CLASS_NAMES = [
11+
'.', 'ComboboxPopdownFrame', 'Heading',
12+
'Horizontal.TProgressbar', 'Horizontal.TScale', 'Item', 'Sash',
13+
'TButton', 'TCheckbutton', 'TCombobox', 'TEntry',
14+
'TLabelframe', 'TLabelframe.Label', 'TMenubutton',
15+
'TNotebook', 'TNotebook.Tab', 'Toolbutton', 'TProgressbar',
16+
'TRadiobutton', 'Treeview', 'TScale', 'TScrollbar', 'TSpinbox',
17+
'Vertical.TProgressbar', 'Vertical.TScale'
18+
]
19+
920
class StyleTest(AbstractTkTest, unittest.TestCase):
1021

1122
def setUp(self):
@@ -23,11 +34,36 @@ def test_configure(self):
2334

2435
def test_map(self):
2536
style = self.style
26-
style.map('TButton', background=[('active', 'background', 'blue')])
27-
self.assertEqual(style.map('TButton', 'background'),
28-
[('active', 'background', 'blue')] if self.wantobjects else
29-
[('active background', 'blue')])
30-
self.assertIsInstance(style.map('TButton'), dict)
37+
38+
# Single state
39+
for states in ['active'], [('active',)]:
40+
with self.subTest(states=states):
41+
style.map('TButton', background=[(*states, 'white')])
42+
expected = [('active', 'white')]
43+
self.assertEqual(style.map('TButton', 'background'), expected)
44+
m = style.map('TButton')
45+
self.assertIsInstance(m, dict)
46+
self.assertEqual(m['background'], expected)
47+
48+
# Multiple states
49+
for states in ['pressed', '!disabled'], ['pressed !disabled'], [('pressed', '!disabled')]:
50+
with self.subTest(states=states):
51+
style.map('TButton', background=[(*states, 'black')])
52+
expected = [('pressed', '!disabled', 'black')]
53+
self.assertEqual(style.map('TButton', 'background'), expected)
54+
m = style.map('TButton')
55+
self.assertIsInstance(m, dict)
56+
self.assertEqual(m['background'], expected)
57+
58+
# Default state
59+
for states in [], [''], [()]:
60+
with self.subTest(states=states):
61+
style.map('TButton', background=[(*states, 'grey')])
62+
expected = [('grey',)]
63+
self.assertEqual(style.map('TButton', 'background'), expected)
64+
m = style.map('TButton')
65+
self.assertIsInstance(m, dict)
66+
self.assertEqual(m['background'], expected)
3167

3268

3369
def test_lookup(self):
@@ -86,6 +122,50 @@ def test_theme_use(self):
86122
self.style.theme_use(curr_theme)
87123

88124

125+
def test_configure_custom_copy(self):
126+
style = self.style
127+
128+
curr_theme = self.style.theme_use()
129+
self.addCleanup(self.style.theme_use, curr_theme)
130+
for theme in self.style.theme_names():
131+
self.style.theme_use(theme)
132+
for name in CLASS_NAMES:
133+
default = style.configure(name)
134+
if not default:
135+
continue
136+
with self.subTest(theme=theme, name=name):
137+
if support.verbose >= 2:
138+
print('configure', theme, name, default)
139+
newname = f'C.{name}'
140+
self.assertEqual(style.configure(newname), None)
141+
style.configure(newname, **default)
142+
self.assertEqual(style.configure(newname), default)
143+
for key, value in default.items():
144+
self.assertEqual(style.configure(newname, key), value)
145+
146+
147+
def test_map_custom_copy(self):
148+
style = self.style
149+
150+
curr_theme = self.style.theme_use()
151+
self.addCleanup(self.style.theme_use, curr_theme)
152+
for theme in self.style.theme_names():
153+
self.style.theme_use(theme)
154+
for name in CLASS_NAMES:
155+
default = style.map(name)
156+
if not default:
157+
continue
158+
with self.subTest(theme=theme, name=name):
159+
if support.verbose >= 2:
160+
print('map', theme, name, default)
161+
newname = f'C.{name}'
162+
self.assertEqual(style.map(newname), {})
163+
style.map(newname, **default)
164+
self.assertEqual(style.map(newname), default)
165+
for key, value in default.items():
166+
self.assertEqual(style.map(newname, key), value)
167+
168+
89169
tests_gui = (StyleTest, )
90170

91171
if __name__ == "__main__":

Lib/tkinter/ttk.py

+19-19
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ def _mapdict_values(items):
8181
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
8282
opt_val = []
8383
for *state, val in items:
84-
# hacks for backward compatibility
85-
state[0] # raise IndexError if empty
8684
if len(state) == 1:
8785
# if it is empty (something that evaluates to False), then
8886
# format it to Tcl code to denote the "normal" state
@@ -243,19 +241,22 @@ def _script_from_settings(settings):
243241
def _list_from_statespec(stuple):
244242
"""Construct a list from the given statespec tuple according to the
245243
accepted statespec accepted by _format_mapdict."""
246-
nval = []
247-
for val in stuple:
248-
typename = getattr(val, 'typename', None)
249-
if typename is None:
250-
nval.append(val)
251-
else: # this is a Tcl object
244+
if isinstance(stuple, str):
245+
return stuple
246+
result = []
247+
it = iter(stuple)
248+
for state, val in zip(it, it):
249+
if hasattr(state, 'typename'): # this is a Tcl object
250+
state = str(state).split()
251+
elif isinstance(state, str):
252+
state = state.split()
253+
elif not isinstance(state, (tuple, list)):
254+
state = (state,)
255+
if hasattr(val, 'typename'):
252256
val = str(val)
253-
if typename == 'StateSpec':
254-
val = val.split()
255-
nval.append(val)
257+
result.append((*state, val))
256258

257-
it = iter(nval)
258-
return [_flatten(spec) for spec in zip(it, it)]
259+
return result
259260

260261
def _list_from_layouttuple(tk, ltuple):
261262
"""Construct a list from the tuple returned by ttk::layout, this is
@@ -395,13 +396,12 @@ def map(self, style, query_opt=None, **kw):
395396
or something else of your preference. A statespec is compound of
396397
one or more states and then a value."""
397398
if query_opt is not None:
398-
return _list_from_statespec(self.tk.splitlist(
399-
self.tk.call(self._name, "map", style, '-%s' % query_opt)))
399+
result = self.tk.call(self._name, "map", style, '-%s' % query_opt)
400+
return _list_from_statespec(self.tk.splitlist(result))
400401

401-
return _splitdict(
402-
self.tk,
403-
self.tk.call(self._name, "map", style, *_format_mapdict(kw)),
404-
conv=_tclobj_to_py)
402+
result = self.tk.call(self._name, "map", style, *_format_mapdict(kw))
403+
return {k: _list_from_statespec(self.tk.splitlist(v))
404+
for k, v in _splitdict(self.tk, result).items()}
405405

406406

407407
def lookup(self, style, option, state=None, default=None):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed :meth:`tkinter.ttk.Style.map`. The function accepts now the
2+
representation of the default state as empty sequence (as returned by
3+
``Style.map()``). The structure of the result is now the same on all platform
4+
and does not depend on the value of ``wantobjects``.

0 commit comments

Comments
 (0)