diff --git a/installers/Windows/req-extras-pull-request.txt b/installers/Windows/req-extras-pull-request.txt
index b92de1d875d..3cc07fec1d1 100644
--- a/installers/Windows/req-extras-pull-request.txt
+++ b/installers/Windows/req-extras-pull-request.txt
@@ -17,9 +17,9 @@ matplotlib
cython
sympy
+# Spyder external plugins
+spyder-terminal>=1.2.1
+
# Spyder external dependencies (spyder-kernels and qdarkstyle)
./external-deps/spyder-kernels
./external-deps/qdarkstyle
-
-# There are no wheels for version 3.3
-cryptography==3.2.1
diff --git a/installers/Windows/req-extras-release.txt b/installers/Windows/req-extras-release.txt
index 7a2a436c027..7ed3847979a 100644
--- a/installers/Windows/req-extras-release.txt
+++ b/installers/Windows/req-extras-release.txt
@@ -17,5 +17,5 @@ matplotlib
cython
sympy
-# There are no wheels for version 3.3
-cryptography==3.2.1
+# Spyder external plugins
+spyder-terminal>=1.2.1
diff --git a/installers/Windows/req-pull-request.txt b/installers/Windows/req-pull-request.txt
index fb042c7cd28..3c37e9dbec2 100644
--- a/installers/Windows/req-pull-request.txt
+++ b/installers/Windows/req-pull-request.txt
@@ -1,6 +1,6 @@
+# Spyder external plugins
+spyder-terminal>=1.2.1
+
# Spyder external dependencies (spyder-kernels and qdarkstyle)
./external-deps/spyder-kernels
./external-deps/qdarkstyle
-
-# There are no wheels for version 3.3
-cryptography==3.2.1
diff --git a/installers/Windows/req-release.txt b/installers/Windows/req-release.txt
index 9f9533086be..11f1aa588a1 100644
--- a/installers/Windows/req-release.txt
+++ b/installers/Windows/req-release.txt
@@ -1,2 +1,2 @@
-# There are no wheels for version 3.3
-cryptography==3.2.1
+# Spyder external plugins
+spyder-terminal>=1.2.1
diff --git a/setup.py b/setup.py
index b5d96ec06be..984ceb1ea81 100644
--- a/setup.py
+++ b/setup.py
@@ -50,6 +50,8 @@
# =============================================================================
NAME = 'spyder'
LIBNAME = 'spyder'
+WINDOWS_INSTALLER_NAME = os.environ.get('EXE_NAME')
+
from spyder import __version__, __website_url__ #analysis:ignore
@@ -243,6 +245,12 @@ def run(self):
'watchdog>=0.10.3'
]
+# Replace spyder-kernels constraint to enable
+# building Windows installers on PRs
+if 'dev' in __version__ and WINDOWS_INSTALLER_NAME:
+ install_requires.remove('spyder-kernels>=2.2.1,<2.3.0')
+ install_requires.append('spyder-kernels>=2.2.1,<=2.3.0.dev0')
+
extras_require = {
'test:platform_system == "Windows"': ['pywin32'],
'test': [
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index 9e478e5758c..40d892a523c 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -345,6 +345,19 @@ def main_window(request, tmpdir, qtbot):
shell = window.ipyconsole.get_current_shellwidget()
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)
+ # Wait until console is up
+ shell = window.ipyconsole.get_current_shellwidget()
+ try:
+ qtbot.waitUntil(lambda: shell._prompt_html is not None,
+ timeout=SHELL_TIMEOUT)
+ except Exception:
+ # Print content of shellwidget and close window
+ print(shell._control.toPlainText())
+ client = window.ipyconsole.get_current_client()
+ if client.info_page != client.blank_page:
+ print('info_page')
+ print(client.info_page)
+ raise
if os.name != 'nt':
# _DummyThread are created if current_thread() is called from them.
@@ -4607,6 +4620,7 @@ def foo(x):
@pytest.mark.slow
@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt', reason="Tour messes up focus on Windows")
def test_focus_to_consoles(main_window, qtbot):
"""
Check that we give focus to the text widget of our consoles after focus
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
index d666cef2602..d22cf533628 100644
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -215,8 +215,18 @@ def __getattr__(self, attr):
# Wait until the window is fully up
qtbot.waitUntil(lambda: console.get_current_shellwidget() is not None)
shell = console.get_current_shellwidget()
- qtbot.waitUntil(lambda: shell._prompt_html is not None,
- timeout=SHELL_TIMEOUT)
+ try:
+ qtbot.waitUntil(lambda: shell._prompt_html is not None,
+ timeout=SHELL_TIMEOUT)
+ except Exception:
+ # Print content of shellwidget and close window
+ print(console.get_current_shellwidget(
+ )._control.toPlainText())
+ client = console.get_current_client()
+ if client.info_page != client.blank_page:
+ print('info_page')
+ print(client.info_page)
+ raise
# Check for thread or open file leaks
known_leak = request.node.get_closest_marker('known_leak')
@@ -1378,30 +1388,33 @@ def test_kernel_crash(ipyconsole, qtbot):
# Create an IPython kernel config file with a bad config
ipy_kernel_cfg = osp.join(get_ipython_dir(), 'profile_default',
'ipython_kernel_config.py')
- with open(ipy_kernel_cfg, 'w') as f:
- # This option must be a string, not an int
- f.write("c.InteractiveShellApp.extra_extension = 1")
-
- ipyconsole.create_new_client()
-
- # Assert that the console is showing an error
- qtbot.waitUntil(lambda: ipyconsole.get_clients()[-1].is_error_shown,
- timeout=6000)
- error_client = ipyconsole.get_clients()[-1]
- assert error_client.is_error_shown
-
- # Assert the error contains the text we expect
- webview = error_client.infowidget
- if WEBENGINE:
- webpage = webview.page()
- else:
- webpage = webview.page().mainFrame()
- qtbot.waitUntil(
- lambda: check_text(webpage, "Bad config encountered"),
- timeout=6000)
+ try:
+ with open(ipy_kernel_cfg, 'w') as f:
+ # This option must be a string, not an int
+ f.write("c.InteractiveShellApp.extra_extension = 1")
+
+ ipyconsole.get_widget().close_cached_kernel()
+ ipyconsole.create_new_client()
+
+ # Assert that the console is showing an error
+ qtbot.waitUntil(lambda: ipyconsole.get_clients()[-1].is_error_shown,
+ timeout=6000)
+ error_client = ipyconsole.get_clients()[-1]
+ assert error_client.is_error_shown
+
+ # Assert the error contains the text we expect
+ webview = error_client.infowidget
+ if WEBENGINE:
+ webpage = webview.page()
+ else:
+ webpage = webview.page().mainFrame()
- # Remove bad kernel config file
- os.remove(ipy_kernel_cfg)
+ qtbot.waitUntil(
+ lambda: check_text(webpage, "Bad config encountered"),
+ timeout=6000)
+ finally:
+ # Remove bad kernel config file
+ os.remove(ipy_kernel_cfg)
@flaky(max_runs=3)
diff --git a/spyder/plugins/ipythonconsole/utils/stdfile.py b/spyder/plugins/ipythonconsole/utils/stdfile.py
index ae8fb05e38a..c0cb22fa454 100644
--- a/spyder/plugins/ipythonconsole/utils/stdfile.py
+++ b/spyder/plugins/ipythonconsole/utils/stdfile.py
@@ -13,15 +13,34 @@
# Standard library imports.
import codecs
import os
+import os.path as osp
# Local imports
from spyder.py3compat import to_text_string
from spyder.utils.encoding import get_coding
+from spyder.utils.programs import get_temp_dir
+
+
+def std_filename(connection_file, extension, std_dir=None):
+ """Filename to save kernel output."""
+ json_file = osp.basename(connection_file)
+ file = json_file.split('.json')[0] + extension
+ if std_dir is not None:
+ file = osp.join(std_dir, file)
+ else:
+ try:
+ file = osp.join(get_temp_dir(), file)
+ except (IOError, OSError):
+ file = None
+ return file
class StdFile:
- def __init__(self, filename):
- self.filename = filename
+ def __init__(self, connection_file, extension=None, std_dir=None):
+ if extension is None:
+ self.filename = connection_file
+ else:
+ self.filename = std_filename(connection_file, extension, std_dir)
self._mtime = 0
self._cursor = 0
self._handle = None
@@ -92,3 +111,7 @@ def poll_file_change(self):
ret_text = text[self._cursor:]
self._cursor = len(text)
return ret_text
+
+ def copy(self):
+ """Return a copy."""
+ return StdFile(self.filename)
diff --git a/spyder/plugins/ipythonconsole/widgets/client.py b/spyder/plugins/ipythonconsole/widgets/client.py
index 228b08fd6b1..7e2645b80de 100644
--- a/spyder/plugins/ipythonconsole/widgets/client.py
+++ b/spyder/plugins/ipythonconsole/widgets/client.py
@@ -39,10 +39,8 @@
from spyder.utils.encoding import get_coding
from spyder.utils.environ import RemoteEnvDialog
from spyder.utils.palette import QStylePalette
-from spyder.utils.programs import get_temp_dir
from spyder.utils.qthelpers import add_actions, DialogManager
from spyder.py3compat import to_text_string
-from spyder.plugins.ipythonconsole.utils.stdfile import StdFile
from spyder.plugins.ipythonconsole.widgets import ShellWidget
from spyder.widgets.collectionseditor import CollectionsEditor
from spyder.widgets.mixins import SaveHistoryMixin
@@ -109,7 +107,9 @@ def __init__(self, parent, id_,
css_path=None,
configuration=None,
handlers={},
- std_dir=None):
+ stderr_obj=None,
+ stdout_obj=None,
+ fault_obj=None):
super(ClientWidget, self).__init__(parent)
SaveHistoryMixin.__init__(self, history_filename)
@@ -132,7 +132,6 @@ def __init__(self, parent, id_,
self.options_button = options_button
self.history = []
self.allow_rename = True
- self.std_dir = std_dir
self.is_error_shown = False
self.error_text = None
self.restart_thread = None
@@ -180,22 +179,16 @@ def __init__(self, parent, id_,
self.dialog_manager = DialogManager()
# --- Standard files handling
- self.stderr_obj = None
- self.stdout_obj = None
- self.fault_obj = None
+ self.stderr_obj = stderr_obj
+ self.stdout_obj = stdout_obj
+ self.fault_obj = fault_obj
self.std_poll_timer = None
- if not self.is_external_kernel:
- # Cannot create std files for external kernels
- self.stderr_obj = StdFile(self.std_filename('.stderr'))
- self.stdout_obj = StdFile(self.std_filename('.stdout'))
+ if self.stderr_obj is not None or self.stdout_obj is not None:
self.std_poll_timer = QTimer(self)
self.std_poll_timer.timeout.connect(self.poll_std_file_change)
self.std_poll_timer.setInterval(1000)
self.std_poll_timer.start()
self.shellwidget.executed.connect(self.poll_std_file_change)
- if self.hostname is None:
- # Cannot read file that is not on this computer
- self.fault_obj = StdFile(self.std_filename('.fault'))
self.start_successful = False
@@ -356,20 +349,6 @@ def _connect_control_signals(self):
self.container.find_widget.show)
# ----- Public API --------------------------------------------------------
- def std_filename(self, extension):
- """Filename to save kernel output."""
- file = None
- if self.connection_file is not None:
- file = self.kernel_id + extension
- if self.std_dir is not None:
- file = osp.join(self.std_dir, file)
- else:
- try:
- file = osp.join(get_temp_dir(), file)
- except (IOError, OSError):
- file = None
- return file
-
@property
def kernel_id(self):
"""Get kernel id."""
@@ -397,31 +376,33 @@ def remove_std_files(self, is_last_client=True):
def poll_std_file_change(self):
"""Check if the stderr or stdout file just changed."""
self.shellwidget.call_kernel().flush_std()
- stderr = self.stderr_obj.poll_file_change()
starting = self.shellwidget._starting
- if stderr:
- if self.is_benign_error(stderr):
- return
- if self.shellwidget.isHidden():
- # Avoid printing the same thing again
- if self.error_text != '%s' % stderr:
- full_stderr = self.stderr_obj.get_contents()
- self.show_kernel_error('%s' % full_stderr)
- if starting:
- self.shellwidget.banner = (
- stderr + '\n' + self.shellwidget.banner)
- else:
- self.shellwidget._append_plain_text(
- '\n' + stderr, before_prompt=True)
-
- stdout = self.stdout_obj.poll_file_change()
- if stdout:
- if starting:
- self.shellwidget.banner = (
- stdout + '\n' + self.shellwidget.banner)
- else:
- self.shellwidget._append_plain_text(
- '\n' + stdout, before_prompt=True)
+ if self.stderr_obj is not None:
+ stderr = self.stderr_obj.poll_file_change()
+ if stderr:
+ if self.is_benign_error(stderr):
+ return
+ if self.shellwidget.isHidden():
+ # Avoid printing the same thing again
+ if self.error_text != '%s' % stderr:
+ full_stderr = self.stderr_obj.get_contents()
+ self.show_kernel_error('%s' % full_stderr)
+ if starting:
+ self.shellwidget.banner = (
+ stderr + '\n' + self.shellwidget.banner)
+ else:
+ self.shellwidget._append_plain_text(
+ '\n' + stderr, before_prompt=True)
+
+ if self.stdout_obj is not None:
+ stdout = self.stdout_obj.poll_file_change()
+ if stdout:
+ if starting:
+ self.shellwidget.banner = (
+ stdout + '\n' + self.shellwidget.banner)
+ else:
+ self.shellwidget._append_plain_text(
+ '\n' + stdout, before_prompt=True)
def configure_shellwidget(self, give_focus=True):
"""Configure shellwidget after kernel is connected."""
diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py
index ee071eff24a..2d73e56ac6a 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -48,6 +48,7 @@
from spyder.widgets.browser import FrameWebView
from spyder.widgets.findreplace import FindReplace
from spyder.widgets.tabs import Tabs
+from spyder.plugins.ipythonconsole.utils.stdfile import StdFile
# Localization
@@ -386,6 +387,7 @@ def __init__(self, name=None, plugin=None, parent=None,
def on_close(self):
self.mainwindow_close = True
self.close_clients()
+ self._cached_kernel_properties = None
# ---- PluginMainWidget API and settings handling
# ------------------------------------------------------------------------
@@ -1056,6 +1058,9 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
known_spyder_kernel = False
slave_ord = ord('A') - 1
kernel_manager = None
+ stderr_obj = None
+ stdout_obj = None
+ fault_obj = None
for cl in self.clients:
if connection_file in cl.connection_file:
@@ -1066,6 +1071,12 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
master_id = cl.id_['int_id']
is_external_kernel = cl.shellwidget.is_external_kernel
known_spyder_kernel = cl.shellwidget.is_spyder_kernel
+ if cl.stderr_obj:
+ stderr_obj = cl.stderr_obj.copy()
+ if cl.stdout_obj:
+ stdout_obj = cl.stdout_obj.copy()
+ if cl.fault_obj:
+ fault_obj = cl.fault_obj.copy()
given_name = cl.given_name
new_slave_ord = ord(cl.id_['str_id'])
if new_slave_ord > slave_ord:
@@ -1085,7 +1096,6 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
show_elapsed_time = self.get_conf('show_elapsed_time')
reset_warning = self.get_conf('show_reset_namespace_warning')
ask_before_restart = self.get_conf('ask_before_restart')
- std_dir = self._test_dir if self._test_dir else None
client = ClientWidget(self,
id_=client_id,
given_name=given_name,
@@ -1105,7 +1115,9 @@ def _create_client_for_kernel(self, connection_file, hostname, sshkey,
css_path=self.css_path,
configuration=self.CONFIGURATION,
handlers=self.registered_spyder_kernel_handlers,
- std_dir=std_dir)
+ stderr_obj=stderr_obj,
+ stdout_obj=stdout_obj,
+ fault_obj=fault_obj)
# Create kernel client
kernel_client = QtKernelClient(connection_file=connection_file)
@@ -1489,12 +1501,19 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
self.master_clients += 1
client_id = dict(int_id=str(self.master_clients),
str_id='A')
- cf = self._new_connection_file()
+ std_dir = self._test_dir if self._test_dir else None
+ cf, km, kc, stderr_obj, stdout_obj = self.get_new_kernel(
+ is_cython, is_pylab, is_sympy, std_dir=std_dir)
+
+ if cf is not None:
+ fault_obj = StdFile(cf, '.fault', std_dir)
+ else:
+ fault_obj = None
+
show_elapsed_time = self.get_conf('show_elapsed_time')
reset_warning = self.get_conf('show_reset_namespace_warning')
ask_before_restart = self.get_conf('ask_before_restart')
ask_before_closing = self.get_conf('ask_before_closing')
- std_dir = self._test_dir if self._test_dir else None
client = ClientWidget(self, id_=client_id,
history_filename=get_conf_path('history.py'),
config_options=self.config_options(),
@@ -1514,7 +1533,9 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
css_path=self.css_path,
configuration=self.CONFIGURATION,
handlers=self.registered_spyder_kernel_handlers,
- std_dir=std_dir)
+ stderr_obj=stderr_obj,
+ stdout_obj=stdout_obj,
+ fault_obj=fault_obj)
self.add_tab(
client, name=client.get_name(), filename=filename,
@@ -1561,8 +1582,7 @@ def create_new_client(self, give_focus=True, filename='', is_cython=False,
)
return
- self.connect_client_to_kernel(client, is_cython=is_cython,
- is_pylab=is_pylab, is_sympy=is_sympy)
+ self.connect_client_to_kernel(client, km, kc)
if client.shellwidget.kernel_manager is None:
return
self.register_client(client, give_focus=give_focus)
@@ -1590,23 +1610,95 @@ def create_client_for_kernel(self):
self._create_client_for_kernel(connection_file, hostname, sshkey,
password)
- def connect_client_to_kernel(self, client, is_cython=False,
- is_pylab=False, is_sympy=False):
- """Connect a client to its kernel."""
- connection_file = client.connection_file
- stderr_handle = (
- None if self._test_no_stderr else client.stderr_obj.handle)
- stdout_handle = (
- None if self._test_no_stderr else client.stdout_obj.handle)
+ def get_new_kernel(self, is_cython=False, is_pylab=False,
+ is_sympy=False, std_dir=None):
+ """Get a new kernel, and cache one for next time."""
+ # Cache another kernel for next time.
+ kernel_spec = self.create_kernel_spec(
+ is_cython=is_cython,
+ is_pylab=is_pylab,
+ is_sympy=is_sympy
+ )
+
+ new_kernel = self.create_new_kernel(kernel_spec, std_dir)
+ if new_kernel[2] is None:
+ # error
+ self.close_cached_kernel()
+ return new_kernel
+
+ # Check cached kernel has the same configuration as is being asked
+ cached_kernel = None
+ if self._cached_kernel_properties is not None:
+ (cached_spec,
+ cached_env,
+ cached_argv,
+ cached_dir,
+ cached_kernel) = self._cached_kernel_properties
+ # Call interrupt_mode so the dict will be the same
+ kernel_spec.interrupt_mode
+ cached_spec.interrupt_mode
+ valid = (std_dir == cached_dir
+ and cached_spec.__dict__ == kernel_spec.__dict__
+ and kernel_spec.argv == cached_argv
+ and kernel_spec.env == cached_env)
+ if not valid:
+ # Close the kernel
+ self.close_cached_kernel()
+ cached_kernel = None
+
+ # Cache the new kernel
+ self._cached_kernel_properties = (
+ kernel_spec,
+ kernel_spec.env,
+ kernel_spec.argv,
+ std_dir,
+ new_kernel)
+
+ if cached_kernel is None:
+ return self.create_new_kernel(kernel_spec, std_dir)
+
+ return cached_kernel
+
+ def close_cached_kernel(self):
+ """Close the cached kernel."""
+ if self._cached_kernel_properties is None:
+ return
+ cached_kernel = self._cached_kernel_properties[-1]
+ _, kernel_manager, _, stderr_obj, stdout_obj = cached_kernel
+ kernel_manager.stop_restarter()
+ kernel_manager.shutdown_kernel(now=True)
+ self._cached_kernel_properties = None
+ if stderr_obj:
+ stderr_obj.remove()
+ if stdout_obj:
+ stdout_obj.remove()
+
+ def create_new_kernel(self, kernel_spec, std_dir=None):
+ """Create a new kernel."""
+ connection_file = self._new_connection_file()
+ if connection_file is None:
+ return None, None, None, None, None
+
+ stderr_obj = None
+ stderr_handle = None
+ stdout_obj = None
+ stdout_handle = None
+ if not self._test_no_stderr:
+ stderr_obj = StdFile(connection_file, '.stderr', std_dir)
+ stderr_handle = stderr_obj.handle
+ stdout_obj = StdFile(connection_file, '.stdout', std_dir)
+ stdout_handle = stdout_obj.handle
+
km, kc = self.create_kernel_manager_and_kernel_client(
connection_file,
stderr_handle,
stdout_handle,
- is_cython=is_cython,
- is_pylab=is_pylab,
- is_sympy=is_sympy,
+ kernel_spec,
)
+ return connection_file, km, kc, stderr_obj, stdout_obj
+ def connect_client_to_kernel(self, client, km, kc):
+ """Connect a client to its kernel."""
# An error occurred if this is True
if isinstance(km, str) and kc is None:
client.shellwidget.kernel_manager = None
@@ -1821,6 +1913,8 @@ def close_clients(self):
open_clients.remove(client)
# Close all closing shellwidgets.
ShellWidget.wait_all_shutdown()
+ # Close cached kernel
+ self.close_cached_kernel()
return True
def get_client_index_from_id(self, client_id):
@@ -1965,15 +2059,8 @@ def create_kernel_spec(self, is_cython=False,
def create_kernel_manager_and_kernel_client(self, connection_file,
stderr_handle,
stdout_handle,
- is_cython=False,
- is_pylab=False,
- is_sympy=False):
+ kernel_spec):
"""Create kernel manager and client."""
- # Kernel spec
- kernel_spec = self.create_kernel_spec(is_cython=is_cython,
- is_pylab=is_pylab,
- is_sympy=is_sympy)
-
# Kernel manager
try:
kernel_manager = SpyderKernelManager(