Issue #15861: tkinter now correctly works with lists and tuples containing

strings with whitespaces, backslashes or unbalanced braces.
This commit is contained in:
Serhiy Storchaka 2013-01-15 17:59:53 +02:00
commit fb3c6286a6
5 changed files with 123 additions and 81 deletions

View File

@ -40,6 +40,7 @@ import warnings
import _tkinter # If this fails your Python may not be configured for Tk import _tkinter # If this fails your Python may not be configured for Tk
TclError = _tkinter.TclError TclError = _tkinter.TclError
from tkinter.constants import * from tkinter.constants import *
import re
wantobjects = 1 wantobjects = 1
@ -52,6 +53,34 @@ WRITABLE = _tkinter.WRITABLE
EXCEPTION = _tkinter.EXCEPTION EXCEPTION = _tkinter.EXCEPTION
_magic_re = re.compile(r'([\\{}])')
_space_re = re.compile(r'([\s])', re.ASCII)
def _join(value):
"""Internal function."""
return ' '.join(map(_stringify, value))
def _stringify(value):
"""Internal function."""
if isinstance(value, (list, tuple)):
if len(value) == 1:
value = _stringify(value[0])
if value[0] == '{':
value = '{%s}' % value
else:
value = '{%s}' % _join(value)
else:
value = str(value)
if not value:
value = '{}'
elif _magic_re.search(value):
# add '\' before special characters and spaces
value = _magic_re.sub(r'\\\1', value)
value = _space_re.sub(r'\\\1', value)
elif value[0] == '"' or _space_re.search(value):
value = '{%s}' % value
return value
def _flatten(seq): def _flatten(seq):
"""Internal function.""" """Internal function."""
res = () res = ()
@ -1089,7 +1118,7 @@ class Misc:
if isinstance(item, int): if isinstance(item, int):
nv.append(str(item)) nv.append(str(item))
elif isinstance(item, str): elif isinstance(item, str):
nv.append(('{%s}' if ' ' in item else '%s') % item) nv.append(_stringify(item))
else: else:
break break
else: else:

View File

@ -49,13 +49,17 @@ class InternalFunctionsTest(unittest.TestCase):
ttk._format_optdict({'test': {'left': 'as is'}}), ttk._format_optdict({'test': {'left': 'as is'}}),
{'-test': {'left': 'as is'}}) {'-test': {'left': 'as is'}})
# check script formatting and untouched value(s) # check script formatting
check_against( check_against(
ttk._format_optdict( ttk._format_optdict(
{'test': [1, -1, '', '2m', 0], 'nochange1': 3, {'test': [1, -1, '', '2m', 0], 'test2': 3,
'nochange2': 'abc def'}, script=True), 'test3': '', 'test4': 'abc def',
{'-test': '{1 -1 {} 2m 0}', '-nochange1': 3, 'test5': '"abc"', 'test6': '{}',
'-nochange2': 'abc def' }) 'test7': '} -spam {'}, script=True),
{'-test': '{1 -1 {} 2m 0}', '-test2': '3',
'-test3': '{}', '-test4': '{abc def}',
'-test5': '{"abc"}', '-test6': r'\{\}',
'-test7': r'\}\ -spam\ \{'})
opts = {'αβγ': True, 'á': False} opts = {'αβγ': True, 'á': False}
orig_opts = opts.copy() orig_opts = opts.copy()
@ -69,6 +73,32 @@ class InternalFunctionsTest(unittest.TestCase):
ttk._format_optdict( ttk._format_optdict(
{'option': ('one two', 'three')}), {'option': ('one two', 'three')}),
{'-option': '{one two} three'}) {'-option': '{one two} three'})
check_against(
ttk._format_optdict(
{'option': ('one\ttwo', 'three')}),
{'-option': '{one\ttwo} three'})
# passing empty strings inside a tuple/list
check_against(
ttk._format_optdict(
{'option': ('', 'one')}),
{'-option': '{} one'})
# passing values with braces inside a tuple/list
check_against(
ttk._format_optdict(
{'option': ('one} {two', 'three')}),
{'-option': r'one\}\ \{two three'})
# passing quoted strings inside a tuple/list
check_against(
ttk._format_optdict(
{'option': ('"one"', 'two')}),
{'-option': '{"one"} two'})
check_against(
ttk._format_optdict(
{'option': ('{one}', 'two')}),
{'-option': r'\{one\} two'})
# ignore an option # ignore an option
amount_opts = len(ttk._format_optdict(opts, ignore=('á'))) / 2 amount_opts = len(ttk._format_optdict(opts, ignore=('á'))) / 2

View File

@ -189,6 +189,14 @@ class ComboboxTest(unittest.TestCase):
self.combo.configure(values=[1, '', 2]) self.combo.configure(values=[1, '', 2])
self.assertEqual(self.combo['values'], ('1', '', '2')) self.assertEqual(self.combo['values'], ('1', '', '2'))
# testing values with spaces
self.combo['values'] = ['a b', 'a\tb', 'a\nb']
self.assertEqual(self.combo['values'], ('a b', 'a\tb', 'a\nb'))
# testing values with special characters
self.combo['values'] = [r'a\tb', '"a"', '} {']
self.assertEqual(self.combo['values'], (r'a\tb', '"a"', '} {'))
# out of range # out of range
self.assertRaises(tkinter.TclError, self.combo.current, self.assertRaises(tkinter.TclError, self.combo.current,
len(self.combo['values'])) len(self.combo['values']))

View File

@ -26,8 +26,7 @@ __all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label",
"tclobjs_to_py", "setup_master"] "tclobjs_to_py", "setup_master"]
import tkinter import tkinter
from tkinter import _flatten, _join, _stringify
_flatten = tkinter._flatten
# Verify if Tk is new enough to not need the Tile package # Verify if Tk is new enough to not need the Tile package
_REQUIRE_TILE = True if tkinter.TkVersion < 8.5 else False _REQUIRE_TILE = True if tkinter.TkVersion < 8.5 else False
@ -47,40 +46,55 @@ def _load_tile(master):
master.tk.eval('package require tile') # TclError may be raised here master.tk.eval('package require tile') # TclError may be raised here
master._tile_loaded = True master._tile_loaded = True
def _format_optvalue(value, script=False):
"""Internal function."""
if script:
# if caller passes a Tcl script to tk.call, all the values need to
# be grouped into words (arguments to a command in Tcl dialect)
value = _stringify(value)
elif isinstance(value, (list, tuple)):
value = _join(value)
return value
def _format_optdict(optdict, script=False, ignore=None): def _format_optdict(optdict, script=False, ignore=None):
"""Formats optdict to a tuple to pass it to tk.call. """Formats optdict to a tuple to pass it to tk.call.
E.g. (script=False): E.g. (script=False):
{'foreground': 'blue', 'padding': [1, 2, 3, 4]} returns: {'foreground': 'blue', 'padding': [1, 2, 3, 4]} returns:
('-foreground', 'blue', '-padding', '1 2 3 4')""" ('-foreground', 'blue', '-padding', '1 2 3 4')"""
format = "%s" if not script else "{%s}"
opts = [] opts = []
for opt, value in optdict.items(): for opt, value in optdict.items():
if ignore and opt in ignore: if not ignore or opt not in ignore:
continue opts.append("-%s" % opt)
if value is not None:
opts.append(_format_optvalue(value, script))
if isinstance(value, (list, tuple)):
v = []
for val in value:
if isinstance(val, str):
v.append(str(val) if val else '{}')
else:
v.append(str(val))
# format v according to the script option, but also check for
# space in any value in v in order to group them correctly
value = format % ' '.join(
('{%s}' if ' ' in val else '%s') % val for val in v)
if script and value == '':
value = '{}' # empty string in Python is equivalent to {} in Tcl
opts.append(("-%s" % opt, value))
# Remember: _flatten skips over None
return _flatten(opts) return _flatten(opts)
def _mapdict_values(items):
# each value in mapdict is expected to be a sequence, where each item
# is another sequence containing a state (or several) and a value
# E.g. (script=False):
# [('active', 'selected', 'grey'), ('focus', [1, 2, 3, 4])]
# returns:
# ['active selected', 'grey', 'focus', [1, 2, 3, 4]]
opt_val = []
for *state, val in items:
# hacks for bakward 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
state = state[0] or ''
else:
# group multiple states
state = ' '.join(state) # raise TypeError if not str
opt_val.append(state)
if val is not None:
opt_val.append(val)
return opt_val
def _format_mapdict(mapdict, script=False): def _format_mapdict(mapdict, script=False):
"""Formats mapdict to pass it to tk.call. """Formats mapdict to pass it to tk.call.
@ -90,32 +104,11 @@ def _format_mapdict(mapdict, script=False):
returns: returns:
('-expand', '{active selected} grey focus {1, 2, 3, 4}')""" ('-expand', '{active selected} grey focus {1, 2, 3, 4}')"""
# if caller passes a Tcl script to tk.call, all the values need to
# be grouped into words (arguments to a command in Tcl dialect)
format = "%s" if not script else "{%s}"
opts = [] opts = []
for opt, value in mapdict.items(): for opt, value in mapdict.items():
opts.extend(("-%s" % opt,
opt_val = [] _format_optvalue(_mapdict_values(value), script)))
# each value in mapdict is expected to be a sequence, where each item
# is another sequence containing a state (or several) and a value
for statespec in value:
state, val = statespec[:-1], statespec[-1]
if len(state) > 1: # group multiple states
state = "{%s}" % ' '.join(state)
else: # single state
# if it is empty (something that evaluates to False), then
# format it to Tcl code to denote the "normal" state
state = state[0] or '{}'
if isinstance(val, (list, tuple)): # val needs to be grouped
val = "{%s}" % ' '.join(map(str, val))
opt_val.append("%s %s" % (state, val))
opts.append(("-%s" % opt, format % ' '.join(opt_val)))
return _flatten(opts) return _flatten(opts)
@ -129,7 +122,7 @@ def _format_elemcreate(etype, script=False, *args, **kw):
iname = args[0] iname = args[0]
# next args, if any, are statespec/value pairs which is almost # next args, if any, are statespec/value pairs which is almost
# a mapdict, but we just need the value # a mapdict, but we just need the value
imagespec = _format_mapdict({None: args[1:]})[1] imagespec = _join(_mapdict_values(args[1:]))
spec = "%s %s" % (iname, imagespec) spec = "%s %s" % (iname, imagespec)
else: else:
@ -138,7 +131,7 @@ def _format_elemcreate(etype, script=False, *args, **kw):
# themed styles on Windows XP and Vista. # themed styles on Windows XP and Vista.
# Availability: Tk 8.6, Windows XP and Vista. # Availability: Tk 8.6, Windows XP and Vista.
class_name, part_id = args[:2] class_name, part_id = args[:2]
statemap = _format_mapdict({None: args[2:]})[1] statemap = _join(_mapdict_values(args[2:]))
spec = "%s %s %s" % (class_name, part_id, statemap) spec = "%s %s %s" % (class_name, part_id, statemap)
opts = _format_optdict(kw, script) opts = _format_optdict(kw, script)
@ -148,11 +141,11 @@ def _format_elemcreate(etype, script=False, *args, **kw):
# otherwise it will clone {} (empty element) # otherwise it will clone {} (empty element)
spec = args[0] # theme name spec = args[0] # theme name
if len(args) > 1: # elementfrom specified if len(args) > 1: # elementfrom specified
opts = (args[1], ) opts = (_format_optvalue(args[1], script),)
if script: if script:
spec = '{%s}' % spec spec = '{%s}' % spec
opts = ' '.join(map(str, opts)) opts = ' '.join(opts)
return spec, opts return spec, opts
@ -189,7 +182,7 @@ def _format_layoutlist(layout, indent=0, indent_size=2):
for layout_elem in layout: for layout_elem in layout:
elem, opts = layout_elem elem, opts = layout_elem
opts = opts or {} opts = opts or {}
fopts = ' '.join(map(str, _format_optdict(opts, True, "children"))) fopts = ' '.join(_format_optdict(opts, True, ("children",)))
head = "%s%s%s" % (' ' * indent, elem, (" %s" % fopts) if fopts else '') head = "%s%s%s" % (' ' * indent, elem, (" %s" % fopts) if fopts else '')
if "children" in opts: if "children" in opts:
@ -215,11 +208,11 @@ def _script_from_settings(settings):
for name, opts in settings.items(): for name, opts in settings.items():
# will format specific keys according to Tcl code # will format specific keys according to Tcl code
if opts.get('configure'): # format 'configure' if opts.get('configure'): # format 'configure'
s = ' '.join(map(str, _format_optdict(opts['configure'], True))) s = ' '.join(_format_optdict(opts['configure'], True))
script.append("ttk::style configure %s %s;" % (name, s)) script.append("ttk::style configure %s %s;" % (name, s))
if opts.get('map'): # format 'map' if opts.get('map'): # format 'map'
s = ' '.join(map(str, _format_mapdict(opts['map'], True))) s = ' '.join(_format_mapdict(opts['map'], True))
script.append("ttk::style map %s %s;" % (name, s)) script.append("ttk::style map %s %s;" % (name, s))
if 'layout' in opts: # format 'layout' which may be empty if 'layout' in opts: # format 'layout' which may be empty
@ -706,30 +699,9 @@ class Combobox(Entry):
exportselection, justify, height, postcommand, state, exportselection, justify, height, postcommand, state,
textvariable, values, width textvariable, values, width
""" """
# The "values" option may need special formatting, so leave to
# _format_optdict the responsibility to format it
if "values" in kw:
kw["values"] = _format_optdict({'v': kw["values"]})[1]
Entry.__init__(self, master, "ttk::combobox", **kw) Entry.__init__(self, master, "ttk::combobox", **kw)
def __setitem__(self, item, value):
if item == "values":
value = _format_optdict({item: value})[1]
Entry.__setitem__(self, item, value)
def configure(self, cnf=None, **kw):
"""Custom Combobox configure, created to properly format the values
option."""
if "values" in kw:
kw["values"] = _format_optdict({'v': kw["values"]})[1]
return Entry.configure(self, cnf, **kw)
def current(self, newindex=None): def current(self, newindex=None):
"""If newindex is supplied, sets the combobox value to the """If newindex is supplied, sets the combobox value to the
element at position newindex in the list of values. Otherwise, element at position newindex in the list of values. Otherwise,

View File

@ -220,6 +220,9 @@ Core and Builtins
Library Library
------- -------
- Issue #15861: tkinter now correctly works with lists and tuples containing
strings with whitespaces, backslashes or unbalanced braces.
- Issue #9720: zipfile now writes correct local headers for files larger than - Issue #9720: zipfile now writes correct local headers for files larger than
4 GiB. 4 GiB.