Hello community, here is the log from the commit of package spyder for openSUSE:Factory checked in at 2020-05-02 22:18:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/spyder (Old) and /work/SRC/openSUSE:Factory/.spyder.new.2738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "spyder" Sat May 2 22:18:39 2020 rev:4 rq:799656 version:4.1.2 Changes: -------- --- /work/SRC/openSUSE:Factory/spyder/spyder.changes 2020-04-21 13:13:12.501116370 +0200 +++ /work/SRC/openSUSE:Factory/.spyder.new.2738/spyder.changes 2020-05-02 22:18:42.152707639 +0200 @@ -1,0 +2,9 @@ +Thu Apr 30 18:23:14 UTC 2020 - Benjamin Greiner <c...@bnavigator.de> + +- Close leaks in tests for mainwindow and ipythonconsole + gh#spyder-ide/spyder#12534 + spyder-pr12534-closeleaks.patch +- Now also run the slow tests except for a few. +- python-opengl is not a requirement + +------------------------------------------------------------------- New: ---- spyder-pr12534-closeleaks.patch ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ spyder.spec ++++++ --- /var/tmp/diff_new_pack.mKiGFX/_old 2020-05-02 22:18:43.068709557 +0200 +++ /var/tmp/diff_new_pack.mKiGFX/_new 2020-05-02 22:18:43.072709566 +0200 @@ -32,6 +32,7 @@ Source: https://github.com/spyder-ide/spyder/archive/v%{version}.tar.gz#/spyder-%{version}.tar.gz Source1: spyder-rpmlintrc Patch0: spyder-pr11704-fixpytestargs.patch +Patch1: spyder-pr12534-closeleaks.patch BuildRequires: fdupes BuildRequires: python-rpm-macros BuildRequires: python3-Pygments >= 2.0 @@ -77,7 +78,6 @@ Requires: python3-keyring Requires: python3-nbconvert >= 4.0 Requires: python3-numpydoc >= 0.6.0 -Requires: python3-opengl Requires: python3-parso >= 0.5.2 Requires: python3-pexpect >= 0.4.4 Requires: python3-pickleshare @@ -131,7 +131,6 @@ BuildRequires: python3-matplotlib >= 2.0.0 BuildRequires: python3-matplotlib-qt5 BuildRequires: python3-matplotlib-tk -BuildRequires: python3-opengl BuildRequires: python3-pandas >= 0.13.1 BuildRequires: python3-pyaml BuildRequires: python3-pydocstyle @@ -222,6 +221,7 @@ %prep %setup -q -n spyder-%{version} %patch0 -p1 +%patch1 -p1 # Fix wrong-file-end-of-line-encoding RPMLint warning sed -i 's/\r$//' spyder/app/restart.py sed -i 's/\r$//' LICENSE.txt CHANGELOG.md @@ -270,11 +270,12 @@ export PYTHONDONTWRITEBYTECODE=1 # upstream splits the tests into slow and fast ones -# we run the ipythonconsole tests separately because they pose issues with asynchonously opened sockets -skiptests="test_ipythonconsole" -skipslowtests="test_ipythonconsole" +# add all tests to skip into $skiptests or $skipslowtests separated by whitespace +# tests to run separately because of leakages go into separateslowtests +skiptests="" +skipslowtests="" +separateslowtests="" -# add all tests to skip into $skiptests or $skipttestsslow separated by whitespace # the shortcut is not sent by the editorbot skiptests+=" test_comment" # the click/tab press is not sent by the bot @@ -288,43 +289,28 @@ # tests not suitable for CIs or OBS as evident from the last assert which fails here skiptests+=" test_connection_dialog_remembers_input_with_ssh_passphrase" skiptests+=" test_connection_dialog_remembers_input_with_password" +# running into timeouts +skiptests+=" test_dbg_input" +skiptests+=" test_mpl_backend_change" + # completes to math.hypot(cooordinates) instead of expected math.hypot(*coordinates) skipslowtests+=" test_completions" - -# the linter does not report warnings with the D and E error codes -skipslowtests+=" test_ignore_warnings test_move_warnings test_get_warnings test_update_warnings" -# opens too many files ? -# likely a test before.... -skipslowtests+=" test_open_notebooks_from_project_explorer" - -# the following tests rely on IPythonConsole behaving well on OBS (which does not..) -skipipythontests="" -skipipythonslowtests="" -# xvfb does not like the repeating restart of the kernels in those tests -skipipythontests+=" test_load_kernel_file" -# running into timeouts -skipipythontests+=" test_mpl_backend_change" -skipipythontests+=" test_calltip" -skipipythontests+=" test_dbg_input" -# pdb seems to be the root cause of a lot of test problems -skipipythontests+=" test_pdb" -# timeout, hard abort, permission errors -skipipythontests+=" test_stderr_" -skipipythontests+=" test_kernel_kill test_conda_env_activation" -# segfault (?) -skipipythonslowtests+=" test_runfile_from_project_explorer" +# Would require network connections +skipslowtests+=" test_update" +# runs into timeout on obs +skipslowtests+=" test_hide_widget_completion" +# some mainwindow tests leak open file sockets and cause python to terminate +# the whole testrun at some later point. +# gh#/spyder-ide/spyder#12534 +separateslowtests+=" test_runcell test_varexp test_edidorstack" %{python_expand PYTHONPATH=%{buildroot}%{$python_sitelib} -for s in tests slowtests ipythontests ipythonslowtests; do - skips=skip$s - declare skip${s}_p="$($python -c "import sys; print(' or '.join('${!skips}'.split()))")" +for s in skiptests skipslowtests separateslowtests; do + declare ${s}_p="$($python -c "import sys; print(' or '.join('${!s}'.split()))")" done -$python runtests.py -k "not ($skiptests_p)" -# the slow tests still segfault unreproducibly at different tests. Something does not clean up -# $python runtests.py --run-slow -k "not ($skipslowtests_p)" -$python runtests.py -k "test_ipythonconsole and not ($skipipythontests_p)" -# the slow tests still fail unreproducibly at different tests. Something does not clean up -# $python runtests.py --run-slow -k "test_ipythonconsole and not ($skipipythonslowtests_p)" +$python runtests.py -k "not ($skiptests_p)" --timeout 1800 +$python runtests.py --run-slow -k "not ($skipslowtests_p or $separateslowtests_p)" --timeout 1800 +$python runtests.py --run-slow -k "$separateslowtests_p" --timeout 1800 } %endif ++++++ spyder-pr12534-closeleaks.patch ++++++ diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index b8436ff0a..ef640cd6a 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -832,8 +832,12 @@ def test_connection_to_external_kernel(main_window, qtbot): assert nsb.editor.source_model.rowCount() == 1 # Shutdown the kernels + spykm.stop_restarter() + km.stop_restarter() spykm.shutdown_kernel(now=True) km.shutdown_kernel(now=True) + spykc.stop_channels() + kc.stop_channels() @pytest.mark.slow diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py index 32c9fdb1d..fdcbe7338 100644 --- a/spyder/plugins/ipythonconsole/plugin.py +++ b/spyder/plugins/ipythonconsole/plugin.py @@ -953,6 +953,9 @@ class IPythonConsole(SpyderPluginWidget): if not self.tabwidget.count(): return if client is not None: + if client not in self.clients: + # Client already closed + return index = self.tabwidget.indexOf(client) # if index is not found in tabwidget it's because this client was # already closed and the call was performed by the exit callback @@ -998,7 +1001,7 @@ class IPythonConsole(SpyderPluginWidget): # if there aren't related clients we can remove stderr_file related_clients = self.get_related_clients(client) - if len(related_clients) == 0 and osp.exists(client.stderr_file): + if len(related_clients) == 0: client.remove_stderr_file() client.dialog_manager.close_all() diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py index 918300d91..8cad08bfc 100644 --- a/spyder/plugins/ipythonconsole/widgets/client.py +++ b/spyder/plugins/ipythonconsole/widgets/client.py @@ -177,6 +177,12 @@ class ClientWidget(QWidget, SaveHistoryMixin): # Show timer self.update_time_label_visibility() + def __del__(self): + """Close threads to avoid segfault""" + if (self.restart_thread is not None + and self.restart_thread.isRunning()): + self.restart_thread.wait() + #------ Public API -------------------------------------------------------- @property def kernel_id(self): @@ -501,24 +507,13 @@ class ClientWidget(QWidget, SaveHistoryMixin): def shutdown(self): """Shutdown kernel""" if self.get_kernel() is not None and not self.slave: - self.shellwidget.spyder_kernel_comm.close() - self.shellwidget.spyder_kernel_comm.shutdown_comm_channel() - self.shellwidget._pdb_history_file.save_thread.stop() - self.shellwidget.kernel_manager.stop_restarter() - self.shutdown_thread = QThread() - self.shutdown_thread.run = self.finalize_shutdown - self.shutdown_thread.finished.connect(self.stop_kernel_channels) - self.shutdown_thread.start() - - def finalize_shutdown(self): - """Finalise the shutdown.""" - if self.get_kernel() is not None and not self.slave: - self.shellwidget.kernel_manager.shutdown_kernel() + self.shellwidget.shutdown() - def stop_kernel_channels(self): - """Stop kernel channels.""" - if self.shellwidget.kernel_client is not None: - self.shellwidget.kernel_client.stop_channels() + def close(self): + """Close client""" + self.shellwidget.will_close( + self.get_kernel() is None or self.slave) + super(ClientWidget, self).close() def interrupt_kernel(self): """Interrupt the associanted Spyder kernel if it's running""" diff --git a/spyder/plugins/ipythonconsole/widgets/debugging.py b/spyder/plugins/ipythonconsole/widgets/debugging.py index 76efc410d..088cbb814 100644 --- a/spyder/plugins/ipythonconsole/widgets/debugging.py +++ b/spyder/plugins/ipythonconsole/widgets/debugging.py @@ -70,6 +70,21 @@ class DebuggingWidget(RichJupyterWidget): self._tmp_reading = False self._pdb_frame_loc = (None, None) + def will_close(self, externally_managed): + """ + Close communication channels with the kernel if shutdown was not + called. If the kernel is not externally managed, shutdown the kernel + as well. + """ + try: + self._pdb_history_file.save_thread.stop() + except AttributeError: + pass + try: + self._pdb_history_file.db.close() + except AttributeError: + pass + def handle_debug_state(self, in_debug_loop): """Update the debug state.""" self._pdb_in_loop = in_debug_loop diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py index dc0eee590..20feecd16 100644 --- a/spyder/plugins/ipythonconsole/widgets/shell.py +++ b/spyder/plugins/ipythonconsole/widgets/shell.py @@ -15,7 +15,7 @@ import uuid from textwrap import dedent # Third party imports -from qtpy.QtCore import Signal +from qtpy.QtCore import Signal, QThread from qtpy.QtWidgets import QMessageBox # Local imports @@ -92,8 +92,10 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget, # set in the qtconsole constructor. See spyder-ide/spyder#4806. self.set_bracket_matcher_color_scheme(self.syntax_style) + self.shutdown_called = False self.kernel_manager = None self.kernel_client = None + self.shutdown_thread = None handlers = { 'pdb_state': self.set_pdb_state, 'pdb_continue': self.pdb_continue, @@ -109,6 +111,46 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget, self.spyder_kernel_comm.register_call_handler( request_id, handlers[request_id]) + def __del__(self): + """Avoid destroying thread""" + if (self.shutdown_thread is not None + and self.shutdown_thread.isRunning()): + self.shutdown_thread.wait() + + def shutdown(self): + """Shutdown kernel""" + self.shutdown_called = True + self.spyder_kernel_comm.close() + self.spyder_kernel_comm.shutdown_comm_channel() + self.kernel_manager.stop_restarter() + + self.shutdown_thread = QThread() + self.shutdown_thread.run = self.kernel_manager.shutdown_kernel + if self.kernel_client is not None: + self.shutdown_thread.finished.connect( + self.kernel_client.stop_channels) + self.shutdown_thread.start() + + def will_close(self, externally_managed): + """ + Close communication channels with the kernel if shutdown was not + called. If the kernel is not externally managed, shutdown the kernel + as well. + """ + if not self.shutdown_called and not externally_managed: + # Make sure the channels are stopped + self.spyder_kernel_comm.close() + self.spyder_kernel_comm.shutdown_comm_channel() + self.kernel_manager.stop_restarter() + self.kernel_manager.shutdown_kernel(now=True) + if self.kernel_client is not None: + self.kernel_client.stop_channels() + if externally_managed: + self.spyder_kernel_comm.close() + if self.kernel_client is not None: + self.kernel_client.stop_channels() + super(ShellWidget, self).will_close(externally_managed) + def call_kernel(self, interrupt=False, blocking=False, callback=None, timeout=None): """ diff --git a/spyder/plugins/onlinehelp/plugin.py b/spyder/plugins/onlinehelp/plugin.py index e1ed28015..214f6a9d1 100644 --- a/spyder/plugins/onlinehelp/plugin.py +++ b/spyder/plugins/onlinehelp/plugin.py @@ -51,7 +51,7 @@ class OnlineHelp(SpyderPluginWidget): else: history = [] return history - + def save_history(self): """Save history to a text file in user home directory""" open(self.LOG_PATH, 'w').write("\n".join( \ @@ -62,7 +62,7 @@ class OnlineHelp(SpyderPluginWidget): def toggle_view(self, checked): """Toggle view action.""" if checked: - if not self.pydocbrowser.is_server_running(): + if self.pydocbrowser.server is None: self.pydocbrowser.initialize() self.dockwidget.show() self.dockwidget.raise_() @@ -72,7 +72,7 @@ class OnlineHelp(SpyderPluginWidget): def get_plugin_title(self): """Return widget title""" return _('Online help') - + def get_focus_widget(self): """ Return the widget to give focus to when @@ -80,12 +80,13 @@ class OnlineHelp(SpyderPluginWidget): """ self.pydocbrowser.url_combo.lineEdit().selectAll() return self.pydocbrowser.url_combo - + def closing_plugin(self, cancelable=False): """Perform actions before parent main window is closed""" self.save_history() self.set_option('zoom_factor', self.pydocbrowser.webview.get_zoom_factor()) + self.pydocbrowser.quit_server() return True def on_first_registration(self): diff --git a/spyder/plugins/onlinehelp/widgets.py b/spyder/plugins/onlinehelp/widgets.py index 59ce0d18a..ee648c81b 100644 --- a/spyder/plugins/onlinehelp/widgets.py +++ b/spyder/plugins/onlinehelp/widgets.py @@ -53,6 +53,7 @@ class PydocServer(QThread): self.port = port self.server = None self.complete = False + self.closed = False def run(self): if PY3: @@ -66,15 +67,34 @@ class PydocServer(QThread): def callback(self, server): self.server = server - self.server_started.emit() + if self.closed: + self.quit_server() + else: + self.server_started.emit() def completer(self): self.complete = True + def is_running(self): + """Check if the server is running""" + if PY3: + if self.isRunning(): + # Startup + return True + if self.server is None: + return False + return self.server.serving + else: + # Py 2 + return self.isRunning() + def quit_server(self): + self.closed = True + if self.server is None: + return if PY3: # Python 3 - if self.isRunning() and self.server.serving: + if self.server.serving: self.server.stop() else: # Python 2 @@ -106,31 +126,33 @@ class PydocBrowser(WebBrowser): self.go_home() QApplication.restoreOverrideCursor() - def is_server_running(self): - """Return True if pydoc server is already running""" - return self.server is not None - def closeEvent(self, event): - if self.is_server_running(): - self.server.quit_server() -# while not self.server.complete: #XXX Is it really necessary? -# pass + """Handle close event""" + self.quit_server() event.accept() - #------ Public API ----------------------------------------------------- + # ------ Public API ------------------------------------------------------- def start_server(self): """Start pydoc server""" if self.server is None: self.port = select_port(default_port=self.DEFAULT_PORT) self.set_home_url('http://127.0.0.1:%d/' % self.port) - elif self.server.isRunning(): - self.server.server_started.disconnect(self.initialize_continued) - self.server.quit() + elif self.server.is_running(): + self.quit_server() self.server = PydocServer(port=self.port) self.server.server_started.connect(self.initialize_continued) self.server.start() - #------ WebBrowser API ----------------------------------------------------- + def quit_server(self): + """Quit the server""" + if self.server is None: + return + if self.server.is_running(): + self.server.server_started.disconnect(self.initialize_continued) + self.server.quit_server() + self.server.quit() + + # ------ WebBrowser API --------------------------------------------------- def get_label(self): """Return address label text""" return _("Module or package:")