-
-
Notifications
You must be signed in to change notification settings - Fork 31k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-75666: Tkinter: "unbind(sequence, funcid)" now only unbinds "funcid" #111322
gh-75666: Tkinter: "unbind(sequence, funcid)" now only unbinds "funcid" #111322
Conversation
…"funcid" Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. Co-authored-by: GiovanniL <[email protected]>
Lib/test/test_tkinter/test_misc.py
Outdated
self.assertIn(funcid2, script) | ||
self.assertEqual(f.bind(), (event,)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To properly test the unbind code (see comment), I think this should bind 3 scripts and unbind the middle one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also like to test that the other two funcids still work in unbind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a separate test, test_bind_events()
, that tests that registered functions are called when the event occurs. Although it does not cover unbinding.
It is not so easy to make it working. For example, currently I cannot make it working in this test, an I do not know why. Maybe it does not work with keyboard events.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I found how to do this. Will see whether it works on Windows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expanded test, not visible above, is what I proposed. When I just ran test_misc, whether by itself or with test_tkinter, whether directly or with regrtest, test_bind_events hangs with two blank tk windows on the screen. Hence the CI test timed out. When I close the first of the two windows, tests 'continue' after this error:
ERROR: test_bind_events (__main__.BindTest.test_bind_events)
----------------------------------------------------------------------
Traceback (most recent call last):
File "F:\dev\3x\Lib\test\test_tkinter\test_misc.py", line 709, in test_bind_events
root.wait_visibility() # needed on Windows
^^^^^^^^^^^^^^^^^^^^^^
File "F:\dev\3x\Lib\tkinter\__init__.py", line 752, in wait_visibility
self.tk.call('tkwait', 'visibility', window._w)
_tkinter.TclError: window "." was deleted before its visibility changed
This is due to the added "self.frame.wait_visibility()" in BindTest.setUp. With root deleted to let tests continue, the remaining BindTest functions fail with various messages.
DefaultRootTest.test_default_root
also fails, with
"AttributeError: module 'tkinter' has no attribute '_default_root'.". A bad inter-testcase dependency? The following DefaultRootTest.test_getboolean
and everything after passes.
With the the test_bind_events wait_visibility commented out, there is this error.
FAIL: test_bind_events (__main__.BindTest.test_bind_events)
----------------------------------------------------------------------
Traceback (most recent call last):
File "F:\dev\3x\Lib\test\test_tkinter\test_misc.py", line 729, in test_bind_events
self.assertEqual(events, [
AssertionError: Lists differ: [] != [('frame', <tkinter.Frame object .!topleve[172 chars]me>)]
With the setUp wait_visibility instead commented out, there is this error:
>FAIL: test_unbind2 (__main__.BindTest.test_unbind2)
Traceback (most recent call last):
File "F:\dev\3x\Lib\test\test_tkinter\test_misc.py", line 496, in test_unbind2
self.assertEqual(events, ['a', 'b', 'c'])
AssertionError: Lists differ: [] != ['a', 'b', 'c']
With the setUp call instead moved into test_unbind2 either at the top or as f.wait_visibility()
before the generate events call, there is no error.
Lib/tkinter/__init__.py
Outdated
"""Unbind for this widget the event SEQUENCE. | ||
|
||
If FUNCID is given, only unbind the function identified with FUNCID | ||
and also delete that command. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make clearer that this will not delete the Python function passed to the binding call, but only the binding-specific (tcl) wrapper (as the bind docstring promises).
and also delete that command. | |
and also delete the binding-specific tcl wrapper function. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is "the binding-specific tcl wrapper function"? This term is not mentioned in the documentation and will confuse users.
It deletes the Tcl command and removes a reference to Python function. These details are not documented, but experienced users can discover this. I do not know what to write here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it does not work with keyboard events.
I used different events for different tests to avoid possible order dependencies or chain reaction of failures after failing in one test. It was easy with keyboard events, it also minimizes risk of generating even by a user during running tests. But it may be not so easy to find a dozen different non-keyboard events (and we can add more tests in future). Is it necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I withdraw the suggestion above. Either insert 'tcl' before 'command' or wait until someone reports being confused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if funcid is None: | ||
self.tk.call('bind', self._w, sequence, '') | ||
else: | ||
lines = self.tk.call('bind', self._w, sequence).split('\n') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without seeing what lines
looks like for various scenarios, I cannot verify the code below. Hence would have to trust test. Hence would like testing of what seems like a harder scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually it looks like:
if {"[140011831004144test1 %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y %D]" == "break"} break
if {"[140011830917168test2 %# %b %f %h %k %s %t %w %x %y %A %E %K %N %W %T %X %Y %D]" == "break"} break
if it only contains bindings added by bind()
. It can also contain arbitrary Tcl commands, including multiline commands, if it is standard binding or bind()
was called with a string instead of callable (AFAIK IDLE does such hacking). It does not matter, because it is unlikely that somebody add commands in the above form, using generated funcid
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked -- IDLE uses this in fix_x11_paste()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's useful to manually register a string (to reuse functions multiple times or to directly call a Tk method), but that would entirely reasonably preclude using unbind()
with that function. Should there be a test or something to verify it leaves other lines intact?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, you wrote that. This comment only serves to explain what I did not review in detail and why I requested the test change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TeamSpen210 If you manually register a string to directly call a Tk method, unbind()
with funcid
will not touch it, and unbind()
without second argument will remove it. If you use funcid
in other binding, it will stop working when unbind()
deletes the command. If you change the line that calls funcid
, unbind()
may not find it or break the script after deleting the line. In any case, this feature is not documented, and doing so, you are at a risk. Of course, we will try to not break it, but our expectation of possible use cases can be wrong if you do something unusual.
if not line.startswith(prefix)) | ||
if not keep.strip(): | ||
keep = '' | ||
self.tk.call('bind', self._w, sequence, keep) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no idea if it is legal to bind a multiple-script script, hence test should require that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is supported, but neither documented nor tested. I have not touched this, because it can mean doubling or tripling the number of tests.
Misc/NEWS.d/next/Library/2023-10-25-16-37-13.gh-issue-75666.BpsWut.rst
Outdated
Show resolved
Hide resolved
When you're done making the requested changes, leave the comment: |
My response above explains how I fixed the test failure on Windows. |
Thank you @terryjreedy. It is not convenient to me to test it on Windows (start a VM, update the workthree, build, and it is with less convenient and slower CLI), so you helped me. But it still fails on Ubuntu. I did not notice it because tests are passed when run only |
After 'rearrange the code', test_misc passes for me both alone and with test_tkinter. |
I have made the requested changes; please review again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I wrote comments a month ago, but did not submit them.
if funcid is None: | ||
self.tk.call('bind', self._w, sequence, '') | ||
else: | ||
lines = self.tk.call('bind', self._w, sequence).split('\n') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TeamSpen210 If you manually register a string to directly call a Tk method, unbind()
with funcid
will not touch it, and unbind()
without second argument will remove it. If you use funcid
in other binding, it will stop working when unbind()
deletes the command. If you change the line that calls funcid
, unbind()
may not find it or break the script after deleting the line. In any case, this feature is not documented, and doing so, you are at a risk. Of course, we will try to not break it, but our expectation of possible use cases can be wrong if you do something unusual.
Lib/tkinter/__init__.py
Outdated
"""Unbind for this widget the event SEQUENCE. | ||
|
||
If FUNCID is given, only unbind the function identified with FUNCID | ||
and also delete that command. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.11. |
Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.12. |
…"funcid" (pythonGH-111322) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. (cherry picked from commit cc7e45c) Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: GiovanniL <[email protected]>
GH-112801 is a backport of this pull request to the 3.11 branch. |
…"funcid" (pythonGH-111322) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. (cherry picked from commit cc7e45c) Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: GiovanniL <[email protected]>
GH-112802 is a backport of this pull request to the 3.12 branch. |
… "funcid" (GH-111322) (GH-112801) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. (cherry picked from commit cc7e45c) Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: GiovanniL <[email protected]>
… "funcid" (GH-111322) (GH-112802) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. (cherry picked from commit cc7e45c) Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: GiovanniL <[email protected]>
…"funcid" (pythonGH-111322) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. Co-authored-by: GiovanniL <[email protected]>
…"funcid" (pythonGH-111322) Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command. Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command. It leaves "sequence" unbound only if "funcid" was the last bound command. Co-authored-by: GiovanniL <[email protected]>
Previously, "widget.unbind(sequence, funcid)" destroyed the current binding for "sequence", leaving "sequence" unbound, and deleted the "funcid" command.
Now it removes only "funcid" from the binding for "sequence", keeping other commands, and deletes the "funcid" command.
It leaves "sequence" unbound only if "funcid" was the last bound command.