From ddb2809e1126e36e4baca8a3ae20283a37267cfa 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/44] 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 258fde12685..a6b246e740f 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 30cb80510aa569c71e9e431644f7b2353e45d62d 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/44] 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 b1aee6006027b436fd1c1590e465600f50ed03fe 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/44] 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 03584f59d2e5ef22bd745c159a7647727d0c9126 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/44] 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 45d9a3dcac3..21cd7f991c4 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -500,7 +500,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):
@@ -1024,7 +1024,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.
@@ -1037,12 +1037,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 3d225b9ed7e..43d4e36ad87 100644
--- a/spyder/plugins/ipythonconsole/utils/kernelspec.py
+++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py
@@ -174,12 +174,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')
@@ -201,7 +195,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 71d151b27e7..b5d79c1f471 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2472,13 +2472,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 e2b1d5b9d12..6d9dc22c313 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 ac5bddd79fddfd687cac2e79c3672b0ef769b072 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/44] 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 849ba2adc23b1dd50e2e7a122e77483f21fb8127 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/44] 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 0eb0b885f6962542b10a25cabf1ed08af93ae3d2 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/44] Update ipythonconsole plugin tests
---
spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py | 2 --
1 file changed, 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)
From c69a70e6587e958b216379c7b5733b46e3fceab1 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/44] 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 21cd7f991c4..e2ee535cd09 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -1024,7 +1024,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.
@@ -1033,9 +1033,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
@@ -1044,7 +1044,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 b5d79c1f471..ed22b801dad 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2472,13 +2472,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 6d9dc22c313..2ecb66be0e0 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 4caed1601c514b0ccd9eea8621b9d95fe15f7cf6 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/44] 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 a6b246e740f..8277c1243fd 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 eadc45a1ec6a93e5f8832ebcd55360051724ef5d 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/44] 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 57e37c619557eba32bad79860167e1274f4ab3b5 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/44] 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 8cb1c4fe4651643b4f2f1eeb7142af358f7bc5d5 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/44] 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 921e4a797851eb96265cadbcd266e5e58df73315 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/44] 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 b020b5a6405d8a82684755758f0ca8f25242ac49 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/44] 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 5179b2e4fa89703369c2242f49344b7c28024113 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/44] 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 2209405c3d28f48f7f2ffd221396792d833450c8 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/44] 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 a6ee2a4c20c6a031b37562a4d6b441e9147d8fc2 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/44] 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 e442da549d81a44b19194462041852ac4390deba 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/44] 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 3e2fbe999c73073cfbb8777f646c759a099932be 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/44] 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 ea7c67c30dfb43d526880601205b5d3f35706601 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/44] 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 74c21d278f00dd4ae4e7f16a7963ab70b625acdc 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/44] 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 fa17c6f52af8a12f07ff5e0f4bde315e3fe8c50d 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/44] 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 01324f7e8b90ba986aaeb97f1ad6a87631808b3e 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/44] 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 e2ee535cd09..c1b6213c7fc 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -1024,7 +1024,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.
@@ -1033,8 +1033,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
@@ -1044,7 +1042,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 ed22b801dad..e1d9bbf74a8 100644
--- a/spyder/plugins/ipythonconsole/widgets/main_widget.py
+++ b/spyder/plugins/ipythonconsole/widgets/main_widget.py
@@ -2472,13 +2472,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 2ecb66be0e0..a72d0c93f1e 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 779edcbc3e5d6ea5b71b75e52a89d96d3fe77c3c 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/44] 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 f590d22aae163db937b25e4381f9d0c0532da9b4 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/44] 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 c0fd72a14b0..bc6b620e00d 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
@@ -6542,9 +6543,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 1813bf9e23c88a66bcb82fbe95813560fa537998 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/44] 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 39f8c474f09f7cb1e82715e5a5ddb9eb8bba9dc6 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/44] 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 1ce13d1216c..6c709d55262 100644
--- a/spyder/utils/icon_manager.py
+++ b/spyder/utils/icon_manager.py
@@ -290,6 +290,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 cccecfad4c98b3d5ad6856fef536f677ab084f83 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/44] 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 fdc432811015f5d26ff791f36ae3633e80a65252 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/44] 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 64fbb629986959aad3f61bbf4cc48da1032318df 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/44] 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 adf8a144c7353be37847c6bee51af2e4ce064dd1 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/44] 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 0a0f3e36e0288a0384b29288eb98cd1bf9b93a19 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/44] 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 b2e8a048f020b19444ff7a9afb9f7f08cdb01fc0 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/44] 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 043f6db1bd4945b5eb7d292bd7274d5b1c551580 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/44] 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 79da753cf70..c639ee02580 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 bc6b620e00d..a23f515da24 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -97,7 +97,6 @@
)
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
@@ -6519,8 +6518,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.
@@ -6534,11 +6532,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'
@@ -6546,25 +6539,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()
@@ -6577,7 +6562,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 649d23b390d991c93d674117cf75112208c13359 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 20 Jan 2025 20:05:49 -0800
Subject: [PATCH 35/44] Apply suggestions from code review
Co-authored-by: Carlos Cordoba
---
spyder/plugins/completion/api.py | 2 +-
spyder/plugins/completion/plugin.py | 3 +-
.../providers/languageserver/provider.py | 8 ++-
spyder/plugins/ipythonconsole/plugin.py | 4 +-
spyder/plugins/pythonpath/container.py | 51 +++++++++++++------
.../plugins/pythonpath/widgets/pathmanager.py | 19 +++++--
6 files changed, 60 insertions(+), 27 deletions(-)
diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py
index 65cdedb99e6..fb55fc218f5 100644
--- a/spyder/plugins/completion/api.py
+++ b/spyder/plugins/completion/api.py
@@ -1068,7 +1068,7 @@ def python_path_update(self, new_path, prioritize):
----------
new_path: list of str
Dictionary containing the current Python path values.
- prioritize
+ prioritize: bool
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 a77f66aa995..55c76b6bef1 100644
--- a/spyder/plugins/completion/plugin.py
+++ b/spyder/plugins/completion/plugin.py
@@ -126,7 +126,8 @@ class CompletionPlugin(SpyderPluginV2):
sig_pythonpath_changed = Signal(object, bool)
"""
- This signal is used to receive changes on the PYTHONPATH.
+ This signal is used to receive changes on the Python path values handled
+ by Spyder.
Parameters
----------
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
index 936ec663d1d..fa6f96756d4 100644
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -537,8 +537,12 @@ def python_path_update(self, new_path, prioritize):
Update server configuration after a change in Spyder's Python
path.
- `new_path` corresponds to the new state of the Python path.
- `prioritize` determines whether to prioritize Python path in sys.path.
+ Parameters
+ ----------
+ new_path: list of str
+ New state of the Python path handled by Spyder.
+ prioritize: bool
+ Whether to prioritize Python path in sys.path.
"""
# Opening/closing a project will create a diff between old_path
# and new_path, but we don't know if prioritize changed.
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index c1b6213c7fc..40d7849e49a 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -1034,9 +1034,9 @@ def update_path(self, new_path, prioritize):
Parameters
----------
new_path : list of str
- Corresponds to the new state of the PYTHONPATH.
+ New state of the Python path handled by Spyder.
prioritize : bool
- Whether to prioritize PYTHONPATH in sys.path
+ Whether to prioritize Python path in sys.path
Returns
-------
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 6badd44eea9..ad7d7d9e25a 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -49,7 +49,8 @@ def setup(self):
# Path manager dialog
self.path_manager_dialog = PathManager(parent=self, sync=True)
self.path_manager_dialog.sig_path_changed.connect(
- self._save_paths)
+ self._save_paths
+ )
self.path_manager_dialog.redirect_stdio.connect(
self.sig_redirect_stdio_requested)
@@ -67,7 +68,8 @@ 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 in _load_paths, but set in this method
and nowhere else.
@@ -84,8 +86,11 @@ def update_active_project_path(self, path):
self._save_paths()
def show_path_manager(self):
- """Show path manager dialog.
+ """
+ Show path manager dialog.
+ Notes
+ -----
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 consolidate possible changes and avoid emitting multiple signals.
@@ -161,16 +166,24 @@ def _get_system_paths(self):
def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
"""
- 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.
+ Save user and system path dictionaries and prioritize to config.
+
+ Notes
+ -----
+ - Each dictionary key is a path and the value is the active state.
+ - sig_pythonpath_changed is emitted from this method, and nowhere else,
+ on condition that _spyder_pythonpath changed.
- `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.
+ Parameters
+ ----------
+ user_paths: OrderedDict
+ Paths set by the user.
+ system_paths: OrderedDict
+ Paths set in the PYTHONPATH environment variable.
+ prioritize: bool
+ 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))
@@ -231,7 +244,7 @@ def _migrate_to_config_options(self):
and system_path is not None
):
# The configuration does not need to be updated
- return None
+ return
path = []
not_active_path = []
@@ -240,20 +253,26 @@ def _migrate_to_config_options(self):
if osp.isfile(path_file):
with open(path_file, 'r', encoding='utf-8') as f:
path = f.read().splitlines()
- os.remove(path_file)
+ try:
+ os.remove(path_file)
+ except OSError:
+ pass
# 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)
+ try:
+ os.remove(not_active_path_file)
+ except OSError:
+ pass
- # Get path from config; supercedes paths from file
+ # Get path from config; supersedes 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
+ # Get inactive path from config; supersedes paths from file
if config_not_active_path is not None:
not_active_path = config_not_active_path
self.remove_conf('not_active_path')
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index e9344b1067d..813f8be5ff9 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -372,12 +372,20 @@ def export_pythonpath(self):
return
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})
+ 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})
+ 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}
+ )
# Desired behavior is active_user | active_system, but Python 3.8 does
# not support | operator for OrderedDict.
@@ -435,7 +443,8 @@ def update_paths(
system_paths=None,
prioritize=None
):
- """Update path attributes.
+ """
+ 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
From 0ff469a88daa5101f8b5333b419e738b3eb8b2ba Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 20 Jan 2025 10:20:40 -0800
Subject: [PATCH 36/44] Apply suggestion from code review per @ccordoba12
---
.../utils/tests/test_spyder_kernel.py | 30 -------------------
.../plugins/pythonpath/widgets/pathmanager.py | 8 -----
2 files changed, 38 deletions(-)
diff --git a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
index 35b1b187f8a..7fe7baa5fb4 100644
--- a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
+++ b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
@@ -8,40 +8,10 @@
Tests for the Spyder kernel
"""
-import os
import pytest
from spyder.config.manager import CONF
from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec
-from spyder.py3compat import to_text_string
-
-
-@pytest.mark.parametrize('default_interpreter', [True, False])
-def test_kernel_pypath(tmpdir, default_interpreter):
- """
- Test that PYTHONPATH and spyder_pythonpath option are properly handled
- when an external interpreter is used or not.
-
- Regression test for spyder-ide/spyder#8681.
- Regression test for spyder-ide/spyder#17511.
- """
- # Set default interpreter value
- CONF.set('main_interpreter', 'default', default_interpreter)
-
- # Add a path to PYTHONPATH and spyder_pythonpath config option
- pypath = to_text_string(tmpdir.mkdir('test-pypath'))
- os.environ['PYTHONPATH'] = pypath
- CONF.set('pythonpath_manager', 'spyder_pythonpath', [pypath])
-
- kernel_spec = SpyderKernelSpec()
-
- # Check that PYTHONPATH is not in our kernelspec
- assert 'PYTHONPATH' not in kernel_spec.env
-
- # Restore default values
- CONF.set('main_interpreter', 'default', True)
- CONF.set('pythonpath_manager', 'spyder_pythonpath', [])
- del os.environ['PYTHONPATH']
def test_python_interpreter(tmpdir):
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index 813f8be5ff9..a598201cee2 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -696,14 +696,6 @@ def accept(self):
)
super().accept()
- def reject(self):
- # ??? Do we need this?
- super().reject()
-
- def closeEvent(self, event):
- # ??? Do we need this?
- super().closeEvent(event)
-
def test():
"""Run path manager test."""
From 2d5ea0805d56d15e3c78784c013d9ab6696640a0 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 20 Jan 2025 18:15:35 -0800
Subject: [PATCH 37/44] Use a configuration key to store whether the
configuration has been migrated. _migrate_to_config_options only converts
from old path configuration files to the new configuration system.
Load paths from configuration (after possibly migrating the configuration) in setup; no need for _load_paths.
Only get up-to-date system paths upon request from user in the pythonpath manager widget.
---
spyder/plugins/pythonpath/container.py | 83 +++++++------------
spyder/plugins/pythonpath/plugin.py | 4 -
.../plugins/pythonpath/widgets/pathmanager.py | 4 +-
.../widgets/tests/test_pathmanager.py | 2 +-
4 files changed, 32 insertions(+), 61 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index ad7d7d9e25a..f3b5cf5b382 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -43,8 +43,20 @@ def __init__(self, *args, **kwargs):
# ---- PluginMainContainer API
# -------------------------------------------------------------------------
def setup(self):
- # Load Python paths
- self._load_paths()
+ # Migrate to new config options if necessary
+ if not self.get_conf("config_options_migrated", False):
+ self._migrate_to_config_options()
+
+ # This attribute is only used to detect changes and after initializing
+ # here should only be set in update_active_project_path.
+ self._project_paths = OrderedDict()
+
+ # These attributes are only used to detect changes and after
+ # initializing here should only be set in _save_paths.
+ self._user_paths = OrderedDict(self.get_conf('user_paths'))
+ self._system_paths = self.get_conf('system_paths')
+ self._prioritize = self.get_conf('prioritize')
+ self._spyder_pythonpath = self.get_conf('spyder_pythonpath')
# Path manager dialog
self.path_manager_dialog = PathManager(parent=self, sync=True)
@@ -71,8 +83,7 @@ def update_active_project_path(self, path):
"""
Update active project path.
- _project_paths is initialized in _load_paths, but set in this method
- and nowhere else.
+ _project_paths is set in this method and nowhere else.
"""
# _project_paths should be reset whenever it is updated.
self._project_paths = OrderedDict()
@@ -88,14 +99,6 @@ def update_active_project_path(self, path):
def show_path_manager(self):
"""
Show path manager dialog.
-
- Notes
- -----
- 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 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 if widget is already open,
# see spyder-ide/spyder#20808.
@@ -126,38 +129,13 @@ def get_spyder_pythonpath(self):
# ---- Private API
# -------------------------------------------------------------------------
- def _load_paths(self):
- """Load Python paths.
-
- The attributes _project_paths, _user_paths, _system_paths, _prioritize,
- 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()
- 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)
-
- # Get current system PYTHONPATH
- system_paths = self._get_system_paths()
-
- # Get prioritize
- prioritize = self.get_conf('prioritize', False)
-
- self._save_paths(user_paths, system_paths, prioritize)
-
def _get_system_paths(self):
system_paths = get_system_pythonpath()
conf_system_paths = self.get_conf('system_paths', {})
+ # If a system path already exists in the configuration, use the
+ # configuration active state. If it does not exist in the
+ # configuration, then set the active state to True.
system_paths = OrderedDict(
{p: conf_system_paths.get(p, True) for p in system_paths}
)
@@ -167,7 +145,7 @@ def _get_system_paths(self):
def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
"""
Save user and system path dictionaries and prioritize to config.
-
+
Notes
-----
- Each dictionary key is a path and the value is the active state.
@@ -226,7 +204,7 @@ def _migrate_to_config_options(self):
Migrate paths saved in the `path` and `not_active_path` files located
in our config directory to our config system.
- This was the way we save those paths in Spyder 5 and before.
+ ??? When should we remove this?
"""
path_file = get_conf_path('path')
not_active_path_file = get_conf_path('not_active_path')
@@ -235,17 +213,6 @@ def _migrate_to_config_options(self):
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
-
path = []
not_active_path = []
@@ -281,7 +248,9 @@ def _migrate_to_config_options(self):
self.remove_conf('paths_in_conf_files')
# Get system path
+ system_paths = {}
if system_path is not None:
+ system_paths = {p: p not in not_active_path for p in system_path}
self.remove_conf('system_path')
# path config has all user and system paths; only want user paths
@@ -289,4 +258,10 @@ def _migrate_to_config_options(self):
p: p not in not_active_path for p in path if p not in system_path
}
- return user_paths
+
+ # Update the configuration
+ self.set_conf('user_paths', user_paths)
+ self.set_conf('system_paths', system_paths)
+
+ # Do not migrate again
+ self.set_conf("config_options_migrated", True)
diff --git a/spyder/plugins/pythonpath/plugin.py b/spyder/plugins/pythonpath/plugin.py
index 0926fcacd46..d7e5c15ee4b 100644
--- a/spyder/plugins/pythonpath/plugin.py
+++ b/spyder/plugins/pythonpath/plugin.py
@@ -46,10 +46,6 @@ class PythonpathManager(SpyderPluginV2):
prioritize
Whether to prioritize PYTHONPATH in sys.path
-
- See Also
- --------
- :py:meth:`.PythonpathContainer._get_spyder_pythonpath_dict`
"""
# ---- SpyderPluginV2 API
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index a598201cee2..bae8c7bbf39 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -175,7 +175,7 @@ def _setup_right_toolbar(self):
PathManagerToolbuttons.ImportPaths,
tip=_('Import from PYTHONPATH environment variable'),
icon=self.create_icon('fileimport'),
- triggered=lambda x: self.import_paths())
+ triggered=lambda x: self.import_pythonpath())
self.export_button = self.create_toolbutton(
PathManagerToolbuttons.ExportPaths,
icon=self.create_icon('fileexport'),
@@ -619,7 +619,7 @@ def remove_path(self, force=False):
self.refresh()
@Slot()
- def import_paths(self):
+ def import_pythonpath(self):
"""Import PYTHONPATH from environment."""
current_system_paths = self.get_system_paths()
system_paths = get_system_pythonpath()
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index c8010e091a2..2f593c03799 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -70,7 +70,7 @@ def test_import_PYTHONPATH(qtbot, pathmanager, tmp_path, restore_user_env):
assert pathmanager.get_system_paths() == OrderedDict()
# Import PYTHONPATH from environment
- pathmanager.import_paths()
+ pathmanager.import_pythonpath()
assert len(pathmanager.headers) == 1
assert pathmanager.get_system_paths() == OrderedDict({str(sys_dir): True})
From 10ccdc3d402e40d1ef912454ab9313eb3ee861b9 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 20 Jan 2025 18:19:23 -0800
Subject: [PATCH 38/44] Rename _project_paths -> _project_path and consider
only a single path.
---
spyder/plugins/pythonpath/container.py | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index f3b5cf5b382..43b27f009b6 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -49,7 +49,7 @@ def setup(self):
# This attribute is only used to detect changes and after initializing
# here should only be set in update_active_project_path.
- self._project_paths = OrderedDict()
+ self._project_path = OrderedDict()
# These attributes are only used to detect changes and after
# initializing here should only be set in _save_paths.
@@ -83,16 +83,15 @@ def update_active_project_path(self, path):
"""
Update active project path.
- _project_paths is set in this method and nowhere else.
+ _project_path is set in this method and nowhere else.
"""
- # _project_paths should be reset whenever it is updated.
- self._project_paths = OrderedDict()
+ # _project_path should be reset whenever it is updated.
+ self._project_path = OrderedDict()
if path is None:
logger.debug("Update Spyder PYTHONPATH because project was closed")
else:
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})
+ self._project_path.update({path: True})
self._save_paths()
@@ -104,7 +103,7 @@ def show_path_manager(self):
# see spyder-ide/spyder#20808.
if not self.path_manager_dialog.isVisible():
self.path_manager_dialog.update_paths(
- project_paths=self._project_paths,
+ project_paths=self._project_path,
user_paths=self._user_paths,
system_paths=self._system_paths,
prioritize=self._prioritize
@@ -122,7 +121,7 @@ def get_spyder_pythonpath(self):
# 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.update(reversed(self._project_path.items()))
all_paths = OrderedDict(reversed(all_paths.items()))
return [p for p, v in all_paths.items() if v]
From b4f210614f65cfc7f64888e0dde7da61203c2cc0 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 20 Jan 2025 20:02:52 -0800
Subject: [PATCH 39/44] Rename attribute project_paths -> project_path and
declare attributes in __init__
---
spyder/plugins/pythonpath/container.py | 4 ++--
.../plugins/pythonpath/widgets/pathmanager.py | 24 ++++++++++++-------
.../widgets/tests/test_pathmanager.py | 4 ++--
3 files changed, 19 insertions(+), 13 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index 43b27f009b6..f154561d867 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -103,7 +103,7 @@ def show_path_manager(self):
# see spyder-ide/spyder#20808.
if not self.path_manager_dialog.isVisible():
self.path_manager_dialog.update_paths(
- project_paths=self._project_path,
+ project_path=self._project_path,
user_paths=self._user_paths,
system_paths=self._system_paths,
prioritize=self._prioritize
@@ -117,7 +117,7 @@ def show_path_manager(self):
def get_spyder_pythonpath(self):
"""Return active Spyder PYTHONPATH as a list of paths."""
- # Desired behavior is project_paths | user_paths | system_paths, but
+ # Desired behavior is project_path | 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()))
diff --git a/spyder/plugins/pythonpath/widgets/pathmanager.py b/spyder/plugins/pythonpath/widgets/pathmanager.py
index bae8c7bbf39..0b9ed6ddb67 100644
--- a/spyder/plugins/pythonpath/widgets/pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/pathmanager.py
@@ -132,6 +132,12 @@ def __init__(self, parent, sync=True):
self.bbox.accepted.connect(self.accept)
self.bbox.rejected.connect(self.reject)
+ # Attributes
+ self.project_path = None
+ self.user_paths = None
+ self.system_paths = None
+ self.prioritize = None
+
# ---- Private methods
# -------------------------------------------------------------------------
def _add_buttons_to_layout(self, widgets, layout):
@@ -200,7 +206,7 @@ def _create_item(self, path, active):
"""Helper to create a new list item."""
item = QListWidgetItem(path)
- if path in self.project_paths:
+ if path in self.project_path:
item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked)
else:
@@ -279,7 +285,7 @@ def editable_bottom_row(self):
bottom_row = 0
if self.project_header:
- bottom_row += len(self.project_paths) + 1
+ bottom_row += len(self.project_path) + 1
if self.user_header:
bottom_row += len(self.get_user_paths())
@@ -291,7 +297,7 @@ def editable_top_row(self):
top_row = 0
if self.project_header:
- top_row += len(self.project_paths) + 1
+ top_row += len(self.project_path) + 1
if self.user_header:
top_row += 1
@@ -306,7 +312,7 @@ def setup(self):
self.system_header = None
# Project path
- if self.project_paths:
+ if self.project_path:
self.project_header, project_widget = (
self._create_header(_("Project path"))
)
@@ -314,7 +320,7 @@ def setup(self):
self.listwidget.addItem(self.project_header)
self.listwidget.setItemWidget(self.project_header, project_widget)
- for path, active in self.project_paths.items():
+ for path, active in self.project_path.items():
item = self._create_item(path, active)
self.listwidget.addItem(item)
@@ -438,7 +444,7 @@ def get_system_paths(self):
def update_paths(
self,
- project_paths=None,
+ project_path=None,
user_paths=None,
system_paths=None,
prioritize=None
@@ -451,8 +457,8 @@ def update_paths(
used to compare with what is shown in the listwidget in order to detect
changes.
"""
- if project_paths is not None:
- self.project_paths = project_paths
+ if project_path is not None:
+ self.project_path = project_path
if user_paths is not None:
self.user_paths = user_paths
if system_paths is not None:
@@ -707,7 +713,7 @@ 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]},
+ project_path={p: True for p in sys.path[:1]},
system_paths={p: True for p in sys.path[-2:]},
prioritize=False
)
diff --git a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
index 2f593c03799..375385cd4cc 100644
--- a/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
+++ b/spyder/plugins/pythonpath/widgets/tests/test_pathmanager.py
@@ -28,12 +28,12 @@
@pytest.fixture
def pathmanager(qtbot, request):
"""Set up PathManager."""
- user_paths, project_paths, system_paths = request.param
+ user_paths, project_path, 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}),
+ project_path=OrderedDict({p: True for p in project_path}),
system_paths=OrderedDict({p: True for p in system_paths}),
prioritize=False
)
From c833ee44502a623ab358abb928b5aea58bc05016 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Thu, 23 Jan 2025 15:43:43 -0800
Subject: [PATCH 40/44] Add test for PYTHONPATH priority. Initially add path
via path manager widget rather than container (just a further check).
---
spyder/app/tests/test_mainwindow.py | 53 ++++++++++++++++++++++-------
1 file changed, 40 insertions(+), 13 deletions(-)
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index a23f515da24..3a29ed5cfdd 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -6526,30 +6526,36 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
# Wait until the window is fully up
ipyconsole = main_window.ipyconsole
shell = ipyconsole.get_current_shellwidget()
- qtbot.waitUntil(lambda: shell._prompt_html is not None,
- timeout=SHELL_TIMEOUT)
+ qtbot.waitUntil(
+ lambda: shell._prompt_html is not None, timeout=SHELL_TIMEOUT
+ )
# Main variables
ppm = main_window.get_plugin(Plugins.PythonpathManager)
- # Add a directory to the current list of paths to simulate a path added by
- # users
+ # Create a directory to use as a user path
user_dir = tmp_path / 'user_dir'
user_dir.mkdir()
- user_paths = OrderedDict({str(user_dir): True})
- if os.name != "nt":
- assert ppm.get_container()._spyder_pythonpath == []
- # Check the PPM emits the right signal after closing the dialog
+ # Check that the inital configured spyder_pythonpath is empty
+ assert ppm.get_container()._spyder_pythonpath == []
+
+ # Add a directory to the current list of paths to simulate a path added by
+ # the user
+ ppm.show_path_manager()
+ qtbot.wait(500)
+
+ ppm.path_manager_dialog.add_path(directory=user_dir)
+
with qtbot.waitSignal(ppm.sig_pythonpath_changed, timeout=1000):
- ppm.get_container()._save_paths(user_paths=user_paths)
+ ppm.path_manager_dialog.accept()
- # Check directories were added to sys.path in the right order
+ # Check that user_dir was 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[-1:] == [str(user_dir)]
+ assert sys_path[-1] == str(user_dir) # Path should be at the end
# Create new console
ipyconsole.create_new_client()
@@ -6557,12 +6563,33 @@ def test_PYTHONPATH_in_consoles(main_window, qtbot, tmp_path):
qtbot.waitUntil(lambda: shell1._prompt_html is not None,
timeout=SHELL_TIMEOUT)
- # Check directories are part of the new console's sys.path
+ # Check user_dir is part of the new console's sys.path
with qtbot.waitSignal(shell1.executed, timeout=2000):
shell1.execute("import sys; sys_path = sys.path")
sys_path = shell1.get_value("sys_path")
- assert sys_path[-1:] == [str(user_dir)]
+ assert sys_path[-1] == str(user_dir) # Path should be at the end
+
+ # Check that user path can be prepended to sys.path
+ ppm.show_path_manager()
+ qtbot.wait(500)
+
+ # ??? Why does this work...
+ ppm.path_manager_dialog.prioritize_button.setChecked(True)
+ qtbot.wait(500)
+ # ...but this does not?
+ # with qtbot.waitUntil(ppm.path_manager_dialog.prioritize_button.isChecked):
+ # ppm.path_manager_dialog.prioritize_button.animateClick()
+
+ with qtbot.waitSignal(ppm.sig_pythonpath_changed, timeout=1000):
+ ppm.path_manager_dialog.accept()
+
+ for s in [shell, shell1]:
+ with qtbot.waitSignal(s.executed, timeout=2000):
+ s.execute("sys_path = sys.path")
+
+ sys_path = shell.get_value("sys_path")
+ assert sys_path[1] == str(user_dir) # Path should be ['', user_dir, ...]
# Check that disabling a path from the PPM removes it from sys.path in all
# consoles
From 339f147dd77936b3759301ca89a7ba858e050b13 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 17 Feb 2025 22:25:50 -0800
Subject: [PATCH 41/44] Apply suggestions from code review
Co-authored-by: Carlos Cordoba
---
spyder/plugins/pythonpath/container.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/spyder/plugins/pythonpath/container.py b/spyder/plugins/pythonpath/container.py
index f154561d867..7325287d74e 100644
--- a/spyder/plugins/pythonpath/container.py
+++ b/spyder/plugins/pythonpath/container.py
@@ -145,12 +145,6 @@ def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
"""
Save user and system path dictionaries and prioritize to config.
- Notes
- -----
- - Each dictionary key is a path and the value is the active state.
- - sig_pythonpath_changed is emitted from this method, and nowhere else,
- on condition that _spyder_pythonpath changed.
-
Parameters
----------
user_paths: OrderedDict
@@ -161,6 +155,12 @@ def _save_paths(self, user_paths=None, system_paths=None, prioritize=None):
Whether paths should be prepended (True) or appended (False) to
sys.path.
+ Notes
+ -----
+ - Each dictionary key is a path and the value is the active state.
+ - 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))
@@ -203,7 +203,7 @@ def _migrate_to_config_options(self):
Migrate paths saved in the `path` and `not_active_path` files located
in our config directory to our config system.
- ??? When should we remove this?
+ # TODO: Remove for Spyder 7
"""
path_file = get_conf_path('path')
not_active_path_file = get_conf_path('not_active_path')
From 2646476a221b468df305471bd9e48f489bac9da3 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 17 Feb 2025 14:45:21 -0800
Subject: [PATCH 42/44] Increase minimal support for python-lsp-server
---
binder/environment.yml | 2 +-
requirements/main.yml | 2 +-
setup.py | 4 ++--
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/binder/environment.yml b/binder/environment.yml
index a13690d78cc..0f033c13e4c 100644
--- a/binder/environment.yml
+++ b/binder/environment.yml
@@ -39,7 +39,7 @@ dependencies:
- pyqt >=5.15,<5.16
- pyqtwebengine >=5.15,<5.16
- python-lsp-black >=2.0.0,<3.0.0
-- python-lsp-server >=1.12.0,<1.13.0
+- python-lsp-server >=1.12.2,<1.13.0
- pyuca >=1.2
- pyxdg >=0.26
- pyzmq >=24.0.0
diff --git a/requirements/main.yml b/requirements/main.yml
index 55d326d9699..20e2bebf58f 100644
--- a/requirements/main.yml
+++ b/requirements/main.yml
@@ -37,7 +37,7 @@ dependencies:
- pyqt >=5.15,<5.16
- pyqtwebengine >=5.15,<5.16
- python-lsp-black >=2.0.0,<3.0.0
- - python-lsp-server >=1.12.0,<1.13.0
+ - python-lsp-server >=1.12.2,<1.13.0
- pyuca >=1.2
- pyzmq >=24.0.0
- qdarkstyle >=3.2.0,<3.3.0
diff --git a/setup.py b/setup.py
index bc986ce5441..788be4c1d0c 100644
--- a/setup.py
+++ b/setup.py
@@ -299,7 +299,7 @@ def run(self):
'pylint-venv>=3.0.2',
'pyls-spyder>=0.4.0',
'python-lsp-black>=2.0.0,<3.0.0',
- 'python-lsp-server[all]>=1.12.0,<1.13.0',
+ 'python-lsp-server[all]>=1.12.2,<1.13.0',
'pyuca>=1.2',
'pyxdg>=0.26;platform_system=="Linux"',
'pyzmq>=24.0.0',
@@ -329,7 +329,7 @@ def run(self):
install_requires = [req for req in install_requires
if req.split(">")[0] not in reqs_to_loosen]
- install_requires.append('python-lsp-server[all]>=1.12.0,<1.14.0')
+ install_requires.append('python-lsp-server[all]>=1.12.2,<1.14.0')
install_requires.append('qtconsole>=5.5.1,<5.7.0')
extras_require = {
From b009815270ee1b0bdfcabac8f0147ac61d16ca98 Mon Sep 17 00:00:00 2001
From: Ryan Clary <9618975+mrclary@users.noreply.github.com>
Date: Mon, 17 Feb 2025 22:31:21 -0800
Subject: [PATCH 43/44] git subrepo clone (merge) --branch=master --force
https://github.com/spyder-ide/spyder-kernels.git external-deps/spyder-kernels
subrepo:
subdir: "external-deps/spyder-kernels"
merged: "633c29712"
upstream:
origin: "https://github.com/spyder-ide/spyder-kernels.git"
branch: "master"
commit: "633c29712"
git-subrepo:
version: "0.4.9"
origin: "???"
commit: "???"
---
external-deps/spyder-kernels/.gitrepo | 4 +-
.../spyder_kernels/console/kernel.py | 52 +++++++++-----
.../spyder_kernels/console/start.py | 29 ++++----
.../console/tests/test_console_kernel.py | 67 +++++++++++++------
.../customize/spydercustomize.py | 14 ----
5 files changed, 100 insertions(+), 66 deletions(-)
diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo
index 57d90f4bf1b..b1737842092 100644
--- a/external-deps/spyder-kernels/.gitrepo
+++ b/external-deps/spyder-kernels/.gitrepo
@@ -6,7 +6,7 @@
[subrepo]
remote = https://github.com/spyder-ide/spyder-kernels.git
branch = master
- commit = 67259709972e39c6d13b463076670a62dc9e832e
- parent = 00f4b6630c6846ebd51514b8bc63f49c4821b38b
+ commit = 633c29712f7260c1dceca25bcecbb5def91e5818
+ parent = 1aab01be14f79b17da61348d564b20cad69dabc4
method = merge
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 477fcd95ff2..43e47d7118d 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
@@ -770,27 +775,42 @@ 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':
Date: Tue, 18 Feb 2025 07:20:53 -0800
Subject: [PATCH 44/44] Apply suggestions from code review
---
changelogs/Spyder-6.md | 1 +
spyder/dependencies.py | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/changelogs/Spyder-6.md b/changelogs/Spyder-6.md
index 01024eeadf8..9a096bde41a 100644
--- a/changelogs/Spyder-6.md
+++ b/changelogs/Spyder-6.md
@@ -4,6 +4,7 @@
### API changes
+* **Breaking** - The `sig_pythonpath_changed` signal of the Python path manager plugin now emits a list of strings and a bool, instead of two dictionaries.
* Add `early_return` and `return_awaitable` kwargs to `AsyncDispatcher` constructor.
* Add `register_api` and `get_api` methods to `RemoteClient` plugin in order to
get and register new rest API modules for the remote client.
diff --git a/spyder/dependencies.py b/spyder/dependencies.py
index 837d1311bdf..8a64654399b 100644
--- a/spyder/dependencies.py
+++ b/spyder/dependencies.py
@@ -60,7 +60,7 @@
PYGMENTS_REQVER = '>=2.0'
PYLINT_REQVER = '>=3.1,<4'
PYLINT_VENV_REQVER = '>=3.0.2'
-PYLSP_REQVER = '>=1.12.0,<1.13.0'
+PYLSP_REQVER = '>=1.12.2,<1.13.0'
PYLSP_BLACK_REQVER = '>=2.0.0,<3.0.0'
PYLS_SPYDER_REQVER = '>=0.4.0'
PYUCA_REQVER = '>=1.2'