Skip to content

Commit 6c4f6c3

Browse files
Merge branch 'main' into curses-textpad-corner
2 parents 916c613 + 1812162 commit 6c4f6c3

12 files changed

Lines changed: 386 additions & 18 deletions

File tree

Doc/library/curses.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,37 @@ The module :mod:`!curses` defines the following functions:
345345
a key with that value.
346346

347347

348+
.. function:: define_key(definition, keycode)
349+
350+
Define an escape sequence *definition*, a string, as a key that generates
351+
the key code *keycode*, so that :mod:`curses` interprets it like one of the
352+
keys predefined in the terminal database.
353+
354+
If *definition* is ``None``, any existing binding for *keycode* is removed.
355+
If *keycode* is zero or negative, any existing binding for *definition* is
356+
removed.
357+
358+
.. versionadded:: next
359+
360+
361+
.. function:: key_defined(definition)
362+
363+
Return the key code bound to the escape sequence *definition*, a string,
364+
``0`` if no key code is bound to it, or ``-1`` if *definition* is a prefix
365+
of a longer bound sequence (and so is ambiguous).
366+
367+
.. versionadded:: next
368+
369+
370+
.. function:: keyok(keycode, enable)
371+
372+
Enable (if *enable* is true) or disable (otherwise) interpretation of the
373+
key code *keycode*. Unlike :meth:`window.keypad`, this affects a single
374+
key code rather than all of them.
375+
376+
.. versionadded:: next
377+
378+
348379
.. function:: halfdelay(tenths)
349380

350381
Used for half-delay mode, which is similar to cbreak mode in that characters

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ curses
165165
:func:`~curses.scr_set`, which dump the whole screen to a file and restore it.
166166
(Contributed by Serhiy Storchaka in :gh:`152260`.)
167167

168+
* Add the :mod:`curses` key-management functions :func:`~curses.define_key`,
169+
:func:`~curses.key_defined` and :func:`~curses.keyok`, available when built
170+
against an ncurses with ``NCURSES_EXT_FUNCS``.
171+
(Contributed by Serhiy Storchaka in :gh:`152334`.)
172+
168173
gzip
169174
----
170175

Lib/_collections_abc.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ def _f(): pass
6767
# are not included on this list.
6868
bytes_iterator = type(iter(b''))
6969
bytearray_iterator = type(iter(bytearray()))
70-
#callable_iterator = ???
7170
dict_keyiterator = type(iter({}.keys()))
7271
dict_valueiterator = type(iter({}.values()))
7372
dict_itemiterator = type(iter({}.items()))
@@ -319,7 +318,6 @@ def __subclasshook__(cls, C):
319318

320319
Iterator.register(bytes_iterator)
321320
Iterator.register(bytearray_iterator)
322-
#Iterator.register(callable_iterator)
323321
Iterator.register(dict_keyiterator)
324322
Iterator.register(dict_valueiterator)
325323
Iterator.register(dict_itemiterator)

Lib/idlelib/idle_test/htest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,9 @@
216216
'file': 'browser',
217217
'kwds': {},
218218
'msg': textwrap.dedent("""
219-
"Inspect names of module, class(with superclass if applicable),
220-
"methods and functions. Toggle nested items. Double clicking
221-
"on items prints a traceback for an exception that is ignored.""")
219+
Inspect names of module, class(with superclass if applicable),
220+
methods and functions. Toggle nested items. Double clicking
221+
on items prints a traceback for an exception that is ignored.""")
222222
}
223223

224224
_multistatus_bar_spec = {

Lib/test/test_curses.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,21 +1129,27 @@ def test_scr_dump(self):
11291129
with tempfile.TemporaryDirectory() as d:
11301130
dump = os.path.join(d, 'dump')
11311131
self.assertIsNone(curses.scr_dump(dump))
1132-
# Dumping the same screen again is deterministic.
1132+
with open(dump, 'rb') as f:
1133+
image = f.read()
1134+
self.assertTrue(image)
1135+
# The dump format embeds raw pointers on some platforms (such as
1136+
# macOS), so two dumps of the same screen are not always identical.
1137+
# Only compare dump files when the format proves deterministic.
11331138
dump2 = os.path.join(d, 'dump2')
11341139
curses.scr_dump(dump2)
1135-
with open(dump, 'rb') as f1, open(dump2, 'rb') as f2:
1136-
self.assertEqual(f1.read(), f2.read())
1140+
with open(dump2, 'rb') as f:
1141+
deterministic = f.read() == image
11371142
# scr_restore() reloads that virtual screen, so dumping it again
11381143
# reproduces the original file even after the screen has changed.
11391144
stdscr.erase()
11401145
stdscr.addstr(0, 0, 'something else')
11411146
stdscr.refresh()
11421147
self.assertIsNone(curses.scr_restore(dump))
1143-
restored = os.path.join(d, 'restored')
1144-
curses.scr_dump(restored)
1145-
with open(dump, 'rb') as f1, open(restored, 'rb') as f2:
1146-
self.assertEqual(f1.read(), f2.read())
1148+
if deterministic:
1149+
restored = os.path.join(d, 'restored')
1150+
curses.scr_dump(restored)
1151+
with open(restored, 'rb') as f:
1152+
self.assertEqual(f.read(), image)
11471153
# scr_init() and scr_set() accept a dump file and return None.
11481154
self.assertIsNone(curses.scr_init(dump))
11491155
self.assertIsNone(curses.scr_set(dump))
@@ -1315,6 +1321,21 @@ def test_env_queries(self):
13151321
self.assertIsInstance(c, str)
13161322
self.assertEqual(len(c), 1)
13171323

1324+
@requires_curses_func('define_key')
1325+
def test_key_management(self):
1326+
# Bind a custom escape sequence to a free key code and read it back.
1327+
seq = '\x1bspam'
1328+
keycode = 0o600
1329+
curses.define_key(seq, keycode)
1330+
self.assertEqual(curses.key_defined(seq), keycode)
1331+
# keyok enables or disables interpretation of a single key code.
1332+
# Use the key code just defined, which is guaranteed to be known.
1333+
self.assertIsNone(curses.keyok(keycode, False))
1334+
self.assertIsNone(curses.keyok(keycode, True))
1335+
# Passing None removes the binding for the key code.
1336+
curses.define_key(None, keycode)
1337+
self.assertEqual(curses.key_defined(seq), 0)
1338+
13181339
def test_output_options(self):
13191340
stdscr = self.stdscr
13201341

Lib/test/test_tkinter/test_misc.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,32 @@ def test_nametowidget(self):
390390
self.assertIs(self.root.nametowidget(str(b)), b)
391391
self.assertRaises(KeyError, self.root.nametowidget, '.nonexistent')
392392

393+
def test_nametowidget_menu_clone(self):
394+
# A menu used as a menubar or cascade is cloned by Tk under an
395+
# auto-generated name (each path component is the original name
396+
# prefixed with one or more '#' clone markers). nametowidget()
397+
# maps such a name back to the original widget (gh-38464).
398+
menubar = tkinter.Menu(self.root)
399+
filemenu = tkinter.Menu(menubar, tearoff=0)
400+
menubar.add_cascade(label='File', menu=filemenu)
401+
submenu = tkinter.Menu(filemenu, tearoff=0)
402+
filemenu.add_cascade(label='More', menu=submenu)
403+
self.root['menu'] = menubar
404+
self.root.update_idletasks()
405+
406+
originals = {menubar, filemenu, submenu}
407+
clones = []
408+
def collect(parent):
409+
for name in self.root.tk.splitlist(
410+
self.root.tk.call('winfo', 'children', parent)):
411+
clones.append(name)
412+
collect(name)
413+
collect('.')
414+
# Every menu (originals and clones) resolves to an original widget.
415+
self.assertTrue(any('#' in name for name in clones))
416+
for name in clones:
417+
self.assertIn(self.root.nametowidget(name), originals)
418+
393419
def test_focus_methods(self):
394420
f = tkinter.Frame(self.root, width=150, height=100)
395421
f.pack()
@@ -407,6 +433,22 @@ def test_focus_methods(self):
407433
self.root.update()
408434
self.assertIs(self.root.focus_get(), b)
409435

436+
def test_focus_methods_unresolvable(self):
437+
# The focus may be on a widget that tkinter did not create and so
438+
# cannot map to an instance (e.g. a torn-off menu). The focus
439+
# methods return None instead of raising KeyError (gh-88758).
440+
menu = tkinter.Menu(self.root, tearoff=1)
441+
menu.add_command(label='Hello')
442+
tearoff = self.root.tk.call('tk::TearOffMenu', str(menu), 0, 0)
443+
self.addCleanup(self.root.tk.call, 'destroy', tearoff)
444+
self.root.update()
445+
self.assertRaises(KeyError, self.root.nametowidget, tearoff)
446+
447+
self.root.tk.call('focus', '-force', tearoff)
448+
self.root.update()
449+
self.assertIsNone(self.root.focus_get())
450+
self.assertIsNone(self.root.focus_displayof())
451+
410452
def test_grab(self):
411453
f = tkinter.Frame(self.root)
412454
f.pack()

Lib/tkinter/__init__.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -894,7 +894,10 @@ def focus_get(self):
894894
the focus."""
895895
name = self.tk.call('focus')
896896
if name == 'none' or not name: return None
897-
return self._nametowidget(name)
897+
try:
898+
return self._nametowidget(name)
899+
except KeyError:
900+
return None
898901

899902
def focus_displayof(self):
900903
"""Return the widget which has currently the focus on the
@@ -903,14 +906,20 @@ def focus_displayof(self):
903906
Return None if the application does not have the focus."""
904907
name = self.tk.call('focus', '-displayof', self._w)
905908
if name == 'none' or not name: return None
906-
return self._nametowidget(name)
909+
try:
910+
return self._nametowidget(name)
911+
except KeyError:
912+
return None
907913

908914
def focus_lastfor(self):
909915
"""Return the widget which would have the focus if top level
910916
for this widget gets the focus from the window manager."""
911917
name = self.tk.call('focus', '-lastfor', self._w)
912918
if name == 'none' or not name: return None
913-
return self._nametowidget(name)
919+
try:
920+
return self._nametowidget(name)
921+
except KeyError:
922+
return None
914923

915924
def tk_focusFollowsMouse(self):
916925
"""The widget under mouse will get automatically focus. Can not
@@ -1313,7 +1322,10 @@ def winfo_containing(self, rootX, rootY, displayof=0):
13131322
+ self._displayof(displayof) + (rootX, rootY)
13141323
name = self.tk.call(args)
13151324
if not name: return None
1316-
return self._nametowidget(name)
1325+
try:
1326+
return self._nametowidget(name)
1327+
except KeyError:
1328+
return None
13171329

13181330
def winfo_depth(self):
13191331
"""Return the number of bits per pixel."""
@@ -1788,7 +1800,16 @@ def nametowidget(self, name):
17881800
for n in name:
17891801
if not n:
17901802
break
1791-
w = w.children[n]
1803+
try:
1804+
w = w.children[n]
1805+
except KeyError:
1806+
# Menu clones (a menu used as a menubar or a cascade) get
1807+
# auto-generated names where each path component is the
1808+
# original name prefixed with one or more '#' clone markers.
1809+
# Map such a name back to the original widget.
1810+
if not n.startswith('#'):
1811+
raise
1812+
w = w.children[n.rsplit('#', 1)[-1]]
17921813

17931814
return w
17941815

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`!tkinter.Misc.nametowidget` now resolves the auto-generated names of
2+
cloned menus (a menu used as a menubar or a cascade) back to the original
3+
widget.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:meth:`!tkinter.Misc.focus_get`, :meth:`!focus_displayof`,
2+
:meth:`!focus_lastfor` and :meth:`!winfo_containing` now return ``None``
3+
instead of raising :exc:`KeyError` when the widget was not created by
4+
:mod:`tkinter` (for example a torn-off menu).
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add the :func:`curses.define_key`, :func:`curses.key_defined` and
2+
:func:`curses.keyok` key-management functions.

0 commit comments

Comments
 (0)