From e39ba04e22c619fe76ff12bd3f6eb5fc0d322416 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 15 Jan 2013 18:01:21 +0200 Subject: [PATCH] Issue #15861: tkinter now correctly works with lists and tuples containing strings with whitespaces, backslashes or unbalanced braces. --- Lib/lib-tk/Tkinter.py | 34 +++++- Lib/lib-tk/test/test_ttk/test_functions.py | 40 ++++++- Lib/lib-tk/test/test_ttk/test_widgets.py | 8 ++ Lib/lib-tk/ttk.py | 124 ++++++++------------- Misc/NEWS | 3 + 5 files changed, 128 insertions(+), 81 deletions(-) diff --git a/Lib/lib-tk/Tkinter.py b/Lib/lib-tk/Tkinter.py index e5c2e5181f1..fcb320f4418 100644 --- a/Lib/lib-tk/Tkinter.py +++ b/Lib/lib-tk/Tkinter.py @@ -41,6 +41,7 @@ tkinter = _tkinter # b/w compat for export TclError = _tkinter.TclError from types import * from Tkconstants import * +import re wantobjects = 1 @@ -58,6 +59,37 @@ try: _tkinter.deletefilehandler except AttributeError: _tkinter.deletefilehandler = None +_magic_re = re.compile(r'([\\{}])') +_space_re = re.compile(r'([\s])') + +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: + if isinstance(value, basestring): + value = unicode(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(tuple): """Internal function.""" res = () @@ -1086,7 +1118,7 @@ class Misc: nv.append('%d' % item) else: # format it to proper Tcl code if it contains space - nv.append(('{%s}' if ' ' in item else '%s') % item) + nv.append(_stringify(item)) else: v = ' '.join(nv) res = res + ('-'+k, v) diff --git a/Lib/lib-tk/test/test_ttk/test_functions.py b/Lib/lib-tk/test/test_ttk/test_functions.py index 15e76c1ed1d..b8f08a5bfc0 100644 --- a/Lib/lib-tk/test/test_ttk/test_functions.py +++ b/Lib/lib-tk/test/test_ttk/test_functions.py @@ -50,13 +50,17 @@ class InternalFunctionsTest(unittest.TestCase): ttk._format_optdict({'test': {'left': 'as is'}}), {'-test': {'left': 'as is'}}) - # check script formatting and untouched value(s) + # check script formatting check_against( ttk._format_optdict( - {'test': [1, -1, '', '2m', 0], 'nochange1': 3, - 'nochange2': 'abc def'}, script=True), - {'-test': '{1 -1 {} 2m 0}', '-nochange1': 3, - '-nochange2': 'abc def' }) + {'test': [1, -1, '', '2m', 0], 'test2': 3, + 'test3': '', 'test4': 'abc def', + 'test5': '"abc"', 'test6': '{}', + 'test7': '} -spam {'}, script=True), + {'-test': '{1 -1 {} 2m 0}', '-test2': '3', + '-test3': '{}', '-test4': '{abc def}', + '-test5': '{"abc"}', '-test6': r'\{\}', + '-test7': r'\}\ -spam\ \{'}) opts = {u'αβγ': True, u'á': False} orig_opts = opts.copy() @@ -70,6 +74,32 @@ class InternalFunctionsTest(unittest.TestCase): ttk._format_optdict( {'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 amount_opts = len(ttk._format_optdict(opts, ignore=(u'á'))) // 2 diff --git a/Lib/lib-tk/test/test_ttk/test_widgets.py b/Lib/lib-tk/test/test_ttk/test_widgets.py index aca90cf80bd..9d06a75610e 100644 --- a/Lib/lib-tk/test/test_ttk/test_widgets.py +++ b/Lib/lib-tk/test/test_ttk/test_widgets.py @@ -188,6 +188,14 @@ class ComboboxTest(unittest.TestCase): self.combo.configure(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 self.assertRaises(Tkinter.TclError, self.combo.current, len(self.combo['values'])) diff --git a/Lib/lib-tk/ttk.py b/Lib/lib-tk/ttk.py index d632f77e9fb..89f7374064b 100644 --- a/Lib/lib-tk/ttk.py +++ b/Lib/lib-tk/ttk.py @@ -26,8 +26,7 @@ __all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label", "tclobjs_to_py", "setup_master"] import Tkinter - -_flatten = Tkinter._flatten +from Tkinter import _flatten, _join, _stringify # Verify if Tk is new enough to not need the Tile package _REQUIRE_TILE = True if Tkinter.TkVersion < 8.5 else False @@ -47,40 +46,57 @@ def _load_tile(master): master.tk.eval('package require tile') # TclError may be raised here 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): """Formats optdict to a tuple to pass it to tk.call. E.g. (script=False): {'foreground': 'blue', 'padding': [1, 2, 3, 4]} returns: ('-foreground', 'blue', '-padding', '1 2 3 4')""" - format = "%s" if not script else "{%s}" opts = [] for opt, value in optdict.iteritems(): - if ignore and opt in ignore: - continue + if not ignore or opt not in ignore: + 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, basestring): - v.append(unicode(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) +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 item in items: + state = item[:-1] + val = item[-1] + # 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): """Formats mapdict to pass it to tk.call. @@ -90,32 +106,11 @@ def _format_mapdict(mapdict, script=False): returns: ('-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 = [] for opt, value in mapdict.iteritems(): - - opt_val = [] - # 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))) + opts.extend(("-%s" % opt, + _format_optvalue(_mapdict_values(value), script))) return _flatten(opts) @@ -129,7 +124,7 @@ def _format_elemcreate(etype, script=False, *args, **kw): iname = args[0] # next args, if any, are statespec/value pairs which is almost # 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) else: @@ -138,7 +133,7 @@ def _format_elemcreate(etype, script=False, *args, **kw): # themed styles on Windows XP and Vista. # Availability: Tk 8.6, Windows XP and Vista. 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) opts = _format_optdict(kw, script) @@ -148,11 +143,11 @@ def _format_elemcreate(etype, script=False, *args, **kw): # otherwise it will clone {} (empty element) spec = args[0] # theme name if len(args) > 1: # elementfrom specified - opts = (args[1], ) + opts = (_format_optvalue(args[1], script),) if script: spec = '{%s}' % spec - opts = ' '.join(map(str, opts)) + opts = ' '.join(opts) return spec, opts @@ -189,7 +184,7 @@ def _format_layoutlist(layout, indent=0, indent_size=2): for layout_elem in layout: elem, opts = layout_elem 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 '') if "children" in opts: @@ -215,11 +210,11 @@ def _script_from_settings(settings): for name, opts in settings.iteritems(): # will format specific keys according to Tcl code if opts.get('configure'): # format 'configure' - s = ' '.join(map(unicode, _format_optdict(opts['configure'], True))) + s = ' '.join(_format_optdict(opts['configure'], True)) script.append("ttk::style configure %s %s;" % (name, s)) if opts.get('map'): # format 'map' - s = ' '.join(map(unicode, _format_mapdict(opts['map'], True))) + s = ' '.join(_format_mapdict(opts['map'], True)) script.append("ttk::style map %s %s;" % (name, s)) if 'layout' in opts: # format 'layout' which may be empty @@ -706,30 +701,9 @@ class Combobox(Entry): exportselection, justify, height, postcommand, state, 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) - 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): """If newindex is supplied, sets the combobox value to the element at position newindex in the list of values. Otherwise, diff --git a/Misc/NEWS b/Misc/NEWS index a227df83763..eb41458e827 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -189,6 +189,9 @@ Core and Builtins Library ------- +- Issue #15861: tkinter now correctly works with lists and tuples containing + strings with whitespaces, backslashes or unbalanced braces. + - Issue #10527: Use poll() instead of select() for multiprocessing pipes. - Issue #9720: zipfile now writes correct local headers for files larger than