From feaa430bfd5d282a415bf39f5e822bef44103b51 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 30 Jan 2024 13:20:54 -0800
Subject: [PATCH 01/35] Update UI with toggle button to prioritize
spyder_pythonpath with respect to sys.path.
---
spyder/config/main.py | 1 +
.../plugins/pythonpath/widgets/pathmanager.py | 43 ++++++++++++++++---
2 files changed, 37 insertions(+), 7 deletions(-)
diff --git a/spyder/config/main.py b/spyder/config/main.py
index ec6f7fe8df0..078db928075 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -116,6 +116,7 @@
('pythonpath_manager',
{
'spyder_pythonpath': [],
+ 'prioritize': False,
}),
('quick_layouts',
{
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index c04994d7657..31e7d9ab7ef 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -44,13 +44,14 @@ class PathManagerToolbuttons:
AddPath = 'add_path'
RemovePath = 'remove_path'
ExportPaths = 'export_paths'
+ Prioritize = 'prioritize'
class PathManager(QDialog, SpyderWidgetMixin):
"""Path manager dialog."""
redirect_stdio = Signal(bool)
- sig_path_changed = Signal(object)
+ sig_path_changed = Signal(object, bool)
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
@@ -79,6 +80,8 @@ def __init__(self, parent, path=None, project_path=None,
self.system_path = ()
self.user_path = []
+ self.original_prioritize = None
+
# This is necessary to run our tests
if self.path:
self.update_paths(system_path=get_system_pythonpath())
@@ -91,6 +94,7 @@ def __init__(self, parent, path=None, project_path=None,
self.movedown_button = None
self.movebottom_button = None
self.export_button = None
+ self.prioritize_button = None
self.user_header = None
self.project_header = None
self.system_header = None
@@ -108,6 +112,9 @@ def __init__(self, parent, path=None, project_path=None,
self.setWindowIcon(self.create_icon('pythonpath'))
self.resize(500, 400)
self.export_button.setVisible(os.name == 'nt' and sync)
+ self.prioritize_button.setChecked(
+ self.get_conf('prioritize', default=False)
+ )
# Description
description = QLabel(
@@ -190,12 +197,20 @@ def _setup_right_toolbar(self):
icon=self.create_icon('fileexport'),
triggered=self.export_pythonpath,
tip=_("Export to PYTHONPATH environment variable"))
+ self.prioritize_button = self.create_toolbutton(
+ PathManagerToolbuttons.Prioritize,
+ icon=self.create_icon('first_page'),
+ option='prioritize',
+ triggered=self.prioritize,
+ tip=_("Place PYTHONPATH at the front of sys.path"))
+ self.prioritize_button.setCheckable(True)
self.selection_widgets = [self.movetop_button, self.moveup_button,
self.movedown_button, self.movebottom_button]
return (
[self.add_button, self.remove_button] +
- self.selection_widgets + [self.export_button]
+ self.selection_widgets + [self.export_button] +
+ [self.prioritize_button]
)
def _create_item(self, path):
@@ -334,6 +349,7 @@ def setup(self):
self.listwidget.setCurrentRow(0)
self.original_path_dict = self.get_path_dict()
+ self.original_prioritize = self.get_conf('prioritize', default=False)
self.refresh()
@Slot()
@@ -462,7 +478,7 @@ def refresh(self):
# Enable remove button only for user paths
self.remove_button.setEnabled(
- not current_item in self.headers
+ current_item not in self.headers
and (self.editable_top_row <= row <= self.editable_bottom_row)
)
@@ -471,6 +487,7 @@ def refresh(self):
# Ok button only enabled if actual changes occur
self.button_ok.setEnabled(
self.original_path_dict != self.get_path_dict()
+ or self.original_prioritize != self.prioritize_button.isChecked()
)
@Slot()
@@ -602,6 +619,10 @@ def move_to(self, absolute=None, relative=None):
self.user_path = self.get_user_path()
self.refresh()
+ def prioritize(self):
+ """Toggle prioritize setting."""
+ self.refresh()
+
def current_row(self):
"""Returns the current row of the list."""
return self.listwidget.currentRow()
@@ -632,14 +653,21 @@ def _update_system_path(self):
system paths are different.
"""
if self.system_path != self.get_conf('system_path', default=()):
- self.sig_path_changed.emit(self.get_path_dict())
+ self.sig_path_changed.emit(
+ self.get_path_dict(),
+ self.get_conf('prioritize', default=False)
+ )
self.set_conf('system_path', self.system_path)
def accept(self):
"""Override Qt method."""
path_dict = self.get_path_dict()
- if self.original_path_dict != path_dict:
- self.sig_path_changed.emit(path_dict)
+ prioritize = self.prioritize_button.isChecked()
+ if (
+ self.original_path_dict != path_dict
+ or self.original_prioritize != prioritize
+ ):
+ self.sig_path_changed.emit(path_dict, prioritize)
super().accept()
def reject(self):
@@ -662,7 +690,8 @@ def test():
project_path=tuple(sys.path[-2:]),
)
- def callback(path_dict):
+ def callback(path_dict, prioritize):
+ sys.stdout.write(f"prioritize: {prioritize}\n")
sys.stdout.write(str(path_dict))
dlg.sig_path_changed.connect(callback)
From e90bc11a3c9f1e56047fd4166d8b69c467092172 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 6 Feb 2024 08:44:19 -0800
Subject: [PATCH 02/35] Add path priority to pythonpath_manager plugin
container
---
spyder/plugins/pythonpath/container.py | 37 +++++++++++++++----
.../plugins/pythonpath/widgets/pathmanager.py | 2 +
2 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 84baa967280..951dabb55d9 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -41,6 +41,7 @@ def __init__(self, *args, **kwargs):
self.path = ()
self.not_active_path = ()
self.project_path = ()
+ self.prioritize = None
# ---- PluginMainContainer API
# -------------------------------------------------------------------------
@@ -175,7 +176,10 @@ def _load_pythonpath(self):
name for name in not_active_paths if osp.isdir(name)
)
- def _save_paths(self, new_path_dict):
+ # Load prioritize
+ self.prioritize = self.get_conf('prioritize', default=False)
+
+ def _save_paths(self, new_path_dict, new_prioritize):
"""
Save tuples for all paths and not active ones to config system and
update their associated attributes.
@@ -183,21 +187,37 @@ def _save_paths(self, new_path_dict):
`new_path_dict` is an OrderedDict that has the new paths as keys and
the state as values. The state is `True` for active and `False` for
inactive.
+
+ `prioritize` is a boolean indicating whether paths should be
+ prioritized over sys.path.
"""
path = tuple(p for p in new_path_dict)
not_active_path = tuple(
p for p in new_path_dict if not new_path_dict[p]
)
+ old_spyder_pythonpath = self.get_spyder_pythonpath()
# Don't set options unless necessary
if path != self.path:
+ logger.debug(f"Saving path: {path}")
self.set_conf('path', path)
self.path = path
if not_active_path != self.not_active_path:
+ logger.debug(f"Saving inactive paths: {not_active_path}")
self.set_conf('not_active_path', not_active_path)
self.not_active_path = not_active_path
+ if new_prioritize != self.prioritize:
+ logger.debug(f"Saving prioritize: {new_prioritize}")
+ self.set_conf('prioritize', new_prioritize)
+ self.prioritize = new_prioritize
+
+ new_spyder_pythonpath = self.get_spyder_pythonpath()
+ if new_spyder_pythonpath != old_spyder_pythonpath:
+ logger.debug(f"Saving Spyder pythonpath: {new_spyder_pythonpath}")
+ self.set_conf('spyder_pythonpath', new_spyder_pythonpath)
+
def _get_spyder_pythonpath_dict(self):
"""
Return Spyder PYTHONPATH plus project path as dictionary of paths.
@@ -220,7 +240,7 @@ def _get_spyder_pythonpath_dict(self):
return path_dict
- def _update_python_path(self, new_path_dict=None):
+ def _update_python_path(self, new_path_dict=None, new_prioritize=None):
"""
Update Python path on language server and kernels.
@@ -228,19 +248,20 @@ def _update_python_path(self, new_path_dict=None):
"""
# Load existing path plus project path
old_path_dict_p = self._get_spyder_pythonpath_dict()
+ old_prioritize = self.prioritize
# Save new path
- if new_path_dict is not None:
- self._save_paths(new_path_dict)
+ if new_path_dict is not None or new_prioritize is not None:
+ self._save_paths(new_path_dict, new_prioritize)
# Load new path plus project path
new_path_dict_p = self._get_spyder_pythonpath_dict()
# Do not notify observers unless necessary
- if new_path_dict_p != old_path_dict_p:
- pypath = self.get_spyder_pythonpath()
- logger.debug(f"Update Pythonpath to {pypath}")
- self.set_conf('spyder_pythonpath', pypath)
+ if (
+ new_path_dict_p != old_path_dict_p
+ or new_prioritize != old_prioritize
+ ):
self.sig_pythonpath_changed.emit(old_path_dict_p, new_path_dict_p)
def _migrate_to_config_options(self):
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 31e7d9ab7ef..676c8cb639c 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -652,6 +652,8 @@ def _update_system_path(self):
Request to update path values on main window if current and previous
system paths are different.
"""
+ # !!! If system path changed, then all changes made by user will be
+ # applied even if though the user cancelled or closed the widget.
if self.system_path != self.get_conf('system_path', default=()):
self.sig_path_changed.emit(
self.get_path_dict(),
From ae0c96783fb35af5a8e137af3eb30e1f889047c7 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 31 Jan 2024 09:18:10 -0800
Subject: [PATCH 03/35] Add path priority to pythonpath_manager
sig_pythonpath_changed signal
---
spyder/plugins/pythonpath/container.py | 12 +++++++++---
spyder/plugins/pythonpath/plugin.py | 5 ++++-
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 951dabb55d9..bb372225400 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -34,7 +34,7 @@ class PythonpathActions:
# -----------------------------------------------------------------------------
class PythonpathContainer(PluginMainContainer):
- sig_pythonpath_changed = Signal(object, object)
+ sig_pythonpath_changed = Signal(object, object, bool)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -100,9 +100,13 @@ def update_active_project_path(self, path):
# New path
new_path_dict_p = self._get_spyder_pythonpath_dict()
+ prioritize = self.get_conf('prioritize', default=False)
+
# Update path
self.set_conf('spyder_pythonpath', self.get_spyder_pythonpath())
- self.sig_pythonpath_changed.emit(old_path_dict_p, new_path_dict_p)
+ self.sig_pythonpath_changed.emit(
+ old_path_dict_p, new_path_dict_p, prioritize
+ )
def show_path_manager(self):
"""Show path manager dialog."""
@@ -262,7 +266,9 @@ def _update_python_path(self, new_path_dict=None, new_prioritize=None):
new_path_dict_p != old_path_dict_p
or new_prioritize != old_prioritize
):
- self.sig_pythonpath_changed.emit(old_path_dict_p, new_path_dict_p)
+ self.sig_pythonpath_changed.emit(
+ old_path_dict_p, new_path_dict_p, new_prioritize
+ )
def _migrate_to_config_options(self):
"""
diff --git a/spyder/plugins/pythonpath/plugin.py b/spyder/plugins/pythonpath/plugin.py
index 879bedd1eb9..e13dc4e76a1 100644
--- a/spyder/plugins/pythonpath/plugin.py
+++ b/spyder/plugins/pythonpath/plugin.py
@@ -34,7 +34,7 @@ class PythonpathManager(SpyderPluginV2):
CONF_SECTION = NAME
CONF_FILE = False
- sig_pythonpath_changed = Signal(object, object)
+ sig_pythonpath_changed = Signal(object, object, bool)
"""
This signal is emitted when there is a change in the Pythonpath handled by
Spyder.
@@ -50,6 +50,9 @@ class PythonpathManager(SpyderPluginV2):
new_path_dict: OrderedDict
New Pythonpath dictionary.
+ prioritize
+ Whether to prioritize Pythonpath in sys.path
+
See Also
--------
:py:meth:`.PythonpathContainer._get_spyder_pythonpath_dict`
From d313ed5370b90e333b573148e94e20a223f0ef88 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 6 Feb 2024 08:45:00 -0800
Subject: [PATCH 04/35] Add path priority to IPython Console plugin Remove
SPY_PYTHONPATH; run update_syspath on setup_spyder_kernel.
I think this would be much cleaner if the the emitted signal carried old/new spyder_pythonpath instead of the dictionary. I don't know of any plugin listening for sig_pythonpath_changed that requires the dictionary version.
---
spyder/plugins/ipythonconsole/plugin.py | 8 +++++---
spyder/plugins/ipythonconsole/utils/kernelspec.py | 7 -------
.../plugins/ipythonconsole/widgets/main_widget.py | 4 ++--
spyder/plugins/ipythonconsole/widgets/shell.py | 14 ++++++++++++--
4 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index cd7374b3789..630793ed7be 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -496,7 +496,7 @@ def on_main_menu_teardown(self):
mainmenu.remove_item_from_application_menu(
IPythonConsoleWidgetMenus.Documentation,
menu_id=ApplicationMenus.Help
- )
+ )
@on_plugin_teardown(plugin=Plugins.Editor)
def on_editor_teardown(self):
@@ -982,7 +982,7 @@ def save_working_directory(self, dirname):
"""
self.get_widget().save_working_directory(dirname)
- def update_path(self, path_dict, new_path_dict):
+ def update_path(self, path_dict, new_path_dict, prioritize):
"""
Update path on consoles.
@@ -995,12 +995,14 @@ def update_path(self, path_dict, new_path_dict):
Corresponds to the previous state of the PYTHONPATH.
new_path_dict : dict
Corresponds to the new state of the PYTHONPATH.
+ prioritize : bool
+ Whether to prioritize PYTHONPATH in sys.path
Returns
-------
None.
"""
- self.get_widget().update_path(path_dict, new_path_dict)
+ self.get_widget().update_path(path_dict, new_path_dict, prioritize)
def restart(self):
"""
diff --git a/spyder/plugins/ipythonconsole/utils/kernelspec.py b/spyder/plugins/ipythonconsole/utils/kernelspec.py
index b29383fd136..428429fc0a3 100644
--- a/spyder/plugins/ipythonconsole/utils/kernelspec.py
+++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py
@@ -171,12 +171,6 @@ def env(self):
# Do not pass PYTHONPATH to kernels directly, spyder-ide/spyder#13519
env_vars.pop('PYTHONPATH', None)
- # List of paths declared by the user, plus project's path, to
- # add to PYTHONPATH
- pathlist = self.get_conf(
- 'spyder_pythonpath', default=[], section='pythonpath_manager')
- pypath = os.pathsep.join(pathlist)
-
# List of modules to exclude from our UMR
umr_namelist = self.get_conf(
'umr/namelist', section='main_interpreter')
@@ -198,7 +192,6 @@ def env(self):
'SPY_JEDI_O': self.get_conf('jedi_completer'),
'SPY_TESTING': running_under_pytest() or get_safe_mode(),
'SPY_HIDE_CMD': self.get_conf('hide_cmd_windows'),
- 'SPY_PYTHONPATH': pypath,
# This env var avoids polluting the OS default temp directory with
# files generated by `conda run`. It's restored/removed in the
# kernel after initialization.
diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py
index 5fd06d36a78..ccbc2d2cd3b 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2447,13 +2447,13 @@ def on_working_directory_changed(self, dirname):
if dirname and osp.isdir(dirname):
self.sig_current_directory_changed.emit(dirname)
- def update_path(self, path_dict, new_path_dict):
+ def update_path(self, path_dict, new_path_dict, prioritize):
"""Update path on consoles."""
logger.debug("Update sys.path in all console clients")
for client in self.clients:
shell = client.shellwidget
if shell is not None:
- shell.update_syspath(path_dict, new_path_dict)
+ shell.update_syspath(path_dict, new_path_dict, prioritize)
def get_active_project_path(self):
"""Get the active project path."""
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
index fb623608525..7cc8b5b9852 100644
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -413,6 +413,16 @@ def setup_spyder_kernel(self):
self.send_spyder_kernel_configuration()
+ # Update sys.path
+ paths = self.get_conf(
+ "spyder_pythonpath", section="pythonpath_manager"
+ )
+ prioritize = self.get_conf(
+ "prioritize", section="pythonpath_manager"
+ )
+ path_dict = {path: True for path in paths}
+ self.update_syspath(path_dict, path_dict, prioritize)
+
run_lines = self.get_conf('startup/run_lines')
if run_lines:
self.execute(run_lines, hidden=True)
@@ -712,14 +722,14 @@ def set_color_scheme(self, color_scheme, reset=True):
"color scheme", "dark" if not dark_color else "light"
)
- def update_syspath(self, path_dict, new_path_dict):
+ def update_syspath(self, path_dict, new_path_dict, prioritize):
"""Update sys.path contents in the kernel."""
# Prevent error when the kernel is not available and users open/close
# projects or use the Python path manager.
# Fixes spyder-ide/spyder#21563
if self.kernel_handler is not None:
self.call_kernel(interrupt=True, blocking=False).update_syspath(
- path_dict, new_path_dict
+ path_dict, new_path_dict, prioritize
)
def request_syspath(self):
From 88871b8f2921ebe3ecf37d852a4488595131160e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 5 Feb 2024 21:26:08 -0800
Subject: [PATCH 05/35] Add path priority to completions language server
---
spyder/config/lsp.py | 1 +
spyder/plugins/completion/api.py | 6 +++--
spyder/plugins/completion/plugin.py | 6 +++--
.../providers/languageserver/provider.py | 27 +++++++++----------
4 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/spyder/config/lsp.py b/spyder/config/lsp.py
index b391d006bd8..4194654b21b 100644
--- a/spyder/config/lsp.py
+++ b/spyder/config/lsp.py
@@ -79,6 +79,7 @@
'environment': None,
'extra_paths': [],
'env_vars': None,
+ 'prioritize': False,
# Until we have a graphical way for users to add modules to
# this option
'auto_import_modules': [
diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py
index f9de9a37526..6465850b7a2 100644
--- a/spyder/plugins/completion/api.py
+++ b/spyder/plugins/completion/api.py
@@ -1059,8 +1059,8 @@ def project_path_update(self, project_path: str, update_kind: str,
"""
pass
- @Slot(object, object)
- def python_path_update(self, previous_path, new_path):
+ @Slot(object, object, bool)
+ def python_path_update(self, previous_path, new_path, prioritize):
"""
Handle Python path updates on Spyder.
@@ -1070,6 +1070,8 @@ def python_path_update(self, previous_path, new_path):
Dictionary containing the previous Python path values.
new_path: Dict
Dictionary containing the current Python path values.
+ prioritize
+ Whether to prioritize Python path values in sys.path
"""
pass
diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py
index e519b58de24..d721d670dfa 100644
--- a/spyder/plugins/completion/plugin.py
+++ b/spyder/plugins/completion/plugin.py
@@ -124,9 +124,9 @@ class CompletionPlugin(SpyderPluginV2):
Name of the completion client.
"""
- sig_pythonpath_changed = Signal(object, object)
+ sig_pythonpath_changed = Signal(object, object, bool)
"""
- This signal is used to receive changes on the PythonPath.
+ This signal is used to receive changes on the PYTHONPATH.
Parameters
----------
@@ -134,6 +134,8 @@ class CompletionPlugin(SpyderPluginV2):
Previous PythonPath settings.
new_path: dict
New PythonPath settings.
+ prioritize
+ Whether to prioritize PYTHONPATH in sys.path
"""
_sig_interpreter_changed = Signal(str)
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
index e5d9f90e987..ed165384a92 100644
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -377,7 +377,6 @@ def project_path_update(self, project_path, update_kind, projects):
self.stop_completion_services_for_language(language)
self.start_completion_services_for_language(language)
-
def report_server_error(self, error):
"""Report server errors in our error report dialog."""
error_data = dict(
@@ -532,26 +531,22 @@ def shutdown(self):
for language in self.clients:
self.stop_completion_services_for_language(language)
- @Slot(object, object)
- def python_path_update(self, path_dict, new_path_dict):
+ @Slot(object, object, bool)
+ def python_path_update(self, path_dict, new_path_dict, prioritize):
"""
Update server configuration after a change in Spyder's Python
path.
`path_dict` corresponds to the previous state of the Python path.
`new_path_dict` corresponds to the new state of the Python path.
+ `prioritize` determines whether to prioritize Python path in sys.path.
"""
- # If path_dict and new_path_dict are the same, it means the change
- # was generated by opening or closing a project. In that case, we
- # don't need to request an update because that's done through the
- # addition/deletion of workspaces.
- update = True
- if path_dict == new_path_dict:
- update = False
-
- if update:
- logger.debug("Update server's sys.path")
- self.update_lsp_configuration(python_only=True)
+ # Opening/closing a project will create a diff between path_dict
+ # and new_path_dict, but we don't know if prioritize changed.
+ # sig_pythonpath_changed is only emitted if there is a change so we
+ # should always update the confguration when this method is called.
+ logger.debug("Update server's sys.path")
+ self.update_lsp_configuration(python_only=True)
@qdebounced(timeout=600)
def interpreter_changed(self, interpreter: str):
@@ -806,13 +801,15 @@ def generate_python_config(self):
# Jedi configuration
env_vars = os.environ.copy() # Ensure env is indepependent of PyLSP's
- env_vars.pop('PYTHONPATH', None)
jedi = {
'environment': self._interpreter,
'extra_paths': self.get_conf('spyder_pythonpath',
section='pythonpath_manager',
default=[]),
+ 'prioritize': self.get_conf('prioritize',
+ section='pythonpath_manager',
+ default=False),
'env_vars': env_vars,
}
jedi_completion = {
From 1b2ca9d4ba01ceef30aafff0216301e5199b989c Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 6 Feb 2024 00:06:40 -0800
Subject: [PATCH 06/35] Add test for prioritize button state
---
.../plugins/pythonpath/widgets/tests/test_pathmanager.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index f6e73a0e384..12070be0e95 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -277,6 +277,14 @@ def test_buttons_state(qtbot, pathmanager, tmpdir):
pathmanager.remove_path(True)
assert not pathmanager.button_ok.isEnabled()
+ # Check prioritize button
+ assert pathmanager.prioritize_button.isEnabled()
+ assert not pathmanager.prioritize_button.isChecked()
+ pathmanager.prioritize_button.animateClick()
+ qtbot.waitUntil(pathmanager.prioritize_button.isChecked)
+ assert pathmanager.prioritize_button.isChecked()
+ assert pathmanager.button_ok.isEnabled()
+
if __name__ == "__main__":
pytest.main([os.path.basename(__file__)])
From c72139401ff0e5fb41b45141677da522bdf8826f Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 6 Feb 2024 07:52:24 -0800
Subject: [PATCH 07/35] Update ipythonconsole plugin tests
test_ipythoncosonle.py had many failures on latest master; attempting CI=1 skipped many tests but hangs on test_pdb_ignore_lib[True]
---
spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py | 2 --
.../plugins/ipythonconsole/widgets/tests/test_kernelconnect.py | 2 ++
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
index 0503af6a6a0..35b1b187f8a 100644
--- a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
+++ b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
@@ -36,9 +36,7 @@ def test_kernel_pypath(tmpdir, default_interpreter):
kernel_spec = SpyderKernelSpec()
# Check that PYTHONPATH is not in our kernelspec
- # and pypath is in SPY_PYTHONPATH
assert 'PYTHONPATH' not in kernel_spec.env
- assert pypath in kernel_spec.env['SPY_PYTHONPATH']
# Restore default values
CONF.set('main_interpreter', 'default', True)
diff --git a/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py b/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
index dea1641bb93..5ee9220ec16 100644
--- a/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
+++ b/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
@@ -130,6 +130,7 @@ def test_connection_dialog_remembers_input_with_ssh_passphrase(
assert new_dlg.pn.text() == str(pytest.pn)
assert new_dlg.kf.text() == pytest.kf
if not running_in_ci():
+ # !!! This fails on latest master...
assert new_dlg.kfp.text() == pytest.kfp
@@ -182,6 +183,7 @@ def test_connection_dialog_remembers_input_with_password(
assert new_dlg.un.text() == pytest.un
assert new_dlg.pn.text() == str(pytest.pn)
if not running_in_ci():
+ # !!! This fails on latest master...
assert new_dlg.pw.text() == pytest.pw
From 8f08701c7b694ae6ea57d79cfcd4f5d91b965b9d Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 12 Feb 2024 15:14:56 -0800
Subject: [PATCH 08/35] Change sig_pythonpath_changed arguments from dictionary
to list of strings.
---
.../providers/languageserver/provider.py | 10 +++++-----
spyder/plugins/ipythonconsole/plugin.py | 8 ++++----
.../ipythonconsole/widgets/main_widget.py | 4 ++--
.../plugins/ipythonconsole/widgets/shell.py | 7 +++----
spyder/plugins/pythonpath/container.py | 20 +++++++++----------
5 files changed, 23 insertions(+), 26 deletions(-)
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
index ed165384a92..7480011d8a8 100644
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -532,17 +532,17 @@ def shutdown(self):
self.stop_completion_services_for_language(language)
@Slot(object, object, bool)
- def python_path_update(self, path_dict, new_path_dict, prioritize):
+ def python_path_update(self, old_path, new_path, prioritize):
"""
Update server configuration after a change in Spyder's Python
path.
- `path_dict` corresponds to the previous state of the Python path.
- `new_path_dict` corresponds to the new state of the Python path.
+ `old_path` corresponds to the previous state of the Python path.
+ `new_path` corresponds to the new state of the Python path.
`prioritize` determines whether to prioritize Python path in sys.path.
"""
- # Opening/closing a project will create a diff between path_dict
- # and new_path_dict, but we don't know if prioritize changed.
+ # Opening/closing a project will create a diff between old_path
+ # and new_path, but we don't know if prioritize changed.
# sig_pythonpath_changed is only emitted if there is a change so we
# should always update the confguration when this method is called.
logger.debug("Update server's sys.path")
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index 630793ed7be..b655c47a3c3 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -982,7 +982,7 @@ def save_working_directory(self, dirname):
"""
self.get_widget().save_working_directory(dirname)
- def update_path(self, path_dict, new_path_dict, prioritize):
+ def update_path(self, old_path, new_path, prioritize):
"""
Update path on consoles.
@@ -991,9 +991,9 @@ def update_path(self, path_dict, new_path_dict, prioritize):
Parameters
----------
- path_dict : dict
+ old_path : list of str
Corresponds to the previous state of the PYTHONPATH.
- new_path_dict : dict
+ new_path : list of str
Corresponds to the new state of the PYTHONPATH.
prioritize : bool
Whether to prioritize PYTHONPATH in sys.path
@@ -1002,7 +1002,7 @@ def update_path(self, path_dict, new_path_dict, prioritize):
-------
None.
"""
- self.get_widget().update_path(path_dict, new_path_dict, prioritize)
+ self.get_widget().update_path(old_path, new_path, prioritize)
def restart(self):
"""
diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py
index ccbc2d2cd3b..2399a9fb699 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2447,13 +2447,13 @@ def on_working_directory_changed(self, dirname):
if dirname and osp.isdir(dirname):
self.sig_current_directory_changed.emit(dirname)
- def update_path(self, path_dict, new_path_dict, prioritize):
+ def update_path(self, old_path, new_path, prioritize):
"""Update path on consoles."""
logger.debug("Update sys.path in all console clients")
for client in self.clients:
shell = client.shellwidget
if shell is not None:
- shell.update_syspath(path_dict, new_path_dict, prioritize)
+ shell.update_syspath(old_path, new_path, prioritize)
def get_active_project_path(self):
"""Get the active project path."""
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
index 7cc8b5b9852..e6fbf5e905d 100644
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -420,8 +420,7 @@ def setup_spyder_kernel(self):
prioritize = self.get_conf(
"prioritize", section="pythonpath_manager"
)
- path_dict = {path: True for path in paths}
- self.update_syspath(path_dict, path_dict, prioritize)
+ self.update_syspath(paths, paths, prioritize)
run_lines = self.get_conf('startup/run_lines')
if run_lines:
@@ -722,14 +721,14 @@ def set_color_scheme(self, color_scheme, reset=True):
"color scheme", "dark" if not dark_color else "light"
)
- def update_syspath(self, path_dict, new_path_dict, prioritize):
+ def update_syspath(self, path, new_path, prioritize):
"""Update sys.path contents in the kernel."""
# Prevent error when the kernel is not available and users open/close
# projects or use the Python path manager.
# Fixes spyder-ide/spyder#21563
if self.kernel_handler is not None:
self.call_kernel(interrupt=True, blocking=False).update_syspath(
- path_dict, new_path_dict, prioritize
+ path, new_path, prioritize
)
def request_syspath(self):
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index bb372225400..30cb2ed0cb2 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -91,22 +91,20 @@ def update_active_project_path(self, path):
path = (path,)
# Old path
- old_path_dict_p = self._get_spyder_pythonpath_dict()
+ old_path = self.get_spyder_pythonpath()
# Change project path
self.project_path = path
self.path_manager_dialog.project_path = path
# New path
- new_path_dict_p = self._get_spyder_pythonpath_dict()
+ new_path = self.get_spyder_pythonpath()
prioritize = self.get_conf('prioritize', default=False)
# Update path
- self.set_conf('spyder_pythonpath', self.get_spyder_pythonpath())
- self.sig_pythonpath_changed.emit(
- old_path_dict_p, new_path_dict_p, prioritize
- )
+ self.set_conf('spyder_pythonpath', new_path)
+ self.sig_pythonpath_changed.emit(old_path, new_path, prioritize)
def show_path_manager(self):
"""Show path manager dialog."""
@@ -248,10 +246,10 @@ def _update_python_path(self, new_path_dict=None, new_prioritize=None):
"""
Update Python path on language server and kernels.
- The new_path_dict should not include the project path.
+ The `new_path_dict` should not include the project path.
"""
# Load existing path plus project path
- old_path_dict_p = self._get_spyder_pythonpath_dict()
+ old_path = self.get_spyder_pythonpath()
old_prioritize = self.prioritize
# Save new path
@@ -259,15 +257,15 @@ def _update_python_path(self, new_path_dict=None, new_prioritize=None):
self._save_paths(new_path_dict, new_prioritize)
# Load new path plus project path
- new_path_dict_p = self._get_spyder_pythonpath_dict()
+ new_path = self.get_spyder_pythonpath()
# Do not notify observers unless necessary
if (
- new_path_dict_p != old_path_dict_p
+ new_path != old_path
or new_prioritize != old_prioritize
):
self.sig_pythonpath_changed.emit(
- old_path_dict_p, new_path_dict_p, new_prioritize
+ old_path, new_path, new_prioritize
)
def _migrate_to_config_options(self):
From 0c998e89aebc53bb963161f4e451512e42f4ce12 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 09:18:08 -0800
Subject: [PATCH 09/35] Add system_paths and user_paths to pythonpath_manager
configuration
---
spyder/config/main.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/spyder/config/main.py b/spyder/config/main.py
index 078db928075..62d465f0368 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -117,6 +117,8 @@
{
'spyder_pythonpath': [],
'prioritize': False,
+ 'system_paths': {},
+ 'user_paths': {},
}),
('quick_layouts',
{
From 1242efdeabcc4f0d6c29bafdc74bb89dece4dc9a Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 10:28:13 -0800
Subject: [PATCH 10/35] Convert (path, project_path, not_active_path) to
(user_paths, project_paths, system_paths) and dictionary type
---
.../plugins/pythonpath/widgets/pathmanager.py | 146 +++++++++---------
1 file changed, 74 insertions(+), 72 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 676c8cb639c..f2b7b38dec4 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -25,7 +25,7 @@
from spyder.api.widgets.dialogs import SpyderDialogButtonBox
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import _
-from spyder.plugins.pythonpath.utils import check_path, get_system_pythonpath
+from spyder.plugins.pythonpath.utils import check_path
from spyder.utils.environ import get_user_env, set_user_env
from spyder.utils.misc import getcwd_or_home
from spyder.utils.stylesheet import (
@@ -56,8 +56,8 @@ class PathManager(QDialog, SpyderWidgetMixin):
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
- def __init__(self, parent, path=None, project_path=None,
- not_active_path=None, sync=True):
+ def __init__(self, parent, user_paths=None, project_paths=None,
+ system_paths=None, sync=True):
"""Path manager dialog."""
if PYQT5 or PYQT6:
super().__init__(parent, class_parent=parent)
@@ -65,27 +65,22 @@ def __init__(self, parent, path=None, project_path=None,
QDialog.__init__(self, parent)
SpyderWidgetMixin.__init__(self, class_parent=parent)
- assert isinstance(path, (tuple, type(None)))
+ assert isinstance(user_paths, (OrderedDict, type(None)))
# Style
# NOTE: This needs to be here so all buttons are styled correctly
self.setStyleSheet(self._stylesheet)
- self.path = path or ()
- self.project_path = project_path or ()
- self.not_active_path = not_active_path or ()
+ self.user_paths = user_paths or OrderedDict()
+ self.project_paths = project_paths or OrderedDict()
+ self.system_paths = system_paths or OrderedDict()
self.last_path = getcwd_or_home()
self.original_path_dict = None
- self.system_path = ()
self.user_path = []
self.original_prioritize = None
- # This is necessary to run our tests
- if self.path:
- self.update_paths(system_path=get_system_pythonpath())
-
# Widgets
self.add_button = None
self.remove_button = None
@@ -213,19 +208,16 @@ def _setup_right_toolbar(self):
[self.prioritize_button]
)
- def _create_item(self, path):
+ def _create_item(self, path, active):
"""Helper to create a new list item."""
item = QListWidgetItem(path)
- if path in self.project_path:
+ if path in self.project_paths:
item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked)
- elif path in self.not_active_path:
- item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
- item.setCheckState(Qt.Unchecked)
else:
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
- item.setCheckState(Qt.Checked)
+ item.setCheckState(Qt.Checked if active else Qt.Unchecked)
return item
@@ -282,9 +274,9 @@ def editable_bottom_row(self):
bottom_row = 0
if self.project_header:
- bottom_row += len(self.project_path) + 1
+ bottom_row += len(self.project_paths) + 1
if self.user_header:
- bottom_row += len(self.user_path)
+ bottom_row += len(self.get_user_paths())
return bottom_row
@@ -294,7 +286,7 @@ def editable_top_row(self):
top_row = 0
if self.project_header:
- top_row += len(self.project_path) + 1
+ top_row += len(self.project_paths) + 1
if self.user_header:
top_row += 1
@@ -309,7 +301,7 @@ def setup(self):
self.system_header = None
# Project path
- if self.project_path:
+ if self.project_paths:
self.project_header, project_widget = (
self._create_header(_("Project path"))
)
@@ -317,12 +309,12 @@ def setup(self):
self.listwidget.addItem(self.project_header)
self.listwidget.setItemWidget(self.project_header, project_widget)
- for path in self.project_path:
- item = self._create_item(path)
+ for path, active in self.project_paths.items():
+ item = self._create_item(path, active)
self.listwidget.addItem(item)
# Paths added by the user
- if self.user_path:
+ if self.user_paths:
self.user_header, user_widget = (
self._create_header(_("User paths"))
)
@@ -330,12 +322,12 @@ def setup(self):
self.listwidget.addItem(self.user_header)
self.listwidget.setItemWidget(self.user_header, user_widget)
- for path in self.user_path:
- item = self._create_item(path)
+ for path, active in self.user_paths.items():
+ item = self._create_item(path, active)
self.listwidget.addItem(item)
# System path
- if self.system_path:
+ if self.system_paths:
self.system_header, system_widget = (
self._create_header(_("System PYTHONPATH"))
)
@@ -343,12 +335,11 @@ def setup(self):
self.listwidget.addItem(self.system_header)
self.listwidget.setItemWidget(self.system_header, system_widget)
- for path in self.system_path:
- item = self._create_item(path)
+ for path, active in self.system_paths.items():
+ item = self._create_item(path, active)
self.listwidget.addItem(item)
self.listwidget.setCurrentRow(0)
- self.original_path_dict = self.get_path_dict()
self.original_prioritize = self.get_conf('prioritize', default=False)
self.refresh()
@@ -401,50 +392,60 @@ def export_pythonpath(self):
env['PYTHONPATH'] = list(ppath)
set_user_env(env, parent=self)
- def get_path_dict(self, project_path=False):
- """
- Return an ordered dict with the path entries as keys and the active
- state as the value.
+ def get_user_paths(self):
+ """Get current user paths as displayed on listwidget."""
+ paths = OrderedDict()
- If `project_path` is True, its entries are also included.
- """
- odict = OrderedDict()
+ if self.user_header is None:
+ return paths
+
+ is_user_path = False
for row in range(self.listwidget.count()):
item = self.listwidget.item(row)
- path = item.text()
- if item not in self.headers:
- if path in self.project_path and not project_path:
- continue
- odict[path] = item.checkState() == Qt.Checked
+ if item in (self.project_header, self.system_header):
+ is_user_path = False
+ continue
+ if item is self.user_header:
+ is_user_path = True
+ continue
+ if not is_user_path:
+ continue
+
+ paths.update({item.text(): item.checkState() == Qt.Checked})
+
+ return paths
- return odict
+ def get_system_paths(self):
+ """Get current system paths as displayed on listwidget."""
+ paths = OrderedDict()
- def get_user_path(self):
- """Get current user path as displayed on listwidget."""
- user_path = []
+ if self.system_header is None:
+ return paths
+
+ is_sys_path = False
for row in range(self.listwidget.count()):
item = self.listwidget.item(row)
- path = item.text()
- if item not in self.headers:
- if path not in (self.project_path + self.system_path):
- user_path.append(path)
+ if item in (self.project_header, self.user_header):
+ is_sys_path = False
+ continue
+ if item is self.system_header:
+ is_sys_path = True
+ continue
+ if not is_sys_path:
+ continue
+
+ paths.update({item.text(): item.checkState() == Qt.Checked})
- return user_path
+ return paths
- def update_paths(self, path=None, not_active_path=None, system_path=None):
+ def update_paths(self, user_paths=None, project_paths=None, system_paths=None):
"""Update path attributes."""
- if path is not None:
- self.path = path
- if not_active_path is not None:
- self.not_active_path = not_active_path
- if system_path is not None:
- self.system_path = system_path
-
- previous_system_path = self.get_conf('system_path', ())
- self.user_path = [
- path for path in self.path
- if path not in (self.system_path + previous_system_path)
- ]
+ if user_paths is not None:
+ self.user_paths = user_paths
+ if project_paths is not None:
+ self.project_paths = project_paths
+ if system_paths is not None:
+ self.system_paths = system_paths
def refresh(self):
"""Refresh toolbar widgets."""
@@ -486,7 +487,8 @@ def refresh(self):
# Ok button only enabled if actual changes occur
self.button_ok.setEnabled(
- self.original_path_dict != self.get_path_dict()
+ self.user_paths != self.get_user_paths()
+ or self.system_paths != self.get_system_paths()
or self.original_prioritize != self.prioritize_button.isChecked()
)
@@ -508,7 +510,7 @@ def add_path(self, directory=None):
directory = osp.abspath(directory)
self.last_path = directory
- if directory in self.get_path_dict():
+ if directory in self.user_paths:
item = self.listwidget.findItems(directory, Qt.MatchExactly)[0]
item.setCheckState(Qt.Checked)
answer = QMessageBox.question(
@@ -541,7 +543,7 @@ def add_path(self, directory=None):
)
# Add new path
- item = self._create_item(directory)
+ item = self._create_item(directory, True)
self.listwidget.insertItem(self.editable_top_row, item)
self.listwidget.setCurrentRow(self.editable_top_row)
@@ -654,12 +656,12 @@ def _update_system_path(self):
"""
# !!! If system path changed, then all changes made by user will be
# applied even if though the user cancelled or closed the widget.
- if self.system_path != self.get_conf('system_path', default=()):
+ if self.system_paths != self.get_conf('system_paths', default=()):
self.sig_path_changed.emit(
self.get_path_dict(),
self.get_conf('prioritize', default=False)
)
- self.set_conf('system_path', self.system_path)
+ self.set_conf('system_paths', self.system_paths)
def accept(self):
"""Override Qt method."""
@@ -688,8 +690,8 @@ def test():
_ = qapplication()
dlg = PathManager(
None,
- path=tuple(sys.path[:1]),
- project_path=tuple(sys.path[-2:]),
+ user_paths={p: True for p in sys.path[:1]},
+ project_paths={p: True for p in sys.path[-2:]},
)
def callback(path_dict, prioritize):
From 5d57b805072d771c0467950bb8eb456b920ed3fa Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 10:54:06 -0800
Subject: [PATCH 11/35] Only set user_paths, project_paths, system_paths, and
prioritize in update_paths method and call setup in update-paths method.
This will allow the container to instantiate the PathManager widget before providing paths. Paths will not be retrieved or determined within the widget, only passed to it by the container.
---
.../plugins/pythonpath/widgets/pathmanager.py | 64 +++++++++----------
1 file changed, 31 insertions(+), 33 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index f2b7b38dec4..4be1e04fa3f 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -56,8 +56,7 @@ class PathManager(QDialog, SpyderWidgetMixin):
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
- def __init__(self, parent, user_paths=None, project_paths=None,
- system_paths=None, sync=True):
+ def __init__(self, parent, sync=True):
"""Path manager dialog."""
if PYQT5 or PYQT6:
super().__init__(parent, class_parent=parent)
@@ -65,22 +64,14 @@ def __init__(self, parent, user_paths=None, project_paths=None,
QDialog.__init__(self, parent)
SpyderWidgetMixin.__init__(self, class_parent=parent)
- assert isinstance(user_paths, (OrderedDict, type(None)))
-
# Style
# NOTE: This needs to be here so all buttons are styled correctly
self.setStyleSheet(self._stylesheet)
- self.user_paths = user_paths or OrderedDict()
- self.project_paths = project_paths or OrderedDict()
- self.system_paths = system_paths or OrderedDict()
-
self.last_path = getcwd_or_home()
self.original_path_dict = None
self.user_path = []
- self.original_prioritize = None
-
# Widgets
self.add_button = None
self.remove_button = None
@@ -107,9 +98,6 @@ def __init__(self, parent, user_paths=None, project_paths=None,
self.setWindowIcon(self.create_icon('pythonpath'))
self.resize(500, 400)
self.export_button.setVisible(os.name == 'nt' and sync)
- self.prioritize_button.setChecked(
- self.get_conf('prioritize', default=False)
- )
# Description
description = QLabel(
@@ -145,9 +133,6 @@ def __init__(self, parent, user_paths=None, project_paths=None,
self.bbox.accepted.connect(self.accept)
self.bbox.rejected.connect(self.reject)
- # Setup
- self.setup()
-
# ---- Private methods
# -------------------------------------------------------------------------
def _add_buttons_to_layout(self, widgets, layout):
@@ -196,7 +181,7 @@ def _setup_right_toolbar(self):
PathManagerToolbuttons.Prioritize,
icon=self.create_icon('first_page'),
option='prioritize',
- triggered=self.prioritize,
+ triggered=self.refresh,
tip=_("Place PYTHONPATH at the front of sys.path"))
self.prioritize_button.setCheckable(True)
@@ -339,8 +324,10 @@ def setup(self):
item = self._create_item(path, active)
self.listwidget.addItem(item)
+ # Prioritize
+ self.prioritize_button.setChecked(self.prioritize)
+
self.listwidget.setCurrentRow(0)
- self.original_prioritize = self.get_conf('prioritize', default=False)
self.refresh()
@Slot()
@@ -438,14 +425,26 @@ def get_system_paths(self):
return paths
- def update_paths(self, user_paths=None, project_paths=None, system_paths=None):
- """Update path attributes."""
- if user_paths is not None:
- self.user_paths = user_paths
- if project_paths is not None:
- self.project_paths = project_paths
- if system_paths is not None:
- self.system_paths = system_paths
+ def update_paths(
+ self,
+ project_paths=OrderedDict(),
+ user_paths=OrderedDict(),
+ system_paths=OrderedDict(),
+ prioritize=False
+ ):
+ """Update path attributes.
+
+ These attributes should only be set in this method and upon activating
+ the dialog. They should remain fixed while the dialog is active and are
+ used to compare with what is shown in the listwidget in order to detect
+ changes.
+ """
+ self.project_paths = project_paths
+ self.user_paths = user_paths
+ self.system_paths = system_paths
+ self.prioritize = prioritize
+
+ self.setup()
def refresh(self):
"""Refresh toolbar widgets."""
@@ -489,7 +488,7 @@ def refresh(self):
self.button_ok.setEnabled(
self.user_paths != self.get_user_paths()
or self.system_paths != self.get_system_paths()
- or self.original_prioritize != self.prioritize_button.isChecked()
+ or self.prioritize != self.prioritize_button.isChecked()
)
@Slot()
@@ -621,10 +620,6 @@ def move_to(self, absolute=None, relative=None):
self.user_path = self.get_user_path()
self.refresh()
- def prioritize(self):
- """Toggle prioritize setting."""
- self.refresh()
-
def current_row(self):
"""Returns the current row of the list."""
return self.listwidget.currentRow()
@@ -690,8 +685,11 @@ def test():
_ = qapplication()
dlg = PathManager(
None,
- user_paths={p: True for p in sys.path[:1]},
- project_paths={p: True for p in sys.path[-2:]},
+ )
+ dlg.update_paths(
+ user_paths={p: True for p in sys.path[1:-2]},
+ project_paths={p: True for p in sys.path[:1]},
+ system_paths={p: True for p in sys.path[-2:]}
)
def callback(path_dict, prioritize):
From f769b62798c2bb1a81d67df1130122c81e7e1ac0 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 11:02:04 -0800
Subject: [PATCH 12/35] Send new user paths, system paths, and prioritize back
to container.
These will be dictionaries and the container will handle updating the pythonpath_manager configuration and assembling the final spyder_pythonpath. There is no need for _update_system_path method because the container will handle updates to the underlying system path. Again, the widget will only handle user-interactive changes.
---
.../plugins/pythonpath/widgets/pathmanager.py | 53 +++++++++----------
1 file changed, 25 insertions(+), 28 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 4be1e04fa3f..23dbd0f664a 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -51,7 +51,7 @@ class PathManager(QDialog, SpyderWidgetMixin):
"""Path manager dialog."""
redirect_stdio = Signal(bool)
- sig_path_changed = Signal(object, bool)
+ sig_path_changed = Signal(object, object, bool)
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
@@ -69,7 +69,6 @@ def __init__(self, parent, sync=True):
self.setStyleSheet(self._stylesheet)
self.last_path = getcwd_or_home()
- self.original_path_dict = None
self.user_path = []
# Widgets
@@ -644,37 +643,27 @@ def count(self):
# ---- Qt methods
# -------------------------------------------------------------------------
- def _update_system_path(self):
- """
- Request to update path values on main window if current and previous
- system paths are different.
- """
- # !!! If system path changed, then all changes made by user will be
- # applied even if though the user cancelled or closed the widget.
- if self.system_paths != self.get_conf('system_paths', default=()):
- self.sig_path_changed.emit(
- self.get_path_dict(),
- self.get_conf('prioritize', default=False)
- )
- self.set_conf('system_paths', self.system_paths)
-
def accept(self):
"""Override Qt method."""
- path_dict = self.get_path_dict()
- prioritize = self.prioritize_button.isChecked()
- if (
- self.original_path_dict != path_dict
- or self.original_prioritize != prioritize
- ):
- self.sig_path_changed.emit(path_dict, prioritize)
+ self.sig_path_changed.emit(
+ self.get_user_paths(),
+ self.get_system_paths(),
+ self.prioritize_button.isChecked()
+ )
super().accept()
def reject(self):
- self._update_system_path()
+ # Send back original paths (system_paths may be updated)
+ self.sig_path_changed.emit(
+ self.user_paths, self.system_paths, self.prioritize
+ )
super().reject()
def closeEvent(self, event):
- self._update_system_path()
+ # Send back original paths (system_paths may be updated)
+ self.sig_path_changed.emit(
+ self.user_paths, self.system_paths, self.prioritize
+ )
super().closeEvent(event)
@@ -692,9 +681,17 @@ def test():
system_paths={p: True for p in sys.path[-2:]}
)
- def callback(path_dict, prioritize):
- sys.stdout.write(f"prioritize: {prioritize}\n")
- sys.stdout.write(str(path_dict))
+ def callback(user_paths, system_paths, prioritize):
+ sys.stdout.write(f"Prioritize: {prioritize}")
+ sys.stdout.write("\n---- User paths ----\n")
+ sys.stdout.write(
+ '\n'.join([f'{k}: {v}' for k, v in user_paths.items()])
+ )
+ sys.stdout.write("\n---- System paths ----\n")
+ sys.stdout.write(
+ '\n'.join([f'{k}: {v}' for k, v in system_paths.items()])
+ )
+ sys.stdout.write('\n')
dlg.sig_path_changed.connect(callback)
sys.exit(dlg.exec_())
From 374b0a7babf964967350c90949b20a8ca927a457 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 11:30:46 -0800
Subject: [PATCH 13/35] Remove superfluous user_path attribute
---
spyder/plugins/pythonpath/widgets/pathmanager.py | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 23dbd0f664a..2c601851a89 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -69,7 +69,6 @@ def __init__(self, parent, sync=True):
self.setStyleSheet(self._stylesheet)
self.last_path = getcwd_or_home()
- self.user_path = []
# Widgets
self.add_button = None
@@ -508,7 +507,7 @@ def add_path(self, directory=None):
directory = osp.abspath(directory)
self.last_path = directory
- if directory in self.user_paths:
+ if directory in self.get_user_paths():
item = self.listwidget.findItems(directory, Qt.MatchExactly)[0]
item.setCheckState(Qt.Checked)
answer = QMessageBox.question(
@@ -516,7 +515,7 @@ def add_path(self, directory=None):
_("Add path"),
_("This directory is already included in the list."
"
"
- "Do you want to move it to the top of it?"),
+ "Do you want to move it to the top of the list?"),
QMessageBox.Yes | QMessageBox.No)
if answer == QMessageBox.Yes:
@@ -544,8 +543,6 @@ def add_path(self, directory=None):
item = self._create_item(directory, True)
self.listwidget.insertItem(self.editable_top_row, item)
self.listwidget.setCurrentRow(self.editable_top_row)
-
- self.user_path.insert(0, directory)
else:
answer = QMessageBox.warning(
self,
@@ -582,15 +579,11 @@ def remove_path(self, force=False):
QMessageBox.Yes | QMessageBox.No)
if force or answer == QMessageBox.Yes:
- # Remove current item from user_path
- item = self.listwidget.currentItem()
- self.user_path.remove(item.text())
-
# Remove selected item from view
self.listwidget.takeItem(self.listwidget.currentRow())
# Remove user header if there are no more user paths
- if len(self.user_path) == 0:
+ if len(self.get_user_paths()) == 0:
self.listwidget.takeItem(
self.listwidget.row(self.user_header)
)
@@ -616,7 +609,6 @@ def move_to(self, absolute=None, relative=None):
self.listwidget.insertItem(new_index, item)
self.listwidget.setCurrentRow(new_index)
- self.user_path = self.get_user_path()
self.refresh()
def current_row(self):
From d49ff7c825b697d450c1aef6c122e007d23e3f20 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 14:18:47 -0800
Subject: [PATCH 14/35] Remove algorithm to save system PYTHONPATH. This will
be done in the container instead.
---
.../plugins/pythonpath/widgets/pathmanager.py | 32 +++----------------
1 file changed, 5 insertions(+), 27 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 2c601851a89..8483411222e 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -26,7 +26,6 @@
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import _
from spyder.plugins.pythonpath.utils import check_path
-from spyder.utils.environ import get_user_env, set_user_env
from spyder.utils.misc import getcwd_or_home
from spyder.utils.stylesheet import (
AppStyle,
@@ -52,6 +51,7 @@ class PathManager(QDialog, SpyderWidgetMixin):
redirect_stdio = Signal(bool)
sig_path_changed = Signal(object, object, bool)
+ sig_export_pythonpath = Signal(object, object, bool)
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
@@ -350,32 +350,10 @@ def export_pythonpath(self):
if answer == QMessageBox.Cancel:
return
- env = get_user_env()
-
- # This doesn't include the project path because it's a transient
- # directory, i.e. only used in Spyder and during specific
- # circumstances.
- active_path = [k for k, v in self.get_path_dict().items() if v]
-
- if answer == QMessageBox.Yes:
- ppath = active_path
- else:
- ppath = env.get('PYTHONPATH', [])
- if not isinstance(ppath, list):
- ppath = [ppath]
-
- ppath = [p for p in ppath if p not in active_path]
- ppath = ppath + active_path
-
- os.environ['PYTHONPATH'] = os.pathsep.join(ppath)
-
- # Update widget so changes are reflected on it immediately
- self.update_paths(system_path=tuple(ppath))
- self.set_conf('system_path', tuple(ppath))
- self.setup()
-
- env['PYTHONPATH'] = list(ppath)
- set_user_env(env, parent=self)
+ self.sig_export_pythonpath(
+ self.get_user_paths(), self.get_system_paths(),
+ answer == QMessageBox.Yes
+ )
def get_user_paths(self):
"""Get current user paths as displayed on listwidget."""
From 8d73b1c5f1b7ae5571951ad29a99e9d0bb6fd233 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Wed, 28 Feb 2024 19:00:37 -0800
Subject: [PATCH 15/35] Simplify get_user_paths and get_system_paths
---
.../plugins/pythonpath/widgets/pathmanager.py | 30 +++++--------------
1 file changed, 8 insertions(+), 22 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 8483411222e..c380edbb736 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -362,18 +362,13 @@ def get_user_paths(self):
if self.user_header is None:
return paths
- is_user_path = False
- for row in range(self.listwidget.count()):
- item = self.listwidget.item(row)
- if item in (self.project_header, self.system_header):
- is_user_path = False
- continue
- if item is self.user_header:
- is_user_path = True
- continue
- if not is_user_path:
- continue
+ start = self.listwidget.row(self.user_header) + 1
+ stop = self.listwidget.count()
+ if self.system_header is not None:
+ stop = self.listwidget.row(self.system_header)
+ for row in range(start, stop):
+ item = self.listwidget.item(row)
paths.update({item.text(): item.checkState() == Qt.Checked})
return paths
@@ -385,18 +380,9 @@ def get_system_paths(self):
if self.system_header is None:
return paths
- is_sys_path = False
- for row in range(self.listwidget.count()):
+ start = self.listwidget.row(self.system_header) + 1
+ for row in range(start, self.listwidget.count()):
item = self.listwidget.item(row)
- if item in (self.project_header, self.user_header):
- is_sys_path = False
- continue
- if item is self.system_header:
- is_sys_path = True
- continue
- if not is_sys_path:
- continue
-
paths.update({item.text(): item.checkState() == Qt.Checked})
return paths
From 262e39243bc353a2907cd949347310a4e92273b5 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 10:04:57 -0800
Subject: [PATCH 16/35] Update container attributes (path, not_active_path,
project_path, prioritize) -> (_user_paths, _system_paths, _project_paths,
_prioritize, _spyder_pythonpath). Path lists are now OrderedDict
* Simplifies _load_pythonpath -> _load_paths
* Move migration method from setup to _load_paths
---
spyder/plugins/pythonpath/container.py | 81 ++++++++++----------------
1 file changed, 30 insertions(+), 51 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 30cb2ed0cb2..0abaee848ca 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -38,24 +38,12 @@ class PythonpathContainer(PluginMainContainer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.path = ()
- self.not_active_path = ()
- self.project_path = ()
- self.prioritize = None
# ---- PluginMainContainer API
# -------------------------------------------------------------------------
def setup(self):
-
- # Migrate from old conf files to config options
- if self.get_conf('paths_in_conf_files', default=True):
- self._migrate_to_config_options()
-
- # Load Python path
- self._load_pythonpath()
-
- # Save current Pythonpath at startup so plugins can use it afterwards
- self.set_conf('spyder_pythonpath', self.get_spyder_pythonpath())
+ # Load Python paths
+ self._load_paths()
# Path manager dialog
self.path_manager_dialog = PathManager(parent=self, sync=True)
@@ -135,51 +123,42 @@ def get_spyder_pythonpath(self):
# ---- Private API
# -------------------------------------------------------------------------
- def _load_pythonpath(self):
- """Load Python paths."""
- # Get current system PYTHONPATH
- system_path = get_system_pythonpath()
-
- # Get previous system PYTHONPATH
- previous_system_path = self.get_conf('system_path', default=())
+ def _load_paths(self):
+ """Load Python paths.
- # Load all paths
- paths = []
- previous_paths = self.get_conf('path')
- for path in previous_paths:
- # Path was removed since last time or it's not a directory
- # anymore
- if not osp.isdir(path):
- continue
-
- # Path was removed from system path
- if path in previous_system_path and path not in system_path:
- continue
-
- paths.append(path)
+ The attributes _project_paths, _user_paths, _system_paths, _prioritize,
+ and _spyder_pythonpath, are initialize here and should be updated only
+ in _save_paths. They are only used to detect changes.
+ """
+ self._project_paths = OrderedDict()
+ self._user_paths = OrderedDict()
+ self._system_paths = OrderedDict()
+ self._prioritize = False
+ self._spyder_pythonpath = []
+
+ # Get user paths. Check migration from old conf files
+ user_paths = self._migrate_to_config_options()
+ if user_paths is None:
+ user_paths = self.get_conf('user_paths', {})
+ user_paths = OrderedDict(user_paths)
- self.path = tuple(paths)
+ # Get current system PYTHONPATH
+ system_paths = self._get_system_paths()
- # Update path option. This avoids loading paths that were removed in
- # this session in later ones.
- self.set_conf('path', self.path)
+ # Get prioritize
+ prioritize = self.get_conf('prioritize', False)
- # Update system path so that path_manager_dialog can work with its
- # latest contents.
- self.set_conf('system_path', system_path)
+ self._save_paths(user_paths, system_paths, prioritize)
- # Add system path
- if system_path:
- self.path = self.path + system_path
+ def _get_system_paths(self):
+ system_paths = get_system_pythonpath()
+ conf_system_paths = self.get_conf('system_paths', {})
- # Load not active paths
- not_active_paths = self.get_conf('not_active_path')
- self.not_active_path = tuple(
- name for name in not_active_paths if osp.isdir(name)
+ system_paths = OrderedDict(
+ {p: conf_system_paths.get(p, True) for p in system_paths}
)
- # Load prioritize
- self.prioritize = self.get_conf('prioritize', default=False)
+ return system_paths
def _save_paths(self, new_path_dict, new_prioritize):
"""
From 82ca63113d27f4f7dd868eaff294f9c3dbfd6a22 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 10:40:07 -0800
Subject: [PATCH 17/35] Revise configuration migration method.
* Promptly exits if remnants of old configuration are not present
* Removes remnants of old configuration if present
* Constructs user paths from old configuration remnants
---
spyder/plugins/pythonpath/container.py | 49 +++++++++++++++++++++++---
1 file changed, 45 insertions(+), 4 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 0abaee848ca..c97ab271a3c 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -9,6 +9,7 @@
from collections import OrderedDict
import logging
+import os
import os.path as osp
from qtpy.QtCore import Signal
@@ -256,17 +257,57 @@ def _migrate_to_config_options(self):
"""
path_file = get_conf_path('path')
not_active_path_file = get_conf_path('not_active_path')
+ config_path = self.get_conf('path', None)
+ config_not_active_path = self.get_conf('not_active_path', None)
+ paths_in_conf_files = self.get_conf('paths_in_conf_files', None)
+ system_path = self.get_conf('system_path', None)
+
+ if (
+ not osp.isfile(path_file)
+ and not osp.isfile(not_active_path_file)
+ and config_path is not None
+ and config_not_active_path is not None
+ and paths_in_conf_files is not None
+ and system_path is not None
+ ):
+ # The configuration does not need to be updated
+ return None
path = []
+ not_active_path = []
+
+ # Get path from file
if osp.isfile(path_file):
with open(path_file, 'r', encoding='utf-8') as f:
path = f.read().splitlines()
+ os.remove(path_file)
- not_active_path = []
+ # Get inactive paths from file
if osp.isfile(not_active_path_file):
with open(not_active_path_file, 'r', encoding='utf-8') as f:
not_active_path = f.read().splitlines()
+ os.remove(not_active_path_file)
+
+ # Get path from config; supercedes paths from file
+ if config_path is not None:
+ path = config_path
+ self.remove_conf('path')
+
+ # Get inactive path from config; supercedes paths from file
+ if config_not_active_path is not None:
+ not_active_path = config_not_active_path
+ self.remove_conf('not_active_path')
+
+ if paths_in_conf_files is not None:
+ self.remove_conf('paths_in_conf_files')
+
+ # Get system path
+ if system_path is not None:
+ self.remove_conf('system_path')
+
+ # path config has all user and system paths; only want user paths
+ user_paths = {
+ p: p not in not_active_path for p in path if p not in system_path
+ }
- self.set_conf('path', tuple(path))
- self.set_conf('not_active_path', tuple(not_active_path))
- self.set_conf('paths_in_conf_files', False)
+ return user_paths
From 0481445b8052a554245f34f0f181625aa01f7bd0 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 10:58:05 -0800
Subject: [PATCH 18/35] Revise _save_paths
* Configuration keys and private attributes for user paths, system paths, prioritize, and spyder_pythonpath are set conditionally in this method and nowhere else.
* sig_pythonpath_changed is conditionally emitted from this method and nowhere else. This signal now sends only the spyder_pythonpath and prioritize, not the old spyder_pythonpath. Subscribers should update accordingly.
---
spyder/plugins/pythonpath/container.py | 105 ++++++++++---------------
1 file changed, 41 insertions(+), 64 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index c97ab271a3c..f03f64756c8 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -35,7 +35,7 @@ class PythonpathActions:
# -----------------------------------------------------------------------------
class PythonpathContainer(PluginMainContainer):
- sig_pythonpath_changed = Signal(object, object, bool)
+ sig_pythonpath_changed = Signal(object, bool)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -49,7 +49,7 @@ def setup(self):
# Path manager dialog
self.path_manager_dialog = PathManager(parent=self, sync=True)
self.path_manager_dialog.sig_path_changed.connect(
- self._update_python_path)
+ self._save_paths)
self.path_manager_dialog.redirect_stdio.connect(
self.sig_redirect_stdio_requested)
@@ -64,10 +64,6 @@ def setup(self):
def update_actions(self):
pass
- def on_close(self):
- # Save current system path to detect changes next time Spyder starts
- self.set_conf('system_path', get_system_pythonpath())
-
# ---- Public API
# -------------------------------------------------------------------------
def update_active_project_path(self, path):
@@ -161,44 +157,51 @@ def _get_system_paths(self):
return system_paths
- def _save_paths(self, new_path_dict, new_prioritize):
+ def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
"""
- Save tuples for all paths and not active ones to config system and
- update their associated attributes.
-
- `new_path_dict` is an OrderedDict that has the new paths as keys and
- the state as values. The state is `True` for active and `False` for
- inactive.
+ Save user and system path dictionaries to config and prioritize to
+ config. Each dictionary key is a path and the value is the active
+ state.
+ `user_paths` is user paths. `system_paths` is system paths, and
`prioritize` is a boolean indicating whether paths should be
- prioritized over sys.path.
+ prepended (True) or appended (False) to sys.path.
"""
- path = tuple(p for p in new_path_dict)
- not_active_path = tuple(
- p for p in new_path_dict if not new_path_dict[p]
- )
- old_spyder_pythonpath = self.get_spyder_pythonpath()
+ assert isinstance(user_paths, (type(None), OrderedDict))
+ assert isinstance(system_paths, (type(None), OrderedDict))
+ assert isinstance(prioritize, (type(None), bool))
+
+ emit = False
# Don't set options unless necessary
- if path != self.path:
- logger.debug(f"Saving path: {path}")
- self.set_conf('path', path)
- self.path = path
-
- if not_active_path != self.not_active_path:
- logger.debug(f"Saving inactive paths: {not_active_path}")
- self.set_conf('not_active_path', not_active_path)
- self.not_active_path = not_active_path
-
- if new_prioritize != self.prioritize:
- logger.debug(f"Saving prioritize: {new_prioritize}")
- self.set_conf('prioritize', new_prioritize)
- self.prioritize = new_prioritize
-
- new_spyder_pythonpath = self.get_spyder_pythonpath()
- if new_spyder_pythonpath != old_spyder_pythonpath:
- logger.debug(f"Saving Spyder pythonpath: {new_spyder_pythonpath}")
- self.set_conf('spyder_pythonpath', new_spyder_pythonpath)
+ if user_paths is not None and user_paths != self._user_paths:
+ logger.debug(f"Saving user paths: {user_paths}")
+ self.set_conf('user_paths', dict(user_paths))
+ self._user_paths = user_paths
+
+ if system_paths is not None and system_paths != self._system_paths:
+ logger.debug(f"Saving system paths: {system_paths}")
+ self.set_conf('system_paths', dict(system_paths))
+ self._system_paths = system_paths
+
+ if prioritize is not None and prioritize != self._prioritize:
+ logger.debug(f"Saving prioritize: {prioritize}")
+ self.set_conf('prioritize', prioritize)
+ self._prioritize = prioritize
+ emit = True
+
+ spyder_pythonpath = self.get_spyder_pythonpath()
+ if spyder_pythonpath != self._spyder_pythonpath:
+ logger.debug(f"Saving Spyder pythonpath: {spyder_pythonpath}")
+ self.set_conf('spyder_pythonpath', spyder_pythonpath)
+ self._spyder_pythonpath = spyder_pythonpath
+ emit = True
+
+ # Only emit signal if spyder_pythonpath or prioritize changed
+ if emit:
+ self.sig_pythonpath_changed.emit(
+ self._spyder_pythonpath, self._prioritize
+ )
def _get_spyder_pythonpath_dict(self):
"""
@@ -222,32 +225,6 @@ def _get_spyder_pythonpath_dict(self):
return path_dict
- def _update_python_path(self, new_path_dict=None, new_prioritize=None):
- """
- Update Python path on language server and kernels.
-
- The `new_path_dict` should not include the project path.
- """
- # Load existing path plus project path
- old_path = self.get_spyder_pythonpath()
- old_prioritize = self.prioritize
-
- # Save new path
- if new_path_dict is not None or new_prioritize is not None:
- self._save_paths(new_path_dict, new_prioritize)
-
- # Load new path plus project path
- new_path = self.get_spyder_pythonpath()
-
- # Do not notify observers unless necessary
- if (
- new_path != old_path
- or new_prioritize != old_prioritize
- ):
- self.sig_pythonpath_changed.emit(
- old_path, new_path, new_prioritize
- )
-
def _migrate_to_config_options(self):
"""
Migrate paths saved in the `path` and `not_active_path` files located
From 206f30e74a1af7d74db59f08276207466b91a467 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:02:18 -0800
Subject: [PATCH 19/35] Simplify get_spyder_pythonpath. spyder_pythonpath is
now straightforwardly constructed from project, user, and system paths
attributes.
---
spyder/plugins/pythonpath/container.py | 34 +++++---------------------
1 file changed, 6 insertions(+), 28 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index f03f64756c8..8dc42f4a67b 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -111,12 +111,12 @@ def show_path_manager(self):
self.path_manager_dialog.setFocus()
def get_spyder_pythonpath(self):
- """
- Return active Spyder PYTHONPATH plus project path as a list of paths.
- """
- path_dict = self._get_spyder_pythonpath_dict()
- path = [k for k, v in path_dict.items() if v]
- return path
+ """Return active Spyder PYTHONPATH as a list of paths."""
+ # Place project path first so that modules developed in a
+ # project are not shadowed by those present in other paths.
+ all_paths = self._project_paths | self._user_paths | self._system_paths
+
+ return [p for p, v in all_paths.items() if v]
# ---- Private API
# -------------------------------------------------------------------------
@@ -203,28 +203,6 @@ def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
self._spyder_pythonpath, self._prioritize
)
- def _get_spyder_pythonpath_dict(self):
- """
- Return Spyder PYTHONPATH plus project path as dictionary of paths.
-
- The returned ordered dictionary has the paths as keys and the state
- as values. The state is `True` for active and `False` for inactive.
-
- Example:
- OrderedDict([('/some/path, True), ('/some/other/path, False)])
- """
- path_dict = OrderedDict()
-
- # Make project path to be the first one so that modules developed in a
- # project are not shadowed by those present in other paths.
- for path in self.project_path:
- path_dict[path] = True
-
- for path in self.path:
- path_dict[path] = path not in self.not_active_path
-
- return path_dict
-
def _migrate_to_config_options(self):
"""
Migrate paths saved in the `path` and `not_active_path` files located
From f06f31edaecb2c6a4498304cf09cd4f46f9e4754 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:11:02 -0800
Subject: [PATCH 20/35] Simplify update_active_project_path.
sig_pythonpath_changed is emitted in _save_paths if spyder_pythonpath is
changed.
---
spyder/plugins/pythonpath/container.py | 30 +++++++++-----------------
1 file changed, 10 insertions(+), 20 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 8dc42f4a67b..394982706af 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -67,29 +67,19 @@ def update_actions(self):
# ---- Public API
# -------------------------------------------------------------------------
def update_active_project_path(self, path):
- """Update active project path."""
+ """Update active project path.
+
+ _project_paths is initialized and set here, and nowhere else.
+ """
+ self._project_paths = OrderedDict()
if path is None:
- logger.debug("Update Pythonpath because project was closed")
- path = ()
+ logger.debug("Update Spyder PYTHONPATH because project was closed")
else:
- logger.debug(f"Add to Pythonpath project's path -> {path}")
- path = (path,)
-
- # Old path
- old_path = self.get_spyder_pythonpath()
-
- # Change project path
- self.project_path = path
- self.path_manager_dialog.project_path = path
-
- # New path
- new_path = self.get_spyder_pythonpath()
-
- prioritize = self.get_conf('prioritize', default=False)
+ logger.debug(f"Add project paths to Spyder PYTHONPATH: {path}")
+ path = [path] if isinstance(path, str) else path
+ self._project_paths.update({p: True for p in path})
- # Update path
- self.set_conf('spyder_pythonpath', new_path)
- self.sig_pythonpath_changed.emit(old_path, new_path, prioritize)
+ self._save_paths()
def show_path_manager(self):
"""Show path manager dialog."""
From 38f4798bf4965c99c357fa9c4b1a5b35695e159b Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:12:56 -0800
Subject: [PATCH 21/35] Update show_path_manager method. Note that
PathManager.setup is called in PathManager.updat_paths
---
spyder/plugins/pythonpath/container.py | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 394982706af..2f136acd673 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -82,18 +82,24 @@ def update_active_project_path(self, path):
self._save_paths()
def show_path_manager(self):
- """Show path manager dialog."""
+ """Show path manager dialog.
+
+ Send the most up-to-date system paths to the dialog in case they have
+ changed. But do not _save_paths until after the dialog exits, in order
+ to consolodate possible changes and avoid emitting multiple signals.
+ This requires that the dialog return its original paths on cancel or
+ close.
+ """
# Do not update paths or run setup if widget is already open,
- # see spyder-ide/spyder#20808
+ # see spyder-ide/spyder#20808.
if not self.path_manager_dialog.isVisible():
- # Set main attributes saved here
self.path_manager_dialog.update_paths(
- self.path, self.not_active_path, get_system_pythonpath()
+ project_paths=self._project_paths,
+ user_paths=self._user_paths,
+ system_paths=self._get_system_paths(),
+ prioritize=self._prioritize
)
- # Setup its contents again
- self.path_manager_dialog.setup()
-
# Show and give it focus
self.path_manager_dialog.show()
self.path_manager_dialog.activateWindow()
From 3500f4502dc88d02211d086aa26cea240940faf3 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:53:55 -0800
Subject: [PATCH 22/35] Propagate changes to sig_pythonpath_changed to
pythonpath plugin
---
spyder/plugins/pythonpath/plugin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/pythonpath/plugin.py b/spyder/plugins/pythonpath/plugin.py
index e13dc4e76a1..950be5bf1d0 100644
--- a/spyder/plugins/pythonpath/plugin.py
+++ b/spyder/plugins/pythonpath/plugin.py
@@ -34,7 +34,7 @@ class PythonpathManager(SpyderPluginV2):
CONF_SECTION = NAME
CONF_FILE = False
- sig_pythonpath_changed = Signal(object, object, bool)
+ sig_pythonpath_changed = Signal(object, bool)
"""
This signal is emitted when there is a change in the Pythonpath handled by
Spyder.
From 846d779fde41612ff55b455ef61bee0f4aaba0eb Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:55:00 -0800
Subject: [PATCH 23/35] Propagate changes to sig_pythonpath_changed to
ipythonconsole plugin. Note that spyder-kernels must be updated to
accommodate.
---
spyder/plugins/ipythonconsole/plugin.py | 6 ++----
spyder/plugins/ipythonconsole/widgets/main_widget.py | 4 ++--
spyder/plugins/ipythonconsole/widgets/shell.py | 6 +++---
3 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index b655c47a3c3..831722fecec 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -982,7 +982,7 @@ def save_working_directory(self, dirname):
"""
self.get_widget().save_working_directory(dirname)
- def update_path(self, old_path, new_path, prioritize):
+ def update_path(self, new_path, prioritize):
"""
Update path on consoles.
@@ -991,8 +991,6 @@ def update_path(self, old_path, new_path, prioritize):
Parameters
----------
- old_path : list of str
- Corresponds to the previous state of the PYTHONPATH.
new_path : list of str
Corresponds to the new state of the PYTHONPATH.
prioritize : bool
@@ -1002,7 +1000,7 @@ def update_path(self, old_path, new_path, prioritize):
-------
None.
"""
- self.get_widget().update_path(old_path, new_path, prioritize)
+ self.get_widget().update_path(new_path, prioritize)
def restart(self):
"""
diff --git a/spyder/plugins/ipythonconsole/widgets/main_widget.py b/spyder/plugins/ipythonconsole/widgets/main_widget.py
index 2399a9fb699..cc2947d20cc 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2447,13 +2447,13 @@ def on_working_directory_changed(self, dirname):
if dirname and osp.isdir(dirname):
self.sig_current_directory_changed.emit(dirname)
- def update_path(self, old_path, new_path, prioritize):
+ def update_path(self, new_path, prioritize):
"""Update path on consoles."""
logger.debug("Update sys.path in all console clients")
for client in self.clients:
shell = client.shellwidget
if shell is not None:
- shell.update_syspath(old_path, new_path, prioritize)
+ shell.update_syspath(new_path, prioritize)
def get_active_project_path(self):
"""Get the active project path."""
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
index e6fbf5e905d..4b5112e6b24 100644
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -420,7 +420,7 @@ def setup_spyder_kernel(self):
prioritize = self.get_conf(
"prioritize", section="pythonpath_manager"
)
- self.update_syspath(paths, paths, prioritize)
+ self.update_syspath(paths, prioritize)
run_lines = self.get_conf('startup/run_lines')
if run_lines:
@@ -721,14 +721,14 @@ def set_color_scheme(self, color_scheme, reset=True):
"color scheme", "dark" if not dark_color else "light"
)
- def update_syspath(self, path, new_path, prioritize):
+ def update_syspath(self, new_paths, prioritize):
"""Update sys.path contents in the kernel."""
# Prevent error when the kernel is not available and users open/close
# projects or use the Python path manager.
# Fixes spyder-ide/spyder#21563
if self.kernel_handler is not None:
self.call_kernel(interrupt=True, blocking=False).update_syspath(
- path, new_path, prioritize
+ new_paths, prioritize
)
def request_syspath(self):
From 8d15eb4cdab9e08dc0d782914133df9c5f25fc48 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 11:56:26 -0800
Subject: [PATCH 24/35] Propagate changes to sig_pythonpath_changed to
completions plugin.
---
spyder/plugins/completion/api.py | 8 +++-----
spyder/plugins/completion/plugin.py | 6 ++----
.../completion/providers/languageserver/provider.py | 12 +++++++-----
3 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py
index 6465850b7a2..65cdedb99e6 100644
--- a/spyder/plugins/completion/api.py
+++ b/spyder/plugins/completion/api.py
@@ -1059,16 +1059,14 @@ def project_path_update(self, project_path: str, update_kind: str,
"""
pass
- @Slot(object, object, bool)
- def python_path_update(self, previous_path, new_path, prioritize):
+ @Slot(object, bool)
+ def python_path_update(self, new_path, prioritize):
"""
Handle Python path updates on Spyder.
Parameters
----------
- previous_path: Dict
- Dictionary containing the previous Python path values.
- new_path: Dict
+ new_path: list of str
Dictionary containing the current Python path values.
prioritize
Whether to prioritize Python path values in sys.path
diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py
index d721d670dfa..a77f66aa995 100644
--- a/spyder/plugins/completion/plugin.py
+++ b/spyder/plugins/completion/plugin.py
@@ -124,15 +124,13 @@ class CompletionPlugin(SpyderPluginV2):
Name of the completion client.
"""
- sig_pythonpath_changed = Signal(object, object, bool)
+ sig_pythonpath_changed = Signal(object, bool)
"""
This signal is used to receive changes on the PYTHONPATH.
Parameters
----------
- prev_path: dict
- Previous PythonPath settings.
- new_path: dict
+ new_path: list of str
New PythonPath settings.
prioritize
Whether to prioritize PYTHONPATH in sys.path
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
index 7480011d8a8..250053342e1 100644
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -531,13 +531,12 @@ def shutdown(self):
for language in self.clients:
self.stop_completion_services_for_language(language)
- @Slot(object, object, bool)
- def python_path_update(self, old_path, new_path, prioritize):
+ @Slot(object, bool)
+ def python_path_update(self, new_path, prioritize):
"""
Update server configuration after a change in Spyder's Python
path.
- `old_path` corresponds to the previous state of the Python path.
`new_path` corresponds to the new state of the Python path.
`prioritize` determines whether to prioritize Python path in sys.path.
"""
@@ -584,8 +583,11 @@ def on_pyls_spyder_configuration_change(self, option, value):
def on_code_snippets_enabled_disabled(self, value):
self.update_lsp_configuration()
- @on_conf_change(section='pythonpath_manager', option='spyder_pythonpath')
- def on_pythonpath_option_update(self, value):
+ @on_conf_change(
+ section='pythonpath_manager',
+ option=['spyder_pythonpath', 'prioritize']
+ )
+ def on_pythonpath_option_update(self, option, value):
# This is only useful to run some self-contained tests
if running_under_pytest():
self.update_lsp_configuration(python_only=True)
From 48b94a49c622f6515dd721b5a153ee19916a2961 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 29 Feb 2024 20:18:04 -0800
Subject: [PATCH 25/35] Update main window test
---
spyder/app/tests/test_mainwindow.py | 6 ++--
.../widgets/tests/test_pathmanager.py | 31 +++++++++++--------
2 files changed, 22 insertions(+), 15 deletions(-)
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index f977b90c2a0..aa7a62e0457 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -11,6 +11,7 @@
"""
# Standard library imports
+from collections import OrderedDict
import gc
import os
import os.path as osp
@@ -6493,9 +6494,10 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
# users
user_dir = tmp_path / 'user_dir'
user_dir.mkdir()
+ user_paths = OrderedDict({str(user_dir): True})
if os.name != "nt":
- assert ppm.get_container().path == ()
- ppm.get_container().path = (str(user_dir),) + ppm.get_container().path
+ assert ppm.get_container()._spyder_pythonpath == []
+ ppm.get_container()._save_paths(user_paths=user_paths)
# Open Pythonpath dialog to detect sys_dir
ppm.show_path_manager()
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index 12070be0e95..d6674fa2f0a 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -8,6 +8,7 @@
Tests for pathmanager.py
"""
# Standard library imports
+from collections import OrderedDict
import sys
import os
@@ -25,20 +26,24 @@
@pytest.fixture
def pathmanager(qtbot, request):
"""Set up PathManager."""
- path, project_path, not_active_path = request.param
- widget = pathmanager_mod.PathManager(
- None,
- path=tuple(path),
- project_path=tuple(project_path),
- not_active_path=tuple(not_active_path))
+ user_paths, project_paths, system_paths = request.param
+
+ widget = pathmanager_mod.PathManager(None)
+ widget.update_paths(
+ user_paths=OrderedDict({p: True for p in user_paths}),
+ project_paths=OrderedDict({p: True for p in project_paths}),
+ system_paths=OrderedDict({p: True for p in system_paths})
+ )
widget.show()
qtbot.addWidget(widget)
return widget
-@pytest.mark.parametrize('pathmanager',
- [(sys.path[:-10], sys.path[-10:], ())],
- indirect=True)
+@pytest.mark.parametrize(
+ 'pathmanager',
+ [(sys.path[:-10], sys.path[-10:], ())],
+ indirect=True
+)
def test_pathmanager(pathmanager, qtbot):
"""Run PathManager test"""
pathmanager.show()
@@ -207,7 +212,7 @@ def test_add_repeated_item(qtbot, pathmanager, tmpdir):
pathmanager.add_path(dir2)
pathmanager.add_path(dir3)
pathmanager.set_row_check_state(2, Qt.Unchecked)
- assert not all(pathmanager.get_path_dict().values())
+ assert not all(pathmanager.get_user_paths().values())
def interact_message_box():
messagebox = pathmanager.findChild(QMessageBox)
@@ -222,12 +227,12 @@ def interact_message_box():
timer.timeout.connect(interact_message_box)
timer.start(500)
pathmanager.add_path(dir2)
- print(pathmanager.get_path_dict())
+ print(pathmanager.get_user_paths())
# Back to main thread
assert pathmanager.count() == 4
- assert list(pathmanager.get_path_dict().keys())[0] == dir2
- assert all(pathmanager.get_path_dict().values())
+ assert list(pathmanager.get_user_paths().keys())[0] == dir2
+ assert all(pathmanager.get_user_paths().values())
@pytest.mark.parametrize('pathmanager',
From ae218e9e64d1ba935bbbf7f68494999d4de2e6b6 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sat, 2 Mar 2024 20:35:12 -0800
Subject: [PATCH 26/35] Update export_pythonpath
---
.../plugins/pythonpath/widgets/pathmanager.py | 55 ++++++++++++++-----
.../widgets/tests/test_pathmanager.py | 3 +-
2 files changed, 44 insertions(+), 14 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index c380edbb736..e13c3a32812 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -26,6 +26,7 @@
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import _
from spyder.plugins.pythonpath.utils import check_path
+from spyder.utils.environ import get_user_env, set_user_env
from spyder.utils.misc import getcwd_or_home
from spyder.utils.stylesheet import (
AppStyle,
@@ -51,7 +52,6 @@ class PathManager(QDialog, SpyderWidgetMixin):
redirect_stdio = Signal(bool)
sig_path_changed = Signal(object, object, bool)
- sig_export_pythonpath = Signal(object, object, bool)
# This is required for our tests
CONF_SECTION = 'pythonpath_manager'
@@ -333,6 +333,15 @@ def export_pythonpath(self):
"""
Export to PYTHONPATH environment variable
Only apply to: current user.
+
+ If the user chooses to clear the contents of the system PYTHONPATH,
+ then the active user paths are prepended to active system paths and
+ the resulting list is saved to the system PYTHONPATH. Inactive system
+ paths are discarded. If the user chooses not to clear the contents of
+ the system PYTHONPATH, then the new system PYTHONPATH comprises the
+ inactive system paths + active user paths + active system paths, and
+ inactive system paths remain inactive. With either choice, inactive
+ user paths are retained in the user paths and remain inactive.
"""
answer = QMessageBox.question(
self,
@@ -350,9 +359,24 @@ def export_pythonpath(self):
if answer == QMessageBox.Cancel:
return
- self.sig_export_pythonpath(
- self.get_user_paths(), self.get_system_paths(),
- answer == QMessageBox.Yes
+ user_paths = self.get_user_paths()
+ active_user_paths = OrderedDict({p: v for p, v in user_paths.items() if v})
+ new_user_paths = OrderedDict({p: v for p, v in user_paths.items() if not v})
+
+ system_paths = self.get_system_paths()
+ active_system_paths = OrderedDict({p: v for p, v in system_paths.items() if v})
+ inactive_system_paths = OrderedDict({p: v for p, v in system_paths.items() if not v})
+
+ new_system_paths = active_user_paths | active_system_paths
+ if answer == QMessageBox.No:
+ new_system_paths = inactive_system_paths | new_system_paths
+
+ env = get_user_env()
+ env['PYTHONPATH'] = list(new_system_paths.keys())
+ set_user_env(env, parent=self)
+
+ self.update_paths(
+ user_paths=new_user_paths, system_paths=new_system_paths
)
def get_user_paths(self):
@@ -389,10 +413,10 @@ def get_system_paths(self):
def update_paths(
self,
- project_paths=OrderedDict(),
- user_paths=OrderedDict(),
- system_paths=OrderedDict(),
- prioritize=False
+ project_paths=None,
+ user_paths=None,
+ system_paths=None,
+ prioritize=None
):
"""Update path attributes.
@@ -401,10 +425,14 @@ def update_paths(
used to compare with what is shown in the listwidget in order to detect
changes.
"""
- self.project_paths = project_paths
- self.user_paths = user_paths
- self.system_paths = system_paths
- self.prioritize = prioritize
+ if project_paths is not None:
+ self.project_paths = project_paths
+ if user_paths is not None:
+ self.user_paths = user_paths
+ if system_paths is not None:
+ self.system_paths = system_paths
+ if prioritize is not None:
+ self.prioritize = prioritize
self.setup()
@@ -634,7 +662,8 @@ def test():
dlg.update_paths(
user_paths={p: True for p in sys.path[1:-2]},
project_paths={p: True for p in sys.path[:1]},
- system_paths={p: True for p in sys.path[-2:]}
+ system_paths={p: True for p in sys.path[-2:]},
+ prioritize=False
)
def callback(user_paths, system_paths, prioritize):
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index d6674fa2f0a..b3cfdb86084 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -32,7 +32,8 @@ def pathmanager(qtbot, request):
widget.update_paths(
user_paths=OrderedDict({p: True for p in user_paths}),
project_paths=OrderedDict({p: True for p in project_paths}),
- system_paths=OrderedDict({p: True for p in system_paths})
+ system_paths=OrderedDict({p: True for p in system_paths}),
+ prioritize=False
)
widget.show()
qtbot.addWidget(widget)
From 229d8d84423903fb0ddb89b3b6e79756c31b9466 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 7 Mar 2024 13:46:32 -0800
Subject: [PATCH 27/35] Update widget icon.
Icon and tooltip are changed to reflect current state.
---
spyder/plugins/pythonpath/widgets/pathmanager.py | 10 ++++++++--
spyder/utils/icon_manager.py | 2 ++
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index e13c3a32812..95c8651ea9c 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -177,10 +177,9 @@ def _setup_right_toolbar(self):
tip=_("Export to PYTHONPATH environment variable"))
self.prioritize_button = self.create_toolbutton(
PathManagerToolbuttons.Prioritize,
- icon=self.create_icon('first_page'),
option='prioritize',
triggered=self.refresh,
- tip=_("Place PYTHONPATH at the front of sys.path"))
+ )
self.prioritize_button.setCheckable(True)
self.selection_widgets = [self.movetop_button, self.moveup_button,
@@ -472,6 +471,13 @@ def refresh(self):
and (self.editable_top_row <= row <= self.editable_bottom_row)
)
+ if self.prioritize_button.isChecked():
+ self.prioritize_button.setIcon(self.create_icon('prepend'))
+ self.prioritize_button.setToolTip(_("Paths are prpended to sys.path"))
+ else:
+ self.prioritize_button.setIcon(self.create_icon('append'))
+ self.prioritize_button.setToolTip(_("Paths are appended to sys.path"))
+
self.export_button.setEnabled(self.listwidget.count() > 0)
# Ok button only enabled if actual changes occur
diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py
index 9289d879a48..e0a1373121c 100644
--- a/spyder/utils/icon_manager.py
+++ b/spyder/utils/icon_manager.py
@@ -288,6 +288,8 @@ def __init__(self):
'1uparrow': [('mdi.arrow-up',), {'color': self.MAIN_FG_COLOR}],
'2downarrow': [('mdi.arrow-collapse-down',), {'color': self.MAIN_FG_COLOR}],
'1downarrow': [('mdi.arrow-down',), {'color': self.MAIN_FG_COLOR}],
+ 'prepend': [('mdi.arrow-collapse-left',), {'color': self.MAIN_FG_COLOR}],
+ 'append': [('mdi.arrow-collapse-right',), {'color': self.MAIN_FG_COLOR}],
'undock': [('mdi.open-in-new',), {'color': self.MAIN_FG_COLOR}],
'close_pane': [('mdi.window-close',), {'color': self.MAIN_FG_COLOR}],
'toolbar_ext_button': [('mdi.dots-horizontal',), {'color': self.MAIN_FG_COLOR}],
From 4c0741badc1c02420b113639c3075faaf901ae69 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 16 May 2024 12:31:45 -0700
Subject: [PATCH 28/35] Apply suggestions from code review
Co-authored-by: Jitse Niesen
Typographical errors.
Improved docstring clarity
---
spyder/plugins/pythonpath/container.py | 16 +++++++++++-----
spyder/plugins/pythonpath/plugin.py | 12 +++---------
spyder/plugins/pythonpath/widgets/pathmanager.py | 2 +-
3 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 2f136acd673..7163a934693 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -69,8 +69,10 @@ def update_actions(self):
def update_active_project_path(self, path):
"""Update active project path.
- _project_paths is initialized and set here, and nowhere else.
+ _project_paths is initialized in _load_paths, but set in this method
+ and nowhere else.
"""
+ # _project_paths should be reset whenever it is updated.
self._project_paths = OrderedDict()
if path is None:
logger.debug("Update Spyder PYTHONPATH because project was closed")
@@ -86,11 +88,11 @@ def show_path_manager(self):
Send the most up-to-date system paths to the dialog in case they have
changed. But do not _save_paths until after the dialog exits, in order
- to consolodate possible changes and avoid emitting multiple signals.
+ to consolidate possible changes and avoid emitting multiple signals.
This requires that the dialog return its original paths on cancel or
close.
"""
- # Do not update paths or run setup if widget is already open,
+ # Do not update paths if widget is already open,
# see spyder-ide/spyder#20808.
if not self.path_manager_dialog.isVisible():
self.path_manager_dialog.update_paths(
@@ -120,8 +122,9 @@ def _load_paths(self):
"""Load Python paths.
The attributes _project_paths, _user_paths, _system_paths, _prioritize,
- and _spyder_pythonpath, are initialize here and should be updated only
- in _save_paths. They are only used to detect changes.
+ and _spyder_pythonpath, are initialized here. All but _project_paths
+ should be updated only in _save_paths. They are only used to detect
+ changes.
"""
self._project_paths = OrderedDict()
self._user_paths = OrderedDict()
@@ -162,6 +165,9 @@ def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
`user_paths` is user paths. `system_paths` is system paths, and
`prioritize` is a boolean indicating whether paths should be
prepended (True) or appended (False) to sys.path.
+
+ sig_pythonpath_changed is emitted from this method, and nowhere else,
+ on condition that _spyder_pythonpath changed.
"""
assert isinstance(user_paths, (type(None), OrderedDict))
assert isinstance(system_paths, (type(None), OrderedDict))
diff --git a/spyder/plugins/pythonpath/plugin.py b/spyder/plugins/pythonpath/plugin.py
index 950be5bf1d0..0926fcacd46 100644
--- a/spyder/plugins/pythonpath/plugin.py
+++ b/spyder/plugins/pythonpath/plugin.py
@@ -41,17 +41,11 @@ class PythonpathManager(SpyderPluginV2):
Parameters
----------
- old_path_dict: OrderedDict
- Previous Pythonpath ordered dictionary. Its keys correspond to the
- project, user and system paths declared by users or detected by Spyder,
- and its values are their state (i.e. True for enabled and False for
- disabled).
-
- new_path_dict: OrderedDict
- New Pythonpath dictionary.
+ new_path_list: list of str
+ New list of PYTHONPATH paths.
prioritize
- Whether to prioritize Pythonpath in sys.path
+ Whether to prioritize PYTHONPATH in sys.path
See Also
--------
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 95c8651ea9c..106e8ae4b6d 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -473,7 +473,7 @@ def refresh(self):
if self.prioritize_button.isChecked():
self.prioritize_button.setIcon(self.create_icon('prepend'))
- self.prioritize_button.setToolTip(_("Paths are prpended to sys.path"))
+ self.prioritize_button.setToolTip(_("Paths are prepended to sys.path"))
else:
self.prioritize_button.setIcon(self.create_icon('append'))
self.prioritize_button.setToolTip(_("Paths are appended to sys.path"))
From 92116f1b39034220505808ca17613f369cd5837e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sat, 18 May 2024 15:47:24 -0700
Subject: [PATCH 29/35] Apply suggestions from python-lsp-server code review
---
spyder/config/lsp.py | 2 +-
.../plugins/completion/providers/languageserver/provider.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/spyder/config/lsp.py b/spyder/config/lsp.py
index 4194654b21b..4410e90b99b 100644
--- a/spyder/config/lsp.py
+++ b/spyder/config/lsp.py
@@ -79,7 +79,7 @@
'environment': None,
'extra_paths': [],
'env_vars': None,
- 'prioritize': False,
+ 'prioritize_extra_paths': False,
# Until we have a graphical way for users to add modules to
# this option
'auto_import_modules': [
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
index 250053342e1..936ec663d1d 100644
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -809,9 +809,9 @@ def generate_python_config(self):
'extra_paths': self.get_conf('spyder_pythonpath',
section='pythonpath_manager',
default=[]),
- 'prioritize': self.get_conf('prioritize',
- section='pythonpath_manager',
- default=False),
+ 'prioritize_extra_paths': self.get_conf(
+ 'prioritize', section='pythonpath_manager', default=False
+ ),
'env_vars': env_vars,
}
jedi_completion = {
From 2adf27dddf8e303cab4f58cdaaabeb186d3de365 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sun, 26 May 2024 00:38:58 -0700
Subject: [PATCH 30/35] Python 3.8 does not support | operator on OrderedDict.
The desired affect is project paths | user paths | system paths, where the paths are in that order and are overwritten in that order. System paths cannot overwrite user paths, which cannot overwrite project paths, i.e we cannot just do project_paths.update(user_paths) etc.
---
spyder/plugins/pythonpath/container.py | 9 ++++++---
spyder/plugins/pythonpath/widgets/pathmanager.py | 9 +++++++--
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 7163a934693..965ae03d97d 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -110,9 +110,12 @@ def show_path_manager(self):
def get_spyder_pythonpath(self):
"""Return active Spyder PYTHONPATH as a list of paths."""
- # Place project path first so that modules developed in a
- # project are not shadowed by those present in other paths.
- all_paths = self._project_paths | self._user_paths | self._system_paths
+ # Desired behavior is project_paths | user_paths | system_paths, but
+ # Python 3.8 does not support | operator for OrderedDict.
+ all_paths = OrderedDict(reversed(self._system_paths.items()))
+ all_paths.update(reversed(self._user_paths.items()))
+ all_paths.update(reversed(self._project_paths.items()))
+ all_paths = OrderedDict(reversed(all_paths.items()))
return [p for p, v in all_paths.items() if v]
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 106e8ae4b6d..22e6561a15d 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -366,9 +366,14 @@ def export_pythonpath(self):
active_system_paths = OrderedDict({p: v for p, v in system_paths.items() if v})
inactive_system_paths = OrderedDict({p: v for p, v in system_paths.items() if not v})
- new_system_paths = active_user_paths | active_system_paths
+ # Desired behavior is active_user | active_system, but Python 3.8 does
+ # not support | operator for OrderedDict.
+ new_system_paths = OrderedDict(reversed(active_system_paths.items()))
+ new_system_paths.update(reversed(active_user_paths.items()))
if answer == QMessageBox.No:
- new_system_paths = inactive_system_paths | new_system_paths
+ # Desired behavior is inactive_system | active_user | active_system
+ new_system_paths.update(reversed(inactive_system_paths.items()))
+ new_system_paths = OrderedDict(reversed(new_system_paths.items()))
env = get_user_env()
env['PYTHONPATH'] = list(new_system_paths.keys())
From c23ac80559d5dc1507015343707f08b548c3a66d Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Fri, 11 Oct 2024 15:08:33 -0700
Subject: [PATCH 31/35] Do not emit sig_path_changed on closeEvent or reject.
---
spyder/plugins/pythonpath/widgets/pathmanager.py | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 22e6561a15d..98f0f619b5d 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -648,17 +648,11 @@ def accept(self):
super().accept()
def reject(self):
- # Send back original paths (system_paths may be updated)
- self.sig_path_changed.emit(
- self.user_paths, self.system_paths, self.prioritize
- )
+ # ??? Do we need this?
super().reject()
def closeEvent(self, event):
- # Send back original paths (system_paths may be updated)
- self.sig_path_changed.emit(
- self.user_paths, self.system_paths, self.prioritize
- )
+ # ??? Do we need this?
super().closeEvent(event)
From 60741b8cee9e0efafb8eaec9ec81e4b5510936de Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sat, 12 Oct 2024 07:09:03 -0700
Subject: [PATCH 32/35] Only update system paths on Spyder startup, not every
time the pythonpath manager widget is invoked. If the system paths have
changed since last widget invocation, then the user may not be aware and
there is no indication in the widget that there has been a change.
Furthermore, canceling the widget may still result in a change to the
pythonpath, which would be inconsistent with the cancel action.
---
spyder/plugins/pythonpath/container.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 965ae03d97d..6badd44eea9 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -98,7 +98,7 @@ def show_path_manager(self):
self.path_manager_dialog.update_paths(
project_paths=self._project_paths,
user_paths=self._user_paths,
- system_paths=self._get_system_paths(),
+ system_paths=self._system_paths,
prioritize=self._prioritize
)
From ed5cb2744e5240d459c76d79f2f3680dffca6155 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Sat, 12 Oct 2024 17:42:29 -0700
Subject: [PATCH 33/35] Add import path functionality. Rather than
automatically updating the system paths, provide mechanism for user to do so.
---
.../plugins/pythonpath/widgets/pathmanager.py | 68 +++++++++++++++----
1 file changed, 54 insertions(+), 14 deletions(-)
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 98f0f619b5d..e9344b1067d 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -25,7 +25,7 @@
from spyder.api.widgets.dialogs import SpyderDialogButtonBox
from spyder.api.widgets.mixins import SpyderWidgetMixin
from spyder.config.base import _
-from spyder.plugins.pythonpath.utils import check_path
+from spyder.plugins.pythonpath.utils import check_path, get_system_pythonpath
from spyder.utils.environ import get_user_env, set_user_env
from spyder.utils.misc import getcwd_or_home
from spyder.utils.stylesheet import (
@@ -43,6 +43,7 @@ class PathManagerToolbuttons:
MoveToBottom = 'move_to_bottom'
AddPath = 'add_path'
RemovePath = 'remove_path'
+ ImportPaths = 'import_paths'
ExportPaths = 'export_paths'
Prioritize = 'prioritize'
@@ -170,6 +171,11 @@ def _setup_right_toolbar(self):
tip=_('Remove path'),
icon=self.create_icon('editclear'),
triggered=lambda x: self.remove_path())
+ self.import_button = self.create_toolbutton(
+ PathManagerToolbuttons.ImportPaths,
+ tip=_('Import from PYTHONPATH environment variable'),
+ icon=self.create_icon('fileimport'),
+ triggered=lambda x: self.import_paths())
self.export_button = self.create_toolbutton(
PathManagerToolbuttons.ExportPaths,
icon=self.create_icon('fileexport'),
@@ -186,7 +192,7 @@ def _setup_right_toolbar(self):
self.movedown_button, self.movebottom_button]
return (
[self.add_button, self.remove_button] +
- self.selection_widgets + [self.export_button] +
+ self.selection_widgets + [self.import_button, self.export_button] +
[self.prioritize_button]
)
@@ -248,6 +254,23 @@ def _stylesheet(self):
return css.toString()
+ def _setup_system_paths(self, paths):
+ """Add system paths, creating system header if necessary"""
+ if not paths:
+ return
+
+ if not self.system_header:
+ self.system_header, system_widget = (
+ self._create_header(_("System PYTHONPATH"))
+ )
+ self.headers.append(self.system_header)
+ self.listwidget.addItem(self.system_header)
+ self.listwidget.setItemWidget(self.system_header, system_widget)
+
+ for path, active in paths.items():
+ item = self._create_item(path, active)
+ self.listwidget.addItem(item)
+
# ---- Public methods
# -------------------------------------------------------------------------
@property
@@ -308,18 +331,8 @@ def setup(self):
item = self._create_item(path, active)
self.listwidget.addItem(item)
- # System path
- if self.system_paths:
- self.system_header, system_widget = (
- self._create_header(_("System PYTHONPATH"))
- )
- self.headers.append(self.system_header)
- self.listwidget.addItem(self.system_header)
- self.listwidget.setItemWidget(self.system_header, system_widget)
-
- for path, active in self.system_paths.items():
- item = self._create_item(path, active)
- self.listwidget.addItem(item)
+ # System paths
+ self._setup_system_paths(self.system_paths)
# Prioritize
self.prioritize_button.setChecked(self.prioritize)
@@ -596,6 +609,33 @@ def remove_path(self, force=False):
# Refresh widget
self.refresh()
+ @Slot()
+ def import_paths(self):
+ """Import PYTHONPATH from environment."""
+ current_system_paths = self.get_system_paths()
+ system_paths = get_system_pythonpath()
+
+ # Inherit active state from current system paths
+ system_paths = OrderedDict(
+ {p: current_system_paths.get(p, True) for p in system_paths}
+ )
+
+ # Remove system paths
+ if self.system_header:
+ header_row = self.listwidget.row(self.system_header)
+ for row in range(self.listwidget.count(), header_row, -1):
+ self.listwidget.takeItem(row)
+
+ # Also remove system header
+ if not system_paths:
+ self.listwidget.takeItem(header_row)
+ self.headers.remove(self.system_header)
+ self.system_header = None
+
+ self._setup_system_paths(system_paths)
+
+ self.refresh()
+
def move_to(self, absolute=None, relative=None):
"""Move items of list widget."""
index = self.listwidget.currentRow()
From 796a563d45954e909ed8161380051f0abdb8b24e Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Tue, 15 Oct 2024 00:52:53 -0700
Subject: [PATCH 34/35] Update unit tests * Test system PYTHONPATH import in
test_pathmanager instead of test_mainwindow * Move restore_user_env fixture
from app/tests/conftest.py to utils/tests/conftest.py * Ensure that the user
environment script runs on posix while testing
---
spyder/app/tests/conftest.py | 21 +---
spyder/app/tests/test_mainwindow.py | 23 +----
.../widgets/tests/test_pathmanager.py | 95 +++++++++++--------
spyder/utils/environ.py | 6 +-
spyder/utils/tests/conftest.py | 31 ++++++
spyder/utils/tests/test_environ.py | 14 +--
6 files changed, 103 insertions(+), 87 deletions(-)
create mode 100644 spyder/utils/tests/conftest.py
diff --git a/spyder/app/tests/conftest.py b/spyder/app/tests/conftest.py
index f67a426208f..920662c141e 100755
--- a/spyder/app/tests/conftest.py
+++ b/spyder/app/tests/conftest.py
@@ -27,15 +27,13 @@
from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY
from spyder.api.plugins import Plugins
from spyder.app import start
-from spyder.config.base import get_home_dir, running_in_ci
+from spyder.config.base import get_home_dir
from spyder.config.manager import CONF
from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec
from spyder.plugins.projects.api import EmptyProject
from spyder.plugins.run.api import RunActions, StoredRunConfigurationExecutor
from spyder.plugins.toolbar.api import ApplicationToolbars
from spyder.utils import encoding
-from spyder.utils.environ import (get_user_env, set_user_env,
- amend_user_shell_init)
# =============================================================================
# ---- Constants
@@ -624,20 +622,3 @@ def threads_condition():
CONF.reset_manager()
PLUGIN_REGISTRY.reset()
raise
-
-
-@pytest.fixture
-def restore_user_env():
- """Set user environment variables and restore upon test exit"""
- if not running_in_ci():
- pytest.skip("Skipped because not in CI.")
-
- if os.name == "nt":
- orig_env = get_user_env()
-
- yield
-
- if os.name == "nt":
- set_user_env(orig_env)
- else:
- amend_user_shell_init(restore=True)
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index aa7a62e0457..e0465457b17 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -86,7 +86,6 @@
WorkingDirSource, RunContext)
from spyder.plugins.shortcuts.widgets.table import SEQUENCE
from spyder.py3compat import qbytearray_to_str, to_text_string
-from spyder.utils.environ import set_user_env
from spyder.utils.conda import get_list_conda_envs
from spyder.utils.misc import remove_backslashes, rename_file
from spyder.utils.clipboard_helper import CLIPBOARD_HELPER
@@ -6470,8 +6469,7 @@ def test_switch_to_plugin(main_window, qtbot):
@flaky(max_runs=5)
-def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
- restore_user_env):
+def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
"""
Test that PYTHONPATH is passed to IPython consoles under different
scenarios.
@@ -6485,11 +6483,6 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
# Main variables
ppm = main_window.get_plugin(Plugins.PythonpathManager)
- # Add a directory to PYTHONPATH
- sys_dir = tmp_path / 'sys_dir'
- sys_dir.mkdir()
- set_user_env({"PYTHONPATH": str(sys_dir)})
-
# Add a directory to the current list of paths to simulate a path added by
# users
user_dir = tmp_path / 'user_dir'
@@ -6497,25 +6490,17 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
user_paths = OrderedDict({str(user_dir): True})
if os.name != "nt":
assert ppm.get_container()._spyder_pythonpath == []
- ppm.get_container()._save_paths(user_paths=user_paths)
-
- # Open Pythonpath dialog to detect sys_dir
- ppm.show_path_manager()
- qtbot.wait(500)
-
- # Check we're showing two headers
- assert len(ppm.path_manager_dialog.headers) == 2
# Check the PPM emits the right signal after closing the dialog
with qtbot.waitSignal(ppm.sig_pythonpath_changed, timeout=1000):
- ppm.path_manager_dialog.close()
+ ppm.get_container()._save_paths(user_paths=user_paths)
# Check directories were added to sys.path in the right order
with qtbot.waitSignal(shell.executed, timeout=2000):
shell.execute("import sys; sys_path = sys.path")
sys_path = shell.get_value("sys_path")
- assert sys_path[-2:] == [str(user_dir), str(sys_dir)]
+ assert sys_path[-1:] == [str(user_dir)]
# Create new console
ipyconsole.create_new_client()
@@ -6528,7 +6513,7 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path,
shell1.execute("import sys; sys_path = sys.path")
sys_path = shell1.get_value("sys_path")
- assert sys_path[-2:] == [str(user_dir), str(sys_dir)]
+ assert sys_path[-1:] == [str(user_dir)]
# Check that disabling a path from the PPM removes it from sys.path in all
# consoles
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index b3cfdb86084..c8010e091a2 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -18,7 +18,9 @@
from qtpy.QtWidgets import QMessageBox, QPushButton
# Local imports
+from spyder.utils.environ import get_user_env, set_user_env
from spyder.utils.programs import is_module_installed
+from spyder.utils.tests.conftest import restore_user_env
from spyder.plugins.pythonpath.utils import check_path
from spyder.plugins.pythonpath.widgets import pathmanager as pathmanager_mod
@@ -41,19 +43,42 @@ def pathmanager(qtbot, request):
@pytest.mark.parametrize(
- 'pathmanager',
- [(sys.path[:-10], sys.path[-10:], ())],
- indirect=True
+ 'pathmanager', [(sys.path[:-10], sys.path[-10:], ())], indirect=True
)
-def test_pathmanager(pathmanager, qtbot):
+def test_pathmanager(qtbot, pathmanager):
"""Run PathManager test"""
pathmanager.show()
assert pathmanager
-@pytest.mark.parametrize('pathmanager',
- [(sys.path[:-10], sys.path[-10:], ())],
- indirect=True)
+@pytest.mark.parametrize('pathmanager', [((), (), ())], indirect=True)
+def test_import_PYTHONPATH(qtbot, pathmanager, tmp_path, restore_user_env):
+ """
+ Test that PYTHONPATH is imported.
+ """
+
+ # Add a directory to PYTHONPATH environment variable
+ sys_dir = tmp_path / 'sys_dir'
+ sys_dir.mkdir()
+ set_user_env({"PYTHONPATH": str(sys_dir)})
+
+ # Open Pythonpath dialog
+ pathmanager.show()
+ qtbot.wait(500)
+
+ assert len(pathmanager.headers) == 0
+ assert pathmanager.get_system_paths() == OrderedDict()
+
+ # Import PYTHONPATH from environment
+ pathmanager.import_paths()
+ assert len(pathmanager.headers) == 1
+
+ assert pathmanager.get_system_paths() == OrderedDict({str(sys_dir): True})
+
+
+@pytest.mark.parametrize(
+ 'pathmanager', [(sys.path[:-10], sys.path[-10:], ())], indirect=True
+)
def test_check_uncheck_path(pathmanager):
"""
Test that checking and unchecking a path in the PathManager correctly
@@ -66,20 +91,16 @@ def test_check_uncheck_path(pathmanager):
assert item.checkState() == Qt.Checked
-@pytest.mark.skipif(os.name != 'nt' or not is_module_installed('win32con'),
- reason=("This feature is not applicable for Unix "
- "systems and pywin32 is needed"))
-@pytest.mark.parametrize('pathmanager',
- [(['p1', 'p2', 'p3'], ['p4', 'p5', 'p6'], [])],
- indirect=True)
-def test_export_to_PYTHONPATH(pathmanager, mocker):
- # Import here to prevent an ImportError when testing on unix systems
- from spyder.utils.environ import (get_user_env, set_user_env,
- listdict2envdict)
-
- # Store PYTHONPATH original state
- env = get_user_env()
- original_pathlist = env.get('PYTHONPATH', [])
+@pytest.mark.skipif(
+ os.name != 'nt' or not is_module_installed('win32con'),
+ reason=("This feature is not applicable for Unix "
+ "systems and pywin32 is needed")
+)
+@pytest.mark.parametrize(
+ 'pathmanager', [(['p1', 'p2', 'p3'], ['p4', 'p5', 'p6'], [])],
+ indirect=True
+)
+def test_export_to_PYTHONPATH(pathmanager, mocker, restore_user_env):
# Mock the dialog window and answer "Yes" to clear contents of PYTHONPATH
# before adding Spyder's path list
@@ -113,14 +134,10 @@ def test_export_to_PYTHONPATH(pathmanager, mocker):
env = get_user_env()
assert env['PYTHONPATH'] == expected_pathlist
- # Restore PYTHONPATH to its original state
- env['PYTHONPATH'] = original_pathlist
- set_user_env(listdict2envdict(env))
-
-@pytest.mark.parametrize('pathmanager',
- [(sys.path[:-10], sys.path[-10:], ())],
- indirect=True)
+@pytest.mark.parametrize(
+ 'pathmanager', [(sys.path[:-10], sys.path[-10:], ())], indirect=True
+)
def test_invalid_directories(qtbot, pathmanager):
"""Check [site/dist]-packages are invalid paths."""
if os.name == 'nt':
@@ -143,9 +160,9 @@ def interact_message_box():
pathmanager.add_path(path)
-@pytest.mark.parametrize('pathmanager',
- [(('/spam', '/bar'), ('/foo', ), ())],
- indirect=True)
+@pytest.mark.parametrize(
+ 'pathmanager', [(('/spam', '/bar'), ('/foo', ), ())], indirect=True
+)
def test_remove_item_and_reply_no(qtbot, pathmanager):
"""Check that the item is not removed after answering 'No'."""
pathmanager.show()
@@ -169,9 +186,9 @@ def interact_message_box():
assert pathmanager.count() == count
-@pytest.mark.parametrize('pathmanager',
- [(('/spam', '/bar'), ('/foo', ), ())],
- indirect=True)
+@pytest.mark.parametrize(
+ 'pathmanager', [(('/spam', '/bar'), ('/foo', ), ())], indirect=True
+)
def test_remove_item_and_reply_yes(qtbot, pathmanager):
"""Check that the item is indeed removed after answering 'Yes'."""
pathmanager.show()
@@ -196,9 +213,7 @@ def interact_message_box():
assert pathmanager.count() == (count - 1)
-@pytest.mark.parametrize('pathmanager',
- [((), (), ())],
- indirect=True)
+@pytest.mark.parametrize('pathmanager', [((), (), ())], indirect=True)
def test_add_repeated_item(qtbot, pathmanager, tmpdir):
"""
Check behavior when an unchecked item that is already on the list is added.
@@ -236,9 +251,9 @@ def interact_message_box():
assert all(pathmanager.get_user_paths().values())
-@pytest.mark.parametrize('pathmanager',
- [(('/spam', '/bar'), ('/foo', ), ())],
- indirect=True)
+@pytest.mark.parametrize(
+ 'pathmanager', [(('/spam', '/bar'), ('/foo', ), ())], indirect=True
+)
def test_buttons_state(qtbot, pathmanager, tmpdir):
"""Check buttons are enabled/disabled based on items and position."""
pathmanager.show()
diff --git a/spyder/utils/environ.py b/spyder/utils/environ.py
index 5b1d36606ed..74c9ea3b388 100644
--- a/spyder/utils/environ.py
+++ b/spyder/utils/environ.py
@@ -27,7 +27,9 @@
from qtpy.QtWidgets import QMessageBox
# Local imports
-from spyder.config.base import _, running_in_ci, get_conf_path
+from spyder.config.base import (
+ _, running_in_ci, get_conf_path, running_under_pytest
+)
from spyder.widgets.collectionseditor import CollectionsEditor
from spyder.utils.icon_manager import ima
from spyder.utils.programs import run_shell_command
@@ -111,7 +113,7 @@ def get_user_environment_variables():
# We only need to do this if Spyder was **not** launched from a
# terminal. Otherwise, it'll inherit the env vars present in it.
# Fixes spyder-ide/spyder#22415
- if not launched_from_terminal:
+ if not launched_from_terminal or running_under_pytest():
try:
user_env_script = _get_user_env_script()
proc = run_shell_command(user_env_script, env={}, text=True)
diff --git a/spyder/utils/tests/conftest.py b/spyder/utils/tests/conftest.py
new file mode 100644
index 00000000000..361d6442845
--- /dev/null
+++ b/spyder/utils/tests/conftest.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Standard library imports
+import os
+
+# Third-party imports
+import pytest
+
+# Local imports
+from spyder.config.base import running_in_ci
+from spyder.utils.environ import (
+ get_user_env, set_user_env, amend_user_shell_init
+)
+
+
+@pytest.fixture
+def restore_user_env():
+ """Set user environment variables and restore upon test exit"""
+ if not running_in_ci():
+ pytest.skip("Skipped because not in CI.")
+
+ if os.name == "nt":
+ orig_env = get_user_env()
+
+ yield
+
+ if os.name == "nt":
+ set_user_env(orig_env)
+ else:
+ amend_user_shell_init(restore=True)
diff --git a/spyder/utils/tests/test_environ.py b/spyder/utils/tests/test_environ.py
index 277f2418be6..d4f771d9848 100644
--- a/spyder/utils/tests/test_environ.py
+++ b/spyder/utils/tests/test_environ.py
@@ -18,15 +18,15 @@
from qtpy.QtCore import QTimer
# Local imports
-from spyder.utils.environ import (get_user_environment_variables,
- UserEnvDialog, amend_user_shell_init)
+from spyder.utils.environ import (
+ get_user_environment_variables, UserEnvDialog, amend_user_shell_init
+)
from spyder.utils.test import close_message_box
-from spyder.app.tests.conftest import restore_user_env
@pytest.fixture
def environ_dialog(qtbot):
- "Setup the Environment variables Dialog."
+ """Setup the Environment variables Dialog."""
QTimer.singleShot(1000, lambda: close_message_box(qtbot))
dialog = UserEnvDialog()
qtbot.addWidget(dialog)
@@ -44,8 +44,10 @@ def test_get_user_environment_variables():
@pytest.mark.skipif(os.name == "nt", reason="Does not apply to Windows")
def test_get_user_env_newline(restore_user_env):
- # Test variable value with newline characters.
- # Regression test for spyder-ide#20097
+ """
+ Test variable value with newline characters.
+ Regression test for spyder-ide#20097.
+ """
text = "myfunc() { echo hello;\n echo world\n}\nexport -f myfunc"
amend_user_shell_init(text)
user_env = get_user_environment_variables()
From b940e1d2a8573e51e6fc6aecb6d989e8f48669b6 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 2 Dec 2024 12:51:10 -0800
Subject: [PATCH 35/35] git subrepo clone --branch=ppm-syspath --force
https://github.com/mrclary/spyder-kernels.git external-deps/spyder-kernels
subrepo:
subdir: "external-deps/spyder-kernels"
merged: "66766d44b"
upstream:
origin: "https://github.com/mrclary/spyder-kernels.git"
branch: "ppm-syspath"
commit: "66766d44b"
git-subrepo:
version: "0.4.9"
origin: "???"
commit: "???"
---
external-deps/spyder-kernels/.gitrepo | 8 +--
.../spyder_kernels/console/kernel.py | 53 ++++++++++-----
.../spyder_kernels/console/start.py | 29 ++++----
.../console/tests/test_console_kernel.py | 67 +++++++++++++------
.../customize/spydercustomize.py | 14 ----
5 files changed, 103 insertions(+), 68 deletions(-)
diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo
index 4f04f00a36f..9ab9efea944 100644
--- a/external-deps/spyder-kernels/.gitrepo
+++ b/external-deps/spyder-kernels/.gitrepo
@@ -5,8 +5,8 @@
;
[subrepo]
remote = https://github.com/spyder-ide/spyder-kernels.git
- branch = master
- commit = 07f24b6fde55585e64d1802036efba19514dbf0c
- parent = 41c288acf1c34b7cd9f1175b4f0fa8233692a26c
+ branch = ppm-syspath
+ commit = 66766d44b0e7a33b08bcfb4ebba9f9c45e848a8b
+ parent = 304ee079a9f75b65cde623b5ae94b6b49d313362
method = merge
- cmdver = 0.4.3
+ cmdver = 0.4.9
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
index 391c7392b24..b1970c2b8a8 100644
--- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py
@@ -93,6 +93,11 @@ def __init__(self, *args, **kwargs):
# To save the python env info
self.pythonenv_info: PythonEnvInfo = {}
+ # Store original sys.path. Kernels are started with PYTHONPATH
+ # removed from environment variables, so this will never have
+ # user paths and should be clean.
+ self._sys_path = sys.path.copy()
+
@property
def kernel_info(self):
# Used for checking correct version by spyder
@@ -765,27 +770,43 @@ def set_special_kernel(self, special):
raise NotImplementedError(f"{special}")
@comm_handler
- def update_syspath(self, path_dict, new_path_dict):
+ def update_syspath(self, new_path, prioritize):
"""
Update the PYTHONPATH of the kernel.
- `path_dict` and `new_path_dict` have the paths as keys and the state
- as values. The state is `True` for active and `False` for inactive.
-
- `path_dict` corresponds to the previous state of the PYTHONPATH.
- `new_path_dict` corresponds to the new state of the PYTHONPATH.
+ Parameters
+ ----------
+ new_path: list of str
+ List of PYTHONPATH paths.
+ prioritize: bool
+ Whether to place PYTHONPATH paths at the front (True) or
+ back (False) of sys.path.
+
+
+ Notes
+ -----
+ A copy of sys.path is made at instantiation, which should be clean,
+ so we can just prepend/append to the copy without having to explicitly
+ remove old user paths. PYTHONPATH can just be overwritten.
"""
- # Remove old paths
- for path in path_dict:
- while path in sys.path:
- sys.path.remove(path)
-
- # Add new paths
- pypath = [path for path, active in new_path_dict.items() if active]
- if pypath:
- sys.path.extend(pypath)
- os.environ.update({'PYTHONPATH': os.pathsep.join(pypath)})
+ if new_path is not None:
+ # Overwrite PYTHONPATH
+ os.environ.update({'PYTHONPATH': os.pathsep.join(new_path)})
+
+ # Add new paths to original sys.path
+ if prioritize:
+ sys.path[:] = new_path + self._sys_path
+
+ # Ensure current directory is always first to imitate Python
+ # standard behavior
+ if '' in sys.path:
+ sys.path.remove('')
+ sys.path.insert(0, '')
+ else:
+ sys.path[:] = self._sys_path + new_path
else:
+ # Restore original sys.path and remove PYTHONPATH
+ sys.path[:] = self._sys_path
os.environ.pop('PYTHONPATH', None)
@comm_handler
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py
index b8a423af2a9..3f7607eb05b 100644
--- a/external-deps/spyder-kernels/spyder_kernels/console/start.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py
@@ -16,6 +16,14 @@
import sys
import site
+# Remove current directory from sys.path to prevent kernel crashes when people
+# name Python files or modules with the same name as standard library modules.
+# See spyder-ide/spyder#8007
+# Inject it back into sys.path after all imports in this module but
+# before the kernel is initialized
+while '' in sys.path:
+ sys.path.remove('')
+
# Third-party imports
from traitlets import DottedObjectName
@@ -29,13 +37,6 @@ def import_spydercustomize():
parent = osp.dirname(here)
customize_dir = osp.join(parent, 'customize')
- # Remove current directory from sys.path to prevent kernel
- # crashes when people name Python files or modules with
- # the same name as standard library modules.
- # See spyder-ide/spyder#8007
- while '' in sys.path:
- sys.path.remove('')
-
# Import our customizations
site.addsitedir(customize_dir)
import spydercustomize # noqa
@@ -46,6 +47,7 @@ def import_spydercustomize():
except ValueError:
pass
+
def kernel_config():
"""Create a config object with IPython kernel options."""
from IPython.core.application import get_ipython_dir
@@ -150,13 +152,6 @@ def main():
# Import our customizations into the kernel
import_spydercustomize()
- # Remove current directory from sys.path to prevent kernel
- # crashes when people name Python files or modules with
- # the same name as standard library modules.
- # See spyder-ide/spyder#8007
- while '' in sys.path:
- sys.path.remove('')
-
# Main imports
from ipykernel.kernelapp import IPKernelApp
from spyder_kernels.console.kernel import SpyderKernel
@@ -189,6 +184,12 @@ def close(self):
kernel.config = kernel_config()
except:
pass
+
+ # Re-add current working directory path into sys.path after all of the
+ # import statements, but before initializing the kernel.
+ if '' not in sys.path:
+ sys.path.insert(0, '')
+
kernel.initialize()
# Set our own magics
diff --git a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
index 7e623b30107..dfcd10086b0 100644
--- a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
+++ b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
@@ -77,13 +77,15 @@ def setup_kernel(cmd):
)
# wait for connection file to exist, timeout after 5s
tic = time.time()
- while not os.path.exists(connection_file) \
- and kernel.poll() is None \
- and time.time() < tic + SETUP_TIMEOUT:
+ while (
+ not os.path.exists(connection_file)
+ and kernel.poll() is None
+ and time.time() < tic + SETUP_TIMEOUT
+ ):
time.sleep(0.1)
if kernel.poll() is not None:
- o,e = kernel.communicate()
+ o, e = kernel.communicate()
raise IOError("Kernel failed to start:\n%s" % e)
if not os.path.exists(connection_file):
@@ -229,7 +231,7 @@ def kernel(request):
'True_'
],
'minmax': False,
- 'filter_on':True
+ 'filter_on': True
}
# Teardown
@@ -468,8 +470,11 @@ def test_is_defined(kernel):
def test_get_doc(kernel):
"""Test to get object documentation dictionary."""
objtxt = 'help'
- assert ("Define the builtin 'help'" in kernel.get_doc(objtxt)['docstring'] or
- "Define the built-in 'help'" in kernel.get_doc(objtxt)['docstring'])
+ assert (
+ "Define the builtin 'help'" in kernel.get_doc(objtxt)['docstring']
+ or "Define the built-in 'help'" in kernel.get_doc(objtxt)['docstring']
+ )
+
def test_get_source(kernel):
"""Test to get object source."""
@@ -507,7 +512,7 @@ def test_cwd_in_sys_path():
with setup_kernel(cmd) as client:
reply = client.execute_interactive(
"import sys; sys_path = sys.path",
- user_expressions={'output':'sys_path'}, timeout=TIMEOUT)
+ user_expressions={'output': 'sys_path'}, timeout=TIMEOUT)
# Transform value obtained through user_expressions
user_expressions = reply['content']['user_expressions']
@@ -518,6 +523,21 @@ def test_cwd_in_sys_path():
assert '' in value
+def test_prioritize(kernel):
+ """Test that user path priority is honored in sys.path."""
+ syspath = kernel.get_syspath()
+ append_path = ['/test/append/path']
+ prepend_path = ['/test/prepend/path']
+
+ kernel.update_syspath(append_path, prioritize=False)
+ new_syspath = kernel.get_syspath()
+ assert new_syspath == syspath + append_path
+
+ kernel.update_syspath(prepend_path, prioritize=True)
+ new_syspath = kernel.get_syspath()
+ assert new_syspath == prepend_path + syspath
+
+
@flaky(max_runs=3)
def test_multiprocessing(tmpdir):
"""
@@ -701,8 +721,10 @@ def test_runfile(tmpdir):
assert content['found']
# Run code file `u` with current namespace
- msg = client.execute_interactive("%runfile {} --current-namespace"
- .format(repr(str(u))), timeout=TIMEOUT)
+ msg = client.execute_interactive(
+ "%runfile {} --current-namespace".format(repr(str(u))),
+ timeout=TIMEOUT
+ )
content = msg['content']
# Verify that the variable `result3` is defined
@@ -727,7 +749,9 @@ def test_runfile(tmpdir):
sys.platform == 'darwin' and sys.version_info[:2] == (3, 8),
reason="Fails on Mac with Python 3.8")
def test_np_threshold(kernel):
- """Test that setting Numpy threshold doesn't make the Variable Explorer slow."""
+ """
+ Test that setting Numpy threshold doesn't make the Variable Explorer slow.
+ """
cmd = "from spyder_kernels.console import start; start.main()"
@@ -786,7 +810,9 @@ def test_np_threshold(kernel):
while "data" not in msg['content']:
msg = client.get_shell_msg(timeout=TIMEOUT)
content = msg['content']['data']['text/plain']
- assert "{'float_kind':