summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/tkinter/__init__.py31
-rw-r--r--Lib/tkinter/test/test_ttk/test_functions.py40
-rw-r--r--Lib/tkinter/test/test_ttk/test_widgets.py8
-rw-r--r--Lib/tkinter/ttk.py122
-rw-r--r--Misc/NEWS3
5 files changed, 123 insertions, 81 deletions
diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py
index 3ba4bb0..a6ad888 100644
--- a/Lib/tkinter/__init__.py
+++ b/Lib/tkinter/__init__.py
@@ -39,6 +39,7 @@ if sys.platform == "win32":
import _tkinter # If this fails your Python may not be configured for Tk
TclError = _tkinter.TclError
from tkinter.constants import *
+import re
wantobjects = 1
@@ -50,6 +51,34 @@ WRITABLE = _tkinter.WRITABLE
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):
"""Internal function."""
res = ()
@@ -1075,7 +1104,7 @@ class Misc:
if isinstance(item, int):
nv.append(str(item))
elif isinstance(item, str):
- nv.append(('{%s}' if ' ' in item else '%s') % item)
+ nv.append(_stringify(item))
else:
break
else:
diff --git a/Lib/tkinter/test/test_ttk/test_functions.py b/Lib/tkinter/test/test_ttk/test_functions.py
index 2303e4c..0d8df16 100644
--- a/Lib/tkinter/test/test_ttk/test_functions.py
+++ b/Lib/tkinter/test/test_ttk/test_functions.py
@@ -49,13 +49,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 = {'αβγ': True, 'á': False}
orig_opts = opts.copy()
@@ -69,6 +73,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=('á'))) / 2
diff --git a/Lib/tkinter/test/test_ttk/test_widgets.py b/Lib/tkinter/test/test_ttk/test_widgets.py
index f5c0f17..c2231dc 100644
--- a/Lib/tkinter/test/test_ttk/test_widgets.py
+++ b/Lib/tkinter/test/test_ttk/test_widgets.py
@@ -189,6 +189,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/tkinter/ttk.py b/Lib/tkinter/ttk.py
index 5ae20a8..6cd4ea6 100644
--- a/Lib/tkinter/ttk.py
+++ b/Lib/tkinter/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,55 @@ 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.items():
- if ignore and opt in ignore:
- continue
-
- 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 not ignore or opt not in ignore:
+ opts.append("-%s" % opt)
+ if value is not None:
+ opts.append(_format_optvalue(value, script))
- 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 *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):
"""Formats mapdict to pass it to tk.call.
@@ -90,32 +104,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.items():
-
- 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 +122,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 +131,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 +141,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 +182,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 +208,11 @@ def _script_from_settings(settings):
for name, opts in settings.items():
# will format specific keys according to Tcl code
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))
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))
if 'layout' in opts: # format 'layout' which may be empty
@@ -706,30 +699,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 45c0598..19a3e93 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -202,6 +202,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