From 90a63438467ed91eab0e7479d8a4364b3b901196 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Fri, 3 Jul 2020 15:06:16 -0400 Subject: [PATCH 01/39] bpo-41176: Expose tkinter dispatching state new method `dispatching` on tkinter, Misc, and _tkinter.tkapp --- Lib/tkinter/__init__.py | 9 +++++++++ Lib/tkinter/test/test_tkinter/test_misc.py | 11 +++++++++++ Modules/_tkinter.c | 13 +++++++++++++ Modules/clinic/_tkinter.c.h | 20 ++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index a3378d012fb41a..23b9eb99e3ca57 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -595,6 +595,11 @@ def mainloop(n=0): _default_root.tk.mainloop(n) +def dispatching(): + """Determine if the tkinter main loop is running""" + return _default_root.tk.dispatching() + + getint = int getdouble = float @@ -1421,6 +1426,10 @@ def mainloop(self, n=0): """Call the mainloop of Tk.""" self.tk.mainloop(n) + def dispatching(self): + """Determine if the tkinter main loop is running.""" + return self.tk.dispatching() + def quit(self): """Quit the Tcl interpreter. All widgets will be destroyed.""" self.tk.quit() diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 1e089747a91ee5..365c4afafdf413 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -192,6 +192,17 @@ def test_clipboard_astral(self): with self.assertRaises(tkinter.TclError): root.clipboard_get() + def test_mainloop_dispatching(self): + # reconstruct default root destroyed by AbstractTkTest + root = tkinter._default_root = self.root + for thing in (root.tk, root, tkinter): + #mainloop, dispatching, willdispatch + self.assertFalse(thing.dispatching()) + root.after(0, lambda:self.assertTrue(thing.dispatching())) + root.after(0, root.quit) + root.mainloop() + self.assertFalse(thing.dispatching()) + tests_gui = (MiscTest, ) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 793c5e71548846..37b7f723830af8 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3033,6 +3033,18 @@ _tkinter_tkapp_willdispatch_impl(TkappObject *self) Py_RETURN_NONE; } +/*[clinic input] +_tkinter.tkapp.dispatching + +[clinic start generated code]*/ + +static PyObject* +_tkinter_tkapp_dispatching_impl(TkappObject* self) +/*[clinic end generated code: output=0e3f46d244642155 input=d88f5970843d6dab]*/ +{ + return PyLong_FromLong(self->dispatching); +} + /**** Tkapp Type Methods ****/ @@ -3252,6 +3264,7 @@ static PyType_Spec Tktt_Type_spec = { static PyMethodDef Tkapp_methods[] = { _TKINTER_TKAPP_WILLDISPATCH_METHODDEF + _TKINTER_TKAPP_DISPATCHING_METHODDEF {"wantobjects", Tkapp_WantObjects, METH_VARARGS}, {"call", Tkapp_Call, METH_VARARGS}, _TKINTER_TKAPP_EVAL_METHODDEF diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 9718986838fbb3..db0a6e05a31c02 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -645,6 +645,26 @@ _tkinter_tkapp_willdispatch(TkappObject *self, PyObject *Py_UNUSED(ignored)) return _tkinter_tkapp_willdispatch_impl(self); } +PyDoc_STRVAR(_tkinter_tkapp_dispatching__doc__, + "dispatching($self, /)\n" + "--\n" + "\n" + "Returns the internal dispatching state.\n" + "Returns 1 if the mainloop is running, or 0 if the mainloop is not running.\n" + "\n"); + +#define _TKINTER_TKAPP_DISPATCHING_METHODDEF \ + {"dispatching", (PyCFunction)_tkinter_tkapp_dispatching, METH_NOARGS, _tkinter_tkapp_dispatching__doc__}, + +static PyObject* +_tkinter_tkapp_dispatching_impl(TkappObject* self); + +static PyObject* +_tkinter_tkapp_dispatching(TkappObject* self, PyObject* Py_UNUSED(ignored)) +{ + return _tkinter_tkapp_dispatching_impl(self); +} + PyDoc_STRVAR(_tkinter__flatten__doc__, "_flatten($module, item, /)\n" "--\n" From f4262786caed872b4efc53a47033efeb880cab57 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Fri, 3 Jul 2020 15:31:03 -0400 Subject: [PATCH 02/39] bpo-41176: Deprecate implicit mainloop waiting behavior DeprecationWarning issued on implicit wait Docstring changes Comment stubs for eventual removal of WaitForMainloop Add setmainloopwaitattempts method to _tkinter.tkapp --- Lib/tkinter/test/test_tkinter/test_misc.py | 34 +++++++++ Modules/_tkinter.c | 88 +++++++++++++++++++--- Modules/clinic/_tkinter.c.h | 51 +++++++++++-- 3 files changed, 157 insertions(+), 16 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 365c4afafdf413..8ebe3b96d39b05 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -203,6 +203,40 @@ def test_mainloop_dispatching(self): root.mainloop() self.assertFalse(thing.dispatching()) + def test_thread_must_wait_for_mainloop(self): + import threading, time + def target(): + self.assertFalse(root.dispatching()) + with self.assertRaises(RuntimeError): + root.after(0) # Null op + + ready_for_mainloop.set() + + # self.assertTrue(root.dispatching()) but patient + t = time.monotonic() + while not root.dispatching(): + time.sleep(.001) + self.assertTrue(time.monotonic() < (t + 10)) + + root.quit() + + root = self.root + + # remove on eventual WaitForMainloop behavior change + root.tk.setmainloopwaitattempts(0) + + ready_for_mainloop = threading.Event() + thread = threading.Thread(target=target) + self.assertFalse(root.dispatching()) + thread.start() + ready_for_mainloop.wait() + root.mainloop() + self.assertFalse(root.dispatching()) + thread.join() + + with self.assertWarns(DeprecationWarning): + root.tk.setmainloopwaitattempts(10) + tests_gui = (MiscTest, ) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 37b7f723830af8..3e0de5f9ee7786 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -360,21 +360,36 @@ Sleep(int milli) } #endif /* MS_WINDOWS */ -/* Wait up to 1s for the mainloop to come up. */ +/* Wait for the mainloop to come up. */ +static int mainloopwaitattempts = 10; // 1 second static int WaitForMainloop(TkappObject* self) { int i; - for (i = 0; i < 10; i++) { - if (self->dispatching) - return 1; + if (self->dispatching) + { + return 1; + } + for (i = 0; i < mainloopwaitattempts; i++) { Py_BEGIN_ALLOW_THREADS Sleep(100); Py_END_ALLOW_THREADS + if (self->dispatching) + { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "It seems you are implicitly waiting for " + "the mainloop to come up.\n" + "This behavior is deprecated; consider polling " + "dispatching() instead.\n", 1)) + { + /* return something nonzero, an error will be + raised elsewhere soon */ + return -1; + } + return 1; + } } - if (self->dispatching) - return 1; PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); return 0; } @@ -1500,7 +1515,10 @@ Tkapp_Call(PyObject *selfptr, PyObject *args) marshal the parameters to the interpreter thread. */ Tkapp_CallEvent *ev; Tcl_Condition cond = NULL; - PyObject *exc_type, *exc_value, *exc_tb; + PyObject *exc_type, *exc_value, *exc_tb; + /* After WaitForMainloop deprecation: + if (!self->dispatching) + return NULL; */ if (!WaitForMainloop(self)) return NULL; ev = (Tkapp_CallEvent*)attemptckalloc(sizeof(Tkapp_CallEvent)); @@ -1777,6 +1795,10 @@ var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) /* The current thread is not the interpreter thread. Marshal the call to the interpreter thread, then wait for completion. */ + + /* After WaitForMainloop deprecation: + if (!self->dispatching) + return NULL; */ if (!WaitForMainloop(self)) return NULL; @@ -2477,6 +2499,10 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, return NULL; } + /*After WaitForMainloop deprecation: + if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && + !self->dispatching) + return NULL; */ if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && !WaitForMainloop(self)) return NULL; @@ -3024,10 +3050,15 @@ _tkinter.tkapp.willdispatch [clinic start generated code]*/ -static PyObject * -_tkinter_tkapp_willdispatch_impl(TkappObject *self) +static PyObject* +_tkinter_tkapp_willdispatch_impl(TkappObject* self) /*[clinic end generated code: output=0e3f46d244642155 input=d88f5970843d6dab]*/ { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "willdispatch() is deprecated; consider polling dispatch() instead.", 1)) + { + return NULL; + } self->dispatching = 1; Py_RETURN_NONE; @@ -3045,6 +3076,44 @@ _tkinter_tkapp_dispatching_impl(TkappObject* self) return PyLong_FromLong(self->dispatching); } +/*[clinic input] +_tkinter.setmainloopwaitattempts + + new_val: int + / + +Set number of 100 millisecond mainloop wait attempts that call(), var_invoke(), +and createcommand() will use. + +Current default is 10 for a 1 second wait, but future behavior will be equivalent 0. + +Setting anything other than 0 will trigger a DeprecationWarning. + +[clinic start generated code]*/ + +static PyObject* +_tkinter_tkapp_setmainloopwaitattempts_impl(PyObject* self, int new_val) +/*[clinic end generated code: output=42bf7757dc2d0ab6 input=deca1d6f9e6dae47]*/ +{ + if (new_val < 0) + { + PyErr_SetString(PyExc_ValueError, + "mainloopwaitattempts must be >= 0"); + return NULL; + } + if (new_val) + { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "It seems you want to wait for the mainloop to come up.\n" + "This behavior is deprecated; consider polling dispatch() instead.", 1)) + { + return NULL; + } + } + mainloopwaitattempts = new_val; + Py_RETURN_NONE; +} + /**** Tkapp Type Methods ****/ @@ -3265,6 +3334,7 @@ static PyMethodDef Tkapp_methods[] = { _TKINTER_TKAPP_WILLDISPATCH_METHODDEF _TKINTER_TKAPP_DISPATCHING_METHODDEF + _TKINTER_TKAPP_SETMAINLOOPWAITATTEMPTS_METHODDEF {"wantobjects", Tkapp_WantObjects, METH_VARARGS}, {"call", Tkapp_Call, METH_VARARGS}, _TKINTER_TKAPP_EVAL_METHODDEF diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index db0a6e05a31c02..b07052b2741c5e 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -629,18 +629,22 @@ _tkinter_tkapp_loadtk(TkappObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(_tkinter_tkapp_willdispatch__doc__, -"willdispatch($self, /)\n" -"--\n" -"\n"); + "willdispatch($self, /)\n" + "--\n" + "\n" + "DEPRECATED\n" + "Sets the internal dispatching flag regardless of mainloop dispatch state\n" + "Skips mainloop waiting time\n" + "\n"); #define _TKINTER_TKAPP_WILLDISPATCH_METHODDEF \ {"willdispatch", (PyCFunction)_tkinter_tkapp_willdispatch, METH_NOARGS, _tkinter_tkapp_willdispatch__doc__}, -static PyObject * -_tkinter_tkapp_willdispatch_impl(TkappObject *self); +static PyObject* +_tkinter_tkapp_willdispatch_impl(TkappObject* self); -static PyObject * -_tkinter_tkapp_willdispatch(TkappObject *self, PyObject *Py_UNUSED(ignored)) +static PyObject* +_tkinter_tkapp_willdispatch(TkappObject* self, PyObject* Py_UNUSED(ignored)) { return _tkinter_tkapp_willdispatch_impl(self); } @@ -665,6 +669,39 @@ _tkinter_tkapp_dispatching(TkappObject* self, PyObject* Py_UNUSED(ignored)) return _tkinter_tkapp_dispatching_impl(self); } +PyDoc_STRVAR(_tkinter_tkapp_setmainloopwaitattempts__doc__, + "setmainloopwaitattempts($self, new_val, /)\n" + "--\n" + "\n" + "Set number of 100 ms mainloop wait attempts that call(), var_invoke(), and createcommand() will use.\n" + "\n" + "Current default is 10 for a 1 second wait, but future behavior will default to 0.\n" + "\n" + "Setting anything other than 0 will trigger a DeprecationWarning.\n" + "\n"); + +#define _TKINTER_TKAPP_SETMAINLOOPWAITATTEMPTS_METHODDEF \ + {"setmainloopwaitattempts", (PyCFunction)_tkinter_tkapp_setmainloopwaitattempts, METH_O, _tkinter_tkapp_setmainloopwaitattempts__doc__}, + +static PyObject* +_tkinter_tkapp_setmainloopwaitattempts_impl(PyObject* self, int new_val); + +static PyObject* +_tkinter_tkapp_setmainloopwaitattempts(PyObject* self, PyObject* arg) +{ + PyObject* return_value = NULL; + int new_val; + + new_val = _PyLong_AsInt(arg); + if (new_val == -1 && PyErr_Occurred()) { + goto exit; + } + return_value = _tkinter_tkapp_setmainloopwaitattempts_impl(self, new_val); + +exit: + return return_value; +} + PyDoc_STRVAR(_tkinter__flatten__doc__, "_flatten($module, item, /)\n" "--\n" From 3350dbc76ce687a929a335cf5ed9ace0dc37b0eb Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Fri, 3 Jul 2020 15:32:30 -0400 Subject: [PATCH 03/39] bpo-41176: additional docstring corrections --- Lib/tkinter/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 23b9eb99e3ca57..d60b087caa86b4 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -591,7 +591,7 @@ def get(self): def mainloop(n=0): - """Run the main loop of Tcl.""" + """Run the main loop of tkinter.""" _default_root.tk.mainloop(n) @@ -1423,7 +1423,7 @@ def unbind_class(self, className, sequence): self.tk.call('bind', className , sequence, '') def mainloop(self, n=0): - """Call the mainloop of Tk.""" + """Call the main loop of tkinter.""" self.tk.mainloop(n) def dispatching(self): From 9db47cf3a67c029b7be47cab4c31367f6249d2bf Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Fri, 3 Jul 2020 15:50:04 -0400 Subject: [PATCH 04/39] bpo-41176: Correct tkinter test usage of `update` Replace all usages of update_idletasks in tests with update. update_idletasks does not in fact process all pending events. It updates the idle tasks only, without touching the normal event queue. This is not normaly a problem... until it is. Update docstrings to clarify. --- Lib/tkinter/__init__.py | 8 ++++---- Lib/tkinter/test/support.py | 4 ++-- Lib/tkinter/test/test_tkinter/test_misc.py | 4 ++-- Lib/tkinter/test/test_ttk/test_extensions.py | 4 ++-- Lib/tkinter/test/test_ttk/test_widgets.py | 12 ++++++------ 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index d60b087caa86b4..e9029bb8a60e74 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1308,13 +1308,13 @@ def winfo_y(self): self.tk.call('winfo', 'y', self._w)) def update(self): - """Enter event loop until all pending events have been processed by Tcl.""" + """Process all events, including idle tasks, in the Tcl queue.""" self.tk.call('update') def update_idletasks(self): - """Enter event loop until all idle callbacks have been called. This - will update the display of windows but not process events caused by - the user.""" + """Process all idle callbacks in the Tcl queue. + This will update the display of windows but not process events + caused by the user.""" self.tk.call('update', 'idletasks') def bindtags(self, tagList=None): diff --git a/Lib/tkinter/test/support.py b/Lib/tkinter/test/support.py index 467a0b66c265c0..1220854205917b 100644 --- a/Lib/tkinter/test/support.py +++ b/Lib/tkinter/test/support.py @@ -22,7 +22,7 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - cls.root.update_idletasks() + cls.root.update() cls.root.destroy() del cls.root tkinter._default_root = None @@ -38,7 +38,7 @@ def tearDown(self): def destroy_default_root(): if getattr(tkinter, '_default_root', None): - tkinter._default_root.update_idletasks() + tkinter._default_root.update() tkinter._default_root.destroy() tkinter._default_root = None diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 8ebe3b96d39b05..40f1a8e3b73620 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -109,7 +109,7 @@ def callback(start=0, step=1): idle1 = root.after_idle(callback) self.assertIn(idle1, root.tk.call('after', 'info')) (script, _) = root.tk.splitlist(root.tk.call('after', 'info', idle1)) - root.update_idletasks() # Process all pending events. + root.update() # Process all pending events. self.assertEqual(count, 1) with self.assertRaises(tkinter.TclError): root.tk.call(script) @@ -117,7 +117,7 @@ def callback(start=0, step=1): # Set up with callback with args. count = 0 idle1 = root.after_idle(callback, 42, 11) - root.update_idletasks() # Process all pending events. + root.update() # Process all pending events. self.assertEqual(count, 53) # Cancel before called. diff --git a/Lib/tkinter/test/test_ttk/test_extensions.py b/Lib/tkinter/test/test_ttk/test_extensions.py index a45f882bb00d48..ff0fb8cacae158 100644 --- a/Lib/tkinter/test/test_ttk/test_extensions.py +++ b/Lib/tkinter/test/test_ttk/test_extensions.py @@ -10,7 +10,7 @@ class LabeledScaleTest(AbstractTkTest, unittest.TestCase): def tearDown(self): - self.root.update_idletasks() + self.root.update() super().tearDown() def test_widget_destroy(self): @@ -219,7 +219,7 @@ def test_widget_destroy(self): var = tkinter.StringVar(self.root) optmenu = ttk.OptionMenu(self.root, var) name = var._name - optmenu.update_idletasks() + optmenu.update() optmenu.destroy() self.assertEqual(optmenu.tk.globalgetvar(name), var.get()) del var diff --git a/Lib/tkinter/test/test_ttk/test_widgets.py b/Lib/tkinter/test/test_ttk/test_widgets.py index 2598bc67652075..4dd09d905aa3c7 100644 --- a/Lib/tkinter/test/test_ttk/test_widgets.py +++ b/Lib/tkinter/test/test_ttk/test_widgets.py @@ -64,7 +64,7 @@ def setUp(self): def test_identify(self): - self.widget.update_idletasks() + self.widget.update() self.assertEqual(self.widget.identify( int(self.widget.winfo_width() / 2), int(self.widget.winfo_height() / 2) @@ -327,7 +327,7 @@ def test_bbox(self): def test_identify(self): self.entry.pack() self.entry.wait_visibility() - self.entry.update_idletasks() + self.entry.update() # bpo-27313: macOS Cocoa widget differs from X, allow either if sys.platform == 'darwin': @@ -439,7 +439,7 @@ def _show_drop_down_listbox(self): width = self.combo.winfo_width() self.combo.event_generate('', x=width - 5, y=5) self.combo.event_generate('', x=width - 5, y=5) - self.combo.update_idletasks() + self.combo.update() def test_virtual_event(self): @@ -1134,7 +1134,7 @@ def _click_increment_arrow(self): y = height//2 - 5 self.spin.event_generate('', x=x, y=y) self.spin.event_generate('', x=x, y=y) - self.spin.update_idletasks() + self.spin.update() def _click_decrement_arrow(self): width = self.spin.winfo_width() @@ -1143,7 +1143,7 @@ def _click_decrement_arrow(self): y = height//2 + 4 self.spin.event_generate('', x=x, y=y) self.spin.event_generate('', x=x, y=y) - self.spin.update_idletasks() + self.spin.update() def test_command(self): success = [] @@ -1159,7 +1159,7 @@ def test_command(self): # testing postcommand removal self.spin['command'] = '' - self.spin.update_idletasks() + self.spin.update() self._click_increment_arrow() self._click_decrement_arrow() self.spin.update() From 2817b2acf79d70c92e5ace72695470f51b2d6e70 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 4 Jul 2020 09:05:51 -0400 Subject: [PATCH 05/39] bpo-41176: Rename according to comment and use Clinic --- Lib/tkinter/test/test_tkinter/test_misc.py | 8 +-- Modules/_tkinter.c | 40 +++++++---- Modules/clinic/_tkinter.c.h | 77 ++++++++++++---------- 3 files changed, 72 insertions(+), 53 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 40f1a8e3b73620..7b800cea691fce 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -195,13 +195,13 @@ def test_clipboard_astral(self): def test_mainloop_dispatching(self): # reconstruct default root destroyed by AbstractTkTest root = tkinter._default_root = self.root - for thing in (root.tk, root, tkinter): + for obj in (root.tk, root, tkinter): #mainloop, dispatching, willdispatch - self.assertFalse(thing.dispatching()) - root.after(0, lambda:self.assertTrue(thing.dispatching())) + self.assertFalse(obj.dispatching()) + root.after(0, lambda:self.assertTrue(obj.dispatching())) root.after(0, root.quit) root.mainloop() - self.assertFalse(thing.dispatching()) + self.assertFalse(obj.dispatching()) def test_thread_must_wait_for_mainloop(self): import threading, time diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 3e0de5f9ee7786..c2d98093f42946 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3048,11 +3048,19 @@ Tkapp_WantObjects(PyObject *self, PyObject *args) /*[clinic input] _tkinter.tkapp.willdispatch +Skips main loop wait polling until next call of mainloop finishes + +DEPRECATED + +call(), var_invoke(), and createcommand() will wait indefinitely for main loop +Sets the internal dispatching flag regardless of mainloop dispatch state +May lead to unexpected hangs or inconsistencies in dispatching + [clinic start generated code]*/ -static PyObject* -_tkinter_tkapp_willdispatch_impl(TkappObject* self) -/*[clinic end generated code: output=0e3f46d244642155 input=d88f5970843d6dab]*/ +static PyObject * +_tkinter_tkapp_willdispatch_impl(TkappObject *self) +/*[clinic end generated code: output=0e3f46d244642155 input=7b46376275304a4a]*/ { if (PyErr_WarnEx(PyExc_DeprecationWarning, "willdispatch() is deprecated; consider polling dispatch() instead.", 1)) @@ -3067,33 +3075,39 @@ _tkinter_tkapp_willdispatch_impl(TkappObject* self) /*[clinic input] _tkinter.tkapp.dispatching +Returns the internal dispatching state + +Returns 1 if the mainloop is running, or 0 if the mainloop is not running. + [clinic start generated code]*/ -static PyObject* -_tkinter_tkapp_dispatching_impl(TkappObject* self) -/*[clinic end generated code: output=0e3f46d244642155 input=d88f5970843d6dab]*/ +static PyObject * +_tkinter_tkapp_dispatching_impl(TkappObject *self) +/*[clinic end generated code: output=1b0192766b008005 input=fca22ac098f764ff]*/ { return PyLong_FromLong(self->dispatching); } /*[clinic input] -_tkinter.setmainloopwaitattempts +_tkinter.tkapp.setmainloopwaitattempts new_val: int / -Set number of 100 millisecond mainloop wait attempts that call(), var_invoke(), -and createcommand() will use. +Set number of 100 millisecond mainloop wait attempts. + +These are used for that call(), var_invoke(), and createcommand(). -Current default is 10 for a 1 second wait, but future behavior will be equivalent 0. +Current default is 10 for a 1 second wait, but future behavior +will be equivalent 0. Setting anything other than 0 will trigger a DeprecationWarning. [clinic start generated code]*/ -static PyObject* -_tkinter_tkapp_setmainloopwaitattempts_impl(PyObject* self, int new_val) -/*[clinic end generated code: output=42bf7757dc2d0ab6 input=deca1d6f9e6dae47]*/ +static PyObject * +_tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) +/*[clinic end generated code: output=a0867fb187c8946e input=e5a1636576621896]*/ { if (new_val < 0) { diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index b07052b2741c5e..db8517c5471f75 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -629,67 +629,72 @@ _tkinter_tkapp_loadtk(TkappObject *self, PyObject *Py_UNUSED(ignored)) } PyDoc_STRVAR(_tkinter_tkapp_willdispatch__doc__, - "willdispatch($self, /)\n" - "--\n" - "\n" - "DEPRECATED\n" - "Sets the internal dispatching flag regardless of mainloop dispatch state\n" - "Skips mainloop waiting time\n" - "\n"); +"willdispatch($self, /)\n" +"--\n" +"\n" +"Skips main loop wait polling until next call of mainloop finishes\n" +"\n" +"DEPRECATED\n" +"\n" +"call(), var_invoke(), and createcommand() will wait indefinitely for main loop\n" +"Sets the internal dispatching flag regardless of mainloop dispatch state\n" +"May lead to unexpected hangs or inconsistencies in dispatching"); #define _TKINTER_TKAPP_WILLDISPATCH_METHODDEF \ {"willdispatch", (PyCFunction)_tkinter_tkapp_willdispatch, METH_NOARGS, _tkinter_tkapp_willdispatch__doc__}, -static PyObject* -_tkinter_tkapp_willdispatch_impl(TkappObject* self); +static PyObject * +_tkinter_tkapp_willdispatch_impl(TkappObject *self); -static PyObject* -_tkinter_tkapp_willdispatch(TkappObject* self, PyObject* Py_UNUSED(ignored)) +static PyObject * +_tkinter_tkapp_willdispatch(TkappObject *self, PyObject *Py_UNUSED(ignored)) { return _tkinter_tkapp_willdispatch_impl(self); } PyDoc_STRVAR(_tkinter_tkapp_dispatching__doc__, - "dispatching($self, /)\n" - "--\n" - "\n" - "Returns the internal dispatching state.\n" - "Returns 1 if the mainloop is running, or 0 if the mainloop is not running.\n" - "\n"); +"dispatching($self, /)\n" +"--\n" +"\n" +"Returns the internal dispatching state\n" +"\n" +"Returns 1 if the mainloop is running, or 0 if the mainloop is not running."); #define _TKINTER_TKAPP_DISPATCHING_METHODDEF \ {"dispatching", (PyCFunction)_tkinter_tkapp_dispatching, METH_NOARGS, _tkinter_tkapp_dispatching__doc__}, -static PyObject* -_tkinter_tkapp_dispatching_impl(TkappObject* self); +static PyObject * +_tkinter_tkapp_dispatching_impl(TkappObject *self); -static PyObject* -_tkinter_tkapp_dispatching(TkappObject* self, PyObject* Py_UNUSED(ignored)) +static PyObject * +_tkinter_tkapp_dispatching(TkappObject *self, PyObject *Py_UNUSED(ignored)) { return _tkinter_tkapp_dispatching_impl(self); } PyDoc_STRVAR(_tkinter_tkapp_setmainloopwaitattempts__doc__, - "setmainloopwaitattempts($self, new_val, /)\n" - "--\n" - "\n" - "Set number of 100 ms mainloop wait attempts that call(), var_invoke(), and createcommand() will use.\n" - "\n" - "Current default is 10 for a 1 second wait, but future behavior will default to 0.\n" - "\n" - "Setting anything other than 0 will trigger a DeprecationWarning.\n" - "\n"); +"setmainloopwaitattempts($self, new_val, /)\n" +"--\n" +"\n" +"Set number of 100 millisecond mainloop wait attempts.\n" +"\n" +"These are used for that call(), var_invoke(), and createcommand().\n" +"\n" +"Current default is 10 for a 1 second wait, but future behavior\n" +"will be equivalent 0.\n" +"\n" +"Setting anything other than 0 will trigger a DeprecationWarning."); #define _TKINTER_TKAPP_SETMAINLOOPWAITATTEMPTS_METHODDEF \ {"setmainloopwaitattempts", (PyCFunction)_tkinter_tkapp_setmainloopwaitattempts, METH_O, _tkinter_tkapp_setmainloopwaitattempts__doc__}, -static PyObject* -_tkinter_tkapp_setmainloopwaitattempts_impl(PyObject* self, int new_val); +static PyObject * +_tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val); -static PyObject* -_tkinter_tkapp_setmainloopwaitattempts(PyObject* self, PyObject* arg) +static PyObject * +_tkinter_tkapp_setmainloopwaitattempts(TkappObject *self, PyObject *arg) { - PyObject* return_value = NULL; + PyObject *return_value = NULL; int new_val; new_val = _PyLong_AsInt(arg); @@ -924,4 +929,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=ab311480dd044fe4 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=df5afd93be3a2bb7 input=a9049054013a1b77]*/ From 8e4371fff3a1540818c669f33589523c5187dbad Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 08:10:38 -0400 Subject: [PATCH 06/39] bpo-41176: Address PR comments, add willdispatch test Exceptions raised in the thread will be ignored, so capture the conditions of interest and assert them after joining. Add WaitForMainloop warning on timeout too. --- Lib/tkinter/test/test_tkinter/test_misc.py | 72 ++++++++++++++++------ Modules/_tkinter.c | 16 ++++- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 7b800cea691fce..d35a2824973f26 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -1,5 +1,7 @@ import unittest import tkinter +import threading, time +import time from test import support from tkinter.test.support import AbstractTkTest @@ -196,27 +198,50 @@ def test_mainloop_dispatching(self): # reconstruct default root destroyed by AbstractTkTest root = tkinter._default_root = self.root for obj in (root.tk, root, tkinter): - #mainloop, dispatching, willdispatch self.assertFalse(obj.dispatching()) root.after(0, lambda:self.assertTrue(obj.dispatching())) + + # guarantees mainloop end after first call to Tcl_DoOneEvent root.after(0, root.quit) + root.mainloop() self.assertFalse(obj.dispatching()) + def test_willdispatch(self): + root = self.root + with self.assertWarns(DeprecationWarning): + root.tk.willdispatch() + self.assertTrue(root.dispatching()) + def test_thread_must_wait_for_mainloop(self): - import threading, time + sentinel = object() + thread_properly_raises = sentinel + thread_not_dispatching_early = sentinel + thread_dispatching_eventually = sentinel + def target(): - self.assertFalse(root.dispatching()) - with self.assertRaises(RuntimeError): + nonlocal thread_not_dispatching_early + nonlocal thread_properly_raises + nonlocal thread_dispatching_eventually + thread_not_dispatching_early = not root.dispatching() + + try: root.after(0) # Null op + except RuntimeError: + thread_properly_raises=True + else: + thread_properly_raises=False ready_for_mainloop.set() # self.assertTrue(root.dispatching()) but patient - t = time.monotonic() - while not root.dispatching(): - time.sleep(.001) - self.assertTrue(time.monotonic() < (t + 10)) + for i in range (1000): + if root.dispatching(): + thread_dispatching_eventually = True + break + time.sleep(0.01) + else: # if not break + thread_dispatching_eventually = False root.quit() @@ -225,17 +250,28 @@ def target(): # remove on eventual WaitForMainloop behavior change root.tk.setmainloopwaitattempts(0) - ready_for_mainloop = threading.Event() - thread = threading.Thread(target=target) - self.assertFalse(root.dispatching()) - thread.start() - ready_for_mainloop.wait() - root.mainloop() - self.assertFalse(root.dispatching()) - thread.join() + try: + ready_for_mainloop = threading.Event() + thread = threading.Thread(target=target) + self.assertFalse(root.dispatching()) + thread.start() + ready_for_mainloop.wait() + root.mainloop() + self.assertFalse(root.dispatching()) + thread.join() + finally: + # this global *must* be reset + with self.assertWarns(DeprecationWarning): + root.tk.setmainloopwaitattempts(10) + + for flag in ( + thread_properly_raises, + thread_not_dispatching_early, + thread_dispatching_eventually, + ): + self.assertFalse(flag is sentinel) + self.assertTrue(flag) - with self.assertWarns(DeprecationWarning): - root.tk.setmainloopwaitattempts(10) tests_gui = (MiscTest, ) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index c2d98093f42946..48250332ef5e22 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -390,6 +390,20 @@ WaitForMainloop(TkappObject* self) return 1; } } + if (i > 0) + { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "It seems you are implicitly waiting for " + "the mainloop to come up.\n" + "This behavior is deprecated; consider polling " + "dispatching() instead.\n", 1)) + { + /* return something nonzero, an error will be + raised elsewhere soon */ + return -1; + } + + } PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); return 0; } @@ -3085,7 +3099,7 @@ static PyObject * _tkinter_tkapp_dispatching_impl(TkappObject *self) /*[clinic end generated code: output=1b0192766b008005 input=fca22ac098f764ff]*/ { - return PyLong_FromLong(self->dispatching); + return PyBool_FromLong(self->dispatching); } /*[clinic input] From 3732864d1a4384a7129d5607998cd0a79079a34c Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 08:15:22 -0400 Subject: [PATCH 07/39] bpo-41176: deduplicate warning code --- Modules/_tkinter.c | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 48250332ef5e22..263805f11518bd 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -371,26 +371,7 @@ WaitForMainloop(TkappObject* self) { return 1; } - for (i = 0; i < mainloopwaitattempts; i++) { - Py_BEGIN_ALLOW_THREADS - Sleep(100); - Py_END_ALLOW_THREADS - if (self->dispatching) - { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - "It seems you are implicitly waiting for " - "the mainloop to come up.\n" - "This behavior is deprecated; consider polling " - "dispatching() instead.\n", 1)) - { - /* return something nonzero, an error will be - raised elsewhere soon */ - return -1; - } - return 1; - } - } - if (i > 0) + if (mainloopwaitattempts) { if (PyErr_WarnEx(PyExc_DeprecationWarning, "It seems you are implicitly waiting for " @@ -404,6 +385,15 @@ WaitForMainloop(TkappObject* self) } } + for (i = 0; i < mainloopwaitattempts; i++) { + Py_BEGIN_ALLOW_THREADS + Sleep(100); + Py_END_ALLOW_THREADS + if (self->dispatching) + { + return 1; + } + } PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); return 0; } From 6df6433427c5e69bd75dd6b00ebe472a401c1ef2 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 08:44:32 -0400 Subject: [PATCH 08/39] bpo-41176: rectify typo --- Modules/_tkinter.c | 4 ++-- Modules/clinic/_tkinter.c.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 263805f11518bd..e18836fac12018 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3103,7 +3103,7 @@ Set number of 100 millisecond mainloop wait attempts. These are used for that call(), var_invoke(), and createcommand(). Current default is 10 for a 1 second wait, but future behavior -will be equivalent 0. +will be equivalent to 0. Setting anything other than 0 will trigger a DeprecationWarning. @@ -3111,7 +3111,7 @@ Setting anything other than 0 will trigger a DeprecationWarning. static PyObject * _tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) -/*[clinic end generated code: output=a0867fb187c8946e input=e5a1636576621896]*/ +/*[clinic end generated code: output=a0867fb187c8946e input=c9e2fc3f41fb6f47]*/ { if (new_val < 0) { diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index db8517c5471f75..eabcbcd0a2050b 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -681,7 +681,7 @@ PyDoc_STRVAR(_tkinter_tkapp_setmainloopwaitattempts__doc__, "These are used for that call(), var_invoke(), and createcommand().\n" "\n" "Current default is 10 for a 1 second wait, but future behavior\n" -"will be equivalent 0.\n" +"will be equivalent to 0.\n" "\n" "Setting anything other than 0 will trigger a DeprecationWarning."); @@ -929,4 +929,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=df5afd93be3a2bb7 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=97dfc564114a5e65 input=a9049054013a1b77]*/ From 0f609d7a746e004a013b892c02e39aac5818996b Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 08:45:20 -0400 Subject: [PATCH 09/39] bpo-41176: ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 641ef0cace00e2..183e19313a44bb 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1563,6 +1563,7 @@ Vlad Shcherbina Justin Sheehy Akash Shende Charlie Shepherd +Richard Sheridan Bruce Sherwood Alexander Shigin Pete Shinners From ad89a27b4d75d52531b1cc1da52a29bb26cf119d Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 08:56:29 -0400 Subject: [PATCH 10/39] bpo-41176: fix future behavior Still need to raise if not dispatching --- Modules/_tkinter.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index e18836fac12018..bb36322d1c9540 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -1522,6 +1522,8 @@ Tkapp_Call(PyObject *selfptr, PyObject *args) PyObject *exc_type, *exc_value, *exc_tb; /* After WaitForMainloop deprecation: if (!self->dispatching) + PyErr_SetString(PyExc_RuntimeError, + "main thread is not in main loop"); return NULL; */ if (!WaitForMainloop(self)) return NULL; @@ -1802,6 +1804,8 @@ var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) /* After WaitForMainloop deprecation: if (!self->dispatching) + PyErr_SetString(PyExc_RuntimeError, + "main thread is not in main loop"); return NULL; */ if (!WaitForMainloop(self)) return NULL; @@ -2506,6 +2510,8 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, /*After WaitForMainloop deprecation: if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && !self->dispatching) + PyErr_SetString(PyExc_RuntimeError, + "main thread is not in main loop"); return NULL; */ if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && !WaitForMainloop(self)) From 49ab7122eb586c73344e0bee90e1d0e6878a4d0c Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 09:00:50 -0400 Subject: [PATCH 11/39] bpo-41176: inline thread flag checks Too hard to tell which flag broke if using the loop --- Lib/tkinter/test/test_tkinter/test_misc.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index d35a2824973f26..b72524afb2ec06 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -264,13 +264,12 @@ def target(): with self.assertWarns(DeprecationWarning): root.tk.setmainloopwaitattempts(10) - for flag in ( - thread_properly_raises, - thread_not_dispatching_early, - thread_dispatching_eventually, - ): - self.assertFalse(flag is sentinel) - self.assertTrue(flag) + self.assertFalse(thread_properly_raises is sentinel) + self.assertTrue(thread_properly_raises) + self.assertFalse(thread_not_dispatching_early is sentinel) + self.assertTrue(thread_not_dispatching_early) + self.assertFalse(thread_dispatching_eventually is sentinel) + self.assertTrue(thread_dispatching_eventually) From ba079da4fff80fcfa4ffd82665e987b61a8dc022 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 09:04:34 -0400 Subject: [PATCH 12/39] bpo-41176: internal flag should flip, not just be true --- Lib/tkinter/test/test_tkinter/test_misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index b72524afb2ec06..c406743c6569b0 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -209,6 +209,7 @@ def test_mainloop_dispatching(self): def test_willdispatch(self): root = self.root + self.assertFalse(root.dispatching()) with self.assertWarns(DeprecationWarning): root.tk.willdispatch() self.assertTrue(root.dispatching()) From 7c5d9fb6889675e2dbe0ff081451f3803c2cc5d2 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 09:19:08 -0400 Subject: [PATCH 13/39] bpo-41176: improve dispatching docstring --- Lib/tkinter/__init__.py | 10 ++++++++-- Modules/_tkinter.c | 5 +++-- Modules/clinic/_tkinter.c.h | 5 +++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index e9029bb8a60e74..2f66cc3a6d3e77 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -596,7 +596,10 @@ def mainloop(n=0): def dispatching(): - """Determine if the tkinter main loop is running""" + """Determine if the tkinter main loop is running. + + Returns True if the mainloop is running. + Returns False if the mainloop is not running.""" return _default_root.tk.dispatching() @@ -1427,7 +1430,10 @@ def mainloop(self, n=0): self.tk.mainloop(n) def dispatching(self): - """Determine if the tkinter main loop is running.""" + """Determine if the tkinter main loop is running. + + Returns True if the mainloop is running. + Returns False if the mainloop is not running.""" return self.tk.dispatching() def quit(self): diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index bb36322d1c9540..e7adaaabc7aee1 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3087,13 +3087,14 @@ _tkinter.tkapp.dispatching Returns the internal dispatching state -Returns 1 if the mainloop is running, or 0 if the mainloop is not running. +Returns True if the mainloop is running. +Returns False if the mainloop is not running. [clinic start generated code]*/ static PyObject * _tkinter_tkapp_dispatching_impl(TkappObject *self) -/*[clinic end generated code: output=1b0192766b008005 input=fca22ac098f764ff]*/ +/*[clinic end generated code: output=1b0192766b008005 input=31efce56f25f6c52]*/ { return PyBool_FromLong(self->dispatching); } diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index eabcbcd0a2050b..34588c32945185 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -658,7 +658,8 @@ PyDoc_STRVAR(_tkinter_tkapp_dispatching__doc__, "\n" "Returns the internal dispatching state\n" "\n" -"Returns 1 if the mainloop is running, or 0 if the mainloop is not running."); +"Returns True if the mainloop is running.\n" +"Returns False if the mainloop is not running."); #define _TKINTER_TKAPP_DISPATCHING_METHODDEF \ {"dispatching", (PyCFunction)_tkinter_tkapp_dispatching, METH_NOARGS, _tkinter_tkapp_dispatching__doc__}, @@ -929,4 +930,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=97dfc564114a5e65 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7900bc7b8d8f91b9 input=a9049054013a1b77]*/ From 2f22316ca48843c60ec7cea2bdebfacc4249d803 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 10:25:11 -0400 Subject: [PATCH 14/39] bpo-41176: Update whatsnew --- Doc/whatsnew/3.10.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index a3b53ba48e9b7c..03ab02b64cdfcb 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -117,6 +117,22 @@ Add :data:`sys.orig_argv` attribute: the list of the original command line arguments passed to the Python executable. (Contributed by Victor Stinner in :issue:`23427`.) +tkinter +--- + +Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and +:meth:`_tkinter.tkapp.dispatching`. +You can reliably call these to determine if the tkinter mainloop is running. + +Some internal thread waiting behavior has been deprecated to preserve the +correctness of the new dispatching methods. + +Added another new method :meth:`_tkinter.tkapp.setmainloopwaitattempts` which +can be used to obtain future thread waiting behavior. + +(see also Deprications section) +(Contributed by Richard Sheridan in :issue:`41176`.) + Optimizations ============= @@ -130,6 +146,13 @@ Optimizations Deprecated ========== +The :mod:`tkinter` module has deprected the undocumented behavior where a +thread will implicitly wait up to one second for the main thread to enter +the main loop and then fail. +The undocumented :data:`_tkinter.tkapp.willdispatch` method which sidesteps +this behavior has also been deprecated. +(See also tkinter in the Improved Modules section) +(Contributed by Richard Sheridan in :issue:`41176`.) Removed ======= From 5e9965cd38cef93de590872d0e74e381859eedaa Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 11:29:12 -0400 Subject: [PATCH 15/39] bpo-41176: whitespace --- Doc/whatsnew/3.10.rst | 2 +- Lib/tkinter/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 03ab02b64cdfcb..31fa4473aa0597 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -125,7 +125,7 @@ Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and You can reliably call these to determine if the tkinter mainloop is running. Some internal thread waiting behavior has been deprecated to preserve the -correctness of the new dispatching methods. +correctness of the new dispatching methods. Added another new method :meth:`_tkinter.tkapp.setmainloopwaitattempts` which can be used to obtain future thread waiting behavior. diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 2f66cc3a6d3e77..1a256b8c0f4820 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -597,7 +597,7 @@ def mainloop(n=0): def dispatching(): """Determine if the tkinter main loop is running. - + Returns True if the mainloop is running. Returns False if the mainloop is not running.""" return _default_root.tk.dispatching() @@ -1431,7 +1431,7 @@ def mainloop(self, n=0): def dispatching(self): """Determine if the tkinter main loop is running. - + Returns True if the mainloop is running. Returns False if the mainloop is not running.""" return self.tk.dispatching() From 5872f5bee120708fcfb24049ed8b79838a4a5f4d Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2020 15:38:27 +0000 Subject: [PATCH 16/39] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2020-07-05-15-38-26.bpo-41176.Q5Ua74.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2020-07-05-15-38-26.bpo-41176.Q5Ua74.rst diff --git a/Misc/NEWS.d/next/Library/2020-07-05-15-38-26.bpo-41176.Q5Ua74.rst b/Misc/NEWS.d/next/Library/2020-07-05-15-38-26.bpo-41176.Q5Ua74.rst new file mode 100644 index 00000000000000..5a629bcfe55e36 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-07-05-15-38-26.bpo-41176.Q5Ua74.rst @@ -0,0 +1,6 @@ +Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and :meth:`_tkinter.tkapp.dispatching`. +These methods may be used to query if the :mod:`tkinter` mainloop is running +Added :meth:`_tkinter.tkapp.setmainloopwaitattempts`. +Deprecated :meth:`_tkinter.tkapp.willdispatch`. +Deprecated undocumented :mod:`tkinter` thread mainloop waiting behavior. +Patch by Richard Sheridan \ No newline at end of file From d1307409e73c83e0dfb1049f138838a54458161d Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 5 Jul 2020 11:39:47 -0400 Subject: [PATCH 17/39] bpo-41176: data to method --- Doc/whatsnew/3.10.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 31fa4473aa0597..cc8044f879c537 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -149,7 +149,7 @@ Deprecated The :mod:`tkinter` module has deprected the undocumented behavior where a thread will implicitly wait up to one second for the main thread to enter the main loop and then fail. -The undocumented :data:`_tkinter.tkapp.willdispatch` method which sidesteps +The undocumented :meth:`_tkinter.tkapp.willdispatch` method which sidesteps this behavior has also been deprecated. (See also tkinter in the Improved Modules section) (Contributed by Richard Sheridan in :issue:`41176`.) From 648434fc7130c2f5f1b8958f30c2d18c6ef79d1e Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Mon, 6 Jul 2020 07:41:15 -0400 Subject: [PATCH 18/39] bpo-41176: fix future behavior brackets --- Modules/_tkinter.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index e7adaaabc7aee1..7be56a169a419e 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -1521,10 +1521,10 @@ Tkapp_Call(PyObject *selfptr, PyObject *args) Tcl_Condition cond = NULL; PyObject *exc_type, *exc_value, *exc_tb; /* After WaitForMainloop deprecation: - if (!self->dispatching) + if (!self->dispatching) { PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); - return NULL; */ + return NULL; } */ if (!WaitForMainloop(self)) return NULL; ev = (Tkapp_CallEvent*)attemptckalloc(sizeof(Tkapp_CallEvent)); @@ -1803,10 +1803,10 @@ var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) completion. */ /* After WaitForMainloop deprecation: - if (!self->dispatching) + if (!self->dispatching) { PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); - return NULL; */ + return NULL; } */ if (!WaitForMainloop(self)) return NULL; @@ -2509,10 +2509,10 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, /*After WaitForMainloop deprecation: if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && - !self->dispatching) + !self->dispatching) { PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); - return NULL; */ + return NULL; } */ if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && !WaitForMainloop(self)) return NULL; From 44fe5fa7de3703d531b4349cd505cb94c290fc30 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Mon, 6 Jul 2020 13:24:38 -0400 Subject: [PATCH 19/39] bpo-41176: Make dispatching return an int In preparation for supporting multiple dispatchers with an IntEnum --- Modules/_tkinter.c | 13 +++++++------ Modules/clinic/_tkinter.c.h | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 7be56a169a419e..8252cf66525cee 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -2900,6 +2900,7 @@ _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) PyThreadState *tstate = PyThreadState_Get(); CHECK_TCL_APPARTMENT; + int previous_dispatching = self->dispatching; self->dispatching = 1; quitMainLoop = 0; @@ -2928,13 +2929,13 @@ _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) } if (PyErr_CheckSignals() != 0) { - self->dispatching = 0; + self->dispatching = previous_dispatching; return NULL; } if (result < 0) break; } - self->dispatching = 0; + self->dispatching = previous_dispatching; quitMainLoop = 0; if (errorInCmd) { @@ -3087,16 +3088,16 @@ _tkinter.tkapp.dispatching Returns the internal dispatching state -Returns True if the mainloop is running. -Returns False if the mainloop is not running. +Returns 0 if the mainloop is running. +Returns 1 if the mainloop is not running. [clinic start generated code]*/ static PyObject * _tkinter_tkapp_dispatching_impl(TkappObject *self) -/*[clinic end generated code: output=1b0192766b008005 input=31efce56f25f6c52]*/ +/*[clinic end generated code: output=1b0192766b008005 input=25c6787f7a5f03f5]*/ { - return PyBool_FromLong(self->dispatching); + return PyLong_FromLong(self->dispatching); } /*[clinic input] diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 34588c32945185..07d2400d98c39a 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -658,8 +658,8 @@ PyDoc_STRVAR(_tkinter_tkapp_dispatching__doc__, "\n" "Returns the internal dispatching state\n" "\n" -"Returns True if the mainloop is running.\n" -"Returns False if the mainloop is not running."); +"Returns 0 if the mainloop is running.\n" +"Returns 1 if the mainloop is not running."); #define _TKINTER_TKAPP_DISPATCHING_METHODDEF \ {"dispatching", (PyCFunction)_tkinter_tkapp_dispatching, METH_NOARGS, _tkinter_tkapp_dispatching__doc__}, @@ -930,4 +930,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=7900bc7b8d8f91b9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=111b16d638d0424b input=a9049054013a1b77]*/ From fbddd0f73e34086a51642640a185ab7c5fedf0ef Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Mon, 6 Jul 2020 17:47:45 -0400 Subject: [PATCH 20/39] bpo-41176: attempt to expose CI error failing with timeout gives no helpful debugging info, so try to eliminate all possible hangs --- Lib/tkinter/test/test_tkinter/test_misc.py | 60 +++++++++++++--------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index c406743c6569b0..105f984b562d37 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -217,34 +217,45 @@ def test_willdispatch(self): def test_thread_must_wait_for_mainloop(self): sentinel = object() thread_properly_raises = sentinel - thread_not_dispatching_early = sentinel + thread_dispatching_early = sentinel thread_dispatching_eventually = sentinel def target(): - nonlocal thread_not_dispatching_early + nonlocal thread_dispatching_early nonlocal thread_properly_raises nonlocal thread_dispatching_eventually - thread_not_dispatching_early = not root.dispatching() try: + thread_dispatching_early = root.dispatching() root.after(0) # Null op - except RuntimeError: - thread_properly_raises=True + except RuntimeError as e: + if str(e) == "main thread is not in main loop": + thread_properly_raises=True + else: + thread_properly_raises=False + raise else: thread_properly_raises=False - - ready_for_mainloop.set() - - # self.assertTrue(root.dispatching()) but patient - for i in range (1000): - if root.dispatching(): - thread_dispatching_eventually = True - break - time.sleep(0.01) - else: # if not break - thread_dispatching_eventually = False - - root.quit() + return + finally: + # must guarantee that any reason not to run mainloop + # is flagged in the above try/except/else and will + # keep the main thread from calling root.mainloop() + ready_for_mainloop.set() + + # Everything after the event set must go in this try + # to guarantee that root.quit() is called in finally + try: + # self.assertTrue(root.dispatching()) but patient + for i in range (1000): + if root.dispatching(): + thread_dispatching_eventually = True + break + time.sleep(0.01) + else: # if not break + thread_dispatching_eventually = False + finally: + root.quit() root = self.root @@ -253,10 +264,17 @@ def target(): try: ready_for_mainloop = threading.Event() - thread = threading.Thread(target=target) + thread = threading.Thread(target=target, daemon=True) self.assertFalse(root.dispatching()) thread.start() ready_for_mainloop.wait() + + # if these fail we don't want to risk starting mainloop + self.assertFalse(thread_dispatching_early is sentinel) + self.assertFalse(thread_dispatching_early) + self.assertFalse(thread_properly_raises is sentinel) + self.assertTrue(thread_properly_raises) + root.mainloop() self.assertFalse(root.dispatching()) thread.join() @@ -265,10 +283,6 @@ def target(): with self.assertWarns(DeprecationWarning): root.tk.setmainloopwaitattempts(10) - self.assertFalse(thread_properly_raises is sentinel) - self.assertTrue(thread_properly_raises) - self.assertFalse(thread_not_dispatching_early is sentinel) - self.assertTrue(thread_not_dispatching_early) self.assertFalse(thread_dispatching_eventually is sentinel) self.assertTrue(thread_dispatching_eventually) From b58f8a160cff8dced4173b62f1c338eb9d8bdc1b Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Mon, 6 Jul 2020 18:10:33 -0400 Subject: [PATCH 21/39] bpo-41176: revert internal memory of previous dispatching Seems to cause CI to never exit mainloop at least on Ubuntu. No idea why yet. --- Modules/_tkinter.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 8252cf66525cee..45b0a5cd75c05a 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -2900,7 +2900,6 @@ _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) PyThreadState *tstate = PyThreadState_Get(); CHECK_TCL_APPARTMENT; - int previous_dispatching = self->dispatching; self->dispatching = 1; quitMainLoop = 0; @@ -2929,13 +2928,13 @@ _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) } if (PyErr_CheckSignals() != 0) { - self->dispatching = previous_dispatching; + self->dispatching = 0; return NULL; } if (result < 0) break; } - self->dispatching = previous_dispatching; + self->dispatching = 0; quitMainLoop = 0; if (errorInCmd) { From ad7d6ec32e5644285b66c6f2d5c51f7633c4dc5b Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Tue, 7 Jul 2020 08:03:22 -0400 Subject: [PATCH 22/39] bpo-41176: Attempt Ubuntu CI fix Co-Authored-By: E-Paine --- Lib/tkinter/test/support.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/tkinter/test/support.py b/Lib/tkinter/test/support.py index 1220854205917b..bbd0bdaef4d396 100644 --- a/Lib/tkinter/test/support.py +++ b/Lib/tkinter/test/support.py @@ -19,6 +19,7 @@ def setUpClass(cls): cls.root.wm_attributes('-zoomed', False) except tkinter.TclError: pass + cls.root.focus_force() @classmethod def tearDownClass(cls): From a5bb18034943165cf7c7f24967ba0991aa50d95d Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Tue, 7 Jul 2020 15:51:26 -0400 Subject: [PATCH 23/39] bpo-41176: Attempt Ubuntu CI fix quit may not quit if no events are forthcoming, but after creates its own event to dispatch while calling quit. Co-Authored-By: E-Paine --- Lib/tkinter/__init__.py | 7 ++++++- Lib/tkinter/test/support.py | 1 - Lib/tkinter/test/test_tkinter/test_misc.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 1a256b8c0f4820..64256d2224f493 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1437,7 +1437,12 @@ def dispatching(self): return self.tk.dispatching() def quit(self): - """Quit the Tcl interpreter. All widgets will be destroyed.""" + """Signal the Tkinter mainloop to stop dispatching. + + The mainloop will exit AFTER THE NEXT Tcl event handler is called. + If no events are forthcoming, mainloop will hang even if quit + has been called. In this case, call after(0, quit). + """ self.tk.quit() def _getints(self, string): diff --git a/Lib/tkinter/test/support.py b/Lib/tkinter/test/support.py index bbd0bdaef4d396..1220854205917b 100644 --- a/Lib/tkinter/test/support.py +++ b/Lib/tkinter/test/support.py @@ -19,7 +19,6 @@ def setUpClass(cls): cls.root.wm_attributes('-zoomed', False) except tkinter.TclError: pass - cls.root.focus_force() @classmethod def tearDownClass(cls): diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 105f984b562d37..98bb4b1a767670 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -255,7 +255,7 @@ def target(): else: # if not break thread_dispatching_eventually = False finally: - root.quit() + root.after(0, root.quit) root = self.root From 2d557c563cb72af55ab2c945cc378a6f8b0c12de Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Wed, 8 Jul 2020 08:57:21 -0400 Subject: [PATCH 24/39] bpo-41176: Quit, mainloop, dispatch docstrings --- Lib/tkinter/__init__.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 64256d2224f493..f81aa4a1aadf77 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1426,22 +1426,25 @@ def unbind_class(self, className, sequence): self.tk.call('bind', className , sequence, '') def mainloop(self, n=0): - """Call the main loop of tkinter.""" + """Call the main loop of Tkinter.""" self.tk.mainloop(n) def dispatching(self): - """Determine if the tkinter main loop is running. + """Determine if the Tkinter main loop is running. - Returns True if the mainloop is running. - Returns False if the mainloop is not running.""" + Returns True if the main loop is running. + Returns False if the main loop is not running.""" return self.tk.dispatching() def quit(self): - """Signal the Tkinter mainloop to stop dispatching. - - The mainloop will exit AFTER THE NEXT Tcl event handler is called. - If no events are forthcoming, mainloop will hang even if quit - has been called. In this case, call after(0, quit). + """Signal the Tkinter main loop to stop dispatching. + + The main loop will exit AFTER the current Tcl event handler is + finished calling. If quit is called outside the context of a Tcl + event, for example from a thread, the main loop will not exit + until after the NEXT event is dispatched. If no more events are + forthcoming, main loop will keep blocking even if quit has been + called. In that case, call after(0, quit) instead. """ self.tk.quit() From ff39387d8f95cabeb8672ed4c56e302aa09b1b18 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Thu, 9 Jul 2020 09:05:12 -0400 Subject: [PATCH 25/39] bpo-41176: WIP Docs in tkinter.rst --- Doc/library/tkinter.rst | 72 ++++++++++++++++++++++++++++++++++++----- Lib/tkinter/__init__.py | 16 ++++----- 2 files changed, 72 insertions(+), 16 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 2dc44ad36a7f73..1c7979dbc88d3d 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -70,10 +70,7 @@ Tkinter Modules Most of the time, :mod:`tkinter` is all you really need, but a number of additional modules are available as well. The Tk interface is located in a -binary module named :mod:`_tkinter`. This module contains the low-level -interface to Tk, and should never be used directly by application programmers. -It is usually a shared library (or DLL), but might in some cases be statically -linked with the Python interpreter. +binary module named :mod:`_tkinter`. In addition to the Tk interface module, :mod:`tkinter` includes a number of Python modules, :mod:`tkinter.constants` being one of the most important. @@ -82,10 +79,6 @@ so, usually, to use Tkinter all you need is a simple import statement:: import tkinter -Or, more often:: - - from tkinter import * - .. class:: Tk(screenName=None, baseName=None, className='Tk', useTk=1) @@ -95,6 +88,29 @@ Or, more often:: .. FIXME: The following keyword arguments are currently recognized: +.. method:: Tk.mainloop(threshold) + + Enters the main loop of Tkinter. This repeatedly dispatches Tcl events until either + :meth:`Tk.quit` is called, the number of open windows drops below `threshold`, or + an error occurs while executing events. Usually the default threshold of 0 is + appropriate. + +.. method:: Tk.quit() + + Signals the Tkinter main loop to stop dispatching. + The main loop will exit AFTER the current Tcl event handler is + finished calling. If quit is called outside the context of a Tcl + event, for example from a thread, the main loop will not exit + until after the NEXT event is dispatched. If no more events are + forthcoming, main loop will keep blocking even if quit has been + called. In that case, call `Tk.after(0, Tk.quit)` instead. + +.. method:: Tk.dispatching() + + Determines if the Tkinter main loop is running. Returns True if the main loop is running, or + returns False if the main loop is not running. It is possible for some entity other than + the main loop to be dispatching events. Some examples are: calling the `update` command, + :meth:`_tkinter.tkapp.doonevent`, and the python command line `EventHook`. .. function:: Tcl(screenName=None, baseName=None, className='Tk', useTk=0) @@ -106,6 +122,46 @@ Or, more often:: created by the :func:`Tcl` object can have a Toplevel window created (and the Tk subsystem initialized) by calling its :meth:`loadtk` method. +.. TODO: Not a class, a module. how do I inline another module into this RST file? + +.. class:: _tkinter + + This module contains the low-level interface to Tk, and should rarely be + used directly by application programmers. It is usually a shared library + (or DLL), but might in some cases be statically linked with the Python + interpreter. + +.. method:: _tkinter.create(screenName=None, baseName="", className="Tk", interactive=False, wantobjects=False, wantTk=True, sync=False, use=None) + + This method is not for general use, but it is responsible for creating the + :class:`_tkinter.tkapp` object that can be found as the attribute + :attr:`Tk.tk`. + +.. class:: _tkinter.tkapp + + This object contains most of the low-level interface to Tk, and should + rarely be used directly by application programmers. + +.. method:: _tkinter.tkapp.call(*args) + + This method is the core entry point to Tcl/Tk. All Tkinter methods that + correspond to Tcl commands flow through here. This function is thread-safe, + meaning it can be used to issue commands to the Tcl interpreter in the main + thread from any other thread. If called from a thread, it will wait up to + one second for :meth:`Tk.mainloop` to be called, and then throw an error. + +.. method:: _tkinter.tkapp.willdispatch() + + This method circumvents the thread waiting behavior of :meth:`_tkinter.tkapp.call` + until after the next call to :meth:`Tk.mainloop` returns. Instead of throwing + an error after one second, :meth:`_tkinter.tkapp.call` will wait indefinitely + for :meth:`Tk.mainloop` to come up, or for some other entity to dispatch the + call. This method is essentially a promise to the thread that you "will dispatch" soon. + +.. method:: _tkinter.tkapp.setmainloopwaitattempts(num=10) + + This method sets the number of 100 ms wait attempts of :meth:`_tkinter.tkapp.call`. + You can trigger an error immediately by setting `num` to 0. Other modules that provide Tk support include: diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index f81aa4a1aadf77..b064e64db32f7d 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -590,16 +590,16 @@ def get(self): raise ValueError("invalid literal for getboolean()") -def mainloop(n=0): - """Run the main loop of tkinter.""" - _default_root.tk.mainloop(n) +def mainloop(threshold=0): + """Call the main loop of Tkinter.""" + _default_root.tk.mainloop(threshold) def dispatching(): - """Determine if the tkinter main loop is running. + """Determine if the Tkinter main loop is running. - Returns True if the mainloop is running. - Returns False if the mainloop is not running.""" + Returns True if the main loop is running. + Returns False if the main loop is not running.""" return _default_root.tk.dispatching() @@ -1425,9 +1425,9 @@ def unbind_class(self, className, sequence): all functions.""" self.tk.call('bind', className , sequence, '') - def mainloop(self, n=0): + def mainloop(self, threshold=0): """Call the main loop of Tkinter.""" - self.tk.mainloop(n) + self.tk.mainloop(threshold) def dispatching(self): """Determine if the Tkinter main loop is running. From 8ea6c5fe8ee38d2395ea9b2e410ffe62cec7cf5a Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Thu, 9 Jul 2020 13:00:20 -0400 Subject: [PATCH 26/39] bpo-41176: satisfy Doc make check --- Doc/library/tkinter.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 1c7979dbc88d3d..e16392dd5fc389 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -70,7 +70,7 @@ Tkinter Modules Most of the time, :mod:`tkinter` is all you really need, but a number of additional modules are available as well. The Tk interface is located in a -binary module named :mod:`_tkinter`. +binary module named :mod:`_tkinter`. In addition to the Tk interface module, :mod:`tkinter` includes a number of Python modules, :mod:`tkinter.constants` being one of the most important. @@ -109,8 +109,8 @@ so, usually, to use Tkinter all you need is a simple import statement:: Determines if the Tkinter main loop is running. Returns True if the main loop is running, or returns False if the main loop is not running. It is possible for some entity other than - the main loop to be dispatching events. Some examples are: calling the `update` command, - :meth:`_tkinter.tkapp.doonevent`, and the python command line `EventHook`. + the main loop to be dispatching events. Some examples are: calling the :meth:`Tk.update` command, + :meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook. .. function:: Tcl(screenName=None, baseName=None, className='Tk', useTk=0) @@ -129,7 +129,7 @@ so, usually, to use Tkinter all you need is a simple import statement:: This module contains the low-level interface to Tk, and should rarely be used directly by application programmers. It is usually a shared library (or DLL), but might in some cases be statically linked with the Python - interpreter. + interpreter. .. method:: _tkinter.create(screenName=None, baseName="", className="Tk", interactive=False, wantobjects=False, wantTk=True, sync=False, use=None) @@ -161,7 +161,7 @@ so, usually, to use Tkinter all you need is a simple import statement:: .. method:: _tkinter.tkapp.setmainloopwaitattempts(num=10) This method sets the number of 100 ms wait attempts of :meth:`_tkinter.tkapp.call`. - You can trigger an error immediately by setting `num` to 0. + You can trigger an error immediately by setting num to 0. Other modules that provide Tk support include: From e7a0611110c764a489dfcdaae4f6407b119d09c4 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 09:46:25 -0400 Subject: [PATCH 27/39] bpo-41176: use local max attempts variable prevent subsequent calls to setmainloopwaitattempts from stepping on the behavior of current WaitForMainloop calls. --- Modules/_tkinter.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 45b0a5cd75c05a..117ab5696fd1c7 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -366,12 +366,13 @@ static int mainloopwaitattempts = 10; // 1 second static int WaitForMainloop(TkappObject* self) { - int i; + int i = 0; + int max_attempts = mainloopwaitattempts; if (self->dispatching) { return 1; } - if (mainloopwaitattempts) + if (max_attempts) { if (PyErr_WarnEx(PyExc_DeprecationWarning, "It seems you are implicitly waiting for " @@ -385,7 +386,7 @@ WaitForMainloop(TkappObject* self) } } - for (i = 0; i < mainloopwaitattempts; i++) { + for (i = 0; i < max_attempts; i++) { Py_BEGIN_ALLOW_THREADS Sleep(100); Py_END_ALLOW_THREADS From 3165806b85eb66772d7bf733a6e5bc5aaffda6ec Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 09:52:04 -0400 Subject: [PATCH 28/39] bpo-41176: dispatching returns PyBool --- Modules/_tkinter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 117ab5696fd1c7..545f00865ebabf 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3097,7 +3097,7 @@ static PyObject * _tkinter_tkapp_dispatching_impl(TkappObject *self) /*[clinic end generated code: output=1b0192766b008005 input=25c6787f7a5f03f5]*/ { - return PyLong_FromLong(self->dispatching); + return PyBool_FromLong(self->dispatching); } /*[clinic input] From 3912603f341e05e9eef42b78ee80c260e3f1a632 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 09:53:54 -0400 Subject: [PATCH 29/39] bpo-41176: update docstrings --- Lib/tkinter/__init__.py | 12 +++++++++--- Modules/_tkinter.c | 24 +++++++++++++++++------- Modules/clinic/_tkinter.c.h | 13 ++++++++----- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index b064e64db32f7d..bb940cf7dd64f8 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -599,7 +599,10 @@ def dispatching(): """Determine if the Tkinter main loop is running. Returns True if the main loop is running. - Returns False if the main loop is not running.""" + Returns False if the main loop is not running. + + NOTE: Using update will dispatch events without the main + loop. Dispatching will return False in these cases.""" return _default_root.tk.dispatching() @@ -1433,7 +1436,10 @@ def dispatching(self): """Determine if the Tkinter main loop is running. Returns True if the main loop is running. - Returns False if the main loop is not running.""" + Returns False if the main loop is not running. + + NOTE: Using update will dispatch events without the main + loop. Dispatching will return False in these cases.""" return self.tk.dispatching() def quit(self): @@ -1444,7 +1450,7 @@ def quit(self): event, for example from a thread, the main loop will not exit until after the NEXT event is dispatched. If no more events are forthcoming, main loop will keep blocking even if quit has been - called. In that case, call after(0, quit) instead. + called. In that case, have the thread call after(0, quit) instead. """ self.tk.quit() diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 545f00865ebabf..69e9b2628af315 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3088,14 +3088,17 @@ _tkinter.tkapp.dispatching Returns the internal dispatching state -Returns 0 if the mainloop is running. -Returns 1 if the mainloop is not running. +Returns True if the main loop is running. +Returns False if the main loop is not running. + +NOTE: Using update or dooneevent will dispatch events without the main + loop. Dispatching will return False in these cases. [clinic start generated code]*/ static PyObject * _tkinter_tkapp_dispatching_impl(TkappObject *self) -/*[clinic end generated code: output=1b0192766b008005 input=25c6787f7a5f03f5]*/ +/*[clinic end generated code: output=1b0192766b008005 input=ff427bd1ca93bac4]*/ { return PyBool_FromLong(self->dispatching); } @@ -3111,16 +3114,23 @@ Set number of 100 millisecond mainloop wait attempts. These are used for that call(), var_invoke(), and createcommand(). Current default is 10 for a 1 second wait, but future behavior -will be equivalent to 0. +will be equivalent to an unlimited number. -Setting anything other than 0 will trigger a DeprecationWarning. +Setting this will trigger a DeprecationWarning. [clinic start generated code]*/ static PyObject * _tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) -/*[clinic end generated code: output=a0867fb187c8946e input=c9e2fc3f41fb6f47]*/ +/*[clinic end generated code: output=a0867fb187c8946e input=9965550e84594ac4]*/ { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "In future behavior, threads will wait indefinitely\n" + "for the main thread to dispatch. Consider polling dispatch()\n" + "before issuing commands from a thread instead.", 1)) + { + return NULL; + } if (new_val < 0) { PyErr_SetString(PyExc_ValueError, @@ -3131,7 +3141,7 @@ _tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) { if (PyErr_WarnEx(PyExc_DeprecationWarning, "It seems you want to wait for the mainloop to come up.\n" - "This behavior is deprecated; consider polling dispatch() instead.", 1)) + "This behavior is deprecated; consider polling dispatching() instead.", 1)) { return NULL; } diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 07d2400d98c39a..0bf8044f0ae7ac 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -658,8 +658,11 @@ PyDoc_STRVAR(_tkinter_tkapp_dispatching__doc__, "\n" "Returns the internal dispatching state\n" "\n" -"Returns 0 if the mainloop is running.\n" -"Returns 1 if the mainloop is not running."); +"Returns True if the main loop is running.\n" +"Returns False if the main loop is not running.\n" +"\n" +"NOTE: Using update or dooneevent will dispatch events without the main\n" +" loop. Dispatching will return False in these cases."); #define _TKINTER_TKAPP_DISPATCHING_METHODDEF \ {"dispatching", (PyCFunction)_tkinter_tkapp_dispatching, METH_NOARGS, _tkinter_tkapp_dispatching__doc__}, @@ -682,9 +685,9 @@ PyDoc_STRVAR(_tkinter_tkapp_setmainloopwaitattempts__doc__, "These are used for that call(), var_invoke(), and createcommand().\n" "\n" "Current default is 10 for a 1 second wait, but future behavior\n" -"will be equivalent to 0.\n" +"will be equivalent to an unlimited number.\n" "\n" -"Setting anything other than 0 will trigger a DeprecationWarning."); +"Setting this will trigger a DeprecationWarning."); #define _TKINTER_TKAPP_SETMAINLOOPWAITATTEMPTS_METHODDEF \ {"setmainloopwaitattempts", (PyCFunction)_tkinter_tkapp_setmainloopwaitattempts, METH_O, _tkinter_tkapp_setmainloopwaitattempts__doc__}, @@ -930,4 +933,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=111b16d638d0424b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=050579715b59903d input=a9049054013a1b77]*/ From 4d81d593342bf9ce49e4deba42ed16276a7dda8d Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 09:57:14 -0400 Subject: [PATCH 30/39] bpo-41176: remove old deprecation warning --- Modules/_tkinter.c | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 69e9b2628af315..e89ab792aecc56 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3137,15 +3137,6 @@ _tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) "mainloopwaitattempts must be >= 0"); return NULL; } - if (new_val) - { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - "It seems you want to wait for the mainloop to come up.\n" - "This behavior is deprecated; consider polling dispatching() instead.", 1)) - { - return NULL; - } - } mainloopwaitattempts = new_val; Py_RETURN_NONE; } From 1f16b8040d676c7c6f4940a128fcd85b8e8ee981 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 10:12:06 -0400 Subject: [PATCH 31/39] bpo-41176: update test with new deprecation and new future behavior --- Lib/tkinter/test/test_tkinter/test_misc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 98bb4b1a767670..0f89baa52038e3 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -215,6 +215,7 @@ def test_willdispatch(self): self.assertTrue(root.dispatching()) def test_thread_must_wait_for_mainloop(self): + # remove test on eventual WaitForMainloop removal sentinel = object() thread_properly_raises = sentinel thread_dispatching_early = sentinel @@ -259,8 +260,8 @@ def target(): root = self.root - # remove on eventual WaitForMainloop behavior change - root.tk.setmainloopwaitattempts(0) + with self.assertWarns(DeprecationWarning): + root.tk.setmainloopwaitattempts(0) try: ready_for_mainloop = threading.Event() From 4ec839ae405fe7fb1fde9c09ed87849cd2098233 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 10:19:57 -0400 Subject: [PATCH 32/39] bpo-41176: update Docs --- Doc/library/tkinter.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index e16392dd5fc389..3d156fcd7ec443 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -157,11 +157,20 @@ so, usually, to use Tkinter all you need is a simple import statement:: an error after one second, :meth:`_tkinter.tkapp.call` will wait indefinitely for :meth:`Tk.mainloop` to come up, or for some other entity to dispatch the call. This method is essentially a promise to the thread that you "will dispatch" soon. + This method is deprecated and future behavior will be equivialent to calling this + function on load and after each invocation of :meth:`Tk.mainloop`. .. method:: _tkinter.tkapp.setmainloopwaitattempts(num=10) This method sets the number of 100 ms wait attempts of :meth:`_tkinter.tkapp.call`. - You can trigger an error immediately by setting num to 0. + You can trigger an error immediately by setting num to 0. This method is deprecated + and future behavior will be equivalent to setting an infinite number of attempts. + +.. method:: _tkinter.tkapp.dooneevent(flags=0) + + Dispatches exactly one event from the Tcl event queue. Must be called from the same + thread as the Tcl interpreter. See Tcl documentation for Tcl_DoOneEvent() for information + on using the flags argument. Other modules that provide Tk support include: From a5e4a8603c73a9b6823aa6b26d5b37f02977edec Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 12:32:55 -0400 Subject: [PATCH 33/39] bpo-41176: Comment new post-deprecation plan --- Modules/_tkinter.c | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index e89ab792aecc56..cb16d5646e21a8 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -1521,11 +1521,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args) Tkapp_CallEvent *ev; Tcl_Condition cond = NULL; PyObject *exc_type, *exc_value, *exc_tb; - /* After WaitForMainloop deprecation: - if (!self->dispatching) { - PyErr_SetString(PyExc_RuntimeError, - "main thread is not in main loop"); - return NULL; } */ + /* Remove after WaitForMainloop deprecation */ if (!WaitForMainloop(self)) return NULL; ev = (Tkapp_CallEvent*)attemptckalloc(sizeof(Tkapp_CallEvent)); @@ -1803,11 +1799,7 @@ var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) the call to the interpreter thread, then wait for completion. */ - /* After WaitForMainloop deprecation: - if (!self->dispatching) { - PyErr_SetString(PyExc_RuntimeError, - "main thread is not in main loop"); - return NULL; } */ + /* Remove after WaitForMainloop deprecation*/ if (!WaitForMainloop(self)) return NULL; @@ -2508,12 +2500,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, return NULL; } - /*After WaitForMainloop deprecation: - if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && - !self->dispatching) { - PyErr_SetString(PyExc_RuntimeError, - "main thread is not in main loop"); - return NULL; } */ + /* Remove after WaitForMainloop deprecation */ if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && !WaitForMainloop(self)) return NULL; From 8efe3feead82fecc8e79cce5ef9c53016b4a560d Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sat, 11 Jul 2020 13:10:08 -0400 Subject: [PATCH 34/39] bpo-41176: New behavior plan: Doc and Warn fixes --- Doc/library/tkinter.rst | 3 +++ Modules/_tkinter.c | 33 ++++++++++++++------------------- Modules/clinic/_tkinter.c.h | 4 ++-- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 3d156fcd7ec443..bfbe7af403774b 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -149,6 +149,9 @@ so, usually, to use Tkinter all you need is a simple import statement:: meaning it can be used to issue commands to the Tcl interpreter in the main thread from any other thread. If called from a thread, it will wait up to one second for :meth:`Tk.mainloop` to be called, and then throw an error. + This error after one second is deprecated behavior, in the future it will + wait indefinitely for :meth:`Tk.mainloop`, :meth:`Tk.update`, or + :meth:`_tkinter.tkapp.dooneevent` to dispatch the event. .. method:: _tkinter.tkapp.willdispatch() diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index cb16d5646e21a8..61fd1d33371533 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -372,20 +372,6 @@ WaitForMainloop(TkappObject* self) { return 1; } - if (max_attempts) - { - if (PyErr_WarnEx(PyExc_DeprecationWarning, - "It seems you are implicitly waiting for " - "the mainloop to come up.\n" - "This behavior is deprecated; consider polling " - "dispatching() instead.\n", 1)) - { - /* return something nonzero, an error will be - raised elsewhere soon */ - return -1; - } - - } for (i = 0; i < max_attempts; i++) { Py_BEGIN_ALLOW_THREADS Sleep(100); @@ -395,6 +381,14 @@ WaitForMainloop(TkappObject* self) return 1; } } + PyErr_WarnEx(PyExc_DeprecationWarning, + "It seems you are waiting for Tcl events " + "to be dispatched.\n" + "Future behavior will wait indefinitely. Adjust the wait time " + "limit with setmainloopwaitattempts().\n" + "If you want to avoid this wait consider polling " + "dispatching() before issuing commands from a thread.\n", 1 + ); PyErr_SetString(PyExc_RuntimeError, "main thread is not in main loop"); return 0; } @@ -3061,7 +3055,7 @@ _tkinter_tkapp_willdispatch_impl(TkappObject *self) /*[clinic end generated code: output=0e3f46d244642155 input=7b46376275304a4a]*/ { if (PyErr_WarnEx(PyExc_DeprecationWarning, - "willdispatch() is deprecated; consider polling dispatch() instead.", 1)) + "willdispatch() is deprecated; in the future it will have no effect.", 1)) { return NULL; } @@ -3098,7 +3092,7 @@ _tkinter.tkapp.setmainloopwaitattempts Set number of 100 millisecond mainloop wait attempts. -These are used for that call(), var_invoke(), and createcommand(). +These are used for threads that call(), var_invoke(), and createcommand(). Current default is 10 for a 1 second wait, but future behavior will be equivalent to an unlimited number. @@ -3109,11 +3103,12 @@ Setting this will trigger a DeprecationWarning. static PyObject * _tkinter_tkapp_setmainloopwaitattempts_impl(TkappObject *self, int new_val) -/*[clinic end generated code: output=a0867fb187c8946e input=9965550e84594ac4]*/ +/*[clinic end generated code: output=a0867fb187c8946e input=2b60ed355c8d7fac]*/ { if (PyErr_WarnEx(PyExc_DeprecationWarning, - "In future behavior, threads will wait indefinitely\n" - "for the main thread to dispatch. Consider polling dispatch()\n" + "In future behavior, threads will wait indefinitely " + "for the main thread to dispatch.\n" + "If you want to avoid delays, consider polling dispatching()" "before issuing commands from a thread instead.", 1)) { return NULL; diff --git a/Modules/clinic/_tkinter.c.h b/Modules/clinic/_tkinter.c.h index 0bf8044f0ae7ac..3a49ae0ace0f2c 100644 --- a/Modules/clinic/_tkinter.c.h +++ b/Modules/clinic/_tkinter.c.h @@ -682,7 +682,7 @@ PyDoc_STRVAR(_tkinter_tkapp_setmainloopwaitattempts__doc__, "\n" "Set number of 100 millisecond mainloop wait attempts.\n" "\n" -"These are used for that call(), var_invoke(), and createcommand().\n" +"These are used for threads that call(), var_invoke(), and createcommand().\n" "\n" "Current default is 10 for a 1 second wait, but future behavior\n" "will be equivalent to an unlimited number.\n" @@ -933,4 +933,4 @@ _tkinter_getbusywaitinterval(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #define _TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF #endif /* !defined(_TKINTER_TKAPP_DELETEFILEHANDLER_METHODDEF) */ -/*[clinic end generated code: output=050579715b59903d input=a9049054013a1b77]*/ +/*[clinic end generated code: output=8a843a5f7728537c input=a9049054013a1b77]*/ From bf2776cae5cf7aae3c6aff2bb867640b0d7e8e7f Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 12 Jul 2020 09:25:00 -0400 Subject: [PATCH 35/39] bpo-41176: Resolve review issues --- Doc/library/tkinter.rst | 59 +++------------------- Doc/whatsnew/3.10.rst | 10 ++-- Lib/tkinter/test/test_tkinter/test_misc.py | 26 ++++------ Modules/_tkinter.c | 2 +- 4 files changed, 23 insertions(+), 74 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index bfbe7af403774b..5c38d07539005b 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -88,6 +88,10 @@ so, usually, to use Tkinter all you need is a simple import statement:: .. FIXME: The following keyword arguments are currently recognized: +Below are a few of the methods provided by the :class:`Tk` class. + +.. sectionauthor:: Richard Sheridan + .. method:: Tk.mainloop(threshold) Enters the main loop of Tkinter. This repeatedly dispatches Tcl events until either @@ -112,6 +116,8 @@ so, usually, to use Tkinter all you need is a simple import statement:: the main loop to be dispatching events. Some examples are: calling the :meth:`Tk.update` command, :meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook. + .. versionadded:: 3.10 + .. function:: Tcl(screenName=None, baseName=None, className='Tk', useTk=0) The :func:`Tcl` function is a factory function which creates an object much like @@ -122,59 +128,6 @@ so, usually, to use Tkinter all you need is a simple import statement:: created by the :func:`Tcl` object can have a Toplevel window created (and the Tk subsystem initialized) by calling its :meth:`loadtk` method. -.. TODO: Not a class, a module. how do I inline another module into this RST file? - -.. class:: _tkinter - - This module contains the low-level interface to Tk, and should rarely be - used directly by application programmers. It is usually a shared library - (or DLL), but might in some cases be statically linked with the Python - interpreter. - -.. method:: _tkinter.create(screenName=None, baseName="", className="Tk", interactive=False, wantobjects=False, wantTk=True, sync=False, use=None) - - This method is not for general use, but it is responsible for creating the - :class:`_tkinter.tkapp` object that can be found as the attribute - :attr:`Tk.tk`. - -.. class:: _tkinter.tkapp - - This object contains most of the low-level interface to Tk, and should - rarely be used directly by application programmers. - -.. method:: _tkinter.tkapp.call(*args) - - This method is the core entry point to Tcl/Tk. All Tkinter methods that - correspond to Tcl commands flow through here. This function is thread-safe, - meaning it can be used to issue commands to the Tcl interpreter in the main - thread from any other thread. If called from a thread, it will wait up to - one second for :meth:`Tk.mainloop` to be called, and then throw an error. - This error after one second is deprecated behavior, in the future it will - wait indefinitely for :meth:`Tk.mainloop`, :meth:`Tk.update`, or - :meth:`_tkinter.tkapp.dooneevent` to dispatch the event. - -.. method:: _tkinter.tkapp.willdispatch() - - This method circumvents the thread waiting behavior of :meth:`_tkinter.tkapp.call` - until after the next call to :meth:`Tk.mainloop` returns. Instead of throwing - an error after one second, :meth:`_tkinter.tkapp.call` will wait indefinitely - for :meth:`Tk.mainloop` to come up, or for some other entity to dispatch the - call. This method is essentially a promise to the thread that you "will dispatch" soon. - This method is deprecated and future behavior will be equivialent to calling this - function on load and after each invocation of :meth:`Tk.mainloop`. - -.. method:: _tkinter.tkapp.setmainloopwaitattempts(num=10) - - This method sets the number of 100 ms wait attempts of :meth:`_tkinter.tkapp.call`. - You can trigger an error immediately by setting num to 0. This method is deprecated - and future behavior will be equivalent to setting an infinite number of attempts. - -.. method:: _tkinter.tkapp.dooneevent(flags=0) - - Dispatches exactly one event from the Tcl event queue. Must be called from the same - thread as the Tcl interpreter. See Tcl documentation for Tcl_DoOneEvent() for information - on using the flags argument. - Other modules that provide Tk support include: :mod:`tkinter.colorchooser` diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index cc8044f879c537..fbb55eaeae2b9c 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -111,16 +111,16 @@ Added the *root_dir* and *dir_fd* parameters in :func:`~glob.glob` and (Contributed by Serhiy Storchaka in :issue:`38144`.) sys ---- +---- Add :data:`sys.orig_argv` attribute: the list of the original command line arguments passed to the Python executable. (Contributed by Victor Stinner in :issue:`23427`.) tkinter ---- +---- -Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and +XXX Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and :meth:`_tkinter.tkapp.dispatching`. You can reliably call these to determine if the tkinter mainloop is running. @@ -130,7 +130,7 @@ correctness of the new dispatching methods. Added another new method :meth:`_tkinter.tkapp.setmainloopwaitattempts` which can be used to obtain future thread waiting behavior. -(see also Deprications section) +(see also Deprecated section) (Contributed by Richard Sheridan in :issue:`41176`.) @@ -146,7 +146,7 @@ Optimizations Deprecated ========== -The :mod:`tkinter` module has deprected the undocumented behavior where a +XXX The :mod:`tkinter` module has deprected the undocumented behavior where a thread will implicitly wait up to one second for the main thread to enter the main loop and then fail. The undocumented :meth:`_tkinter.tkapp.willdispatch` method which sidesteps diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index 0f89baa52038e3..f172981c738617 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -1,6 +1,6 @@ import unittest import tkinter -import threading, time +import threading import time from test import support from tkinter.test.support import AbstractTkTest @@ -244,19 +244,15 @@ def target(): # keep the main thread from calling root.mainloop() ready_for_mainloop.set() - # Everything after the event set must go in this try - # to guarantee that root.quit() is called in finally - try: - # self.assertTrue(root.dispatching()) but patient - for i in range (1000): - if root.dispatching(): - thread_dispatching_eventually = True - break - time.sleep(0.01) - else: # if not break - thread_dispatching_eventually = False - finally: - root.after(0, root.quit) + # self.assertTrue(root.dispatching()) but patient + for i in range(1000): + if root.dispatching(): + thread_dispatching_eventually = True + break + time.sleep(0.01) + else: # if not break + thread_dispatching_eventually = False + root.after(0, root.quit) root = self.root @@ -265,7 +261,7 @@ def target(): try: ready_for_mainloop = threading.Event() - thread = threading.Thread(target=target, daemon=True) + thread = threading.Thread(target=target) self.assertFalse(root.dispatching()) thread.start() ready_for_mainloop.wait() diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 61fd1d33371533..57c50fc6b1378a 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -366,7 +366,7 @@ static int mainloopwaitattempts = 10; // 1 second static int WaitForMainloop(TkappObject* self) { - int i = 0; + int i; int max_attempts = mainloopwaitattempts; if (self->dispatching) { From 1be91e02960bab06ddd89f4ac9bb7e132d85cdfb Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 12 Jul 2020 09:45:39 -0400 Subject: [PATCH 36/39] bpo-41176: Title underlines should be same length as title --- Doc/whatsnew/3.10.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index fbb55eaeae2b9c..696e1e38269cf9 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -111,14 +111,14 @@ Added the *root_dir* and *dir_fd* parameters in :func:`~glob.glob` and (Contributed by Serhiy Storchaka in :issue:`38144`.) sys ----- +--- Add :data:`sys.orig_argv` attribute: the list of the original command line arguments passed to the Python executable. (Contributed by Victor Stinner in :issue:`23427`.) tkinter ----- +------- XXX Added :meth:`tkinter.dispatching`, :meth:`tkinter.Misc.dispatching`, and :meth:`_tkinter.tkapp.dispatching`. From ad6e8820ce75430907800e820ffbe718bdced8fd Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 12 Jul 2020 12:06:06 -0400 Subject: [PATCH 37/39] bpo-41176: reset dispatching flag in test --- Lib/tkinter/test/test_tkinter/test_misc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/tkinter/test/test_tkinter/test_misc.py b/Lib/tkinter/test/test_tkinter/test_misc.py index f172981c738617..5e71e8fa0c7c92 100644 --- a/Lib/tkinter/test/test_tkinter/test_misc.py +++ b/Lib/tkinter/test/test_tkinter/test_misc.py @@ -213,6 +213,10 @@ def test_willdispatch(self): with self.assertWarns(DeprecationWarning): root.tk.willdispatch() self.assertTrue(root.dispatching()) + # reset dispatching flag + root.after(0,root.quit) + root.mainloop() + self.assertFalse(root.dispatching()) def test_thread_must_wait_for_mainloop(self): # remove test on eventual WaitForMainloop removal From 6143a185c1133d4f7422ee917754f845e9038b96 Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 12 Jul 2020 14:47:38 -0400 Subject: [PATCH 38/39] bpo-41176: layout Tk methods --- Doc/library/tkinter.rst | 43 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 5c38d07539005b..d94113fa00f972 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -88,35 +88,36 @@ so, usually, to use Tkinter all you need is a simple import statement:: .. FIXME: The following keyword arguments are currently recognized: -Below are a few of the methods provided by the :class:`Tk` class. + Below are a few of the methods provided by the :class:`Tk` class. -.. sectionauthor:: Richard Sheridan + .. sectionauthor:: Richard Sheridan -.. method:: Tk.mainloop(threshold) + .. method:: mainloop(threshold) - Enters the main loop of Tkinter. This repeatedly dispatches Tcl events until either - :meth:`Tk.quit` is called, the number of open windows drops below `threshold`, or - an error occurs while executing events. Usually the default threshold of 0 is - appropriate. + Enters the main loop of Tkinter. This repeatedly dispatches Tcl events + until either :meth:`Tk.quit` is called, the number of open windows drops + below ``threshold``, or an error occurs while executing events. Usually + the default threshold of 0 is appropriate. -.. method:: Tk.quit() + .. method:: quit() - Signals the Tkinter main loop to stop dispatching. - The main loop will exit AFTER the current Tcl event handler is - finished calling. If quit is called outside the context of a Tcl - event, for example from a thread, the main loop will not exit - until after the NEXT event is dispatched. If no more events are - forthcoming, main loop will keep blocking even if quit has been - called. In that case, call `Tk.after(0, Tk.quit)` instead. + Signals the Tkinter main loop to stop dispatching. + The main loop will exit AFTER the current Tcl event handler is + finished calling. If quit is called outside the context of a Tcl + event, for example from a thread, the main loop will not exit + until after the NEXT event is dispatched. If no more events are + forthcoming, main loop will keep blocking even if quit has been + called. In that case, call ``after(0, quit)`` instead. -.. method:: Tk.dispatching() + .. method:: dispatching() - Determines if the Tkinter main loop is running. Returns True if the main loop is running, or - returns False if the main loop is not running. It is possible for some entity other than - the main loop to be dispatching events. Some examples are: calling the :meth:`Tk.update` command, - :meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook. + Determines if the Tkinter main loop is running. Returns True if the main + loop is running, or returns False if the main loop is not running. It is + possible for some entity other than the main loop to be dispatching + events. Some examples are: calling the :meth:`Tk.update` command, + :meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook. - .. versionadded:: 3.10 + .. versionadded:: 3.10 .. function:: Tcl(screenName=None, baseName=None, className='Tk', useTk=0) From 516a3853ace9313d3842f90fd1813131f7e47d4e Mon Sep 17 00:00:00 2001 From: Richard Sheridan Date: Sun, 12 Jul 2020 14:48:03 -0400 Subject: [PATCH 39/39] bpo-41176: clarify dispatching() limitations --- Doc/library/tkinter.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index d94113fa00f972..8bfa17957913f9 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -116,6 +116,8 @@ so, usually, to use Tkinter all you need is a simple import statement:: possible for some entity other than the main loop to be dispatching events. Some examples are: calling the :meth:`Tk.update` command, :meth:`_tkinter.tkapp.doonevent`, and the python command line EventHook. + :meth:`Tk.dispatching` does not provide any information about entities + dispatching other than :meth:`Tk.mainloop`. .. versionadded:: 3.10