Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b139652

Browse files
Issue #15861: tkinter now correctly works with lists and tuples containing
strings with whitespaces, backslashes or unbalanced braces.
1 parent 44763dd commit b139652

5 files changed

Lines changed: 123 additions & 81 deletions

File tree

Lib/tkinter/__init__.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import _tkinter # If this fails your Python may not be configured for Tk
4040
TclError = _tkinter.TclError
4141
from tkinter.constants import *
42+
import re
4243

4344
wantobjects = 1
4445

@@ -50,6 +51,34 @@
5051
EXCEPTION = _tkinter.EXCEPTION
5152

5253

54+
_magic_re = re.compile(r'([\\{}])')
55+
_space_re = re.compile(r'([\s])', re.ASCII)
56+
57+
def _join(value):
58+
"""Internal function."""
59+
return ' '.join(map(_stringify, value))
60+
61+
def _stringify(value):
62+
"""Internal function."""
63+
if isinstance(value, (list, tuple)):
64+
if len(value) == 1:
65+
value = _stringify(value[0])
66+
if value[0] == '{':
67+
value = '{%s}' % value
68+
else:
69+
value = '{%s}' % _join(value)
70+
else:
71+
value = str(value)
72+
if not value:
73+
value = '{}'
74+
elif _magic_re.search(value):
75+
# add '\' before special characters and spaces
76+
value = _magic_re.sub(r'\\\1', value)
77+
value = _space_re.sub(r'\\\1', value)
78+
elif value[0] == '"' or _space_re.search(value):
79+
value = '{%s}' % value
80+
return value
81+
5382
def _flatten(seq):
5483
"""Internal function."""
5584
res = ()
@@ -1075,7 +1104,7 @@ def _options(self, cnf, kw = None):
10751104
if isinstance(item, int):
10761105
nv.append(str(item))
10771106
elif isinstance(item, str):
1078-
nv.append(('{%s}' if ' ' in item else '%s') % item)
1107+
nv.append(_stringify(item))
10791108
else:
10801109
break
10811110
else:

Lib/tkinter/test/test_ttk/test_functions.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ def check_against(fmt_opts, result):
4949
ttk._format_optdict({'test': {'left': 'as is'}}),
5050
{'-test': {'left': 'as is'}})
5151

52-
# check script formatting and untouched value(s)
52+
# check script formatting
5353
check_against(
5454
ttk._format_optdict(
55-
{'test': [1, -1, '', '2m', 0], 'nochange1': 3,
56-
'nochange2': 'abc def'}, script=True),
57-
{'-test': '{1 -1 {} 2m 0}', '-nochange1': 3,
58-
'-nochange2': 'abc def' })
55+
{'test': [1, -1, '', '2m', 0], 'test2': 3,
56+
'test3': '', 'test4': 'abc def',
57+
'test5': '"abc"', 'test6': '{}',
58+
'test7': '} -spam {'}, script=True),
59+
{'-test': '{1 -1 {} 2m 0}', '-test2': '3',
60+
'-test3': '{}', '-test4': '{abc def}',
61+
'-test5': '{"abc"}', '-test6': r'\{\}',
62+
'-test7': r'\}\ -spam\ \{'})
5963

6064
opts = {'αβγ': True, 'á': False}
6165
orig_opts = opts.copy()
@@ -69,6 +73,32 @@ def check_against(fmt_opts, result):
6973
ttk._format_optdict(
7074
{'option': ('one two', 'three')}),
7175
{'-option': '{one two} three'})
76+
check_against(
77+
ttk._format_optdict(
78+
{'option': ('one\ttwo', 'three')}),
79+
{'-option': '{one\ttwo} three'})
80+
81+
# passing empty strings inside a tuple/list
82+
check_against(
83+
ttk._format_optdict(
84+
{'option': ('', 'one')}),
85+
{'-option': '{} one'})
86+
87+
# passing values with braces inside a tuple/list
88+
check_against(
89+
ttk._format_optdict(
90+
{'option': ('one} {two', 'three')}),
91+
{'-option': r'one\}\ \{two three'})
92+
93+
# passing quoted strings inside a tuple/list
94+
check_against(
95+
ttk._format_optdict(
96+
{'option': ('"one"', 'two')}),
97+
{'-option': '{"one"} two'})
98+
check_against(
99+
ttk._format_optdict(
100+
{'option': ('{one}', 'two')}),
101+
{'-option': r'\{one\} two'})
72102

73103
# ignore an option
74104
amount_opts = len(ttk._format_optdict(opts, ignore=('á'))) / 2

Lib/tkinter/test/test_ttk/test_widgets.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ def check_get_current(getval, currval):
189189
self.combo.configure(values=[1, '', 2])
190190
self.assertEqual(self.combo['values'], ('1', '', '2'))
191191

192+
# testing values with spaces
193+
self.combo['values'] = ['a b', 'a\tb', 'a\nb']
194+
self.assertEqual(self.combo['values'], ('a b', 'a\tb', 'a\nb'))
195+
196+
# testing values with special characters
197+
self.combo['values'] = [r'a\tb', '"a"', '} {']
198+
self.assertEqual(self.combo['values'], (r'a\tb', '"a"', '} {'))
199+
192200
# out of range
193201
self.assertRaises(tkinter.TclError, self.combo.current,
194202
len(self.combo['values']))

Lib/tkinter/ttk.py

Lines changed: 47 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
"tclobjs_to_py", "setup_master"]
2727

2828
import tkinter
29-
30-
_flatten = tkinter._flatten
29+
from tkinter import _flatten, _join, _stringify
3130

3231
# Verify if Tk is new enough to not need the Tile package
3332
_REQUIRE_TILE = True if tkinter.TkVersion < 8.5 else False
@@ -47,40 +46,55 @@ def _load_tile(master):
4746
master.tk.eval('package require tile') # TclError may be raised here
4847
master._tile_loaded = True
4948

49+
def _format_optvalue(value, script=False):
50+
"""Internal function."""
51+
if script:
52+
# if caller passes a Tcl script to tk.call, all the values need to
53+
# be grouped into words (arguments to a command in Tcl dialect)
54+
value = _stringify(value)
55+
elif isinstance(value, (list, tuple)):
56+
value = _join(value)
57+
return value
58+
5059
def _format_optdict(optdict, script=False, ignore=None):
5160
"""Formats optdict to a tuple to pass it to tk.call.
5261
5362
E.g. (script=False):
5463
{'foreground': 'blue', 'padding': [1, 2, 3, 4]} returns:
5564
('-foreground', 'blue', '-padding', '1 2 3 4')"""
56-
format = "%s" if not script else "{%s}"
5765

5866
opts = []
5967
for opt, value in optdict.items():
60-
if ignore and opt in ignore:
61-
continue
62-
63-
if isinstance(value, (list, tuple)):
64-
v = []
65-
for val in value:
66-
if isinstance(val, str):
67-
v.append(str(val) if val else '{}')
68-
else:
69-
v.append(str(val))
70-
71-
# format v according to the script option, but also check for
72-
# space in any value in v in order to group them correctly
73-
value = format % ' '.join(
74-
('{%s}' if ' ' in val else '%s') % val for val in v)
68+
if not ignore or opt not in ignore:
69+
opts.append("-%s" % opt)
70+
if value is not None:
71+
opts.append(_format_optvalue(value, script))
7572

76-
if script and value == '':
77-
value = '{}' # empty string in Python is equivalent to {} in Tcl
78-
79-
opts.append(("-%s" % opt, value))
80-
81-
# Remember: _flatten skips over None
8273
return _flatten(opts)
8374

75+
def _mapdict_values(items):
76+
# each value in mapdict is expected to be a sequence, where each item
77+
# is another sequence containing a state (or several) and a value
78+
# E.g. (script=False):
79+
# [('active', 'selected', 'grey'), ('focus', [1, 2, 3, 4])]
80+
# returns:
81+
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
82+
opt_val = []
83+
for *state, val in items:
84+
# hacks for bakward compatibility
85+
state[0] # raise IndexError if empty
86+
if len(state) == 1:
87+
# if it is empty (something that evaluates to False), then
88+
# format it to Tcl code to denote the "normal" state
89+
state = state[0] or ''
90+
else:
91+
# group multiple states
92+
state = ' '.join(state) # raise TypeError if not str
93+
opt_val.append(state)
94+
if val is not None:
95+
opt_val.append(val)
96+
return opt_val
97+
8498
def _format_mapdict(mapdict, script=False):
8599
"""Formats mapdict to pass it to tk.call.
86100
@@ -90,32 +104,11 @@ def _format_mapdict(mapdict, script=False):
90104
returns:
91105
92106
('-expand', '{active selected} grey focus {1, 2, 3, 4}')"""
93-
# if caller passes a Tcl script to tk.call, all the values need to
94-
# be grouped into words (arguments to a command in Tcl dialect)
95-
format = "%s" if not script else "{%s}"
96107

97108
opts = []
98109
for opt, value in mapdict.items():
99-
100-
opt_val = []
101-
# each value in mapdict is expected to be a sequence, where each item
102-
# is another sequence containing a state (or several) and a value
103-
for statespec in value:
104-
state, val = statespec[:-1], statespec[-1]
105-
106-
if len(state) > 1: # group multiple states
107-
state = "{%s}" % ' '.join(state)
108-
else: # single state
109-
# if it is empty (something that evaluates to False), then
110-
# format it to Tcl code to denote the "normal" state
111-
state = state[0] or '{}'
112-
113-
if isinstance(val, (list, tuple)): # val needs to be grouped
114-
val = "{%s}" % ' '.join(map(str, val))
115-
116-
opt_val.append("%s %s" % (state, val))
117-
118-
opts.append(("-%s" % opt, format % ' '.join(opt_val)))
110+
opts.extend(("-%s" % opt,
111+
_format_optvalue(_mapdict_values(value), script)))
119112

120113
return _flatten(opts)
121114

@@ -129,7 +122,7 @@ def _format_elemcreate(etype, script=False, *args, **kw):
129122
iname = args[0]
130123
# next args, if any, are statespec/value pairs which is almost
131124
# a mapdict, but we just need the value
132-
imagespec = _format_mapdict({None: args[1:]})[1]
125+
imagespec = _join(_mapdict_values(args[1:]))
133126
spec = "%s %s" % (iname, imagespec)
134127

135128
else:
@@ -138,7 +131,7 @@ def _format_elemcreate(etype, script=False, *args, **kw):
138131
# themed styles on Windows XP and Vista.
139132
# Availability: Tk 8.6, Windows XP and Vista.
140133
class_name, part_id = args[:2]
141-
statemap = _format_mapdict({None: args[2:]})[1]
134+
statemap = _join(_mapdict_values(args[2:]))
142135
spec = "%s %s %s" % (class_name, part_id, statemap)
143136

144137
opts = _format_optdict(kw, script)
@@ -148,11 +141,11 @@ def _format_elemcreate(etype, script=False, *args, **kw):
148141
# otherwise it will clone {} (empty element)
149142
spec = args[0] # theme name
150143
if len(args) > 1: # elementfrom specified
151-
opts = (args[1], )
144+
opts = (_format_optvalue(args[1], script),)
152145

153146
if script:
154147
spec = '{%s}' % spec
155-
opts = ' '.join(map(str, opts))
148+
opts = ' '.join(opts)
156149

157150
return spec, opts
158151

@@ -189,7 +182,7 @@ def _format_layoutlist(layout, indent=0, indent_size=2):
189182
for layout_elem in layout:
190183
elem, opts = layout_elem
191184
opts = opts or {}
192-
fopts = ' '.join(map(str, _format_optdict(opts, True, "children")))
185+
fopts = ' '.join(_format_optdict(opts, True, ("children",)))
193186
head = "%s%s%s" % (' ' * indent, elem, (" %s" % fopts) if fopts else '')
194187

195188
if "children" in opts:
@@ -215,11 +208,11 @@ def _script_from_settings(settings):
215208
for name, opts in settings.items():
216209
# will format specific keys according to Tcl code
217210
if opts.get('configure'): # format 'configure'
218-
s = ' '.join(map(str, _format_optdict(opts['configure'], True)))
211+
s = ' '.join(_format_optdict(opts['configure'], True))
219212
script.append("ttk::style configure %s %s;" % (name, s))
220213

221214
if opts.get('map'): # format 'map'
222-
s = ' '.join(map(str, _format_mapdict(opts['map'], True)))
215+
s = ' '.join(_format_mapdict(opts['map'], True))
223216
script.append("ttk::style map %s %s;" % (name, s))
224217

225218
if 'layout' in opts: # format 'layout' which may be empty
@@ -706,30 +699,9 @@ def __init__(self, master=None, **kw):
706699
exportselection, justify, height, postcommand, state,
707700
textvariable, values, width
708701
"""
709-
# The "values" option may need special formatting, so leave to
710-
# _format_optdict the responsibility to format it
711-
if "values" in kw:
712-
kw["values"] = _format_optdict({'v': kw["values"]})[1]
713-
714702
Entry.__init__(self, master, "ttk::combobox", **kw)
715703

716704

717-
def __setitem__(self, item, value):
718-
if item == "values":
719-
value = _format_optdict({item: value})[1]
720-
721-
Entry.__setitem__(self, item, value)
722-
723-
724-
def configure(self, cnf=None, **kw):
725-
"""Custom Combobox configure, created to properly format the values
726-
option."""
727-
if "values" in kw:
728-
kw["values"] = _format_optdict({'v': kw["values"]})[1]
729-
730-
return Entry.configure(self, cnf, **kw)
731-
732-
733705
def current(self, newindex=None):
734706
"""If newindex is supplied, sets the combobox value to the
735707
element at position newindex in the list of values. Otherwise,

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ Core and Builtins
202202
Library
203203
-------
204204

205+
- Issue #15861: tkinter now correctly works with lists and tuples containing
206+
strings with whitespaces, backslashes or unbalanced braces.
207+
205208
- Issue #10527: Use poll() instead of select() for multiprocessing pipes.
206209

207210
- Issue #9720: zipfile now writes correct local headers for files larger than

0 commit comments

Comments
 (0)