bpo-42328: Fix tkinter.ttk.Style.map(). (GH-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.
(cherry picked from commit dd844a2916
)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
5aa6c99da1
commit
3e53301308
|
@ -137,6 +137,9 @@ class InternalFunctionsTest(unittest.TestCase):
|
||||||
result = ttk._format_mapdict(opts)
|
result = ttk._format_mapdict(opts)
|
||||||
self.assertEqual(result, ('-üñíćódè', 'á vãl'))
|
self.assertEqual(result, ('-üñíćódè', 'á vãl'))
|
||||||
|
|
||||||
|
self.assertEqual(ttk._format_mapdict({'opt': [('value',)]}),
|
||||||
|
('-opt', '{} value'))
|
||||||
|
|
||||||
# empty states
|
# empty states
|
||||||
valid = {'opt': [('', '', 'hi')]}
|
valid = {'opt': [('', '', 'hi')]}
|
||||||
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi'))
|
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi'))
|
||||||
|
@ -159,10 +162,6 @@ class InternalFunctionsTest(unittest.TestCase):
|
||||||
opts = {'a': None}
|
opts = {'a': None}
|
||||||
self.assertRaises(TypeError, ttk._format_mapdict, opts)
|
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):
|
def test_format_elemcreate(self):
|
||||||
self.assertTrue(ttk._format_elemcreate(None), (None, ()))
|
self.assertTrue(ttk._format_elemcreate(None), (None, ()))
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
import unittest
|
import unittest
|
||||||
import tkinter
|
import tkinter
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
from test import support
|
||||||
from test.support import requires, run_unittest
|
from test.support import requires, run_unittest
|
||||||
from tkinter.test.support import AbstractTkTest
|
from tkinter.test.support import AbstractTkTest
|
||||||
|
|
||||||
requires('gui')
|
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):
|
class StyleTest(AbstractTkTest, unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -23,11 +34,36 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
|
||||||
|
|
||||||
def test_map(self):
|
def test_map(self):
|
||||||
style = self.style
|
style = self.style
|
||||||
style.map('TButton', background=[('active', 'background', 'blue')])
|
|
||||||
self.assertEqual(style.map('TButton', 'background'),
|
# Single state
|
||||||
[('active', 'background', 'blue')] if self.wantobjects else
|
for states in ['active'], [('active',)]:
|
||||||
[('active background', 'blue')])
|
with self.subTest(states=states):
|
||||||
self.assertIsInstance(style.map('TButton'), dict)
|
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):
|
def test_lookup(self):
|
||||||
|
@ -86,6 +122,50 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
|
||||||
self.style.theme_use(curr_theme)
|
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, )
|
tests_gui = (StyleTest, )
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -81,8 +81,6 @@ def _mapdict_values(items):
|
||||||
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
|
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
|
||||||
opt_val = []
|
opt_val = []
|
||||||
for *state, val in items:
|
for *state, val in items:
|
||||||
# hacks for backward compatibility
|
|
||||||
state[0] # raise IndexError if empty
|
|
||||||
if len(state) == 1:
|
if len(state) == 1:
|
||||||
# if it is empty (something that evaluates to False), then
|
# if it is empty (something that evaluates to False), then
|
||||||
# format it to Tcl code to denote the "normal" state
|
# format it to Tcl code to denote the "normal" state
|
||||||
|
@ -243,19 +241,22 @@ def _script_from_settings(settings):
|
||||||
def _list_from_statespec(stuple):
|
def _list_from_statespec(stuple):
|
||||||
"""Construct a list from the given statespec tuple according to the
|
"""Construct a list from the given statespec tuple according to the
|
||||||
accepted statespec accepted by _format_mapdict."""
|
accepted statespec accepted by _format_mapdict."""
|
||||||
nval = []
|
if isinstance(stuple, str):
|
||||||
for val in stuple:
|
return stuple
|
||||||
typename = getattr(val, 'typename', None)
|
result = []
|
||||||
if typename is None:
|
it = iter(stuple)
|
||||||
nval.append(val)
|
for state, val in zip(it, it):
|
||||||
else: # this is a Tcl object
|
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)
|
val = str(val)
|
||||||
if typename == 'StateSpec':
|
result.append((*state, val))
|
||||||
val = val.split()
|
|
||||||
nval.append(val)
|
|
||||||
|
|
||||||
it = iter(nval)
|
return result
|
||||||
return [_flatten(spec) for spec in zip(it, it)]
|
|
||||||
|
|
||||||
def _list_from_layouttuple(tk, ltuple):
|
def _list_from_layouttuple(tk, ltuple):
|
||||||
"""Construct a list from the tuple returned by ttk::layout, this is
|
"""Construct a list from the tuple returned by ttk::layout, this is
|
||||||
|
@ -395,13 +396,12 @@ class Style(object):
|
||||||
or something else of your preference. A statespec is compound of
|
or something else of your preference. A statespec is compound of
|
||||||
one or more states and then a value."""
|
one or more states and then a value."""
|
||||||
if query_opt is not None:
|
if query_opt is not None:
|
||||||
return _list_from_statespec(self.tk.splitlist(
|
result = self.tk.call(self._name, "map", style, '-%s' % query_opt)
|
||||||
self.tk.call(self._name, "map", style, '-%s' % query_opt)))
|
return _list_from_statespec(self.tk.splitlist(result))
|
||||||
|
|
||||||
return _splitdict(
|
result = self.tk.call(self._name, "map", style, *_format_mapdict(kw))
|
||||||
self.tk,
|
return {k: _list_from_statespec(self.tk.splitlist(v))
|
||||||
self.tk.call(self._name, "map", style, *_format_mapdict(kw)),
|
for k, v in _splitdict(self.tk, result).items()}
|
||||||
conv=_tclobj_to_py)
|
|
||||||
|
|
||||||
|
|
||||||
def lookup(self, style, option, state=None, default=None):
|
def lookup(self, style, option, state=None, default=None):
|
||||||
|
|
|
@ -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``.
|
Loading…
Reference in New Issue