Skip to content
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

Merged

Conversation

serhiy-storchaka
Copy link
Member

@serhiy-storchaka serhiy-storchaka commented Oct 25, 2023

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.

…"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]>
Comment on lines 494 to 495
self.assertIn(funcid2, script)
self.assertEqual(f.bind(), (event,))
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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.

"""Unbind for this widget the event SEQUENCE.

If FUNCID is given, only unbind the function identified with FUNCID
and also delete that command.
Copy link
Member

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).

Suggested change
and also delete that command.
and also delete the binding-specific tcl wrapper function.

Copy link
Member Author

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.

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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')
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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().

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?

Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member Author

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.

@bedevere-app
Copy link

bedevere-app bot commented Oct 26, 2023

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

@terryjreedy
Copy link
Member

My response above explains how I fixed the test failure on Windows.

@serhiy-storchaka
Copy link
Member Author

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 test_tkinter.test_misc, but fail when run all test_tkinter tests.

@terryjreedy
Copy link
Member

After 'rearrange the code', test_misc passes for me both alone and with test_tkinter.

@serhiy-storchaka
Copy link
Member Author

I have made the requested changes; please review again.

Copy link
Member Author

@serhiy-storchaka serhiy-storchaka left a 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')
Copy link
Member Author

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.

"""Unbind for this widget the event SEQUENCE.

If FUNCID is given, only unbind the function identified with FUNCID
and also delete that command.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@serhiy-storchaka serhiy-storchaka merged commit cc7e45c into python:main Dec 6, 2023
1 check passed
@serhiy-storchaka serhiy-storchaka deleted the tkinter-unbind-funcid branch December 6, 2023 14:42
@serhiy-storchaka serhiy-storchaka added needs backport to 3.11 only security fixes needs backport to 3.12 bug and security fixes labels Dec 6, 2023
@miss-islington-app
Copy link

Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.11.
🐍🍒⛏🤖 I'm not a witch! I'm not a witch!

@miss-islington-app
Copy link

Thanks @serhiy-storchaka for the PR 🌮🎉.. I'm working now to backport this PR to: 3.12.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Dec 6, 2023
…"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]>
@bedevere-app
Copy link

bedevere-app bot commented Dec 6, 2023

GH-112801 is a backport of this pull request to the 3.11 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.11 only security fixes label Dec 6, 2023
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Dec 6, 2023
…"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]>
@bedevere-app
Copy link

bedevere-app bot commented Dec 6, 2023

GH-112802 is a backport of this pull request to the 3.12 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.12 bug and security fixes label Dec 6, 2023
serhiy-storchaka added a commit that referenced this pull request Dec 6, 2023
… "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]>
serhiy-storchaka added a commit that referenced this pull request Dec 6, 2023
… "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]>
aisk pushed a commit to aisk/cpython that referenced this pull request Feb 11, 2024
…"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]>
Glyphack pushed a commit to Glyphack/cpython that referenced this pull request Sep 2, 2024
…"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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants