[Python-checkins] bpo-42328: Fix tkinter.ttk.Style.map(). (GH-23300)

serhiy-storchaka webhook-mailer at python.org
Sun Nov 22 15:49:00 EST 2020


https://github.com/python/cpython/commit/dd844a2916fb3a8f481ec7c732802c13c3375691
commit: dd844a2916fb3a8f481ec7c732802c13c3375691
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: serhiy-storchaka <storchaka at gmail.com>
date: 2020-11-22T22:48:52+02:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2020-11-15-17-02-00.bpo-42328.bqpPlR.rst
M Lib/tkinter/test/test_ttk/test_functions.py
M Lib/tkinter/test/test_ttk/test_style.py
M Lib/tkinter/ttk.py

diff --git a/Lib/tkinter/test/test_ttk/test_functions.py b/Lib/tkinter/test/test_ttk/test_functions.py
index f8e69a9f4165d..5c23d6fecf8b4 100644
--- a/Lib/tkinter/test/test_ttk/test_functions.py
+++ b/Lib/tkinter/test/test_ttk/test_functions.py
@@ -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'))
@@ -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, ()))
diff --git a/Lib/tkinter/test/test_ttk/test_style.py b/Lib/tkinter/test/test_ttk/test_style.py
index 3537536d81bc5..54e913311766f 100644
--- a/Lib/tkinter/test/test_ttk/test_style.py
+++ b/Lib/tkinter/test/test_ttk/test_style.py
@@ -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):
@@ -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):
@@ -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__":
diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py
index c7c71cd5a559c..968fd54dce1ee 100644
--- a/Lib/tkinter/ttk.py
+++ b/Lib/tkinter/ttk.py
@@ -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
@@ -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
@@ -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):
diff --git a/Misc/NEWS.d/next/Library/2020-11-15-17-02-00.bpo-42328.bqpPlR.rst b/Misc/NEWS.d/next/Library/2020-11-15-17-02-00.bpo-42328.bqpPlR.rst
new file mode 100644
index 0000000000000..7e6a176c88941
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2020-11-15-17-02-00.bpo-42328.bqpPlR.rst
@@ -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``.



More information about the Python-checkins mailing list