https://github.com/python/cpython/commit/389e00f13fb5b20b424980b08678753441db7529
commit: 389e00f13fb5b20b424980b08678753441db7529
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-27T07:09:07Z
summary:

gh-151678: Add more tests for tkinter.dnd (GH-152362)

Cover the drag cursor, the Motion and ButtonRelease bindings, switching
between targets, the target search up the master chain, dnd_accept()
returning None, and restarting after a drag has finished.

Co-authored-by: Claude Opus 4.8 <[email protected]>

files:
M Lib/test/test_tkinter/test_dnd.py

diff --git a/Lib/test/test_tkinter/test_dnd.py 
b/Lib/test/test_tkinter/test_dnd.py
index 501b0d7f78586c..fe3685aefe48be 100644
--- a/Lib/test/test_tkinter/test_dnd.py
+++ b/Lib/test/test_tkinter/test_dnd.py
@@ -60,6 +60,15 @@ def setUp(self):
         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)
@@ -93,6 +102,70 @@ def test_no_recursive_start(self):
     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()

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to