diff --git a/pylsp/plugins/symbols.py b/pylsp/plugins/symbols.py index 4c0ac855..2a00e612 100644 --- a/pylsp/plugins/symbols.py +++ b/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/test/test_language_server.py b/test/test_language_server.py index 8d1f8927..92d1ea84 100644 --- a/test/test_language_server.py +++ b/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):