diff --git a/CHANGES.txt b/CHANGES.txt index dd142fe17..8b5c59e90 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,26 @@ Envisage CHANGELOG ==================== +Version 4.7.2 +============= + +Released: 03 May 2019 + +Fixes +----- + +* Fix some broken imports and name errors in the ``envisage.developer`` + package. (#130) +* Add missing test data to support running tests on Python 3.7. (#136) +* Fix reversed interpretation of the + ``TasksApplication.always_use_default_layout`` when creating task windows. + (#144) +* In the ``InternalIPKernel`` plugin, restore original standard streams + (``stdout``, ``stdin``, ``stderr``) at plugin stop time. (#146) +* In the ``InternalIPKernel`` plugin, fix ``ResourceWarnings`` from + unclosed pipes attached to qt consoles. (#147) + + Version 4.7.1 ============= @@ -62,7 +82,7 @@ Fixes * Fixes for tests under Python 3.5 (#86) * Work around for issue with Traits in Python 3 (#78) -* Replace uses of ‘file’ and ‘execfile’ (#75) +* Replace uses of ‘file’ and ‘execfile’ (#75) * Fix MOTD_Using_Eggs example (#66) * Fix broken and outdated links in documentation (#72) * Fix link to docs from README (#70) diff --git a/envisage/developer/charm/charm.py b/envisage/developer/charm/charm.py index 174f62fa5..d943c57b1 100644 --- a/envisage/developer/charm/charm.py +++ b/envisage/developer/charm/charm.py @@ -2,7 +2,7 @@ # Enthought library imports. -from envisage.developer.api import CodeBrowser, Module +from envisage.developer.code_browser.api import CodeBrowser, Module from traits.api import Event, HasTraits, Instance, Str diff --git a/envisage/developer/ui/view/plugin_browser.py b/envisage/developer/ui/view/plugin_browser.py index 671b0cf4c..1589263ad 100644 --- a/envisage/developer/ui/view/plugin_browser.py +++ b/envisage/developer/ui/view/plugin_browser.py @@ -2,13 +2,13 @@ # Enthought library imports. -from envisage.api import ExtensionPoint, IPlugin +from envisage.api import ExtensionPoint, IApplication, IExtensionPoint, IPlugin from traits.api import Delegate, HasTraits, Instance, List, Property from traits.api import Code, Str from traitsui.api import Item, TableEditor, View, VGroup from traitsui.table_column import ObjectColumn # fixme: non-api! -class ExtensionPointModel(Hastraits): +class ExtensionPointModel(HasTraits): """ A model for browsing an extension point. """ # The plugin that offered the extension point. @@ -21,7 +21,7 @@ class ExtensionPointModel(Hastraits): -class ExtensionModel(Hastraits): +class ExtensionModel(HasTraits): """ A model for browsing a contribution to an extension point. """ # The plugin that made the contribution. diff --git a/envisage/plugins/ipython_kernel/internal_ipkernel.py b/envisage/plugins/ipython_kernel/internal_ipkernel.py index a22511ed6..54ca33093 100644 --- a/envisage/plugins/ipython_kernel/internal_ipkernel.py +++ b/envisage/plugins/ipython_kernel/internal_ipkernel.py @@ -4,6 +4,7 @@ """ from distutils.version import StrictVersion as Version +import sys import ipykernel from ipykernel.connect import connect_qtconsole @@ -55,6 +56,12 @@ class InternalIPKernel(HasStrictTraits): #: This is a list of tuples (name, value). initial_namespace = List() + #: sys.stdout value at time kernel was started + _original_stdout = Any() + + #: sys.stderr value at time kernel was started + _original_stderr = Any() + def init_ipkernel(self, gui_backend): """ Initialize the IPython kernel. @@ -64,6 +71,12 @@ def init_ipkernel(self, gui_backend): The GUI mode used to initialize the GUI mode. For options, see the `ipython --gui` help pages. """ + # The IPython kernel modifies sys.stdout and sys.stderr when started, + # and doesn't currently provide a way to restore them. So we restore + # them ourselves at shutdown. + self._original_stdout = sys.stdout + self._original_stderr = sys.stderr + # Start IPython kernel with GUI event loop support self.ipkernel = gui_kernel(gui_backend) @@ -73,7 +86,7 @@ def init_ipkernel(self, gui_backend): # Workaround: Retrieve the kernel on the IPykernelApp and set the # io_loop without starting it! if NEEDS_IOLOOP_PATCH and not hasattr(self.ipkernel.kernel, 'io_loop'): - self.ipkernel.kernel.io_loop = ioloop.IOLoop.instance() + self.ipkernel.kernel.io_loop = ioloop.IOLoop.instance() # This application will also act on the shell user namespace self.namespace = self.ipkernel.shell.user_ns @@ -90,6 +103,8 @@ def cleanup_consoles(self): """ Kill all existing consoles. """ for c in self.consoles: c.kill() + c.stdout.close() + c.stderr.close() self.consoles = [] def shutdown(self): @@ -101,5 +116,20 @@ def shutdown(self): self.cleanup_consoles() self.ipkernel.shell.exit_now = True self.ipkernel.cleanup_connection_file() + + # The stdout and stderr streams created by the kernel use the + # IOPubThread, so we need to close them and restore the originals + # before we shut down the thread. Without this, we get obscure + # errors of the form "TypeError: heap argument must be a list". + kernel_stdout = sys.stdout + if kernel_stdout is not self._original_stdout: + sys.stdout = self._original_stdout + kernel_stdout.close() + + kernel_stderr = sys.stderr + if kernel_stderr is not self._original_stderr: + sys.stderr = self._original_stderr + kernel_stderr.close() + self.ipkernel.iopub_thread.stop() self.ipkernel = None diff --git a/envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py b/envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py index 52d86d5eb..fe2e5d90a 100644 --- a/envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py +++ b/envisage/plugins/ipython_kernel/tests/test_internal_ipkernel.py @@ -1,4 +1,5 @@ import gc +import sys import unittest try: @@ -47,6 +48,27 @@ def test_initial_namespace(self): self.assertEqual(kernel.namespace['x'], 42.1) kernel.shutdown() + def test_shutdown_restores_output_streams(self): + original_stdout = sys.stdout + original_stderr = sys.stderr + + kernel = InternalIPKernel(initial_namespace=[('x', 42.1)]) + kernel.init_ipkernel(gui_backend=None) + kernel.shutdown() + + self.assertIs(sys.stdout, original_stdout) + self.assertIs(sys.stderr, original_stderr) + + def test_shutdown_closes_console_pipes(self): + kernel = InternalIPKernel(initial_namespace=[('x', 42.1)]) + kernel.init_ipkernel(gui_backend=None) + console = kernel.new_qt_console() + self.assertFalse(console.stdout.closed) + self.assertFalse(console.stderr.closed) + kernel.shutdown() + self.assertTrue(console.stdout.closed) + self.assertTrue(console.stderr.closed) + def test_io_pub_thread_stopped(self): kernel = InternalIPKernel() kernel.init_ipkernel(gui_backend=None) diff --git a/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.7.egg b/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.7.egg new file mode 100644 index 000000000..27ccc4589 Binary files /dev/null and b/envisage/tests/bad_eggs/acme.bad-0.1a1-py3.7.egg differ diff --git a/envisage/tests/eggs/README.txt b/envisage/tests/eggs/README.txt new file mode 100644 index 000000000..108ce05c8 --- /dev/null +++ b/envisage/tests/eggs/README.txt @@ -0,0 +1,12 @@ +This directory contains eggs used by the test suite, along with the packages +that were used to generate those eggs. + +To generate eggs for a new version of Python: + +- Change to the acme.bar directory +- Run "python setup.py bdist_egg" using the desired version of Python. This + will create a new egg under the "dist/" directory. +- Copy that egg to this directory. + +Now repeat for acme.baz and acme.foo, as well as acme.bad in the bad_eggs +directory adjacent to this one. diff --git a/envisage/tests/eggs/acme.bar-0.1a1-py3.7.egg b/envisage/tests/eggs/acme.bar-0.1a1-py3.7.egg new file mode 100644 index 000000000..2101d8949 Binary files /dev/null and b/envisage/tests/eggs/acme.bar-0.1a1-py3.7.egg differ diff --git a/envisage/tests/eggs/acme.baz-0.1a1-py3.7.egg b/envisage/tests/eggs/acme.baz-0.1a1-py3.7.egg new file mode 100644 index 000000000..6bc22c679 Binary files /dev/null and b/envisage/tests/eggs/acme.baz-0.1a1-py3.7.egg differ diff --git a/envisage/tests/eggs/acme.foo-0.1a1-py3.7.egg b/envisage/tests/eggs/acme.foo-0.1a1-py3.7.egg new file mode 100644 index 000000000..a343d6d7e Binary files /dev/null and b/envisage/tests/eggs/acme.foo-0.1a1-py3.7.egg differ diff --git a/envisage/ui/single_project/view/project_view.py b/envisage/ui/single_project/view/project_view.py index 5aed41948..0e93a3455 100644 --- a/envisage/ui/single_project/view/project_view.py +++ b/envisage/ui/single_project/view/project_view.py @@ -12,7 +12,6 @@ # Standard library imports. import logging -from string import rfind # Enthought library imports from apptools.naming.api import Binding @@ -262,7 +261,7 @@ def _name_suffix_changed(self, old, new): # on the new suffix, if any. name = self.name if old is not None and len(old) > 0: - index = rfind(name, " " + old) + index = (" " + old).rfind(name) if index > -1: name = name[0:index] if new is not None and len(new) > 0: diff --git a/envisage/ui/tasks/tasks_application.py b/envisage/ui/tasks/tasks_application.py index 98d9c4ddb..1ebbf4f78 100644 --- a/envisage/ui/tasks/tasks_application.py +++ b/envisage/ui/tasks/tasks_application.py @@ -301,8 +301,10 @@ def _create_windows(self): # Create a TaskWindow for each TaskWindowLayout. for window_layout in window_layouts: - window = self.create_window(window_layout, - restore=self.always_use_default_layout) + if self.always_use_default_layout: + window = self.create_window(window_layout, restore=False) + else: + window = self.create_window(window_layout, restore=True) window.open() def _get_task_factory(self, id): diff --git a/setup.py b/setup.py index 971ae3542..64f52199b 100644 --- a/setup.py +++ b/setup.py @@ -8,9 +8,9 @@ MAJOR = 4 MINOR = 7 -MICRO = 1 +MICRO = 2 -IS_RELEASED = True +IS_RELEASED = False VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO)