diff --git a/CONFIGURATION.md b/CONFIGURATION.md index e84c09d0..46ab4adb 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -32,6 +32,7 @@ This server can be configured using `workspace/didChangeConfiguration` method. E | `pylsp.plugins.jedi_signature_help.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_symbols.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.jedi_symbols.all_scopes` | `boolean` | If True lists the names of all scopes instead of only the module namespace. | `true` | +| `pylsp.plugins.jedi_symbols.include_import_symbols` | `boolean` | If True includes symbols imported from other libraries. | `true` | | `pylsp.plugins.mccabe.enabled` | `boolean` | Enable or disable the plugin. | `true` | | `pylsp.plugins.mccabe.threshold` | `number` | The minimum threshold that triggers warnings about cyclomatic complexity. | `15` | | `pylsp.plugins.preload.enabled` | `boolean` | Enable or disable the plugin. | `true` | diff --git a/pylsp/config/schema.json b/pylsp/config/schema.json index bd4c2e70..d95db74a 100644 --- a/pylsp/config/schema.json +++ b/pylsp/config/schema.json @@ -157,6 +157,11 @@ "default": true, "description": "If True lists the names of all scopes instead of only the module namespace." }, + "pylsp.plugins.jedi_symbols.include_import_symbols": { + "type": "boolean", + "default": true, + "description": "If True includes symbols imported from other libraries." + }, "pylsp.plugins.mccabe.enabled": { "type": "boolean", "default": true, diff --git a/pylsp/plugins/symbols.py b/pylsp/plugins/symbols.py index 87c4bd2c..4c0ac855 100644 --- a/pylsp/plugins/symbols.py +++ b/pylsp/plugins/symbols.py @@ -19,28 +19,46 @@ def pylsp_document_symbols(config, document): 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) - - use_document_path = False - document_dir = os.path.normpath(os.path.dirname(document.path)) - if not os.path.isfile(os.path.join(document_dir, '__init__.py')): - use_document_path = True - - definitions = document.jedi_names(use_document_path, all_scopes=all_scopes) - module_name = document.dot_path + definitions = document.jedi_names(all_scopes=all_scopes) symbols = [] exclude = set({}) redefinitions = {} while definitions != []: d = definitions.pop(0) + + # Skip symbols imported from other modules. if not add_import_symbols: + # Skip if there's an import in the code the symbol is defined. + code = d.get_line_code() + if ' import ' in code or 'import ' in code: + continue + + # Skip comparing module names. sym_full_name = d.full_name + module_name = document.dot_path if sym_full_name is not None: - if (not sym_full_name.startswith(module_name) and - not sym_full_name.startswith('__main__')): - continue + # 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. + imported_symbol = True + for mod in sym_full_name.split('.')[:-1]: + if mod in module_name: + imported_symbol = False + + # 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 + # 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__'): + continue + try: docismodule = os.path.samefile(document.path, d.module_path) - except TypeError: + except (TypeError, FileNotFoundError): # Python 2 on Windows has no .samefile, but then these are # strings for sure docismodule = document.path == d.module_path diff --git a/pylsp/python_lsp.py b/pylsp/python_lsp.py index ec099234..c71f08b3 100644 --- a/pylsp/python_lsp.py +++ b/pylsp/python_lsp.py @@ -383,7 +383,8 @@ def m_text_document__signature_help(self, textDocument=None, position=None, **_k return self.signature_help(textDocument['uri'], position) def m_workspace__did_change_configuration(self, settings=None): - self.config.update((settings or {}).get('pylsp', {})) + if self.config is not None: + self.config.update((settings or {}).get('pylsp', {})) for workspace in self.workspaces.values(): workspace.update_config(settings) for doc_uri in workspace.documents: diff --git a/pylsp/workspace.py b/pylsp/workspace.py index a063cabe..ec031b6f 100644 --- a/pylsp/workspace.py +++ b/pylsp/workspace.py @@ -239,8 +239,8 @@ def word_at_position(self, position): return m_start[0] + m_end[-1] @lock - def jedi_names(self, use_document_path, all_scopes=False, definitions=True, references=False): - script = self.jedi_script(use_document_path=use_document_path) + def jedi_names(self, all_scopes=False, definitions=True, references=False): + script = self.jedi_script() return script.get_names(all_scopes=all_scopes, definitions=definitions, references=references) diff --git a/test/test_language_server.py b/test/test_language_server.py index 0b5c1ae8..c6f66001 100644 --- a/test/test_language_server.py +++ b/test/test_language_server.py @@ -7,6 +7,7 @@ import sys from threading import Thread +from flaky import flaky from pylsp_jsonrpc.exceptions import JsonRpcMethodNotFound import pytest @@ -75,6 +76,7 @@ def client_exited_server(): assert client_server_pair.process.is_alive() is False +@flaky(max_runs=10, min_passes=1) @pytest.mark.skipif(sys.platform == 'darwin', reason='Too flaky on Mac') def test_initialize(client_server): # pylint: disable=redefined-outer-name response = client_server._endpoint.request('initialize', {