diff --git a/.github/scripts/install.sh b/.github/scripts/install.sh index 7f0872ea05e..ebd1af30d85 100755 --- a/.github/scripts/install.sh +++ b/.github/scripts/install.sh @@ -14,7 +14,11 @@ if [ "$USE_CONDA" = "true" ]; then fi # Install main dependencies - conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y + if [ "$PYTHON_VERSION" = "2.7" ]; then + conda install python=$PYTHON_VERSION --file requirements/conda-2.7.txt -q -y + else + conda install python=$PYTHON_VERSION --file requirements/conda.txt -q -y + fi # Install test ones conda install python=$PYTHON_VERSION --file requirements/tests.txt -c spyder-ide -q -y @@ -45,13 +49,17 @@ else # Remove packages we have subrepos for pip uninstall spyder-kernels -q -y - pip uninstall python-language-server -q -y + if [ "$PYTHON_VERSION" != "2.7" ]; then + pip uninstall python-language-server -q -y + fi fi # Install python-language-server from our subrepo -pushd external-deps/python-language-server -pip install --no-deps -q -e . -popd +if [ "$PYTHON_VERSION" != "2.7" ]; then + pushd external-deps/python-language-server + pip install --no-deps -q -e . + popd +fi # To check our manifest pip install check-manifest diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml index fdc10022d79..341ec8a8051 100644 --- a/.github/workflows/test-linux.yml +++ b/.github/workflows/test-linux.yml @@ -53,6 +53,9 @@ jobs: - INSTALL_TYPE: 'pip' PYTHON_VERSION: '2.7' TEST_TYPE: 'slow' + - INSTALL_TYPE: 'conda' + PYTHON_VERSION: '2.7' + TEST_TYPE: 'slow' steps: - name: Checkout Pull Requests if: github.event_name == 'pull_request' diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index cc0570a0829..c2acd4a9ac1 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -46,6 +46,10 @@ jobs: INSTALL_TYPE: ['conda'] PYTHON_VERSION: ['3.7', '2.7'] TEST_TYPE: ['fast', 'slow'] + exclude: + - INSTALL_TYPE: 'conda' + PYTHON_VERSION: '2.7' + TEST_TYPE: 'slow' steps: - name: Checkout Pull Requests if: github.event_name == 'pull_request' diff --git a/.pep8speaks.yml b/.pep8speaks.yml index 17e3d03e5eb..1fa77a15bf2 100644 --- a/.pep8speaks.yml +++ b/.pep8speaks.yml @@ -1,4 +1,6 @@ -# File : .pep8speaks.yml - scanner: diff_only: True # Errors caused by only the patch are shown + linter: pycodestyle + +pycodestyle: + exclude: external-deps diff --git a/binder/environment.yml b/binder/environment.yml index 0f65f6007b4..7c03cbf45c6 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -14,12 +14,12 @@ dependencies: - diff-match-patch >=20181111 - intervaltree - ipython >=4.0 -- jedi =0.15.2 +- jedi =0.17.0 - keyring - nbconvert >=4.0 - numpydoc >=0.6.0 - paramiko >=2.4.0 -- parso =0.5.2 +- parso =0.7.0 - pexpect >=4.4.0 - pickleshare >=0.4 - psutil >=5.3 diff --git a/external-deps/python-language-server/.circleci/config.yml b/external-deps/python-language-server/.circleci/config.yml index 7ec7b8a3fd2..4beef059a47 100644 --- a/external-deps/python-language-server/.circleci/config.yml +++ b/external-deps/python-language-server/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: python3-test: docker: - - image: "python:3.5-stretch" + - image: "python:3.6-stretch" steps: - checkout # To test Jedi environments @@ -35,7 +35,7 @@ jobs: publish: docker: - - image: "python:3.5-stretch" + - image: "python:3.6-stretch" steps: - checkout - run: ./scripts/circle/pypi.sh diff --git a/external-deps/python-language-server/.gitrepo b/external-deps/python-language-server/.gitrepo index c0079bf129d..fd7d1946dd4 100644 --- a/external-deps/python-language-server/.gitrepo +++ b/external-deps/python-language-server/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/palantir/python-language-server.git branch = develop - commit = fdb8b3dbc5df7d12729d135932bf2264e0883061 - parent = 0798ff6e51f6d1c353b8278469a32adab54b874f + commit = 9b5cb2197405b2290161deb2abd8e2b139a53691 + parent = dbb0398cd7e309e6de7a7740c57a03ecbb26ac11 method = merge cmdver = 0.4.1 diff --git a/external-deps/python-language-server/README.rst b/external-deps/python-language-server/README.rst index 0648a8ff2cd..166391cea89 100644 --- a/external-deps/python-language-server/README.rst +++ b/external-deps/python-language-server/README.rst @@ -72,6 +72,10 @@ To enable pydocstyle for linting docstrings add the following setting in your LS "pyls.plugins.pydocstyle.enabled": true ``` +See `vscode-client/package.json`_ for the full set of supported configuration options. + +.. _vscode-client/package.json: vscode-client/package.json + Language Server Features ------------------------ diff --git a/external-deps/python-language-server/appveyor.yml b/external-deps/python-language-server/appveyor.yml index 69e3e858696..4950b8aba0e 100644 --- a/external-deps/python-language-server/appveyor.yml +++ b/external-deps/python-language-server/appveyor.yml @@ -6,8 +6,8 @@ environment: PYTHON_VERSION: "2.7.15" PYTHON_ARCH: "64" - - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.7" + - PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.8" PYTHON_ARCH: "64" matrix: diff --git a/external-deps/python-language-server/pyls/_utils.py b/external-deps/python-language-server/pyls/_utils.py index 919bf1c5064..b1a3bd96c8d 100644 --- a/external-deps/python-language-server/pyls/_utils.py +++ b/external-deps/python-language-server/pyls/_utils.py @@ -1,5 +1,4 @@ # Copyright 2017 Palantir Technologies, Inc. -from distutils.version import LooseVersion import functools import inspect import logging @@ -140,18 +139,34 @@ def format_docstring(contents): """ contents = contents.replace('\t', u'\u00A0' * 4) contents = contents.replace(' ', u'\u00A0' * 2) - if LooseVersion(JEDI_VERSION) < LooseVersion('0.15.0'): - contents = contents.replace('*', '\\*') return contents def clip_column(column, lines, line_number): - # Normalise the position as per the LSP that accepts character positions > line length - # https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#position + """ + Normalise the position as per the LSP that accepts character positions > line length + + https://microsoft.github.io/language-server-protocol/specification#position + """ max_column = len(lines[line_number].rstrip('\r\n')) if len(lines) > line_number else 0 return min(column, max_column) +def position_to_jedi_linecolumn(document, position): + """ + Convert the LSP format 'line', 'character' to Jedi's 'line', 'column' + + https://microsoft.github.io/language-server-protocol/specification#position + """ + code_position = {} + if position: + code_position = {'line': position['line'] + 1, + 'column': clip_column(position['character'], + document.lines, + position['line'])} + return code_position + + if os.name == 'nt': import ctypes diff --git a/external-deps/python-language-server/pyls/config/pycodestyle_conf.py b/external-deps/python-language-server/pyls/config/pycodestyle_conf.py index c09375b040c..49d3d16f6f9 100644 --- a/external-deps/python-language-server/pyls/config/pycodestyle_conf.py +++ b/external-deps/python-language-server/pyls/config/pycodestyle_conf.py @@ -15,6 +15,7 @@ ('ignore', 'plugins.pycodestyle.ignore', list), ('max-line-length', 'plugins.pycodestyle.maxLineLength', int), ('select', 'plugins.pycodestyle.select', list), + ('aggressive', 'plugins.pycodestyle.aggressive', int), ] diff --git a/external-deps/python-language-server/pyls/lsp.py b/external-deps/python-language-server/pyls/lsp.py index 36a8d84228b..a3f88d38ca4 100644 --- a/external-deps/python-language-server/pyls/lsp.py +++ b/external-deps/python-language-server/pyls/lsp.py @@ -24,6 +24,13 @@ class CompletionItemKind(object): Color = 16 File = 17 Reference = 18 + Folder = 19 + EnumMember = 20 + Constant = 21 + Struct = 22 + Event = 23 + Operator = 24 + TypeParameter = 25 class DocumentHighlightKind(object): diff --git a/external-deps/python-language-server/pyls/plugins/autopep8_format.py b/external-deps/python-language-server/pyls/plugins/autopep8_format.py index 61260068481..1e4e4e1a44c 100644 --- a/external-deps/python-language-server/pyls/plugins/autopep8_format.py +++ b/external-deps/python-language-server/pyls/plugins/autopep8_format.py @@ -57,6 +57,7 @@ def _autopep8_config(config): 'ignore': settings.get('ignore'), 'max_line_length': settings.get('maxLineLength'), 'select': settings.get('select'), + 'aggressive': settings.get('aggressive'), } # Filter out null options diff --git a/external-deps/python-language-server/pyls/plugins/definition.py b/external-deps/python-language-server/pyls/plugins/definition.py index 8ec3b1adb01..d4c131798e0 100644 --- a/external-deps/python-language-server/pyls/plugins/definition.py +++ b/external-deps/python-language-server/pyls/plugins/definition.py @@ -1,6 +1,6 @@ # Copyright 2017 Palantir Technologies, Inc. import logging -from pyls import hookimpl, uris +from pyls import hookimpl, uris, _utils log = logging.getLogger(__name__) @@ -8,9 +8,11 @@ @hookimpl def pyls_definitions(config, document, position): settings = config.plugin_settings('jedi_definition') - definitions = document.jedi_script(position).goto_assignments( + code_position = _utils.position_to_jedi_linecolumn(document, position) + definitions = document.jedi_script().goto( follow_imports=settings.get('follow_imports', True), - follow_builtin_imports=settings.get('follow_builtin_imports', True)) + follow_builtin_imports=settings.get('follow_builtin_imports', True), + **code_position) return [ { diff --git a/external-deps/python-language-server/pyls/plugins/flake8_lint.py b/external-deps/python-language-server/pyls/plugins/flake8_lint.py index 69f3f40a685..9bd8ae26fef 100644 --- a/external-deps/python-language-server/pyls/plugins/flake8_lint.py +++ b/external-deps/python-language-server/pyls/plugins/flake8_lint.py @@ -58,11 +58,10 @@ def run_flake8(args): cmd = ['python', '-m', 'flake8'] cmd.extend(args) p = Popen(cmd, stdout=PIPE, stderr=PIPE) - stderr = p.stderr.read().decode() + (stdout, stderr) = p.communicate() if stderr: - log.error("Error while running flake8 '%s'", stderr) - stdout = p.stdout - return stdout.read().decode() + log.error("Error while running flake8 '%s'", stderr.decode()) + return stdout.decode() def build_args(options, doc_path): @@ -119,10 +118,16 @@ def parse_stdout(document, stdout): diagnostics = [] lines = stdout.splitlines() for raw_line in lines: - parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*) (.*)', raw_line).groups() - if not parsed_line or len(parsed_line) != 5: + parsed_line = re.match(r'(.*):(\d*):(\d*): (\w*) (.*)', raw_line) + if not parsed_line: log.debug("Flake8 output parser can't parse line '%s'", raw_line) continue + + parsed_line = parsed_line.groups() + if len(parsed_line) != 5: + log.debug("Flake8 output parser can't parse line '%s'", raw_line) + continue + _, line, character, code, msg = parsed_line line = int(line) - 1 character = int(character) - 1 diff --git a/external-deps/python-language-server/pyls/plugins/highlight.py b/external-deps/python-language-server/pyls/plugins/highlight.py index 839ffb265b6..4c4c195c16b 100644 --- a/external-deps/python-language-server/pyls/plugins/highlight.py +++ b/external-deps/python-language-server/pyls/plugins/highlight.py @@ -1,13 +1,14 @@ # Copyright 2017 Palantir Technologies, Inc. import logging -from pyls import hookimpl, lsp +from pyls import hookimpl, lsp, _utils log = logging.getLogger(__name__) @hookimpl def pyls_document_highlight(document, position): - usages = document.jedi_script(position).usages() + code_position = _utils.position_to_jedi_linecolumn(document, position) + usages = document.jedi_script().get_references(**code_position) def is_valid(definition): return definition.line is not None and definition.column is not None diff --git a/external-deps/python-language-server/pyls/plugins/hover.py b/external-deps/python-language-server/pyls/plugins/hover.py index a98c0ea04a0..9332a52dcf9 100644 --- a/external-deps/python-language-server/pyls/plugins/hover.py +++ b/external-deps/python-language-server/pyls/plugins/hover.py @@ -1,5 +1,5 @@ # Copyright 2017 Palantir Technologies, Inc. -from distutils.version import LooseVersion + import logging from pyls import hookimpl, _utils @@ -9,43 +9,40 @@ @hookimpl def pyls_hover(document, position): - definitions = document.jedi_script(position).goto_definitions() + code_position = _utils.position_to_jedi_linecolumn(document, position) + definitions = document.jedi_script().infer(**code_position) word = document.word_at_position(position) - if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'): - # Find first exact matching definition - definition = next((x for x in definitions if x.name == word), None) - - # Ensure a definition is used if only one is available - # even if the word doesn't match. An example of this case is 'np' - # where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs - if len(definitions) == 1: - definition = definitions[0] - - if not definition: - return {'contents': ''} - - # raw docstring returns only doc, without signature - doc = _utils.format_docstring(definition.docstring(raw=True)) - - # Find first exact matching signature - signature = next((x.to_string() for x in definition.get_signatures() if x.name == word), '') - - contents = [] - if signature: - contents.append({ - 'language': 'python', - 'value': signature, - }) - if doc: - contents.append(doc) - if not contents: - return {'contents': ''} - return {'contents': contents} - else: - # Find an exact match for a completion - for d in definitions: - if d.name == word: - return {'contents': _utils.format_docstring(d.docstring()) or ''} + # Find first exact matching definition + definition = next((x for x in definitions if x.name == word), None) + + # Ensure a definition is used if only one is available + # even if the word doesn't match. An example of this case is 'np' + # where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs + if len(definitions) == 1: + definition = definitions[0] + if not definition: return {'contents': ''} + + # raw docstring returns only doc, without signature + doc = _utils.format_docstring(definition.docstring(raw=True)) + + # Find first exact matching signature + signature = next((x.to_string() for x in definition.get_signatures() + if x.name == word), '') + + contents = [] + if signature: + contents.append({ + 'language': 'python', + 'value': signature, + }) + + if doc: + contents.append(doc) + + if not contents: + return {'contents': ''} + + return {'contents': contents} diff --git a/external-deps/python-language-server/pyls/plugins/jedi_completion.py b/external-deps/python-language-server/pyls/plugins/jedi_completion.py index caa543a1ff1..921df628d8b 100644 --- a/external-deps/python-language-server/pyls/plugins/jedi_completion.py +++ b/external-deps/python-language-server/pyls/plugins/jedi_completion.py @@ -1,8 +1,10 @@ # Copyright 2017 Palantir Technologies, Inc. import logging import os.path as osp + import parso -from pyls import hookimpl, lsp, _utils + +from pyls import _utils, hookimpl, lsp log = logging.getLogger(__name__) @@ -50,26 +52,39 @@ @hookimpl def pyls_completions(config, document, position): - try: - definitions = document.jedi_script(position).completions() - except AttributeError as e: - if 'CompiledObject' in str(e): - # Needed to handle missing CompiledObject attribute - # 'sub_modules_dict' - definitions = None - else: - raise e + """Get formatted completions for current code position""" + settings = config.plugin_settings('jedi_completion', document_path=document.path) + code_position = _utils.position_to_jedi_linecolumn(document, position) + + code_position["fuzzy"] = settings.get("fuzzy", False) + completions = document.jedi_script().complete(**code_position) - if not definitions: + if not completions: return None completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {}) snippet_support = completion_capabilities.get('completionItem', {}).get('snippetSupport') - settings = config.plugin_settings('jedi_completion', document_path=document.path) should_include_params = settings.get('include_params') + should_include_class_objects = settings.get('include_class_objects', True) + include_params = snippet_support and should_include_params and use_snippets(document, position) - return [_format_completion(d, include_params) for d in definitions] or None + include_class_objects = snippet_support and should_include_class_objects and use_snippets(document, position) + + ready_completions = [ + _format_completion(c, include_params) + for c in completions + ] + + if include_class_objects: + for c in completions: + if c.type == 'class': + completion_dict = _format_completion(c, False) + completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter + completion_dict['label'] += ' object' + ready_completions.append(completion_dict) + + return ready_completions or None def is_exception_class(name): @@ -138,9 +153,9 @@ def _format_completion(d, include_params=True): path = path.replace('/', '\\/') completion['insertText'] = path - if (include_params and hasattr(d, 'params') and d.params and - not is_exception_class(d.name)): - positional_args = [param for param in d.params + sig = d.get_signatures() + if (include_params and sig and not is_exception_class(d.name)): + positional_args = [param for param in sig[0].params if '=' not in param.description and param.name not in {'/', '*'}] @@ -155,6 +170,7 @@ def _format_completion(d, include_params=True): snippet += ')$0' completion['insertText'] = snippet elif len(positional_args) == 1: + completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet completion['insertText'] = d.name + '($0)' else: completion['insertText'] = d.name + '()' @@ -163,8 +179,9 @@ def _format_completion(d, include_params=True): def _label(definition): - if definition.type in ('function', 'method') and hasattr(definition, 'params'): - params = ', '.join([param.name for param in definition.params]) + sig = definition.get_signatures() + if definition.type in ('function', 'method') and sig: + params = ', '.join([param.name for param in sig[0].params]) return '{}({})'.format(definition.name, params) return definition.name diff --git a/external-deps/python-language-server/pyls/plugins/jedi_rename.py b/external-deps/python-language-server/pyls/plugins/jedi_rename.py new file mode 100644 index 00000000000..2e633d71a29 --- /dev/null +++ b/external-deps/python-language-server/pyls/plugins/jedi_rename.py @@ -0,0 +1,48 @@ +# Copyright 2020 Palantir Technologies, Inc. +import logging + +from pyls import hookimpl, uris, _utils + +log = logging.getLogger(__name__) + + +@hookimpl +def pyls_rename(config, workspace, document, position, new_name): # pylint: disable=unused-argument + log.debug('Executing rename of %s to %s', document.word_at_position(position), new_name) + kwargs = _utils.position_to_jedi_linecolumn(document, position) + kwargs['new_name'] = new_name + try: + refactoring = document.jedi_script().rename(**kwargs) + except NotImplementedError: + raise Exception('No support for renaming in Python 2/3.5 with Jedi. ' + 'Consider using the rope_rename plugin instead') + log.debug('Finished rename: %s', refactoring.get_diff()) + + return { + 'documentChanges': [ + { + 'textDocument': { + 'uri': uris.uri_with(document.uri, path=file_path), + 'version': workspace.get_document(document.uri).version, + }, + 'edits': [ + { + 'range': { + 'start': {'line': 0, 'character': 0}, + 'end': { + 'line': _num_lines(changed_file.get_new_code()), + 'character': 0, + }, + }, + 'newText': changed_file.get_new_code(), + } + ], + } + for file_path, changed_file in refactoring.get_changed_files().items() + ], + } + + +def _num_lines(file_contents): + 'Count the number of lines in the given string.' + return len(file_contents.splitlines()) diff --git a/external-deps/python-language-server/pyls/plugins/pycodestyle_lint.py b/external-deps/python-language-server/pyls/plugins/pycodestyle_lint.py index c12f9f48f11..490b65d31ee 100644 --- a/external-deps/python-language-server/pyls/plugins/pycodestyle_lint.py +++ b/external-deps/python-language-server/pyls/plugins/pycodestyle_lint.py @@ -1,15 +1,19 @@ # Copyright 2017 Palantir Technologies, Inc. import logging import pycodestyle -from autopep8 import continued_indentation as autopep8_c_i from pyls import hookimpl, lsp -# Check if autopep8's continued_indentation implementation -# is overriding pycodestyle's and if so, re-register -# the check using pycodestyle's implementation as expected -if autopep8_c_i in pycodestyle._checks['logical_line']: - del pycodestyle._checks['logical_line'][autopep8_c_i] - pycodestyle.register_check(pycodestyle.continued_indentation) +try: + from autopep8 import continued_indentation as autopep8_c_i +except ImportError: + pass +else: + # Check if autopep8's continued_indentation implementation + # is overriding pycodestyle's and if so, re-register + # the check using pycodestyle's implementation as expected + if autopep8_c_i in pycodestyle._checks['logical_line']: + del pycodestyle._checks['logical_line'][autopep8_c_i] + pycodestyle.register_check(pycodestyle.continued_indentation) log = logging.getLogger(__name__) diff --git a/external-deps/python-language-server/pyls/plugins/references.py b/external-deps/python-language-server/pyls/plugins/references.py index 120cde41b48..4bd47c96d59 100644 --- a/external-deps/python-language-server/pyls/plugins/references.py +++ b/external-deps/python-language-server/pyls/plugins/references.py @@ -1,14 +1,14 @@ # Copyright 2017 Palantir Technologies, Inc. import logging -from pyls import hookimpl, uris +from pyls import hookimpl, uris, _utils log = logging.getLogger(__name__) @hookimpl def pyls_references(document, position, exclude_declaration=False): - # Note that usages is not that great in a lot of cases: https://github.com/davidhalter/jedi/issues/744 - usages = document.jedi_script(position).usages() + code_position = _utils.position_to_jedi_linecolumn(document, position) + usages = document.jedi_script().get_references(**code_position) if exclude_declaration: # Filter out if the usage is the actual declaration of the thing diff --git a/external-deps/python-language-server/pyls/plugins/rope_rename.py b/external-deps/python-language-server/pyls/plugins/rope_rename.py index 3dec3153556..80091c0df39 100644 --- a/external-deps/python-language-server/pyls/plugins/rope_rename.py +++ b/external-deps/python-language-server/pyls/plugins/rope_rename.py @@ -10,6 +10,12 @@ log = logging.getLogger(__name__) +@hookimpl +def pyls_settings(): + # Default rope_rename to disabled + return {'plugins': {'rope_rename': {'enabled': False}}} + + @hookimpl def pyls_rename(config, workspace, document, position, new_name): rope_config = config.settings(document_path=document.path).get('rope', {}) diff --git a/external-deps/python-language-server/pyls/plugins/signature.py b/external-deps/python-language-server/pyls/plugins/signature.py index 6c509272702..fff7a5760ac 100644 --- a/external-deps/python-language-server/pyls/plugins/signature.py +++ b/external-deps/python-language-server/pyls/plugins/signature.py @@ -14,7 +14,8 @@ @hookimpl def pyls_signature_help(document, position): - signatures = document.jedi_script(position).call_signatures() + code_position = _utils.position_to_jedi_linecolumn(document, position) + signatures = document.jedi_script().get_signatures(**code_position) if not signatures: return {'signatures': []} diff --git a/external-deps/python-language-server/pyls/python_ls.py b/external-deps/python-language-server/pyls/python_ls.py index 577675f7441..8d6c5f0d7f9 100644 --- a/external-deps/python-language-server/pyls/python_ls.py +++ b/external-deps/python-language-server/pyls/python_ls.py @@ -361,14 +361,37 @@ def m_workspace__did_change_configuration(self, settings=None): for doc_uri in workspace.documents: self.lint(doc_uri, is_saved=False) - def m_workspace__did_change_workspace_folders(self, added=None, removed=None, **_kwargs): + def m_workspace__did_change_workspace_folders(self, event=None, **_kwargs): # pylint: disable=too-many-locals + if event is None: + return + added = event.get('added', []) + removed = event.get('removed', []) + for removed_info in removed: - removed_uri = removed_info['uri'] - self.workspaces.pop(removed_uri) + if 'uri' in removed_info: + removed_uri = removed_info['uri'] + self.workspaces.pop(removed_uri, None) for added_info in added: - added_uri = added_info['uri'] - self.workspaces[added_uri] = Workspace(added_uri, self._endpoint, self.config) + if 'uri' in added_info: + added_uri = added_info['uri'] + self.workspaces[added_uri] = Workspace(added_uri, self._endpoint, self.config) + + root_workspace_removed = any(removed_info['uri'] == self.root_uri for removed_info in removed) + workspace_added = len(added) > 0 and 'uri' in added[0] + if root_workspace_removed and workspace_added: + added_uri = added[0]['uri'] + self.root_uri = added_uri + self.workspace = self.workspaces[added_uri] + elif root_workspace_removed: + # NOTE: Removing the root workspace can only happen when the server + # is closed, thus the else condition of this if can never happen. + if self.workspaces: + log.debug('Root workspace deleted!') + available_workspaces = sorted(self.workspaces) + first_workspace = available_workspaces[0] + self.root_uri = first_workspace + self.workspace = self.workspaces[first_workspace] # Migrate documents that are on the root workspace and have a better # match now diff --git a/external-deps/python-language-server/pyls/workspace.py b/external-deps/python-language-server/pyls/workspace.py index a58b76a21a3..d89fe08b44e 100644 --- a/external-deps/python-language-server/pyls/workspace.py +++ b/external-deps/python-language-server/pyls/workspace.py @@ -103,17 +103,17 @@ def source_roots(self, document_path): def _create_document(self, doc_uri, source=None, version=None): path = uris.to_fs_path(doc_uri) return Document( - doc_uri, source=source, version=version, + doc_uri, self, source=source, version=version, extra_sys_path=self.source_roots(path), rope_project_builder=self._rope_project_builder, - config=self._config, workspace=self, + config=self._config, ) class Document(object): - def __init__(self, uri, source=None, version=None, local=True, extra_sys_path=None, rope_project_builder=None, - config=None, workspace=None): + def __init__(self, uri, workspace, source=None, version=None, local=True, extra_sys_path=None, + rope_project_builder=None, config=None): self.uri = uri self.version = version self.path = uris.to_fs_path(uri) @@ -213,16 +213,9 @@ def word_at_position(self, position): return m_start[0] + m_end[-1] def jedi_names(self, all_scopes=False, definitions=True, references=False): - environment_path = None - if self._config: - jedi_settings = self._config.plugin_settings('jedi', document_path=self.path) - environment_path = jedi_settings.get('environment') - environment = self.get_enviroment(environment_path) if environment_path else None - - return jedi.api.names( - source=self.source, path=self.path, all_scopes=all_scopes, - definitions=definitions, references=references, environment=environment, - ) + script = self.jedi_script() + return script.get_names(all_scopes=all_scopes, definitions=definitions, + references=references) def jedi_script(self, position=None): extra_paths = [] @@ -233,19 +226,20 @@ def jedi_script(self, position=None): environment_path = jedi_settings.get('environment') extra_paths = jedi_settings.get('extra_paths') or [] - sys_path = self.sys_path(environment_path) + extra_paths environment = self.get_enviroment(environment_path) if environment_path else None + sys_path = self.sys_path(environment_path) + extra_paths + project_path = self._workspace.root_path kwargs = { - 'source': self.source, + 'code': self.source, 'path': self.path, - 'sys_path': sys_path, 'environment': environment, + 'project': jedi.Project(path=project_path, sys_path=sys_path), } if position: - kwargs['line'] = position['line'] + 1 - kwargs['column'] = _utils.clip_column(position['character'], self.lines, position['line']) + # Deprecated by Jedi to use in Script() constructor + kwargs += _utils.position_to_jedi_linecolumn(self, position) return jedi.Script(**kwargs) diff --git a/external-deps/python-language-server/setup.py b/external-deps/python-language-server/setup.py index f1ddef5e341..f3c465db93f 100755 --- a/external-deps/python-language-server/setup.py +++ b/external-deps/python-language-server/setup.py @@ -35,7 +35,7 @@ 'configparser; python_version<"3.0"', 'future>=0.14.0; python_version<"3"', 'backports.functools_lru_cache; python_version<"3.2"', - 'jedi>=0.14.1,<0.16', + 'jedi>=0.17.0,<0.18.0', 'python-jsonrpc-server>=0.3.2', 'pluggy', 'ujson<=1.35; platform_system!="Windows"' @@ -48,21 +48,21 @@ extras_require={ 'all': [ 'autopep8', - 'flake8', - 'mccabe', - 'pycodestyle', + 'flake8>=3.8.0', + 'mccabe>=0.6.0,<0.7.0', + 'pycodestyle>=2.6.0,<2.7.0', 'pydocstyle>=2.0.0', - 'pyflakes>=1.6.0,<2.2.0', + 'pyflakes>=2.2.0,<2.3.0', 'pylint', 'rope>=0.10.5', 'yapf', ], 'autopep8': ['autopep8'], - 'flake8': ['flake8'], - 'mccabe': ['mccabe'], - 'pycodestyle': ['pycodestyle'], + 'flake8': ['flake8>=3.8.0'], + 'mccabe': ['mccabe>=0.6.0,<0.7.0'], + 'pycodestyle': ['pycodestyle>=2.6.0,<2.7.0'], 'pydocstyle': ['pydocstyle>=2.0.0'], - 'pyflakes': ['pyflakes>=1.6.0,<2.2.0'], + 'pyflakes': ['pyflakes>=2.2.0,<2.3.0'], 'pylint': ['pylint'], 'rope': ['rope>0.10.5'], 'yapf': ['yapf'], @@ -87,6 +87,7 @@ 'jedi_hover = pyls.plugins.hover', 'jedi_highlight = pyls.plugins.highlight', 'jedi_references = pyls.plugins.references', + 'jedi_rename = pyls.plugins.jedi_rename', 'jedi_signature_help = pyls.plugins.signature', 'jedi_symbols = pyls.plugins.symbols', 'mccabe = pyls.plugins.mccabe_lint', diff --git a/external-deps/python-language-server/test/fixtures.py b/external-deps/python-language-server/test/fixtures.py index 4e915e17862..490938a64de 100644 --- a/external-deps/python-language-server/test/fixtures.py +++ b/external-deps/python-language-server/test/fixtures.py @@ -1,4 +1,5 @@ # Copyright 2017 Palantir Technologies, Inc. +import os import sys from mock import Mock import pytest @@ -48,5 +49,25 @@ def config(workspace): # pylint: disable=redefined-outer-name @pytest.fixture -def doc(): - return Document(DOC_URI, DOC) +def doc(workspace): # pylint: disable=redefined-outer-name + return Document(DOC_URI, workspace, DOC) + + +@pytest.fixture +def temp_workspace_factory(workspace): # pylint: disable=redefined-outer-name + ''' + Returns a function that creates a temporary workspace from the files dict. + The dict is in the format {"file_name": "file_contents"} + ''' + def fn(files): + def create_file(name, content): + fn = os.path.join(workspace.root_path, name) + with open(fn, 'w') as f: + f.write(content) + workspace.put_document(uris.from_fs_path(fn), content) + + for name, content in files.items(): + create_file(name, content) + return workspace + + return fn diff --git a/external-deps/python-language-server/test/plugins/test_autopep8_format.py b/external-deps/python-language-server/test/plugins/test_autopep8_format.py index 09c769aef1b..8138952d4e2 100644 --- a/external-deps/python-language-server/test/plugins/test_autopep8_format.py +++ b/external-deps/python-language-server/test/plugins/test_autopep8_format.py @@ -16,16 +16,16 @@ def func(): GOOD_DOC = """A = ['hello', 'world']\n""" -def test_format(config): - doc = Document(DOC_URI, DOC) +def test_format(config, workspace): + doc = Document(DOC_URI, workspace, DOC) res = pyls_format_document(config, doc) assert len(res) == 1 assert res[0]['newText'] == "a = 123\n\n\ndef func():\n pass\n" -def test_range_format(config): - doc = Document(DOC_URI, DOC) +def test_range_format(config, workspace): + doc = Document(DOC_URI, workspace, DOC) def_range = { 'start': {'line': 0, 'character': 0}, @@ -39,6 +39,6 @@ def test_range_format(config): assert res[0]['newText'] == "a = 123\n\n\n\n\ndef func():\n pass\n" -def test_no_change(config): - doc = Document(DOC_URI, GOOD_DOC) +def test_no_change(config, workspace): + doc = Document(DOC_URI, workspace, GOOD_DOC) assert not pyls_format_document(config, doc) diff --git a/external-deps/python-language-server/test/plugins/test_completion.py b/external-deps/python-language-server/test/plugins/test_completion.py index 57caa8b3059..2b8847fcfa8 100644 --- a/external-deps/python-language-server/test/plugins/test_completion.py +++ b/external-deps/python-language-server/test/plugins/test_completion.py @@ -1,5 +1,4 @@ # Copyright 2017 Palantir Technologies, Inc. -from distutils.version import LooseVersion import os import sys @@ -7,7 +6,6 @@ import pytest from pyls import uris, lsp -from pyls._utils import JEDI_VERSION from pyls.workspace import Document from pyls.plugins.jedi_completion import pyls_completions as pyls_jedi_completions from pyls.plugins.rope_completion import pyls_completions as pyls_rope_completions @@ -46,19 +44,35 @@ def everyone(self, a, b, c=None, d=2): def test_rope_import_completion(config, workspace): com_position = {'line': 0, 'character': 7} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) items = pyls_rope_completions(config, workspace, doc, com_position) assert items is None -def test_jedi_completion(config): +def test_jedi_completion(config, workspace): # Over 'i' in os.path.isabs(...) com_position = {'line': 1, 'character': 15} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) items = pyls_jedi_completions(config, doc, com_position) assert items - assert items[0]['label'] == 'isabs(path)' + labels = [i['label'] for i in items] + assert 'isabs(path)' in labels + + # Test we don't throw with big character + pyls_jedi_completions(config, doc, {'line': 1, 'character': 1000}) + + +def test_jedi_completion_with_fuzzy_enabled(config, workspace): + # Over 'i' in os.path.isabs(...) + config.update({'plugins': {'jedi_completion': {'fuzzy': True}}}) + com_position = {'line': 1, 'character': 15} + doc = Document(DOC_URI, workspace, DOC) + + items = pyls_jedi_completions(config, doc, com_position) + + assert items + assert items[0]['label'] == 'commonprefix(list)' # Test we don't throw with big character pyls_jedi_completions(config, doc, {'line': 1, 'character': 1000}) @@ -75,10 +89,10 @@ def test_rope_completion(config, workspace): assert items[0]['label'] == 'isabs' -def test_jedi_completion_ordering(config): +def test_jedi_completion_ordering(config, workspace): # Over the blank line com_position = {'line': 8, 'character': 0} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) completions = pyls_jedi_completions(config, doc, com_position) items = {c['label']: c['sortText'] for c in completions} @@ -87,10 +101,10 @@ def test_jedi_completion_ordering(config): assert items['hello()'] < items['_a_hello()'] -def test_jedi_property_completion(config): +def test_jedi_property_completion(config, workspace): # Over the 'w' in 'print Hello().world' com_position = {'line': 18, 'character': 15} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) completions = pyls_jedi_completions(config, doc, com_position) items = {c['label']: c['sortText'] for c in completions} @@ -99,10 +113,10 @@ def test_jedi_property_completion(config): assert 'world' in list(items.keys())[0] -def test_jedi_method_completion(config): +def test_jedi_method_completion(config, workspace): # Over the 'y' in 'print Hello().every' com_position = {'line': 20, 'character': 19} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) config.capabilities['textDocument'] = {'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) @@ -126,61 +140,50 @@ def test_jedi_method_completion(config): @pytest.mark.skipif(PY2 or (sys.platform.startswith('linux') and os.environ.get('CI') is not None), reason="Test in Python 3 and not on CIs on Linux because wheels don't work on them.") -def test_pyqt_completion(config): +def test_pyqt_completion(config, workspace): # Over 'QA' in 'from PyQt5.QtWidgets import QApplication' doc_pyqt = "from PyQt5.QtWidgets import QA" com_position = {'line': 0, 'character': len(doc_pyqt)} - doc = Document(DOC_URI, doc_pyqt) + doc = Document(DOC_URI, workspace, doc_pyqt) completions = pyls_jedi_completions(config, doc, com_position) - # Test we don't throw an error for Jedi < 0.15.2 and get completions - # for Jedi 0.15.2+ - if LooseVersion(JEDI_VERSION) < LooseVersion('0.15.2'): - assert completions is None - else: - assert completions is not None + assert completions is not None -@pytest.mark.skipif(LooseVersion('0.15.0') <= LooseVersion(JEDI_VERSION) < LooseVersion('0.15.2'), - reason='This test fails with Jedi 0.15.0 and 0.15.1') -def test_numpy_completions(config): +def test_numpy_completions(config, workspace): doc_numpy = "import numpy as np; np." com_position = {'line': 0, 'character': len(doc_numpy)} - doc = Document(DOC_URI, doc_numpy) + doc = Document(DOC_URI, workspace, doc_numpy) items = pyls_jedi_completions(config, doc, com_position) assert items assert any(['array' in i['label'] for i in items]) -@pytest.mark.skipif(LooseVersion('0.15.0') <= LooseVersion(JEDI_VERSION) < LooseVersion('0.15.2'), - reason='This test fails with Jedi 0.15.0 and 0.15.1') -def test_pandas_completions(config): +def test_pandas_completions(config, workspace): doc_pandas = "import pandas as pd; pd." com_position = {'line': 0, 'character': len(doc_pandas)} - doc = Document(DOC_URI, doc_pandas) + doc = Document(DOC_URI, workspace, doc_pandas) items = pyls_jedi_completions(config, doc, com_position) assert items assert any(['DataFrame' in i['label'] for i in items]) -def test_matplotlib_completions(config): +def test_matplotlib_completions(config, workspace): doc_mpl = "import matplotlib.pyplot as plt; plt." com_position = {'line': 0, 'character': len(doc_mpl)} - doc = Document(DOC_URI, doc_mpl) + doc = Document(DOC_URI, workspace, doc_mpl) items = pyls_jedi_completions(config, doc, com_position) assert items assert any(['plot' in i['label'] for i in items]) -@pytest.mark.skipif(LooseVersion(JEDI_VERSION) < LooseVersion('0.15.2'), - reason='This test fails with Jedi 0.15.1 or less') -def test_snippets_completion(config): +def test_snippets_completion(config, workspace): doc_snippets = 'from collections import defaultdict \na=defaultdict' com_position = {'line': 0, 'character': 35} - doc = Document(DOC_URI, doc_snippets) + doc = Document(DOC_URI, workspace, doc_snippets) config.capabilities['textDocument'] = { 'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) @@ -190,12 +193,33 @@ def test_snippets_completion(config): com_position = {'line': 1, 'character': len(doc_snippets)} completions = pyls_jedi_completions(config, doc, com_position) assert completions[0]['insertText'] == 'defaultdict($0)' + assert completions[0]['insertTextFormat'] == lsp.InsertTextFormat.Snippet + + +def test_completion_with_class_objects(config, workspace): + doc_text = 'class FOOBAR(Object): pass\nFOOB' + com_position = {'line': 1, 'character': 4} + doc = Document(DOC_URI, workspace, doc_text) + config.capabilities['textDocument'] = { + 'completion': {'completionItem': {'snippetSupport': True}}} + config.update({'plugins': {'jedi_completion': { + 'include_params': True, + 'include_class_objects': True, + }}}) + completions = pyls_jedi_completions(config, doc, com_position) + assert len(completions) == 2 + + assert completions[0]['label'] == 'FOOBAR' + assert completions[0]['kind'] == lsp.CompletionItemKind.Class + + assert completions[1]['label'] == 'FOOBAR object' + assert completions[1]['kind'] == lsp.CompletionItemKind.TypeParameter -def test_snippet_parsing(config): +def test_snippet_parsing(config, workspace): doc = 'import numpy as np\nnp.logical_and' completion_position = {'line': 1, 'character': 14} - doc = Document(DOC_URI, doc) + doc = Document(DOC_URI, workspace, doc) config.capabilities['textDocument'] = { 'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) @@ -204,9 +228,9 @@ def test_snippet_parsing(config): assert completions[0]['insertText'] == out -def test_multiline_import_snippets(config): +def test_multiline_import_snippets(config, workspace): document = 'from datetime import(\n date,\n datetime)\na=date' - doc = Document(DOC_URI, document) + doc = Document(DOC_URI, workspace, document) config.capabilities['textDocument'] = { 'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) @@ -220,9 +244,9 @@ def test_multiline_import_snippets(config): assert completions[0]['insertText'] == 'datetime' -def test_multiline_snippets(config): +def test_multiline_snippets(config, workspace): document = 'from datetime import\\\n date,\\\n datetime \na=date' - doc = Document(DOC_URI, document) + doc = Document(DOC_URI, workspace, document) config.capabilities['textDocument'] = { 'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) @@ -236,25 +260,25 @@ def test_multiline_snippets(config): assert completions[0]['insertText'] == 'datetime' -def test_multistatement_snippet(config): +def test_multistatement_snippet(config, workspace): config.capabilities['textDocument'] = { 'completion': {'completionItem': {'snippetSupport': True}}} config.update({'plugins': {'jedi_completion': {'include_params': True}}}) document = 'a = 1; from datetime import date' - doc = Document(DOC_URI, document) + doc = Document(DOC_URI, workspace, document) position = {'line': 0, 'character': len(document)} completions = pyls_jedi_completions(config, doc, position) assert completions[0]['insertText'] == 'date' document = 'from datetime import date; a = date' - doc = Document(DOC_URI, document) + doc = Document(DOC_URI, workspace, document) position = {'line': 0, 'character': len(document)} completions = pyls_jedi_completions(config, doc, position) assert completions[0]['insertText'] == 'date(${1:year}, ${2:month}, ${3:day})$0' -def test_jedi_completion_extra_paths(config, tmpdir): +def test_jedi_completion_extra_paths(config, tmpdir, workspace): # Create a tempfile with some content and pass to extra_paths temp_doc_content = ''' def spam(): @@ -268,7 +292,7 @@ def spam(): # Content of doc to test completion doc_content = """import foo foo.s""" - doc = Document(DOC_URI, doc_content) + doc = Document(DOC_URI, workspace, doc_content) # After 'foo.s' without extra paths com_position = {'line': 1, 'character': 5} @@ -290,7 +314,7 @@ def test_jedi_completion_environment(config): # Content of doc to test completion doc_content = '''import logh ''' - doc = Document(DOC_URI, doc_content, workspace=MockWorkspace()) + doc = Document(DOC_URI, MockWorkspace(), doc_content) # After 'import logh' with default environment com_position = {'line': 0, 'character': 11} diff --git a/external-deps/python-language-server/test/plugins/test_definitions.py b/external-deps/python-language-server/test/plugins/test_definitions.py index e2db9c6fd53..660741f6075 100644 --- a/external-deps/python-language-server/test/plugins/test_definitions.py +++ b/external-deps/python-language-server/test/plugins/test_definitions.py @@ -20,7 +20,7 @@ def add_member(self, id, name): """ -def test_definitions(config): +def test_definitions(config, workspace): # Over 'a' in print a cursor_pos = {'line': 3, 'character': 6} @@ -30,20 +30,20 @@ def test_definitions(config): 'end': {'line': 0, 'character': 5} } - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) assert [{'uri': DOC_URI, 'range': def_range}] == pyls_definitions(config, doc, cursor_pos) -def test_builtin_definition(config): +def test_builtin_definition(config, workspace): # Over 'i' in dict cursor_pos = {'line': 8, 'character': 24} # No go-to def for builtins - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) assert not pyls_definitions(config, doc, cursor_pos) -def test_assignment(config): +def test_assignment(config, workspace): # Over 's' in self.members[id] cursor_pos = {'line': 11, 'character': 19} @@ -53,5 +53,5 @@ def test_assignment(config): 'end': {'line': 8, 'character': 20} } - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) assert [{'uri': DOC_URI, 'range': def_range}] == pyls_definitions(config, doc, cursor_pos) diff --git a/external-deps/python-language-server/test/plugins/test_flake8_lint.py b/external-deps/python-language-server/test/plugins/test_flake8_lint.py index d4858f8e352..78f486e87d2 100644 --- a/external-deps/python-language-server/test/plugins/test_flake8_lint.py +++ b/external-deps/python-language-server/test/plugins/test_flake8_lint.py @@ -1,12 +1,13 @@ # Copyright 2019 Palantir Technologies, Inc. import tempfile import os +from test.test_utils import MockWorkspace from mock import patch - from pyls import lsp, uris from pyls.plugins import flake8_lint from pyls.workspace import Document + DOC_URI = uris.from_fs_path(__file__) DOC = """import pyls @@ -23,18 +24,18 @@ def temp_document(doc_text): name = temp_file.name temp_file.write(doc_text) temp_file.close() - doc = Document(uris.from_fs_path(name)) + doc = Document(uris.from_fs_path(name), MockWorkspace()) return name, doc -def test_flake8_no_checked_file(config): +def test_flake8_no_checked_file(config, workspace): # A bad uri or a non-saved file may cause the flake8 linter to do nothing. # In this situtation, the linter will return an empty list. - doc = Document('', DOC) + doc = Document('', workspace, DOC) diags = flake8_lint.pyls_lint(config, doc) - assert diags == [] + assert 'Error' in diags[0]['message'] def test_flake8_lint(config): @@ -56,6 +57,8 @@ def test_flake8_lint(config): def test_flake8_config_param(config): with patch('pyls.plugins.flake8_lint.Popen') as popen_mock: + mock_instance = popen_mock.return_value + mock_instance.communicate.return_value = [bytes(), bytes()] flake8_conf = '/tmp/some.cfg' config.update({'plugins': {'flake8': {'config': flake8_conf}}}) _name, doc = temp_document(DOC) diff --git a/external-deps/python-language-server/test/plugins/test_folding.py b/external-deps/python-language-server/test/plugins/test_folding.py index ec6dd316c39..05f0cdd8c00 100644 --- a/external-deps/python-language-server/test/plugins/test_folding.py +++ b/external-deps/python-language-server/test/plugins/test_folding.py @@ -111,8 +111,8 @@ class A(: """) -def test_folding(): - doc = Document(DOC_URI, DOC) +def test_folding(workspace): + doc = Document(DOC_URI, workspace, DOC) ranges = pyls_folding_range(doc) expected = [{'startLine': 1, 'endLine': 6}, {'startLine': 2, 'endLine': 3}, @@ -149,8 +149,8 @@ def test_folding(): assert ranges == expected -def test_folding_syntax_error(): - doc = Document(DOC_URI, SYNTAX_ERR) +def test_folding_syntax_error(workspace): + doc = Document(DOC_URI, workspace, SYNTAX_ERR) ranges = pyls_folding_range(doc) expected = [{'startLine': 1, 'endLine': 6}, {'startLine': 2, 'endLine': 3}, diff --git a/external-deps/python-language-server/test/plugins/test_highlight.py b/external-deps/python-language-server/test/plugins/test_highlight.py index 41e9075be02..40bf52f2b69 100644 --- a/external-deps/python-language-server/test/plugins/test_highlight.py +++ b/external-deps/python-language-server/test/plugins/test_highlight.py @@ -10,11 +10,11 @@ """ -def test_highlight(): +def test_highlight(workspace): # Over 'a' in a.startswith cursor_pos = {'line': 1, 'character': 0} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) assert pyls_document_highlight(doc, cursor_pos) == [{ 'range': { 'start': {'line': 0, 'character': 0}, @@ -37,10 +37,10 @@ def test_highlight(): ''' -def test_sys_highlight(): +def test_sys_highlight(workspace): cursor_pos = {'line': 0, 'character': 8} - doc = Document(DOC_URI, SYS_DOC) + doc = Document(DOC_URI, workspace, SYS_DOC) assert pyls_document_highlight(doc, cursor_pos) == [{ 'range': { 'start': {'line': 0, 'character': 7}, diff --git a/external-deps/python-language-server/test/plugins/test_hover.py b/external-deps/python-language-server/test/plugins/test_hover.py index 4ae29cd905e..2302b865b46 100644 --- a/external-deps/python-language-server/test/plugins/test_hover.py +++ b/external-deps/python-language-server/test/plugins/test_hover.py @@ -1,7 +1,6 @@ # Copyright 2017 Palantir Technologies, Inc. -from distutils.version import LooseVersion -from pyls import uris, _utils +from pyls import uris from pyls.plugins.hover import pyls_hover from pyls.workspace import Document @@ -21,7 +20,7 @@ def main(): """ -def test_numpy_hover(): +def test_numpy_hover(workspace): # Over the blank line no_hov_position = {'line': 1, 'character': 0} # Over 'numpy' in import numpy as np @@ -33,38 +32,34 @@ def test_numpy_hover(): # Over 'sin' in np.sin numpy_sin_hov_position = {'line': 3, 'character': 4} - doc = Document(DOC_URI, NUMPY_DOC) + doc = Document(DOC_URI, workspace, NUMPY_DOC) - if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'): - contents = '' - assert contents in pyls_hover(doc, no_hov_position)['contents'] + contents = '' + assert contents in pyls_hover(doc, no_hov_position)['contents'] - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pyls_hover(doc, numpy_hov_position_1)['contents'][0] + contents = 'NumPy\n=====\n\nProvides\n' + assert contents in pyls_hover(doc, numpy_hov_position_1)['contents'][0] - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pyls_hover(doc, numpy_hov_position_2)['contents'][0] + contents = 'NumPy\n=====\n\nProvides\n' + assert contents in pyls_hover(doc, numpy_hov_position_2)['contents'][0] - contents = 'NumPy\n=====\n\nProvides\n' - assert contents in pyls_hover(doc, numpy_hov_position_3)['contents'][0] + contents = 'NumPy\n=====\n\nProvides\n' + assert contents in pyls_hover(doc, numpy_hov_position_3)['contents'][0] - contents = 'Trigonometric sine, element-wise.\n\n' - assert contents in pyls_hover( - doc, numpy_sin_hov_position)['contents'][0] + contents = 'Trigonometric sine, element-wise.\n\n' + assert contents in pyls_hover( + doc, numpy_sin_hov_position)['contents'][0] -def test_hover(): +def test_hover(workspace): # Over 'main' in def main(): hov_position = {'line': 2, 'character': 6} # Over the blank second line no_hov_position = {'line': 1, 'character': 0} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) - if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'): - contents = [{'language': 'python', 'value': 'main()'}, 'hello world'] - else: - contents = 'main()\n\nhello world' + contents = [{'language': 'python', 'value': 'main()'}, 'hello world'] assert { 'contents': contents diff --git a/external-deps/python-language-server/test/plugins/test_jedi_rename.py b/external-deps/python-language-server/test/plugins/test_jedi_rename.py new file mode 100644 index 00000000000..1d82d9542ca --- /dev/null +++ b/external-deps/python-language-server/test/plugins/test_jedi_rename.py @@ -0,0 +1,48 @@ +# Copyright 2020 Palantir Technologies, Inc. +import os +import sys + +import pytest +from pyls import uris +from pyls.plugins.jedi_rename import pyls_rename +from pyls.workspace import Document + +LT_PY36 = sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 6) + +DOC_NAME = 'test1.py' +DOC = '''class Test1(): + pass + +class Test2(Test1): + pass +''' + + +@pytest.fixture +def tmp_workspace(temp_workspace_factory): + return temp_workspace_factory({DOC_NAME: DOC}) + + +@pytest.mark.skipif(LT_PY36, reason='Jedi refactoring isnt supported on Python 2.x/3.5') +def test_jedi_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name + # rename the `Test1` class + position = {'line': 0, 'character': 6} + DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME)) + doc = Document(DOC_URI, tmp_workspace) + + result = pyls_rename(config, tmp_workspace, doc, position, 'ShouldBeRenamed') + assert len(result.keys()) == 1 + + changes = result.get('documentChanges') + assert len(changes) == 1 + changes = changes[0] + + assert changes.get('edits') == [ + { + 'range': { + 'start': {'line': 0, 'character': 0}, + 'end': {'line': 5, 'character': 0}, + }, + 'newText': 'class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n', + } + ] diff --git a/external-deps/python-language-server/test/plugins/test_mccabe_lint.py b/external-deps/python-language-server/test/plugins/test_mccabe_lint.py index e5bc27f471d..6fa4f0bfe42 100644 --- a/external-deps/python-language-server/test/plugins/test_mccabe_lint.py +++ b/external-deps/python-language-server/test/plugins/test_mccabe_lint.py @@ -12,11 +12,11 @@ \tpass""" -def test_mccabe(config): +def test_mccabe(config, workspace): old_settings = config.settings try: config.update({'plugins': {'mccabe': {'threshold': 1}}}) - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) diags = mccabe_lint.pyls_lint(config, doc) assert all([d['source'] == 'mccabe' for d in diags]) @@ -32,6 +32,6 @@ def test_mccabe(config): config._settings = old_settings -def test_mccabe_syntax_error(config): - doc = Document(DOC_URI, DOC_SYNTAX_ERR) +def test_mccabe_syntax_error(config, workspace): + doc = Document(DOC_URI, workspace, DOC_SYNTAX_ERR) assert mccabe_lint.pyls_lint(config, doc) is None diff --git a/external-deps/python-language-server/test/plugins/test_pycodestyle_lint.py b/external-deps/python-language-server/test/plugins/test_pycodestyle_lint.py index eb1429b5fe4..f2769a3d499 100644 --- a/external-deps/python-language-server/test/plugins/test_pycodestyle_lint.py +++ b/external-deps/python-language-server/test/plugins/test_pycodestyle_lint.py @@ -20,8 +20,8 @@ def hello( ): """ -def test_pycodestyle(config): - doc = Document(DOC_URI, DOC) +def test_pycodestyle(config, workspace): + doc = Document(DOC_URI, workspace, DOC) diags = pycodestyle_lint.pyls_lint(config, doc) assert all([d['source'] == 'pycodestyle' for d in diags]) diff --git a/external-deps/python-language-server/test/plugins/test_pydocstyle_lint.py b/external-deps/python-language-server/test/plugins/test_pydocstyle_lint.py index f1c32703b8c..9ee7b289dde 100644 --- a/external-deps/python-language-server/test/plugins/test_pydocstyle_lint.py +++ b/external-deps/python-language-server/test/plugins/test_pydocstyle_lint.py @@ -16,8 +16,8 @@ def hello(): """ -def test_pydocstyle(config): - doc = Document(DOC_URI, DOC) +def test_pydocstyle(config, workspace): + doc = Document(DOC_URI, workspace, DOC) diags = pydocstyle_lint.pyls_lint(config, doc) assert all([d['source'] == 'pydocstyle' for d in diags]) @@ -35,22 +35,22 @@ def test_pydocstyle(config): } -def test_pydocstyle_test_document(config): +def test_pydocstyle_test_document(config, workspace): # The default --match argument excludes test_* documents. - doc = Document(TEST_DOC_URI, "") + doc = Document(TEST_DOC_URI, workspace, "") diags = pydocstyle_lint.pyls_lint(config, doc) assert not diags -def test_pydocstyle_empty_source(config): - doc = Document(DOC_URI, "") +def test_pydocstyle_empty_source(config, workspace): + doc = Document(DOC_URI, workspace, "") diags = pydocstyle_lint.pyls_lint(config, doc) assert diags[0]['message'] == 'D100: Missing docstring in public module' assert len(diags) == 1 -def test_pydocstyle_invalid_source(config): - doc = Document(DOC_URI, "bad syntax") +def test_pydocstyle_invalid_source(config, workspace): + doc = Document(DOC_URI, workspace, "bad syntax") diags = pydocstyle_lint.pyls_lint(config, doc) # We're unable to parse the file, so can't get any pydocstyle diagnostics assert not diags diff --git a/external-deps/python-language-server/test/plugins/test_pyflakes_lint.py b/external-deps/python-language-server/test/plugins/test_pyflakes_lint.py index cf824c08a26..aa96826592e 100644 --- a/external-deps/python-language-server/test/plugins/test_pyflakes_lint.py +++ b/external-deps/python-language-server/test/plugins/test_pyflakes_lint.py @@ -24,8 +24,8 @@ def hello(): """ -def test_pyflakes(): - doc = Document(DOC_URI, DOC) +def test_pyflakes(workspace): + doc = Document(DOC_URI, workspace, DOC) diags = pyflakes_lint.pyls_lint(doc) # One we're expecting is: @@ -36,8 +36,8 @@ def test_pyflakes(): assert unused_import['severity'] == lsp.DiagnosticSeverity.Warning -def test_syntax_error_pyflakes(): - doc = Document(DOC_URI, DOC_SYNTAX_ERR) +def test_syntax_error_pyflakes(workspace): + doc = Document(DOC_URI, workspace, DOC_SYNTAX_ERR) diag = pyflakes_lint.pyls_lint(doc)[0] assert diag['message'] == 'invalid syntax' @@ -45,8 +45,8 @@ def test_syntax_error_pyflakes(): assert diag['severity'] == lsp.DiagnosticSeverity.Error -def test_undefined_name_pyflakes(): - doc = Document(DOC_URI, DOC_UNDEFINED_NAME_ERR) +def test_undefined_name_pyflakes(workspace): + doc = Document(DOC_URI, workspace, DOC_UNDEFINED_NAME_ERR) diag = pyflakes_lint.pyls_lint(doc)[0] assert diag['message'] == 'undefined name \'b\'' @@ -54,8 +54,8 @@ def test_undefined_name_pyflakes(): assert diag['severity'] == lsp.DiagnosticSeverity.Error -def test_unicode_encoding(): - doc = Document(DOC_URI, DOC_ENCODING) +def test_unicode_encoding(workspace): + doc = Document(DOC_URI, workspace, DOC_ENCODING) diags = pyflakes_lint.pyls_lint(doc) assert len(diags) == 1 diff --git a/external-deps/python-language-server/test/plugins/test_pylint_lint.py b/external-deps/python-language-server/test/plugins/test_pylint_lint.py index b0d85fdc5ee..c2bf1bca32a 100644 --- a/external-deps/python-language-server/test/plugins/test_pylint_lint.py +++ b/external-deps/python-language-server/test/plugins/test_pylint_lint.py @@ -4,6 +4,7 @@ import tempfile from test import py2_only, py3_only +from test.test_utils import MockWorkspace from pyls import lsp, uris from pyls.workspace import Document from pyls.plugins import pylint_lint @@ -29,7 +30,7 @@ def temp_document(doc_text): name = temp_file.name temp_file.write(doc_text) temp_file.close() - yield Document(uris.from_fs_path(name)) + yield Document(uris.from_fs_path(name), MockWorkspace()) finally: os.remove(name) @@ -72,12 +73,12 @@ def test_syntax_error_pylint_py2(config): assert diag['severity'] == lsp.DiagnosticSeverity.Error -def test_lint_free_pylint(config): +def test_lint_free_pylint(config, workspace): # Can't use temp_document because it might give us a file that doesn't # match pylint's naming requirements. We should be keeping this file clean # though, so it works for a test of an empty lint. assert not pylint_lint.pyls_lint( - config, Document(uris.from_fs_path(__file__)), True) + config, Document(uris.from_fs_path(__file__), workspace), True) def test_lint_caching(): @@ -108,10 +109,10 @@ def test_lint_caching(): assert not pylint_lint.PylintLinter.lint(doc, False, flags) -def test_per_file_caching(config): +def test_per_file_caching(config, workspace): # Ensure that diagnostics are cached per-file. with temp_document(DOC) as doc: assert pylint_lint.pyls_lint(config, doc, True) assert not pylint_lint.pyls_lint( - config, Document(uris.from_fs_path(__file__)), False) + config, Document(uris.from_fs_path(__file__), workspace), False) diff --git a/external-deps/python-language-server/test/plugins/test_references.py b/external-deps/python-language-server/test/plugins/test_references.py index 7e7cbe75251..a3e5f889ccb 100644 --- a/external-deps/python-language-server/test/plugins/test_references.py +++ b/external-deps/python-language-server/test/plugins/test_references.py @@ -1,9 +1,12 @@ # Copyright 2017 Palantir Technologies, Inc. import os + import pytest + from pyls import uris from pyls.workspace import Document from pyls.plugins.references import pyls_references +from pyls._utils import PY2 DOC1_NAME = 'test1.py' @@ -23,24 +26,18 @@ @pytest.fixture -def tmp_workspace(workspace): - def create_file(name, content): - fn = os.path.join(workspace.root_path, name) - with open(fn, 'w') as f: - f.write(content) - workspace.put_document(uris.from_fs_path(fn), content) - - create_file(DOC1_NAME, DOC1) - create_file(DOC2_NAME, DOC2) - - return workspace +def tmp_workspace(temp_workspace_factory): + return temp_workspace_factory({ + DOC1_NAME: DOC1, + DOC2_NAME: DOC2, + }) def test_references(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'Test1' in class Test1(): position = {'line': 0, 'character': 8} DOC1_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC1_NAME)) - doc1 = Document(DOC1_URI) + doc1 = Document(DOC1_URI, tmp_workspace) refs = pyls_references(doc1, position) @@ -66,14 +63,18 @@ def test_references(tmp_workspace): # pylint: disable=redefined-outer-name assert doc2_usage_ref['range']['end'] == {'line': 3, 'character': 9} +@pytest.mark.skipif(PY2, reason="Jedi sometimes fails while checking pylint " + "example files in the modules path") def test_references_builtin(tmp_workspace): # pylint: disable=redefined-outer-name # Over 'UnicodeError': position = {'line': 4, 'character': 7} doc2_uri = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC2_NAME)) - doc2 = Document(doc2_uri) + doc2 = Document(doc2_uri, tmp_workspace) refs = pyls_references(doc2, position) assert len(refs) >= 1 - assert refs[0]['range']['start'] == {'line': 4, 'character': 7} - assert refs[0]['range']['end'] == {'line': 4, 'character': 19} + expected = {'start': {'line': 4, 'character': 7}, + 'end': {'line': 4, 'character': 19}} + ranges = [r['range'] for r in refs] + assert expected in ranges diff --git a/external-deps/python-language-server/test/plugins/test_rope_rename.py b/external-deps/python-language-server/test/plugins/test_rope_rename.py new file mode 100644 index 00000000000..45bd6bbd440 --- /dev/null +++ b/external-deps/python-language-server/test/plugins/test_rope_rename.py @@ -0,0 +1,42 @@ +import os + +import pytest +from pyls import uris +from pyls.plugins.rope_rename import pyls_rename +from pyls.workspace import Document + +DOC_NAME = "test1.py" +DOC = """class Test1(): + pass + +class Test2(Test1): + pass +""" + + +@pytest.fixture +def tmp_workspace(temp_workspace_factory): + return temp_workspace_factory({DOC_NAME: DOC}) + + +def test_rope_rename(tmp_workspace, config): # pylint: disable=redefined-outer-name + position = {"line": 0, "character": 6} + DOC_URI = uris.from_fs_path(os.path.join(tmp_workspace.root_path, DOC_NAME)) + doc = Document(DOC_URI, tmp_workspace) + + result = pyls_rename(config, tmp_workspace, doc, position, "ShouldBeRenamed") + assert len(result.keys()) == 1 + + changes = result.get("documentChanges") + assert len(changes) == 1 + changes = changes[0] + + assert changes.get("edits") == [ + { + "range": { + "start": {"line": 0, "character": 0}, + "end": {"line": 5, "character": 0}, + }, + "newText": "class ShouldBeRenamed():\n pass\n\nclass Test2(ShouldBeRenamed):\n pass\n", + } + ] diff --git a/external-deps/python-language-server/test/plugins/test_signature.py b/external-deps/python-language-server/test/plugins/test_signature.py index 01a439a26c8..b6b5111a9af 100644 --- a/external-deps/python-language-server/test/plugins/test_signature.py +++ b/external-deps/python-language-server/test/plugins/test_signature.py @@ -39,19 +39,19 @@ def main(param1=None, """ -def test_no_signature(): +def test_no_signature(workspace): # Over blank line sig_position = {'line': 9, 'character': 0} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) sigs = signature.pyls_signature_help(doc, sig_position)['signatures'] assert not sigs -def test_signature(): +def test_signature(workspace): # Over '( ' in main( sig_position = {'line': 10, 'character': 5} - doc = Document(DOC_URI, DOC) + doc = Document(DOC_URI, workspace, DOC) sig_info = signature.pyls_signature_help(doc, sig_position) @@ -64,10 +64,10 @@ def test_signature(): assert sig_info['activeParameter'] == 0 -def test_multi_line_signature(): +def test_multi_line_signature(workspace): # Over '( ' in main( sig_position = {'line': 17, 'character': 5} - doc = Document(DOC_URI, MULTI_LINE_DOC) + doc = Document(DOC_URI, workspace, MULTI_LINE_DOC) sig_info = signature.pyls_signature_help(doc, sig_position) diff --git a/external-deps/python-language-server/test/plugins/test_symbols.py b/external-deps/python-language-server/test/plugins/test_symbols.py index fa6f7df1bba..7bfb73ea01f 100644 --- a/external-deps/python-language-server/test/plugins/test_symbols.py +++ b/external-deps/python-language-server/test/plugins/test_symbols.py @@ -48,8 +48,8 @@ def sym(name): assert sym('a')['location']['range']['start'] == {'line': 2, 'character': 0} -def test_symbols(config): - doc = Document(DOC_URI, DOC) +def test_symbols(config, workspace): + doc = Document(DOC_URI, workspace, DOC) config.update({'plugins': {'jedi_symbols': {'all_scopes': False}}}) symbols = pyls_document_symbols(config, doc) @@ -73,15 +73,15 @@ def sym(name): assert sym('main')['location']['range']['end'] == {'line': 12, 'character': 0} -def test_symbols_all_scopes(config): - doc = Document(DOC_URI, DOC) +def test_symbols_all_scopes(config, workspace): + doc = Document(DOC_URI, workspace, DOC) symbols = pyls_document_symbols(config, doc) helper_check_symbols_all_scope(symbols) @pytest.mark.skipif(PY2 or not LINUX or not CI, reason="tested on linux and python 3 only") def test_symbols_all_scopes_with_jedi_environment(config): - doc = Document(DOC_URI, DOC, workspace=MockWorkspace()) + doc = Document(DOC_URI, MockWorkspace(), DOC) # Update config extra environment env_path = '/tmp/pyenv/bin/python' diff --git a/external-deps/python-language-server/test/plugins/test_yapf_format.py b/external-deps/python-language-server/test/plugins/test_yapf_format.py index 4bf6be32f8e..e3e198e6048 100644 --- a/external-deps/python-language-server/test/plugins/test_yapf_format.py +++ b/external-deps/python-language-server/test/plugins/test_yapf_format.py @@ -19,16 +19,16 @@ GOOD_DOC = """A = ['hello', 'world']\n""" -def test_format(): - doc = Document(DOC_URI, DOC) +def test_format(workspace): + doc = Document(DOC_URI, workspace, DOC) res = pyls_format_document(doc) assert len(res) == 1 assert res[0]['newText'] == "A = ['h', 'w', 'a']\n\nB = ['h', 'w']\n" -def test_range_format(): - doc = Document(DOC_URI, DOC) +def test_range_format(workspace): + doc = Document(DOC_URI, workspace, DOC) def_range = { 'start': {'line': 0, 'character': 0}, @@ -42,17 +42,17 @@ def test_range_format(): assert res[0]['newText'] == "A = ['h', 'w', 'a']\n\nB = ['h',\n\n\n'w']\n" -def test_no_change(): - doc = Document(DOC_URI, GOOD_DOC) +def test_no_change(workspace): + doc = Document(DOC_URI, workspace, GOOD_DOC) assert not pyls_format_document(doc) -def test_config_file(tmpdir): +def test_config_file(tmpdir, workspace): # a config file in the same directory as the source file will be used conf = tmpdir.join('.style.yapf') conf.write('[style]\ncolumn_limit = 14') src = tmpdir.join('test.py') - doc = Document(uris.from_fs_path(src.strpath), DOC) + doc = Document(uris.from_fs_path(src.strpath), workspace, DOC) # A was split on multiple lines because of column_limit from config file assert pyls_format_document(doc)[0]['newText'] == "A = [\n 'h', 'w',\n 'a'\n]\n\nB = ['h', 'w']\n" diff --git a/external-deps/python-language-server/test/test_document.py b/external-deps/python-language-server/test/test_document.py index 4fd4ea2fa5c..dc54613a84f 100644 --- a/external-deps/python-language-server/test/test_document.py +++ b/external-deps/python-language-server/test/test_document.py @@ -13,9 +13,9 @@ def test_document_lines(doc): assert doc.lines[0] == 'import sys\n' -def test_document_source_unicode(): - document_mem = Document(DOC_URI, u'my source') - document_disk = Document(DOC_URI) +def test_document_source_unicode(workspace): + document_mem = Document(DOC_URI, workspace, u'my source') + document_disk = Document(DOC_URI, workspace) assert isinstance(document_mem.source, type(document_disk.source)) @@ -41,8 +41,8 @@ def test_word_at_position(doc): assert doc.word_at_position({'line': 4, 'character': 0}) == '' -def test_document_empty_edit(): - doc = Document('file:///uri', u'') +def test_document_empty_edit(workspace): + doc = Document('file:///uri', workspace, u'') doc.apply_change({ 'range': { 'start': {'line': 0, 'character': 0}, @@ -53,8 +53,8 @@ def test_document_empty_edit(): assert doc.source == u'f' -def test_document_line_edit(): - doc = Document('file:///uri', u'itshelloworld') +def test_document_line_edit(workspace): + doc = Document('file:///uri', workspace, u'itshelloworld') doc.apply_change({ 'text': u'goodbye', 'range': { @@ -65,13 +65,13 @@ def test_document_line_edit(): assert doc.source == u'itsgoodbyeworld' -def test_document_multiline_edit(): +def test_document_multiline_edit(workspace): old = [ "def hello(a, b):\n", " print a\n", " print b\n" ] - doc = Document('file:///uri', u''.join(old)) + doc = Document('file:///uri', workspace, u''.join(old)) doc.apply_change({'text': u'print a, b', 'range': { 'start': {'line': 1, 'character': 4}, 'end': {'line': 2, 'character': 11} @@ -82,12 +82,12 @@ def test_document_multiline_edit(): ] -def test_document_end_of_file_edit(): +def test_document_end_of_file_edit(workspace): old = [ "print 'a'\n", "print 'b'\n" ] - doc = Document('file:///uri', u''.join(old)) + doc = Document('file:///uri', workspace, u''.join(old)) doc.apply_change({'text': u'o', 'range': { 'start': {'line': 2, 'character': 0}, 'end': {'line': 2, 'character': 0} diff --git a/external-deps/python-language-server/test/test_utils.py b/external-deps/python-language-server/test/test_utils.py index d27f24ba6fe..e6f6e56ad02 100644 --- a/external-deps/python-language-server/test/test_utils.py +++ b/external-deps/python-language-server/test/test_utils.py @@ -17,6 +17,9 @@ def __init__(self): # This is to avoid pyling tests of the variable not being used sys.stdout.write(str(self._environments)) + # This necessary for the new Jedi 0.17+ API. + self.root_path = '' + def test_debounce(): interval = 0.1 diff --git a/external-deps/python-language-server/test/test_workspace.py b/external-deps/python-language-server/test/test_workspace.py index 9b5b7b06e48..6ecdfbf5956 100644 --- a/external-deps/python-language-server/test/test_workspace.py +++ b/external-deps/python-language-server/test/test_workspace.py @@ -96,8 +96,8 @@ def test_multiple_workspaces(tmpdir, pyls): added_workspaces = [{'uri': path_as_uri(str(x))} for x in (workspace1_dir, workspace2_dir)] - pyls.m_workspace__did_change_workspace_folders( - added=added_workspaces, removed=[]) + event = {'added': added_workspaces, 'removed': []} + pyls.m_workspace__did_change_workspace_folders(event) for workspace in added_workspaces: assert workspace['uri'] in pyls.workspaces @@ -116,6 +116,84 @@ def test_multiple_workspaces(tmpdir, pyls): workspace2_uri = added_workspaces[1]['uri'] assert msg['uri'] in pyls.workspaces[workspace2_uri]._docs - pyls.m_workspace__did_change_workspace_folders( - added=[], removed=[added_workspaces[0]]) + event = {'added': [], 'removed': [added_workspaces[0]]} + pyls.m_workspace__did_change_workspace_folders(event) assert workspace1_uri not in pyls.workspaces + + +def test_multiple_workspaces_wrong_removed_uri(pyls): + workspace = {'uri': 'Test123'} + event = {'added': [], 'removed': [workspace]} + pyls.m_workspace__did_change_workspace_folders(event) + assert workspace['uri'] not in pyls.workspaces + + +def test_root_workspace_changed(pyls): + test_uri = 'Test123' + pyls.root_uri = test_uri + pyls.workspace._root_uri = test_uri + + workspace1 = {'uri': test_uri} + workspace2 = {'uri': 'NewTest456'} + + event = {'added': [workspace2], 'removed': [workspace1]} + pyls.m_workspace__did_change_workspace_folders(event) + + assert workspace2['uri'] == pyls.workspace._root_uri + assert workspace2['uri'] == pyls.root_uri + + +def test_root_workspace_not_changed(pyls): + # removed uri != root_uri + test_uri_1 = 'Test12' + pyls.root_uri = test_uri_1 + pyls.workspace._root_uri = test_uri_1 + workspace1 = {'uri': 'Test1234'} + workspace2 = {'uri': 'NewTest456'} + event = {'added': [workspace2], 'removed': [workspace1]} + pyls.m_workspace__did_change_workspace_folders(event) + assert test_uri_1 == pyls.workspace._root_uri + assert test_uri_1 == pyls.root_uri + # empty 'added' list + test_uri_2 = 'Test123' + new_root_uri = workspace2['uri'] + pyls.root_uri = test_uri_2 + pyls.workspace._root_uri = test_uri_2 + workspace1 = {'uri': test_uri_2} + event = {'added': [], 'removed': [workspace1]} + pyls.m_workspace__did_change_workspace_folders(event) + assert new_root_uri == pyls.workspace._root_uri + assert new_root_uri == pyls.root_uri + # empty 'removed' list + event = {'added': [workspace1], 'removed': []} + pyls.m_workspace__did_change_workspace_folders(event) + assert new_root_uri == pyls.workspace._root_uri + assert new_root_uri == pyls.root_uri + # 'added' list has no 'uri' + workspace2 = {'TESTuri': 'Test1234'} + event = {'added': [workspace2], 'removed': [workspace1]} + pyls.m_workspace__did_change_workspace_folders(event) + assert new_root_uri == pyls.workspace._root_uri + assert new_root_uri == pyls.root_uri + + +def test_root_workspace_removed(tmpdir, pyls): + workspace1_dir = tmpdir.mkdir('workspace1') + workspace2_dir = tmpdir.mkdir('workspace2') + root_uri = pyls.root_uri + + # Add workspaces to the pyls + added_workspaces = [{'uri': path_as_uri(str(x))} + for x in (workspace1_dir, workspace2_dir)] + event = {'added': added_workspaces, 'removed': []} + pyls.m_workspace__did_change_workspace_folders(event) + + # Remove the root workspace + removed_workspaces = [{'uri': root_uri}] + event = {'added': [], 'removed': removed_workspaces} + pyls.m_workspace__did_change_workspace_folders(event) + + # Assert that the first of the workspaces (in alphabetical order) is now + # the root workspace + assert pyls.root_uri == path_as_uri(str(workspace1_dir)) + assert pyls.workspace._root_uri == path_as_uri(str(workspace1_dir)) diff --git a/external-deps/python-language-server/vscode-client/package.json b/external-deps/python-language-server/vscode-client/package.json index eb4faeeaa89..7e4ee59f85b 100644 --- a/external-deps/python-language-server/vscode-client/package.json +++ b/external-deps/python-language-server/vscode-client/package.json @@ -55,6 +55,16 @@ "default": true, "description": "Auto-completes methods and classes with tabstops for each parameter." }, + "pyls.plugins.jedi_completion.include_class_objects": { + "type": "boolean", + "default": true, + "description": "Adds class objects as a separate completion item." + }, + "pyls.plugins.jedi_completion.fuzzy": { + "type": "boolean", + "default": false, + "description": "Enable fuzzy when requesting autocomplete." + }, "pyls.plugins.jedi_definition.enabled": { "type": "boolean", "default": true, diff --git a/requirements/conda-2.7.txt b/requirements/conda-2.7.txt new file mode 100644 index 00000000000..6492a8693d6 --- /dev/null +++ b/requirements/conda-2.7.txt @@ -0,0 +1,38 @@ +# We can not refer to an environment.yml file from another +# So to get performant launches on mybinder.org, we have copied +# the contents of this file to binder/environment.yml. If you +# make changes here, please copy them over there too. +applaunchservices >=0.1.7 +atomicwrites >=1.2.0 +chardet >=2.0.0 +cloudpickle >=0.5.0 +diff-match-patch >=20181111 +intervaltree +IPython >=4.0 +jedi =0.15.2 +keyring +nbconvert >=4.0 +numpydoc >=0.6.0 +Paramiko >=2.4.0 +parso =0.5.2 +pexpect >=4.4.0 +pickleshare >=0.4 +psutil >=5.3 +pygments >=2.0 +pylint >=1.0 +pyqt <5.13 +# There's no need to set a version for python-language-server +# because we install it from master for our tests. +python-language-server +pyxdg >=0.26 +pyzmq >=17 +qdarkstyle >=2.8 +qtawesome >=0.5.7 +qtconsole >=4.6.0 +qtpy >=1.5.0 +rtree >=0.8.3 +sphinx >=0.6.6 +# NOTE: There's no need to set a version for spyder-kernels +# here because we're using a subrepo for it to run our tests. +spyder-kernels +watchdog diff --git a/requirements/conda.txt b/requirements/conda.txt index 6492a8693d6..cf7a70f974a 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -9,12 +9,12 @@ cloudpickle >=0.5.0 diff-match-patch >=20181111 intervaltree IPython >=4.0 -jedi =0.15.2 +jedi =0.17.0 keyring nbconvert >=4.0 numpydoc >=0.6.0 Paramiko >=2.4.0 -parso =0.5.2 +parso =0.7.0 pexpect >=4.4.0 pickleshare >=0.4 psutil >=5.3 diff --git a/setup.py b/setup.py index af35b28e3f9..870d0c2a67c 100644 --- a/setup.py +++ b/setup.py @@ -210,7 +210,7 @@ def run(self): 'ipython>=4.0', # This is here until Jedi 0.15+ fixes completions for # Numpy and Pandas - 'jedi==0.15.2', + 'jedi==0.17.0', # Don't require keyring for Python 2 and Linux # because it depends on system packages 'keyring;sys_platform!="linux2"', @@ -218,7 +218,7 @@ def run(self): 'numpydoc>=0.6.0', # Required to get SSH connections to remote kernels 'paramiko>=2.4.0;platform_system=="Windows"', - 'parso==0.5.2', + 'parso==0.7.0', 'pexpect>=4.4.0', 'pickleshare>=0.4', 'psutil>=5.3', @@ -226,7 +226,7 @@ def run(self): 'pylint>=1.0', 'pyqt5<5.13;python_version>="3"', 'pyqtwebengine<5.13;python_version>="3"', - 'python-language-server[all]>=0.31.9,<0.32.0', + 'python-language-server[all]>=0.33.0,<0.34.0', 'pyxdg>=0.26;platform_system=="Linux"', 'pyzmq>=17', 'qdarkstyle>=2.8', diff --git a/spyder/dependencies.py b/spyder/dependencies.py index 249c0bd9909..275163ccd45 100644 --- a/spyder/dependencies.py +++ b/spyder/dependencies.py @@ -36,18 +36,18 @@ DIFF_MATCH_PATCH_REQVER = '>=20181111' INTERVALTREE_REQVER = None IPYTHON_REQVER = ">=4.0;<6.0" if PY2 else ">=4.0" -JEDI_REQVER = '=0.15.2' +JEDI_REQVER = '=0.17.0' KEYRING_REQVER = None NBCONVERT_REQVER = '>=4.0' NUMPYDOC_REQVER = '>=0.6.0' PARAMIKO_REQVER = '>=2.4.0' -PARSO_REQVER = '=0.5.2' +PARSO_REQVER = '=0.7.0' PEXPECT_REQVER = '>=4.4.0' PICKLESHARE_REQVER = '>=0.4' PSUTIL_REQVER = '>=5.3' PYGMENTS_REQVER = '>=2.0' PYLINT_REQVER = '>=1.0' -PYLS_REQVER = '>=0.31.9;<0.32.0' +PYLS_REQVER = '>=0.33.0;<0.34.0' PYXDG_REQVER = '>=0.26' PYZMQ_REQVER = '>=17' QDARKSTYLE_REQVER = '>=2.8'