Sync canonical Python 3.15 GUI libraries and tests (#9283)

Refresh the IDLE, Tkinter and turtle family through update_lib, including
the direct canonical text-only helper and image fixture dependencies.
Preserve capability/display guards without changing native Tk support.

Assisted-by: OpenAI Codex:model-version-unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-22 10:12:45 +09:00
1 parent bcfad3554e
commit b976859e8d
87 files changed
+14930 -803

No files matched your search

+47 -2
View File
@@ -1,9 +1,54 @@
What's New in IDLE 3.14.z
What's New in IDLE 3.15.0
(since 3.14.0)
Released after 2025-10-07
Released on 2026-10-01
=========================
gh-152745: When "Run... Customized" with "Restart shell" unchecked
while Shell is running code, including waiting for an input('prompt:')
response, just report that the shell is executing instead of
reporting twice and restarting anyway. Patch by Serhiy Storchaka.
gh-152743: When an integer entry in IDLE's Settings dialog, such as
"Auto squeeze min lines", is deleted, do not save '', which is an
invalid configuration value. Patch by Serhiy Storchaka.
gh-152742: MakeChange the default extension when saving a Shell or
Output window '.txt' and list text files before Python files. Their
content is not Python source. Patch by Serhiy Storchaka and Claude Code.
gh-152740: Fill the "In files:" field of IDLE's Find in Files dialog
with a full directory path, even for an unsaved editor or the Shell.
In the grep output whow which directory was searched. Patch by Serhiy
Storchaka and Claude Code.
gh-152739: Omit the idlelib directory from the path of the IDLE user
process. User code imports such as `import help' can no longer import
an idlelib submodule (here idlelib.help) instead of the intended
top-level module. Patch by Serhiy Storchaka and Claude Code.
gh-152738: Fix a rare crash in the IDLE editor when the completion
window is closed: deleting a key binding for a sequence that is not
bound to the virtual event is now ignored instead of raising a
ValueError. Patch by Serhiy Storchaka and Claude Code.
gh-152737: Fix Replace All in the IDLE editor's Replace dialog when the
search direction is "Up" and "Wrap around" is off: it now replaces all
matches above the current position instead of only the first one.
Patch by Serhiy Storchaka and Claude Code.
gh-152733: For X11 window managers, set toplevel window classes to
"Idle". Patch by Serhiy Storchaka and Claude Code.
gh-152728: Move functions run.fix_scaling, editor.fixwordbreaks
(as 'fix_word_breaks') and pyshell.fix_x11_paste to module util.
Patch by Terry J. Reedy.
gh-85320: IDLE now reads and writes its configuration files and the
breakpoints file using UTF-8 instead of the locale encoding.
Files with non-ASCII characters and non-UTF-8 encoding may need
to be opened in an editor and resaved with UTF-8 encoding.
gh-143774: Better explain the operation of Format / Format Paragraph.
Patch by Terry J. Reedy.
+7 -1
View File
@@ -42,6 +42,11 @@ def make_pat():
]) +
r"))"
)
lazy_softkw = ( # lazy new in 3.15 (+ 2 lines below).
r"^[ \t]*" + # at beginning of line + possible indentation
r"(?P<LAZY_SOFTKW>lazy)" +
r"(?=[ \t]+(?:import|from)\b)" # followed by 'import' or 'from'
)
builtinlist = [str(name) for name in dir(builtins)
if not name.startswith('_') and
name not in keyword.kwlist]
@@ -56,7 +61,7 @@ def make_pat():
prog = re.compile("|".join([
builtin, comment, string, kw,
match_softkw, case_default,
case_softkw_and_pattern,
case_softkw_and_pattern, lazy_softkw,
any("SYNC", [r"\n"]),
]),
re.DOTALL | re.MULTILINE)
@@ -70,6 +75,7 @@ prog_group_name_to_tag = {
"CASE_SOFTKW": "KEYWORD",
"CASE_DEFAULT_UNDERSCORE": "KEYWORD",
"CASE_SOFTKW2": "KEYWORD",
"LAZY_SOFTKW": "KEYWORD",
}
+73 -36
View File
@@ -73,8 +73,9 @@ class IdleConfParser(ConfigParser):
def Load(self):
"Load the configuration file from disk."
if self.file:
self.read(self.file)
if self.file and os.path.exists(self.file):
with open(self.file, encoding='utf-8', errors='replace') as f:
self.read_file(f)
class IdleUserConfParser(IdleConfParser):
"""
@@ -133,10 +134,10 @@ class IdleUserConfParser(IdleConfParser):
if fname and fname[0] != '#':
if not self.IsEmpty():
try:
cfgFile = open(fname, 'w')
cfgFile = open(fname, 'w', encoding='utf-8')
except OSError:
os.unlink(fname)
cfgFile = open(fname, 'w')
cfgFile = open(fname, 'w', encoding='utf-8')
with cfgFile:
self.write(cfgFile)
elif os.path.exists(self.file):
@@ -476,34 +477,58 @@ class IdleConf:
Keybindings come from GetCurrentKeySet() active key dict,
where previously used bindings are disabled.
"""
keysName = extensionName + '_cfgBindings'
activeKeys = self.GetCurrentKeySet()
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
event = '<<' + eventName + '>>'
binding = activeKeys[event]
extKeys[event] = binding
return extKeys
bindings_section = f'{extensionName}_cfgBindings'
current_keyset = self.GetCurrentKeySet()
extension_keys = {}
def __GetRawExtensionKeys(self,extensionName):
event_names = set()
if self.userCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.userCfg['extensions'].GetOptionList(bindings_section)
)
if self.defaultCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.defaultCfg['extensions'].GetOptionList(bindings_section)
)
for event_name in event_names:
event = f'<<{event_name}>>'
binding = current_keyset.get(event, None)
if binding is None:
continue
extension_keys[event] = binding
return extension_keys
def __GetRawExtensionKeys(self, extension_name):
"""Return dict {configurable extensionName event : keybinding list}.
Events come from default config extension_cfgBindings section.
Keybindings list come from the splitting of GetOption, which
tries user config before default config.
"""
keysName = extensionName+'_cfgBindings'
extKeys = {}
if self.defaultCfg['extensions'].has_section(keysName):
eventNames = self.defaultCfg['extensions'].GetOptionList(keysName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', keysName, eventName, default='').split()
event = '<<' + eventName + '>>'
extKeys[event] = binding
return extKeys
bindings_section = f'{extension_name}_cfgBindings'
extension_keys = {}
event_names = set()
if self.userCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.userCfg['extensions'].GetOptionList(bindings_section)
)
if self.defaultCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.defaultCfg['extensions'].GetOptionList(bindings_section)
)
for event_name in event_names:
binding = self.GetOption(
'extensions',
bindings_section,
event_name,
default='',
).split()
event = f'<<{event_name}>>'
extension_keys[event] = binding
return extension_keys
def GetExtensionBindings(self, extensionName):
"""Return dict {extensionName event : active or defined keybinding}.
@@ -512,18 +537,30 @@ class IdleConf:
configurable events (from default config) to GetOption splits,
as in self.__GetRawExtensionKeys.
"""
bindsName = extensionName + '_bindings'
extBinds = self.GetExtensionKeys(extensionName)
#add the non-configurable bindings
if self.defaultCfg['extensions'].has_section(bindsName):
eventNames = self.defaultCfg['extensions'].GetOptionList(bindsName)
for eventName in eventNames:
binding = self.GetOption(
'extensions', bindsName, eventName, default='').split()
event = '<<' + eventName + '>>'
extBinds[event] = binding
bindings_section = f'{extensionName}_bindings'
extension_keys = self.GetExtensionKeys(extensionName)
return extBinds
# add the non-configurable bindings
event_names = set()
if self.userCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.userCfg['extensions'].GetOptionList(bindings_section)
)
if self.defaultCfg['extensions'].has_section(bindings_section):
event_names |= set(
self.defaultCfg['extensions'].GetOptionList(bindings_section)
)
for event_name in event_names:
binding = self.GetOption(
'extensions',
bindings_section,
event_name,
default=''
).split()
event = f'<<{event_name}>>'
extension_keys[event] = binding
return extension_keys
def GetKeyBinding(self, keySetName, eventStr):
"""Return the keybinding list for keySetName eventStr.
+49 -18
View File
@@ -1146,8 +1146,8 @@ class KeysPage(Frame):
selected keyset. The keybindings are loaded in load_keys_list()
and are pairs of (event, [keys]) where keys can be a list
of one or more key combinations to bind to the same event.
Mouse button 1 click invokes on_bindingslist_select(), which
allows button_new_keys to be clicked.
Mouse button 1 click or Up or Down key invokes
on_bindingslist_select(), which allows button_new_keys to be clicked.
So, an item is selected in listbindings, which activates
button_new_keys, and clicking button_new_keys calls function
@@ -1221,9 +1221,12 @@ class KeysPage(Frame):
scroll_target_y = Scrollbar(frame_target)
scroll_target_x = Scrollbar(frame_target, orient=HORIZONTAL)
self.bindingslist = Listbox(
frame_target, takefocus=FALSE, exportselection=FALSE)
frame_target, takefocus=True, exportselection=FALSE)
self.bindingslist.bind('<ButtonRelease-1>',
self.on_bindingslist_select)
self.bindingslist.bind('<KeyRelease-Up>', self.on_bindingslist_select)
self.bindingslist.bind('<KeyRelease-Down>',
self.on_bindingslist_select)
scroll_target_y['command'] = self.bindingslist.yview
scroll_target_x['command'] = self.bindingslist.xview
self.bindingslist['yscrollcommand'] = scroll_target_y.set
@@ -1427,7 +1430,14 @@ class KeysPage(Frame):
self.create_new_key_set(new_keys_name)
def on_bindingslist_select(self, event):
"Activate button to assign new keys to selected action."
"""Activate button to assign new keys to selected action.
Event can result from either mouse click or Up or Down key.
The keys move the selection, but not the anchor used by
get_new_keys and var_changed_keybinding.
"""
if event.type.name == 'KeyRelease':
self.bindingslist.selection_anchor(ACTIVE)
self.button_new_keys.state(('!disabled',))
def create_new_key_set(self, new_key_set_name):
@@ -1465,9 +1475,8 @@ class KeysPage(Frame):
An action/key binding can be selected to change the key binding.
"""
reselect = False
list_index = 0
if self.bindingslist.curselection():
reselect = True
list_index = self.bindingslist.index(ANCHOR)
keyset = idleConf.GetKeySet(keyset_name)
# 'set' is dict mapping virtual event to list of key events.
@@ -1482,10 +1491,11 @@ class KeysPage(Frame):
if bind_name in changes['keys'][keyset_name]:
key = changes['keys'][keyset_name][bind_name]
self.bindingslist.insert(END, bind_name+' - '+key)
if reselect:
self.bindingslist.see(list_index)
self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
self.bindingslist.see(list_index)
self.bindingslist.select_set(list_index)
self.bindingslist.select_anchor(list_index)
self.bindingslist.activate(list_index)
self.button_new_keys.state(('!disabled',))
@staticmethod
def save_new_key_set(keyset_name, keyset):
@@ -1960,12 +1970,15 @@ class ExtPage(Frame):
def load_extensions(self):
"Fill self.extensions with data from the default and user configs."
self.extensions = {}
for ext_name in idleConf.GetExtensions(active_only=False):
# Former built-in extensions are already filtered out.
self.extensions[ext_name] = []
for ext_name in self.extensions:
opt_list = sorted(self.ext_defaultCfg.GetOptionList(ext_name))
default = set(self.ext_defaultCfg.GetOptionList(ext_name))
user = set(self.ext_userCfg.GetOptionList(ext_name))
opt_list = sorted(default | user)
# Bring 'enable' options to the beginning of the list.
enables = [opt_name for opt_name in opt_list
@@ -1975,8 +1988,12 @@ class ExtPage(Frame):
opt_list = enables + opt_list
for opt_name in opt_list:
def_str = self.ext_defaultCfg.Get(
ext_name, opt_name, raw=True)
if opt_name in default:
def_str = self.ext_defaultCfg.Get(
ext_name, opt_name, raw=True)
else:
def_str = self.ext_userCfg.Get(
ext_name, opt_name, raw=True)
try:
def_obj = {'True':True, 'False':False}[def_str]
opt_type = 'bool'
@@ -2054,10 +2071,11 @@ class ExtPage(Frame):
default = opt['default']
value = opt['var'].get().strip() or default
opt['var'].set(value)
# if self.defaultCfg.has_section(section):
# Currently, always true; if not, indent to return.
if (value == default):
# Only save option in user config if it differs from the default
if self.ext_defaultCfg.has_section(section) and value == default:
return self.ext_userCfg.RemoveOption(section, name)
# Set the option.
return self.ext_userCfg.SetOption(section, name, value)
@@ -2116,6 +2134,8 @@ class HelpFrame(LabelFrame):
scroll_helplist['command'] = self.helplist.yview
self.helplist['yscrollcommand'] = scroll_helplist.set
self.helplist.bind('<ButtonRelease-1>', self.help_source_selected)
self.helplist.bind('<KeyRelease-Up>', self.help_source_selected)
self.helplist.bind('<KeyRelease-Down>', self.help_source_selected)
frame_buttons = Frame(self)
self.button_helplist_edit = Button(
@@ -2138,7 +2158,14 @@ class HelpFrame(LabelFrame):
self.button_helplist_remove.pack(side=TOP, anchor=W, pady=5)
def help_source_selected(self, event):
"Handle event for selecting additional help."
"""Handle event for selecting additional help.
Event can result from either mouse click or Up or Down key.
The keys move the selection, but not the anchor used by
helplist_item_edit and helplist_item_remove.
"""
if event.type.name == 'KeyRelease':
self.helplist.selection_anchor(ACTIVE)
self.set_add_delete_state()
def set_add_delete_state(self):
@@ -2257,7 +2284,11 @@ class VarTrace:
"Return default callback function to add values to changes instance."
def default_callback(*params):
"Add config values to changes instance."
changes.add_option(*config, var.get())
value = var.get()
# A blanked int entry is an empty string; do not save it as an
# invalid config value (gh-83653).
if value != '':
changes.add_option(*config, value)
return default_callback
def attach(self):
+26 -16
View File
@@ -312,6 +312,9 @@ class EditorWindow:
else:
self.update_menu_state('options', '*ine*umbers', 'disabled')
self.mtime = self.last_mtime()
text_frame.bind('<FocusIn>', self.focus_in_event)
def handle_winconfig(self, event=None):
self.set_width()
@@ -857,9 +860,8 @@ class EditorWindow:
self.text.event_delete(event, *keylist)
for extensionName in self.get_standard_extension_names():
xkeydefs = idleConf.GetExtensionBindings(extensionName)
if xkeydefs:
for event, keylist in xkeydefs.items():
self.text.event_delete(event, *keylist)
for event, keylist in xkeydefs.items():
self.text.event_delete(event, *keylist)
def ApplyKeybindings(self):
"""Apply the virtual, configurable keybindings.
@@ -1027,6 +1029,8 @@ class EditorWindow:
def set_saved(self, flag):
self.undo.set_saved(flag)
if flag:
self.mtime = self.last_mtime()
def reset_undo(self):
self.undo.reset_undo()
@@ -1112,6 +1116,21 @@ class EditorWindow:
# unless override: unregister from flist, terminate if last window
self.close_hook()
def last_mtime(self):
file = self.io.filename
return os.path.getmtime(file) if file else 0
def focus_in_event(self, event):
mtime = self.last_mtime()
if self.mtime != mtime:
self.mtime = mtime
if self. askyesno(
'Reload', '"%s"\n\nThis script has been modified by another program.'
'\nDo you want to reload it?' % self.io.filename, parent=self.text):
self.io.loadfile(self.io.filename)
else:
self.set_saved(False)
def load_extensions(self):
self.extensions = {}
self.load_standard_extensions()
@@ -1685,19 +1704,10 @@ def get_accelerator(keydefs, eventname):
return s
def fixwordbreaks(root):
# On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt.
# We want Motif style everywhere. See #21474, msg218992 and followup.
tk = root.tk
tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
tk.call('set', 'tcl_wordchars', r'\w')
tk.call('set', 'tcl_nonwordchars', r'\W')
def _editor_window(parent): # htest #
# error if close master window first - timer event, after script
root = parent
fixwordbreaks(root)
def _editor_window(root): # htest #
# Error if close master window first - timer event, after script
from util import fix_word_breaks
fix_word_breaks(root)
if sys.argv[1:]:
filename = sys.argv[1]
else:
+3 -3
View File
@@ -112,12 +112,12 @@ class FileList:
def _test(): # TODO check and convert to htest
# Maybe redundant with test_filelist.FileListTest.test_new_empty.
from tkinter import Tk
from idlelib.editor import fixwordbreaks
from idlelib.run import fix_scaling
from idlelib.util import fix_scaling, fix_word_breaks
root = Tk()
fix_scaling(root)
fixwordbreaks(root)
fix_word_breaks(root)
root.withdraw()
flist = FileList(root)
flist.new()
+15 -5
View File
@@ -40,6 +40,20 @@ def grep(text, io=None, flist=None):
dialog.open(text, searchphrase, io)
def default_glob(path):
"""Return the initial "In files:" pattern for a file path (gh-80504).
Always include a full directory so that grep output shows which
directory was searched.
"""
dir, base = os.path.split(path)
dir = os.path.abspath(dir) # An empty dir becomes the current directory.
head, tail = os.path.splitext(base)
if not tail:
tail = ".py"
return os.path.join(dir, "*" + tail)
def walk_error(msg):
"Handle os.walk error."
print(msg)
@@ -103,11 +117,7 @@ class GrepDialog(SearchDialogBase):
path = io.filename or ""
else:
path = ""
dir, base = os.path.split(path)
head, tail = os.path.splitext(base)
if not tail:
tail = ".py"
self.globvar.set(os.path.join(dir, "*" + tail))
self.globvar.set(default_glob(path))
def create_entries(self):
"Create base entry widgets and add widget for search path."
+1 -1
View File
@@ -18,7 +18,7 @@ The idle directory, idlelib, has over 60 xyz.py files. The idle_test
subdirectory contains test_xyz.py for each implementation file xyz.py.
To add a test for abc.py, open idle_test/template.py and immediately
Save As test_abc.py. Insert 'abc' on the first line, and replace
'zzdummy' with 'abc.
'zzdummy' with 'abc'.
Remove the imports of requires and tkinter if not needed. Otherwise,
add to the tkinter imports as needed.
+3 -3
View File
@@ -216,9 +216,9 @@ _module_browser_spec = {
'file': 'browser',
'kwds': {},
'msg': textwrap.dedent("""
"Inspect names of module, class(with superclass if applicable),
"methods and functions. Toggle nested items. Double clicking
"on items prints a traceback for an exception that is ignored.""")
Inspect names of module, class(with superclass if applicable),
methods and functions. Toggle nested items. Double clicking
on items prints a traceback for an exception that is ignored.""")
}
_multistatus_bar_spec = {
+3 -2
View File
@@ -1,6 +1,6 @@
"Test , coverage %."
from idlelib import zzdummy
from idlelib import zzdummy # Replace with the module to test.
import unittest
from test.support import requires
from tkinter import Tk
@@ -17,11 +17,12 @@ class Test(unittest.TestCase):
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## for id in cls.root.after_info():
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@unittest.skip('Dummy test')
def test_init(self):
self.assertTrue(True)
+7 -9
View File
@@ -230,16 +230,14 @@ class AutoCompleteTest(unittest.TestCase):
# For file completion, a large list containing all files in the path,
# and a small list containing files that do not start with '.'.
acp = self.autocomplete
small, large = acp.fetch_completions(
'', ac.ATTRS)
if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__:
self.assertNotIn('AutoComplete', small) # See issue 36405.
# Test attributes
s, b = acp.fetch_completions('', ac.ATTRS)
self.assertLess(len(small), len(large))
self.assertTrue(all(filter(lambda x: x.startswith('_'), s)))
self.assertTrue(any(filter(lambda x: x.startswith('_'), b)))
# Test current module (what='') attributes.
small, large = acp.fetch_completions('', ac.ATTRS)
if hasattr(__main__, '__file__') and __main__.__file__ != ac.__file__:
self.assertNotIn('AutoComplete', small) # See gh-80586.
self.assertLess(len(small), len(large)) # Not equal
self.assertFalse(any(a[:1] == '_' for a in small))
self.assertTrue(any(a[:1] == '_' for a in large))
# Test smalll should respect to __all__.
with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}):
+2 -2
View File
@@ -127,7 +127,7 @@ class CodeContextTest(unittest.TestCase):
timer = self.cc.t1 = self.text.after(10000, lambda: None)
self.cc.__del__()
with self.assertRaises(TclError) as cm:
self.root.tk.call('after', 'info', timer)
self.root.after_info(timer)
self.assertIn("doesn't exist", str(cm.exception))
def test_reload(self):
@@ -151,7 +151,7 @@ class CodeContextTest(unittest.TestCase):
eq(cc.context['bg'], self.highlight_cfg['background'])
eq(cc.context.get('1.0', 'end-1c'), '')
eq(cc.editwin.label, 'Hide Code Context')
eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
eq(self.root.after_info(self.cc.t1)[1], 'timer')
# Toggle off.
toggle()
+23 -5
View File
@@ -260,7 +260,7 @@ class ColorDelegatorTest(unittest.TestCase):
# Colorizing already scheduled.
save_id = color.after_id
eq(self.root.tk.call('after', 'info', save_id)[1], 'timer')
eq(self.root.after_info(save_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
@@ -277,7 +277,7 @@ class ColorDelegatorTest(unittest.TestCase):
color.notify_range('1.0', '1.0+3c')
self.assertTrue(color.stop_colorizing)
self.assertIsNotNone(color.after_id)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
eq(self.root.after_info(color.after_id)[1], 'timer')
# New event scheduled.
self.assertNotEqual(color.after_id, save_id)
@@ -297,7 +297,7 @@ class ColorDelegatorTest(unittest.TestCase):
self.assertFalse(color.colorizing)
self.assertFalse(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
eq(self.root.after_info(color.after_id)[1], 'timer')
# Toggle colorizing off.
color.toggle_colorize_event()
@@ -324,7 +324,7 @@ class ColorDelegatorTest(unittest.TestCase):
# Toggle on while colorizing not in progress.
color.colorizing = False
color.toggle_colorize_event()
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
eq(self.root.after_info(color.after_id)[1], 'timer')
self.assertFalse(color.colorizing)
self.assertTrue(color.stop_colorizing)
self.assertTrue(color.allow_colorizing)
@@ -363,7 +363,7 @@ class ColorDelegatorTest(unittest.TestCase):
mock_recmain.assert_called()
eq(mock_recmain.call_count, 1)
# Rescheduled when TODO tag still exists.
eq(self.root.tk.call('after', 'info', color.after_id)[1], 'timer')
eq(self.root.after_info(color.after_id)[1], 'timer')
# No changes to text, so no scheduling added.
text.tag_remove('TODO', '1.0', 'end')
@@ -542,6 +542,24 @@ class ColorDelegatorTest(unittest.TestCase):
self._assert_highlighting('case _:', {'KEYWORD': [('1.0', '1.4'),
('1.5', '1.6')]})
def test_lazy_soft_keyword(self): # lazy new in 3.15.
# lazy followed by import
self._assert_highlighting('lazy import foo',
{'KEYWORD': [('1.0', '1.4'),
('1.5', '1.11')]})
self._assert_highlighting(' lazy import foo',
{'KEYWORD': [('1.4', '1.8'),
('1.9', '1.15')]})
# lazy followed by from
self._assert_highlighting('lazy from foo import bar',
{'KEYWORD': [('1.0', '1.4'), ('1.5', '1.9'),
('1.14', '1.20')]})
# lazy not followed by import/from (not highlighted)
self._assert_highlighting('lazy = 1', {})
self._assert_highlighting('lazy foo', {})
def test_long_multiline_string(self):
source = textwrap.dedent('''\
"""a
+42 -16
View File
@@ -454,9 +454,6 @@ class IdleConfTest(unittest.TestCase):
self.assertEqual(idleConf.GetExtensionKeys('ZzDummy'),
{'<<z-in>>': ['<Control-Shift-KeyRelease-Insert>']})
userextn.remove_section('ZzDummy')
# need option key test
## key = ['<Option-Key-2>'] if sys.platform == 'darwin' else ['<Alt-Key-2>']
## eq(conf.GetExtensionKeys('ZoomHeight'), {'<<zoom-height>>': key})
def test_get_extension_bindings(self):
userextn.read_string('''
@@ -491,19 +488,27 @@ class IdleConfTest(unittest.TestCase):
def test_get_current_keyset(self):
current_platform = sys.platform
conf = self.mock_config()
try:
# Ensure that platform isn't darwin
sys.platform = 'some-linux'
self.assertEqual(conf.GetCurrentKeySet(),
conf.GetKeySet(conf.CurrentKeys()))
# Ensure that platform isn't darwin
sys.platform = 'some-linux'
self.assertEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys()))
# This should not be the same, since replace <Alt- to <Option-.
# Above depended on config-extensions.def having Alt keys,
# which is no longer true.
# sys.platform = 'darwin'
# self.assertNotEqual(conf.GetCurrentKeySet(), conf.GetKeySet(conf.CurrentKeys()))
# Restore platform
sys.platform = current_platform
# On darwin, '<Alt-' is replaced with '<Option-'. Add an
# extension binding, as the default key sets have no Alt keys.
conf.defaultCfg['extensions'].add_section('Foobar')
conf.defaultCfg['extensions'].add_section('Foobar_cfgBindings')
conf.defaultCfg['extensions'].set('Foobar', 'enable', 'True')
conf.defaultCfg['extensions'].set('Foobar_cfgBindings', 'newfoo',
'<Alt-Shift-Key-F12>')
self.assertEqual(conf.GetKeySet(conf.CurrentKeys())['<<newfoo>>'],
['<Alt-Shift-Key-F12>'])
sys.platform = 'darwin'
self.assertEqual(conf.GetCurrentKeySet()['<<newfoo>>'],
['<Option-Shift-Key-F12>'])
finally:
# Restore platform
sys.platform = current_platform
def test_get_keyset(self):
conf = self.mock_config()
@@ -762,8 +767,29 @@ class ChangesTest(unittest.TestCase):
changes = self.changes
changes.add_option('main', 'Indent', 'use-spaces', '1')
# save_option returns False; cfg_type_changed remains False.
self.assertFalse(changes.save_all())
self.assertFalse(usermain.has_option('Indent', 'use-spaces'))
self.assertEqual(changes, self.empty)
# TODO: test that save_all calls usercfg Saves.
def test_save_all_saves_files(self):
eq = self.assertEqual
changes = self.changes
for config in testcfg.values():
config.Save = Func()
try:
# 'main', 'highlight' and 'keys' are saved even if unchanged.
self.assertFalse(changes.save_all())
eq([testcfg[cfgtype].Save.called
for cfgtype in ('main', 'highlight', 'keys', 'extensions')],
[1, 1, 1, 0])
# A changed configuration type is saved too.
changes.add_option('extensions', 'Esec', 'eitem', 'eval')
self.assertTrue(changes.save_all())
eq(testcfg['extensions'].Save.called, 1)
finally:
for config in testcfg.values():
del config.Save
userextn.remove_section('Esec')
def test_delete_section(self):
changes = self.load()
+213 -44
View File
@@ -3,12 +3,15 @@
Half the class creates dialog, half works with user customizations.
"""
from idlelib import configdialog
from idlelib.editor import EditorWindow
from test.support import requires
requires('gui')
import unittest
from unittest import mock
from idlelib.idle_test.mock_idle import Func
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL)
from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL,
EventType)
from types import SimpleNamespace
from idlelib import config
from idlelib.configdialog import idleConf, changes, tracers
@@ -51,12 +54,27 @@ def tearDownModule():
class ConfigDialogTest(unittest.TestCase):
# The methods tested here are mocked out in the tests below.
def setUp(self):
self.parent = dialog.parent
self.instance = mock.create_autospec(EditorWindow, instance=True)
dialog.parent = mock.Mock(instance_dict={self.instance: []})
def tearDown(self):
dialog.parent = self.parent
def test_deactivate_current_config(self):
pass
dialog.deactivate_current_config()
self.instance.RemoveKeybindings.assert_called_once_with()
def activate_config_changes(self):
pass
def test_activate_config_changes(self):
dialog.activate_config_changes()
for name in ('ResetColorizer', 'ResetFont', 'set_notabs_indentwidth',
'ApplyKeybindings', 'reset_help_menu_entries',
'update_cursor_blink'):
with self.subTest(name=name):
getattr(self.instance, name).assert_called_once_with()
class ButtonTest(unittest.TestCase):
@@ -149,8 +167,8 @@ class FontPageTest(unittest.TestCase):
font = d.fontlist.get('active')
# Test Down key.
fontlist.focus_force()
fontlist.update()
fontlist.focus_force()
fontlist.event_generate('<Key-Down>')
fontlist.event_generate('<KeyRelease-Down>')
@@ -159,8 +177,8 @@ class FontPageTest(unittest.TestCase):
self.assertIn(d.font_name.get(), down_font.lower())
# Test Up key.
fontlist.focus_force()
fontlist.update()
fontlist.focus_force()
fontlist.event_generate('<Key-Up>')
fontlist.event_generate('<KeyRelease-Up>')
@@ -244,6 +262,7 @@ class FontPageTest(unittest.TestCase):
def test_set_samples(self):
d = self.page
self.addCleanup(setattr, d, 'set_samples', Func()) # Re-mask for other tests.
del d.set_samples # Unmask method for test
orig_samples = d.font_sample, d.highlight_sample
d.font_sample, d.highlight_sample = {}, {}
@@ -257,7 +276,6 @@ class FontPageTest(unittest.TestCase):
self.assertTrue(d.font_sample == d.highlight_sample == expected)
d.font_sample, d.highlight_sample = orig_samples
d.set_samples = Func() # Re-mask for other tests.
class HighPageTest(unittest.TestCase):
@@ -307,7 +325,7 @@ class HighPageTest(unittest.TestCase):
# builtinlist sets variable builtin_name to the CurrentTheme default.
eq(d.builtin_name.get(), 'IDLE Classic')
eq(d.custom_name.get(), '- no custom themes -')
eq(d.custom_theme_on.state(), ('disabled',))
self.assertIn('disabled', d.custom_theme_on.state())
eq(d.set_theme_type.called, 1)
eq(d.paint_theme_sample.called, 1)
eq(d.set_highlight_target.called, 1)
@@ -495,6 +513,7 @@ class HighPageTest(unittest.TestCase):
def test_set_theme_type(self):
eq = self.assertEqual
d = self.page
self.addCleanup(setattr, d, 'set_theme_type', Func()) # Re-mask method.
del d.set_theme_type
# Builtin theme selected.
@@ -502,16 +521,15 @@ class HighPageTest(unittest.TestCase):
d.set_theme_type()
eq(d.builtinlist['state'], NORMAL)
eq(d.customlist['state'], DISABLED)
eq(d.button_delete_custom.state(), ('disabled',))
self.assertIn('disabled', d.button_delete_custom.state())
# Custom theme selected.
d.theme_source.set(False)
d.set_theme_type()
eq(d.builtinlist['state'], DISABLED)
eq(d.custom_theme_on.state(), ('selected',))
self.assertNotIn('disabled', d.custom_theme_on.state())
eq(d.customlist['state'], NORMAL)
eq(d.button_delete_custom.state(), ())
d.set_theme_type = Func()
self.assertNotIn('disabled', d.button_delete_custom.state())
def test_get_color(self):
eq = self.assertEqual
@@ -637,24 +655,23 @@ class HighPageTest(unittest.TestCase):
def test_set_highlight_target(self):
eq = self.assertEqual
d = self.page
self.addCleanup(setattr, d, 'set_highlight_target', Func()) # Re-mask method.
del d.set_highlight_target
# Target is cursor.
d.highlight_target.set('Cursor')
eq(d.fg_on.state(), ('disabled', 'selected'))
eq(d.bg_on.state(), ('disabled',))
self.assertIn('disabled', d.fg_on.state())
self.assertIn('disabled', d.bg_on.state())
self.assertTrue(d.fg_bg_toggle)
eq(d.set_color_sample.called, 1)
# Target is not cursor.
d.highlight_target.set('Comment')
eq(d.fg_on.state(), ('selected',))
eq(d.bg_on.state(), ())
self.assertNotIn('disabled', d.fg_on.state())
self.assertNotIn('disabled', d.bg_on.state())
self.assertTrue(d.fg_bg_toggle)
eq(d.set_color_sample.called, 2)
d.set_highlight_target = Func()
def test_set_color_sample_binding(self):
d = self.page
scs = d.set_color_sample
@@ -667,6 +684,7 @@ class HighPageTest(unittest.TestCase):
def test_set_color_sample(self):
d = self.page
self.addCleanup(setattr, d, 'set_color_sample', Func()) # Re-mask method.
del d.set_color_sample
d.highlight_target.set('Selected Text')
d.fg_bg_toggle.set(True)
@@ -674,7 +692,6 @@ class HighPageTest(unittest.TestCase):
self.assertEqual(
d.style.lookup(d.frame_color_set['style'], 'background'),
d.highlight_sample.tag_cget('hilite', 'foreground'))
d.set_color_sample = Func()
def test_paint_theme_sample(self):
eq = self.assertEqual
@@ -745,7 +762,7 @@ class HighPageTest(unittest.TestCase):
eq(yesno.called, 2)
self.assertNotIn(theme_name, highpage)
eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2])
eq(d.custom_theme_on.state(), ())
self.assertNotIn('disabled', d.custom_theme_on.state())
eq(d.custom_name.get(), theme_name2)
eq(dialog.deactivate_current_config.called, 1)
eq(dialog.activate_config_changes.called, 1)
@@ -758,7 +775,7 @@ class HighPageTest(unittest.TestCase):
eq(yesno.called, 3)
self.assertNotIn(theme_name, highpage)
eq(idleConf.GetSectionList('user', 'highlight'), [])
eq(d.custom_theme_on.state(), ('disabled',))
self.assertIn('disabled', d.custom_theme_on.state())
eq(d.custom_name.get(), '- no custom themes -')
eq(dialog.deactivate_current_config.called, 2)
eq(dialog.activate_config_changes.called, 2)
@@ -809,7 +826,7 @@ class KeysPageTest(unittest.TestCase):
# builtinlist sets variable builtin_name to the CurrentKeys default.
eq(d.builtin_name.get(), 'IDLE Classic OSX')
eq(d.custom_name.get(), '- no custom keys -')
eq(d.custom_keyset_on.state(), ('disabled',))
self.assertIn('disabled', d.custom_keyset_on.state())
eq(d.set_keys_type.called, 1)
eq(d.load_keys_list.called, 1)
eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
@@ -930,6 +947,7 @@ class KeysPageTest(unittest.TestCase):
def test_set_keys_type(self):
eq = self.assertEqual
d = self.page
self.addCleanup(setattr, d, 'set_keys_type', Func()) # Re-mask method.
del d.set_keys_type
# Builtin keyset selected.
@@ -937,16 +955,15 @@ class KeysPageTest(unittest.TestCase):
d.set_keys_type()
eq(d.builtinlist['state'], NORMAL)
eq(d.customlist['state'], DISABLED)
eq(d.button_delete_custom_keys.state(), ('disabled',))
self.assertIn('disabled', d.button_delete_custom_keys.state())
# Custom keyset selected.
d.keyset_source.set(False)
d.set_keys_type()
eq(d.builtinlist['state'], DISABLED)
eq(d.custom_keyset_on.state(), ('selected',))
self.assertNotIn('disabled', d.custom_keyset_on.state())
eq(d.customlist['state'], NORMAL)
eq(d.button_delete_custom_keys.state(), ())
d.set_keys_type = Func()
self.assertNotIn('disabled', d.button_delete_custom_keys.state())
def test_get_new_keys(self):
eq = self.assertEqual
@@ -1043,7 +1060,15 @@ class KeysPageTest(unittest.TestCase):
b.event_generate('<Button-1>', x=x, y=y)
b.event_generate('<ButtonRelease-1>', x=x, y=y)
self.assertEqual(b.get('anchor'), 'find')
self.assertEqual(d.button_new_keys.state(), ())
self.assertNotIn('disabled', d.button_new_keys.state())
# gh-75234: Up and Down keys move the active item, but not the
# anchor; the handler moves the anchor.
d.button_new_keys.state(('disabled',))
b.activate(0)
d.on_bindingslist_select(SimpleNamespace(type=EventType.KeyRelease))
self.assertEqual(b.get('anchor'), 'copy')
self.assertNotIn('disabled', d.button_new_keys.state())
def test_create_new_key_set_and_save_new_key_set(self):
eq = self.assertEqual
@@ -1080,6 +1105,7 @@ class KeysPageTest(unittest.TestCase):
eq = self.assertEqual
d = self.page
gks = idleConf.GetKeySet = Func()
self.addCleanup(setattr, d, 'load_keys_list', Func()) # Re-mask method.
del d.load_keys_list
b = d.bindingslist
@@ -1094,11 +1120,14 @@ class KeysPageTest(unittest.TestCase):
'force-open-completions - <Control-Key-space>',
'spam - <Shift-Key-a>')
# No current selection.
# No current selection: select the first item.
d.button_new_keys.state(('disabled',))
d.load_keys_list('my keys')
eq(b.get(0, 'end'), expected)
eq(b.get('anchor'), '')
eq(b.curselection(), ())
eq(b.get('anchor'), 'copy - <Control-Key-c> <Control-Key-C>')
eq(b.curselection(), (0, ))
eq(b.index('active'), 0)
self.assertNotIn('disabled', d.button_new_keys.state())
# Check selection.
b.selection_set(1)
@@ -1115,7 +1144,6 @@ class KeysPageTest(unittest.TestCase):
eq(b.get(0, 'end'), expected)
eq(b.get('anchor'), 'spam - <Shift-Key-a>')
eq(b.curselection(), (2, ))
d.load_keys_list = Func()
del idleConf.GetKeySet
@@ -1156,7 +1184,7 @@ class KeysPageTest(unittest.TestCase):
eq(yesno.called, 2)
self.assertNotIn(keyset_name, keyspage)
eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2])
eq(d.custom_keyset_on.state(), ())
self.assertNotIn('disabled', d.custom_keyset_on.state())
eq(d.custom_name.get(), keyset_name2)
eq(dialog.deactivate_current_config.called, 1)
eq(dialog.activate_config_changes.called, 1)
@@ -1169,7 +1197,7 @@ class KeysPageTest(unittest.TestCase):
eq(yesno.called, 3)
self.assertNotIn(keyset_name, keyspage)
eq(idleConf.GetSectionList('user', 'keys'), [])
eq(d.custom_keyset_on.state(), ('disabled',))
self.assertIn('disabled', d.custom_keyset_on.state())
eq(d.custom_name.get(), '- no custom keys -')
eq(dialog.deactivate_current_config.called, 2)
eq(dialog.activate_config_changes.called, 2)
@@ -1296,13 +1324,123 @@ class ShedPageTest(unittest.TestCase):
self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}})
#unittest.skip("Nothing here yet TODO")
class ExtPageTest(unittest.TestCase):
"""Test that the help source list works correctly."""
"""Test that the extension page works correctly.
The page loads the options of each extension from the default and
user config files, displays those of the selected extension, and
saves the changed ones to the user config file. ZzDummy is the
only extension shipped with IDLE.
"""
@classmethod
def setUpClass(cls):
page = dialog.extpage
page = cls.page = dialog.extpage
dialog.note.select(page)
page.update()
def setUp(self):
# Restore the option vars changed by the previous test.
self.page.load_extensions()
def tearDown(self):
self.page.ext_userCfg.remove_section('ZzDummy')
def test_load_extensions(self):
eq = self.assertEqual
extensions = self.page.extensions
eq(list(extensions), sorted(idleConf.GetExtensions(active_only=False)))
opts = extensions['ZzDummy']
# The 'enable' options come first, the others follow, both sorted.
eq([opt['name'] for opt in opts],
['enable', 'enable_editor', 'enable_shell', 'z-text'])
eq([opt['type'] for opt in opts], ['bool', 'bool', 'bool', None])
eq([opt['default'] for opt in opts], ['False', 'True', 'False', 'Z'])
eq([opt['value'] for opt in opts], [False, True, False, 'Z'])
for opt in opts:
with self.subTest(name=opt['name']):
eq(opt['var'].get(), str(opt['value']))
def test_load_extensions_enable_first(self):
# The 'enable' options come first even if they sort last.
page = self.page
page.ext_userCfg.SetOption('ZzDummy', 'a-text', 'A')
page.load_extensions()
self.assertEqual([opt['name'] for opt in page.extensions['ZzDummy']],
['enable', 'enable_editor', 'enable_shell',
'a-text', 'z-text'])
def test_load_extensions_user_value(self):
# A user option overrides the default value, but not the default.
page = self.page
page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user')
page.load_extensions()
opt = page.extensions['ZzDummy'][-1]
self.assertEqual(opt['name'], 'z-text')
self.assertEqual(opt['default'], 'Z')
self.assertEqual(opt['value'], 'user')
self.assertEqual(opt['var'].get(), 'user')
def test_extension_selected(self):
eq = self.assertEqual
page = self.page
frame = page.config_frame['ZzDummy']
# Deselecting hides the options of the current extension.
page.extension_list.selection_clear(0, 'end')
page.extension_selected(None)
eq(page.current_extension, None)
eq(page.details_frame.cget('text'), '')
eq(frame.winfo_manager(), '')
# Selecting shows the options of the selected extension.
page.extension_list.selection_set(0)
page.extension_selected(None)
eq(page.current_extension, 'ZzDummy')
eq(page.details_frame.cget('text'), 'ZzDummy')
eq(frame.winfo_manager(), 'grid')
def test_set_extension_value_changed(self):
page = self.page
opt = page.extensions['ZzDummy'][-1] # z-text, default 'Z'.
opt['var'].set('user')
self.assertTrue(page.set_extension_value('ZzDummy', opt))
self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user')
# Saving the same value again is not a change.
self.assertFalse(page.set_extension_value('ZzDummy', opt))
def test_set_extension_value_default(self):
page = self.page
opt = page.extensions['ZzDummy'][-1]
# The default value is not saved in the user config file.
opt['var'].set('Z')
self.assertFalse(page.set_extension_value('ZzDummy', opt))
self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text'))
# Setting it back to the default removes the user option.
page.ext_userCfg.SetOption('ZzDummy', 'z-text', 'user')
self.assertTrue(page.set_extension_value('ZzDummy', opt))
self.assertFalse(page.ext_userCfg.has_option('ZzDummy', 'z-text'))
def test_set_extension_value_empty(self):
# An empty value is replaced with the default.
page = self.page
opt = page.extensions['ZzDummy'][-1]
opt['var'].set(' ')
self.assertFalse(page.set_extension_value('ZzDummy', opt))
self.assertEqual(opt['var'].get(), 'Z')
def test_save_all_changed_extensions(self):
page = self.page
page.ext_userCfg.Save = Func()
try:
# Nothing is saved if nothing is changed.
page.save_all_changed_extensions()
self.assertEqual(page.ext_userCfg.Save.called, 0)
page.extensions['ZzDummy'][0]['var'].set('True')
page.extensions['ZzDummy'][-1]['var'].set('user')
page.save_all_changed_extensions()
self.assertEqual(page.ext_userCfg.Save.called, 1)
self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'enable'), 'True')
self.assertEqual(page.ext_userCfg.Get('ZzDummy', 'z-text'), 'user')
finally:
del page.ext_userCfg.Save
class HelpSourceTest(unittest.TestCase):
@@ -1365,25 +1503,25 @@ class HelpSourceTest(unittest.TestCase):
# Call with 0 items, 1 unselected item, 1 selected item.
eq = self.assertEqual
fr = self.frame
self.addCleanup(setattr, fr, 'set_add_delete_state', Func()) # Re-mask method.
del fr.set_add_delete_state # Unmask method.
sad = fr.set_add_delete_state
h = fr.helplist
h.delete(0, 'end')
sad()
eq(fr.button_helplist_edit.state(), ('disabled',))
eq(fr.button_helplist_remove.state(), ('disabled',))
self.assertIn('disabled', fr.button_helplist_edit.state())
self.assertIn('disabled', fr.button_helplist_remove.state())
h.insert(0, 'source')
sad()
eq(fr.button_helplist_edit.state(), ('disabled',))
eq(fr.button_helplist_remove.state(), ('disabled',))
self.assertIn('disabled', fr.button_helplist_edit.state())
self.assertIn('disabled', fr.button_helplist_remove.state())
h.selection_set(0)
sad()
eq(fr.button_helplist_edit.state(), ())
eq(fr.button_helplist_remove.state(), ())
fr.set_add_delete_state = Func() # Mask method.
self.assertNotIn('disabled', fr.button_helplist_edit.state())
self.assertNotIn('disabled', fr.button_helplist_remove.state())
def test_helplist_item_add(self):
# Call without and twice with HelpSource result.
@@ -1459,8 +1597,29 @@ class HelpSourceTest(unittest.TestCase):
eq(fr.user_helplist, [])
self.assertTrue(fr.upc.called == fr.set.called == 1)
def test_helplist_item_remove_keyboard_selection(self):
# gh-75234: Up and Down keys move the active item, but not the
# anchor; the handler moves the anchor.
eq = self.assertEqual
fr = self.frame
fr.helplist.delete(0, 'end')
fr.helplist.insert('end', 'name1', 'name2')
fr.helplist.selection_anchor(0)
fr.helplist.selection_set(1)
fr.helplist.activate(1)
fr.user_helplist.clear()
fr.user_helplist.extend([('name1', 'file1'), ('name2', 'file2')])
fr.set.called = fr.upc.called = 0
fr.help_source_selected(SimpleNamespace(type=EventType.KeyRelease))
eq(fr.helplist.get('anchor'), 'name2')
fr.helplist_item_remove()
eq(fr.helplist.get(0, 'end'), ('name1',))
eq(fr.user_helplist, [('name1', 'file1')])
def test_update_help_changes(self):
fr = self.frame
self.addCleanup(setattr, fr, 'update_help_changes', Func()) # Re-mask method.
del fr.update_help_changes
fr.user_helplist.clear()
fr.user_helplist.append(('name1', 'file1'))
@@ -1469,7 +1628,6 @@ class HelpSourceTest(unittest.TestCase):
fr.update_help_changes()
self.assertEqual(mainpage['HelpFiles'],
{'1': 'name1;file1', '2': 'name2;file2'})
fr.update_help_changes = Func()
class VarTraceTest(unittest.TestCase):
@@ -1544,6 +1702,17 @@ class VarTraceTest(unittest.TestCase):
self.assertEqual(changes['main']['section']['option'], '42')
changes.clear()
# gh-83653: a blank int entry is not saved as bad config data.
sv = StringVar(root)
cb = self.tracers.make_callback(sv, ('main', 'section', 'option'))
sv.set('')
cb()
self.assertNotIn('section', changes['main'])
sv.set('5')
cb()
self.assertEqual(changes['main']['section']['option'], '5')
changes.clear()
def test_attach_detach(self):
tr = self.tracers
iv = tr.add(self.iv, self.var_changed_increment)
+2 -2
View File
@@ -7,7 +7,7 @@ requires('gui')
import tkinter as tk
from tkinter import ttk
import unittest
from idlelib import pyshell
from idlelib.util import fix_x11_paste
class PasteTest(unittest.TestCase):
'''Test pasting into widgets that allow pasting.
@@ -18,7 +18,7 @@ class PasteTest(unittest.TestCase):
def setUpClass(cls):
cls.root = root = tk.Tk()
cls.root.withdraw()
pyshell.fix_x11_paste(root)
fix_x11_paste(root)
cls.text = tk.Text(root)
cls.entry = tk.Entry(root)
cls.tentry = ttk.Entry(root)
+57 -6
View File
@@ -20,7 +20,7 @@ class EditorWindowTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
@@ -114,7 +114,7 @@ class IndentAndNewlineTest(unittest.TestCase):
cls.window._close()
del cls.window
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
@@ -212,6 +212,9 @@ class IndentSearcherTest(unittest.TestCase):
class RMenuTest(unittest.TestCase):
# Test selection-rclick interaction in right_click_event and
# rmenu_check_copy(cut) status settings. These are part of the rmenu
# functions common to all text windows with context windows.
@classmethod
def setUpClass(cls):
@@ -219,13 +222,15 @@ class RMenuTest(unittest.TestCase):
cls.root = Tk()
cls.root.withdraw()
cls.window = Editor(root=cls.root)
cls.text = cls.window.text
cls.window.rmenu = cls.DummyRMenu
@classmethod
def tearDownClass(cls):
cls.window._close()
del cls.window
del cls.window, cls.text
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
@@ -233,8 +238,54 @@ class RMenuTest(unittest.TestCase):
class DummyRMenu:
def tk_popup(x, y): pass
def test_rclick(self):
pass
def click(self):
"""Simulate a right click at(text pixel 0,0).
"""
Event = namedtuple('Event', ['x', 'y', 'x_root', 'y_root'])
event = Event(0, 0, 0, 0)
self.assertEqual(self.window.right_menu_event(event), 'break')
# This assertion should be moved to a new method that also
# text that dummy rmenu.tk_popup is called.
def test_rclick_not_in_a_selection(self):
# Like left click, 'insert' moves to click and any selection is deleted.
eq = self.assertEqual
text = self.text
insert(text, 'one two three')
# Selection exists but not clicked.
text.tag_add('sel', '1.4', '1.7') # 'two' selected.
text.mark_set('insert', '1.7') # Outside of selection.
self.click()
eq(text.tag_ranges('sel'), ())
eq(text.index('insert'), '1.0')
# No selection to click, same result.
text.mark_set('insert', '1.8')
index = self.click()
eq(text.tag_ranges('sel'), ())
eq(text.index('insert'), '1.0')
def test_rclick_inside_selection(self):
# Unlike left click, selection is not deleted.
eq = self.assertEqual
text = self.text
insert(text, 'one two three') # 'insert' at 1.13.
# The selection contains the clicked character.
text.tag_add('sel', '1.0', '1.3') # Select 'one'.
text.mark_set('insert', '1.3') # If select rightward, 'insert' at 1.3.
self.click()
eq((text.index('sel.first'), text.index('sel.last')), ('1.0', '1.3'))
eq(text.index('insert'), '1.3')
def test_rmenu_check_copy(self):
# copy and cut only valid for click inside selection.
eq = self.assertEqual
text = self.text
insert(text, 'one two three')
eq(self.window.rmenu_check_copy(), 'disabled')
eq(self.window.rmenu_check_cut(), 'disabled')
text.tag_add('sel', '1.0', '1.3') # Includes '1.0' click.
eq(self.window.rmenu_check_copy(), 'normal')
eq(self.window.rmenu_check_cut(), 'normal')
if __name__ == '__main__':
+1 -1
View File
@@ -16,7 +16,7 @@ class FileListTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id)
cls.root.destroy()
del cls.root
+78 -10
View File
@@ -1,16 +1,19 @@
""" !Changing this line will break Test_findfile.test_found!
Non-gui unit tests for grep.GrepDialog methods.
dummy_command calls grep_it calls findfiles.
default_command calls grep_it calls findfiles.
An exception raised in one method will fail callers.
Otherwise, tests are mostly independent.
Currently only test grep_it, coverage 51%.
Currently test default_command and grep_it, coverage 51%.
"""
from idlelib import grep
import unittest
from unittest import mock
from test.support import captured_stdout
from idlelib.idle_test.mock_tk import Var
import io
import os
import re
import sys
class Dummy_searchengine:
@@ -21,22 +24,48 @@ class Dummy_searchengine:
def getpat(self):
return self._pat
def getprog(self):
return self._prog
searchengine = Dummy_searchengine()
class Dummy_grep:
# Methods tested
#default_command = GrepDialog.default_command
# Simplifications: 1. Don't initialize superclass SearchEngineBase
# with searchengine. 2. Use directly set class vars instead of
# instance vars set from the GUI.
globvar = Var('') # File name.
recvar = Var(False) # Recurse down directories?
engine = searchengine # Pattern and flags.
# Methods tested.
grep_it = grep.GrepDialog.grep_it
# Other stuff needed
recvar = Var(False)
engine = searchengine
default_command = grep.GrepDialog.default_command # Uses grep_it.
def close(self): # gui method
pass
_grep = Dummy_grep()
class DefaultGlobTest(unittest.TestCase):
def test_no_path(self):
# gh-80504: an unsaved editor or the Shell has no path, so the
# pattern uses the current directory, not just '*.py'.
self.assertEqual(grep.default_glob(''),
os.path.join(os.getcwd(), '*.py'))
def test_full_path(self):
path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.py')
self.assertEqual(grep.default_glob(path),
os.path.join(os.path.dirname(path), '*.py'))
def test_other_extension(self):
path = os.path.join(os.path.abspath(os.sep), 'ab', 'foo.txt')
self.assertEqual(grep.default_glob(path),
os.path.join(os.path.dirname(path), '*.txt'))
class FindfilesTest(unittest.TestCase):
@classmethod
@@ -147,9 +176,48 @@ class Grep_itTest(unittest.TestCase):
class Default_commandTest(unittest.TestCase):
# To write this, move outwin import to top of GrepDialog
# so it can be replaced by captured_stdout in class setup/teardown.
pass
# default_command searches with grep_it, tested above, writing to an
# OutputWindow, which is replaced here by a text buffer.
def setUp(self):
self.dialog = Dummy_grep()
self.dialog.top = mock.Mock() # For top.bell().
self.dialog.flist = 'flist'
self.dialog.globvar = Var(value=__file__)
searchengine._pat = pat = 'xyz*' * 7 # Not in this file.
searchengine._prog = re.compile(pat)
def default_command(self):
"Return the mock OutputWindow class and what was written to it."
save = sys.stdout
with mock.patch('idlelib.outwin.OutputWindow') as OutputWindow:
OutputWindow.return_value = out = io.StringIO()
self.dialog.default_command()
self.assertIs(sys.stdout, save) # Restored even when not replaced.
return OutputWindow, out.getvalue()
def test_no_pattern(self):
# An invalid pattern is reported by getprog, not here.
searchengine._prog = None
OutputWindow, output = self.default_command()
OutputWindow.assert_not_called()
self.assertEqual(output, '')
self.dialog.top.bell.assert_not_called()
def test_no_path(self):
self.dialog.globvar = Var(value='')
OutputWindow, output = self.default_command()
self.dialog.top.bell.assert_called_once()
OutputWindow.assert_not_called()
self.assertEqual(output, '')
def test_search(self):
OutputWindow, output = self.default_command()
OutputWindow.assert_called_once_with('flist') # The flist argument.
lines = output.split('\n')
self.assertIn(searchengine._pat, lines[0])
self.assertEqual(lines[1], 'No hits.')
self.dialog.top.bell.assert_not_called()
if __name__ == '__main__':
-1
View File
@@ -4,7 +4,6 @@ from idlelib import help
import unittest
from test.support import requires
requires('gui')
from os.path import abspath, dirname, join
from tkinter import Tk
+17 -5
View File
@@ -1,11 +1,13 @@
"Test , coverage 17%."
from idlelib import iomenu
import os
import unittest
from unittest import mock
from test.support import requires
from tkinter import Tk
from tkinter import Tk, TclVersion
from idlelib.editor import EditorWindow
from idlelib import util
from idlelib import macosx, util
from idlelib.idle_test.mock_idle import Func
# Fail if either tokenize.open and t.detect_encoding does not exist.
@@ -23,15 +25,14 @@ class IOBindingTest(unittest.TestCase):
cls.root = Tk()
cls.root.withdraw()
cls.editwin = EditorWindow(root=cls.root)
cls.io = iomenu.IOBinding(cls.editwin)
cls.io = cls.editwin.io
@classmethod
def tearDownClass(cls):
cls.io.close()
cls.editwin._close()
del cls.editwin
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -39,6 +40,17 @@ class IOBindingTest(unittest.TestCase):
def test_init(self):
self.assertIs(self.io.editwin, self.editwin)
def test_defaultfilename_tilde(self):
# gh-59568: escape a leading tilde for Tcl older than 9.
io = self.io
with mock.patch.object(io, 'filename', os.path.join('dir', '~file.py')):
dirname, base = io.defaultfilename()
self.assertEqual(dirname, 'dir')
if TclVersion < 9 and not macosx.isAquaTk():
self.assertEqual(base, os.path.join(os.curdir, '~file.py'))
else:
self.assertEqual(base, '~file.py')
def test_fixnewlines_end(self):
eq = self.assertEqual
io = self.io
+1
View File
@@ -22,6 +22,7 @@ class MainMenuTest(unittest.TestCase):
# On failure, change pattern here and in function(s).
# Patterns here have '.*' for re instead of '*' for tcl.
for menu, pattern in (
('debug', '.*ebugger'), # PyShell.debug_menu_postcommand
('debug', '.*tack.*iewer'), # PyShell.debug_menu_postcommand
('options', '.*ode.*ontext'), # EW.__init__, CodeContext.toggle...
('options', '.*ine.*umbers'), # EW.__init__, EW.toggle...event.
+48 -2
View File
@@ -2,7 +2,7 @@
from idlelib import multicall
import unittest
from test.support import requires
from test.support import requires, captured_stderr
from tkinter import Tk, Text
@@ -19,7 +19,7 @@ class MultiCallTest(unittest.TestCase):
def tearDownClass(cls):
del cls.mc
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## for id in cls.root.after_info():
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -43,6 +43,52 @@ class MultiCallTest(unittest.TestCase):
mctext = self.mc(self.root)
self.assertIs(mctext.yview.__func__, Text.yview)
def test_valid_binding(self):
# A valid key binding must bind without a warning (cf. gh-55646).
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-good>>', '<Control-Key-Up>')
mctext.bind('<<test-good>>', lambda e: None)
self.assertEqual(stderr.getvalue(), '')
def test_invalid_triplet_binding(self):
# gh-55646: '<Control-Key-up>' parses into a triplet on every platform,
# so Tk rejects it in bind().
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-bad>>', '<Control-Key-up>') # Must not raise.
mctext.bind('<<test-bad>>', lambda e: None) # Must not raise.
warning = stderr.getvalue()
self.assertIn('invalid key binding', warning)
self.assertIn('test-bad', warning) # The offending action is named.
def test_invalid_nontriplet_binding(self):
# gh-55646: '<Foo-Key-Up>' has no valid modifier, so MultiCall does not
# parse it and falls back to Tk's event_add, which rejects it.
mctext = self.mc(self.root)
with captured_stderr() as stderr:
mctext.event_add('<<test-bad>>', '<Foo-Key-Up>') # Must not raise.
mctext.bind('<<test-bad>>', lambda e: None) # Must not raise.
warning = stderr.getvalue()
self.assertIn('invalid key binding', warning)
self.assertIn('test-bad', warning) # The offending action is named.
def test_event_delete_unbound_sequence(self):
# gh-89360: deleting a sequence that was not added to a virtual
# event is ignored instead of raising ValueError.
mctext = self.mc(self.root)
mctext.event_add('<<tester>>', '<Control-Key-a>')
info = mctext.event_info('<<tester>>')
self.assertEqual(len(info), 1)
# A different sequence, never added: a no-op, not an error.
mctext.event_delete('<<tester>>', '<Control-Key-b>')
self.assertEqual(mctext.event_info('<<tester>>'), info)
# The added sequence can still be deleted normally.
mctext.event_delete('<<tester>>', '<Control-Key-a>')
self.assertNotIn(info[0], mctext.event_info('<<tester>>'))
if __name__ == '__main__':
unittest.main(verbosity=2)
+11
View File
@@ -41,6 +41,17 @@ class OutputWindowTest(unittest.TestCase):
self.assertFalse(w.ispythonsource('test.txt'))
self.assertFalse(w.ispythonsource(__file__))
def test_save_defaults_to_text(self):
# gh-65339: output is saved as text, not Python source.
io = self.window.io
self.assertEqual(io.defaultextension, '.txt')
# Text files are offered before Python files.
self.assertEqual(io.save_filetypes[0][0], 'Text files')
def test_open_defaults_to_python(self):
# gh-158067: File Open still lists Python files first.
self.assertEqual(self.window.io.filetypes[0][0], 'Python files')
def test_window_title(self):
self.assertEqual(self.window.top.title(), 'Output' + ' (%s)' % platform.python_version())
+50 -1
View File
@@ -2,6 +2,7 @@
# Plus coverage of test_warning. Was 20% with test_openshell.
from idlelib import pyshell
import os
import unittest
from test.support import requires
from tkinter import Tk
@@ -28,6 +29,14 @@ class FunctionTest(unittest.TestCase):
self.assertEqual(pyshell.restart_line(width, ''), expect)
self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =')
def test_fix_user_path(self):
# gh-134300: the idlelib directory is removed, other entries kept.
eq = self.assertEqual
idlelib_dir = os.path.dirname(os.path.abspath(pyshell.__file__))
eq(pyshell.fix_user_path(['', '/a', idlelib_dir, '/b']), ['', '/a', '/b'])
eq(pyshell.fix_user_path(['/a', '/b']), ['/a', '/b'])
eq(pyshell.fix_user_path([idlelib_dir]), [])
class PyShellFileListTest(unittest.TestCase):
@@ -40,7 +49,7 @@ class PyShellFileListTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
#cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## for id in cls.root.after_info():
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -60,6 +69,46 @@ class PyShellFileListTest(unittest.TestCase):
## self.assertIsInstance(ps, pyshell.PyShell)
class PyShellTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
requires('gui')
cls.root = Tk()
cls.root.withdraw()
cls.shell = pyshell.PyShell(pyshell.PyShellFileList(cls.root))
@classmethod
def tearDownClass(cls):
cls.shell.close()
del cls.shell
cls.root.destroy()
del cls.root
def setUp(self):
text = self.shell.text
self.shell.per.bottom.delete('1.0', 'end')
text.mark_set('iomark', '1.0') # As after begin().
text.mark_gravity('iomark', 'left')
self.shell.undo.reset_undo()
def test_output_at_prompt(self):
# gh-75512: output from a thread while a prompt is shown goes
# before the prompt, which stays with the input in progress.
shell = self.shell
text = shell.text
shell.write('x\n', 'stdout')
shell.resetoutput()
text.tag_add('console', 'iomark-1c') # As showprompt() does.
text.insert('end-1c', 'a = (')
shell.write('hel', 'stdout')
shell.write('lo\n', 'stdout')
self.assertEqual(text.get('iomark-6c', 'end-1c'), 'hello\na = (')
self.assertIn('console', text.tag_names('iomark-1c'))
self.assertNotIn('console', text.tag_names('iomark-7c'))
self.assertEqual(shell.shell_sidebar.line_prompts, {3: '>>>'})
class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase):
regexp = pyshell.PyShell._last_newline_re
+81 -5
View File
@@ -246,32 +246,108 @@ class ReplaceDialogTest(unittest.TestCase):
equal(text.get('1.2', '1.5'), 'was')
def test_replace_all(self):
# The default mode, forward with wrap around, replaces every
# match, both below and above the current position.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
text.insert('insert', '\n')
text.insert('insert', text.get('1.0', 'end')*100)
pv.set('is')
rv.set('was')
text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
self.assertNotIn('is', text.get('1.0', 'end'))
equal(text.get('1.0', '3.end'), 'b\nb\nb') # Wrapped around.
# An empty regular expression is reported as an error.
self.engine.revar.set(True)
pv.set('')
replace_all()
self.assertIn('error', showerror.title)
self.assertIn('Empty', showerror.message)
# An invalid replacement expression is reported as an error,
# and nothing is replaced.
text.delete('1.0', 'end')
text.insert('1.0', 'asT')
pv.set('[s][T]')
rv.set('\\')
replace_all()
self.assertIn('error', showerror.title)
self.assertIn('Invalid Replace Expression', showerror.message)
equal(text.get('1.0', '1.end'), 'asT')
# A pattern that is not present replaces nothing.
self.engine.revar.set(False)
text.delete('1.0', 'end')
text.insert('1.0', 'unchanged')
pv.set('text which is not present')
rv.set('foobar')
replace_all()
equal(text.get('1.0', '1.end'), 'unchanged')
def test_replace_all_backwards_no_wrap(self):
# gh-71956: 'up' without wrap replaces all matches from the start
# of the text down to the current position, not just one up.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(True)
self.engine.wrapvar.set(False)
text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '1.end'), 'b') # Above the cursor.
equal(text.get('2.0', '2.end'), 'b') # At the cursor.
equal(text.get('3.0', '3.end'), 'a') # Below the cursor, untouched.
def test_replace_all_forwards_no_wrap(self):
# 'down' without wrap replaces all matches from the current
# position to the end of the text, and none before it.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(False)
self.engine.wrapvar.set(False)
text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '1.end'), 'a') # Before the cursor, untouched.
equal(text.get('2.0', '2.end'), 'a') # Before the cursor, untouched.
equal(text.get('3.0', '3.end'), 'b') # After the cursor.
def test_replace_all_backwards_wrap(self):
# With wrap around, an 'up' search also replaces every match.
equal = self.assertEqual
text = self.text
pv = self.engine.patvar
rv = self.dialog.replvar
replace_all = self.dialog.replace_all
self.engine.backvar.set(True)
self.engine.wrapvar.set(True)
text.delete('1.0', 'end')
text.insert('1.0', 'a\na\na\n')
text.mark_set('insert', '2.1')
pv.set('a')
rv.set('b')
replace_all()
equal(text.get('1.0', '3.end'), 'b\nb\nb')
def test_default_command(self):
text = self.text
+19
View File
@@ -1,9 +1,28 @@
"Test rpc, coverage 20%."
from idlelib import rpc
import socket
import struct
import unittest
class SocketIOTest(unittest.TestCase):
def test_reconnect_discards_partial_packet(self):
# gh-89544: RPCClient.accept() reinitializes the SocketIO after
# a restart; a partially received packet must not be kept.
old_sock, old_peer = socket.socketpair()
new_sock, new_peer = socket.socketpair()
with old_sock, old_peer, new_sock, new_peer:
sockio = rpc.SocketIO(old_sock)
old_peer.sendall(struct.pack('<i', 100) + b'x' * 10)
self.assertIsNone(sockio.pollpacket(1))
sockio.close()
rpc.SocketIO.__init__(sockio, new_sock)
new_peer.sendall(struct.pack('<i', 3) + b'abc')
self.assertEqual(sockio.pollpacket(1), b'abc')
class CodePicklerTest(unittest.TestCase):
+94 -1
View File
@@ -44,7 +44,7 @@ class ExceptionTest(unittest.TestCase):
"Or did you forget to import 'abc'?\n"),
('int.reel', AttributeError,
"type object 'int' has no attribute 'reel'. "
"Did you mean: 'real'?\n"),
"Did you mean '.real' instead of '.reel'?\n"), # More in 3.15.
)
@force_not_colorized
@@ -82,6 +82,99 @@ class ExceptionTest(unittest.TestCase):
subtests += 1
self.assertEqual(subtests, len(data2)) # All subtests ran?
def _capture_exception(self):
"""Call run.print_exception() and return its stderr output."""
with captured_stderr() as output:
with mock.patch.object(run, 'cleanup_traceback') as ct:
ct.side_effect = lambda t, e: t
run.print_exception()
return output.getvalue()
@force_not_colorized
def test_print_exception_group_nested(self):
try:
try:
raise ExceptionGroup('inner', [ValueError('v1')])
except ExceptionGroup as inner:
raise ExceptionGroup('outer', [inner, TypeError('t1')])
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('ExceptionGroup: outer (2 sub-exceptions)', tb)
self.assertIn('ExceptionGroup: inner', tb)
self.assertIn('ValueError: v1', tb)
self.assertIn('TypeError: t1', tb)
# Verify tree structure characters.
self.assertIn('+-+---------------- 1 ----------------', tb)
self.assertIn('+---------------- 2 ----------------', tb)
self.assertIn('+------------------------------------', tb)
@force_not_colorized
def test_print_exception_group_chaining(self):
# __cause__ on a sub-exception exercises the prefixed
# chaining-message path (margin chars on separator lines).
sub = TypeError('t1')
sub.__cause__ = ValueError('original')
try:
raise ExceptionGroup('eg1', [sub])
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('ValueError: original', tb)
self.assertIn('| The above exception was the direct cause', tb)
self.assertIn('ExceptionGroup: eg1', tb)
# __context__ (implicit chaining) on a sub-exception.
sub = TypeError('t2')
sub.__context__ = ValueError('first')
try:
raise ExceptionGroup('eg2', [sub])
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('ValueError: first', tb)
self.assertIn('| During handling of the above exception', tb)
self.assertIn('ExceptionGroup: eg2', tb)
@force_not_colorized
def test_print_exception_group_seen(self):
shared = ValueError('shared')
try:
raise ExceptionGroup('eg', [shared, shared])
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('ValueError: shared', tb)
self.assertIn('<exception ValueError has printed>', tb)
@force_not_colorized
def test_print_exception_group_max_width(self):
excs = [ValueError(f'v{i}') for i in range(20)]
try:
raise ExceptionGroup('eg', excs)
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('+---------------- 15 ----------------', tb)
self.assertIn('+---------------- ... ----------------', tb)
self.assertIn('and 5 more exceptions', tb)
self.assertNotIn('+---------------- 16 ----------------', tb)
@force_not_colorized
def test_print_exception_group_max_depth(self):
def make_nested(depth):
if depth == 0:
return ValueError('leaf')
return ExceptionGroup(f'level{depth}',
[make_nested(depth - 1)])
try:
raise make_nested(15)
except ExceptionGroup:
tb = self._capture_exception()
self.assertIn('... (max_group_depth is 10)', tb)
self.assertIn('ExceptionGroup: level15', tb)
self.assertNotIn('ValueError: leaf', tb)
# StdioFile tests.
class S(str):
+22 -1
View File
@@ -2,6 +2,7 @@
from idlelib import runscript
import unittest
from unittest import mock
from test.support import requires
from tkinter import Tk
from idlelib.editor import EditorWindow
@@ -18,7 +19,7 @@ class ScriptBindingTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -28,6 +29,26 @@ class ScriptBindingTest(unittest.TestCase):
sb = runscript.ScriptBinding(ew)
ew._close()
def test_run_module_event_shell_busy_no_restart(self):
# gh-82183: running without restarting the busy shell aborts.
ew = EditorWindow(root=self.root)
sb = runscript.ScriptBinding(ew)
sb.getfilename = mock.Mock(return_value='test.py')
sb.checksyntax = mock.Mock(return_value='code')
sb.tabnanny = mock.Mock(return_value=True)
sb.shell = shell = mock.Mock()
shell.executing = True
interp = shell.interp
with mock.patch.object(runscript, 'CustomRun') as customrun:
# Restart shell unchecked.
customrun.return_value.result = (['arg'], False)
result = sb.run_module_event(None, customize=True)
self.assertEqual(result, 'break')
interp.display_executing_dialog.assert_called_once()
interp.restart_subprocess.assert_not_called()
interp.runcode.assert_not_called()
ew._close()
if __name__ == '__main__':
unittest.main(verbosity=2)
+10 -7
View File
@@ -11,11 +11,10 @@ import tkinter as tk
from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop
from idlelib.delegator import Delegator
from idlelib.editor import fixwordbreaks
from idlelib.percolator import Percolator
import idlelib.pyshell
from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList
from idlelib.run import fix_scaling
from idlelib.pyshell import PyShell, PyShellFileList
from idlelib.util import fix_scaling, fix_word_breaks, fix_x11_paste
import idlelib.sidebar
from idlelib.sidebar import get_end_linenumber, get_lineno
@@ -403,7 +402,7 @@ class ShellSidebarTest(unittest.TestCase):
root.withdraw()
fix_scaling(root)
fixwordbreaks(root)
fix_word_breaks(root)
fix_x11_paste(root)
cls.flist = flist = PyShellFileList(root)
@@ -690,11 +689,15 @@ class ShellSidebarTest(unittest.TestCase):
last_lineno = get_end_linenumber(text)
self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
# Delta for <MouseWheel>, whose meaning is platform-dependent.
# Simulate a mouse wheel notch. Tk 8.7 replaced the X11
# <Button-4>/<Button-5> wheel events with <MouseWheel> (whose delta is
# platform-dependent); older Tk on X11 still uses the button events.
x11_buttons = (sidebar.canvas._windowingsystem == 'x11'
and tk.TkVersion < 8.7)
delta = 1 if sidebar.canvas._windowingsystem == 'aqua' else 120
# Scroll up.
if sidebar.canvas._windowingsystem == 'x11':
if x11_buttons:
sidebar.canvas.event_generate('<Button-4>', x=0, y=0)
else:
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta)
@@ -702,7 +705,7 @@ class ShellSidebarTest(unittest.TestCase):
self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
# Scroll back down.
if sidebar.canvas._windowingsystem == 'x11':
if x11_buttons:
sidebar.canvas.event_generate('<Button-5>', x=0, y=0)
else:
sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=-delta)
+3 -1
View File
@@ -21,7 +21,7 @@ class StackBrowserTest(unittest.TestCase):
def tearDownClass(cls):
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## for id in cls.root.after_info():
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -35,6 +35,8 @@ class StackBrowserTest(unittest.TestCase):
isi(stackviewer.sc, ScrolledCanvas)
isi(stackviewer.item, stackviewer.StackTreeItem)
isi(stackviewer.node, TreeNode)
top = stackviewer.sc.frame.winfo_toplevel()
self.assertEqual(top.winfo_class(), 'Idle')
if __name__ == '__main__':
+6 -1
View File
@@ -29,7 +29,7 @@ class ListedToplevelTest(unittest.TestCase):
def tearDownClass(cls):
window.registry = window.WindowList()
cls.root.update_idletasks()
## for id in cls.root.tk.call('after', 'info'):
## for id in cls.root.after_info():
## cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
@@ -39,6 +39,11 @@ class ListedToplevelTest(unittest.TestCase):
win = window.ListedToplevel(self.root)
self.assertIn(win, window.registry)
self.assertEqual(win.focused_widget, win)
self.assertEqual(win.winfo_class(), 'Idle')
def test_init_class_override(self):
win = window.ListedToplevel(self.root, class_='Other')
self.assertEqual(win.winfo_class(), 'Other')
if __name__ == '__main__':
+1 -1
View File
@@ -21,7 +21,7 @@ class Test(unittest.TestCase):
def tearDownClass(cls):
cls.editwin._close()
cls.root.update_idletasks()
for id in cls.root.tk.call('after', 'info'):
for id in cls.root.after_info():
cls.root.after_cancel(id) # Need for EditorWindow.
cls.root.destroy()
del cls.root
+20 -2
View File
@@ -5,11 +5,13 @@ import sys
import tempfile
import tokenize
from tkinter import TclVersion
from tkinter import filedialog
from tkinter import messagebox
from tkinter.simpledialog import askstring # loadfile encoding.
from idlelib.config import idleConf
from idlelib import macosx
from idlelib.util import py_extensions
py_extensions = ' '.join("*"+ext for ext in py_extensions)
@@ -384,7 +386,18 @@ class IOBinding:
("All files", "*"),
)
save_filetypes = filetypes
# Output windows (Shell, Output) are not Python source, so they save
# with text files listed first and default to ".txt" (gh-65339).
text_filetypes = (
("Text files", "*.txt", "TEXT"),
("Python files", py_extensions, "TEXT"),
("All files", "*"),
)
defaultextension = '.py' if sys.platform == 'darwin' else ''
text_defaultextension = '.txt'
def askopenfile(self):
dir, base = self.defaultfilename("open")
@@ -396,7 +409,12 @@ class IOBinding:
def defaultfilename(self, mode="open"):
if self.filename:
return os.path.split(self.filename)
dirname, base = os.path.split(self.filename)
if base[:1] == '~' and TclVersion < 9 and not macosx.isAquaTk():
# Tcl before version 9 substitutes a leading tilde in
# a file name (gh-59568). The macOS dialog does not.
base = os.path.join(os.curdir, base)
return dirname, base
elif self.dirname:
return self.dirname, ""
else:
@@ -411,7 +429,7 @@ class IOBinding:
if not self.savedialog:
self.savedialog = filedialog.SaveAs(
parent=self.text,
filetypes=self.filetypes,
filetypes=self.save_filetypes,
defaultextension=self.defaultextension)
filename = self.savedialog.show(initialdir=dir, initialfile=base)
return filename
+1 -1
View File
@@ -39,7 +39,7 @@ def _init_tk_type():
_tk_type = "xquartz"
elif 'aqua' not in ws:
_tk_type = "other"
elif 'AppKit' in root.tk.call('winfo', 'server', '.'):
elif 'AppKit' in root.winfo_server():
_tk_type = "cocoa"
else:
_tk_type = "carbon"
+34 -4
View File
@@ -310,6 +310,17 @@ def _triplet_to_sequence(triplet):
else:
return '<'+_state_names[triplet[0]]+_types[triplet[1]][0]+'>'
def warn_bad_binding(virtual, sequence, err):
# gh-55646: warn instead of crashing on an invalid key binding.
action = virtual[2:-2] if virtual[:2] == '<<' and virtual[-2:] == '>>' \
else virtual
print(f'Warning: ignoring invalid key binding {sequence!r} '
f'for {action!r}: {err}. '
f'Please reconfigure it in the IDLE Settings dialog.',
file=sys.stderr)
_multicall_dict = {}
def MultiCallCreator(widget):
"""Return a MultiCall class which inherits its methods from the
@@ -343,8 +354,17 @@ def MultiCallCreator(widget):
self.__binders[triplet[1]].unbind(triplet, ei[0])
ei[0] = func
if ei[0] is not None:
bad = []
for triplet in ei[1]:
self.__binders[triplet[1]].bind(triplet, func)
try:
self.__binders[triplet[1]].bind(triplet, func)
except tkinter.TclError as err:
warn_bad_binding(sequence,
_triplet_to_sequence(triplet),
err)
bad.append(triplet)
for triplet in bad: # Drop the invalid sequences.
ei[1].remove(triplet)
else:
self.__eventinfo[sequence] = [func, []]
return widget.bind(self, sequence, func, add)
@@ -371,10 +391,19 @@ def MultiCallCreator(widget):
triplet = _parse_sequence(seq)
if triplet is None:
#print("Tkinter event_add(%s)" % seq, file=sys.__stderr__)
widget.event_add(self, virtual, seq)
try:
widget.event_add(self, virtual, seq)
except tkinter.TclError as err:
warn_bad_binding(virtual, seq, err)
continue # Drop the invalid sequence.
else:
if func is not None:
self.__binders[triplet[1]].bind(triplet, func)
try:
self.__binders[triplet[1]].bind(triplet, func)
except tkinter.TclError as err:
warn_bad_binding(virtual,
_triplet_to_sequence(triplet), err)
continue # Drop the invalid sequence.
triplets.append(triplet)
def event_delete(self, virtual, *sequences):
@@ -386,10 +415,11 @@ def MultiCallCreator(widget):
if triplet is None:
#print("Tkinter event_delete: %s" % seq, file=sys.__stderr__)
widget.event_delete(self, virtual, seq)
else:
elif triplet in triplets:
if func is not None:
self.__binders[triplet[1]].unbind(triplet, func)
triplets.remove(triplet)
# Else the sequence is not bound; ignore it (gh-89360).
def event_info(self, virtual=None):
if virtual is None or virtual not in self.__eventinfo:
+4
View File
@@ -78,6 +78,10 @@ class OutputWindow(EditorWindow):
def __init__(self, *args):
EditorWindow.__init__(self, *args)
self.text.bind("<<goto-file-line>>", self.goto_file_line)
# Output is not Python source, so save it as text by default
# (gh-65339).
self.io.save_filetypes = self.io.text_filetypes
self.io.defaultextension = self.io.text_defaultextension
# Customize EditorWindow
def ispythonsource(self, filename):
+40 -24
View File
@@ -36,13 +36,14 @@ from idlelib.config import idleConf
from idlelib.delegator import Delegator
from idlelib import debugger
from idlelib import debugger_r
from idlelib.editor import EditorWindow, fixwordbreaks
from idlelib.editor import EditorWindow
from idlelib.filelist import FileList
from idlelib.outwin import OutputWindow
from idlelib import replace
from idlelib import rpc
from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
from idlelib.undo import UndoDelegator
from idlelib.util import fix_word_breaks
# Default for testing; defaults to True in main() for running.
use_subprocess = False
@@ -242,12 +243,13 @@ class PyShellEditorWindow(EditorWindow):
breaks = self.breakpoints
filename = self.io.filename
try:
with open(self.breakpointPath) as fp:
with open(self.breakpointPath,
encoding='utf-8', errors='replace') as fp:
lines = fp.readlines()
except OSError:
lines = []
try:
with open(self.breakpointPath, "w") as new_file:
with open(self.breakpointPath, "w", encoding='utf-8') as new_file:
for line in lines:
if not line.startswith(filename + '='):
new_file.write(line)
@@ -272,7 +274,8 @@ class PyShellEditorWindow(EditorWindow):
if filename is None:
return
if os.path.isfile(self.breakpointPath):
with open(self.breakpointPath) as fp:
with open(self.breakpointPath,
encoding='utf-8', errors='replace') as fp:
lines = fp.readlines()
for line in lines:
if line.startswith(filename + '='):
@@ -405,6 +408,17 @@ def restart_line(width, filename): # See bpo-38141.
return tag[:-2] # Remove ' ='.
def fix_user_path(path):
"""Return path without the idlelib directory (gh-134300).
That directory is on sys.path when idle.py is run as a script.
Otherwise user code could import idlelib submodules as top-level
modules, such as "import help".
"""
idlelib_dir = os.path.dirname(os.path.abspath(__file__))
return [p for p in path if p != idlelib_dir]
class ModifiedInterpreter(InteractiveInterpreter):
def __init__(self, tkconsole):
@@ -498,7 +512,6 @@ class ModifiedInterpreter(InteractiveInterpreter):
self.rpcclt.close()
self.terminate_subprocess()
console = self.tkconsole
was_executing = console.executing
console.executing = False
self.spawn_subprocess()
try:
@@ -566,6 +579,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
path.extend(sys.path)
else:
path = sys.path
path = fix_user_path(path) # gh-134300
self.runcommand("""if 1:
import sys as _sys
@@ -642,7 +656,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
return
item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid)
from idlelib.tree import ScrolledCanvas, TreeNode
top = Toplevel(self.tkconsole.root)
top = Toplevel(self.tkconsole.root, class_='Idle')
theme = idleConf.CurrentTheme()
background = idleConf.GetHighlight(theme, 'normal')['background']
sc = ScrolledCanvas(top, bg=background, highlightthickness=0)
@@ -680,6 +694,8 @@ class ModifiedInterpreter(InteractiveInterpreter):
def runsource(self, source):
"Extend base class method: Stuff the source in the line cache first"
# Remove the highlighting of a previous syntax error (gh-93966).
self.tkconsole.text.tag_remove("ERROR", "1.0", "end")
filename = self.stuffsource(source)
# at the moment, InteractiveInterpreter expects str
assert isinstance(source, str)
@@ -716,7 +732,6 @@ class ModifiedInterpreter(InteractiveInterpreter):
"""
tkconsole = self.tkconsole
text = tkconsole.text
text.tag_remove("ERROR", "1.0", "end")
type, value, tb = sys.exc_info()
msg = getattr(value, 'msg', '') or value or "<no detail available>"
lineno = getattr(value, 'lineno', '') or 1
@@ -880,9 +895,9 @@ class PyShell(OutputWindow):
if ms[2][0] != "shell":
ms.insert(2, ("shell", "She_ll"))
self.interp = ModifiedInterpreter(self)
if flist is None:
if flist is None: # TODO possible? root and flist in main.
root = Tk()
fixwordbreaks(root)
fix_word_breaks(root)
root.withdraw()
flist = PyShellFileList(root)
@@ -1085,6 +1100,7 @@ class PyShell(OutputWindow):
def debug_menu_postcommand(self):
state = 'disabled' if self.executing else 'normal'
self.update_menu_state('debug', '*ebugger', state)
self.update_menu_state('debug', '*tack*iewer', state)
def beginexecuting(self):
@@ -1417,13 +1433,24 @@ class PyShell(OutputWindow):
self.ctip.remove_calltip_window()
def write(self, s, tags=()):
text = self.text
# Move the prompt (the "console" tag on the preceding newline)
# after output which comes while it is shown (gh-75512).
at_prompt = (s and tags in ("stdout", "stderr")
and not self.executing and not self.reading)
if at_prompt:
text.tag_remove("console", "iomark-1c")
text.tag_remove("stdin", "iomark-1c")
try:
self.text.mark_gravity("iomark", "right")
text.mark_gravity("iomark", "right")
count = OutputWindow.write(self, s, tags, "iomark")
self.text.mark_gravity("iomark", "left")
text.mark_gravity("iomark", "left")
except:
raise ###pass # ### 11Aug07 KBK if we are expecting exceptions
# let's find out what they are and be specific.
if at_prompt and s.endswith('\n'):
text.tag_add("console", "iomark-1c")
self.shell_sidebar.update_sidebar()
if self.canceled:
self.canceled = False
if not use_subprocess:
@@ -1451,17 +1478,6 @@ class PyShell(OutputWindow):
self.shell_sidebar.update_sidebar()
def fix_x11_paste(root):
"Make paste replace selection on x11. See issue #5124."
if root._windowingsystem == 'x11':
for cls in 'Text', 'Entry', 'Spinbox':
root.bind_class(
cls,
'<<Paste>>',
'catch {%W delete sel.first sel.last}\n' +
root.bind_class(cls, '<<Paste>>'))
usage_msg = """\
USAGE: idle [-deins] [-t title] [file]*
@@ -1521,6 +1537,7 @@ def main():
from platform import system
from idlelib import testing # bool value
from idlelib import macosx
from idlelib.util import fix_scaling, fix_x11_paste
global flist, root, use_subprocess
@@ -1606,7 +1623,6 @@ def main():
NoDefaultRoot()
root = Tk(className="Idle")
root.withdraw()
from idlelib.run import fix_scaling
fix_scaling(root)
# set application icon
@@ -1628,7 +1644,7 @@ def main():
root.wm_iconphoto(True, *icons)
# start editor and/or shell windows:
fixwordbreaks(root)
fix_word_breaks(root)
fix_x11_paste(root)
flist = PyShellFileList(root)
macosx.setupApp(root, flist)
+21 -8
View File
@@ -122,12 +122,13 @@ class ReplaceDialog(SearchDialogBase):
def replace_all(self, event=None):
"""Handle the Replace All button.
Search text for occurrences of the Find value and replace
each of them. The 'wrap around' value controls the start
point for searching. If wrap isn't set, then the searching
starts at the first occurrence after the current selection;
if wrap is set, the replacement starts at the first line.
The replacement is always done top-to-bottom in the text.
Search text for occurrences of the Find value and replace each
of them. The 'wrap around' and direction values control which
occurrences are replaced. With wrap around, every occurrence is
replaced. Without it, a forward search replaces occurrences from
the current position to the end of the text, and a backward search
replaces occurrences from the beginning of the text to the current
position. The replacement is always done top-to-bottom.
"""
prog = self.engine.getprog()
if not prog:
@@ -142,9 +143,18 @@ class ReplaceDialog(SearchDialogBase):
text.tag_remove("hit", "1.0", "end")
line = res[0]
col = res[1].start()
# For a backward search without wrap, replace top-to-bottom from
# the start of the text down to the first match at or above the
# current position (gh-71956). A mark tracks that stop point.
stop = None
if self.engine.iswrap():
line = 1
col = 0
elif self.engine.isback():
stop = "replace_all_stop"
text.mark_set(stop, "%d.%d" % (line, res[1].end()))
line = 1
col = 0
ok = True
first = last = None
# XXX ought to replace circular instead of top-to-bottom when wrapping
@@ -152,12 +162,13 @@ class ReplaceDialog(SearchDialogBase):
while res := self.engine.search_forward(
text, prog, line, col, wrap=False, ok=ok):
line, m = res
chars = text.get("%d.0" % line, "%d.0" % (line+1))
i, j = m.span()
if stop is not None and text.compare("%d.%d" % (line, i), ">=", stop):
break
orig = m.group()
new = self._replace_expand(m, repl)
if new is None:
break
i, j = m.span()
first = "%d.%d" % (line, i)
last = "%d.%d" % (line, j)
if new == orig:
@@ -170,6 +181,8 @@ class ReplaceDialog(SearchDialogBase):
text.insert(first, new, self.insert_tags)
col = i + len(new)
ok = False
if stop is not None:
text.mark_unset(stop)
text.undo_block_stop()
if first and last:
self.show_hit(first, last)
+5 -4
View File
@@ -139,6 +139,11 @@ class SocketIO:
self.objtable = objtable
self.responses = {}
self.cvars = {}
# Receive buffer state. A new connection must not inherit a
# partially received packet from the old one (gh-89544).
self.buff = b''
self.bufneed = 4
self.bufstate = 0 # meaning: 0 => reading count; 1 => reading data
def close(self):
sock = self.sock
@@ -345,10 +350,6 @@ class SocketIO:
raise OSError("socket no longer exists")
s = s[n:]
buff = b''
bufneed = 4
bufstate = 0 # meaning: 0 => reading count; 1 => reading data
def pollpacket(self, wait):
self._stage0()
if len(self.buff) < self.bufneed:
+82 -28
View File
@@ -25,6 +25,7 @@ from idlelib import debugobj_r # remote_object_tree_item
from idlelib import iomenu # encoding
from idlelib import rpc # multiple objects
from idlelib import stackviewer # StackTreeItem
from idlelib import util # fix_scaling
import __main__
import tkinter # Use tcl and, if startup fails, messagebox.
@@ -216,7 +217,7 @@ def show_socket_error(err, address):
import tkinter
from tkinter.messagebox import showerror
root = tkinter.Tk()
fix_scaling(root)
util.fix_scaling(root)
root.withdraw()
showerror(
"Subprocess Connection Error",
@@ -249,31 +250,94 @@ def print_exception():
sys.last_type, sys.last_value, sys.last_traceback = excinfo
sys.last_exc = val
seen = set()
exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
"debugger_r.py", "bdb.py")
max_group_width = 15
max_group_depth = 10
group_depth = 0
def print_exc(typ, exc, tb):
def print_exc_group(typ, exc, tb, prefix=""):
nonlocal group_depth
group_depth += 1
prefix2 = prefix or " "
if group_depth > max_group_depth:
print(f"{prefix2}| ... (max_group_depth is {max_group_depth})",
file=efile)
group_depth -= 1
return
if tb:
if not prefix:
print(" + Exception Group Traceback (most recent call last):", file=efile)
else:
print(f"{prefix}| Exception Group Traceback (most recent call last):", file=efile)
tbe = traceback.extract_tb(tb)
cleanup_traceback(tbe, exclude)
for line in traceback.format_list(tbe):
for subline in line.rstrip().splitlines():
print(f"{prefix2}| {subline}", file=efile)
lines = get_message_lines(typ, exc, tb)
for line in lines:
print(f"{prefix2}| {line}", end="", file=efile)
num_excs = len(exc.exceptions)
if num_excs <= max_group_width:
n = num_excs
else:
n = max_group_width + 1
for i, sub in enumerate(exc.exceptions[:n], 1):
truncated = (i > max_group_width)
first_line_pre = "+-" if i == 1 else " "
title = str(i) if not truncated else '...'
print(f"{prefix2}{first_line_pre}+---------------- {title} ----------------", file=efile)
if truncated:
remaining = num_excs - max_group_width
plural = 's' if remaining > 1 else ''
print(f"{prefix2} | and {remaining} more exception{plural}",
file=efile)
need_print_underline = True
elif id(sub) not in seen:
if not prefix:
print_exc(type(sub), sub, sub.__traceback__, " ")
else:
print_exc(type(sub), sub, sub.__traceback__, prefix + " ")
need_print_underline = not isinstance(sub, BaseExceptionGroup)
else:
print(f"{prefix2} | <exception {type(sub).__name__} has printed>", file=efile)
need_print_underline = True
if need_print_underline and i == n:
print(f"{prefix2} +------------------------------------", file=efile)
group_depth -= 1
def print_exc(typ, exc, tb, prefix=""):
seen.add(id(exc))
context = exc.__context__
cause = exc.__cause__
prefix2 = f"{prefix}| " if prefix else ""
if cause is not None and id(cause) not in seen:
print_exc(type(cause), cause, cause.__traceback__)
print("\nThe above exception was the direct cause "
"of the following exception:\n", file=efile)
print_exc(type(cause), cause, cause.__traceback__, prefix)
print(f"{prefix2}\n{prefix2}The above exception was the direct cause "
f"of the following exception:\n{prefix2}", file=efile)
elif (context is not None and
not exc.__suppress_context__ and
id(context) not in seen):
print_exc(type(context), context, context.__traceback__)
print("\nDuring handling of the above exception, "
"another exception occurred:\n", file=efile)
if tb:
tbe = traceback.extract_tb(tb)
print('Traceback (most recent call last):', file=efile)
exclude = ("run.py", "rpc.py", "threading.py", "queue.py",
"debugger_r.py", "bdb.py")
cleanup_traceback(tbe, exclude)
traceback.print_list(tbe, file=efile)
lines = get_message_lines(typ, exc, tb)
for line in lines:
print(line, end='', file=efile)
print_exc(type(context), context, context.__traceback__, prefix)
print(f"{prefix2}\n{prefix2}During handling of the above exception, "
f"another exception occurred:\n{prefix2}", file=efile)
if isinstance(exc, BaseExceptionGroup):
print_exc_group(typ, exc, tb, prefix=prefix)
else:
if tb:
print(f"{prefix2}Traceback (most recent call last):", file=efile)
tbe = traceback.extract_tb(tb)
cleanup_traceback(tbe, exclude)
if prefix:
for line in traceback.format_list(tbe):
for subline in line.rstrip().splitlines():
print(f"{prefix}| {subline}", file=efile)
else:
traceback.print_list(tbe, file=efile)
lines = get_message_lines(typ, exc, tb)
for line in lines:
print(f"{prefix2}{line}", end="", file=efile)
print_exc(typ, val, tb)
@@ -326,16 +390,6 @@ def exit():
sys.exit(0)
def fix_scaling(root):
"""Scale fonts on HiDPI displays."""
import tkinter.font
scaling = float(root.tk.call('tk', 'scaling'))
if scaling > 1.4:
for name in tkinter.font.names(root):
font = tkinter.font.Font(root=root, name=name, exists=True)
size = int(font['size'])
if size < 0:
font['size'] = round(-0.75*size)
def fixdoc(fun, text):
+4
View File
@@ -139,6 +139,10 @@ class ScriptBinding:
return 'break'
self.cli_args, restart = run_args if customize else ([], True)
interp = self.shell.interp
if self.shell.executing and not restart:
# Cannot run without restarting the busy shell (gh-82183).
interp.display_executing_dialog()
return 'break'
if pyshell.use_subprocess and restart:
interp.restart_subprocess(
with_cwd=False, filename=filename)
+1 -1
View File
@@ -11,7 +11,7 @@ from idlelib.tree import TreeNode, TreeItem, ScrolledCanvas
def StackBrowser(root, exc, flist=None, top=None):
global sc, item, node # For testing.
if top is None:
top = tk.Toplevel(root)
top = tk.Toplevel(root, class_='Idle')
sc = ScrolledCanvas(top, bg="white", highlightthickness=0)
sc.frame.pack(expand=1, fill="both")
item = StackTreeItem(exc, flist)
+3 -5
View File
@@ -92,10 +92,10 @@ class ViewFrame(Frame):
color_config(text)
text.focus_set()
self.button_ok = button_ok = Button(
self.button_ok = Button(
self, text='Close', command=self.ok, takefocus=False)
self.textframe.pack(side='top', expand=True, fill='both')
button_ok.pack(side='bottom')
self.button_ok.pack(side='bottom')
def ok(self, event=None):
"""Dismiss text viewer dialog."""
@@ -127,10 +127,8 @@ class ViewWindow(Toplevel):
self.geometry(f'=750x500+{x}+{y}')
self.title(title)
self.viewframe = ViewFrame(self, contents, wrap=wrap)
self.protocol("WM_DELETE_WINDOW", self.ok)
self.button_ok = button_ok = Button(self, text='Close',
command=self.ok, takefocus=False)
self.viewframe = ViewFrame(self, contents, wrap=wrap)
self.viewframe.pack(side='top', expand=True, fill='both')
self.is_modal = modal
+33
View File
@@ -19,6 +19,20 @@ import sys
py_extensions = ('.py', '.pyw', '.pyi')
# fix_x functions seem only needed once per process.
def fix_scaling(root): # Called in filelist _test, pyshell, and run.
"""Scale fonts on HiDPI displays, once per process."""
import tkinter.font
scaling = float(root.tk.call('tk', 'scaling'))
if scaling > 1.4:
for name in tkinter.font.names(root):
font = tkinter.font.Font(root=root, name=name, exists=True)
size = int(font['size'])
if size < 0:
font['size'] = round(-0.75*size)
# Fix for HiDPI screens on Windows. CALL BEFORE ANY TK OPERATIONS!
# URL for arguments for the ...Awareness call below.
# https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx
@@ -31,6 +45,25 @@ if sys.platform == 'win32': # pragma: no cover
except (ImportError, AttributeError, OSError):
pass
def fix_word_breaks(root): # Called in editor htest, filelist _test, pyshell.
# On Windows, tcl/tk breaks 'words' only on spaces, as in Command Prompt.
# We want Motif style everywhere. See #21474, msg218992 and followup.
tk = root.tk
tk.call('tcl_wordBreakAfter', 'a b', 0) # make sure word.tcl is loaded
tk.call('set', 'tcl_wordchars', r'\w')
tk.call('set', 'tcl_nonwordchars', r'\W')
def fix_x11_paste(root):
"Make paste replace selection on x11. See issue #5124."
if root._windowingsystem == 'x11':
for cls in 'Text', 'Entry', 'Spinbox':
root.bind_class(
cls,
'<<Paste>>',
'catch {%W delete sel.first sel.last}\n' +
root.bind_class(cls, '<<Paste>>'))
if __name__ == '__main__':
from unittest import main
+4
View File
@@ -61,6 +61,10 @@ unregister_callback = registry.unregister_callback
class ListedToplevel(Toplevel):
def __init__(self, master, **kw):
# Set the WM_CLASS property so that X11 window managers group and
# label IDLE's windows under 'Idle' instead of the default
# 'Toplevel' (gh-66331). It matches the class name passed to Tk().
kw.setdefault('class_', 'Idle')
Toplevel.__init__(self, master, kw)
registry.add(self)
self.focused_widget = self
+14
View File
@@ -0,0 +1,14 @@
Writing new tests
=================
Precaution
----------
New tests should always use only one Tk window at once, like all the
current tests do. This means that you have to destroy the current window
before creating another one, and clean up after the test. The motivation
behind this is that some tests may depend on having its window focused
while it is running to work properly, and it may be hard to force focus
on your window across platforms (right now only test_traversal at
test_ttk.test_widgets.NotebookTest depends on this).
+30
View File
@@ -0,0 +1,30 @@
import os.path
import unittest
from test.support import (
check_sanitizer,
import_helper,
load_package_tests,
requires,
)
if check_sanitizer(address=True, memory=True):
# See gh-90791 for details
raise unittest.SkipTest("Tests involving libX11 can SEGFAULT on ASAN/MSAN builds")
# Skip test if _tkinter wasn't built.
import_helper.import_module('_tkinter')
# Skip test if tk cannot be initialized.
requires('gui')
def load_tests(*args):
return load_package_tests(os.path.dirname(__file__), *args)
def setUpModule():
wantobjects = support.get_resource_value('wantobjects')
if wantobjects is not None:
unittest.enterModuleContext(
support.swap_attr(tkinter, 'wantobjects', int(wantobjects)))
+4
View File
@@ -0,0 +1,4 @@
from . import load_tests
import unittest
unittest.main()
+211
View File
@@ -0,0 +1,211 @@
import functools
import time
import tkinter
import unittest
from test import support
def setUpModule():
wantobjects = support.get_resource_value('wantobjects')
if wantobjects is not None:
unittest.enterModuleContext(
support.swap_attr(tkinter, 'wantobjects', int(wantobjects)))
class AbstractTkTest:
@classmethod
def setUpClass(cls):
cls._old_support_default_root = tkinter._support_default_root
destroy_default_root()
tkinter.NoDefaultRoot()
cls.root = tkinter.Tk()
cls.wantobjects = cls.root.wantobjects()
if support.is_resource_enabled('wantobjects'):
assert cls.wantobjects == int(support.get_resource_value('wantobjects'))
# De-maximize main window.
# Some window managers can maximize new windows.
cls.root.wm_state('normal')
try:
cls.root.wm_attributes(zoomed=False)
except tkinter.TclError:
pass
@classmethod
def tearDownClass(cls):
cls.root.update_idletasks()
cls.root.destroy()
del cls.root
tkinter._default_root = None
tkinter._support_default_root = cls._old_support_default_root
def setUp(self):
self.root.deiconify()
def tearDown(self):
for w in self.root.winfo_children():
w.destroy()
self.root.withdraw()
def require_mapped(self, widget, timeout=None):
"""Realize *widget*, or skip the test if the window manager will
not map it (e.g. a tiling WM or a headless/contended display).
Use this instead of a bare update() before querying realized
geometry (winfo_width(), identify(), coords(), place_info(), ...).
See gh-69134, gh-74941 and bpo-40722.
"""
if timeout is None:
timeout = support.LOOPBACK_TIMEOUT
if not wait_until_mapped(widget, timeout):
self.skipTest('widget was not mapped by the window manager '
f'(timed out after {timeout:g}s)')
class AbstractDialogTest(AbstractTkTest):
# Tk delivers generated keyboard events to the focused window. Hide the
# root window, otherwise the window manager can take the focus back from
# the dialog (gh-154357).
def setUp(self):
super().setUp()
self.root.withdraw()
class AbstractDefaultRootTest:
def setUp(self):
self._old_support_default_root = tkinter._support_default_root
destroy_default_root()
tkinter._support_default_root = True
self.wantobjects = tkinter.wantobjects
def tearDown(self):
destroy_default_root()
tkinter._default_root = None
tkinter._support_default_root = self._old_support_default_root
def _test_widget(self, constructor):
# no master passing
x = constructor()
self.assertIsNotNone(tkinter._default_root)
self.assertIs(x.master, tkinter._default_root)
self.assertIs(x.tk, tkinter._default_root.tk)
x.destroy()
destroy_default_root()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, constructor)
self.assertNotHasAttr(tkinter, '_default_root')
def destroy_default_root():
if getattr(tkinter, '_default_root', None):
tkinter._default_root.update_idletasks()
tkinter._default_root.destroy()
tkinter._default_root = None
def wait_until_mapped(widget, timeout=None, *, full_size=False):
"""Wait until *widget* is actually mapped and laid out by the window
manager, so that realized-geometry queries (winfo_width(), identify(),
coords(), ...) return meaningful values.
Return True once the widget is mapped with a non-trivial size, or False
if that has not happened within *timeout* seconds (default:
``support.LOOPBACK_TIMEOUT``). Unlike Misc.wait_visibility(), this
never blocks indefinitely, so it is safe under a window manager that
never maps the window (see gh-69134, gh-74941, bpo-40722).
If *full_size* is true, also wait until the realized size reaches the
requested size, so that per-pixel queries near an edge (e.g. identify())
are reliable even under load.
"""
if timeout is None:
timeout = support.LOOPBACK_TIMEOUT
deadline = time.monotonic() + timeout
widget.update_idletasks()
reset = False
while True:
widget.update() # drain pending Map/Configure events
if widget.winfo_ismapped():
if full_size:
w_ok = widget.winfo_width() >= widget.winfo_reqwidth() > 1
h_ok = widget.winfo_height() >= widget.winfo_reqheight() > 1
else:
w_ok = widget.winfo_width() > 1
h_ok = widget.winfo_height() > 1
if w_ok and h_ok:
return True
if full_size and not reset:
# Tk no longer resizes the toplevel to fit its content if
# the window manager has resized it. Undo this.
widget.winfo_toplevel().wm_geometry('')
reset = True
if time.monotonic() >= deadline:
return False
time.sleep(0.01)
def simulate_mouse_click(widget, x, y):
"""Generate proper events to click at the x, y position (tries to act
like an X server)."""
widget.event_generate('<Enter>', x=0, y=0)
widget.event_generate('<Motion>', x=x, y=y)
widget.event_generate('<ButtonPress-1>', x=x, y=y)
widget.event_generate('<ButtonRelease-1>', x=x, y=y)
import _tkinter
tcl_version = tuple(map(int, _tkinter.TCL_VERSION.split('.')))
tk_version = tuple(map(int, _tkinter.TK_VERSION.split('.')))
def requires_tk(*version):
if len(version) <= 2 and tk_version >= version:
return lambda test: test
def deco(test):
@functools.wraps(test)
def newtest(self):
root = getattr(self, 'root', None)
if get_tk_patchlevel(root) < version:
self.skipTest('requires Tk version >= ' +
'.'.join(map(str, version)))
test(self)
return newtest
return deco
_tk_patchlevel = None
def get_tk_patchlevel(root):
global _tk_patchlevel
if _tk_patchlevel is None:
_tk_patchlevel = tkinter._parse_version(root.tk.globalgetvar('tk_patchLevel'))
return _tk_patchlevel
units = {
'c': 72 / 2.54, # centimeters
'i': 72, # inches
'm': 72 / 25.4, # millimeters
'p': 1, # points
}
def pixels_conv(value):
return float(value[:-1]) * units[value[-1:]]
def tcl_obj_eq(actual, expected):
if actual == expected:
return True
if isinstance(actual, _tkinter.Tcl_Obj):
if isinstance(expected, str):
return str(actual) == expected
if isinstance(actual, tuple):
if isinstance(expected, tuple):
return (len(actual) == len(expected) and
all(tcl_obj_eq(act, exp)
for act, exp in zip(actual, expected)))
return False
def widget_eq(actual, expected):
if actual == expected:
return True
if isinstance(actual, (str, tkinter.Widget)):
if isinstance(expected, (str, tkinter.Widget)):
return str(actual) == str(expected)
return False
+69
View File
@@ -0,0 +1,69 @@
import unittest
import tkinter
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest
from tkinter import colorchooser
from tkinter.colorchooser import askcolor
from tkinter.commondialog import Dialog
requires('gui')
class ChooserTest(AbstractTkTest, unittest.TestCase):
@classmethod
def setUpClass(cls):
AbstractTkTest.setUpClass.__func__(cls)
cls.cc = colorchooser.Chooser(initialcolor='dark blue slate')
def test_fixoptions(self):
cc = self.cc
cc._fixoptions()
self.assertEqual(cc.options['initialcolor'], 'dark blue slate')
cc.options['initialcolor'] = '#D2D269691E1E'
cc._fixoptions()
self.assertEqual(cc.options['initialcolor'], '#D2D269691E1E')
cc.options['initialcolor'] = (210, 105, 30)
cc._fixoptions()
self.assertEqual(cc.options['initialcolor'], '#d2691e')
def test_fixresult(self):
cc = self.cc
self.assertEqual(cc._fixresult(self.root, ()), (None, None))
self.assertEqual(cc._fixresult(self.root, ''), (None, None))
self.assertEqual(cc._fixresult(self.root, 'chocolate'),
((210, 105, 30), 'chocolate'))
self.assertEqual(cc._fixresult(self.root, '#4a3c8c'),
((74, 60, 140), '#4a3c8c'))
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_askcolor(self):
def test_callback(dialog, master):
nonlocal ismapped
master.update()
ismapped = master.winfo_ismapped()
raise ZeroDivisionError
with swap_attr(Dialog, '_test_callback', test_callback):
ismapped = None
self.assertRaises(ZeroDivisionError, askcolor)
#askcolor()
self.assertEqual(ismapped, False)
root = tkinter.Tk()
ismapped = None
self.assertRaises(ZeroDivisionError, askcolor)
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, askcolor)
if __name__ == "__main__":
unittest.main()
+171
View File
@@ -0,0 +1,171 @@
import unittest
import tkinter
from tkinter import dnd
from test.support import requires
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest
requires('gui')
class Target:
def __init__(self, widget, log):
self.widget = widget
self.log = log
widget.dnd_accept = self.dnd_accept
def dnd_accept(self, source, event):
self.log.append('accept')
return self
def dnd_enter(self, source, event):
self.log.append('enter')
def dnd_motion(self, source, event):
self.log.append('motion')
def dnd_leave(self, source, event):
self.log.append('leave')
def dnd_commit(self, source, event):
self.log.append('commit')
class Source:
def __init__(self, log):
self.log = log
def dnd_end(self, target, event):
self.log.append('end')
class FakeEvent:
def __init__(self, widget, num=1):
self.num = num
self.widget = widget
self.x = self.y = self.x_root = self.y_root = 0
class DndTest(AbstractTkTest, unittest.TestCase):
def setUp(self):
super().setUp()
self.canvas = tkinter.Canvas(self.root)
self.canvas.pack()
# on_motion() locates the target with winfo_containing(). Bypass that
# real screen lookup, which depends on the window being visible and
# unobscured, so the test does not hinge on the window manager.
self.canvas.winfo_containing = lambda x, y: self.canvas
self.log = []
self.source = Source(self.log)
self.target = Target(self.canvas, self.log)
def tearDown(self):
# Make sure no drag-and-drop is left active between tests: the
# recursion guard is a name-mangled attribute on the root.
try:
del self.root._DndHandler__dnd
except AttributeError:
pass
super().tearDown()
def test_drag_and_drop(self):
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
self.assertIsNotNone(handler)
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
handler.on_motion(FakeEvent(self.canvas)) # Move within the target.
handler.on_release(FakeEvent(self.canvas)) # Drop on the target.
self.assertEqual(self.log,
['accept', 'enter', 'accept', 'motion', 'commit', 'end'])
def test_cancel(self):
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
handler.cancel() # Leaves the target without committing.
self.assertEqual(self.log, ['accept', 'enter', 'leave', 'end'])
def test_no_target(self):
# Nothing under the pointer accepts the drag.
self.canvas.winfo_containing = lambda x, y: None
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
handler.on_motion(FakeEvent(self.canvas))
handler.on_release(FakeEvent(self.canvas))
self.assertEqual(self.log, ['end'])
def test_no_recursive_start(self):
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
self.assertIsNotNone(handler)
# A drag is already in progress, so a second start is ignored.
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas)))
handler.cancel()
def test_high_button_number_ignored(self):
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6)))
def test_restart_after_finish(self):
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
handler.cancel()
# Once a drag has finished a new one can start.
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
self.assertIsNotNone(handler)
handler.cancel()
def test_drag_cursor(self):
self.canvas['cursor'] = 'watch'
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
# The drag cursor is shown while dragging, the original restored after.
self.assertEqual(handler.save_cursor, 'watch')
self.assertEqual(str(self.canvas['cursor']), 'hand2')
handler.cancel()
self.assertEqual(str(self.canvas['cursor']), 'watch')
def test_bindings_added_and_removed(self):
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
self.assertIn('<Motion>', self.canvas.bind())
self.assertIn('<B1-ButtonRelease-1>', self.canvas.bind())
handler.cancel()
self.assertNotIn('<Motion>', self.canvas.bind())
self.assertNotIn('<B1-ButtonRelease-1>', self.canvas.bind())
def test_switch_target(self):
log1, log2 = [], []
w1, w2 = tkinter.Frame(self.root), tkinter.Frame(self.root)
target1, target2 = Target(w1, log1), Target(w2, log2)
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
self.canvas.winfo_containing = lambda x, y: w1
handler.on_motion(FakeEvent(self.canvas)) # Enter target1.
self.canvas.winfo_containing = lambda x, y: w2
handler.on_motion(FakeEvent(self.canvas)) # Leave target1, enter target2.
self.assertIs(handler.target, target2)
self.assertEqual(log1, ['accept', 'enter', 'leave'])
self.assertEqual(log2, ['accept', 'enter'])
handler.cancel()
def test_target_in_ancestor(self):
# The widget under the pointer has no dnd_accept, but an ancestor
# does: the search walks up the master chain to find it.
parent = tkinter.Frame(self.root)
target = Target(parent, self.log)
child = tkinter.Frame(parent)
self.canvas.winfo_containing = lambda x, y: child
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
handler.on_motion(FakeEvent(self.canvas))
self.assertIs(handler.target, target)
self.assertEqual(self.log, ['accept', 'enter'])
handler.cancel()
def test_accept_returning_none_continues(self):
# dnd_accept() returning None means "not me, keep looking up".
parent = tkinter.Frame(self.root)
target = Target(parent, self.log)
child = tkinter.Frame(parent)
child.dnd_accept = lambda source, event: None
self.canvas.winfo_containing = lambda x, y: child
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
handler.on_motion(FakeEvent(self.canvas))
self.assertIs(handler.target, target)
handler.cancel()
if __name__ == "__main__":
unittest.main()
+73
View File
@@ -0,0 +1,73 @@
import os
import unittest
from tkinter import filedialog
from tkinter.commondialog import Dialog
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest
requires('gui')
class NativeDialogTest(AbstractTkTest, unittest.TestCase):
# Open, SaveAs and Directory wrap modal native dialogs. The _test_callback
# seam is called by show() just before the dialog would open; replacing it
# with a function that raises exercises show() without blocking on the
# dialog.
def check(self, dialog_class, command):
self.assertEqual(dialog_class.command, command)
master = None
def test_callback(dialog, parent):
nonlocal master
master = parent
raise ZeroDivisionError
with swap_attr(Dialog, '_test_callback', test_callback):
d = dialog_class(self.root, title='Test')
self.assertRaises(ZeroDivisionError, d.show)
self.assertIs(master, self.root)
def test_open(self):
self.check(filedialog.Open, 'tk_getOpenFile')
def test_saveas(self):
self.check(filedialog.SaveAs, 'tk_getSaveFile')
def test_directory(self):
self.check(filedialog.Directory, 'tk_chooseDirectory')
class FileDialogTest(AbstractDialogTest, unittest.TestCase):
# The pure-Python FileDialog runs its own modal loop in go(); its logic is
# exercised here without entering the loop.
def test_selection(self):
d = filedialog.FileDialog(self.root)
d.directory = os.path.abspath(os.sep)
d.set_selection('spam.txt')
self.assertEqual(os.path.basename(d.get_selection()), 'spam.txt')
def test_filter(self):
d = filedialog.FileDialog(self.root)
d.set_filter(os.getcwd(), '*.py')
self.assertEqual(d.get_filter(), (os.getcwd(), '*.py'))
def test_ok_cancel(self):
d = filedialog.FileDialog(self.root)
d.directory = os.getcwd()
d.set_selection('spam.txt')
d.ok_command() # Accepts the current selection.
self.assertEqual(os.path.basename(d.how), 'spam.txt')
d.cancel_command() # Returns no selection.
self.assertIsNone(d.how)
def test_subclasses(self):
for cls in filedialog.LoadFileDialog, filedialog.SaveFileDialog:
with self.subTest(cls=cls.__name__):
d = cls(self.root)
self.assertIsInstance(d, filedialog.FileDialog)
self.assertEqual(d.top.title(), cls.title)
if __name__ == "__main__":
unittest.main()
+250
View File
@@ -0,0 +1,250 @@
import collections.abc
import unittest
import tkinter
from tkinter import font
from test.support import requires, gc_collect, ALWAYS_EQ
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest
requires('gui')
fontname = "TkDefaultFont"
class FontTest(AbstractTkTest, unittest.TestCase):
@classmethod
def setUpClass(cls):
AbstractTkTest.setUpClass.__func__(cls)
try:
cls.font = font.Font(root=cls.root, name=fontname, exists=True)
except tkinter.TclError:
cls.font = font.Font(root=cls.root, name=fontname, exists=False)
def test_configure(self):
self.assertEqual(self.font.config, self.font.configure)
options = self.font.configure()
self.assertGreaterEqual(set(options),
{'family', 'size', 'weight', 'slant', 'underline', 'overstrike'})
for key in options:
self.assertEqual(self.font.cget(key), options[key])
self.assertEqual(self.font[key], options[key])
for key in 'family', 'weight', 'slant':
self.assertIsInstance(options[key], str)
self.assertIsInstance(self.font.cget(key), str)
self.assertIsInstance(self.font[key], str)
sizetype = int if self.wantobjects else str
for key in 'size', 'underline', 'overstrike':
self.assertIsInstance(options[key], sizetype)
self.assertIsInstance(self.font.cget(key), sizetype)
self.assertIsInstance(self.font[key], sizetype)
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
self.font.cget, 'spam')
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
self.font.configure, spam='x')
self.assertRaises(TypeError, self.font.cget)
self.assertRaises(TypeError, self.font.cget, 'size', 'weight')
def test_create_from_named_font(self):
# gh-143990: a font created from a named font copies its configured
# options, preserving a size specified in pixels (a negative size).
sizetype = int if self.wantobjects else str
named = font.Font(root=self.root, name='my named font', # name with spaces
family='Times', size=-20, weight='bold')
# The source is the name of a named font or a Font representing one.
for source in ['my named font', named]:
with self.subTest(source=source):
f = font.Font(root=self.root, font=source)
self.assertEqual(f.cget('size'), sizetype(-20))
self.assertEqual(f.actual('family'), named.actual('family'))
self.assertEqual(f.actual('weight'), 'bold')
def test_create_from_description(self):
# gh-143990: a font created from a font description is resolved via
# "font actual", so a size in pixels (negative) becomes a size in points.
descriptions = [
('Times', -20), # tuple
('Times', -20, 'bold'), # tuple with a style
'Times -20', # string
'Times -20 bold', # string with a style
'{Times New Roman} -20', # string, family with spaces
]
for desc in descriptions:
with self.subTest(font=desc):
f = font.Font(root=self.root, font=desc)
self.assertGreater(int(f.cget('size')), 0) # pixels -> points
def test_copy(self):
# size=-20 (pixels): copy() copies the configured options, so the
# size is preserved rather than resolved (gh-143990).
f = font.Font(root=self.root, family='Times', size=-20, weight='bold')
copied = f.copy()
self.assertIsInstance(copied, font.Font)
self.assertIsNot(copied, f)
self.assertNotEqual(copied.name, f.name)
self.assertEqual(copied.actual(), f.actual())
sizetype = int if self.wantobjects else str
self.assertEqual(copied.cget('size'), sizetype(-20))
# The copy is independent of the original.
copied.configure(size=20)
self.assertEqual(f.cget('size'), sizetype(-20))
self.assertEqual(copied.cget('size'), sizetype(20))
self.assertRaises(TypeError, f.copy, 'x')
def test_unicode_family(self):
family = 'MS \u30b4\u30b7\u30c3\u30af'
try:
f = font.Font(root=self.root, family=family, exists=True)
except tkinter.TclError:
f = font.Font(root=self.root, family=family, exists=False)
self.assertEqual(f.cget('family'), family)
del f
gc_collect()
def test_actual(self):
options = self.font.actual()
self.assertGreaterEqual(set(options),
{'family', 'size', 'weight', 'slant', 'underline', 'overstrike'})
for key in options:
self.assertEqual(self.font.actual(key), options[key])
for key in 'family', 'weight', 'slant':
self.assertIsInstance(options[key], str)
self.assertIsInstance(self.font.actual(key), str)
sizetype = int if self.wantobjects else str
for key in 'size', 'underline', 'overstrike':
self.assertIsInstance(options[key], sizetype)
self.assertIsInstance(self.font.actual(key), sizetype)
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
self.font.actual, 'spam')
self.assertRaises(TypeError, self.font.actual, 'size', 'weight', 'slant')
def test_name(self):
self.assertEqual(self.font.name, fontname)
self.assertEqual(str(self.font), fontname)
def test_equality(self):
font1 = font.Font(root=self.root, name=fontname, exists=True)
font2 = font.Font(root=self.root, name=fontname, exists=True)
self.assertIsNot(font1, font2)
self.assertEqual(font1, font2)
self.assertNotEqual(font1, font1.copy())
self.assertNotEqual(font1, 0)
self.assertEqual(font1, ALWAYS_EQ)
root2 = tkinter.Tk()
self.addCleanup(root2.destroy)
font3 = font.Font(root=root2, name=fontname, exists=True)
self.assertEqual(str(font1), str(font3))
self.assertNotEqual(font1, font3)
def test_measure(self):
self.assertIsInstance(self.font.measure('abc'), int)
self.assertEqual(self.font.measure(''), 0)
self.assertIsInstance(
self.font.measure('abc', displayof=self.root), int)
self.assertRaises(TypeError, self.font.measure)
self.assertRaises(TypeError, self.font.measure, 'a', 'b', 'c')
def test_metrics(self):
metrics = self.font.metrics()
self.assertGreaterEqual(set(metrics),
{'ascent', 'descent', 'linespace', 'fixed'})
for key in metrics:
self.assertEqual(self.font.metrics(key), metrics[key])
self.assertEqual(self.font.metrics(key, displayof=self.root),
metrics[key])
self.assertIsInstance(metrics[key], int)
self.assertIsInstance(self.font.metrics(key), int)
self.assertRaisesRegex(tkinter.TclError, 'bad metric "-spam"',
self.font.metrics, 'spam')
def test_families(self):
families = font.families(self.root)
self.assertIsInstance(families, tuple)
self.assertTrue(families)
for family in families:
self.assertIsInstance(family, str)
self.assertTrue(family)
def test_names(self):
names = font.names(self.root)
self.assertIsInstance(names, tuple)
self.assertTrue(names)
for name in names:
self.assertIsInstance(name, str)
self.assertTrue(name)
self.assertIn(fontname, names)
def test_nametofont(self):
testfont = font.nametofont(fontname, root=self.root)
self.assertIsInstance(testfont, font.Font)
self.assertEqual(testfont.name, fontname)
def test_repr(self):
self.assertEqual(
repr(self.font), f'<tkinter.font.Font object {fontname!r}>'
)
def test_iterable_protocol(self):
self.assertNotIsSubclass(font.Font, collections.abc.Iterable)
self.assertNotIsSubclass(font.Font, collections.abc.Container)
self.assertNotIsInstance(self.font, collections.abc.Iterable)
self.assertNotIsInstance(self.font, collections.abc.Container)
with self.assertRaisesRegex(TypeError, 'is not iterable'):
iter(self.font)
with self.assertRaisesRegex(TypeError, 'is not a container or iterable'):
self.font in self.font
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_families(self):
self.assertRaises(RuntimeError, font.families)
root = tkinter.Tk()
families = font.families()
self.assertIsInstance(families, tuple)
self.assertTrue(families)
for family in families:
self.assertIsInstance(family, str)
self.assertTrue(family)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, font.families)
def test_names(self):
self.assertRaises(RuntimeError, font.names)
root = tkinter.Tk()
names = font.names()
self.assertIsInstance(names, tuple)
self.assertTrue(names)
for name in names:
self.assertIsInstance(name, str)
self.assertTrue(name)
self.assertIn(fontname, names)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, font.names)
def test_nametofont(self):
self.assertRaises(RuntimeError, font.nametofont, fontname)
root = tkinter.Tk()
testfont = font.nametofont(fontname)
self.assertIsInstance(testfont, font.Font)
self.assertEqual(testfont.name, fontname)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, font.nametofont, fontname)
class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
with self.assertWarnsRegex(
DeprecationWarning,
"'__version__' is deprecated and slated for removal in Python 3.20",
) as cm:
getattr(font, "__version__")
self.assertEqual(cm.filename, __file__)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large. Load diff
+675
View File
@@ -0,0 +1,675 @@
import collections.abc
import unittest
import tkinter
from test import support
from test.support import os_helper
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest, AbstractDefaultRootTest, requires_tk
support.requires('gui')
class MiscTest(AbstractTkTest, unittest.TestCase):
def test_image_types(self):
image_types = self.root.image_types()
self.assertIsInstance(image_types, tuple)
self.assertIn('photo', image_types)
self.assertIn('bitmap', image_types)
def test_image_names(self):
image_names = self.root.image_names()
self.assertIsInstance(image_names, tuple)
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_image_types(self):
self.assertRaises(RuntimeError, tkinter.image_types)
root = tkinter.Tk()
image_types = tkinter.image_types()
self.assertIsInstance(image_types, tuple)
self.assertIn('photo', image_types)
self.assertIn('bitmap', image_types)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, tkinter.image_types)
def test_image_names(self):
self.assertRaises(RuntimeError, tkinter.image_names)
root = tkinter.Tk()
image_names = tkinter.image_names()
self.assertIsInstance(image_names, tuple)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, tkinter.image_names)
def test_image_create_bitmap(self):
self.assertRaises(RuntimeError, tkinter.BitmapImage)
root = tkinter.Tk()
image = tkinter.BitmapImage()
self.assertIn(image.name, tkinter.image_names())
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, tkinter.BitmapImage)
def test_image_create_photo(self):
self.assertRaises(RuntimeError, tkinter.PhotoImage)
root = tkinter.Tk()
image = tkinter.PhotoImage()
self.assertIn(image.name, tkinter.image_names())
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, tkinter.PhotoImage)
class BaseImageTest:
def create(self):
return self.image_class('::img::test', master=self.root,
file=self.testfile)
def test_bug_100814(self):
# gh-100814: Passing a callable option value causes AttributeError.
with self.assertRaises(tkinter.TclError):
self.image_class('::img::test', master=self.root, spam=print)
image = self.image_class('::img::test', master=self.root)
with self.assertRaises(tkinter.TclError):
image.configure(spam=print)
def test_iterable_protocol(self):
image = self.create()
self.assertNotIsSubclass(self.image_class, collections.abc.Iterable)
self.assertNotIsSubclass(self.image_class, collections.abc.Container)
self.assertNotIsInstance(image, collections.abc.Iterable)
self.assertNotIsInstance(image, collections.abc.Container)
with self.assertRaisesRegex(TypeError, 'is not iterable'):
iter(image)
with self.assertRaisesRegex(TypeError, 'is not a container or iterable'):
image in image
class BitmapImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase):
image_class = tkinter.BitmapImage
@classmethod
def setUpClass(cls):
AbstractTkTest.setUpClass.__func__(cls)
cls.testfile = support.findfile('python.xbm', subdir='tkinterdata')
def test_create_from_file(self):
image = tkinter.BitmapImage('::img::test', master=self.root,
foreground='yellow', background='blue',
file=self.testfile)
self.assertEqual(str(image), '::img::test')
self.assertEqual(image.type(), 'bitmap')
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertIn('::img::test', self.root.image_names())
del image
support.gc_collect() # For PyPy or other GCs.
self.assertNotIn('::img::test', self.root.image_names())
def test_create_from_data(self):
with open(self.testfile, 'rb') as f:
data = f.read()
image = tkinter.BitmapImage('::img::test', master=self.root,
foreground='yellow', background='blue',
data=data)
self.assertEqual(str(image), '::img::test')
self.assertEqual(image.type(), 'bitmap')
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertIn('::img::test', self.root.image_names())
del image
support.gc_collect() # For PyPy or other GCs.
self.assertNotIn('::img::test', self.root.image_names())
def assertEqualStrList(self, actual, expected):
self.assertIsInstance(actual, str)
self.assertEqual(self.root.splitlist(actual), expected)
def test_configure_data(self):
image = tkinter.BitmapImage('::img::test', master=self.root)
self.assertEqual(image['data'], '-data {} {} {} {}')
with open(self.testfile, 'rb') as f:
data = f.read()
image.configure(data=data)
self.assertEqualStrList(image['data'],
('-data', '', '', '', data.decode('ascii')))
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertEqual(image['maskdata'], '-maskdata {} {} {} {}')
image.configure(maskdata=data)
self.assertEqualStrList(image['maskdata'],
('-maskdata', '', '', '', data.decode('ascii')))
def test_configure_file(self):
image = tkinter.BitmapImage('::img::test', master=self.root)
self.assertEqual(image['file'], '-file {} {} {} {}')
image.configure(file=self.testfile)
self.assertEqualStrList(image['file'],
('-file', '', '', '',self.testfile))
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertEqual(image['maskfile'], '-maskfile {} {} {} {}')
image.configure(maskfile=self.testfile)
self.assertEqualStrList(image['maskfile'],
('-maskfile', '', '', '', self.testfile))
def test_configure_background(self):
image = tkinter.BitmapImage('::img::test', master=self.root)
self.assertEqual(image['background'], '-background {} {} {} {}')
image.configure(background='blue')
self.assertEqual(image['background'], '-background {} {} {} blue')
def test_configure_foreground(self):
image = tkinter.BitmapImage('::img::test', master=self.root)
self.assertEqual(image['foreground'],
'-foreground {} {} #000000 #000000')
image.configure(foreground='yellow')
self.assertEqual(image['foreground'],
'-foreground {} {} #000000 yellow')
class PhotoImageTest(BaseImageTest, AbstractTkTest, unittest.TestCase):
image_class = tkinter.PhotoImage
@classmethod
def setUpClass(cls):
AbstractTkTest.setUpClass.__func__(cls)
cls.testfile = support.findfile('python.gif', subdir='tkinterdata')
def colorlist(self, *args):
if tkinter.TkVersion >= 8.6 and self.wantobjects:
return args
else:
return tkinter._join(args)
def check_create_from_file(self, ext):
testfile = support.findfile('python.' + ext, subdir='tkinterdata')
image = tkinter.PhotoImage('::img::test', master=self.root,
file=testfile)
self.assertEqual(str(image), '::img::test')
self.assertEqual(image.type(), 'photo')
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertEqual(image['data'], '')
self.assertEqual(image['file'], testfile)
self.assertIn('::img::test', self.root.image_names())
del image
support.gc_collect() # For PyPy or other GCs.
self.assertNotIn('::img::test', self.root.image_names())
def check_create_from_data(self, ext):
testfile = support.findfile('python.' + ext, subdir='tkinterdata')
with open(testfile, 'rb') as f:
data = f.read()
image = tkinter.PhotoImage('::img::test', master=self.root,
data=data)
self.assertEqual(str(image), '::img::test')
self.assertEqual(image.type(), 'photo')
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertEqual(image['data'], data if self.wantobjects
else data.decode('latin1'))
self.assertEqual(image['file'], '')
self.assertIn('::img::test', self.root.image_names())
del image
support.gc_collect() # For PyPy or other GCs.
self.assertNotIn('::img::test', self.root.image_names())
def test_create_from_ppm_file(self):
self.check_create_from_file('ppm')
def test_create_from_ppm_data(self):
self.check_create_from_data('ppm')
def test_create_from_pgm_file(self):
self.check_create_from_file('pgm')
def test_create_from_pgm_data(self):
self.check_create_from_data('pgm')
def test_create_from_gif_file(self):
self.check_create_from_file('gif')
def test_create_from_gif_data(self):
self.check_create_from_data('gif')
@requires_tk(8, 6)
def test_create_from_png_file(self):
self.check_create_from_file('png')
@requires_tk(8, 6)
def test_create_from_png_data(self):
self.check_create_from_data('png')
def test_configure_data(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['data'], '')
with open(self.testfile, 'rb') as f:
data = f.read()
image.configure(data=data)
self.assertEqual(image['data'], data if self.wantobjects
else data.decode('latin1'))
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
def test_configure_format(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['format'], '')
image.configure(file=self.testfile, format='gif')
self.assertEqual(image['format'], ('gif',) if self.wantobjects
else 'gif')
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
def test_configure_file(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['file'], '')
image.configure(file=self.testfile)
self.assertEqual(image['file'], self.testfile)
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
def test_configure_gamma(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['gamma'], '1.0')
image.configure(gamma=2.0)
self.assertEqual(image['gamma'], '2.0')
def test_configure_width_height(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['width'], '0')
self.assertEqual(image['height'], '0')
image.configure(width=20)
image.configure(height=10)
self.assertEqual(image['width'], '20')
self.assertEqual(image['height'], '10')
self.assertEqual(image.cget('width'), image['width'])
self.assertEqual(image.cget('height'), image['height'])
self.assertRaises(TypeError, image.cget)
self.assertRaises(TypeError, image.cget, 'width', 'height')
self.assertEqual(image.config, image.configure)
self.assertEqual(image.width(), 20)
self.assertEqual(image.height(), 10)
def test_configure_palette(self):
image = tkinter.PhotoImage('::img::test', master=self.root)
self.assertEqual(image['palette'], '')
image.configure(palette=256)
self.assertEqual(image['palette'], '256')
image.configure(palette='3/4/2')
self.assertEqual(image['palette'], '3/4/2')
def test_blank(self):
image = self.create()
image.blank()
self.assertEqual(image.width(), 16)
self.assertEqual(image.height(), 16)
self.assertEqual(image.get(4, 6), self.colorlist(0, 0, 0))
def test_copy(self):
image = self.create()
image2 = image.copy()
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(4, 6), image.get(4, 6))
image2 = image.copy(from_coords=(2, 3, 14, 11))
self.assertEqual(image2.width(), 12)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
image2 = image.copy(from_coords=(2, 3, 14, 11), zoom=2)
self.assertEqual(image2.width(), 24)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(23, 15), image.get(13, 10))
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2)
self.assertEqual(image2.width(), 6)
self.assertEqual(image2.height(), 4)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(5, 3), image.get(12, 9))
self.assertEqual(image2.get(3, 2), image.get(3*2+2, 2*2+3))
image2 = image.copy(from_coords=(2, 3, 14, 11), subsample=2, zoom=3)
self.assertEqual(image2.width(), 18)
self.assertEqual(image2.height(), 12)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(17, 11), image.get(12, 9))
self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3))
self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3))
def test_subsample(self):
image = self.create()
image2 = image.subsample(2, 3)
self.assertEqual(image2.width(), 8)
self.assertEqual(image2.height(), 6)
self.assertEqual(image2.get(2, 2), image.get(4, 6))
image2 = image.subsample(2)
self.assertEqual(image2.width(), 8)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(2, 3), image.get(4, 6))
image2 = image.subsample(2, from_coords=(2, 3, 14, 11))
self.assertEqual(image2.width(), 6)
self.assertEqual(image2.height(), 4)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(5, 3), image.get(12, 9))
self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3))
def test_zoom(self):
image = self.create()
image2 = image.zoom(2, 3)
self.assertEqual(image2.width(), 32)
self.assertEqual(image2.height(), 48)
self.assertEqual(image2.get(8, 18), image.get(4, 6))
self.assertEqual(image2.get(9, 20), image.get(4, 6))
image2 = image.zoom(2)
self.assertEqual(image2.width(), 32)
self.assertEqual(image2.height(), 32)
self.assertEqual(image2.get(8, 12), image.get(4, 6))
self.assertEqual(image2.get(9, 13), image.get(4, 6))
image2 = image.zoom(2, from_coords=(2, 3, 14, 11))
self.assertEqual(image2.width(), 24)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(23, 15), image.get(13, 10))
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
def test_copy_replace(self):
image = self.create()
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image)
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(4, 6), image.get(4, 6))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11))
self.assertEqual(image2.width(), 12)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), shrink=True)
self.assertEqual(image2.width(), 12)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), to=(3, 6))
self.assertEqual(image2.width(), 15)
self.assertEqual(image2.height(), 14)
self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3))
self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10))
self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), to=(0, 0, 100, 50))
self.assertEqual(image2.width(), 100)
self.assertEqual(image2.height(), 50)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
self.assertEqual(image2.get(2+12, 4+8), image.get(2+2, 4+3))
self.assertEqual(image2.get(2+12*2, 4), image.get(2+2, 4+3))
self.assertEqual(image2.get(2, 4+8*3), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), zoom=2)
self.assertEqual(image2.width(), 24)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(23, 15), image.get(13, 10))
self.assertEqual(image2.get(2*2, 4*2), image.get(2+2, 4+3))
self.assertEqual(image2.get(2*2+1, 4*2+1), image.get(6+2, 2+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2)
self.assertEqual(image2.width(), 6)
self.assertEqual(image2.height(), 4)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(5, 3), image.get(12, 9))
self.assertEqual(image2.get(1, 2), image.get(1*2+2, 2*2+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.copy_replace(image, from_coords=(2, 3, 14, 11), subsample=2, zoom=3)
self.assertEqual(image2.width(), 18)
self.assertEqual(image2.height(), 12)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(17, 11), image.get(12, 9))
self.assertEqual(image2.get(3*3, 2*3), image.get(3*2+2, 2*2+3))
self.assertEqual(image2.get(3*3+2, 2*3+2), image.get(3*2+2, 2*2+3))
self.assertEqual(image2.get(1*3, 2*3), image.get(1*2+2, 2*2+3))
self.assertEqual(image2.get(1*3+2, 2*3+2), image.get(1*2+2, 2*2+3))
def checkImgTrans(self, image, expected):
actual = {(x, y)
for x in range(image.width())
for y in range(image.height())
if image.transparency_get(x, y)}
self.assertEqual(actual, expected)
def test_copy_replace_compositingrule(self):
image1 = tkinter.PhotoImage(master=self.root, width=2, height=2)
image1.blank()
image1.put('black', to=(0, 0, 2, 2))
image1.transparency_set(0, 0, True)
# default compositingrule
image2 = tkinter.PhotoImage(master=self.root, width=3, height=3)
image2.blank()
image2.put('white', to=(0, 0, 2, 2))
image2.copy_replace(image1, to=(1, 1))
self.checkImgTrans(image2, {(0, 2), (2, 0)})
image3 = tkinter.PhotoImage(master=self.root, width=3, height=3)
image3.blank()
image3.put('white', to=(0, 0, 2, 2))
image3.copy_replace(image1, to=(1, 1), compositingrule='overlay')
self.checkImgTrans(image3, {(0, 2), (2, 0)})
image4 = tkinter.PhotoImage(master=self.root, width=3, height=3)
image4.blank()
image4.put('white', to=(0, 0, 2, 2))
image4.copy_replace(image1, to=(1, 1), compositingrule='set')
self.checkImgTrans(image4, {(0, 2), (1, 1), (2, 0)})
def test_put(self):
image = self.create()
image.put('{red green} {blue yellow}', to=(4, 6))
self.assertEqual(image.get(4, 6), self.colorlist(255, 0, 0))
self.assertEqual(image.get(5, 6),
self.colorlist(0, 128 if tkinter.TkVersion >= 8.6
else 255, 0))
self.assertEqual(image.get(4, 7), self.colorlist(0, 0, 255))
self.assertEqual(image.get(5, 7), self.colorlist(255, 255, 0))
image.put((('#f00', '#00ff00'), ('#000000fff', '#ffffffff0000')))
self.assertEqual(image.get(0, 0), self.colorlist(255, 0, 0))
self.assertEqual(image.get(1, 0), self.colorlist(0, 255, 0))
self.assertEqual(image.get(0, 1), self.colorlist(0, 0, 255))
self.assertEqual(image.get(1, 1), self.colorlist(255, 255, 0))
def test_get(self):
image = self.create()
self.assertEqual(image.get(4, 6), self.colorlist(62, 116, 162))
self.assertEqual(image.get(0, 0), self.colorlist(0, 0, 0))
self.assertEqual(image.get(15, 15), self.colorlist(0, 0, 0))
self.assertRaises(tkinter.TclError, image.get, -1, 0)
self.assertRaises(tkinter.TclError, image.get, 0, -1)
self.assertRaises(tkinter.TclError, image.get, 16, 15)
self.assertRaises(tkinter.TclError, image.get, 15, 16)
def test_read(self):
# Due to the Tk bug https://core.tcl-lang.org/tk/tktview/1576528
# the -from option does not work correctly for GIF and PNG files.
# Use the PPM file for this test.
testfile = support.findfile('python.ppm', subdir='tkinterdata')
image = tkinter.PhotoImage(master=self.root, file=testfile)
image2 = tkinter.PhotoImage(master=self.root)
image2.read(testfile)
self.assertEqual(image2.type(), 'photo')
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(0, 0))
self.assertEqual(image2.get(4, 6), image.get(4, 6))
self.assertRaises(tkinter.TclError, image2.read, self.testfile, 'ppm')
image2 = tkinter.PhotoImage(master=self.root)
image2.read(testfile, from_coords=(2, 3, 14, 11))
self.assertEqual(image2.width(), 12)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root, file=testfile)
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
image2.read(testfile, from_coords=(2, 3, 14, 11), shrink=True)
self.assertEqual(image2.width(), 12)
self.assertEqual(image2.height(), 8)
self.assertEqual(image2.get(0, 0), image.get(2, 3))
self.assertEqual(image2.get(11, 7), image.get(13, 10))
self.assertEqual(image2.get(2, 4), image.get(2+2, 4+3))
image2 = tkinter.PhotoImage(master=self.root)
image2.read(testfile, from_coords=(2, 3, 14, 11), to=(3, 6))
self.assertEqual(image2.type(), 'photo')
self.assertEqual(image2.width(), 15)
self.assertEqual(image2.height(), 14)
self.assertEqual(image2.get(0+3, 0+6), image.get(2, 3))
self.assertEqual(image2.get(11+3, 7+6), image.get(13, 10))
self.assertEqual(image2.get(2+3, 4+6), image.get(2+2, 4+3))
def test_write(self):
filename = os_helper.TESTFN
import locale
if locale.getlocale()[0] is None:
# Tcl uses Latin1 in the C locale
filename = os_helper.TESTFN_ASCII
image = self.create()
self.addCleanup(os_helper.unlink, filename)
image.write(filename)
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
format='ppm', file=filename)
self.assertEqual(str(image2), '::img::test2')
self.assertEqual(image2.type(), 'photo')
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(0, 0))
self.assertEqual(image2.get(4, 6), image.get(4, 6))
image.write(filename, format='gif', from_coords=(4, 6, 6, 9))
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
format='gif', file=filename)
self.assertEqual(str(image3), '::img::test3')
self.assertEqual(image3.type(), 'photo')
self.assertEqual(image3.width(), 2)
self.assertEqual(image3.height(), 3)
self.assertEqual(image3.get(0, 0), image.get(4, 6))
self.assertEqual(image3.get(1, 2), image.get(5, 8))
image.write(filename, background='#ff0000')
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
format='ppm', file=filename)
self.assertEqual(image4.get(0, 0), (255, 0, 0) if self.wantobjects else '255 0 0')
self.assertEqual(image4.get(4, 6), image.get(4, 6))
image.write(filename, grayscale=True)
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
format='ppm', file=filename)
c = image5.get(4, 6)
if not self.wantobjects:
c = c.split()
self.assertTrue(c[0] == c[1] == c[2], c)
def test_data(self):
image = self.create()
data = image.data()
self.assertIsInstance(data, tuple)
for row in data:
self.assertIsInstance(row, str)
c = image.get(4, 6)
if not self.wantobjects:
c = tuple(map(int, c.split()))
self.assertEqual(data[6].split()[4], '#%02x%02x%02x' % c)
data = image.data('ppm')
image2 = tkinter.PhotoImage('::img::test2', master=self.root,
format='ppm', data=data)
self.assertEqual(str(image2), '::img::test2')
self.assertEqual(image2.type(), 'photo')
self.assertEqual(image2.width(), 16)
self.assertEqual(image2.height(), 16)
self.assertEqual(image2.get(0, 0), image.get(0, 0))
self.assertEqual(image2.get(4, 6), image.get(4, 6))
data = image.data(format='gif', from_coords=(4, 6, 6, 9))
image3 = tkinter.PhotoImage('::img::test3', master=self.root,
format='gif', data=data)
self.assertEqual(str(image3), '::img::test3')
self.assertEqual(image3.type(), 'photo')
self.assertEqual(image3.width(), 2)
self.assertEqual(image3.height(), 3)
self.assertEqual(image3.get(0, 0), image.get(4, 6))
self.assertEqual(image3.get(1, 2), image.get(5, 8))
data = image.data('ppm', background='#ff0000')
image4 = tkinter.PhotoImage('::img::test4', master=self.root,
format='ppm', data=data)
self.assertEqual(image4.get(0, 0), (255, 0, 0) if self.wantobjects else '255 0 0')
self.assertEqual(image4.get(4, 6), image.get(4, 6))
data = image.data('ppm', grayscale=True)
image5 = tkinter.PhotoImage('::img::test5', master=self.root,
format='ppm', data=data)
c = image5.get(4, 6)
if not self.wantobjects:
c = c.split()
self.assertTrue(c[0] == c[1] == c[2], c)
def test_transparency(self):
image = self.create()
self.assertIs(image.transparency_get(0, 0), True)
self.assertIs(image.transparency_get(4, 6), False)
image.transparency_set(4, 6, True)
self.assertIs(image.transparency_get(4, 6), True)
image.transparency_set(4, 6, False)
self.assertIs(image.transparency_get(4, 6), False)
self.assertRaises(tkinter.TclError, image.transparency_get, -1, 0)
self.assertRaises(tkinter.TclError, image.transparency_get, 16, 0)
self.assertRaises(tkinter.TclError, image.transparency_set, -1, 0, True)
self.assertRaises(tkinter.TclError, image.transparency_set, 16, 0, True)
self.assertRaises(TypeError, image.transparency_get, 0)
self.assertRaises(TypeError, image.transparency_get, 0, 0, 0)
self.assertRaises(TypeError, image.transparency_set, 0, 0)
self.assertRaises(TypeError, image.transparency_set, 0, 0, True, 0)
if __name__ == "__main__":
unittest.main()
+47
View File
@@ -0,0 +1,47 @@
import os
import sys
import unittest
import test.support as test_support
from test.support import os_helper
from test.test_tkinter.support import setUpModule # noqa: F401
from tkinter import Tcl, TclError
test_support.requires('gui')
class TkLoadTest(unittest.TestCase):
@unittest.skipIf('DISPLAY' not in os.environ, 'No $DISPLAY set.')
def testLoadTk(self):
tcl = Tcl()
self.assertRaises(TclError,tcl.winfo_geometry)
tcl.loadtk()
self.assertEqual('1x1+0+0', tcl.winfo_geometry())
tcl.destroy()
def testLoadTkFailure(self):
old_display = None
if sys.platform.startswith(('win', 'darwin', 'cygwin')):
# no failure possible on windows?
# XXX Maybe on tk older than 8.4.13 it would be possible,
# see tkinter.h.
return
with os_helper.EnvironmentVarGuard() as env:
if 'DISPLAY' in os.environ:
del env['DISPLAY']
# on some platforms, deleting environment variables
# doesn't actually carry through to the process level
# because they don't support unsetenv
# If that's the case, abort.
with os.popen('echo $DISPLAY') as pipe:
display = pipe.read().strip()
if display:
return
tcl = Tcl()
self.assertRaises(TclError, tcl.winfo_geometry)
self.assertRaises(TclError, tcl.loadtk)
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -0,0 +1,37 @@
import unittest
import tkinter
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractDefaultRootTest
from tkinter.commondialog import Dialog
from tkinter.messagebox import showinfo
requires('gui')
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_showinfo(self):
def test_callback(dialog, master):
nonlocal ismapped
master.update()
ismapped = master.winfo_ismapped()
raise ZeroDivisionError
with swap_attr(Dialog, '_test_callback', test_callback):
ismapped = None
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
self.assertEqual(ismapped, False)
root = tkinter.Tk()
ismapped = None
self.assertRaises(ZeroDivisionError, showinfo, "Spam", "Egg Information")
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, showinfo, "Spam", "Egg Information")
if __name__ == "__main__":
unittest.main()
+1982
View File
File diff suppressed because it is too large. Load diff
+65
View File
@@ -0,0 +1,65 @@
import unittest
import tkinter
from tkinter.scrolledtext import ScrolledText
from test.support import requires
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest
requires('gui')
class ScrolledTextTest(AbstractTkTest, unittest.TestCase):
def create(self, **kwargs):
st = ScrolledText(self.root, **kwargs)
self.addCleanup(st.destroy)
return st
def test_create(self):
st = self.create(background='red', height=5)
# It is a Text widget held in a Frame together with a Scrollbar.
self.assertIsInstance(st, tkinter.Text)
self.assertIsInstance(st.frame, tkinter.Frame)
self.assertIsInstance(st.vbar, tkinter.Scrollbar)
self.assertEqual(st.winfo_parent(), str(st.frame))
# str() returns the frame, so that geometry managers manage it.
self.assertEqual(str(st), str(st.frame))
# Keyword options configure the Text.
self.assertEqual(str(st['background']), 'red')
self.assertEqual(st['height'], 5 if self.wantobjects else '5')
def test_text_methods(self):
st = self.create()
st.insert('1.0', 'hello\nworld')
self.assertEqual(st.get('1.0', 'end-1c'), 'hello\nworld')
self.assertEqual(st.index('end-1c'), '2.5')
st.delete('1.0', 'end')
self.assertEqual(st.get('1.0', 'end-1c'), '')
def test_geometry_methods(self):
st = self.create()
# configure is not redirected; it configures the Text.
st.configure(height=8)
self.assertEqual(st['height'], 8 if self.wantobjects else '8')
# Pack, Grid and Place methods are redirected to the frame.
st.pack()
self.root.update()
self.assertEqual(st.frame.winfo_manager(), 'pack')
self.assertEqual(st.pack_info(), st.frame.pack_info())
st.pack_forget()
self.assertEqual(st.frame.winfo_manager(), '')
def test_scrollbar(self):
st = self.create(height=5)
st.pack()
st.insert('1.0', '\n'.join(map(str, range(100))))
self.root.update()
# The scrollbar tracks the text view.
self.assertEqual(st.vbar.get(), st.yview())
st.yview_moveto(1.0)
self.root.update()
self.assertEqual(st.vbar.get()[1], 1.0)
if __name__ == "__main__":
unittest.main()
+279
View File
@@ -0,0 +1,279 @@
import unittest
import tkinter
from tkinter import messagebox
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (AbstractDefaultRootTest,
AbstractDialogTest, AbstractTkTest)
from tkinter.simpledialog import (Dialog, SimpleDialog,
askinteger, askfloat, askstring,
_QueryInteger, _QueryFloat, _QueryString)
requires('gui')
class SimpleDialogTest(AbstractTkTest, unittest.TestCase):
# The root window is not hidden here (cf. AbstractDialogTest): SimpleDialog
# makes its window transient for the master unconditionally, and a
# transient of a hidden window is never mapped on Windows, so that go()
# would block in wait_visibility().
# SimpleDialog's modal loop is in go(); its bindings are exercised here by
# generating events on the constructed dialog, without entering the loop.
def create(self, **kw):
kw.setdefault('text', 'Question?')
kw.setdefault('buttons', ['Yes', 'No'])
kw.setdefault('default', 0)
kw.setdefault('cancel', 1)
d = SimpleDialog(self.root, **kw)
self.addCleanup(lambda: d.root.winfo_exists() and d.root.destroy())
return d
def test_message(self):
# The text is shown in a message widget.
d = self.create(text='Hello?')
self.assertEqual(d.message.winfo_class(), 'Message')
self.assertEqual(str(d.message.cget('text')), 'Hello?')
def test_class_name(self):
# class_ sets the Tk class of the dialog window.
d = self.create(class_='MyDialog')
self.assertEqual(d.root.winfo_class(), 'MyDialog')
def test_button(self):
# Pressing a button records its index.
d = self.create(buttons=['Yes', 'No'])
d.frame.winfo_children()[1].invoke() # "No"
self.assertEqual(d.num, 1)
def test_default_button(self):
# The default button is drawn with a raised border.
d = self.create(buttons=['Yes', 'No'], default=0)
self.assertEqual(str(d.frame.winfo_children()[0].cget('relief')), 'ridge')
def test_return_activates_default(self):
# <Return> invokes the default button.
d = self.create() # default 0
self.require_mapped(d.root)
d.root.update()
d.root.focus_force()
d.root.event_generate('<Return>')
d.root.update()
self.assertEqual(d.num, 0)
def test_return_no_default(self):
# With no default button, <Return> rings the bell and leaves the dialog
# open instead of activating a button.
d = self.create(default=None)
self.require_mapped(d.root)
bells = []
with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)):
d.root.focus_force()
d.root.event_generate('<Return>')
d.root.update()
self.assertTrue(bells) # rang the bell
self.assertIsNone(d.num)
self.assertTrue(d.root.winfo_exists())
def test_wm_delete_cancels(self):
# Closing the window through the window manager records the cancel index.
d = self.create() # cancel 1
d.wm_delete_window()
self.assertEqual(d.num, 1)
def test_wm_delete_no_cancel(self):
# With no cancel index, closing the window through the window manager
# rings the bell and leaves the dialog open instead of recording an
# index.
d = self.create(default=None, cancel=None)
d.root.update()
bells = []
with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)):
d.wm_delete_window()
d.root.update()
self.assertTrue(bells) # rang the bell
self.assertIsNone(d.num)
self.assertTrue(d.root.winfo_exists())
def test_go(self):
# go() runs the modal loop and returns the chosen button's index.
d = self.create()
d.root.after(1, lambda: d.done(0))
self.assertEqual(d.go(), 0)
class DialogTest(AbstractDialogTest, unittest.TestCase):
# Dialog is a base class for custom dialogs; exercise it via _QueryInteger.
def open(self, **kw):
with swap_attr(Dialog, 'wait_window', staticmethod(lambda w: None)):
d = _QueryInteger('Title', 'Prompt', parent=self.root, **kw)
self.addCleanup(lambda: d.winfo_exists() and d.destroy())
return d
def buttons(self, d):
# Map the button box's buttons by their label.
return {str(b.cget('text')): b
for frame in d.winfo_children()
for b in frame.winfo_children()
if b.winfo_class() == 'Button'}
def test_background(self):
# The classic dialog keeps the default Toplevel background.
d = self.open()
ref = tkinter.Toplevel(self.root)
self.addCleanup(ref.destroy)
self.assertEqual(str(d.cget('background')), str(ref.cget('background')))
def test_buttons(self):
# The button box has OK (the default) and Cancel buttons.
buttons = self.buttons(self.open())
self.assertEqual(set(buttons), {'OK', 'Cancel'})
self.assertEqual(str(buttons['OK'].cget('default')), 'active')
def test_ok(self):
# The OK button validates the entry and stores the result.
d = self.open()
d.entry.insert(0, '42')
self.buttons(d)['OK'].invoke()
self.assertEqual(d.result, 42)
self.assertFalse(d.winfo_exists()) # The dialog closed.
def test_cancel(self):
# The Cancel button closes the dialog without a result.
d = self.open()
self.buttons(d)['Cancel'].invoke()
self.assertIsNone(d.result)
self.assertFalse(d.winfo_exists())
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_askinteger(self):
@staticmethod
def mock_wait_window(w):
nonlocal ismapped
ismapped = w.master.winfo_ismapped()
w.destroy()
with swap_attr(Dialog, 'wait_window', mock_wait_window):
ismapped = None
askinteger("Go To Line", "Line number")
self.assertEqual(ismapped, False)
root = tkinter.Tk()
ismapped = None
askinteger("Go To Line", "Line number")
self.assertEqual(ismapped, True)
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
class QueryDialogTest(AbstractDialogTest, unittest.TestCase):
# The query dialogs are modal: their __init__ blocks in wait_window().
# Mock that out so the dialog stays alive and can be driven with generated
# events, exercising the <Return>/<Escape> bindings and the validation.
def open(self, query, **kw):
with swap_attr(Dialog, 'wait_window', staticmethod(lambda w: None)):
d = query("Title", "Prompt", parent=self.root, **kw)
self.addCleanup(lambda: d.winfo_exists() and d.destroy())
d.focus_force()
d.update()
return d
def enter(self, d, value, key='<Return>'):
d.entry.delete(0, 'end')
d.entry.insert(0, value)
d.event_generate(key)
d.update()
def test_initialvalue(self):
# The entry is pre-filled with the initial value, which is accepted.
d = self.open(_QueryInteger, initialvalue=42)
self.assertEqual(d.entry.get(), '42')
d.event_generate('<Return>')
d.update()
self.assertEqual(d.result, 42)
def test_show(self):
# _QueryString hides the entered text when show is given.
d = self.open(_QueryString, show='*')
self.assertEqual(str(d.entry.cget('show')), '*')
def test_return_accepts(self):
for query, value, expected in [
(_QueryInteger, '42', 42),
(_QueryFloat, '1.5', 1.5),
(_QueryString, 'spam', 'spam'),
]:
with self.subTest(query=query.__name__):
d = self.open(query)
self.enter(d, value)
self.assertEqual(d.result, expected)
self.assertFalse(d.winfo_exists()) # The dialog closed.
def test_escape_cancels(self):
d = self.open(_QueryString)
self.enter(d, 'spam', '<Escape>')
self.assertIsNone(d.result)
self.assertFalse(d.winfo_exists())
def test_invalid_value(self):
warnings = []
d = self.open(_QueryInteger)
with swap_attr(messagebox, 'showwarning',
lambda *a, **k: warnings.append(a)):
self.enter(d, 'not a number')
self.assertIsNone(d.result)
self.assertTrue(d.winfo_exists()) # The dialog stays open.
self.assertTrue(warnings)
def test_out_of_range(self):
warnings = []
d = self.open(_QueryInteger, minvalue=10, maxvalue=20)
with swap_attr(messagebox, 'showwarning',
lambda *a, **k: warnings.append(a)):
self.enter(d, '5') # Below the minimum.
self.assertIsNone(d.result)
self.enter(d, '25') # Above the maximum.
self.assertIsNone(d.result)
self.assertTrue(d.winfo_exists())
self.assertEqual(len(warnings), 2)
def test_boundary_values_accepted(self):
# The min/max checks are inclusive: a value equal to a bound passes.
d = self.open(_QueryInteger, minvalue=10, maxvalue=20)
self.enter(d, '10') # Exactly the minimum.
self.assertEqual(d.result, 10)
self.assertFalse(d.winfo_exists())
d = self.open(_QueryInteger, minvalue=10, maxvalue=20)
self.enter(d, '20') # Exactly the maximum.
self.assertEqual(d.result, 20)
self.assertFalse(d.winfo_exists())
def run_ask(self, ask, value, **kw):
# Drive a modal ask* function: enter a value and accept it.
def accept(d):
d.entry.delete(0, 'end')
d.entry.insert(0, value)
d.ok()
with swap_attr(Dialog, 'wait_window', staticmethod(accept)):
return ask('Title', 'Prompt', parent=self.root, **kw)
def test_ask_functions(self):
self.assertEqual(self.run_ask(askinteger, '42'), 42)
self.assertEqual(self.run_ask(askfloat, '1.5'), 1.5)
self.assertEqual(self.run_ask(askstring, 'spam'), 'spam')
def test_ask_cancelled(self):
# A cancelled ask* returns None.
with swap_attr(Dialog, 'wait_window', staticmethod(lambda d: d.cancel())):
self.assertIsNone(askstring('Title', 'Prompt', parent=self.root))
if __name__ == "__main__":
unittest.main()
+747
View File
@@ -0,0 +1,747 @@
import unittest
import tkinter
from tkinter import TclError
from test.support import requires
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest, requires_tk
requires('gui')
class TextTest(AbstractTkTest, unittest.TestCase):
def setUp(self):
super().setUp()
self.text = tkinter.Text(self.root)
self.text.pack()
def test_debug(self):
text = self.text
olddebug = text.debug()
try:
text.debug(False)
self.assertIs(text.debug(), False)
text.debug(True)
self.assertIs(text.debug(), True)
finally:
text.debug(olddebug)
self.assertEqual(text.debug(), olddebug)
def test_index(self):
text = self.text
text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
self.assertEqual(text.index('1.0'), '1.0')
self.assertEqual(text.index('1.end'), '1.11')
self.assertEqual(text.index('end'), '3.0')
self.assertEqual(text.index('2.5'), '2.5')
# Index past the end of a line is clamped to its end.
self.assertEqual(text.index('1.100'), '1.11')
self.assertRaises(TclError, text.index, 'invalid')
self.assertRaises(TypeError, text.index)
self.assertRaises(TypeError, text.index, '1.0', '2.5')
def test_compare(self):
text = self.text
text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
self.assertIs(text.compare('1.0', '<', '2.0'), True)
self.assertIs(text.compare('2.0', '<', '1.0'), False)
self.assertIs(text.compare('1.5', '==', '1.5'), True)
self.assertIs(text.compare('1.5', '!=', '1.5'), False)
self.assertIs(text.compare('2.0', '>=', '2.0'), True)
self.assertIs(text.compare('1.0', '<=', 'end'), True)
self.assertRaises(TclError, text.compare, '1.0', 'invalid', '2.0')
self.assertRaises(TypeError, text.compare, '1.0', '<')
self.assertRaises(TypeError, text.compare, '1.0', '<', '2.0', '3.0')
def test_insert_get_delete(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n')
self.assertEqual(text.get('1.0', '1.5'), 'Lorem')
self.assertEqual(text.get('1.6'), 'i') # single character
# insert before an existing index
text.insert('1.0', '*** ')
self.assertEqual(text.get('1.0', 'end'), '*** Lorem ipsum\n')
text.delete('1.0', '1.4')
self.assertEqual(text.get('1.0', 'end'), 'Lorem ipsum\n')
text.delete('1.5') # delete a single character
self.assertEqual(text.get('1.0', 'end'), 'Loremipsum\n')
self.assertRaises(TypeError, text.get)
self.assertRaises(TypeError, text.get, '1.0', '1.5', 'end')
self.assertRaises(TypeError, text.delete, '1.0', '1.5', 'end')
def test_insert_with_tags(self):
text = self.text
text.insert('1.0', 'hello', 'a', ' ', ('a', 'b'), 'world', 'b')
self.assertEqual(text.get('1.0', 'end'), 'hello world\n')
self.assertEqual([str(i) for i in text.tag_ranges('a')],
['1.0', '1.6'])
self.assertEqual([str(i) for i in text.tag_ranges('b')],
['1.5', '1.11'])
def test_replace(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
text.replace('1.0', '1.5', 'Hello')
self.assertEqual(text.get('1.0', 'end'), 'Hello ipsum\n')
text.replace('1.6', 'end - 1 char', 'world')
self.assertEqual(text.get('1.0', 'end'), 'Hello world\n')
self.assertRaises(TclError, text.replace, 'invalid', '1.5', 'x')
# The first index must not be after the second.
self.assertRaises(TclError, text.replace, '1.5', '1.0', 'x')
self.assertRaises(TypeError, text.replace, '1.0', '1.5')
def test_mark(self):
text = self.text
text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
# 'insert' and 'current' marks always exist.
self.assertIn('insert', text.mark_names())
self.assertIn('current', text.mark_names())
text.mark_set('here', '1.5')
self.assertIn('here', text.mark_names())
self.assertEqual(text.index('here'), '1.5')
text.mark_set('here', '2.3')
self.assertEqual(text.index('here'), '2.3')
text.mark_unset('here')
self.assertNotIn('here', text.mark_names())
# Unsetting a non-existent mark is not an error.
text.mark_unset('nonexistent')
self.assertRaises(TclError, text.mark_set, 'm', 'invalid')
self.assertRaises(TypeError, text.mark_set, 'm')
self.assertRaises(TypeError, text.mark_set, 'm', '1.0', '2.0')
def test_mark_gravity(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
text.mark_set('here', '1.5')
# The default gravity is right.
self.assertEqual(text.mark_gravity('here'), 'right')
text.mark_gravity('here', 'left')
self.assertEqual(text.mark_gravity('here'), 'left')
# With left gravity the mark stays before inserted text.
text.insert('here', 'XXX')
self.assertEqual(text.index('here'), '1.5')
# With right gravity the mark moves after inserted text.
text.mark_gravity('here', 'right')
text.insert('here', 'YYY')
self.assertEqual(text.index('here'), '1.8')
self.assertRaises(TclError, text.mark_gravity, 'nonexistent')
self.assertRaises(TclError, text.mark_gravity, 'here', 'invalid')
self.assertRaises(TypeError, text.mark_gravity)
self.assertRaises(TypeError, text.mark_gravity, 'here', 'left', 'extra')
def test_mark_next_previous(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
# Keep the builtin 'insert' and 'current' marks at 1.0 so they do
# not interfere with the queries below.
text.mark_set('insert', '1.0')
text.mark_set('current', '1.0')
text.mark_set('m1', '1.3')
text.mark_set('m2', '1.7')
# mark_next finds a mark at a position at or after the index,
# mark_previous finds one at a position strictly before it.
self.assertEqual(text.mark_next('1.1'), 'm1')
self.assertEqual(text.mark_next('1.3'), 'm1')
self.assertEqual(text.mark_next('1.4'), 'm2')
self.assertIsNone(text.mark_next('1.8'))
self.assertEqual(text.mark_previous('1.4'), 'm1')
self.assertEqual(text.mark_previous('1.7'), 'm1')
self.assertEqual(text.mark_previous('end'), 'm2')
self.assertIsNone(text.mark_previous('1.0'))
self.assertRaises(TclError, text.mark_next, 'invalid')
self.assertRaises(TclError, text.mark_previous, 'invalid')
self.assertRaises(TypeError, text.mark_next)
self.assertRaises(TypeError, text.mark_previous)
self.assertRaises(TypeError, text.mark_next, '1.0', '2.0')
self.assertRaises(TypeError, text.mark_previous, '1.0', '2.0')
def test_tag_add_remove_ranges(self):
text = self.text
text.insert('1.0', 'Lorem ipsum\ndolor sit amet')
self.assertEqual(text.tag_ranges('sel'), ())
text.tag_add('big', '1.0', '1.5')
self.assertEqual([str(i) for i in text.tag_ranges('big')],
['1.0', '1.5'])
# Add a second, disjoint range.
text.tag_add('big', '2.0', '2.5')
self.assertEqual([str(i) for i in text.tag_ranges('big')],
['1.0', '1.5', '2.0', '2.5'])
text.tag_remove('big', '1.0', '1.3')
self.assertEqual([str(i) for i in text.tag_ranges('big')],
['1.3', '1.5', '2.0', '2.5'])
# tag_ranges of an undefined tag is empty, not an error.
self.assertEqual(text.tag_ranges('nonexistent'), ())
self.assertRaises(TclError, text.tag_add, 'big', 'invalid')
self.assertRaises(TypeError, text.tag_add, 'big')
self.assertRaises(TclError, text.tag_remove, 'big', 'invalid')
self.assertRaises(TypeError, text.tag_remove, 'big', '1.0', '2.0', '3.0')
self.assertRaises(TypeError, text.tag_ranges, 'big', 'extra')
def test_tag_names(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
text.tag_add('a', '1.0', '1.5')
text.tag_add('b', '1.3', '1.8')
# 'sel' always exists; order reflects priority (creation order).
self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'})
# Tags applied to the character at the given index.
self.assertEqual(set(text.tag_names('1.4')), {'a', 'b'})
self.assertEqual(set(text.tag_names('1.0')), {'a'})
self.assertEqual(set(text.tag_names('1.10')), set())
self.assertRaises(TclError, text.tag_names, 'invalid')
self.assertRaises(TypeError, text.tag_names, '1.0', '2.0')
def test_tag_nextrange_prevrange(self):
text = self.text
text.insert('1.0', 'Lorem ipsum dolor')
text.tag_add('a', '1.0', '1.5')
text.tag_add('a', '1.12', '1.17')
self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.0')],
['1.0', '1.5'])
self.assertEqual([str(i) for i in text.tag_nextrange('a', '1.5')],
['1.12', '1.17'])
self.assertEqual(text.tag_nextrange('a', '1.17'), ())
self.assertEqual([str(i) for i in text.tag_prevrange('a', 'end')],
['1.12', '1.17'])
self.assertEqual([str(i) for i in text.tag_prevrange('a', '1.12')],
['1.0', '1.5'])
self.assertEqual(text.tag_prevrange('a', '1.0'), ())
# An undefined tag has no ranges, but the index must be valid.
self.assertEqual(text.tag_nextrange('nonexistent', '1.0'), ())
self.assertRaises(TclError, text.tag_nextrange, 'a', 'invalid')
self.assertRaises(TclError, text.tag_prevrange, 'a', 'invalid')
self.assertRaises(TypeError, text.tag_nextrange, 'a')
self.assertRaises(TypeError, text.tag_prevrange, 'a')
self.assertRaises(TypeError, text.tag_nextrange, 'a', '1.0', '2.0', '3.0')
self.assertRaises(TypeError, text.tag_prevrange, 'a', '1.0', '2.0', '3.0')
def test_tag_delete(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
text.tag_add('a', '1.0', '1.5')
text.tag_add('b', '1.6', '1.11')
self.assertEqual(set(text.tag_names()), {'sel', 'a', 'b'})
text.tag_delete('a', 'b')
self.assertEqual(set(text.tag_names()), {'sel'})
self.assertEqual(text.tag_ranges('a'), ())
def test_tag_raise_lower(self):
text = self.text
text.tag_configure('a', foreground='red')
text.tag_configure('b', foreground='green')
text.tag_configure('c', foreground='blue')
# Creation order is lowest-to-highest priority; tag_names lists
# them in increasing priority order.
self.assertEqual(text.tag_names(), ('sel', 'a', 'b', 'c'))
text.tag_raise('a')
self.assertEqual(text.tag_names(), ('sel', 'b', 'c', 'a'))
text.tag_lower('a')
self.assertEqual(text.tag_names(), ('a', 'sel', 'b', 'c'))
text.tag_raise('a', 'b')
self.assertEqual(text.tag_names(), ('sel', 'b', 'a', 'c'))
text.tag_lower('c', 'b')
self.assertEqual(text.tag_names(), ('sel', 'c', 'b', 'a'))
self.assertRaises(TclError, text.tag_raise, 'nonexistent')
self.assertRaises(TclError, text.tag_lower, 'nonexistent')
self.assertRaises(TclError, text.tag_raise, 'a', 'nonexistent')
self.assertRaises(TypeError, text.tag_raise)
self.assertRaises(TypeError, text.tag_raise, 'a', 'b', 'c')
self.assertRaises(TypeError, text.tag_lower, 'a', 'b', 'c')
def test_tag_cget_configure(self):
text = self.text
text.tag_configure('a', foreground='red', underline=True)
self.assertEqual(str(text.tag_cget('a', 'foreground')), 'red')
self.assertIn(text.tag_cget('a', 'underline'), (1, '1', True))
# tag_cget normalizes the option name (no leading hyphen needed).
self.assertEqual(str(text.tag_cget('a', '-foreground')), 'red')
# configure() query returns the full option spec.
self.assertEqual(text.tag_configure('a', 'foreground')[-1], 'red')
text.tag_configure('a', foreground='blue')
self.assertEqual(str(text.tag_cget('a', 'foreground')), 'blue')
self.assertRaises(TclError, text.tag_cget, 'nonexistent',
'foreground')
self.assertRaises(TypeError, text.tag_cget, 'a')
self.assertRaises(TypeError, text.tag_cget, 'a', 'foreground', 'extra')
def test_edit_modified(self):
text = self.text
self.assertEqual(text.edit_modified(), 0)
text.insert('1.0', 'spam')
self.assertEqual(text.edit_modified(), 1)
text.edit_modified(False)
self.assertEqual(text.edit_modified(), 0)
text.edit_modified(True)
self.assertEqual(text.edit_modified(), 1)
def test_edit_undo_redo(self):
text = self.text
text.configure(undo=True)
text.insert('1.0', 'spam')
text.edit_separator()
text.insert('end', ' eggs')
self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n')
text.edit_undo()
self.assertEqual(text.get('1.0', 'end'), 'spam\n')
text.edit_redo()
self.assertEqual(text.get('1.0', 'end'), 'spam eggs\n')
# An empty undo stack raises.
text.edit_reset()
self.assertRaises(TclError, text.edit_undo)
def test_dump(self):
text = self.text
text.insert('1.0', 'hello')
text.tag_add('a', '1.0', '1.3')
text.mark_set('here', '1.2')
dump = text.dump('1.0', 'end')
self.assertIsInstance(dump, list)
for item in dump:
self.assertIsInstance(item, tuple)
self.assertEqual(len(item), 3)
kinds = {item[0] for item in dump}
self.assertIn('text', kinds)
self.assertIn('tagon', kinds)
self.assertIn('mark', kinds)
# Filtering by kind.
text_only = text.dump('1.0', 'end', text=True)
self.assertEqual({item[0] for item in text_only}, {'text'})
self.assertEqual(''.join(item[1] for item in text_only), 'hello\n')
# The command callback receives each triple and suppresses the result.
collected = []
result = text.dump('1.0', 'end', text=True,
command=lambda *a: collected.append(a))
self.assertIsNone(result)
self.assertEqual(''.join(key_value_index[1]
for key_value_index in collected), 'hello\n')
self.assertRaises(TclError, text.dump, 'invalid')
self.assertRaises(TypeError, text.dump)
self.assertRaises(TypeError, text.dump, '1.0', 'end', None, 'extra')
def test_image(self):
text = self.text
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
text.insert('1.0', 'AB')
name = text.image_create('1.1', image=image)
self.assertIsInstance(name, str)
self.assertIn(name, self.root.splitlist(text.image_names()))
self.assertEqual(str(text.image_cget(name, 'image')), str(image))
text.image_configure(name, align='top')
self.assertEqual(str(text.image_cget(name, 'align')), 'top')
# An embedded image occupies a single index position.
self.assertEqual(text.index('end - 1 char'), '1.3')
# Either a name or an image is required, and the index must be valid.
self.assertRaises(TclError, text.image_create, '1.0')
self.assertRaises(TclError, text.image_create, 'invalid',
image=image)
self.assertRaises(TypeError, text.image_cget, name)
self.assertRaises(TypeError, text.image_cget, name, 'image', 'extra')
def test_window(self):
text = self.text
button = tkinter.Button(text, text='ok')
text.insert('1.0', 'AB')
text.window_create('1.1', window=button)
self.assertEqual(self.root.splitlist(text.window_names()),
(str(button),))
self.assertEqual(text.window_cget('1.1', 'window'), str(button))
text.window_configure('1.1', padx=5)
self.assertEqual(text.window_cget('1.1', 'padx'), 5)
# No embedded window at this index, and the index must be valid.
self.assertRaises(TclError, text.window_cget, '1.0', 'window')
self.assertRaises(TclError, text.window_cget, 'invalid', 'window')
self.assertRaises(TypeError, text.window_cget, '1.1')
self.assertRaises(TypeError, text.window_cget, '1.1', 'window', 'extra')
button.destroy()
def test_tag_configure(self):
text = self.text
tag = 'a'
getint = text.tk.getint
getboolean = text.tk.getboolean
# Color options.
for opt in ('foreground', 'background'):
text.tag_configure(tag, **{opt: '#ff0000'})
self.assertEqual(str(text.tag_cget(tag, opt)), '#ff0000')
# Stipple (bitmap) options.
for opt in ('bgstipple', 'fgstipple'):
text.tag_configure(tag, **{opt: 'gray50'})
self.assertEqual(str(text.tag_cget(tag, opt)), 'gray50')
# Enumerated options.
for opt, value in (('justify', 'center'), ('wrap', 'word'),
('relief', 'raised'), ('tabstyle', 'wordprocessor'),
('offset', '5')):
text.tag_configure(tag, **{opt: value})
self.assertEqual(str(text.tag_cget(tag, 'justify')), 'center')
self.assertEqual(str(text.tag_cget(tag, 'wrap')), 'word')
self.assertEqual(str(text.tag_cget(tag, 'relief')), 'raised')
self.assertEqual(str(text.tag_cget(tag, 'tabstyle')), 'wordprocessor')
# Boolean options.
for opt in ('underline', 'overstrike', 'elide'):
text.tag_configure(tag, **{opt: True})
self.assertIs(getboolean(text.tag_cget(tag, opt)), True)
text.tag_configure(tag, **{opt: False})
self.assertIs(getboolean(text.tag_cget(tag, opt)), False)
# Screen-distance (pixel) options.
for opt in ('borderwidth', 'lmargin1', 'lmargin2', 'rmargin',
'spacing1', 'spacing2', 'spacing3'):
text.tag_configure(tag, **{opt: 7})
self.assertEqual(getint(text.tag_cget(tag, opt)), 7)
# Other options.
text.tag_configure(tag, font='Helvetica 12')
self.assertEqual(str(text.tag_cget(tag, 'font')), 'Helvetica 12')
text.tag_configure(tag, tabs=(10.2, '1i'))
self.assertEqual([str(x) for x in text.tag_ranges('sel')], [])
self.assertRaises(TclError, text.tag_cget, tag, 'spam')
@requires_tk(8, 6, 6)
def test_tag_configure_colors(self):
# Tag color options added in Tk 8.6.6.
text = self.text
tag = 'a'
for opt in ('selectforeground', 'selectbackground',
'lmargincolor', 'rmargincolor',
'underlinefg', 'overstrikefg'):
text.tag_configure(tag, **{opt: '#00ff00'})
self.assertEqual(str(text.tag_cget(tag, opt)), '#00ff00')
def test_tag_configure_query(self):
text = self.text
tag = 'a'
# Querying all options returns a dict keyed by option name.
cnf = text.tag_configure(tag)
self.assertIsInstance(cnf, dict)
self.assertIn('foreground', cnf)
# The value is the full 5-tuple option specification.
self.assertEqual(len(cnf['foreground']), 5)
# Querying a single option returns its specification.
spec = text.tag_configure(tag, 'foreground')
self.assertEqual(spec[0], 'foreground')
self.assertEqual(spec[-1], '') # unset by default
# Setting via keyword arguments and via a dict are equivalent.
text.tag_configure(tag, foreground='red')
self.assertEqual(str(text.tag_configure(tag, 'foreground')[-1]), 'red')
text.tag_configure(tag, {'foreground': 'blue'})
self.assertEqual(str(text.tag_cget(tag, 'foreground')), 'blue')
# tag_config is an alias of tag_configure.
self.assertEqual(text.tag_config, text.tag_configure)
def test_image_configure(self):
text = self.text
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
text.insert('1.0', 'AB')
name = text.image_create('1.1', image=image, name='img')
self.assertEqual(name, 'img')
self.assertEqual(str(text.image_cget(name, 'name')), 'img')
self.assertEqual(str(text.image_cget(name, 'image')), str(image))
for value in ('top', 'center', 'bottom', 'baseline'):
text.image_configure(name, align=value)
self.assertEqual(str(text.image_cget(name, 'align')), value)
text.image_configure(name, padx=3, pady=4)
self.assertEqual(text.tk.getint(text.image_cget(name, 'padx')), 3)
self.assertEqual(text.tk.getint(text.image_cget(name, 'pady')), 4)
# Querying returns the full option set.
cnf = text.image_configure(name)
self.assertIsInstance(cnf, dict)
self.assertIn('align', cnf)
self.assertRaises(TclError, text.image_cget, name, 'spam')
def test_window_configure(self):
text = self.text
button = tkinter.Button(text, text='ok')
text.insert('1.0', 'AB')
text.window_create('1.1', window=button)
self.assertEqual(text.window_cget('1.1', 'window'), str(button))
for value in ('top', 'center', 'bottom', 'baseline'):
text.window_configure('1.1', align=value)
self.assertEqual(str(text.window_cget('1.1', 'align')), value)
text.window_configure('1.1', padx=3, pady=4)
self.assertEqual(text.tk.getint(text.window_cget('1.1', 'padx')), 3)
self.assertEqual(text.tk.getint(text.window_cget('1.1', 'pady')), 4)
text.window_configure('1.1', stretch=True)
self.assertIs(text.tk.getboolean(text.window_cget('1.1', 'stretch')),
True)
cnf = text.window_configure('1.1')
self.assertIsInstance(cnf, dict)
self.assertIn('stretch', cnf)
self.assertRaises(TclError, text.window_cget, '1.1', 'spam')
self.assertEqual(text.window_config, text.window_configure)
button.destroy()
def test_peer(self):
text = self.text
text.insert('1.0', 'Lorem ipsum')
self.assertEqual(text.peer_names(), ())
text.peer_create('.peer1')
names = self.root.splitlist(text.tk.call('winfo', 'children', '.'))
self.assertIn('.peer1', [str(n) for n in names])
self.assertEqual([str(p) for p in text.peer_names()], ['.peer1'])
# Peers share content.
self.assertEqual(text.tk.call('.peer1', 'get', '1.0', 'end'),
'Lorem ipsum\n')
text.tk.call('destroy', '.peer1')
def test_bbox(self):
text = self.text
text.insert('1.0', 'hello')
text.update()
bbox = text.bbox('1.0')
self.assertIsInstance(bbox, tuple)
self.assertEqual(len(bbox), 4)
for v in bbox:
self.assertIsInstance(v, int)
# A character that is not displayed has no bounding box.
self.assertIsNone(text.bbox('end'))
self.assertRaises(TclError, text.bbox, 'invalid')
self.assertRaises(TypeError, text.bbox)
self.assertRaises(TypeError, text.bbox, '1.0', '2.0')
def test_dlineinfo(self):
text = self.text
text.insert('1.0', 'hello\nworld')
text.update()
info = text.dlineinfo('1.0')
self.assertIsInstance(info, tuple)
self.assertEqual(len(info), 5)
for v in info:
self.assertIsInstance(v, int)
self.assertRaises(TclError, text.dlineinfo, 'invalid')
self.assertRaises(TypeError, text.dlineinfo)
self.assertRaises(TypeError, text.dlineinfo, '1.0', '2.0')
def test_see(self):
text = self.text
text.insert('1.0', '\n'.join('line %d' % i for i in range(200)))
text.update()
# Initially the last line is not visible.
self.assertIsNone(text.bbox('200.0'))
text.see('200.0')
text.update()
self.assertIsNotNone(text.bbox('200.0'))
self.assertRaises(TclError, text.see, 'invalid')
self.assertRaises(TypeError, text.see)
self.assertRaises(TypeError, text.see, '1.0', '2.0')
# yview_pickplace is a deprecated way to make an index visible.
text.yview_pickplace('1.0')
text.update()
self.assertIsNotNone(text.bbox('1.0'))
def test_search(self):
text = self.text
# pattern and index are obligatory arguments.
self.assertRaises(TclError, text.search, None, '1.0')
self.assertRaises(TclError, text.search, 'a', None)
self.assertRaises(TclError, text.search, None, None)
# Invalid text index.
self.assertRaises(TclError, text.search, '', 0)
self.assertRaises(TclError, text.search, '', '')
self.assertRaises(TclError, text.search, '', 'invalid')
self.assertRaises(TclError, text.search, '', '1.0', 0)
self.assertRaises(TclError, text.search, '', '1.0', '')
self.assertRaises(TclError, text.search, '', '1.0', 'invalid')
text.insert('1.0',
'This is a test. This is only a test.\n'
'Another line.\n'
'Yet another line.\n'
'64-bit')
self.assertEqual(text.search('test', '1.0'), '1.10')
self.assertEqual(text.search('test', '1.0', 'end'), '1.10')
self.assertEqual(text.search('test', '1.0', '1.10'), '')
self.assertEqual(text.search('test', '1.11'), '1.31')
self.assertEqual(text.search('test', '1.32', 'end'), '')
self.assertEqual(text.search('test', '1.32'), '1.10')
self.assertEqual(text.search('', '1.0'), '1.0') # empty pattern
self.assertEqual(text.search('nonexistent', '1.0'), '')
self.assertEqual(text.search('-bit', '1.0'), '4.2') # starts with a hyphen
self.assertEqual(text.search('line', '3.0'), '3.12')
self.assertEqual(text.search('line', '3.0', forwards=True), '3.12')
self.assertEqual(text.search('line', '3.0', backwards=True), '2.8')
self.assertEqual(text.search('line', '3.0', forwards=True, backwards=True), '2.8')
self.assertEqual(text.search('t.', '1.0'), '1.13')
self.assertEqual(text.search('t.', '1.0', exact=True), '1.13')
self.assertEqual(text.search('t.', '1.0', regexp=True), '1.10')
self.assertEqual(text.search('t.', '1.0', exact=True, regexp=True), '1.10')
self.assertEqual(text.search('TEST', '1.0'), '')
self.assertEqual(text.search('TEST', '1.0', nocase=True), '1.10')
self.assertEqual(text.search('.*line', '1.0', regexp=True), '2.0')
self.assertEqual(text.search('.*line', '1.0', regexp=True, nolinestop=True), '1.0')
self.assertEqual(text.search('test', '1.0', '1.13'), '1.10')
self.assertEqual(text.search('test', '1.0', '1.13', strictlimits=True), '')
self.assertEqual(text.search('test', '1.0', '1.14', strictlimits=True), '1.10')
var = tkinter.Variable(self.root)
self.assertEqual(text.search('test', '1.0', count=var), '1.10')
self.assertEqual(var.get(), 4 if self.wantobjects else '4')
# TODO: Add test for elide=True
def test_search_all(self):
text = self.text
# pattern and index are obligatory arguments.
self.assertRaises(TclError, text.search_all, None, '1.0')
self.assertRaises(TclError, text.search_all, 'a', None)
self.assertRaises(TclError, text.search_all, None, None)
# Keyword-only arguments
self.assertRaises(TypeError, text.search_all, 'a', '1.0', 'end', None)
# Invalid text index.
self.assertRaises(TclError, text.search_all, '', 0)
self.assertRaises(TclError, text.search_all, '', '')
self.assertRaises(TclError, text.search_all, '', 'invalid')
self.assertRaises(TclError, text.search_all, '', '1.0', 0)
self.assertRaises(TclError, text.search_all, '', '1.0', '')
self.assertRaises(TclError, text.search_all, '', '1.0', 'invalid')
def eq(res, expected):
self.assertIsInstance(res, tuple)
self.assertEqual([str(i) for i in res], expected)
text.insert('1.0', 'ababa\naba\n64-bit')
eq(text.search_all('aba', '1.0'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.0', 'end'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.1', 'end'), ['1.2', '2.0'])
eq(text.search_all('aba', '1.1'), ['1.2', '2.0', '1.0'])
res = text.search_all('', '1.0') # empty pattern
eq(res[:5], ['1.0', '1.1', '1.2', '1.3', '1.4'])
eq(res[-5:], ['3.2', '3.3', '3.4', '3.5', '3.6'])
eq(text.search_all('nonexistent', '1.0'), [])
eq(text.search_all('-bit', '1.0'), ['3.2']) # starts with a hyphen
eq(text.search_all('aba', '1.0', 'end', forwards=True), ['1.0', '2.0'])
eq(text.search_all('aba', 'end', '1.0', backwards=True), ['2.0', '1.2'])
eq(text.search_all('aba', '1.0', overlap=True), ['1.0', '1.2', '2.0'])
eq(text.search_all('aba', 'end', '1.0', overlap=True, backwards=True), ['2.0', '1.2', '1.0'])
eq(text.search_all('aba', '1.0', exact=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', exact=True), [])
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
eq(text.search_all('ABA', '1.0'), [])
eq(text.search_all('ABA', '1.0', nocase=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', regexp=True), ['1.0', '2.0'])
eq(text.search_all('a.a', '1.0', regexp=True, nolinestop=True), ['1.0', '1.4'])
eq(text.search_all('aba', '1.0', '2.2'), ['1.0', '2.0'])
eq(text.search_all('aba', '1.0', '2.2', strictlimits=True), ['1.0'])
eq(text.search_all('aba', '1.0', '2.3', strictlimits=True), ['1.0', '2.0'])
var = tkinter.Variable(self.root)
eq(text.search_all('aba', '1.0', count=var), ['1.0', '2.0'])
self.assertEqual(var.get(), (3, 3) if self.wantobjects else '3 3')
# TODO: Add test for elide=True
def test_count(self):
text = self.text
text.insert('1.0',
'Lorem ipsum dolor sit amet,\n'
'consectetur adipiscing elit,\n'
'sed do eiusmod tempor incididunt\n'
'ut labore et dolore magna aliqua.')
options = ('chars', 'indices', 'lines',
'displaychars', 'displayindices', 'displaylines',
'xpixels', 'ypixels')
self.assertEqual(len(text.count('1.0', 'end', *options, return_ints=True)), 8)
self.assertEqual(len(text.count('1.0', 'end', *options)), 8)
self.assertEqual(text.count('1.0', 'end', 'chars', 'lines', return_ints=True),
(124, 4))
self.assertEqual(text.count('1.3', '4.5', 'chars', 'lines'), (92, 3))
self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines', return_ints=True),
(-92, -3))
self.assertEqual(text.count('4.5', '1.3', 'chars', 'lines'), (-92, -3))
self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines', return_ints=True),
(0, 0))
self.assertEqual(text.count('1.3', '1.3', 'chars', 'lines'), (0, 0))
self.assertEqual(text.count('1.0', 'end', 'lines', return_ints=True), 4)
self.assertEqual(text.count('1.0', 'end', 'lines'), (4,))
self.assertEqual(text.count('end', '1.0', 'lines', return_ints=True), -4)
self.assertEqual(text.count('end', '1.0', 'lines'), (-4,))
self.assertEqual(text.count('1.3', '1.5', 'lines', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.5', 'lines'), None)
self.assertEqual(text.count('1.3', '1.3', 'lines', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'lines'), None)
# Count 'indices' by default.
self.assertEqual(text.count('1.0', 'end', return_ints=True), 124)
self.assertEqual(text.count('1.0', 'end'), (124,))
self.assertEqual(text.count('1.0', 'end', 'indices', return_ints=True), 124)
self.assertEqual(text.count('1.0', 'end', 'indices'), (124,))
self.assertRaises(TclError, text.count, '1.0', 'end', 'spam')
self.assertRaises(TclError, text.count, '1.0', 'end', '-lines')
self.assertIsInstance(text.count('1.3', '1.5', 'ypixels', return_ints=True), int)
self.assertIsInstance(text.count('1.3', '1.5', 'ypixels'), tuple)
self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels', return_ints=True), int)
self.assertIsInstance(text.count('1.3', '1.5', 'update', 'ypixels'), int)
self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'update', 'ypixels'), None)
self.assertEqual(text.count('1.3', '1.5', 'update', 'indices', return_ints=True), 2)
self.assertEqual(text.count('1.3', '1.5', 'update', 'indices'), 2)
self.assertEqual(text.count('1.3', '1.3', 'update', 'indices', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'update', 'indices'), None)
self.assertEqual(text.count('1.3', '1.5', 'update', return_ints=True), 2)
self.assertEqual(text.count('1.3', '1.5', 'update'), (2,))
self.assertEqual(text.count('1.3', '1.3', 'update', return_ints=True), 0)
self.assertEqual(text.count('1.3', '1.3', 'update'), None)
if __name__ == "__main__":
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
# test_tkinter_pipe.py
import unittest
import subprocess
import sys
from test import support
@unittest.skipUnless(support.has_subprocess_support, "test requires subprocess")
class TkinterPipeTest(unittest.TestCase):
def test_tkinter_pipe_buffered(self):
args = [sys.executable, "-i"]
proc = subprocess.Popen(args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.stdin.write(b"import tkinter\n")
proc.stdin.write(b"interpreter = tkinter.Tcl()\n")
proc.stdin.write(b"print('hello')\n")
proc.stdin.write(b"print('goodbye')\n")
proc.stdin.write(b"quit()\n")
stdout, stderr = proc.communicate(timeout=support.SHORT_TIMEOUT)
stdout = stdout.decode()
self.assertEqual(stdout.split(), ['hello', 'goodbye'])
def test_tkinter_pipe_unbuffered(self):
args = [sys.executable, "-i", "-u"]
proc = subprocess.Popen(args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.stdin.write(b"import tkinter\n")
proc.stdin.write(b"interpreter = tkinter.Tcl()\n")
proc.stdin.write(b"print('hello')\n")
proc.stdin.flush()
stdout = proc.stdout.readline()
stdout = stdout.decode()
self.assertEqual(stdout.strip(), 'hello')
proc.stdin.write(b"print('hello again')\n")
proc.stdin.flush()
stdout = proc.stdout.readline()
stdout = stdout.decode()
self.assertEqual(stdout.strip(), 'hello again')
proc.stdin.write(b"print('goodbye')\n")
proc.stdin.write(b"quit()\n")
stdout, stderr = proc.communicate(timeout=support.SHORT_TIMEOUT)
stdout = stdout.decode()
self.assertEqual(stdout.strip(), 'goodbye')
if __name__ == "__main__":
unittest.main()
+363
View File
@@ -0,0 +1,363 @@
import unittest
from test import support
import gc
import tkinter
from tkinter import (Variable, StringVar, IntVar, DoubleVar, BooleanVar, Tcl,
TclError)
from test.support import ALWAYS_EQ
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractDefaultRootTest, tcl_version
class Var(Variable):
_default = "default"
side_effect = False
def set(self, value):
self.side_effect = True
super().set(value)
class TestBase(unittest.TestCase):
def setUp(self):
self.root = Tcl()
def tearDown(self):
del self.root
class TestVariable(TestBase):
def info_exists(self, *args):
return self.root.getboolean(self.root.call("info", "exists", *args))
def test_default(self):
v = Variable(self.root)
self.assertEqual("", v.get())
self.assertRegex(str(v), r"^PY_VAR(\d+)$")
def test_name_and_value(self):
v = Variable(self.root, "sample string", "varname")
self.assertEqual("sample string", v.get())
self.assertEqual("varname", str(v))
def test___del__(self):
self.assertFalse(self.info_exists("varname"))
v = Variable(self.root, "sample string", "varname")
self.assertTrue(self.info_exists("varname"))
del v
support.gc_collect() # For PyPy or other GCs.
self.assertFalse(self.info_exists("varname"))
def test_dont_unset_not_existing(self):
self.assertFalse(self.info_exists("varname"))
v1 = Variable(self.root, name="name")
v2 = Variable(self.root, name="name")
del v1
support.gc_collect() # For PyPy or other GCs.
self.assertFalse(self.info_exists("name"))
# shouldn't raise exception
del v2
support.gc_collect() # For PyPy or other GCs.
self.assertFalse(self.info_exists("name"))
def test_equality(self):
# values doesn't matter, only class and name are checked
v1 = Variable(self.root, name="abc")
v2 = Variable(self.root, name="abc")
self.assertIsNot(v1, v2)
self.assertEqual(v1, v2)
v3 = Variable(self.root, name="cba")
self.assertNotEqual(v1, v3)
v4 = StringVar(self.root, name="abc")
self.assertEqual(str(v1), str(v4))
self.assertNotEqual(v1, v4)
V = type('Variable', (), {})
self.assertNotEqual(v1, V())
self.assertNotEqual(v1, object())
self.assertEqual(v1, ALWAYS_EQ)
root2 = tkinter.Tk()
self.addCleanup(root2.destroy)
v5 = Variable(root2, name="abc")
self.assertEqual(str(v1), str(v5))
self.assertNotEqual(v1, v5)
def test_invalid_name(self):
with self.assertRaises(TypeError):
Variable(self.root, name=123)
def test_null_in_name(self):
with self.assertRaises(ValueError):
Variable(self.root, name='var\x00name')
with self.assertRaises(ValueError):
self.root.globalsetvar('var\x00name', "value")
with self.assertRaises(ValueError):
self.root.globalsetvar(b'var\x00name', "value")
with self.assertRaises(ValueError):
self.root.setvar('var\x00name', "value")
with self.assertRaises(ValueError):
self.root.setvar(b'var\x00name', "value")
def test_initialize(self):
v = Var(self.root)
self.assertFalse(v.side_effect)
v.set("value")
self.assertTrue(v.side_effect)
self.assertEqual(Variable.initialize, Variable.set)
def test_trace_old(self):
if tcl_version >= (9, 0):
self.skipTest('requires Tcl version < 9.0')
# Old interface
v = Variable(self.root)
vname = str(v)
self.assertEqual(v.trace, v.trace_variable)
trace = []
def read_tracer(*args):
trace.append(('read',) + args)
def write_tracer(*args):
trace.append(('write',) + args)
with self.assertWarns(DeprecationWarning) as cm:
cb1 = v.trace_variable('r', read_tracer)
self.assertEqual(cm.filename, __file__)
with self.assertWarns(DeprecationWarning):
cb2 = v.trace_variable('wu', write_tracer)
with self.assertWarns(DeprecationWarning) as cm:
self.assertEqual(sorted(v.trace_vinfo()), [('r', cb1), ('wu', cb2)])
self.assertEqual(cm.filename, __file__)
self.assertEqual(trace, [])
v.set('spam')
self.assertEqual(trace, [('write', vname, '', 'w')])
trace = []
v.get()
self.assertEqual(trace, [('read', vname, '', 'r')])
trace = []
with self.assertWarns(DeprecationWarning):
info = sorted(v.trace_vinfo())
with self.assertWarns(DeprecationWarning):
v.trace_vdelete('w', cb1) # Wrong mode
with self.assertWarns(DeprecationWarning):
self.assertEqual(sorted(v.trace_vinfo()), info)
with self.assertRaises(TclError):
with self.assertWarns(DeprecationWarning):
v.trace_vdelete('r', 'spam') # Wrong command name
with self.assertWarns(DeprecationWarning):
self.assertEqual(sorted(v.trace_vinfo()), info)
with self.assertWarns(DeprecationWarning):
v.trace_vdelete('r', (cb1, 43)) # Wrong arguments
with self.assertWarns(DeprecationWarning):
self.assertEqual(sorted(v.trace_vinfo()), info)
v.get()
self.assertEqual(trace, [('read', vname, '', 'r')])
trace = []
with self.assertWarns(DeprecationWarning) as cm:
v.trace_vdelete('r', cb1)
self.assertEqual(cm.filename, __file__)
with self.assertWarns(DeprecationWarning):
self.assertEqual(v.trace_vinfo(), [('wu', cb2)])
v.get()
self.assertEqual(trace, [])
trace = []
del write_tracer
gc.collect()
v.set('eggs')
self.assertEqual(trace, [('write', vname, '', 'w')])
trace = []
del v
gc.collect()
self.assertEqual(trace, [('write', vname, '', 'u')])
def test_trace(self):
v = Variable(self.root)
vname = str(v)
trace = []
def read_tracer(*args):
trace.append(('read',) + args)
def write_tracer(*args):
trace.append(('write',) + args)
tr1 = v.trace_add('read', read_tracer)
tr2 = v.trace_add(['write', 'unset'], write_tracer)
self.assertEqual(sorted(v.trace_info()), [
(('read',), tr1),
(('write', 'unset'), tr2)])
self.assertEqual(trace, [])
v.set('spam')
self.assertEqual(trace, [('write', vname, '', 'write')])
trace = []
v.get()
self.assertEqual(trace, [('read', vname, '', 'read')])
trace = []
info = sorted(v.trace_info())
v.trace_remove('write', tr1) # Wrong mode
self.assertEqual(sorted(v.trace_info()), info)
with self.assertRaises(TclError):
v.trace_remove('read', 'spam') # Wrong command name
self.assertEqual(sorted(v.trace_info()), info)
v.get()
self.assertEqual(trace, [('read', vname, '', 'read')])
trace = []
v.trace_remove('read', tr1)
self.assertEqual(v.trace_info(), [(('write', 'unset'), tr2)])
v.get()
self.assertEqual(trace, [])
trace = []
del write_tracer
gc.collect()
v.set('eggs')
self.assertEqual(trace, [('write', vname, '', 'write')])
trace = []
del v
gc.collect()
self.assertEqual(trace, [('write', vname, '', 'unset')])
class TestStringVar(TestBase):
def test_default(self):
v = StringVar(self.root)
self.assertEqual("", v.get())
def test_get(self):
v = StringVar(self.root, "abc", "name")
self.assertEqual("abc", v.get())
self.root.globalsetvar("name", "value")
self.assertEqual("value", v.get())
def test_get_null(self):
v = StringVar(self.root, "abc\x00def", "name")
self.assertEqual("abc\x00def", v.get())
self.root.globalsetvar("name", "val\x00ue")
self.assertEqual("val\x00ue", v.get())
class TestIntVar(TestBase):
def test_default(self):
v = IntVar(self.root)
self.assertEqual(0, v.get())
def test_get(self):
v = IntVar(self.root, 123, "name")
self.assertEqual(123, v.get())
self.root.globalsetvar("name", "345")
self.assertEqual(345, v.get())
self.root.globalsetvar("name", "876.5")
self.assertEqual(876, v.get())
def test_invalid_value(self):
v = IntVar(self.root, name="name")
self.root.globalsetvar("name", "value")
with self.assertRaises((ValueError, TclError)):
v.get()
class TestDoubleVar(TestBase):
def test_default(self):
v = DoubleVar(self.root)
self.assertEqual(0.0, v.get())
def test_get(self):
v = DoubleVar(self.root, 1.23, "name")
self.assertAlmostEqual(1.23, v.get())
self.root.globalsetvar("name", "3.45")
self.assertAlmostEqual(3.45, v.get())
def test_get_from_int(self):
v = DoubleVar(self.root, 1.23, "name")
self.assertAlmostEqual(1.23, v.get())
self.root.globalsetvar("name", "3.45")
self.assertAlmostEqual(3.45, v.get())
self.root.globalsetvar("name", "456")
self.assertAlmostEqual(456, v.get())
def test_invalid_value(self):
v = DoubleVar(self.root, name="name")
self.root.globalsetvar("name", "value")
with self.assertRaises((ValueError, TclError)):
v.get()
class TestBooleanVar(TestBase):
def test_default(self):
v = BooleanVar(self.root)
self.assertIs(v.get(), False)
def test_get(self):
v = BooleanVar(self.root, True, "name")
self.assertIs(v.get(), True)
self.root.globalsetvar("name", "0")
self.assertIs(v.get(), False)
self.root.globalsetvar("name", 42 if self.root.wantobjects() else 1)
self.assertIs(v.get(), True)
self.root.globalsetvar("name", 0)
self.assertIs(v.get(), False)
self.root.globalsetvar("name", "on")
self.assertIs(v.get(), True)
def test_set(self):
true = 1 if self.root.wantobjects() else "1"
false = 0 if self.root.wantobjects() else "0"
v = BooleanVar(self.root, name="name")
v.set(True)
self.assertEqual(self.root.globalgetvar("name"), true)
v.set("0")
self.assertEqual(self.root.globalgetvar("name"), false)
v.set(42)
self.assertEqual(self.root.globalgetvar("name"), true)
v.set(0)
self.assertEqual(self.root.globalgetvar("name"), false)
v.set("on")
self.assertEqual(self.root.globalgetvar("name"), true)
self.assertEqual(BooleanVar.initialize, BooleanVar.set)
def test_invalid_value_domain(self):
false = 0 if self.root.wantobjects() else "0"
v = BooleanVar(self.root, name="name")
with self.assertRaises(TclError):
v.set("value")
self.assertEqual(self.root.globalgetvar("name"), false)
self.root.globalsetvar("name", "value")
with self.assertRaises(ValueError):
v.get()
self.root.globalsetvar("name", "1.0")
with self.assertRaises(ValueError):
v.get()
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_variable(self):
self.assertRaises(RuntimeError, Variable)
root = tkinter.Tk()
v = Variable()
v.set("value")
self.assertEqual(v.get(), "value")
root.destroy()
tkinter.NoDefaultRoot()
self.assertRaises(RuntimeError, Variable)
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large. Load diff
+628
View File
@@ -0,0 +1,628 @@
# Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py
import re
import tkinter
from test.test_tkinter.support import (AbstractTkTest, requires_tk, tk_version,
pixels_conv, tcl_obj_eq)
import test.support
_sentinel = object()
# Abbreviated option names accepted by Tk in addition to the full names.
_OPTION_ALIASES = {
'bd': 'borderwidth',
'bg': 'background',
'bgimg': 'backgroundimage',
'fg': 'foreground',
'invcmd': 'invalidcommand',
'vcmd': 'validatecommand',
}
# Options which accept all values allowed by Tk_GetPixels
# borderwidth = bd
class AbstractWidgetTest(AbstractTkTest):
_default_pixels = '' if tk_version >= (9, 0) else -1 # Value for unset pixel options.
_rounds_pixels = (tk_version < (9, 0)) # True if some pixel options are rounded.
_no_round = set() # Pixel options which are not rounded nonetheless
_stringify = False # Whether to convert tuples to strings
_allow_empty_justify = False
_clipped_to_default = set()
@property
def scaling(self):
try:
return self._scaling
except AttributeError:
self._scaling = float(self.root.call('tk', 'scaling'))
return self._scaling
def _str(self, value):
if not self._stringify and self.wantobjects and tk_version >= (8, 6):
return value
if isinstance(value, tuple):
return ' '.join(map(self._str, value))
return str(value)
def assertEqual2(self, actual, expected, msg=None, eq=object.__eq__):
if eq(actual, expected):
return
self.assertEqual(actual, expected, msg)
def checkParam(self, widget, name, value, *, expected=_sentinel,
conv=False, eq=None):
widget[name] = value
if expected is _sentinel:
expected = value
if name in self._clipped:
if not isinstance(expected, str) and expected < 0:
if tk_version >= (8, 7) and name in self._clipped_to_default:
expected = self._default_pixels
else:
expected = 0
if conv:
expected = conv(expected)
if self._stringify or not self.wantobjects:
if isinstance(expected, tuple):
expected = tkinter._join(expected)
else:
expected = str(expected)
if eq is None:
eq = tcl_obj_eq
self.assertEqual2(widget[name], expected, eq=eq)
self.assertEqual2(widget.cget(name), expected, eq=eq)
t = widget.configure(name)
self.assertEqual(len(t), 5)
self.assertEqual2(t[4], expected, eq=eq)
def checkInvalidParam(self, widget, name, value, errmsg=None):
orig = widget[name]
if errmsg is not None:
errmsg = errmsg.format(re.escape(str(value)))
errmsg = fr'\A{errmsg}\z'
with self.assertRaisesRegex(tkinter.TclError, errmsg or ''):
widget[name] = value
self.assertEqual(widget[name], orig)
with self.assertRaisesRegex(tkinter.TclError, errmsg or ''):
widget.configure({name: value})
self.assertEqual(widget[name], orig)
def checkParams(self, widget, name, *values, **kwargs):
for value in values:
self.checkParam(widget, name, value, **kwargs)
def checkIntegerParam(self, widget, name, *values, **kwargs):
self.checkParams(widget, name, *values, **kwargs)
errmsg = 'expected integer but got "{}"'
self.checkInvalidParam(widget, name, '', errmsg=errmsg)
self.checkInvalidParam(widget, name, '10p', errmsg=errmsg)
self.checkInvalidParam(widget, name, 3.2, errmsg=errmsg)
def checkFloatParam(self, widget, name, *values, conv=float, **kwargs):
for value in values:
self.checkParam(widget, name, value, conv=conv, **kwargs)
errmsg = 'expected floating-point number but got "{}"'
self.checkInvalidParam(widget, name, '', errmsg=errmsg)
self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
def checkBooleanParam(self, widget, name):
for value in (False, 0, 'false', 'no', 'off'):
self.checkParam(widget, name, value, expected=0)
for value in (True, 1, 'true', 'yes', 'on'):
self.checkParam(widget, name, value, expected=1)
errmsg = 'expected boolean value but got "{}"'
self.checkInvalidParam(widget, name, '', errmsg=errmsg)
self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
def checkColorParam(self, widget, name, *, allow_empty=None, **kwargs):
self.checkParams(widget, name,
'#ff0000', '#00ff00', '#0000ff', '#123456',
'red', 'green', 'blue', 'white', 'black', 'grey',
**kwargs)
self.checkInvalidParam(widget, name, 'spam',
errmsg='unknown color name "spam"')
def checkCursorParam(self, widget, name, **kwargs):
self.checkParams(widget, name, 'arrow', 'watch', 'cross', '',**kwargs)
self.checkParam(widget, name, 'none')
self.checkInvalidParam(widget, name, 'spam',
errmsg='bad cursor spec "spam"')
def checkCommandParam(self, widget, name):
def command(*args):
pass
widget[name] = command
self.assertTrue(widget[name])
self.checkParams(widget, name, '')
def checkEnumParam(self, widget, name, *values,
errmsg=None, allow_empty=False, fullname=None,
sort=False, **kwargs):
self.checkParams(widget, name, *values, **kwargs)
if errmsg is None:
if sort:
if values[-1]:
values = tuple(sorted(values))
else:
values = tuple(sorted(values[:-1])) + ('',)
errmsg2 = ' %s "{}": must be %s%s or %s' % (
fullname or name,
', '.join(values[:-1]),
',' if len(values) > 2 else '',
values[-1] or '""')
if '' not in values and not allow_empty:
self.checkInvalidParam(widget, name, '',
errmsg='ambiguous' + errmsg2)
errmsg = 'bad' + errmsg2
self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
def checkPixelsParam(self, widget, name, *values, conv=None, **kwargs):
if conv is None:
if self._rounds_pixels and name not in self._no_round:
conv = round
alow_neg = tk_version < (9, 1)
for value in values:
expected = _sentinel
conv1 = conv
if isinstance(value, str):
if not getattr(self, '_converts_pixels', True):
conv1 = str
if conv1 and conv1 is not str:
expected = pixels_conv(value) * self.scaling
conv1 = round
elif not alow_neg and isinstance(value, (int, float)) and value < 0:
self.checkInvalidParam(widget, name, value)
continue
self.checkParam(widget, name, value, expected=expected,
conv=conv1, **kwargs)
errmsg = '(bad|expected) screen distance ((or "" )?but got )?"{}"'
self.checkInvalidParam(widget, name, '6x', errmsg=errmsg)
self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
def checkReliefParam(self, widget, name, *, allow_empty=False):
values = ('flat', 'groove', 'raised', 'ridge', 'solid', 'sunken')
if allow_empty:
values += ('',)
self.checkParams(widget, name, *values)
errmsg = 'bad relief "{}": must be %s, or %s' % (
', '.join(values[:-1]),
values[-1] or '""')
if tk_version < (8, 6):
errmsg = None
self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
def checkImageParam(self, widget, name):
image = tkinter.PhotoImage(master=self.root, name='image1')
self.checkParam(widget, name, image, conv=str)
if tk_version < (8, 7):
errmsg = 'image "spam" doesn\'t exist'
else:
errmsg = 'image "spam" does not exist'
self.checkInvalidParam(widget, name, 'spam',
errmsg=errmsg)
widget[name] = ''
def checkVariableParam(self, widget, name, var):
self.checkParam(widget, name, var, conv=str)
def assertIsBoundingBox(self, bbox):
self.assertIsNotNone(bbox)
self.assertIsInstance(bbox, tuple)
if len(bbox) != 4:
self.fail('Invalid bounding box: %r' % (bbox,))
for item in bbox:
if not isinstance(item, int):
self.fail('Invalid bounding box: %r' % (bbox,))
break
def test_keys(self):
widget = self.create()
keys = widget.keys()
self.assertEqual(sorted(keys), sorted(widget.configure()))
for k in keys:
widget[k]
# Test if OPTIONS contains all keys
if test.support.verbose:
keys = set(keys)
expected = set(self.OPTIONS)
for k in sorted(keys - expected):
if not (k in _OPTION_ALIASES and
_OPTION_ALIASES[k] in keys and
_OPTION_ALIASES[k] in expected):
print('%s.OPTIONS doesn\'t contain "%s"' %
(self.__class__.__name__, k))
def test_options_in_docstring(self):
# Every option in OPTIONS must be listed in the docstring of the
# __init__ method (see gh-78335). Options reported by keys() but
# missing from the docstring are only printed in verbose mode, as
# some of them depend on the Tk version.
widget = self.create()
doc = type(widget).__init__.__doc__
if doc is None:
self.skipTest('docstrings are not available (run with -OO)')
# Look at the option list only, not the leading description.
start = doc.find('Valid option names')
if start < 0:
start = doc.find('OPTIONS')
if start < 0:
self.skipTest('the __init__ docstring does not list options')
documented = set(re.findall(r'[a-z][a-z0-9]+', doc[start:]))
def is_documented(option):
return (option in documented or
_OPTION_ALIASES.get(option) in documented)
missing = sorted(o for o in self.OPTIONS if not is_documented(o))
self.assertEqual(missing, [],
'%s options missing from the __init__ docstring: %s'
% (type(widget).__name__, missing))
if test.support.verbose:
for key in sorted(set(widget.keys())):
if not is_documented(key):
print('%s.__init__ docstring doesn\'t contain "%s"'
% (type(widget).__name__, key))
class PixelOptionsTests:
"""Standard options that accept all formats acceptable to Tk_GetPixels.
In addition to numbers, these options can be set with distances
specified as a string consisting of a number followed by a single
character giving the unit of distance. The allowed units are:
millimeters ('m'), centimeters ('c'), inches ('i') or points ('p').
In Tk 9 a cget call for one of these options returns a Tcl_Obj of
type "pixels", whose string representation is the distance string
passed to configure.
"""
PIXEL_OPTIONS = ('activeborderwidth', 'borderwidth', 'highlightthickness',
'insertborderwidth', 'insertwidth', 'padx', 'pady', 'selectborderwidth')
def test_configure_activeborderwidth(self):
widget = self.create()
self.checkPixelsParam(widget, 'activeborderwidth',
0, 1.3, 2.9, 6, -2, '10p')
def test_configure_borderwidth(self):
widget = self.create()
self.checkPixelsParam(widget, 'borderwidth',
0, 1.3, 2.6, 6, -2, '10p')
if 'bd' in self.OPTIONS:
self.checkPixelsParam(widget, 'bd', 0, 1.3, 2.6, 6, '10p')
self.checkParam(widget, 'bd', -2, expected=expected)
def test_configure_highlightthickness(self):
widget = self.create()
self.checkPixelsParam(widget, 'highlightthickness',
0, 1.3, 2.6, 6, -2, '10p')
def test_configure_insertborderwidth(self):
widget = self.create()
self.checkPixelsParam(widget, 'insertborderwidth', 0, 1.3, 2.6, 6, -2, '10p')
def test_configure_insertwidth(self):
widget = self.create()
self.checkPixelsParam(widget, 'insertwidth', 1.3, 2.6, -2, '10p')
def test_configure_padx(self):
widget = self.create()
self.checkPixelsParam(widget, 'padx', 3, 4.4, 5.6, -2, '12m')
def test_configure_pady(self):
widget = self.create()
self.checkPixelsParam(widget, 'pady', 3, 4.4, 5.6, -2, '12m')
def test_configure_selectborderwidth(self):
widget = self.create()
self.checkPixelsParam(widget, 'selectborderwidth', 1.3, 2.6, -2, '10p')
class StandardOptionsTests(PixelOptionsTests):
STANDARD_OPTIONS = ( 'activebackground', 'activeforeground',
'anchor', 'background', 'bitmap', 'compound', 'cursor',
'disabledforeground', 'exportselection', 'font', 'foreground',
'highlightbackground', 'highlightcolor', 'image',
'insertbackground', 'insertofftime', 'insertontime', 'jump',
'justify', 'orient', 'relief', 'repeatdelay', 'repeatinterval',
'selectbackground', 'selectforeground', 'setgrid', 'takefocus',
'text', 'textvariable', 'troughcolor', 'underline', 'wraplength',
'xscrollcommand', 'yscrollcommand', ) + PixelOptionsTests.PIXEL_OPTIONS
def test_configure_activebackground(self):
widget = self.create()
self.checkColorParam(widget, 'activebackground')
def test_configure_activeforeground(self):
widget = self.create()
self.checkColorParam(widget, 'activeforeground')
def test_configure_activerelief(self):
widget = self.create()
self.checkReliefParam(widget, 'activerelief')
def test_configure_anchor(self):
widget = self.create()
self.checkEnumParam(widget, 'anchor',
'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center')
def test_configure_background(self):
widget = self.create()
self.checkColorParam(widget, 'background')
if 'bg' in self.OPTIONS:
self.checkColorParam(widget, 'bg')
@requires_tk(8, 7)
def test_configure_backgroundimage(self):
widget = self.create()
self.checkImageParam(widget, 'backgroundimage')
def test_configure_bitmap(self):
widget = self.create()
self.checkParam(widget, 'bitmap', 'questhead')
self.checkParam(widget, 'bitmap', 'gray50')
filename = test.support.findfile('python.xbm', subdir='tkinterdata')
self.checkParam(widget, 'bitmap', '@' + filename)
# Cocoa Tk widgets don't detect invalid -bitmap values
# See https://core.tcl.tk/tk/info/31cd33dbf0
if not ('aqua' in self.root.tk.call('tk', 'windowingsystem') and
'AppKit' in self.root.winfo_server()):
self.checkInvalidParam(widget, 'bitmap', 'spam',
errmsg='bitmap "spam" not defined')
def test_configure_compound(self):
widget = self.create()
self.checkEnumParam(widget, 'compound',
'bottom', 'center', 'left', 'none', 'right', 'top')
def test_configure_cursor(self):
widget = self.create()
self.checkCursorParam(widget, 'cursor')
def test_configure_disabledforeground(self):
widget = self.create()
self.checkColorParam(widget, 'disabledforeground')
def test_configure_exportselection(self):
widget = self.create()
self.checkBooleanParam(widget, 'exportselection')
def test_configure_font(self):
widget = self.create()
self.checkParam(widget, 'font',
'-Adobe-Helvetica-Medium-R-Normal--*-120-*-*-*-*-*-*')
is_ttk = widget.__class__.__module__ == 'tkinter.ttk'
if not is_ttk:
errmsg = 'font "" does ?n[o\']t exist'
self.checkInvalidParam(widget, 'font', '', errmsg=errmsg)
def test_configure_foreground(self):
widget = self.create()
self.checkColorParam(widget, 'foreground')
if 'fg' in self.OPTIONS:
self.checkColorParam(widget, 'fg')
def test_configure_highlightbackground(self):
widget = self.create()
self.checkColorParam(widget, 'highlightbackground')
def test_configure_highlightcolor(self):
widget = self.create()
self.checkColorParam(widget, 'highlightcolor')
def test_configure_image(self):
widget = self.create()
self.checkImageParam(widget, 'image')
def test_configure_insertbackground(self):
widget = self.create()
self.checkColorParam(widget, 'insertbackground')
def test_configure_insertofftime(self):
widget = self.create()
self.checkIntegerParam(widget, 'insertofftime', 100)
def test_configure_insertontime(self):
widget = self.create()
self.checkIntegerParam(widget, 'insertontime', 100)
def test_configure_jump(self):
widget = self.create()
self.checkBooleanParam(widget, 'jump')
def test_configure_justify(self):
widget = self.create()
values = ('left', 'right', 'center')
if self._allow_empty_justify:
values += ('',)
self.checkEnumParam(widget, 'justify', *values,
fullname='justification')
def test_configure_orient(self):
widget = self.create()
self.assertEqual(str(widget['orient']), self.default_orient)
self.checkEnumParam(widget, 'orient', 'horizontal', 'vertical')
@requires_tk(8, 7)
def test_configure_placeholder(self):
widget = self.create()
self.checkParam(widget, 'placeholder', 'xxx')
@requires_tk(8, 7)
def test_configure_placeholderforeground(self):
widget = self.create()
self.checkColorParam(widget, 'placeholderforeground')
def test_configure_relief(self):
widget = self.create()
self.checkReliefParam(widget, 'relief')
def test_configure_repeatdelay(self):
widget = self.create()
self.checkIntegerParam(widget, 'repeatdelay', -500, 500)
def test_configure_repeatinterval(self):
widget = self.create()
self.checkIntegerParam(widget, 'repeatinterval', -500, 500)
def test_configure_selectbackground(self):
widget = self.create()
self.checkColorParam(widget, 'selectbackground')
def test_configure_selectforeground(self):
widget = self.create()
self.checkColorParam(widget, 'selectforeground')
def test_configure_setgrid(self):
widget = self.create()
self.checkBooleanParam(widget, 'setgrid')
def test_configure_state(self):
widget = self.create()
self.checkEnumParam(widget, 'state', 'active', 'disabled', 'normal')
def test_configure_takefocus(self):
widget = self.create()
self.checkParams(widget, 'takefocus', '0', '1', '')
def test_configure_text(self):
widget = self.create()
self.checkParams(widget, 'text', '', 'any string')
def test_configure_textvariable(self):
widget = self.create()
var = tkinter.StringVar(self.root)
self.checkVariableParam(widget, 'textvariable', var)
@requires_tk(8, 7)
def test_configure_tile(self):
widget = self.create()
self.checkBooleanParam(widget, 'tile')
def test_configure_troughcolor(self):
widget = self.create()
self.checkColorParam(widget, 'troughcolor')
def test_configure_underline(self):
widget = self.create()
self.checkParams(widget, 'underline', 0, 1, 10)
if tk_version >= (8, 7):
is_ttk = widget.__class__.__module__ == 'tkinter.ttk'
self.checkParam(widget, 'underline', '',
expected='' if is_ttk else self._default_pixels)
self.checkParam(widget, 'underline', '5+2',
expected='5+2' if is_ttk else 7)
self.checkParam(widget, 'underline', '5-2',
expected='5-2' if is_ttk else 3)
self.checkParam(widget, 'underline', 'end', expected='end')
self.checkParam(widget, 'underline', 'end-2', expected='end-2')
errmsg = (r'bad index "{}": must be integer\?\[\+-\]integer\?, '
r'end\?\[\+-\]integer\?, or ""')
else:
errmsg = 'expected integer but got "{}"'
self.checkInvalidParam(widget, 'underline', '', errmsg=errmsg)
self.checkInvalidParam(widget, 'underline', '10p', errmsg=errmsg)
self.checkInvalidParam(widget, 'underline', 3.2, errmsg=errmsg)
def test_configure_wraplength(self):
widget = self.create()
self.checkPixelsParam(widget, 'wraplength', 100)
def test_configure_xscrollcommand(self):
widget = self.create()
self.checkCommandParam(widget, 'xscrollcommand')
def test_configure_yscrollcommand(self):
widget = self.create()
self.checkCommandParam(widget, 'yscrollcommand')
# non-standard but common options
def test_configure_command(self):
widget = self.create()
self.checkCommandParam(widget, 'command')
def test_configure_indicatoron(self):
widget = self.create()
self.checkBooleanParam(widget, 'indicatoron')
def test_configure_offrelief(self):
widget = self.create()
self.checkReliefParam(widget, 'offrelief')
def test_configure_overrelief(self):
widget = self.create()
self.checkReliefParam(widget, 'overrelief',
allow_empty=(tk_version >= (8, 7)))
def test_configure_selectcolor(self):
widget = self.create()
self.checkColorParam(widget, 'selectcolor')
def test_configure_selectimage(self):
widget = self.create()
self.checkImageParam(widget, 'selectimage')
def test_configure_tristateimage(self):
widget = self.create()
self.checkImageParam(widget, 'tristateimage')
def test_configure_tristatevalue(self):
widget = self.create()
self.checkParam(widget, 'tristatevalue', 'unknowable')
def test_configure_variable(self):
widget = self.create()
var = tkinter.DoubleVar(self.root)
self.checkVariableParam(widget, 'variable', var)
class IntegerSizeTests:
""" Tests widgets which only accept integral width and height."""
def test_configure_height(self):
widget = self.create()
self.checkIntegerParam(widget, 'height', 100, -100, 0)
def test_configure_width(self):
widget = self.create()
self.checkIntegerParam(widget, 'width', 402, -402, 0)
class PixelSizeTests:
""" Tests widgets which accept screen distances for width and height."""
def test_configure_height(self):
widget = self.create()
self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, -100, 0, '3c')
def test_configure_width(self):
widget = self.create()
self.checkPixelsParam(widget, 'width', 402, 403.4, 404.6, -402, 0, '5i')
def add_configure_tests(*source_classes):
# This decorator adds test_configure_xxx methods from source classes for
# every xxx option in the OPTIONS class attribute if they are not defined
# explicitly.
def decorator(cls):
for option in cls.OPTIONS:
methodname = 'test_configure_' + option
if not hasattr(cls, methodname):
for source_class in source_classes:
if hasattr(source_class, methodname):
setattr(cls, methodname,
getattr(source_class, methodname))
break
else:
def test(self, option=option):
widget = self.create()
widget[option]
raise AssertionError('Option "%s" is not tested in %s' %
(option, cls.__name__))
test.__name__ = methodname
setattr(cls, methodname, test)
return cls
return decorator
def setUpModule():
if test.support.verbose:
tcl = tkinter.Tcl()
print('patchlevel =', tcl.call('info', 'patchlevel'), flush=True)
+53
View File
@@ -0,0 +1,53 @@
import os.path
import unittest
from test import support
from test.support import import_helper
if support.check_sanitizer(address=True, memory=True):
raise unittest.SkipTest("Tests involving libX11 can SEGFAULT on ASAN/MSAN builds")
# Skip this test if _tkinter wasn't built.
import_helper.import_module('_tkinter')
# Skip test if tk cannot be initialized.
support.requires('gui')
import tkinter
from _tkinter import TclError
from tkinter import ttk
class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
with self.assertWarnsRegex(
DeprecationWarning,
"'__version__' is deprecated and slated for removal in Python 3.20",
) as cm:
getattr(ttk, "__version__")
self.assertEqual(cm.filename, __file__)
def setUpModule():
wantobjects = support.get_resource_value('wantobjects')
if wantobjects is not None:
unittest.enterModuleContext(
support.swap_attr(tkinter, 'wantobjects', int(wantobjects)))
root = None
try:
root = tkinter.Tk()
button = ttk.Button(root)
button.destroy()
del button
except TclError as msg:
# assuming ttk is not available
raise unittest.SkipTest("ttk not available: %s" % msg)
finally:
if root is not None:
root.destroy()
del root
def load_tests(*args):
return support.load_package_tests(os.path.dirname(__file__), *args)
+4
View File
@@ -0,0 +1,4 @@
from . import load_tests
import unittest
unittest.main()
+404
View File
@@ -0,0 +1,404 @@
import sys
import unittest
import tkinter
from tkinter import ttk
from test.support import requires, gc_collect
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
get_tk_patchlevel, widget_eq)
requires('gui')
from test.test_ttk import test_widgets
class LabeledScaleTest(AbstractTkTest, unittest.TestCase):
def tearDown(self):
self.root.update_idletasks()
super().tearDown()
def test_widget_destroy(self):
# automatically created variable
x = ttk.LabeledScale(self.root)
var = x._variable._name
x.destroy()
gc_collect() # For PyPy or other GCs.
self.assertRaises(tkinter.TclError, x.tk.globalgetvar, var)
# manually created variable
myvar = tkinter.DoubleVar(self.root)
name = myvar._name
x = ttk.LabeledScale(self.root, variable=myvar)
x.destroy()
if self.wantobjects:
self.assertEqual(x.tk.globalgetvar(name), myvar.get())
else:
self.assertEqual(float(x.tk.globalgetvar(name)), myvar.get())
del myvar
gc_collect() # For PyPy or other GCs.
self.assertRaises(tkinter.TclError, x.tk.globalgetvar, name)
# checking that the tracing callback is properly removed
myvar = tkinter.IntVar(self.root)
# LabeledScale will start tracing myvar
x = ttk.LabeledScale(self.root, variable=myvar)
x.destroy()
# Unless the tracing callback was removed, creating a new
# LabeledScale with the same var will cause an error now. This
# happens because the variable will be set to (possibly) a new
# value which causes the tracing callback to be called and then
# it tries calling instance attributes not yet defined.
ttk.LabeledScale(self.root, variable=myvar)
if hasattr(sys, 'last_exc'):
self.assertNotEqual(type(sys.last_exc), tkinter.TclError)
elif hasattr(sys, 'last_type'):
self.assertNotEqual(sys.last_type, tkinter.TclError)
def test_initialization(self):
# master passing
master = tkinter.Frame(self.root)
x = ttk.LabeledScale(master)
self.assertEqual(x.master, master)
x.destroy()
# variable initialization/passing
passed_expected = (('0', 0), (0, 0), (10, 10),
(-1, -1), (sys.maxsize + 1, sys.maxsize + 1),
(2.5, 2), ('2.5', 2))
for pair in passed_expected:
x = ttk.LabeledScale(self.root, from_=pair[0])
self.assertEqual(x.value, pair[1])
x.destroy()
x = ttk.LabeledScale(self.root, from_=None)
self.assertRaises((ValueError, tkinter.TclError), x._variable.get)
x.destroy()
# variable should have its default value set to the from_ value
myvar = tkinter.DoubleVar(self.root, value=20)
x = ttk.LabeledScale(self.root, variable=myvar)
self.assertEqual(x.value, 0)
x.destroy()
# check that it is really using a DoubleVar
x = ttk.LabeledScale(self.root, variable=myvar, from_=0.5)
self.assertEqual(x.value, 0.5)
self.assertEqual(x._variable._name, myvar._name)
x.destroy()
# widget positionment
def check_positions(scale, scale_pos, label, label_pos):
self.assertEqual(scale.pack_info()['side'], scale_pos)
self.assertEqual(label.place_info()['anchor'], label_pos)
x = ttk.LabeledScale(self.root, compound='top')
check_positions(x.scale, 'bottom', x.label, 'n')
x.destroy()
x = ttk.LabeledScale(self.root, compound='bottom')
check_positions(x.scale, 'top', x.label, 's')
x.destroy()
# invert default positions
x = ttk.LabeledScale(self.root, compound='unknown')
check_positions(x.scale, 'top', x.label, 's')
x.destroy()
x = ttk.LabeledScale(self.root) # take default positions
check_positions(x.scale, 'bottom', x.label, 'n')
x.destroy()
# extra, and invalid, kwargs
self.assertRaises(tkinter.TclError, ttk.LabeledScale, master, a='b')
def test_horizontal_range(self):
lscale = ttk.LabeledScale(self.root, from_=0, to=10)
lscale.pack()
self.require_mapped(lscale)
linfo_1 = lscale.label.place_info()
prev_xcoord = lscale.scale.coords()[0]
self.assertEqual(prev_xcoord, int(linfo_1['x']))
# change range to: from -5 to 5. This should change the x coord of
# the scale widget, since 0 is at the middle of the new
# range.
lscale.scale.configure(from_=-5, to=5)
# The following update is needed since the test doesn't use mainloop,
# at the same time this shouldn't affect test outcome
lscale.update()
curr_xcoord = lscale.scale.coords()[0]
self.assertNotEqual(prev_xcoord, curr_xcoord)
# the label widget should have been repositioned too
linfo_2 = lscale.label.place_info()
self.assertEqual(lscale.label['text'], 0 if self.wantobjects else '0')
self.assertEqual(curr_xcoord, int(linfo_2['x']))
# change the range back
lscale.scale.configure(from_=0, to=10)
self.assertNotEqual(prev_xcoord, curr_xcoord)
self.assertEqual(prev_xcoord, int(linfo_1['x']))
lscale.destroy()
def test_variable_change(self):
x = ttk.LabeledScale(self.root)
x.pack()
self.require_mapped(x)
curr_xcoord = x.scale.coords()[0]
newval = x.value + 1
x.value = newval
# The following update is needed since the test doesn't use mainloop,
# at the same time this shouldn't affect test outcome
x.update()
self.assertEqual(x.value, newval)
self.assertEqual(x.label['text'],
newval if self.wantobjects else str(newval))
self.assertEqual(float(x.scale.get()), newval)
self.assertGreater(x.scale.coords()[0], curr_xcoord)
self.assertEqual(x.scale.coords()[0],
int(x.label.place_info()['x']))
# value outside range
if self.wantobjects:
conv = lambda x: x
else:
conv = int
x.value = conv(x.scale['to']) + 1 # no changes shouldn't happen
x.update()
self.assertEqual(x.value, newval)
self.assertEqual(conv(x.label['text']), newval)
self.assertEqual(float(x.scale.get()), newval)
self.assertEqual(x.scale.coords()[0],
int(x.label.place_info()['x']))
# non-integer value
x.value = newval = newval + 1.5
x.update()
self.assertEqual(x.value, int(newval))
self.assertEqual(conv(x.label['text']), int(newval))
self.assertEqual(float(x.scale.get()), newval)
x.destroy()
def test_resize(self):
x = ttk.LabeledScale(self.root)
x.pack(expand=True, fill='both')
gc_collect() # For PyPy or other GCs.
self.require_mapped(x)
width, height = x.master.winfo_width(), x.master.winfo_height()
width_new, height_new = width * 2, height * 2
x.value = 3
x.update()
x.master.wm_geometry("%dx%d" % (width_new, height_new))
self.assertEqual(int(x.label.place_info()['x']),
x.scale.coords()[0])
# Reset geometry
x.master.wm_geometry("%dx%d" % (width, height))
x.destroy()
class OptionMenuTest(test_widgets.MenubuttonTest, unittest.TestCase):
# OptionMenu documents only its own options, not the inherited
# Menubutton options (like the classic tkinter.OptionMenu).
test_options_in_docstring = None
def setUp(self):
super().setUp()
self.textvar = tkinter.StringVar(self.root)
def tearDown(self):
del self.textvar
super().tearDown()
def create(self, default='b', values=('a', 'b', 'c'), **kwargs):
return ttk.OptionMenu(self.root, self.textvar, default, *values, **kwargs)
def test_bad_kwarg(self):
with self.assertRaisesRegex(tkinter.TclError, r"^unknown option -image$"):
ttk.OptionMenu(self.root, self.textvar, 'b', image='')
def test_configure_class(self):
# Unlike a plain Menubutton, OptionMenu does not accept a class at
# construction, so only the read-only nature of the option is tested.
widget = self.create()
self.assertEqual(widget['class'], '')
errmsg = 'attempt to change read-only option'
if get_tk_patchlevel(self.root) < (8, 6, 0, 'beta', 3):
errmsg = 'Attempt to change read-only option'
self.checkInvalidParam(widget, 'class', 'Foo', errmsg=errmsg)
def test_configure_style(self):
# Like Menubutton, but OptionMenu does not accept a class at
# construction, so the custom-class part of the standard test is omitted.
widget = self.create()
self.assertEqual(widget['style'], '')
self.checkInvalidParam(widget, 'style', 'Foo',
errmsg='Layout Foo not found')
style = ttk.Style(self.root)
style.configure('Custom.TMenubutton')
self.checkParam(widget, 'style', 'Custom.TMenubutton')
def test_configure_menu(self):
# OptionMenu manages its own menu; ['menu'] returns that Menu widget.
widget = self.create()
self.assertIsInstance(widget['menu'], tkinter.Menu)
self.checkParam(widget, 'menu', widget['menu'], eq=widget_eq)
def test_configure_text(self):
# The displayed text is governed by the textvariable.
widget = self.create()
self.textvar.set('a')
self.assertEqual(widget['text'], 'a')
self.textvar.set('c')
self.assertEqual(widget['text'], 'c')
def test_widget_destroy(self):
var = tkinter.StringVar(self.root)
optmenu = ttk.OptionMenu(self.root, var)
name = var._name
optmenu.update_idletasks()
optmenu.destroy()
self.assertEqual(optmenu.tk.globalgetvar(name), var.get())
del var
gc_collect() # For PyPy or other GCs.
self.assertRaises(tkinter.TclError, optmenu.tk.globalgetvar, name)
def test_initialization(self):
self.assertRaises(tkinter.TclError,
ttk.OptionMenu, self.root, self.textvar, invalid='thing')
optmenu = ttk.OptionMenu(self.root, self.textvar, 'b', 'a', 'b')
self.assertEqual(optmenu._variable.get(), 'b')
self.assertTrue(optmenu['menu'])
self.assertTrue(optmenu['textvariable'])
optmenu.destroy()
def test_menu(self):
items = ('a', 'b', 'c')
default = 'a'
optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items)
found_default = False
for i in range(len(items)):
value = optmenu['menu'].entrycget(i, 'value')
self.assertEqual(value, items[i])
if value == default:
found_default = True
self.assertTrue(found_default)
optmenu.destroy()
# default shouldn't be in menu if it is not part of values
default = 'd'
optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items)
curr = None
i = 0
while True:
last, curr = curr, optmenu['menu'].entryconfigure(i, 'value')
if last == curr:
# no more menu entries
break
self.assertNotEqual(curr, default)
i += 1
self.assertEqual(i, len(items))
# check that variable is updated correctly
optmenu.pack()
gc_collect() # For PyPy or other GCs.
optmenu['menu'].invoke(0)
self.assertEqual(optmenu._variable.get(), items[0])
# changing to an invalid index shouldn't change the variable
self.assertRaises(tkinter.TclError, optmenu['menu'].invoke, -1)
self.assertEqual(optmenu._variable.get(), items[0])
optmenu.destroy()
# specifying a callback
success = []
def cb_test(item):
self.assertEqual(item, items[1])
success.append(True)
optmenu = ttk.OptionMenu(self.root, self.textvar, 'a', command=cb_test,
*items)
optmenu['menu'].invoke(1)
if not success:
self.fail("Menu callback not invoked")
optmenu.destroy()
def test_set_menu(self):
optmenu = ttk.OptionMenu(self.root, self.textvar, 'a', 'a', 'b', 'c')
menu = optmenu['menu']
self.assertEqual(menu.index('end'), 2)
# set_menu rebuilds the menu with new values and an optional default.
optmenu.set_menu('y', 'x', 'y', 'z')
self.assertEqual(self.textvar.get(), 'y')
self.assertEqual([menu.entrycget(i, 'label') for i in range(3)],
['x', 'y', 'z'])
# Without a default the variable is left unchanged.
optmenu.set_menu(None, 'p', 'q')
self.assertEqual(self.textvar.get(), 'y')
self.assertEqual([menu.entrycget(i, 'label') for i in range(2)],
['p', 'q'])
optmenu.destroy()
def test_unique_radiobuttons(self):
# check that radiobuttons are unique across instances (bpo25684)
items = ('a', 'b', 'c')
default = 'a'
optmenu = ttk.OptionMenu(self.root, self.textvar, default, *items)
textvar2 = tkinter.StringVar(self.root)
optmenu2 = ttk.OptionMenu(self.root, textvar2, default, *items)
optmenu.pack()
optmenu2.pack()
optmenu['menu'].invoke(1)
optmenu2['menu'].invoke(2)
optmenu_stringvar_name = optmenu['menu'].entrycget(0, 'variable')
optmenu2_stringvar_name = optmenu2['menu'].entrycget(0, 'variable')
self.assertNotEqual(optmenu_stringvar_name,
optmenu2_stringvar_name)
self.assertEqual(self.root.tk.globalgetvar(optmenu_stringvar_name),
items[1])
self.assertEqual(self.root.tk.globalgetvar(optmenu2_stringvar_name),
items[2])
optmenu.destroy()
optmenu2.destroy()
def test_trace_variable(self):
# prior to bpo45160, tracing a variable would cause the callback to be made twice
success = []
items = ('a', 'b', 'c')
textvar = tkinter.StringVar(self.root)
def cb_test(*args):
success.append(textvar.get())
optmenu = ttk.OptionMenu(self.root, textvar, "a", *items)
optmenu.pack()
cb_name = textvar.trace_add("write", cb_test)
optmenu['menu'].invoke(1)
self.assertEqual(success, ['b'])
self.assertEqual(textvar.get(), 'b')
textvar.trace_remove("write", cb_name)
optmenu.destroy()
def test_specify_name(self):
textvar = tkinter.StringVar(self.root)
widget = ttk.OptionMenu(self.root, textvar, ":)", name="option_menu_ex")
self.assertEqual(str(widget), ".option_menu_ex")
self.assertIs(self.root.children["option_menu_ex"], widget)
class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
def test_labeledscale(self):
self._test_widget(ttk.LabeledScale)
if __name__ == "__main__":
unittest.main()
+464
View File
@@ -0,0 +1,464 @@
import unittest
import sys
import tkinter
from tkinter import ttk
from tkinter import TclError
from test import support
from test.support import requires
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest, get_tk_patchlevel
requires('gui')
CLASS_NAMES = [
'.', 'ComboboxPopdownFrame', 'Heading',
'Horizontal.TProgressbar', 'Horizontal.TScale', 'Item', 'Sash',
'TButton', 'TCheckbutton', 'TCombobox', 'TEntry',
'TLabelframe', 'TLabelframe.Label', 'TMenubutton',
'TNotebook', 'TNotebook.Tab', 'Toolbutton', 'TProgressbar',
'TRadiobutton', 'Treeview', 'TScale', 'TScrollbar', 'TSpinbox',
'Vertical.TProgressbar', 'Vertical.TScale'
]
class StyleTest(AbstractTkTest, unittest.TestCase):
def setUp(self):
super().setUp()
self.style = ttk.Style(self.root)
def test_configure(self):
style = self.style
style.configure('TButton', background='yellow')
self.assertEqual(style.configure('TButton', 'background'),
'yellow')
self.assertIsInstance(style.configure('TButton'), dict)
def test_map(self):
style = self.style
# Single state
for states in ['active'], [('active',)]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'white')])
expected = [('active', 'white')]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)
# Multiple states
for states in ['pressed', '!disabled'], ['pressed !disabled'], [('pressed', '!disabled')]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'black')])
expected = [('pressed', '!disabled', 'black')]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)
# Default state
for states in [], [''], [()]:
with self.subTest(states=states):
style.map('TButton', background=[(*states, 'grey')])
expected = [('grey',)]
self.assertEqual(style.map('TButton', 'background'), expected)
m = style.map('TButton')
self.assertIsInstance(m, dict)
self.assertEqual(m['background'], expected)
def test_lookup(self):
style = self.style
style.configure('TButton', background='yellow')
style.map('TButton', background=[('active', 'background', 'blue')])
self.assertEqual(style.lookup('TButton', 'background'), 'yellow')
self.assertEqual(style.lookup('TButton', 'background',
['active', 'background']), 'blue')
self.assertEqual(style.lookup('TButton', 'optionnotdefined',
default='iknewit'), 'iknewit')
def test_layout(self):
style = self.style
self.assertRaises(tkinter.TclError, style.layout, 'NotALayout')
tv_style = style.layout('Treeview')
# "erase" Treeview layout
style.layout('Treeview', '')
self.assertEqual(style.layout('Treeview'),
[('null', {'sticky': 'nswe'})]
)
# restore layout
style.layout('Treeview', tv_style)
self.assertEqual(style.layout('Treeview'), tv_style)
# should return a list
self.assertIsInstance(style.layout('TButton'), list)
# correct layout, but "option" doesn't exist as option
self.assertRaises(tkinter.TclError, style.layout, 'Treeview',
[('name', {'option': 'inexistent'})])
def test_theme_use(self):
self.assertRaises(tkinter.TclError, self.style.theme_use,
'nonexistingname')
curr_theme = self.style.theme_use()
new_theme = None
for theme in self.style.theme_names():
if theme != curr_theme:
new_theme = theme
self.style.theme_use(theme)
break
else:
# just one theme available, can't go on with tests
return
self.assertFalse(curr_theme == new_theme)
self.assertFalse(new_theme != self.style.theme_use())
self.style.theme_use(curr_theme)
def test_theme_settings(self):
style = self.style
theme = style.theme_use()
style.theme_settings(theme, {
'Test.TLabel': {
'configure': {'foreground': 'red', 'background': 'blue'},
'map': {'foreground': [('active', 'green')]},
},
})
self.assertEqual(style.lookup('Test.TLabel', 'foreground'), 'red')
self.assertEqual(style.lookup('Test.TLabel', 'background'), 'blue')
self.assertEqual(style.map('Test.TLabel', 'foreground'),
[('active', 'green')])
self.assertRaises(tkinter.TclError, style.theme_settings,
'nonexistingname', {})
def test_configure_custom_copy(self):
style = self.style
curr_theme = self.style.theme_use()
self.addCleanup(self.style.theme_use, curr_theme)
for theme in self.style.theme_names():
self.style.theme_use(theme)
for name in CLASS_NAMES:
default = style.configure(name)
if not default:
continue
with self.subTest(theme=theme, name=name):
if support.verbose >= 2:
print('configure', theme, name, default)
if (theme in ('vista', 'xpnative')
and sys.getwindowsversion()[:2] == (6, 1)):
# Fails on the Windows 7 buildbot
continue
newname = f'C.{name}'
self.assertEqual(style.configure(newname), None)
style.configure(newname, **default)
self.assertEqual(style.configure(newname), default)
for key, value in default.items():
self.assertEqual(style.configure(newname, key), value)
def test_map_custom_copy(self):
style = self.style
curr_theme = self.style.theme_use()
self.addCleanup(self.style.theme_use, curr_theme)
for theme in self.style.theme_names():
self.style.theme_use(theme)
for name in CLASS_NAMES:
default = style.map(name)
if not default:
continue
with self.subTest(theme=theme, name=name):
if support.verbose >= 2:
print('map', theme, name, default)
if (theme in ('vista', 'xpnative')
and sys.getwindowsversion()[:2] == (6, 1)):
# Fails on the Windows 7 buildbot
continue
newname = f'C.{name}'
self.assertEqual(style.map(newname), {})
style.map(newname, **default)
if theme == 'alt' and name == '.' and get_tk_patchlevel(self.root) < (8, 6, 1):
default['embossed'] = [('disabled', '1')]
self.assertEqual(style.map(newname), default)
for key, value in default.items():
self.assertEqual(style.map(newname, key), value)
def test_element_options(self):
style = self.style
element_names = style.element_names()
self.assertNotIsInstance(element_names, str)
for name in element_names:
self.assertIsInstance(name, str)
element_options = style.element_options(name)
self.assertNotIsInstance(element_options, str)
for optname in element_options:
self.assertIsInstance(optname, str)
def test_element_create_errors(self):
style = self.style
with self.assertRaises(TypeError):
style.element_create('plain.newelem')
with self.assertRaisesRegex(TclError, 'No such element type spam'):
style.element_create('plain.newelem', 'spam')
def test_element_create_from(self):
style = self.style
style.element_create('plain.background', 'from', 'default')
self.assertIn('plain.background', style.element_names())
style.element_create('plain.arrow', 'from', 'default', 'rightarrow')
self.assertIn('plain.arrow', style.element_names())
def test_element_create_from_errors(self):
style = self.style
with self.assertRaises(IndexError):
style.element_create('plain.newelem', 'from')
with self.assertRaisesRegex(TclError,
'theme "spam" (does not|doesn\'t) exist'):
style.element_create('plain.newelem', 'from', 'spam')
def test_element_create_image(self):
style = self.style
image = tkinter.PhotoImage(master=self.root, width=12, height=10)
style.element_create('block', 'image', image)
self.assertIn('block', style.element_names())
style.layout('TestLabel1', [('block', {'sticky': 'news'})])
a = ttk.Label(self.root, style='TestLabel1')
a.pack(expand=True, fill='both')
self.assertEqual(a.winfo_reqwidth(), 12)
self.assertEqual(a.winfo_reqheight(), 10)
imgfile = support.findfile('python.xbm', subdir='tkinterdata')
img1 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='yellow', background='blue')
img2 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='blue', background='yellow')
img3 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='white', background='black')
style.element_create('TestButton.button', 'image',
img1, ('pressed', img2), ('active', img3),
border=(2, 4), sticky='we')
self.assertIn('TestButton.button', style.element_names())
style.layout('TestButton', [('TestButton.button', {'sticky': 'news'})])
b = ttk.Button(self.root, style='TestButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 16)
self.assertEqual(b.winfo_reqheight(), 16)
def test_element_create_image_errors(self):
style = self.style
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
with self.assertRaises(IndexError):
style.element_create('block2', 'image')
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, 1)
with self.assertRaises(ValueError):
style.element_create('block2', 'image', image, ())
with self.assertRaisesRegex(TclError, 'Invalid state name'):
style.element_create('block2', 'image', image, ('spam', image))
with self.assertRaisesRegex(TclError, 'Invalid state name'):
style.element_create('block2', 'image', image, (1, image))
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, ('pressed', 1, image))
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, (1, 'selected', image))
with self.assertRaisesRegex(TclError, 'bad option'):
style.element_create('block2', 'image', image, spam=1)
def test_element_create_vsapi_1(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('smallclose', 'vsapi', 'WINDOW', 19, [
('disabled', 4),
('pressed', 3),
('active', 2),
('', 1)])
style.layout('CloseButton',
[('CloseButton.smallclose', {'sticky': 'news'})])
b = ttk.Button(self.root, style='CloseButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 13)
self.assertEqual(b.winfo_reqheight(), 13)
def test_element_create_vsapi_2(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, [
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1)])
style.layout('Explorer.Pin',
[('Explorer.Pin.pin', {'sticky': 'news'})])
pin = ttk.Checkbutton(self.root, style='Explorer.Pin')
pin.pack(expand=True, fill='both')
self.assertEqual(pin.winfo_reqwidth(), 16)
self.assertEqual(pin.winfo_reqheight(), 16)
def test_element_create_vsapi_3(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('headerclose', 'vsapi', 'EXPLORERBAR', 2, [
('pressed', 3),
('active', 2),
('', 1)])
style.layout('Explorer.CloseButton',
[('Explorer.CloseButton.headerclose', {'sticky': 'news'})])
b = ttk.Button(self.root, style='Explorer.CloseButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 16)
self.assertEqual(b.winfo_reqheight(), 16)
def test_theme_create(self):
style = self.style
curr_theme = style.theme_use()
curr_layout = style.layout('TLabel')
style.theme_create('testtheme1')
self.assertIn('testtheme1', style.theme_names())
style.theme_create('testtheme2', settings={
'elem' : {'element create': ['from', 'default'],},
'TLabel' : {
'configure': {'padding': 10},
'layout': [('elem', {'sticky': 'we'})],
},
})
self.assertIn('testtheme2', style.theme_names())
style.theme_create('testtheme3', 'testtheme2')
self.assertIn('testtheme3', style.theme_names())
style.theme_use('testtheme1')
self.assertEqual(style.element_names(), ())
self.assertEqual(style.layout('TLabel'), curr_layout)
style.theme_use('testtheme2')
self.assertEqual(style.element_names(), ('elem',))
self.assertEqual(style.lookup('TLabel', 'padding'), '10')
self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})])
style.theme_use('testtheme3')
self.assertEqual(style.element_names(), ())
self.assertEqual(style.lookup('TLabel', 'padding'), '')
self.assertEqual(style.layout('TLabel'), [('elem', {'sticky': 'we'})])
style.theme_use(curr_theme)
def test_theme_create_image(self):
style = self.style
curr_theme = style.theme_use()
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
new_theme = 'testtheme4'
style.theme_create(new_theme, settings={
'block' : {
'element create': ['image', image, {'width': 120, 'height': 100}],
},
'TestWidget.block2' : {
'element create': ['image', image],
},
'TestWidget' : {
'configure': {
'anchor': 'left',
'padding': (3, 0, 0, 2),
'foreground': 'yellow',
},
'map': {
'foreground': [
('pressed', 'red'),
('active', 'disabled', 'blue'),
],
},
'layout': [
('TestWidget.block', {'sticky': 'we', 'side': 'left'}),
('TestWidget.border', {
'sticky': 'nsw',
'border': 1,
'children': [
('TestWidget.block2', {'sticky': 'nswe'})
]
})
],
},
})
style.theme_use(new_theme)
self.assertIn('block', style.element_names())
self.assertEqual(style.lookup('TestWidget', 'anchor'), 'left')
self.assertEqual(style.lookup('TestWidget', 'padding'), '3 0 0 2')
self.assertEqual(style.lookup('TestWidget', 'foreground'), 'yellow')
self.assertEqual(style.lookup('TestWidget', 'foreground',
['active']), 'yellow')
self.assertEqual(style.lookup('TestWidget', 'foreground',
['active', 'pressed']), 'red')
self.assertEqual(style.lookup('TestWidget', 'foreground',
['active', 'disabled']), 'blue')
self.assertEqual(style.layout('TestWidget'),
[
('TestWidget.block', {'side': 'left', 'sticky': 'we'}),
('TestWidget.border', {
'sticky': 'nsw',
'border': '1',
'children': [('TestWidget.block2', {'sticky': 'nswe'})]
})
])
b = ttk.Label(self.root, style='TestWidget')
b.pack(expand=True, fill='both')
# The exact width varies with the Tk version and display scaling.
self.assertGreater(b.winfo_reqwidth(), 130)
self.assertEqual(b.winfo_reqheight(), 100)
style.theme_use(curr_theme)
def test_theme_create_vsapi(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
curr_theme = style.theme_use()
new_theme = 'testtheme5'
style.theme_create(new_theme, settings={
'pin' : {
'element create': ['vsapi', 'EXPLORERBAR', 3, [
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1)]],
},
'Explorer.Pin' : {
'layout': [('Explorer.Pin.pin', {'sticky': 'news'})],
},
})
style.theme_use(new_theme)
self.assertIn('pin', style.element_names())
self.assertEqual(style.layout('Explorer.Pin'),
[('Explorer.Pin.pin', {'sticky': 'nswe'})])
pin = ttk.Checkbutton(self.root, style='Explorer.Pin')
pin.pack(expand=True, fill='both')
self.assertEqual(pin.winfo_reqwidth(), 16)
self.assertEqual(pin.winfo_reqheight(), 16)
style.theme_use(curr_theme)
if __name__ == "__main__":
unittest.main()
+2202
View File
File diff suppressed because it is too large. Load diff
+481
View File
@@ -0,0 +1,481 @@
from test.support import import_helper
# Skip this test if _tkinter does not exist.
import_helper.import_module('_tkinter')
import unittest
from tkinter import ttk
class MockTkApp:
def splitlist(self, arg):
if isinstance(arg, tuple):
return arg
return arg.split(':')
def wantobjects(self):
return True
class MockTclObj(object):
typename = 'test'
def __init__(self, val):
self.val = val
def __str__(self):
return str(self.val)
class MockStateSpec(object):
typename = 'StateSpec'
def __init__(self, *args):
self.val = args
def __str__(self):
return ' '.join(self.val)
class InternalFunctionsTest(unittest.TestCase):
def test_format_optdict(self):
def check_against(fmt_opts, result):
for i in range(0, len(fmt_opts), 2):
self.assertEqual(result.pop(fmt_opts[i]), fmt_opts[i + 1])
if result:
self.fail("result still got elements: %s" % result)
# passing an empty dict should return an empty object (tuple here)
self.assertFalse(ttk._format_optdict({}))
# check list formatting
check_against(
ttk._format_optdict({'fg': 'blue', 'padding': [1, 2, 3, 4]}),
{'-fg': 'blue', '-padding': '1 2 3 4'})
# check tuple formatting (same as list)
check_against(
ttk._format_optdict({'test': (1, 2, '', 0)}),
{'-test': '1 2 {} 0'})
# check untouched values
check_against(
ttk._format_optdict({'test': {'left': 'as is'}}),
{'-test': {'left': 'as is'}})
# check script formatting
check_against(
ttk._format_optdict(
{'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()
# check if giving unicode keys is fine
check_against(ttk._format_optdict(opts), {'-αβγ': True, '-á': False})
# opts should remain unchanged
self.assertEqual(opts, orig_opts)
# passing values with spaces inside a tuple/list
check_against(
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
self.assertEqual(amount_opts, len(opts) - 1)
# ignore non-existing options
amount_opts = len(ttk._format_optdict(opts, ignore=('á', 'b'))) / 2
self.assertEqual(amount_opts, len(opts) - 1)
# ignore every option
self.assertFalse(ttk._format_optdict(opts, ignore=list(opts.keys())))
def test_format_mapdict(self):
opts = {'a': [('b', 'c', 'val'), ('d', 'otherval'), ('', 'single')]}
result = ttk._format_mapdict(opts)
self.assertEqual(len(result), len(list(opts.keys())) * 2)
self.assertEqual(result, ('-a', '{b c} val d otherval {} single'))
self.assertEqual(ttk._format_mapdict(opts, script=True),
('-a', '{{b c} val d otherval {} single}'))
self.assertEqual(ttk._format_mapdict({2: []}), ('-2', ''))
opts = {'üñíćódè': [('á', 'vãl')]}
result = ttk._format_mapdict(opts)
self.assertEqual(result, ('-üñíćódè', 'á vãl'))
self.assertEqual(ttk._format_mapdict({'opt': [('value',)]}),
('-opt', '{} value'))
# empty states
valid = {'opt': [('', '', 'hi')]}
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '{ } hi'))
# when passing multiple states, they all must be strings
invalid = {'opt': [(1, 2, 'valid val')]}
self.assertRaises(TypeError, ttk._format_mapdict, invalid)
invalid = {'opt': [([1], '2', 'valid val')]}
self.assertRaises(TypeError, ttk._format_mapdict, invalid)
# but when passing a single state, it can be anything
valid = {'opt': [[1, 'value']]}
self.assertEqual(ttk._format_mapdict(valid), ('-opt', '1 value'))
# special attention to single states which evaluate to False
for stateval in (None, 0, False, '', set()): # just some samples
valid = {'opt': [(stateval, 'value')]}
self.assertEqual(ttk._format_mapdict(valid),
('-opt', '{} value'))
# values must be iterable
opts = {'a': None}
self.assertRaises(TypeError, ttk._format_mapdict, opts)
def test_format_elemcreate(self):
self.assertTrue(ttk._format_elemcreate(None), (None, ()))
## Testing type = image
# image type expects at least an image name, so this should raise
# IndexError since it tries to access the index 0 of an empty tuple
self.assertRaises(IndexError, ttk._format_elemcreate, 'image')
# don't format returned values as a tcl script
# minimum acceptable for image type
self.assertEqual(ttk._format_elemcreate('image', False, 'test'),
("test", ()))
# specifying a state spec
self.assertEqual(ttk._format_elemcreate('image', False, 'test',
('', 'a')), ("test {} a", ()))
# state spec with multiple states
self.assertEqual(ttk._format_elemcreate('image', False, 'test',
('a', 'b', 'c')), ("test {a b} c", ()))
# state spec and options
self.assertEqual(ttk._format_elemcreate('image', False, 'test',
('a', 'b'), a='x'), ("test a b", ("-a", "x")))
# format returned values as a tcl script
# state spec with multiple states and an option with a multivalue
self.assertEqual(ttk._format_elemcreate('image', True, 'test',
('a', 'b', 'c', 'd'), x=[2, 3]), ("{test {a b c} d}", "-x {2 3}"))
## Testing type = vsapi
# vsapi type expects at least a class name and a part_id, so this
# should raise a ValueError since it tries to get two elements from
# an empty tuple
self.assertRaises(ValueError, ttk._format_elemcreate, 'vsapi')
# don't format returned values as a tcl script
# minimum acceptable for vsapi
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b'),
('a', 'b', ('', 1), ()))
# now with a state spec with multiple states
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
[('a', 'b', 'c')]), ('a', 'b', ('a b', 'c'), ()))
# state spec and option
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
[('a', 'b')], opt='x'), ('a', 'b', ('a', 'b'), ("-opt", "x")))
# format returned values as a tcl script
# state spec with a multivalue and an option
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
opt='x'), ("a b {{} 1}", "-opt x"))
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
[('a', 'b', [1, 2])], opt='x'), ("a b {{a b} {1 2}}", "-opt x"))
# Testing type = from
# from type expects at least a type name
self.assertRaises(IndexError, ttk._format_elemcreate, 'from')
self.assertEqual(ttk._format_elemcreate('from', False, 'a'),
('a', ()))
self.assertEqual(ttk._format_elemcreate('from', False, 'a', 'b'),
('a', ('b',)))
self.assertEqual(ttk._format_elemcreate('from', True, 'a', 'b'),
('a', 'b'))
def test_format_layoutlist(self):
def sample(indent=0, indent_size=2):
return ttk._format_layoutlist(
[('a', {'other': [1, 2, 3], 'children':
[('b', {'children':
[('c', {'children':
[('d', {'nice': 'opt'})], 'something': (1, 2)
})]
})]
})], indent=indent, indent_size=indent_size)[0]
def sample_expected(indent=0, indent_size=2):
spaces = lambda amount=0: ' ' * (amount + indent)
return (
"%sa -other {1 2 3} -children {\n"
"%sb -children {\n"
"%sc -something {1 2} -children {\n"
"%sd -nice opt\n"
"%s}\n"
"%s}\n"
"%s}" % (spaces(), spaces(indent_size),
spaces(2 * indent_size), spaces(3 * indent_size),
spaces(2 * indent_size), spaces(indent_size), spaces()))
# empty layout
self.assertEqual(ttk._format_layoutlist([])[0], '')
# _format_layoutlist always expects the second item (in every item)
# to act like a dict (except when the value evaluates to False).
self.assertRaises(AttributeError,
ttk._format_layoutlist, [('a', 'b')])
smallest = ttk._format_layoutlist([('a', None)], indent=0)
self.assertEqual(smallest,
ttk._format_layoutlist([('a', '')], indent=0))
self.assertEqual(smallest[0], 'a')
# testing indentation levels
self.assertEqual(sample(), sample_expected())
for i in range(4):
self.assertEqual(sample(i), sample_expected(i))
self.assertEqual(sample(i, i), sample_expected(i, i))
# invalid layout format, different kind of exceptions will be
# raised by internal functions
# plain wrong format
self.assertRaises(ValueError, ttk._format_layoutlist,
['bad', 'format'])
# will try to use iteritems in the 'bad' string
self.assertRaises(AttributeError, ttk._format_layoutlist,
[('name', 'bad')])
# bad children formatting
self.assertRaises(ValueError, ttk._format_layoutlist,
[('name', {'children': {'a': None}})])
def test_script_from_settings(self):
# empty options
self.assertFalse(ttk._script_from_settings({'name':
{'configure': None, 'map': None, 'element create': None}}))
# empty layout
self.assertEqual(
ttk._script_from_settings({'name': {'layout': None}}),
"ttk::style layout name {\nnull\n}")
configdict = {'αβγ': True, 'á': False}
self.assertTrue(
ttk._script_from_settings({'name': {'configure': configdict}}))
mapdict = {'üñíćódè': [('á', 'vãl')]}
self.assertTrue(
ttk._script_from_settings({'name': {'map': mapdict}}))
# invalid image element
self.assertRaises(IndexError,
ttk._script_from_settings, {'name': {'element create': ['image']}})
# minimal valid image
self.assertTrue(ttk._script_from_settings({'name':
{'element create': ['image', 'name']}}))
image = {'thing': {'element create':
['image', 'name', ('state1', 'state2', 'val')]}}
self.assertEqual(ttk._script_from_settings(image),
"ttk::style element create thing image {name {state1 state2} val} ")
image['thing']['element create'].append({'opt': 30})
self.assertEqual(ttk._script_from_settings(image),
"ttk::style element create thing image {name {state1 state2} val} "
"-opt 30")
image['thing']['element create'][-1]['opt'] = [MockTclObj(3),
MockTclObj('2m')]
self.assertEqual(ttk._script_from_settings(image),
"ttk::style element create thing image {name {state1 state2} val} "
"-opt {3 2m}")
vsapi = {'pin': {'element create':
['vsapi', 'EXPLORERBAR', 3, [
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1)]]}}
self.assertEqual(ttk._script_from_settings(vsapi),
"ttk::style element create pin vsapi EXPLORERBAR 3 {"
"{pressed !selected} 3 "
"{active !selected} 2 "
"{pressed selected} 6 "
"{active selected} 5 "
"selected 4 "
"{} 1} ")
def test_tclobj_to_py(self):
self.assertEqual(
ttk._tclobj_to_py((MockStateSpec('a', 'b'), 'val')),
[('a', 'b', 'val')])
self.assertEqual(
ttk._tclobj_to_py([MockTclObj('1'), 2, MockTclObj('3m')]),
[1, 2, '3m'])
def test_list_from_statespec(self):
def test_it(sspec, value, res_value, states):
self.assertEqual(ttk._list_from_statespec(
(sspec, value)), [states + (res_value, )])
states_even = tuple('state%d' % i for i in range(6))
statespec = MockStateSpec(*states_even)
test_it(statespec, 'val', 'val', states_even)
test_it(statespec, MockTclObj('val'), 'val', states_even)
states_odd = tuple('state%d' % i for i in range(5))
statespec = MockStateSpec(*states_odd)
test_it(statespec, 'val', 'val', states_odd)
test_it(('a', 'b', 'c'), MockTclObj('val'), 'val', ('a', 'b', 'c'))
def test_list_from_layouttuple(self):
tk = MockTkApp()
# empty layout tuple
self.assertFalse(ttk._list_from_layouttuple(tk, ()))
# shortest layout tuple
self.assertEqual(ttk._list_from_layouttuple(tk, ('name', )),
[('name', {})])
# not so interesting ltuple
sample_ltuple = ('name', '-option', 'value')
self.assertEqual(ttk._list_from_layouttuple(tk, sample_ltuple),
[('name', {'option': 'value'})])
# empty children
self.assertEqual(ttk._list_from_layouttuple(tk,
('something', '-children', ())),
[('something', {'children': []})]
)
# more interesting ltuple
ltuple = (
'name', '-option', 'niceone', '-children', (
('otherone', '-children', (
('child', )), '-otheropt', 'othervalue'
)
)
)
self.assertEqual(ttk._list_from_layouttuple(tk, ltuple),
[('name', {'option': 'niceone', 'children':
[('otherone', {'otheropt': 'othervalue', 'children':
[('child', {})]
})]
})]
)
# bad tuples
self.assertRaises(ValueError, ttk._list_from_layouttuple, tk,
('name', 'no_minus'))
self.assertRaises(ValueError, ttk._list_from_layouttuple, tk,
('name', 'no_minus', 'value'))
self.assertRaises(ValueError, ttk._list_from_layouttuple, tk,
('something', '-children')) # no children
def test_val_or_dict(self):
def func(res, opt=None, val=None):
if opt is None:
return res
if val is None:
return "test val"
return (opt, val)
tk = MockTkApp()
tk.call = func
self.assertEqual(ttk._val_or_dict(tk, {}, '-test:3'),
{'test': '3'})
self.assertEqual(ttk._val_or_dict(tk, {}, ('-test', 3)),
{'test': 3})
self.assertEqual(ttk._val_or_dict(tk, {'test': None}, 'x:y'),
'test val')
self.assertEqual(ttk._val_or_dict(tk, {'test': 3}, 'x:y'),
{'test': 3})
def test_convert_stringval(self):
tests = (
(0, 0), ('09', 9), ('a', 'a'), ('áÚ', 'áÚ'), ([], '[]'),
(None, 'None')
)
for orig, expected in tests:
self.assertEqual(ttk._convert_stringval(orig), expected)
class TclObjsToPyTest(unittest.TestCase):
def test_unicode(self):
adict = {'opt': 'välúè'}
self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': 'välúè'})
adict['opt'] = MockTclObj(adict['opt'])
self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': 'välúè'})
def test_multivalues(self):
adict = {'opt': [1, 2, 3, 4]}
self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': [1, 2, 3, 4]})
adict['opt'] = [1, 'xm', 3]
self.assertEqual(ttk.tclobjs_to_py(adict), {'opt': [1, 'xm', 3]})
adict['opt'] = (MockStateSpec('a', 'b'), 'válũè')
self.assertEqual(ttk.tclobjs_to_py(adict),
{'opt': [('a', 'b', 'válũè')]})
self.assertEqual(ttk.tclobjs_to_py({'x': ['y z']}),
{'x': ['y z']})
def test_nosplit(self):
self.assertEqual(ttk.tclobjs_to_py({'text': 'some text'}),
{'text': 'some text'})
if __name__ == '__main__':
unittest.main()
+22
View File
@@ -577,6 +577,28 @@ class TestTurtleScreen(unittest.TestCase):
s.update.assert_not_called()
s.update.assert_called_once()
def test_update_is_not_reentrant(self):
# ondrag(goto) reenters _update() while cv.update() processes events;
# without a guard this recurses without bound (gh-50966).
s = turtle.TurtleScreen(cv=unittest.mock.MagicMock())
depth = max_depth = 0
def reenter():
nonlocal depth, max_depth
depth += 1
max_depth = max(max_depth, depth)
if depth < 50:
s._update() # as an event handler would
depth -= 1
s.cv.update.reset_mock() # ignore calls made during construction
s.cv.update.side_effect = reenter
s._update()
# cv.update() runs once; reentrant calls only flush idle tasks.
self.assertEqual(s.cv.update.call_count, 1)
self.assertEqual(max_depth, 1)
self.assertTrue(s.cv.update_idletasks.called)
class TestTurtle(unittest.TestCase):
def setUp(self):
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

BIN
View File
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#define python_width 16
#define python_height 16
static char python_bits[] = {
0xDF, 0xFE, 0x8F, 0xFD, 0x5F, 0xFB, 0xAB, 0xFE, 0xB5, 0x8D, 0xDA, 0x8F,
0xA5, 0x86, 0xFA, 0x83, 0x1A, 0x80, 0x0D, 0x80, 0x0D, 0x80, 0x0F, 0xE0,
0x0F, 0xF8, 0x0F, 0xF8, 0x0F, 0xFC, 0xFF, 0xFF, };
+245 -86
View File
@@ -3,7 +3,7 @@
Tkinter provides classes which allow the display, positioning and
control of widgets. Toplevel widgets are Tk and Toplevel. Other
widgets are Frame, Label, Entry, Text, Canvas, Button, Radiobutton,
Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox
Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox,
LabelFrame and PanedWindow.
Properties of the widgets are specified with keyword arguments.
@@ -255,6 +255,10 @@ class Event:
type - type of the event as a number
widget - widget in which the event occurred
delta - delta of wheel movement (MouseWheel)
detail - certain fixed strings (see Tcl/Tk documentation)
(Enter, Leave, FocusIn, FocusOut, ConfigureRequest)
user_data - data string which was passed to event_generate() or empty
string (VirtualEvent)
"""
def __repr__(self):
@@ -501,12 +505,14 @@ class Variable:
Return the name of the callback.
This deprecated method wraps a deprecated Tcl method removed
in Tcl 9.0. Use trace_add() instead.
in Tcl 9.0 and will be removed in Python 3.17. Use trace_add()
instead.
"""
import warnings
warnings.warn(
"trace_variable() is deprecated and not supported with Tcl 9; "
"use trace_add() instead.",
"trace_variable() is deprecated and will be removed in Python "
"3.17; use trace_add() instead. It is not supported with "
"Tcl 9.",
DeprecationWarning, stacklevel=2)
cbname = self._register(callback)
self._tk.call("trace", "variable", self._name, mode, cbname)
@@ -521,12 +527,14 @@ class Variable:
CBNAME is the name of the callback returned from trace_variable or trace.
This deprecated method wraps a deprecated Tcl method removed
in Tcl 9.0. Use trace_remove() instead.
in Tcl 9.0 and will be removed in Python 3.17. Use trace_remove()
instead.
"""
import warnings
warnings.warn(
"trace_vdelete() is deprecated and not supported with Tcl 9; "
"use trace_remove() instead.",
"trace_vdelete() is deprecated and will be removed in Python "
"3.17; use trace_remove() instead. It is not supported with "
"Tcl 9.",
DeprecationWarning, stacklevel=2)
self._tk.call("trace", "vdelete", self._name, mode, cbname)
cbname = self._tk.splitlist(cbname)[0]
@@ -544,12 +552,14 @@ class Variable:
"""Return all trace callback information.
This deprecated method wraps a deprecated Tcl method removed
in Tcl 9.0. Use trace_info() instead.
in Tcl 9.0 and will be removed in Python 3.17. Use trace_info()
instead.
"""
import warnings
warnings.warn(
"trace_vinfo() is deprecated and not supported with Tcl 9; "
"use trace_info() instead.",
"trace_vinfo() is deprecated and will be removed in Python "
"3.17; use trace_info() instead. It is not supported with "
"Tcl 9.",
DeprecationWarning, stacklevel=2)
return [self._tk.splitlist(x) for x in self._tk.splitlist(
self._tk.call("trace", "vinfo", self._name))]
@@ -817,7 +827,10 @@ class Misc:
the focus."""
name = self.tk.call('focus')
if name == 'none' or not name: return None
return self._nametowidget(name)
try:
return self._nametowidget(name)
except KeyError:
return None
def focus_displayof(self):
"""Return the widget which has currently the focus on the
@@ -826,14 +839,20 @@ class Misc:
Return None if the application does not have the focus."""
name = self.tk.call('focus', '-displayof', self._w)
if name == 'none' or not name: return None
return self._nametowidget(name)
try:
return self._nametowidget(name)
except KeyError:
return None
def focus_lastfor(self):
"""Return the widget which would have the focus if top level
for this widget gets the focus from the window manager."""
name = self.tk.call('focus', '-lastfor', self._w)
if name == 'none' or not name: return None
return self._nametowidget(name)
try:
return self._nametowidget(name)
except KeyError:
return None
def tk_focusFollowsMouse(self):
"""The widget under mouse will get automatically focus. Can not
@@ -1236,7 +1255,10 @@ class Misc:
+ self._displayof(displayof) + (rootX, rootY)
name = self.tk.call(args)
if not name: return None
return self._nametowidget(name)
try:
return self._nametowidget(name)
except KeyError:
return None
def winfo_depth(self):
"""Return the number of bits per pixel."""
@@ -1499,13 +1521,26 @@ class Misc:
else:
self.tk.call('bindtags', self._w, tagList)
def _bind(self, what, sequence, func, add, needcleanup=1):
def _delete_bind_commands(self, *what):
lines = self.tk.call(what).split('\n')
p = re.compile(r'if \{"\[([^ ]+) .*\]" == "break"\} break')
for line in lines:
m = p.fullmatch(line)
if m:
funcid = m[1]
try:
self.deletecommand(funcid)
except TclError:
pass
def _bind(self, what, sequence, func, add):
"""Internal function."""
if isinstance(func, str):
self.tk.call(what + (sequence, func))
elif func:
funcid = self._register(func, self._substitute,
needcleanup)
if not add:
self._delete_bind_commands(*what, sequence)
funcid = self._register(func, self._substitute, needcleanup=True)
cmd = ('%sif {"[%s %s]" == "break"} break\n'
%
(add and '+' or '',
@@ -1538,7 +1573,7 @@ class Misc:
<Alt-A> for pressing A and the Alt key (KeyPress can be omitted).
An event pattern can also be a virtual event of the form
<<AString>> where AString can be arbitrary. This
event can be generated by event_generate.
event can be generated by event_generate().
If events are concatenated they must appear shortly
after each other.
@@ -1571,6 +1606,7 @@ class Misc:
def _unbind(self, what, funcid=None):
if funcid is None:
self._delete_bind_commands(*what)
self.tk.call(*what, '')
else:
lines = self.tk.call(what).split('\n')
@@ -1587,7 +1623,7 @@ class Misc:
An additional boolean parameter ADD specifies whether FUNC will
be called additionally to the other bound function or whether
it will replace the previous function. See bind for the return value."""
return self._root()._bind(('bind', 'all'), sequence, func, add, True)
return self._root()._bind(('bind', 'all'), sequence, func, add)
def unbind_all(self, sequence):
"""Unbind for all widgets for event SEQUENCE all functions."""
@@ -1601,7 +1637,7 @@ class Misc:
whether it will replace the previous function. See bind for
the return value."""
return self._root()._bind(('bind', className), sequence, func, add, True)
return self._root()._bind(('bind', className), sequence, func, add)
def unbind_class(self, className, sequence):
"""Unbind for all widgets with bindtag CLASSNAME for event SEQUENCE
@@ -1688,7 +1724,16 @@ class Misc:
for n in name:
if not n:
break
w = w.children[n]
try:
w = w.children[n]
except KeyError:
# Menu clones (a menu used as a menubar or a cascade) get
# auto-generated names where each path component is the
# original name prefixed with one or more '#' clone markers.
# Map such a name back to the original widget.
if not n.startswith('#'):
raise
w = w.children[n.rsplit('#', 1)[-1]]
return w
@@ -1723,7 +1768,7 @@ class Misc:
w = self
while w.master is not None: w = w.master
return w
_subst_format = ('%#', '%b', '%f', '%h', '%k',
_subst_format = ('%#', '%b', '%d', '%f', '%h', '%k',
'%s', '%t', '%w', '%x', '%y',
'%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D')
_subst_format_str = " ".join(_subst_format)
@@ -1744,11 +1789,14 @@ class Misc:
if any(isinstance(s, tuple) for s in args):
args = [s[0] if isinstance(s, tuple) and len(s) == 1 else s
for s in args]
nsign, b, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args
# Missing: (a, c, d, m, o, v, B, R)
nsign, b, d, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args
# Missing: (a, c, m, o, v, B, R)
e = Event()
# serial field: valid for all events
# number of button: ButtonPress and ButtonRelease events only
# detail: for Enter, Leave, FocusIn, FocusOut and ConfigureRequest
# events certain fixed strings (see Tcl/Tk documentation)
# user_data: data string from a virtual event or an empty string
# height field: Configure, ConfigureRequest, Create,
# ResizeRequest, and Expose events only
# keycode field: KeyPress and KeyRelease events only
@@ -1762,6 +1810,12 @@ class Misc:
# KeyRelease, and Motion events
e.serial = getint(nsign)
e.num = getint_event(b)
if T == EventType.VirtualEvent:
e.user_data = d
e.detail = '??'
else:
e.user_data = '??'
e.detail = d
try: e.focus = getboolean(f)
except TclError: pass
e.height = getint_event(h)
@@ -1848,6 +1902,7 @@ class Misc:
return self.tk.call(self._w, 'cget', '-' + key)
__getitem__ = cget
__iter__ = None # prevent using __getitem__ for iteration
def __setitem__(self, key, value):
self.configure({key: value})
@@ -1884,9 +1939,21 @@ class Misc:
propagate = pack_propagate
def pack_slaves(self):
def pack_content(self):
"""Returns a list of all of the content widgets in the packing order
for this container."""
try:
res = self.tk.call('pack', 'content', self._w)
except TclError:
if self.info_patchlevel() >= (8, 6):
raise
res = self.tk.call('pack', 'slaves', self._w)
return [self._nametowidget(x) for x in self.tk.splitlist(res)]
content = pack_content
def pack_slaves(self):
"""Synonym for pack_content()."""
return [self._nametowidget(x) for x in
self.tk.splitlist(
self.tk.call('pack', 'slaves', self._w))]
@@ -1894,9 +1961,19 @@ class Misc:
slaves = pack_slaves
# Place method that applies to the container widget
def place_slaves(self):
def place_content(self):
"""Returns a list of all the content widgets for which this widget is
the container."""
try:
res = self.tk.call('place', 'content', self._w)
except TclError:
if self.info_patchlevel() >= (8, 6):
raise
res = self.tk.call('place', 'slaves', self._w)
return [self._nametowidget(x) for x in self.tk.splitlist(res)]
def place_slaves(self):
"""Synonym for place_content()."""
return [self._nametowidget(x) for x in
self.tk.splitlist(
self.tk.call(
@@ -2017,7 +2094,7 @@ class Misc:
size = grid_size
def grid_slaves(self, row=None, column=None):
def grid_content(self, row=None, column=None):
"""Returns a list of the content widgets.
If no arguments are supplied, a list of all of the content in this
@@ -2026,6 +2103,21 @@ class Misc:
column is returned.
"""
args = ()
if row is not None:
args = args + ('-row', row)
if column is not None:
args = args + ('-column', column)
try:
res = self.tk.call('grid', 'content', self._w, *args)
except TclError:
if self.info_patchlevel() >= (8, 6):
raise
res = self.tk.call('grid', 'slaves', self._w, *args)
return [self._nametowidget(x) for x in self.tk.splitlist(res)]
def grid_slaves(self, row=None, column=None):
"""Synonym for grid_content()."""
args = ()
if row is not None:
args = args + ('-row', row)
if column is not None:
@@ -2344,7 +2436,7 @@ class Wm:
def wm_iconposition(self, x=None, y=None):
"""Set the position of the icon of this widget to X and Y. Return
a tuple of the current values of X and X if None is given."""
a tuple of the current values of X and Y if None is given."""
return self._getints(self.tk.call(
'wm', 'iconposition', self._w, x, y))
@@ -2547,11 +2639,13 @@ class Tk(Misc, Wm):
if os.path.isfile(class_tcl):
self.tk.call('source', class_tcl)
if os.path.isfile(class_py):
exec(open(class_py).read(), dir)
with open(class_py, 'rb') as f:
exec(f.read(), dir)
if os.path.isfile(base_tcl):
self.tk.call('source', base_tcl)
if os.path.isfile(base_py):
exec(open(base_py).read(), dir)
with open(base_py, 'rb') as f:
exec(f.read(), dir)
def report_callback_exception(self, exc, val, tb):
"""Report callback exception on sys.stderr.
@@ -2640,6 +2734,7 @@ class Pack:
info = pack_info
propagate = pack_propagate = Misc.pack_propagate
content = pack_content = Misc.pack_content
slaves = pack_slaves = Misc.pack_slaves
@@ -2697,6 +2792,7 @@ class Place:
return d
info = place_info
content = place_content = Misc.place_content
slaves = place_slaves = Misc.place_slaves
@@ -2752,6 +2848,7 @@ class Grid:
propagate = grid_propagate = Misc.grid_propagate
rowconfigure = grid_rowconfigure = Misc.grid_rowconfigure
size = grid_size = Misc.grid_size
content = grid_content = Misc.grid_content
slaves = grid_slaves = Misc.grid_slaves
@@ -2834,10 +2931,11 @@ class Toplevel(BaseWidget, Wm):
def __init__(self, master=None, cnf={}, **kw):
"""Construct a toplevel widget with the parent MASTER.
Valid option names: background, bd, bg, borderwidth, class,
colormap, container, cursor, height, highlightbackground,
highlightcolor, highlightthickness, menu, relief, screen, takefocus,
use, visual, width."""
Valid option names: background, backgroundimage (Tk 9.0+), bd, bg,
bgimg (Tk 9.0+), borderwidth, class, colormap, container,
cursor, height, highlightbackground, highlightcolor,
highlightthickness, menu, padx, pady, relief, screen,
takefocus, tile (Tk 9.0+), use, visual, width."""
if kw:
cnf = _cnfmerge((cnf, kw))
extra = ()
@@ -2957,7 +3055,7 @@ class Canvas(Widget, XView, YView):
"""Add tag NEWTAG to all items with TAGORID."""
self.addtag(newtag, 'withtag', tagOrId)
def bbox(self, *args):
def bbox(self, *args): # overrides Misc.bbox
"""Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle
which encloses all items with tags specified as arguments."""
return self._getints(
@@ -3096,7 +3194,7 @@ class Canvas(Widget, XView, YView):
"""Return all items with TAGORID."""
return self.find('withtag', tagOrId)
def focus(self, *args):
def focus(self, *args): # overrides Misc.focus
"""Set focus to the first item specified in ARGS."""
return self.tk.call((self._w, 'focus') + args)
@@ -3142,7 +3240,7 @@ class Canvas(Widget, XView, YView):
(optional below another item)."""
self.tk.call((self._w, 'lower') + args)
lower = tag_lower
lower = tag_lower # overrides Misc.lower
def move(self, *args):
"""Move an item TAGORID given in ARGS."""
@@ -3170,7 +3268,7 @@ class Canvas(Widget, XView, YView):
(optional above another item)."""
self.tk.call((self._w, 'raise') + args)
lift = tkraise = tag_raise
lift = tkraise = tag_raise # overrides Misc.tkraise
def scale(self, *args):
"""Scale item TAGORID with XORIGIN, YORIGIN, XSCALE, YSCALE."""
@@ -3220,12 +3318,13 @@ class Checkbutton(Widget):
"""Construct a checkbutton widget with the parent MASTER.
Valid option names: activebackground, activeforeground, anchor,
background, bd, bg, bitmap, borderwidth, command, cursor,
disabledforeground, fg, font, foreground, height,
highlightbackground, highlightcolor, highlightthickness, image,
indicatoron, justify, offvalue, onvalue, padx, pady, relief,
selectcolor, selectimage, state, takefocus, text, textvariable,
underline, variable, width, wraplength."""
background, bd, bg, bitmap, borderwidth, command, compound,
cursor, disabledforeground, fg, font, foreground, height,
highlightbackground, highlightcolor, highlightthickness,
image, indicatoron, justify, offrelief, offvalue, onvalue,
overrelief, padx, pady, relief, selectcolor, selectimage,
state, takefocus, text, textvariable, tristateimage,
tristatevalue, underline, variable, width, wraplength."""
Widget.__init__(self, master, 'checkbutton', cnf, kw)
def _setup(self, master, cnf):
@@ -3269,13 +3368,15 @@ class Entry(Widget, XView):
"""Construct an entry widget with the parent MASTER.
Valid option names: background, bd, bg, borderwidth, cursor,
exportselection, fg, font, foreground, highlightbackground,
highlightcolor, highlightthickness, insertbackground,
insertborderwidth, insertofftime, insertontime, insertwidth,
invalidcommand, invcmd, justify, relief, selectbackground,
selectborderwidth, selectforeground, show, state, takefocus,
textvariable, validate, validatecommand, vcmd, width,
xscrollcommand."""
disabledbackground, disabledforeground, exportselection, fg,
font, foreground, highlightbackground, highlightcolor,
highlightthickness, insertbackground, insertborderwidth,
insertofftime, insertontime, insertwidth, invalidcommand,
invcmd, justify, locale (Tk 9.1+), placeholder (Tk 9.0+),
placeholderforeground (Tk 9.0+), readonlybackground, relief,
selectbackground, selectborderwidth, selectforeground, show,
state, takefocus, textvariable, validate, validatecommand,
vcmd, width, xscrollcommand."""
Widget.__init__(self, master, 'entry', cnf, kw)
def delete(self, first, last=None):
@@ -3315,7 +3416,7 @@ class Entry(Widget, XView):
select_adjust = selection_adjust
def selection_clear(self):
def selection_clear(self): # overrides Misc.selection_clear
"""Clear the selection if it is in this widget."""
self.tk.call(self._w, 'selection', 'clear')
@@ -3354,9 +3455,11 @@ class Frame(Widget):
def __init__(self, master=None, cnf={}, **kw):
"""Construct a frame widget with the parent MASTER.
Valid option names: background, bd, bg, borderwidth, class,
colormap, container, cursor, height, highlightbackground,
highlightcolor, highlightthickness, relief, takefocus, visual, width."""
Valid option names: background, backgroundimage (Tk 9.0+), bd, bg,
bgimg (Tk 9.0+), borderwidth, class, colormap, container,
cursor, height, highlightbackground, highlightcolor,
highlightthickness, padx, pady, relief, takefocus, tile (Tk
9.0+), visual, width."""
cnf = _cnfmerge((cnf, kw))
extra = ()
if 'class_' in cnf:
@@ -3386,7 +3489,8 @@ class Label(Widget):
WIDGET-SPECIFIC OPTIONS
height, state, width
compound, height, state,
textangle (Tk 9.1+), width
"""
Widget.__init__(self, master, 'label', cnf, kw)
@@ -3398,18 +3502,21 @@ class Listbox(Widget, XView, YView):
def __init__(self, master=None, cnf={}, **kw):
"""Construct a listbox widget with the parent MASTER.
Valid option names: background, bd, bg, borderwidth, cursor,
exportselection, fg, font, foreground, height, highlightbackground,
highlightcolor, highlightthickness, relief, selectbackground,
selectborderwidth, selectforeground, selectmode, setgrid, takefocus,
width, xscrollcommand, yscrollcommand, listvariable."""
Valid option names: activestyle, background, bd, bg, borderwidth,
cursor, disabledforeground, exportselection, fg, font,
foreground, height, highlightbackground, highlightcolor,
highlightthickness, inactiveselectbackground (Tk 9.1+),
inactiveselectforeground (Tk 9.1+), justify, listvariable,
relief, selectbackground, selectborderwidth, selectforeground,
selectmode, setgrid, state, takefocus, width, xscrollcommand,
yscrollcommand."""
Widget.__init__(self, master, 'listbox', cnf, kw)
def activate(self, index):
"""Activate item identified by INDEX."""
self.tk.call(self._w, 'activate', index)
def bbox(self, index):
def bbox(self, index): # overrides Misc.bbox
"""Return a tuple of X1,Y1,X2,Y2 coordinates for a rectangle
which encloses the item identified by the given index."""
return self._getints(self.tk.call(self._w, 'bbox', index)) or None
@@ -3465,7 +3572,7 @@ class Listbox(Widget, XView, YView):
select_anchor = selection_anchor
def selection_clear(self, first, last=None):
def selection_clear(self, first, last=None): # overrides Misc.selection_clear
"""Clear the selection from FIRST to LAST (included)."""
self.tk.call(self._w,
'selection', 'clear', first, last)
@@ -3486,7 +3593,7 @@ class Listbox(Widget, XView, YView):
select_set = selection_set
def size(self):
def size(self): # overrides Misc.size
"""Return the number of elements in the listbox."""
return self.tk.getint(self.tk.call(self._w, 'size'))
@@ -3512,7 +3619,8 @@ class Menu(Widget):
"""Construct menu widget with the parent MASTER.
Valid option names: activebackground, activeborderwidth,
activeforeground, background, bd, bg, borderwidth, cursor,
activeforeground, activerelief (Tk 9.0+), background, bd, bg,
borderwidth, cursor,
disabledforeground, fg, font, foreground, postcommand, relief,
selectcolor, takefocus, tearoff, tearoffcommand, title, type."""
Widget.__init__(self, master, 'menu', cnf, kw)
@@ -3642,6 +3750,14 @@ class Menubutton(Widget):
"""Menubutton widget, obsolete since Tk8.0."""
def __init__(self, master=None, cnf={}, **kw):
"""Construct a menubutton widget with the parent MASTER.
Valid option names: activebackground, activeforeground, anchor,
background, bd, bg, bitmap, borderwidth, compound, cursor,
direction, disabledforeground, fg, font, foreground, height,
highlightbackground, highlightcolor, highlightthickness,
image, indicatoron, justify, menu, padx, pady, relief, state,
takefocus, text, textvariable, underline, width, wraplength."""
Widget.__init__(self, master, 'menubutton', cnf, kw)
@@ -3649,6 +3765,12 @@ class Message(Widget):
"""Message widget to display multiline text. Obsolete since Label does it too."""
def __init__(self, master=None, cnf={}, **kw):
"""Construct a message widget with the parent MASTER.
Valid option names: anchor, aspect, background, bd, bg, borderwidth,
cursor, fg, font, foreground, highlightbackground,
highlightcolor, highlightthickness, justify, padx, pady,
relief, takefocus, text, textvariable, width."""
Widget.__init__(self, master, 'message', cnf, kw)
@@ -3659,12 +3781,13 @@ class Radiobutton(Widget):
"""Construct a radiobutton widget with the parent MASTER.
Valid option names: activebackground, activeforeground, anchor,
background, bd, bg, bitmap, borderwidth, command, cursor,
disabledforeground, fg, font, foreground, height,
highlightbackground, highlightcolor, highlightthickness, image,
indicatoron, justify, padx, pady, relief, selectcolor, selectimage,
state, takefocus, text, textvariable, underline, value, variable,
width, wraplength."""
background, bd, bg, bitmap, borderwidth, command, compound,
cursor, disabledforeground, fg, font, foreground, height,
highlightbackground, highlightcolor, highlightthickness,
image, indicatoron, justify, offrelief, overrelief, padx,
pady, relief, selectcolor, selectimage, state, takefocus,
text, textvariable, tristateimage, tristatevalue, underline,
value, variable, width, wraplength."""
Widget.__init__(self, master, 'radiobutton', cnf, kw)
def deselect(self):
@@ -3795,14 +3918,16 @@ class Text(Widget, XView, YView):
WIDGET-SPECIFIC OPTIONS
autoseparators, height, maxundo,
spacing1, spacing2, spacing3,
state, tabs, undo, width, wrap,
autoseparators, blockcursor, endline,
height, inactiveselectbackground,
insertunfocussed, locale (Tk 9.1+), maxundo,
spacing1, spacing2, spacing3, startline,
state, tabs, tabstyle, undo, width, wrap,
"""
Widget.__init__(self, master, 'text', cnf, kw)
def bbox(self, index):
def bbox(self, index): # overrides Misc.bbox
"""Return a tuple of (x,y,width,height) which gives the bounding
box of the visible part of the character at the given index."""
return self._getints(
@@ -3987,7 +4112,7 @@ class Text(Widget, XView, YView):
self._w, "image", "create", index,
*self._options(cnf, kw))
def image_names(self):
def image_names(self): # overrides Misc.image_names
"""Return all names of embedded images in this widget."""
return self.tk.call(self._w, "image", "names")
@@ -4059,8 +4184,9 @@ class Text(Widget, XView, YView):
self.tk.call(self._w, 'scan', 'dragto', x, y)
def search(self, pattern, index, stopindex=None,
forwards=None, backwards=None, exact=None,
regexp=None, nocase=None, count=None, elide=None):
forwards=None, backwards=None, exact=None,
regexp=None, nocase=None, count=None,
elide=None, *, nolinestop=None, strictlimits=None):
"""Search PATTERN beginning from INDEX until STOPINDEX.
Return the index of the first character of a match or an
empty string."""
@@ -4072,12 +4198,39 @@ class Text(Widget, XView, YView):
if nocase: args.append('-nocase')
if elide: args.append('-elide')
if count: args.append('-count'); args.append(count)
if nolinestop: args.append('-nolinestop')
if strictlimits: args.append('-strictlimits')
if pattern and pattern[0] == '-': args.append('--')
args.append(pattern)
args.append(index)
if stopindex: args.append(stopindex)
if stopindex is not None: args.append(stopindex)
return str(self.tk.call(tuple(args)))
def search_all(self, pattern, index, stopindex=None, *,
forwards=None, backwards=None, exact=None,
regexp=None, nocase=None, count=None,
elide=None, nolinestop=None, overlap=None,
strictlimits=None):
"""Search all occurrences of PATTERN from INDEX to STOPINDEX.
Return a tuple of indices where matches begin."""
args = [self._w, 'search', '-all']
if forwards: args.append('-forwards')
if backwards: args.append('-backwards')
if exact: args.append('-exact')
if regexp: args.append('-regexp')
if nocase: args.append('-nocase')
if elide: args.append('-elide')
if count: args.append('-count'); args.append(count)
if nolinestop: args.append('-nolinestop')
if overlap: args.append('-overlap')
if strictlimits: args.append('-strictlimits')
if pattern and pattern[0] == '-': args.append('--')
args.append(pattern)
args.append(index)
if stopindex is not None: args.append(stopindex)
result = self.tk.call(tuple(args))
return self.tk.splitlist(result)
def see(self, index):
"""Scroll such that the character at INDEX is visible."""
self.tk.call(self._w, 'see', index)
@@ -4291,6 +4444,8 @@ class Image:
def __getitem__(self, key):
return self.tk.call(self.name, 'configure', '-'+key)
__iter__ = None # prevent using __getitem__ for iteration
def configure(self, **kw):
"""Configure the image."""
res = ()
@@ -4625,14 +4780,17 @@ class Spinbox(Widget, XView):
buttondownrelief, buttonuprelief,
command, disabledbackground,
disabledforeground, format, from,
invalidcommand, increment,
invalidcommand, invcmd, increment,
locale (Tk 9.1+),
placeholder (Tk 9.0+),
placeholderforeground (Tk 9.0+),
readonlybackground, state, to,
validate, validatecommand values,
validate, validatecommand, vcmd, values,
width, wrap,
"""
Widget.__init__(self, master, 'spinbox', cnf, kw)
def bbox(self, index):
def bbox(self, index): # overrides Misc.bbox
"""Return a tuple of X1,Y1,X2,Y2 coordinates for a
rectangle which encloses the character given by index.
@@ -4741,7 +4899,7 @@ class Spinbox(Widget, XView):
"""
return self.selection("adjust", index)
def selection_clear(self):
def selection_clear(self): # overrides Misc.selection_clear
"""Clear the selection
If the selection isn't in this widget then the
@@ -4816,8 +4974,9 @@ class PanedWindow(Widget):
WIDGET-SPECIFIC OPTIONS
handlepad, handlesize, opaqueresize,
sashcursor, sashpad, sashrelief,
sashwidth, showhandle,
proxybackground, proxyborderwidth,
proxyrelief, sashcursor, sashpad,
sashrelief, sashwidth, showhandle,
"""
Widget.__init__(self, master, 'panedwindow', cnf, kw)
@@ -4838,7 +4997,7 @@ class PanedWindow(Widget):
"""
self.tk.call(self._w, 'forget', child)
forget = remove
forget = remove # overrides Pack.forget
def identify(self, x, y):
"""Identify the panedwindow component at point x, y
+2
View File
@@ -1,3 +1,5 @@
"""Interface to the native Tk color selection dialog."""
# tk common color chooser dialogue
#
# this module provides an interface to the native color dialogue
+2
View File
@@ -1,3 +1,5 @@
"""Base class for the Tk common dialogs."""
# base class for tk common dialogues
#
# this module provides a base class for accessing the common
+6 -1
View File
@@ -1,5 +1,7 @@
# dialog.py -- Tkinter interface to the tk_dialog script.
"""Classic Tk dialog box, wrapping the tk_dialog script."""
from tkinter import _cnfmerge, Widget, TclError, Button, Pack
__all__ = ["Dialog"]
@@ -8,6 +10,8 @@ DIALOG_ICON = 'questhead'
class Dialog(Widget):
"""A modal dialog box built from the classic (non-themed) Tk widgets."""
def __init__(self, master=None, cnf={}, **kw):
cnf = _cnfmerge((cnf, kw))
self.widgetName = '__dialog__'
@@ -21,7 +25,8 @@ class Dialog(Widget):
try: Widget.destroy(self)
except TclError: pass
def destroy(self): pass
def destroy(self):
"""Do nothing; the dialog window is already destroyed."""
def _test():
+23 -4
View File
@@ -1,3 +1,5 @@
"""Utilities to help work with fonts in Tkinter."""
# Tkinter font wrapper
#
# written by Fredrik Lundh, February 1998
@@ -6,7 +8,6 @@
import itertools
import tkinter
__version__ = "0.9"
__all__ = ["NORMAL", "ROMAN", "BOLD", "ITALIC",
"nametofont", "Font", "families", "names"]
@@ -72,8 +73,15 @@ class Font:
root = tkinter._get_default_root('use font')
tk = getattr(root, 'tk', root)
if font:
# get actual settings corresponding to the given font
font = tk.splitlist(tk.call("font", "actual", font))
# start from the settings of the given font
try:
# a named font: copy its options, preserving the size,
# which can be negative (specified in pixels)
font = tk.splitlist(tk.call("font", "configure", font))
except tkinter.TclError:
# a font description: resolve it ("font configure" only
# accepts a font name); this loses a size in pixels
font = tk.splitlist(tk.call("font", "actual", font))
else:
font = self._set(options)
if not name:
@@ -115,6 +123,8 @@ class Font:
def __setitem__(self, key, value):
self.configure(**{key: value})
__iter__ = None # prevent using __getitem__ for iteration
def __del__(self):
try:
if self.delete_font:
@@ -124,7 +134,7 @@ class Font:
def copy(self):
"Return a distinct copy of the current font"
return Font(self._tk, **self.actual())
return Font(self._tk, self.name)
def actual(self, option=None, displayof=None):
"Return actual font attributes"
@@ -198,6 +208,15 @@ def names(root=None):
return root.tk.splitlist(root.tk.call("font", "names"))
def __getattr__(name):
if name == "__version__":
from warnings import _deprecated
_deprecated("__version__", remove=(3, 20))
return "0.9" # Do not change
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# --------------------------------------------------------------------
# test stuff
+7 -5
View File
@@ -1,3 +1,5 @@
"""Interface to the standard Tk message boxes."""
# tk common message boxes
#
# this module provides an interface to the native message boxes
@@ -99,24 +101,24 @@ def showerror(title=None, message=None, **options):
def askquestion(title=None, message=None, **options):
"Ask a question"
"Ask a question; return the symbolic name of the selected button"
return _show(title, message, QUESTION, YESNO, **options)
def askokcancel(title=None, message=None, **options):
"Ask if operation should proceed; return true if the answer is ok"
"Ask if operation should proceed; return True if the answer is ok"
s = _show(title, message, QUESTION, OKCANCEL, **options)
return s == OK
def askyesno(title=None, message=None, **options):
"Ask a question; return true if the answer is yes"
"Ask a question; return True if the answer is yes"
s = _show(title, message, QUESTION, YESNO, **options)
return s == YES
def askyesnocancel(title=None, message=None, **options):
"Ask a question; return true if the answer is yes, None if cancelled."
"Ask a question; return True if the answer is yes, None if cancelled"
s = _show(title, message, QUESTION, YESNOCANCEL, **options)
# s might be a Tcl index object, so convert it to a string
s = str(s)
@@ -126,7 +128,7 @@ def askyesnocancel(title=None, message=None, **options):
def askretrycancel(title=None, message=None, **options):
"Ask if operation should be retried; return true if the answer is yes"
"Ask if operation should be retried; return True if the answer is retry"
s = _show(title, message, WARNING, RETRYCANCEL, **options)
return s == RETRY
+5 -2
View File
@@ -8,7 +8,7 @@
# fredrik@pythonware.com
# http://www.pythonware.com
#
"""This modules handles dialog boxes.
"""This module handles dialog boxes.
It contains the following public symbols:
@@ -23,9 +23,12 @@ askfloat -- get a float from the user
askstring -- get a string from the user
"""
from tkinter import *
from tkinter import Button, Entry, Frame, Label, Message, Tk, Toplevel
from tkinter import _get_temp_root, _destroy_temp_root
from tkinter import messagebox
from tkinter.constants import ACTIVE, BOTH, END, LEFT, RIDGE, W, E
__all__ = ["SimpleDialog", "Dialog", "askinteger", "askfloat", "askstring"]
class SimpleDialog:
+51 -31
View File
@@ -12,8 +12,6 @@ maintaining the widget state and invoking callbacks, all aspects
of the widgets appearance lies at Themes.
"""
__version__ = "0.3.1"
__author__ = "Guilherme Polo <ggpolo@gmail.com>"
__all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label",
@@ -581,8 +579,8 @@ class Button(Widget):
STANDARD OPTIONS
class, compound, cursor, image, state, style, takefocus,
text, textvariable, underline, width
class, compound, cursor, image, justify (Tk 9.0+), padding,
state, style, takefocus, text, textvariable, underline, width
WIDGET-SPECIFIC OPTIONS
@@ -604,8 +602,8 @@ class Checkbutton(Widget):
STANDARD OPTIONS
class, compound, cursor, image, state, style, takefocus,
text, textvariable, underline, width
class, compound, cursor, image, justify (Tk 9.0+), padding,
state, style, takefocus, text, textvariable, underline, width
WIDGET-SPECIFIC OPTIONS
@@ -638,8 +636,10 @@ class Entry(Widget, tkinter.Entry):
WIDGET-SPECIFIC OPTIONS
exportselection, invalidcommand, justify, show, state,
textvariable, validate, validatecommand, width
background, exportselection, font, foreground, invalidcommand,
justify, locale (Tk 9.1+), placeholder (Tk 9.0+),
placeholderforeground (Tk 9.0+), show, state, textvariable,
validate, validatecommand, width
VALIDATION MODES
@@ -648,7 +648,7 @@ class Entry(Widget, tkinter.Entry):
Widget.__init__(self, master, widget or "ttk::entry", kw)
def bbox(self, index):
def bbox(self, index): # overrides Misc.bbox
"""Return a tuple of (x, y, width, height) which describes the
bounding box of the character given by index."""
return self._getints(self.tk.call(self._w, "bbox", index))
@@ -676,12 +676,14 @@ class Combobox(Entry):
STANDARD OPTIONS
class, cursor, style, takefocus
class, cursor, style, takefocus, xscrollcommand
WIDGET-SPECIFIC OPTIONS
exportselection, justify, height, postcommand, state,
textvariable, values, width
background, exportselection, font, foreground, height,
invalidcommand, justify, locale (Tk 9.1+), placeholder (Tk 9.0+),
placeholderforeground (Tk 9.0+), postcommand, show, state,
textvariable, validate, validatecommand, values, width
"""
Entry.__init__(self, master, "ttk::combobox", **kw)
@@ -730,13 +732,13 @@ class Label(Widget):
STANDARD OPTIONS
class, compound, cursor, image, style, takefocus, text,
textvariable, underline, width
class, compound, cursor, image, state, style, takefocus,
text, textvariable, underline, width
WIDGET-SPECIFIC OPTIONS
anchor, background, font, foreground, justify, padding,
relief, text, wraplength
anchor, background, borderwidth, font, foreground, justify,
padding, relief, text, textangle (Tk 9.1+), wraplength
"""
Widget.__init__(self, master, "ttk::label", kw)
@@ -754,8 +756,9 @@ class Labelframe(Widget):
class, cursor, style, takefocus
WIDGET-SPECIFIC OPTIONS
labelanchor, text, underline, padding, labelwidget, width,
height
borderwidth, height, labelanchor, labelwidget, padding,
relief, text, underline, width
"""
Widget.__init__(self, master, "ttk::labelframe", kw)
@@ -771,8 +774,8 @@ class Menubutton(Widget):
STANDARD OPTIONS
class, compound, cursor, image, state, style, takefocus,
text, textvariable, underline, width
class, compound, cursor, image, justify (Tk 9.0+), padding,
state, style, takefocus, text, textvariable, underline, width
WIDGET-SPECIFIC OPTIONS
@@ -826,7 +829,7 @@ class Notebook(Widget):
self.tk.call(self._w, "add", child, *(_format_optdict(kw)))
def forget(self, tab_id):
def forget(self, tab_id): # overrides Pack.forget
"""Removes the tab specified by tab_id, unmaps and unmanages the
associated window."""
self.tk.call(self._w, "forget", tab_id)
@@ -985,7 +988,9 @@ class Progressbar(Widget):
STANDARD OPTIONS
class, cursor, style, takefocus
anchor (Tk 9.0+), class, cursor, font (Tk 9.0+),
foreground (Tk 9.0+), justify (Tk 9.0+), style, takefocus,
text (Tk 9.0+), wraplength (Tk 9.0+)
WIDGET-SPECIFIC OPTIONS
@@ -1024,8 +1029,8 @@ class Radiobutton(Widget):
STANDARD OPTIONS
class, compound, cursor, image, state, style, takefocus,
text, textvariable, underline, width
class, compound, cursor, image, justify (Tk 9.0+), padding,
state, style, takefocus, text, textvariable, underline, width
WIDGET-SPECIFIC OPTIONS
@@ -1052,7 +1057,7 @@ class Scale(Widget, tkinter.Scale):
STANDARD OPTIONS
class, cursor, style, takefocus
class, cursor, state, style, takefocus
WIDGET-SPECIFIC OPTIONS
@@ -1149,7 +1154,10 @@ class Spinbox(Entry):
WIDGET-SPECIFIC OPTIONS
to, from_, increment, values, wrap, format, command
background, command, exportselection, font, foreground,
format, from_, increment, justify, locale (Tk 9.1+),
placeholder (Tk 9.0+), placeholderforeground (Tk 9.0+), show,
state, textvariable, to, values, width, wrap
"""
Entry.__init__(self, master, "ttk::spinbox", **kw)
@@ -1176,7 +1184,10 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
WIDGET-SPECIFIC OPTIONS
columns, displaycolumns, height, padding, selectmode, show
columns, displaycolumns, headingheight (Tk 9.1+), height,
padding, rowheight (Tk 9.1+), selectmode, selecttype (Tk 9.0+),
show, striped (Tk 9.0+), titlecolumns (Tk 9.0+),
titleitems (Tk 9.0+)
ITEM OPTIONS
@@ -1189,7 +1200,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
Widget.__init__(self, master, "ttk::treeview", kw)
def bbox(self, item, column=None):
def bbox(self, item, column=None): # overrides Misc.bbox
"""Returns the bounding box (relative to the treeview widget's
window) of the specified item in the form x y width height.
@@ -1248,7 +1259,7 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
return self.tk.getboolean(self.tk.call(self._w, "exists", item))
def focus(self, item=None):
def focus(self, item=None): # overrides Misc.focus
"""If item is specified, sets the focus item to item. Otherwise,
returns the current focus item, or '' if there is none."""
return self.tk.call(self._w, "focus", item)
@@ -1472,8 +1483,8 @@ class Treeview(Widget, tkinter.XView, tkinter.YView):
def tag_has(self, tagname, item=None):
"""If item is specified, returns 1 or 0 depending on whether the
specified item has the given tagname. Otherwise, returns a list of
"""If item is specified, returns True if the specified item has the
given tagname, False otherwise. Otherwise, returns a list of
all items which have the specified tag.
* Availability: Tk 8.6"""
@@ -1648,3 +1659,12 @@ class OptionMenu(Menubutton):
except AttributeError:
pass
super().destroy()
def __getattr__(name):
if name == "__version__":
from warnings import _deprecated
_deprecated("__version__", remove=(3, 20))
return "0.3.1" # Do not change
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+341 -389
View File
File diff suppressed because it is too large. Load diff