[Python-checkins] cpython (2.7): Issue #15861: tkinter now correctly works with lists and tuples containing

serhiy.storchaka python-checkins at python.org
Tue Jan 15 17:04:42 CET 2013


http://hg.python.org/cpython/rev/917ae14831ec
changeset:   81530:917ae14831ec
branch:      2.7
parent:      81521:2e1f1b096f3d
user:        Serhiy Storchaka <storchaka at gmail.com>
date:        Tue Jan 15 18:01:21 2013 +0200
summary:
  Issue #15861: tkinter now correctly works with lists and tuples containing
strings with whitespaces, backslashes or unbalanced braces.

files:
  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
--- a/Lib/lib-tk/Tkinter.py
+++ b/Lib/lib-tk/Tkinter.py
@@ -41,6 +41,7 @@
 TclError = _tkinter.TclError
 from types import *
 from Tkconstants import *
+import re
 
 wantobjects = 1
 
@@ -58,6 +59,37 @@
 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 @@
                             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
--- a/Lib/lib-tk/test/test_ttk/test_functions.py
+++ b/Lib/lib-tk/test/test_ttk/test_functions.py
@@ -50,13 +50,17 @@
             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 @@
             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
--- a/Lib/lib-tk/test/test_ttk/test_widgets.py
+++ b/Lib/lib-tk/test/test_ttk/test_widgets.py
@@ -188,6 +188,14 @@
         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
--- a/Lib/lib-tk/ttk.py
+++ b/Lib/lib-tk/ttk.py
@@ -26,8 +26,7 @@
            "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,39 +46,56 @@
         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))
+    return _flatten(opts)
 
-            # 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 @@
       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 @@
             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 @@
             # 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 @@
         # 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 @@
     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 @@
     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 @@
             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
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -189,6 +189,9 @@
 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

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list