From cb346166a713dcf8e2278951c5ddc96e8337d203 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 13:31:32 -0500 Subject: [PATCH 1/7] Outline: Restore functionality to update all editors This is necessary to update the outline for all files when closing a project. --- spyder/plugins/editor/plugin.py | 6 ++++++ spyder/plugins/outlineexplorer/plugin.py | 17 +++++++++++++---- spyder/plugins/outlineexplorer/widgets.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py index da3fe43f879..2c6235f8435 100644 --- a/spyder/plugins/editor/plugin.py +++ b/spyder/plugins/editor/plugin.py @@ -149,6 +149,11 @@ class Editor(SpyderPluginWidget): :py:meth:spyder.plugins.editor.widgets.editor.EditorStack.send_to_help """ + sig_open_files_finished = Signal() + """ + This signal is emitted when the editor finished to open files. + """ + def __init__(self, parent, ignore_last_opened_files=False): SpyderPluginWidget.__init__(self, parent) @@ -3175,6 +3180,7 @@ def setup_open_files(self, close_previous_files=True): else: self.__load_temp_file() self.set_create_new_file_if_empty(True) + self.sig_open_files_finished.emit() def save_open_files(self): """Save the list of open files""" diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py index 2fd2f552b90..0b715bb8573 100644 --- a/spyder/plugins/outlineexplorer/plugin.py +++ b/spyder/plugins/outlineexplorer/plugin.py @@ -8,14 +8,11 @@ # Third party imports from qtpy.QtCore import Slot -from qtpy.QtWidgets import QVBoxLayout # Local imports from spyder.api.plugin_registration.decorators import on_plugin_available from spyder.api.translations import get_translation from spyder.api.plugins import SpyderDockablePlugin, Plugins -from spyder.py3compat import is_text_string -from spyder.utils.icon_manager import ima from spyder.plugins.outlineexplorer.widgets import OutlineExplorerWidget # Localization @@ -25,7 +22,7 @@ class OutlineExplorer(SpyderDockablePlugin): NAME = 'outline_explorer' CONF_SECTION = 'outline_explorer' - REQUIRES = [Plugins.Completions] + REQUIRES = [Plugins.Completions, Plugins.Editor] OPTIONAL = [] CONF_FILE = False @@ -59,6 +56,13 @@ def on_completions_available(self): completions.sig_stop_completions.connect( self.stop_symbol_services) + @on_plugin_available(plugin=Plugins.Editor) + def on_editor_available(self): + editor = self.get_plugin(Plugins.Editor) + + editor.sig_open_files_finished.connect( + self.update_all_editors) + #------ Public API --------------------------------------------------------- def restore_scrollbar_position(self): """Restoring scrollbar position after main window is visible""" @@ -79,3 +83,8 @@ def stop_symbol_services(self, language): """Disable LSP symbols functionality.""" explorer = self.get_widget() explorer.stop_symbol_services(language) + + def update_all_editors(self): + """Update all editors with an associated LSP server.""" + explorer = self.get_widget() + explorer.update_all_editors() diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py index f40dfa68a04..14323b5c243 100644 --- a/spyder/plugins/outlineexplorer/widgets.py +++ b/spyder/plugins/outlineexplorer/widgets.py @@ -552,6 +552,12 @@ def update_editors(self, language): self.editors_to_update[language].remove(editor) self.update_timers[language].start() + def update_all_editors(self, reset_info=False): + """Update all editors with LSP support.""" + for language in self._languages: + self.set_editors_to_update(language, reset_info=reset_info) + self.update_timers[language].start() + @Slot(list) def update_editor(self, items, editor=None): """ @@ -998,6 +1004,8 @@ def setup(self): def update_actions(self): pass + # ---- Public API + # ------------------------------------------------------------------------ def set_current_editor(self, editor, update, clear): if clear: self.remove_editor(editor) @@ -1020,3 +1028,7 @@ def start_symbol_services(self, language): def stop_symbol_services(self, language): """Disable LSP symbols functionality.""" self.treewidget.stop_symbol_services(language) + + def update_all_editors(self): + """Update all editors with an associated LSP server.""" + self.treewidget.update_all_editors() From 1543a7e25b4bc064f1135ddb52369888d40aacbb Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 15:11:38 -0500 Subject: [PATCH 2/7] git subrepo clone --branch=improve-skip-symbols --force https://github.com/ccordoba12/python-lsp-server.git external-deps/python-lsp-server subrepo: subdir: "external-deps/python-lsp-server" merged: "535d6e927" upstream: origin: "https://github.com/ccordoba12/python-lsp-server.git" branch: "improve-skip-symbols" commit: "535d6e927" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" --- external-deps/python-lsp-server/.gitrepo | 6 +-- .../pylsp/plugins/pylint_lint.py | 1 + .../pylsp/plugins/symbols.py | 49 ++++++++++++++----- .../test/test_language_server.py | 2 + 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/external-deps/python-lsp-server/.gitrepo b/external-deps/python-lsp-server/.gitrepo index d5114286f4d..8dfe685e89a 100644 --- a/external-deps/python-lsp-server/.gitrepo +++ b/external-deps/python-lsp-server/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/python-lsp/python-lsp-server.git - branch = develop - commit = e802f2889ae33d45166c6cf3efbeb3a87a39c23b - parent = 1ffd2053d8c97c4573bb6633590b12291a3c7704 + branch = improve-skip-symbols + commit = 535d6e927b84ca5daf8146040523a0cd36c41944 + parent = cb346166a713dcf8e2278951c5ddc96e8337d203 method = merge cmdver = 0.4.3 diff --git a/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py b/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py index d5ff3dbf753..bdb65fe23db 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py +++ b/external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py @@ -280,6 +280,7 @@ def _parse_pylint_stdio_result(document, stdout): 'C': lsp.DiagnosticSeverity.Information, 'E': lsp.DiagnosticSeverity.Error, 'F': lsp.DiagnosticSeverity.Error, + 'I': lsp.DiagnosticSeverity.Information, 'R': lsp.DiagnosticSeverity.Hint, 'W': lsp.DiagnosticSeverity.Warning, } diff --git a/external-deps/python-lsp-server/pylsp/plugins/symbols.py b/external-deps/python-lsp-server/pylsp/plugins/symbols.py index 4c0ac855d33..2a00e612fb4 100644 --- a/external-deps/python-lsp-server/pylsp/plugins/symbols.py +++ b/external-deps/python-lsp-server/pylsp/plugins/symbols.py @@ -16,6 +16,8 @@ def pylsp_document_symbols(config, document): # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-locals # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + symbols_settings = config.plugin_settings('jedi_symbols') all_scopes = symbols_settings.get('all_scopes', True) add_import_symbols = symbols_settings.get('include_import_symbols', True) @@ -23,6 +25,7 @@ def pylsp_document_symbols(config, document): symbols = [] exclude = set({}) redefinitions = {} + while definitions != []: d = definitions.pop(0) @@ -33,27 +36,47 @@ def pylsp_document_symbols(config, document): if ' import ' in code or 'import ' in code: continue - # Skip comparing module names. + # Skip imported symbols comparing module names. sym_full_name = d.full_name - module_name = document.dot_path + document_dot_path = document.dot_path if sym_full_name is not None: - # module_name returns where the symbol is imported, whereas - # full_name says where it really comes from. So if the parent - # modules in full_name are not in module_name, it means the - # symbol was not defined there. - # Note: The last element of sym_full_name is the symbol itself, - # so we don't need to use it below. + # We assume a symbol is imported from another module to start + # with. imported_symbol = True - for mod in sym_full_name.split('.')[:-1]: - if mod in module_name: - imported_symbol = False + + # The last element of sym_full_name is the symbol itself, so + # we need to discard it to do module comparisons below. + if '.' in sym_full_name: + sym_module_name = sym_full_name.rpartition('.')[0] + + # This is necessary to display symbols in init files (the checks + # below fail without it). + if document_dot_path.endswith('__init__'): + document_dot_path = document_dot_path.rpartition('.')[0] + + # document_dot_path is the module where the symbol is imported, + # whereas sym_module_name is the one where it was declared. + if sym_module_name.startswith(document_dot_path): + # If sym_module_name starts with the same string as document_dot_path, + # we can safely assume it was declared in the document. + imported_symbol = False + elif sym_module_name.split('.')[0] in document_dot_path.split('.'): + # If the first module in sym_module_name is one of the modules in + # document_dot_path, we need to check if sym_module_name starts + # with the modules in document_dot_path. + document_mods = document_dot_path.split('.') + for i in range(1, len(document_mods) + 1): + submod = '.'.join(document_mods[-i:]) + if sym_module_name.startswith(submod): + imported_symbol = False + break # When there's no __init__.py next to a file or in one of its - # parents, the check above fails. However, Jedi has a nice way + # parents, the checks above fail. However, Jedi has a nice way # to tell if the symbol was declared in the same file: if # full_name starts by __main__. if imported_symbol: - if not sym_full_name.startswith('__main__'): + if not sym_module_name.startswith('__main__'): continue try: diff --git a/external-deps/python-lsp-server/test/test_language_server.py b/external-deps/python-lsp-server/test/test_language_server.py index 8d1f89276a3..92d1ea84d5e 100644 --- a/external-deps/python-lsp-server/test/test_language_server.py +++ b/external-deps/python-lsp-server/test/test_language_server.py @@ -102,6 +102,7 @@ def test_exit_with_parent_process_died(client_exited_server): # pylint: disable assert not client_exited_server.client_thread.is_alive() +@flaky(max_runs=10, min_passes=1) @pytest.mark.skipif(sys.platform.startswith('linux'), reason='Fails on linux') def test_not_exit_without_check_parent_process_flag(client_server): # pylint: disable=redefined-outer-name response = client_server._endpoint.request('initialize', { @@ -112,6 +113,7 @@ def test_not_exit_without_check_parent_process_flag(client_server): # pylint: d assert 'capabilities' in response +@flaky(max_runs=10, min_passes=1) @pytest.mark.skipif(RUNNING_IN_CI, reason='This test is hanging on CI') def test_missing_message(client_server): # pylint: disable=redefined-outer-name with pytest.raises(JsonRpcMethodNotFound): From b4a2ccb3baf9696642dbddac1f0a8a28c3e4a47c Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 15:15:15 -0500 Subject: [PATCH 3/7] Testing: Improve test_update_outline to catch other cases --- spyder/app/tests/script_outline_1.py | 15 ++++++ spyder/app/tests/script_outline_2.py | 18 +++++++ spyder/app/tests/script_outline_3.py | 18 +++++++ spyder/app/tests/test_mainwindow.py | 73 ++++++++++++++++------------ 4 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 spyder/app/tests/script_outline_1.py create mode 100644 spyder/app/tests/script_outline_2.py create mode 100644 spyder/app/tests/script_outline_3.py diff --git a/spyder/app/tests/script_outline_1.py b/spyder/app/tests/script_outline_1.py new file mode 100644 index 00000000000..c9f7ff9bfa8 --- /dev/null +++ b/spyder/app/tests/script_outline_1.py @@ -0,0 +1,15 @@ +from math import cos +from numpy import ( + linspace) + +def foo(x): + return x + +class MyClass: + C = 1 + + def one(self): + return 1 + + def two(self): + return 2 diff --git a/spyder/app/tests/script_outline_2.py b/spyder/app/tests/script_outline_2.py new file mode 100644 index 00000000000..dab69d31ae3 --- /dev/null +++ b/spyder/app/tests/script_outline_2.py @@ -0,0 +1,18 @@ +from math import cos +from numpy import ( + linspace) + +from file1 import ( + foo) + +def bar(x): + return x + +class MyOtherClass: + D = 1 + + def three(self): + return 3 + + def four(self): + return 4 diff --git a/spyder/app/tests/script_outline_3.py b/spyder/app/tests/script_outline_3.py new file mode 100644 index 00000000000..8e36e9fca3a --- /dev/null +++ b/spyder/app/tests/script_outline_3.py @@ -0,0 +1,18 @@ +from math import cos +from numpy import ( + linspace) + +from subdir.a import ( + bar) + +def baz(x): + return x + +class AnotherClass: + E = 1 + + def five(self): + return 5 + + def six(self): + return 4 diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 45695230128..40d3ac9172a 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -60,6 +60,7 @@ from spyder.plugins.layout.layouts import DefaultLayouts from spyder.plugins.projects.api import EmptyProject from spyder.py3compat import PY2, to_text_string +from spyder.utils import encoding from spyder.utils.misc import remove_backslashes from spyder.utils.clipboard_helper import CLIPBOARD_HELPER from spyder.widgets.dock import DockTitleBar @@ -183,10 +184,14 @@ def remove_fake_entrypoints(): pass +def read_asset_file(filename): + """Read contents of an asset file.""" + return encoding.read(osp.join(LOCATION, filename))[0] + + # ============================================================================= # ---- Fixtures # ============================================================================= - @pytest.fixture def main_window(request, tmpdir): """Main Window fixture""" @@ -230,6 +235,7 @@ def main_window(request, tmpdir): # Create project project = tmpdir.mkdir('test_project') project_subdir = project.mkdir('subdir') + project_sub_subdir = project_subdir.mkdir('sub_subdir') # Create directories out of the project out_of_project_1 = tmpdir.mkdir('out_of_project_1') @@ -242,36 +248,33 @@ def main_window(request, tmpdir): CONF.set('project_explorer', 'current_project_path', project_path) # Add some files to project. This is necessary to test that we get - # symgbols for all these files. + # symbols for all these files. abs_filenames = [] filenames_to_create = { project: ['file1.py', 'file2.py', 'file3.txt', '__init__.py'], project_subdir: ['a.py', '__init__.py'], - out_of_project_1: ['b.py'], - out_of_project_2: ['c.py', '__init__.py'], - out_of_project_1_subdir: ['d.py', '__init__.py'], - out_of_project_2_subdir: ['e.py'] + project_sub_subdir: ['b.py', '__init__.py'], + out_of_project_1: ['c.py'], + out_of_project_2: ['d.py', '__init__.py'], + out_of_project_1_subdir: ['e.py', '__init__.py'], + out_of_project_2_subdir: ['f.py'] } for path in filenames_to_create.keys(): filenames = filenames_to_create[path] for filename in filenames: - f = path.join(filename) - abs_filenames.append(str(f)) + file = path.join(filename) + abs_filenames.append(str(file)) if osp.splitext(filename)[1] == '.py': - code = dedent( - """ - from math import cos - from numpy import ( - linspace) - - def f(x): - return x - """ - ) - f.write(code) + if path == project_subdir: + code = read_asset_file('script_outline_2.py') + elif path == project_sub_subdir: + code = read_asset_file('script_outline_3.py') + else: + code = read_asset_file('script_outline_1.py') + file.write(code) else: - f.write("Hello world!") + file.write("Hello world!") spy_project.set_recent_files(abs_filenames) else: @@ -3842,12 +3845,12 @@ def test_update_outline(main_window, qtbot, tmpdir): ] # Wait a bit for trees to be filled - qtbot.wait(20000) + qtbot.wait(25000) # Assert all Python editors are filled assert all( [ - len(treewidget.editor_tree_cache[editor.get_id()]) == 1 + len(treewidget.editor_tree_cache[editor.get_id()]) == 4 for editor in editors_py ] ) @@ -3872,21 +3875,29 @@ def test_update_outline(main_window, qtbot, tmpdir): # Assert spinner is not shown assert not outline_explorer.get_widget()._spinner.isSpinning() - # Set one file as session without projects - prev_file = tmpdir.join("foo.py") - prev_file.write("def zz(x):\n" - " return x**2\n") - CONF.set('editor', 'filenames', [str(prev_file)]) + # Set some files as session without projects + prev_filenames = ["prev_file_1.py", "prev_file_2.py"] + prev_paths = [] + for fname in prev_filenames: + file = tmpdir.join(fname) + file.write(read_asset_file("script_outline_1.py")) + prev_paths.append(str(file)) + + CONF.set('editor', 'filenames', prev_paths) # Close project to open that file automatically main_window.projects.close_project() # Wait a bit for its tree to be filled - qtbot.wait(1000) + qtbot.wait(3000) - # Assert the editor was filled - editor = list(treewidget.editor_ids.keys())[0] - assert len(treewidget.editor_tree_cache[editor.get_id()]) > 0 + # Assert the editors were filled + assert all( + [ + len(treewidget.editor_tree_cache[editor.get_id()]) == 4 + for editor in treewidget.editor_ids.keys() + ] + ) # Remove test file from session CONF.set('editor', 'filenames', []) From e932d0cc192e28eaf2c7c598b18430fcd3c5dca9 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 15:57:11 -0500 Subject: [PATCH 4/7] Testing: Install jupyter-client 6 in pip slots --- .github/scripts/install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 1f31faf2657..5322f9f3197 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -51,6 +51,9 @@ else pip uninstall spyder-kernels -q -y pip uninstall python-lsp-server -q -y pip uninstall qdarkstyle -q -y + + # Install jupyter-client 6 until we release spyder-kernels 2.1.1 + pip install jupyter-client==6.1.12 fi # Install subrepos in development mode From 32131a32531cba48d1e629f1d19f817c0c938d81 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 19:49:04 -0500 Subject: [PATCH 5/7] Testing: Add a way to create simple projects to the main_window fixture --- pytest.ini | 1 + spyder/app/tests/test_mainwindow.py | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 6b610ee72be..ac5e04a96b8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -25,6 +25,7 @@ markers = change_directory use_startup_wdir: Test startup workingdir CONF preload_project: Preload a project on the main window + preload_complex_project: Preload a complex project on the main window no_xvfb external_interpreter test_environment_interpreter diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index 40d3ac9172a..c03f6968a73 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -228,10 +228,30 @@ def main_window(request, tmpdir): else: CONF.set('main', 'single_instance', False) - # Check if we need to preload a project in a give test + # Check if we need to load a simple project to the interface preload_project = request.node.get_closest_marker('preload_project') if preload_project: + # Create project directory + project = tmpdir.mkdir('test_project') + project_path = str(project) + + # Create Spyder project + spy_project = EmptyProject(project_path) + CONF.set('project_explorer', 'current_project_path', project_path) + + # Add a file to the project + file = project.join('file.py') + file.write(read_asset_file('script_outline_1.py')) + spy_project.set_recent_files([str(file)]) + else: + CONF.set('project_explorer', 'current_project_path', None) + + # Check if we need to preload a complex project in a give test + preload_complex_project = request.node.get_closest_marker( + 'preload_complex_project') + + if preload_complex_project: # Create project project = tmpdir.mkdir('test_project') project_subdir = project.mkdir('subdir') @@ -278,7 +298,8 @@ def main_window(request, tmpdir): spy_project.set_recent_files(abs_filenames) else: - CONF.set('project_explorer', 'current_project_path', None) + if not preload_project: + CONF.set('project_explorer', 'current_project_path', None) # Get config values passed in parametrize and apply them try: @@ -3825,7 +3846,7 @@ def test_tour_message(main_window, qtbot): @pytest.mark.slow @flaky(max_runs=3) @pytest.mark.use_introspection -@pytest.mark.preload_project +@pytest.mark.preload_complex_project @pytest.mark.skipif(not sys.platform.startswith('linux'), reason="Only works on Linux") def test_update_outline(main_window, qtbot, tmpdir): From f1612a3d40e43b49241b9df0241cd45d515aa3b4 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 20:29:56 -0500 Subject: [PATCH 6/7] Testing: Skip a test on Windows because it started to time out --- spyder/app/tests/test_mainwindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py index c03f6968a73..a783459996e 100644 --- a/spyder/app/tests/test_mainwindow.py +++ b/spyder/app/tests/test_mainwindow.py @@ -3742,6 +3742,7 @@ def hello(): @flaky(max_runs=3) @pytest.mark.use_introspection @pytest.mark.preload_project +@pytest.mark.skipif(os.name == 'nt', reason='Times out on Windows') def test_ordering_lsp_requests_at_startup(main_window, qtbot): """ Test the ordering of requests we send to the LSP at startup when a From a5411c95a69e7d9b1f5ece111f25c39be43cc754 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 20 Aug 2021 21:10:41 -0500 Subject: [PATCH 7/7] git subrepo clone --branch=develop --force https://github.com/python-lsp/python-lsp-server.git external-deps/python-lsp-server subrepo: subdir: "external-deps/python-lsp-server" merged: "b5b2ff027" upstream: origin: "https://github.com/python-lsp/python-lsp-server.git" branch: "develop" commit: "b5b2ff027" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" [ci skip] --- external-deps/python-lsp-server/.gitrepo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-deps/python-lsp-server/.gitrepo b/external-deps/python-lsp-server/.gitrepo index 8dfe685e89a..d06b8de8418 100644 --- a/external-deps/python-lsp-server/.gitrepo +++ b/external-deps/python-lsp-server/.gitrepo @@ -5,8 +5,8 @@ ; [subrepo] remote = https://github.com/python-lsp/python-lsp-server.git - branch = improve-skip-symbols - commit = 535d6e927b84ca5daf8146040523a0cd36c41944 - parent = cb346166a713dcf8e2278951c5ddc96e8337d203 + branch = develop + commit = b5b2ff02703209e800633ead5b4764ca4459e274 + parent = f1612a3d40e43b49241b9df0241cd45d515aa3b4 method = merge cmdver = 0.4.3