Skip to content

Commit 8abc1cf

Browse files
Merge branch 'main' into tkinter-text-sync
2 parents cfde17e + aa5b164 commit 8abc1cf

10 files changed

Lines changed: 228 additions & 0 deletions

File tree

Doc/library/tkinter.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5475,6 +5475,20 @@ Widget classes
54755475
inserted or deleted.
54765476
Otherwise set the flag to the boolean *arg*.
54775477

5478+
.. method:: edit_canundo()
5479+
5480+
Return ``True`` if there is an edit action on the undo stack that can be
5481+
undone, and ``False`` otherwise.
5482+
5483+
.. versionadded:: next
5484+
5485+
.. method:: edit_canredo()
5486+
5487+
Return ``True`` if there is an edit action on the redo stack that can be
5488+
reapplied, and ``False`` otherwise.
5489+
5490+
.. versionadded:: next
5491+
54785492
.. method:: edit_undo()
54795493

54805494
Undo the most recent edit action, that is, all the inserts and deletes

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ shlex
147147
tkinter
148148
-------
149149

150+
* Added new :class:`!tkinter.Text` methods :meth:`~tkinter.Text.edit_canundo`
151+
and :meth:`~tkinter.Text.edit_canredo` which return whether an undo or redo
152+
is possible.
153+
(Contributed by Serhiy Storchaka in :gh:`151674`.)
154+
150155
* Added new :class:`!tkinter.Text` methods :meth:`~tkinter.Text.sync` and
151156
:meth:`~tkinter.Text.pendingsync` which control and report the
152157
synchronization of the displayed view with the underlying text.

Lib/test/test_tkinter/test_font.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def setUpClass(cls):
2121
cls.font = font.Font(root=cls.root, name=fontname, exists=False)
2222

2323
def test_configure(self):
24+
self.assertEqual(self.font.config, self.font.configure)
2425
options = self.font.configure()
2526
self.assertGreaterEqual(set(options),
2627
{'family', 'size', 'weight', 'slant', 'underline', 'overstrike'})
@@ -36,6 +37,26 @@ def test_configure(self):
3637
self.assertIsInstance(options[key], sizetype)
3738
self.assertIsInstance(self.font.cget(key), sizetype)
3839
self.assertIsInstance(self.font[key], sizetype)
40+
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
41+
self.font.cget, 'spam')
42+
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
43+
self.font.configure, spam='x')
44+
self.assertRaises(TypeError, self.font.cget)
45+
self.assertRaises(TypeError, self.font.cget, 'size', 'weight')
46+
47+
def test_copy(self):
48+
f = font.Font(root=self.root, family='Times', size=10, weight='bold')
49+
copied = f.copy()
50+
self.assertIsInstance(copied, font.Font)
51+
self.assertIsNot(copied, f)
52+
self.assertNotEqual(copied.name, f.name)
53+
self.assertEqual(copied.actual(), f.actual())
54+
# The copy is independent of the original.
55+
sizetype = int if self.wantobjects else str
56+
copied.configure(size=20)
57+
self.assertEqual(f.cget('size'), sizetype(10))
58+
self.assertEqual(copied.cget('size'), sizetype(20))
59+
self.assertRaises(TypeError, f.copy, 'x')
3960

4061
def test_unicode_family(self):
4162
family = 'MS \u30b4\u30b7\u30c3\u30af'
@@ -60,6 +81,9 @@ def test_actual(self):
6081
for key in 'size', 'underline', 'overstrike':
6182
self.assertIsInstance(options[key], sizetype)
6283
self.assertIsInstance(self.font.actual(key), sizetype)
84+
self.assertRaisesRegex(tkinter.TclError, 'bad option "-spam"',
85+
self.font.actual, 'spam')
86+
self.assertRaises(TypeError, self.font.actual, 'size', 'weight', 'slant')
6387

6488
def test_name(self):
6589
self.assertEqual(self.font.name, fontname)
@@ -83,15 +107,24 @@ def test_equality(self):
83107

84108
def test_measure(self):
85109
self.assertIsInstance(self.font.measure('abc'), int)
110+
self.assertEqual(self.font.measure(''), 0)
111+
self.assertIsInstance(
112+
self.font.measure('abc', displayof=self.root), int)
113+
self.assertRaises(TypeError, self.font.measure)
114+
self.assertRaises(TypeError, self.font.measure, 'a', 'b', 'c')
86115

87116
def test_metrics(self):
88117
metrics = self.font.metrics()
89118
self.assertGreaterEqual(set(metrics),
90119
{'ascent', 'descent', 'linespace', 'fixed'})
91120
for key in metrics:
92121
self.assertEqual(self.font.metrics(key), metrics[key])
122+
self.assertEqual(self.font.metrics(key, displayof=self.root),
123+
metrics[key])
93124
self.assertIsInstance(metrics[key], int)
94125
self.assertIsInstance(self.font.metrics(key), int)
126+
self.assertRaisesRegex(tkinter.TclError, 'bad metric "-spam"',
127+
self.font.metrics, 'spam')
95128

96129
def test_families(self):
97130
families = font.families(self.root)

Lib/test/test_tkinter/test_images.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ def test_configure_width_height(self):
288288
image.configure(height=10)
289289
self.assertEqual(image['width'], '20')
290290
self.assertEqual(image['height'], '10')
291+
self.assertEqual(image.cget('width'), image['width'])
292+
self.assertEqual(image.cget('height'), image['height'])
293+
self.assertRaises(TypeError, image.cget)
294+
self.assertRaises(TypeError, image.cget, 'width', 'height')
295+
self.assertEqual(image.config, image.configure)
291296
self.assertEqual(image.width(), 20)
292297
self.assertEqual(image.height(), 10)
293298

@@ -656,6 +661,14 @@ def test_transparency(self):
656661
self.assertEqual(image.transparency_get(4, 6), True)
657662
image.transparency_set(4, 6, False)
658663
self.assertEqual(image.transparency_get(4, 6), False)
664+
self.assertRaises(tkinter.TclError, image.transparency_get, -1, 0)
665+
self.assertRaises(tkinter.TclError, image.transparency_get, 16, 0)
666+
self.assertRaises(tkinter.TclError, image.transparency_set, -1, 0, True)
667+
self.assertRaises(tkinter.TclError, image.transparency_set, 16, 0, True)
668+
self.assertRaises(TypeError, image.transparency_get, 0)
669+
self.assertRaises(TypeError, image.transparency_get, 0, 0, 0)
670+
self.assertRaises(TypeError, image.transparency_set, 0, 0)
671+
self.assertRaises(TypeError, image.transparency_set, 0, 0, True, 0)
659672

660673

661674
if __name__ == "__main__":

Lib/test/test_tkinter/test_misc.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from tkinter import TclError
88
import enum
99
from test import support
10+
from test.support import os_helper
1011
from test.test_tkinter.support import setUpModule # noqa: F401
1112
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1213
requires_tk, get_tk_patchlevel)
@@ -357,6 +358,19 @@ def test_option(self):
357358
self.root.option_clear()
358359
self.assertEqual(b.option_get('background', 'Background'), '')
359360

361+
def test_option_readfile(self):
362+
self.addCleanup(self.root.option_clear)
363+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
364+
with open(os_helper.TESTFN, 'w') as f:
365+
f.write('*Button.background: red\n')
366+
self.root.option_readfile(os_helper.TESTFN)
367+
b = tkinter.Button(self.root)
368+
self.assertEqual(b.option_get('background', 'Background'), 'red')
369+
self.assertRaises(TclError, self.root.option_readfile,
370+
os_helper.TESTFN + '.nonexistent')
371+
self.assertRaises(TypeError, self.root.option_readfile)
372+
self.assertRaises(TypeError, self.root.option_readfile, 'a', 'b', 'c')
373+
360374
def test_nametowidget(self):
361375
b = tkinter.Button(self.root, name='btn')
362376
self.assertIs(self.root.nametowidget('btn'), b)
@@ -417,6 +431,38 @@ def test_bell(self):
417431
self.root.bell() # No exception.
418432
self.root.bell(displayof=self.root)
419433

434+
def test_tk_focusNext_focusPrev(self):
435+
f = tkinter.Frame(self.root)
436+
f.pack()
437+
entries = [tkinter.Entry(f) for _ in range(3)]
438+
for entry in entries:
439+
entry.pack()
440+
# tk_focusNext skips widgets that are not viewable.
441+
entries[-1].wait_visibility()
442+
self.assertIs(entries[0].tk_focusNext(), entries[1])
443+
self.assertIs(entries[1].tk_focusNext(), entries[2])
444+
self.assertIs(entries[2].tk_focusPrev(), entries[1])
445+
self.assertIs(entries[1].tk_focusPrev(), entries[0])
446+
self.assertRaises(TypeError, entries[0].tk_focusNext, 'x')
447+
self.assertRaises(TypeError, entries[0].tk_focusPrev, 'x')
448+
449+
def test_tk_strictMotif(self):
450+
self.addCleanup(self.root.tk_strictMotif, False)
451+
self.assertIs(self.root.tk_strictMotif(), False)
452+
self.assertIs(self.root.tk_strictMotif(True), True)
453+
self.assertIs(self.root.tk_strictMotif(), True)
454+
self.assertIs(self.root.tk_strictMotif(False), False)
455+
self.assertRaises(TypeError, self.root.tk_strictMotif, 1, 2)
456+
457+
def test_tk_bisque(self):
458+
# tk_bisque resets the color palette; use a separate root so that
459+
# the shared one is not affected.
460+
root = tkinter.Tk()
461+
self.addCleanup(root.destroy)
462+
root.tk_bisque()
463+
self.assertEqual(root['background'], '#ffe4c4')
464+
self.assertRaises(TypeError, root.tk_bisque, 'x')
465+
420466
def test_event_repr_defaults(self):
421467
e = tkinter.Event()
422468
e.serial = 12345
@@ -819,6 +865,13 @@ def test_wm_iconbitmap(self):
819865

820866
t.destroy()
821867

868+
def test_wm_iconphoto(self):
869+
t = tkinter.Toplevel(self.root)
870+
img = tkinter.PhotoImage(master=t, width=16, height=16)
871+
t.wm_iconphoto(False, img) # No exception.
872+
t.wm_iconphoto(True, img)
873+
self.assertRaises(tkinter.TclError, t.wm_iconphoto, False, 'spam')
874+
822875
def test_wm_title(self):
823876
t = tkinter.Toplevel(self.root)
824877
t.title('Hello')
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import unittest
2+
import tkinter
3+
from tkinter.scrolledtext import ScrolledText
4+
from test.support import requires
5+
from test.test_tkinter.support import setUpModule # noqa: F401
6+
from test.test_tkinter.support import AbstractTkTest
7+
8+
requires('gui')
9+
10+
11+
class ScrolledTextTest(AbstractTkTest, unittest.TestCase):
12+
13+
def create(self, **kwargs):
14+
st = ScrolledText(self.root, **kwargs)
15+
self.addCleanup(st.destroy)
16+
return st
17+
18+
def test_create(self):
19+
st = self.create(background='red', height=5)
20+
# It is a Text widget held in a Frame together with a Scrollbar.
21+
self.assertIsInstance(st, tkinter.Text)
22+
self.assertIsInstance(st.frame, tkinter.Frame)
23+
self.assertIsInstance(st.vbar, tkinter.Scrollbar)
24+
self.assertEqual(st.winfo_parent(), str(st.frame))
25+
# str() returns the frame, so that geometry managers manage it.
26+
self.assertEqual(str(st), str(st.frame))
27+
# Keyword options configure the Text.
28+
self.assertEqual(str(st['background']), 'red')
29+
self.assertEqual(st['height'], 5 if self.wantobjects else '5')
30+
31+
def test_text_methods(self):
32+
st = self.create()
33+
st.insert('1.0', 'hello\nworld')
34+
self.assertEqual(st.get('1.0', 'end-1c'), 'hello\nworld')
35+
self.assertEqual(st.index('end-1c'), '2.5')
36+
st.delete('1.0', 'end')
37+
self.assertEqual(st.get('1.0', 'end-1c'), '')
38+
39+
def test_geometry_methods(self):
40+
st = self.create()
41+
# configure is not redirected; it configures the Text.
42+
st.configure(height=8)
43+
self.assertEqual(st['height'], 8 if self.wantobjects else '8')
44+
# Pack, Grid and Place methods are redirected to the frame.
45+
st.pack()
46+
self.root.update()
47+
self.assertEqual(st.frame.winfo_manager(), 'pack')
48+
self.assertEqual(st.pack_info(), st.frame.pack_info())
49+
st.pack_forget()
50+
self.assertEqual(st.frame.winfo_manager(), '')
51+
52+
def test_scrollbar(self):
53+
st = self.create(height=5)
54+
st.pack()
55+
st.insert('1.0', '\n'.join(map(str, range(100))))
56+
self.root.update()
57+
# The scrollbar tracks the text view.
58+
self.assertEqual(st.vbar.get(), st.yview())
59+
st.yview_moveto(1.0)
60+
self.root.update()
61+
self.assertEqual(st.vbar.get()[1], 1.0)
62+
63+
64+
if __name__ == "__main__":
65+
unittest.main()

Lib/test/test_tkinter/test_text.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,29 @@ def test_edit_undo_redo(self):
318318
text.edit_reset()
319319
self.assertRaises(TclError, text.edit_undo)
320320

321+
def test_edit_canundo_canredo(self):
322+
text = self.text
323+
text.configure(undo=True)
324+
325+
self.assertIs(text.edit_canundo(), False)
326+
self.assertIs(text.edit_canredo(), False)
327+
328+
text.insert('1.0', 'spam')
329+
self.assertIs(text.edit_canundo(), True)
330+
self.assertIs(text.edit_canredo(), False)
331+
332+
text.edit_undo()
333+
self.assertIs(text.edit_canundo(), False)
334+
self.assertIs(text.edit_canredo(), True)
335+
336+
text.edit_redo()
337+
self.assertIs(text.edit_canundo(), True)
338+
self.assertIs(text.edit_canredo(), False)
339+
340+
text.edit_reset()
341+
self.assertIs(text.edit_canundo(), False)
342+
self.assertIs(text.edit_canredo(), False)
343+
321344
def test_dump(self):
322345
text = self.text
323346
text.insert('1.0', 'hello')

Lib/test/test_tkinter/test_variables.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ def test_initialize(self):
111111
self.assertFalse(v.side_effect)
112112
v.set("value")
113113
self.assertTrue(v.side_effect)
114+
self.assertEqual(Variable.initialize, Variable.set)
114115

115116
def test_trace_old(self):
116117
if tcl_version >= (9, 0):
117118
self.skipTest('requires Tcl version < 9.0')
118119
# Old interface
119120
v = Variable(self.root)
120121
vname = str(v)
122+
self.assertEqual(v.trace, v.trace_variable)
121123
trace = []
122124
def read_tracer(*args):
123125
trace.append(('read',) + args)
@@ -328,6 +330,7 @@ def test_set(self):
328330
self.assertEqual(self.root.globalgetvar("name"), false)
329331
v.set("on")
330332
self.assertEqual(self.root.globalgetvar("name"), true)
333+
self.assertEqual(BooleanVar.initialize, BooleanVar.set)
331334

332335
def test_invalid_value_domain(self):
333336
false = 0 if self.root.wantobjects() else "0"

Lib/tkinter/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,6 +3970,22 @@ def edit(self, *args):
39703970
"""
39713971
return self.tk.call(self._w, 'edit', *args)
39723972

3973+
def edit_canredo(self):
3974+
"""Return whether redo is possible.
3975+
3976+
Return True if redo is possible, i.e. when the redo stack is
3977+
not empty, and False otherwise.
3978+
"""
3979+
return self.tk.getboolean(self.edit("canredo"))
3980+
3981+
def edit_canundo(self):
3982+
"""Return whether undo is possible.
3983+
3984+
Return True if undo is possible, i.e. when the undo stack is
3985+
not empty, and False otherwise.
3986+
"""
3987+
return self.tk.getboolean(self.edit("canundo"))
3988+
39733989
def edit_modified(self, arg=None):
39743990
"""Get or Set the modified flag
39753991
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add the :meth:`~tkinter.Text.edit_canundo` and :meth:`~tkinter.Text.edit_canredo`
2+
methods of :class:`!tkinter.Text`, wrapping the Tk ``edit canundo`` and
3+
``edit canredo`` subcommands.

0 commit comments

Comments
 (0)