classification
Title: ttk style.map function incorrectly handles the default state for element options.
Type: behavior Stage: resolved
Components: Tkinter Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: closed Resolution: fixed
Dependencies: Superseder:
Assigned To: Nosy List: Pat Thoyts, db3l, miss-islington, patthoyts, serhiy.storchaka, terry.reedy
Priority: normal Keywords: patch

Created on 2020-11-12 00:27 by patthoyts, last changed 2020-12-15 16:07 by serhiy.storchaka. This issue is now closed.

Pull Requests
URL Status Linked Edit
PR 23241 closed Pat Thoyts, 2020-11-12 00:46
PR 23300 merged serhiy.storchaka, 2020-11-15 16:13
PR 23470 merged miss-islington, 2020-11-22 20:49
PR 23471 merged miss-islington, 2020-11-22 20:49
PR 23612 merged serhiy.storchaka, 2020-12-02 06:56
PR 23624 merged miss-islington, 2020-12-03 08:48
PR 23625 merged miss-islington, 2020-12-03 08:48
Messages (14)
msg380798 - (view) Author: Pat Thoyts (patthoyts) Date: 2020-11-12 00:27
When cloning a ttk style it is useful to copy an existing style and make changes. We can copy the configuration and layout using:

    style.layout('Custom.TEntry', **style.layout('TEntry'))
    style.configure('Custom.TEntry', **style.configure('TEntry))

However, doing this with style.map can result in an exception. An example of this occurs for any style that has a defined default state in the map eg the TNotebook.Tab in the clam theme:

    >>> style.map('TNotebook.Tab','background')
    [('selected', '#dcdad5'), ('#bab5ab',)]

However, calling Tk directly:

    >>> style.tk.call(style._name,"map","TNotebook.Tab","-background")
    (<StateSpec object: 'selected'>, '#dcdad5', <StateSpec object: ''>, '#bab5ab')

The second pair is defining the default state ('') to use color #bab5ab but this is being mangled by the code that converts this into pythons response.

The culprit is ttk._list_from_statespec which treats the statespec with the empty string as None and drops it and then returns the value in place of the state and then puts in an empty value.
msg380967 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2020-11-14 09:54
You have 2 user names, both nosy.  You logged in with the lower-case name, hence the Author listing above.  However, your CLA was registered with your uppercase (older?) login name.  (If this is not what you intended, please write ewa at ython.org and explain.) Hence your name (login name) is not followed by the CLA * marker.  This was a bit confusing until I decyphered the situaiton.
msg380992 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2020-11-14 21:48
Sorry, I did not understand how did you get an empty state? Could you please provide complete reproducible example?
msg380994 - (view) Author: Pat Thoyts (Pat Thoyts) * Date: 2020-11-14 23:32
So if you look at the clamTheme.tcl file you can see the definition of the map for the TNotebook.Tab style looks like the following:

    ttk::style map TNotebook.Tab \
      -padding [list selected {6 4 6 2}] \
      -background [list selected $colors(-frame) {} $colors(-darker)] \
      -lightcolor [list selected $colors(-lighter) {} $colors(-dark)] \
      ;

The vista theme uses these too on Windows.

So calling this from script we can see the resulting empty elements in tcl:

    % ttk::style map TNotebook.Tab
    -lightcolor {selected #eeebe7 {} #cfcdc8} -padding {selected {6 4 6 2}} -background {selected #dcdad5 {} #bab5ab}

As I put in the bug, this gets mistranslated in python with the value for that state map element getting put into the first element.

The simplest demonstration is that the following raises an exception:

    import tkinter as tk
    import tkinter.ttk as ttk
    style = ttk.Style()
    style.theme_use('clam')
    style.map('Custom.TNotebook.Tab', **style.map('TNotebook.Tab'))
msg380995 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2020-11-14 23:57
Verified
Traceback (most recent call last):
  File "F:\Python\a\tem4.py", line 5, in <module>
    style.map('Custom.TNotebook.Tab', **style.map('TNotebook.Tab'))
  File "C:\Program Files\Python310\lib\tkinter\ttk.py", line 403, in map
    self.tk.call(self._name, "map", style, *_format_mapdict(kw)),
  File "C:\Program Files\Python310\lib\tkinter\ttk.py", line 111, in _format_mapdict
    _format_optvalue(_mapdict_values(value), script)))
  File "C:\Program Files\Python310\lib\tkinter\ttk.py", line 85, in _mapdict_values
    state[0] # raise IndexError if empty
IndexError: list index out of range
>>> 

PS. Pat, please don't indent code that someone might reasonably copy and paste to run.
msg381013 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2020-11-15 12:24
Thank you for your examples Pat. Now it looks clearer to me.

Semantically this data in Tk is a sequence of pairs: states (which can be a single word or several words or empty) and default value.

>>> from tkinter import ttk
>>> style = ttk.Style()
>>> style.theme_use('clam')
>>> style.tk.eval(f'{style._name} map TCombobox -fieldbackground')
'{readonly focus} #4a6984 readonly #dcdad5'
>>> style.tk.eval(f'{style._name} map TNotebook.Tab -background')
'selected #dcdad5 {} #bab5ab'

Internally states represented in Tk as the StateSpec object. When automatically convert to Python it became the Tcl_Obj object with typename 'StateSpec'. Without postprocessing.

>>> style.tk.call(style._name, 'map', 'TCombobox', '-fieldbackground')
(<StateSpec object: 'readonly focus'>, '#4a6984', <StateSpec object: 'readonly'>, '#dcdad5')
>>> style.tk.call(style._name, 'map', 'TNotebook.Tab', '-background')
(<StateSpec object: 'selected'>, '#dcdad5', <StateSpec object: ''>, '#bab5ab')

Style.map() does postprocessing. It converts a sequence (with even items number) to a list of tuples. The last item of a tuple is the default value, and the rest are items of the StateSpec object.

>>> style.map('TCombobox', 'fieldbackground')
[('readonly', 'focus', '#4a6984'), ('readonly', '#dcdad5')]
>>> style.map('TNotebook.Tab', 'background')
[('selected', '#dcdad5'), ('#bab5ab',)]

But when set tkinter.wantobjects = 0 before running this example the result will be different, because StateSpec objects will be automatically represented as strings (it matches the behavior of initial versions of Tkinter):

>>> style.tk.call(style._name, 'map', 'TCombobox', '-fieldbackground')
'{readonly focus} #4a6984 readonly #dcdad5'
>>> style.tk.call(style._name, 'map', 'TNotebook.Tab', '-background')
'selected #dcdad5 {} #bab5ab'
>>> style.map('TCombobox', 'fieldbackground')
[('readonly focus', '#4a6984'), ('readonly', '#dcdad5')]
>>> style.map('TNotebook.Tab', 'background')
[('selected', '#dcdad5'), ('', '#bab5ab')]

The main problem is in representing an empty StateSpec. As every string in Python contains an empty string, {} can represent an empty sequence and a sequence containing single empty string. In Python, it can be no items before default value (like in ('#bab5ab',)) or a single item containing an empty string (like in ('', '#bab5ab')).

The former representation (an empty sequence) is default for the style.map() output, but it is rejected as the style.map() input (see state[0] in _mapdict_values). There are two ways to fix it: either change the output of style.map() (what PR 23241 does) or change the validation of the input. I think that the latter solution can be backported, but the former can be used only in the future Python version.
msg381636 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2020-11-22 20:48
New changeset dd844a2916fb3a8f481ec7c732802c13c3375691 by Serhiy Storchaka in branch 'master':
bpo-42328: Fix tkinter.ttk.Style.map(). (GH-23300)
https://github.com/python/cpython/commit/dd844a2916fb3a8f481ec7c732802c13c3375691
msg381652 - (view) Author: miss-islington (miss-islington) Date: 2020-11-23 08:51
New changeset 3e5330130810e485f1abbf192590b7109880a4da by Miss Islington (bot) in branch '3.9':
bpo-42328: Fix tkinter.ttk.Style.map(). (GH-23300)
https://github.com/python/cpython/commit/3e5330130810e485f1abbf192590b7109880a4da
msg381653 - (view) Author: miss-islington (miss-islington) Date: 2020-11-23 08:51
New changeset ad49526c80fedf7469bd65b44d8021bab5fb998b by Miss Islington (bot) in branch '3.8':
bpo-42328: Fix tkinter.ttk.Style.map(). (GH-23300)
https://github.com/python/cpython/commit/ad49526c80fedf7469bd65b44d8021bab5fb998b
msg381964 - (view) Author: David Bolen (db3l) Date: 2020-11-27 20:43
This change to the 3.8 branch appears to be consistently failing on the Windows 7 buildbot (first failing build at https://buildbot.python.org/all/#/builders/270/builds/126).

It's all failures in the new test_configure_custom_copy and test_map_custom_copy tests, such as:

FAIL: test_configure_custom_copy (tkinter.test.test_ttk.test_style.StyleTest) (theme='vista', name='.')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\cygwin\home\db3l\buildarea\3.8.bolen-windows7\build\lib\tkinter\test\test_ttk\test_style.py", line 140, in test_configure_custom_copy
    self.assertEqual(style.configure(newname), None)
AssertionError: {'foreground': 'SystemWindowText', 'selec[233 chars]bar'} != None

and

======================================================================
FAIL: test_map_custom_copy (tkinter.test.test_ttk.test_style.StyleTest) (theme='vista', name='.')
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\cygwin\home\db3l\buildarea\3.8.bolen-windows7\build\lib\tkinter\test\test_ttk\test_style.py", line 162, in test_map_custom_copy
    self.assertEqual(style.map(newname), {})
AssertionError: {'foreground': [('disabled', 'SystemGrayTe[34 chars]1')]} != {}
+ {}
- {'embossed': [('disabled', '1')],
-  'foreground': [('disabled', 'SystemGrayText')]}

Since it seems related to themes, I should mention that the buildbot is running with a Windows 7 classic (non-Aero) theme with all appearance effects disabled (the "best performance" option).  So I'm not sure if this is an issue with the code changes, or behavior due to the host environment not anticipated by the tests.
msg382298 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2020-12-02 06:58
Thank you for your report David. I have not Windows 7 and does not know how to reproduce this on Windows 10 so will just skip tests for specific themes on Windows 7.
msg382386 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2020-12-03 08:48
New changeset f3c3ea91a76526edff928c95b9c6767e077b7448 by Serhiy Storchaka in branch 'master':
bpo-42328: Skip some tests with themes vista and xpnative on Windows 7 (GH-23612)
https://github.com/python/cpython/commit/f3c3ea91a76526edff928c95b9c6767e077b7448
msg382389 - (view) Author: miss-islington (miss-islington) Date: 2020-12-03 09:07
New changeset 12d2306a1db48f71b15ceaecf3d5ce06dbbe06c1 by Miss Islington (bot) in branch '3.8':
bpo-42328: Skip some tests with themes vista and xpnative on Windows 7 (GH-23612)
https://github.com/python/cpython/commit/12d2306a1db48f71b15ceaecf3d5ce06dbbe06c1
msg382390 - (view) Author: miss-islington (miss-islington) Date: 2020-12-03 09:10
New changeset ae67db6b314e297a1b67ed15c0bb560b8ce5b856 by Miss Islington (bot) in branch '3.9':
bpo-42328: Skip some tests with themes vista and xpnative on Windows 7 (GH-23612)
https://github.com/python/cpython/commit/ae67db6b314e297a1b67ed15c0bb560b8ce5b856
History
Date User Action Args
2020-12-15 19:38:46serhiy.storchakalinkissue38661 superseder
2020-12-15 16:07:46serhiy.storchakasetstatus: open -> closed
stage: patch review -> resolved
resolution: fixed
versions: + Python 3.9, Python 3.10
2020-12-03 09:10:31miss-islingtonsetmessages: + msg382390
2020-12-03 09:07:22miss-islingtonsetmessages: + msg382389
2020-12-03 08:48:54miss-islingtonsetpull_requests: + pull_request22493
2020-12-03 08:48:46miss-islingtonsetpull_requests: + pull_request22492
2020-12-03 08:48:46serhiy.storchakasetmessages: + msg382386
2020-12-02 06:58:53serhiy.storchakasetmessages: + msg382298
2020-12-02 06:56:44serhiy.storchakasetpull_requests: + pull_request22479
2020-11-27 20:43:25db3lsetnosy: + db3l
messages: + msg381964
2020-11-23 08:51:48miss-islingtonsetmessages: + msg381653
2020-11-23 08:51:48miss-islingtonsetmessages: + msg381652
2020-11-22 20:49:16miss-islingtonsetpull_requests: + pull_request22361
2020-11-22 20:49:07miss-islingtonsetnosy: + miss-islington
pull_requests: + pull_request22360
2020-11-22 20:48:59serhiy.storchakasetmessages: + msg381636
2020-11-15 16:13:05serhiy.storchakasetpull_requests: + pull_request22191
2020-11-15 12:24:29serhiy.storchakasetmessages: + msg381013
2020-11-14 23:57:31terry.reedysetmessages: + msg380995
2020-11-14 23:32:22Pat Thoytssetmessages: + msg380994
2020-11-14 21:48:35serhiy.storchakasetmessages: + msg380992
2020-11-14 09:54:35terry.reedysetmessages: + msg380967
2020-11-14 02:19:45terry.reedysetnosy: + terry.reedy, serhiy.storchaka
2020-11-12 00:46:13Pat Thoytssetkeywords: + patch
nosy: + Pat Thoyts

pull_requests: + pull_request22138
stage: patch review
2020-11-12 00:27:54patthoytscreate