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-112898: Fix double close dialog with warning about unsafed files #113513

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

ronaldoussoren
Copy link
Contributor

@ronaldoussoren ronaldoussoren commented Dec 27, 2023

Without this changeset I get two consecutive dialogs asking if I want to save an unsafed file, when choosing "Cancel" in the first one.

…iles

Without this changeset I get two consecutive dialogs asking
if I want to save an unsafed file, when choosing "Cancel" in the
first one.
@ronaldoussoren
Copy link
Contributor Author

I've added "skip news" because the relevant news item is in #112898

@@ -221,7 +221,7 @@ def help_dialog(event=None):
# The binding above doesn't reliably work on all versions of Tk
# on macOS. Adding command definition below does seem to do the
# right thing for now.
root.createcommand('::tk::mac::Quit', flist.close_all_callback)
root.createcommand('::tk::mac::Quit', lambda: "break")
Copy link
Member

Choose a reason for hiding this comment

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

And what happens when remove this line at all?

Please try different ways to quit the application: not only via menu or closing the main window, but via the dock or when try to shutdown the computer. Try with several modified not saved files, are any changes lost when you cancel save for one of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On macOS there is no "main" window in IDLE, the shell en editor windows are treated equaly and IDLE will quit when you close all of them. Thus the ways to quit IDLE:

  • Manually close all windows
  • Use the quit menu
  • Use the quit keyboard shortcut
  • Log off (or shutdown).

The last one should result in a quit event, and is something I haven't tested yet (too many open windows...), lets first get the normal behaviour correct, especially since retesting shows that the PR doesn't do what I say...

The behaviour with multiple unsaved windows is not yet good enough:

  • Quiting using the menu (IDLE -> Quit IDLE): all is fine, I get warnings for all unsaved files and choosing Cancel in one of them aborts quoting the app, that is IDLE keeps running and no more windows are closed. I'm convinced that the menu did work earlier, but now it doesn't, the menu selection is completely ignored.
  • Quiting using the keyboard shortcut: not quite there yet, I do get warnings for all unsaved files, but choosing Cancel still results in a warning for all other unsaved files.

We also don't quite match system behaviour, although that's hard to be sure about these days due to auto-saving in most system apps. I did compare with two other apps:

  1. Terminal.app: This app shows a warning for windows with active command-line apps, and does this before closing idle windows, whereas IDLE first closes windows that don't have unsaved state (including the shell window) and then warns about unsaved content
  2. Microsoft Word: This shows a pop-up with a summary of unsaved changes ("you have 2 documents with unsaved changes"), again before closing windows with unsaved state. When you do review changes you can cancel at any of the open files, but already closed files will stay closed.

It would be nice to try to close windows with unsaved state before closing the rest, but that's not essential. Same for Word's summary dialog before trying to close any window.

I now have something that works for me, but is not quite ready.

The cleaned up patch (on top of this PR):

diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py
index 92992fd9cc..c6f09335d6 100644
--- a/Lib/idlelib/config.py
+++ b/Lib/idlelib/config.py
@@ -31,6 +31,7 @@
 
 from tkinter.font import Font
 import idlelib
+from idlelib import macosx
 
 class InvalidConfigType(Exception): pass
 class InvalidConfigSet(Exception): pass
@@ -660,6 +661,10 @@ def GetCoreKeys(self, keySetName=None):
             '<<zoom-height>>': ['<Alt-Key-2>'],
             }
 
+        if macosx.isAquaTk():
+            assert '<<close-all-windows>>' in keyBindings
+            del keyBindings['<<close-all-windows>>']
+
         if keySetName:
             if not (self.userCfg['keys'].has_section(keySetName) or
                     self.defaultCfg['keys'].has_section(keySetName)):
diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py
index 683817d1ae..aca5c5f31c 100644
--- a/Lib/idlelib/macosx.py
+++ b/Lib/idlelib/macosx.py
@@ -216,12 +216,10 @@ def help_dialog(event=None):
     root.bind('<<open-config-dialog>>', config_dialog)
     root.createcommand('::tk::mac::ShowPreferences', config_dialog)
     if flist:
-        root.bind('<<close-all-windows>>', flist.close_all_callback)
-
         # The binding above doesn't reliably work on all versions of Tk
         # on macOS. Adding command definition below does seem to do the
         # right thing for now.
-        root.createcommand('::tk::mac::Quit', lambda: "break")
+        root.createcommand('::tk::mac::Quit', flist.close_all_callback)
 
     if isCarbonTk():
         # for Carbon AquaTk, replace the default Tk apple menu

The change to macosx.py reverts this PR, and the other change removes the default key binding for <<close-all-windows>>``. That change doesn't work for me though, the code as written results in getting no pop-ups for unsaved windows at all. The alternative change to config.py` does work, but would break other platforms.

diff --git a/Lib/idlelib/config.py b/Lib/idlelib/config.py
index 92992fd9cc..46c39b3718 100644
--- a/Lib/idlelib/config.py
+++ b/Lib/idlelib/config.py
@@ -31,6 +31,7 @@
 
 from tkinter.font import Font
 import idlelib
+from idlelib import macosx
 
 class InvalidConfigType(Exception): pass
 class InvalidConfigSet(Exception): pass
@@ -605,7 +606,7 @@ def GetCoreKeys(self, keySetName=None):
             '<<paste>>': ['<Control-v>', '<Control-V>'],
             '<<beginning-of-line>>': ['<Control-a>', '<Home>'],
             '<<center-insert>>': ['<Control-l>'],
-            '<<close-all-windows>>': ['<Control-q>'],
+            #'<<close-all-windows>>': ['<Control-q>'],
             '<<close-window>>': ['<Alt-F4>'],
             '<<do-nothing>>': ['<Control-x>'],
             '<<end-of-file>>': ['<Control-d>'],

I'll return to this in the morning, I'm must be overlooking something that explains why the clean patch for config.py doesn't have the expected behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll return to this in the morning, I'm must be overlooking something that explains why the clean patch for config.py doesn't have the expected behaviour.

I couldn't let this go just yet...

For reasons I don't understand the call to macosx.isAquaTk breaks things, if I replace that by a check for sys.platform == "darwin" the code works as expected. That does break using the X11 bindings for Tk on macOS though, although I expected the number of users for that is negligible.

@serhiy-storchaka
Copy link
Member

Please test with #112994. It shows different dialog windows (with and without the "Cancel" button) depending on the way the application is closed, so it can help you to understand what action is triggered.

I think that ::tk::mac::Quit exists because there is a way to close the application on macOS other than via menu, keyboard shortcut or closing all windows.

@ronaldoussoren
Copy link
Contributor Author

Please test with #112994. It shows different dialog windows (with and without the "Cancel" button) depending on the way the application is closed, so it can help you to understand what action is triggered.

I think that ::tk::mac::Quit exists because there is a way to close the application on macOS other than via menu, keyboard shortcut or closing all windows.

osascript -e 'tell application "IDLE" to quit', that's the event used during logging out. The update I just pushed works for that as well.

But as I wrote above: the added if statement in config.py isn't correct and needed in its current form because I've not yet found out why macosx.isAquaTk() causes the app to ignore the quit handler.

Comment on lines 219 to 221
root.bind('<<close-all-windows>>', flist.close_all_callback)

# The binding above doesn't reliably work on all versions of Tk
# on macOS. Adding command definition below does seem to do the
# right thing for now.
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment “The binding above…” should likely also be adjusted so that it makes sense after removing the binding it refers to.

@chrstphrchvz
Copy link
Contributor

chrstphrchvz commented Dec 27, 2023

But as I wrote above: the added if statement in config.py isn't correct and needed in its current form because I've not yet found out why macosx.isAquaTk() causes the app to ignore the quit handler.

I agree that the windowing system should be checked instead, so I would like to understand why ::tk::mac::Quit is ignored. But I notice that if macosx.isAquaTk() is checked instead, then quitting triggers a use-after-free bug in Tk Aqua, one with the same cause and workaround as https://core.tcl-lang.org/tk/info/1ab7a5f299

ASan report:
==76471==ERROR: AddressSanitizer: heap-use-after-free on address 0x61a0000a5ca8 at pc 0x00010bd99942 bp 0x7ff7bd8860f0 sp 0x7ff7bd8860e8
READ of size 8 at 0x61a0000a5ca8 thread T0
    #0 0x10bd99941 in Tcl_FindCommand tclNamesp.c:2553
    #1 0x108e3ac6b in ReallyKillMe tkMacOSXHLEvents.c:594
    #2 0x10bdc097b in Tcl_ServiceEvent tclNotify.c:670
    #3 0x10bdc355b in Tcl_DoOneEvent tclNotify.c:967
    #4 0x106d13c6c in _tkinter_tkapp_mainloop _tkinter.c.h:534
    #5 0x1028f93b4 in method_vectorcall_FASTCALL descrobject.c:393
    #6 0x1028b929e in PyObject_Vectorcall call.c:327
    #7 0x102e953f3 in _PyEval_EvalFrameDefault generated_cases.c.h:815
    #8 0x102e2c2a8 in PyEval_EvalCode ceval.c:592
    #9 0x102e1c2c8 in builtin_exec bltinmodule.c.h:540
    #10 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441
    #11 0x1028b929e in PyObject_Vectorcall call.c:327
    #12 0x102e953f3 in _PyEval_EvalFrameDefault generated_cases.c.h:815
    #13 0x1028b904b in _PyVectorcall_Call call.c:273
    #14 0x1031b53e2 in pymain_run_module main.c:297
    #15 0x1031b29ce in Py_RunMain main.c:707
    #16 0x1031b4a6a in pymain_main main.c:737
    #17 0x1031b4ea8 in Py_BytesMain main.c:761
    #18 0x113cdd52d in start+0x1cd (dyld:x86_64+0x552d)

0x61a0000a5ca8 is located 40 bytes inside of 1304-byte region [0x61a0000a5c80,0x61a0000a6198) freed by thread T0 here: #0 0x10440ccf9 in free+0xa9 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x53cf9) #1 0x10b0cfd1c in TclpFree tclAlloc.c:722 #2 0x10b1201bd in DeleteInterpProc tclBasic.c:1737 #3 0x10be6faae in Tcl_EventuallyFree tclPreserve.c:296 #4 0x10b11979c in Tcl_DeleteInterp tclBasic.c:1412 #5 0x106d08a60 in Tkapp_Dealloc _tkinter.c:2820 #6 0x102a3cf58 in _PyObject_FreeInstanceAttributes dictobject.c:5731 #7 0x102b5f8e3 in subtype_dealloc typeobject.c:2051 #8 0x102ffbf1c in _PyFrame_ClearExceptCode frame.c:140 #9 0x102eeb0b5 in clear_thread_frame ceval.c:1635 #10 0x102e4aa6e in _PyEval_EvalFrameDefault generated_cases.c.h:4853 #11 0x102e2c2a8 in PyEval_EvalCode ceval.c:592 #12 0x102e1c2c8 in builtin_exec bltinmodule.c.h:540 #13 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441 #14 0x1028b904b in _PyVectorcall_Call call.c:273 #15 0x102e5345f in _PyEval_EvalFrameDefault generated_cases.c.h:1250 #16 0x1028bd2b4 in object_vacall call.c:819 #17 0x1028bcc4d in PyObject_CallMethodObjArgs call.c:880 #18 0x10304aa7d in PyImport_ImportModuleLevelObject import.c:2837 #19 0x102e1802b in builtin___import__ bltinmodule.c.h:107 #20 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441 #21 0x1028b904b in _PyVectorcall_Call call.c:273 #22 0x102e5345f in _PyEval_EvalFrameDefault generated_cases.c.h:1250 #23 0x1028bd2b4 in object_vacall call.c:819 #24 0x1028bcc4d in PyObject_CallMethodObjArgs call.c:880 #25 0x10304ad49 in PyImport_ImportModuleLevelObject import.c:2905 #26 0x102e5439f in _PyEval_EvalFrameDefault generated_cases.c.h:2882 #27 0x102e16aa8 in builtin___build_class__ bltinmodule.c:205 #28 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441 #29 0x1028b929e in PyObject_Vectorcall call.c:327
previously allocated by thread T0 here: #0 0x10440cbb0 in malloc+0xa0 (libclang_rt.asan_osx_dynamic.dylib:x86_64h+0x53bb0) #1 0x10b0cfcec in TclpAlloc tclAlloc.c:699 #2 0x10b1b199a in Tcl_Alloc tclCkalloc.c:1059 #3 0x10b10450a in Tcl_CreateInterp tclBasic.c:565 #4 0x106d06381 in Tkapp_New _tkinter.c:567 #5 0x106d041ba in _tkinter_create _tkinter.c.h:793 #6 0x102a7e3b6 in cfunction_vectorcall_FASTCALL methodobject.c:425 #7 0x1028b929e in PyObject_Vectorcall call.c:327 #8 0x102e953f3 in _PyEval_EvalFrameDefault generated_cases.c.h:815 #9 0x1028b61cf in _PyObject_VectorcallDictTstate call.c:135 #10 0x1028ba219 in _PyObject_Call_Prepend call.c:504 #11 0x102b67cd1 in slot_tp_init typeobject.c:9063 #12 0x102b4ebc8 in type_call typeobject.c:1687 #13 0x1028b662a in _PyObject_MakeTpCall call.c:242 #14 0x102e953f3 in _PyEval_EvalFrameDefault generated_cases.c.h:815 #15 0x102e2c2a8 in PyEval_EvalCode ceval.c:592 #16 0x102e1c2c8 in builtin_exec bltinmodule.c.h:540 #17 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441 #18 0x1028b904b in _PyVectorcall_Call call.c:273 #19 0x102e5345f in _PyEval_EvalFrameDefault generated_cases.c.h:1250 #20 0x1028bd2b4 in object_vacall call.c:819 #21 0x1028bcc4d in PyObject_CallMethodObjArgs call.c:880 #22 0x10304aa7d in PyImport_ImportModuleLevelObject import.c:2837 #23 0x102e1802b in builtin___import__ bltinmodule.c.h:107 #24 0x102a7e4db in cfunction_vectorcall_FASTCALL_KEYWORDS methodobject.c:441 #25 0x1028b904b in _PyVectorcall_Call call.c:273 #26 0x102e5345f in _PyEval_EvalFrameDefault generated_cases.c.h:1250 #27 0x1028bd2b4 in object_vacall call.c:819 #28 0x1028bcc4d in PyObject_CallMethodObjArgs call.c:880 #29 0x10304ad49 in PyImport_ImportModuleLevelObject import.c:2905

@chrstphrchvz
Copy link
Contributor

chrstphrchvz commented Dec 27, 2023

In Tk Aqua, the first Tk interpreter (i.e. the first Tcl interpreter to load Tk) is the one that will receive Apple high-level events, such as quitting. (The use-after-free bug I mentioned has to do with how Tk Aqua keeps a reference to the first Tk interpreter.) This means that defining ::tk::mac::Quit is only effective when done from the first created Tk interpreter.

macosx.isAquaTk() relies on creating a new tkinter.Tk() instance, i.e. a separate Tk interpreter. Calling macosx.isAquaTk() from GetCoreKeys() ends up creating the first Tk interpreter, and so the ::tk::mac::Quit definition in overrideRootMenu() gets ignored.

@chrstphrchvz
Copy link
Contributor

If specifying bindings in config-keys.def is mandatory as I described in #112898 (comment) then a hacky workaround would be to specify a key which is nonexistent in practice but still recognized by Tk, such as F35:

--- a/Lib/idlelib/config-keys.def
+++ b/Lib/idlelib/config-keys.def
@@ -264,7 +264,7 @@ redo = <Shift-Command-Key-Z>
 close-window = <Command-Key-w>
 restart-shell = <Control-Key-F6>
 save-window-as-file = <Shift-Command-Key-S>
-close-all-windows = <Command-Key-q>
+close-all-windows = <Key-F35>
 view-restart = <Key-F6>
 tabify-region = <Control-Key-5>
 find-again = <Command-Key-g> <Key-F3>

Then there would be no need to modify idlelib/config.py.

@ronaldoussoren
Copy link
Contributor Author

ronaldoussoren commented Dec 27, 2023

But as I wrote above: the added if statement in config.py isn't correct and needed in its current form because I've not yet found out why macosx.isAquaTk() causes the app to ignore the quit handler.

I agree that the windowing system should be checked instead, so I would like to understand why ::tk::mac::Quit is ignored.

I haven't tried to debug this yet, other than noticing that IDLE quits without asking about unsafed data if I test using macosx.isAquaTk().

But I notice that if macosx.isAquaTk() is checked instead, then quitting triggers a use-after-free bug in Tk Aqua, one with the same cause and workaround as https://core.tcl-lang.org/tk/info/1ab7a5f299

ASan report:

:-(

@ronaldoussoren
Copy link
Contributor Author

If specifying bindings in config-keys.def is mandatory as I described in #112898 (comment) then a hacky workaround would be to specify a key which is nonexistent in practice but still recognized by Tk, such as F35:

--- a/Lib/idlelib/config-keys.def
+++ b/Lib/idlelib/config-keys.def
@@ -264,7 +264,7 @@ redo = <Shift-Command-Key-Z>
 close-window = <Command-Key-w>
 restart-shell = <Control-Key-F6>
 save-window-as-file = <Shift-Command-Key-S>
-close-all-windows = <Command-Key-q>
+close-all-windows = <Key-F35>
 view-restart = <Key-F6>
 tabify-region = <Control-Key-5>
 find-again = <Command-Key-g> <Key-F3>

Then there would be no need to modify idlelib/config.py.

That sadly doesn't work, IDLE just quits when quitting with unsaved files (both using the menu and Cmd-Q shortcut).

This is definitely related to macosx.py:_init_tk_type(), I get the same behaviour when calling that function in the body of macosx.py (e.g. eagerly initialising the variable).

And that in turn is to be triggered by creating a tkinter.Tk object in _init_tk_type. If I create a '::tk::mac::Quit command using that root the function registered there will get called and not the callable registered in overrideRootMenu.

@ronaldoussoren
Copy link
Contributor Author

That's it for tonight.

The current PR works for me (TM), but contains code in macosx.py that I don't particularly like and still removes the <<close-all-windows>> binding in config.py (on AquaTk).

@chrstphrchvz
Copy link
Contributor

… a hacky workaround would be to specify a key which is nonexistent in practice but still recognized by Tk, such as F35…

That sadly doesn't work, IDLE just quits when quitting with unsaved files (both using the menu and Cmd-Q shortcut).

I should clarify that I meant for the change I suggested to be used without the changes in this PR; were there other changes present when you tested it? Also note the line number of the change: it should apply to the binding for [IDLE Classic OSX], rather than the similar binding at line 196 for [IDLE Classic Mac].

Copy link
Contributor

@chrstphrchvz chrstphrchvz left a comment

Choose a reason for hiding this comment

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

I have verified that 4cefbea works as expected.

It also does not trigger the use-after-free bug, which can only occur once the first Tk interpreter is deleted. Calling root.createcommand(…) prevents root from deallocating, which in turn prevents the first Tk interpreter from being deleted.

@@ -158,6 +168,7 @@ def overrideRootMenu(root, flist):
from idlelib import mainmenu
from idlelib import window


Copy link
Contributor

Choose a reason for hiding this comment

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

Stray newline?

``macosx.isAquaTk()`` needs access to Tk to query it. Before this
changeset it created a temporary Tk instance. It turns out that
the *first* instance of Tk created on macOS is special and must
to be used to set up the commands to handle mac specific system
events (file open, quit, ...).

By restructuring the code a little we can avoid creating a temporary
Tk object.
@ronaldoussoren
Copy link
Contributor Author

My latest commits to the PR clean up the code by tweaking the code a little to avoid having to create a temporary tkinter.Tk instance in macosx._init_tk_type. This required some reordering in pyshell.main and required adding getters and setters for idlelib.mainmenu.default_keydefs, both to avoid needing to call macosx._init_tk_type before IDLE's Tk root is created.

The code in macosx._init_tk_type that accesses macosx._idle_root is intentionally fragile, IDLE will fail to start up when macosx.isAquaTk is called before calling macosx.setupApp (on macOS).

The current code looks mergeable to me now.

@ronaldoussoren
Copy link
Contributor Author

@terryjreedy, could you please review this PR? This fixes a real issue for macOS users (work may be lost when quitting IDLE, for example during system shutdown).

@terryjreedy
Copy link
Member

terryjreedy commented Dec 29, 2023

[Started before the last commit]
After reading #112898 (comment) and the following comment late last night, USA east coast time, I planned to write today a (separate) PR to change macosx._init_tk_type and pyshell.main to use the initial IDLE root when IDLE is running. But without deleting the temporary root when testing. It and the complexity of having the isXyz functions call _init_tk_type is only needed for testing, when the idlelib code called during the test happens to call one of the isXyz functions. Such calls may not be easy to predict. Hence the multiple test failures when the temp root was unconditionally deleted.

To fix the tests, there are multiple options.

  1. Restore the temporary root when testing and delete if created. Something like
        if testing:
            ...
                requires('gui')
                temroot = ...
                ws = temroot.tk.call...
        else:
            ws = _idle_root...
        ...
        if testing and 'temroot' in locals: temroot.destroy()  # or try: temroot.destroy\nexcept NameError: pass

2. (Long term easiest if possible.)  Always set _tk_type to "cocoa" when testing on Darwin.  I suggested in the existing comment, added in my last revision, that this might be possible (at least as long as we do not add macOS specific tests).  To test, run test_idle locally on non-cocoa machines after omitting the 'requires' test and see if anything fails.

3. Add a call to _init_tk_type after the first root creation in the test files that have a failure.  Fragile if test-cases are reordered or the call is forgotten or not added when needed for new tests.   Though CI will now catch such.

4. Change tests to create one root per test file, when needed anywhere, and call _init_tk_type.  Then gate gui-requiring test cases with `requires('gui')`.

@ronaldoussoren
Copy link
Contributor Author

[Started before the last commit] After reading #112898 (comment) and the following comment late last night, USA east coast time, I planned to write today a (separate) PR to change macosx._init_tk_type and pyshell.main to use the initial IDLE root when IDLE is running. But without deleting the temporary root when testing. It and the complexity of having the isXyz functions call _init_tk_type is only needed for testing, when the idlelib code called during the test happens to call one of the isXyz functions. Such calls may not be easy to predict. Hence the multiple test failures when the temp root was unconditionally deleted.

To fix the tests, there are multiple options.

  1. Restore the temporary root when testing and delete if created. Something like
        if testing:
            ...
                requires('gui')
                temroot = ...
                ws = temroot.tk.call...
        else:
            ws = _idle_root...
        ...
        if testing and 'temroot' in locals: temroot.destroy()  # or try: temroot.destroy\nexcept NameError: pass

TBH I'm not a fan of the if testing block in the first place, the requires('gui') marker should be in tests that end up calling _init_tk type.

  1. (Long term easiest if possible.) Always set _tk_type to "cocoa" when testing on Darwin. I suggested in the existing comment, added in my last revision, that this might be possible (at least as long as we do not add macOS specific tests). To test, run test_idle locally on non-cocoa machines after omitting the 'requires' test and see if anything fails.

This change would be fine with me, I'd be surprised if there's anyone left that would want to run (let alone test) the X11 based Tk backend on macOS (let alone the Carbon backend, that might not even exist in Tk 8.6)

That would IMHO require adding a test that asserts that the GUI type is "cocoa" on macOS to make it abundantly clear that this is the only supported setup.

  1. Add a call to _init_tk_type after the first root creation in the test files that have a failure. Fragile if test-cases are reordered or the call is forgotten or not added when needed for new tests. Though CI will now catch such.

Currently all tests pass on macOS with this PR (alternate framework name to not affect a python.org install):

% /Library/Frameworks/DebugPython.framework/Versions/3.13/bin/python3 -m test -u gui test_tkinter                                                                                                                                                                   
Using random seed: 987269575
0:00:00 load avg: 2.39 Run 1 test sequentially
0:00:00 load avg: 2.39 [1/1] test_tkinter

== Tests result: SUCCESS ==

1 test OK.

Total duration: 10.7 sec
Total tests: run=754 skipped=3
Total test files: run=1/1
Result: SUCCESS

You'll quickly notice if a test reorg requires changes because _init_tk_type will now raise an exception when called at the wrong time.

  1. Change tests to create one root per test file, when needed anywhere, and call _init_tk_type. Then gate gui-requiring test cases with requires('gui').

As @chrstphrchvz noted we should try to avoid more than one root, and that IMHO should include tests. We tend to run into edge cases in Tk when using multiple root's because Tcl users generally don't do this and also some functionality only works in the first root.

Change the tests to ensure that the new invariants for
macosx are met.

This does assume that we're only interested in testing
CocoaTk on macOS (which is IMHO a reasonable assumption)
@ronaldoussoren
Copy link
Contributor Author

@terryjreedy, I've updated the tests as well they now pass on all platforms. I chose to mock macosx._tk_type to avoid adding a dependency GUI testing just because some code calls macosx.isAquaTk().

The tests assume that the GUI variant on macOS will be "cocoa", which is IMHO a safe assumption to make. The Carbon back-end to Tk is AFAIK long gone and X11 will be tested on other platforms.

Sorry about pushing, but what's needed to bring this PR over the finish line?

@ronaldoussoren
Copy link
Contributor Author

@terryjreedy, I intent to merge this tomorrow afternoon (CET timezone).

Copy link
Member

@terryjreedy terryjreedy left a comment

Choose a reason for hiding this comment

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

I did not review the long issue discussion to really understand the <> issue. I reviewed the other non-testing idlelib changes.

@@ -1611,6 +1611,12 @@ def main():
from idlelib.run import fix_scaling
fix_scaling(root)

# start editor and/or shell windows:
Copy link
Member

Choose a reason for hiding this comment

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

Move this comment back where is was.

# to the key bindings an rely the default binding.
#
# Without this IDLE will prompt twice about closing a file with
# unsaved # changes when the user quits IDLE using the keyboard
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# unsaved # changes when the user quits IDLE using the keyboard
# unsaved changes when the user quits IDLE using the keyboard

Comment on lines +38 to +40
if _idle_root is None:
_tk_type = "cocoa"
return
Copy link
Member

Choose a reason for hiding this comment

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

I believe everything from the 'testing' import to here can be deleted and these 3 lines dedented twice. IDLE will set _idle_root, at least when on mac and if a test on mac needs an accurate _tk_type, it should check requires('gui') and set _idle_root. Checking it here is redundant. Do as you wish for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm leaving this as is for now, I can check later if it is save to remove the testing import.

@bedevere-app
Copy link

bedevere-app bot commented Feb 3, 2024

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

@terryjreedy
Copy link
Member

Manual checks on Windows of saving, saving as, and closing with unsaved changes seems to behave the same without and with this patch.

@ronaldoussoren
Copy link
Contributor Author

I have made the requested changes; please review again.

@ronaldoussoren
Copy link
Contributor Author

Sorry about the slow response, I didn't have time to work on Python last weekend after all.

@terryjreedy
Copy link
Member

Ronald, I am leaving this for you to commit as I cannot test the macOS-specific changes except that they do not affect behavior on Windows. And that the cosmetic changes I requested are correct ;-).

@serhiy-storchaka serhiy-storchaka added needs backport to 3.13 bugs and security fixes and removed needs backport to 3.11 only security fixes labels May 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants