Skip to content

[3.9] bpo-42328: Fix tkinter.ttk.Style.map(). (GH-23300) #23470

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 23, 2020
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
7 changes: 3 additions & 4 deletions Lib/tkinter/test/test_ttk/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def test_format_mapdict(self):
result = ttk._format_mapdict(opts)
self.assertEqual(result, ('-üñíćódè', 'á vãl'))

self.assertEqual(ttk._format_mapdict({'opt': [('value',)]}),
('-opt', '{} value'))

# empty states
valid = {'opt': [('', '', 'hi')]}
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi'))
Expand All @@ -159,10 +162,6 @@ def test_format_mapdict(self):
opts = {'a': None}
self.assertRaises(TypeError, ttk._format_mapdict, opts)

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


def test_format_elemcreate(self):
self.assertTrue(ttk._format_elemcreate(None), (None, ()))
Expand Down
90 changes: 85 additions & 5 deletions Lib/tkinter/test/test_ttk/test_style.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import unittest
import tkinter
from tkinter import ttk
from test import support
from test.support import requires, run_unittest
from tkinter.test.support import AbstractTkTest

requires('gui')

CLASS_NAMES = [
'.', 'ComboboxPopdownFrame', 'Heading',
'Horizontal.TProgressbar', 'Horizontal.TScale', 'Item', 'Sash',
'TButton', 'TCheckbutton', 'TCombobox', 'TEntry',
'TLabelframe', 'TLabelframe.Label', 'TMenubutton',
'TNotebook', 'TNotebook.Tab', 'Toolbutton', 'TProgressbar',
'TRadiobutton', 'Treeview', 'TScale', 'TScrollbar', 'TSpinbox',
'Vertical.TProgressbar', 'Vertical.TScale'
]

class StyleTest(AbstractTkTest, unittest.TestCase):

def setUp(self):
Expand All @@ -23,11 +34,36 @@ def test_configure(self):

def test_map(self):
style = self.style
style.map('TButton', background=[('active', 'background', 'blue')])
self.assertEqual(style.map('TButton', 'background'),
[('active', 'background', 'blue')] if self.wantobjects else
[('active background', 'blue')])
self.assertIsInstance(style.map('TButton'), dict)

# Single state
for states in ['active'], [('active',)]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'white')])
expected = [('active', 'white')]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)

# Multiple states
for states in ['pressed', '!disabled'], ['pressed !disabled'], [('pressed', '!disabled')]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'black')])
expected = [('pressed', '!disabled', 'black')]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)

# Default state
for states in [], [''], [()]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'grey')])
expected = [('grey',)]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)


def test_lookup(self):
Expand Down Expand Up @@ -86,6 +122,50 @@ def test_theme_use(self):
self.style.theme_use(curr_theme)


def test_configure_custom_copy(self):
style = self.style

curr_theme = self.style.theme_use()
self.addCleanup(self.style.theme_use, curr_theme)
for theme in self.style.theme_names():
self.style.theme_use(theme)
for name in CLASS_NAMES:
default = style.configure(name)
if not default:
continue
with self.subTest(theme=theme, name=name):
if support.verbose >= 2:
print('configure', theme, name, default)
newname = f'C.{name}'
self.assertEqual(style.configure(newname), None)
style.configure(newname, **default)
self.assertEqual(style.configure(newname), default)
for key, value in default.items():
self.assertEqual(style.configure(newname, key), value)


def test_map_custom_copy(self):
style = self.style

curr_theme = self.style.theme_use()
self.addCleanup(self.style.theme_use, curr_theme)
for theme in self.style.theme_names():
self.style.theme_use(theme)
for name in CLASS_NAMES:
default = style.map(name)
if not default:
continue
with self.subTest(theme=theme, name=name):
if support.verbose >= 2:
print('map', theme, name, default)
newname = f'C.{name}'
self.assertEqual(style.map(newname), {})
style.map(newname, **default)
self.assertEqual(style.map(newname), default)
for key, value in default.items():
self.assertEqual(style.map(newname, key), value)


tests_gui = (StyleTest, )

if __name__ == "__main__":
Expand Down
38 changes: 19 additions & 19 deletions Lib/tkinter/ttk.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ def _mapdict_values(items):
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
opt_val = []
for *state, val in items:
# hacks for backward compatibility
state[0] # raise IndexError if empty
if len(state) == 1:
# if it is empty (something that evaluates to False), then
# format it to Tcl code to denote the "normal" state
Expand Down Expand Up @@ -243,19 +241,22 @@ def _script_from_settings(settings):
def _list_from_statespec(stuple):
"""Construct a list from the given statespec tuple according to the
accepted statespec accepted by _format_mapdict."""
nval = []
for val in stuple:
typename = getattr(val, 'typename', None)
if typename is None:
nval.append(val)
else: # this is a Tcl object
if isinstance(stuple, str):
return stuple
result = []
it = iter(stuple)
for state, val in zip(it, it):
if hasattr(state, 'typename'): # this is a Tcl object
state = str(state).split()
elif isinstance(state, str):
state = state.split()
elif not isinstance(state, (tuple, list)):
state = (state,)
if hasattr(val, 'typename'):
val = str(val)
if typename == 'StateSpec':
val = val.split()
nval.append(val)
result.append((*state, val))

it = iter(nval)
return [_flatten(spec) for spec in zip(it, it)]
return result

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

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


def lookup(self, style, option, state=None, default=None):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fixed :meth:`tkinter.ttk.Style.map`. 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``.