Issue #15861: tkinter now correctly works with lists and tuples containing
strings with whitespaces, backslashes or unbalanced braces.
This commit is contained in:
commit
fb3c6286a6
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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']))
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue