Skip to content

Commit a19632e

Browse files
committed
Merge branch 'develop' of github.com:palantir/python-language-server into develop
2 parents e3b6920 + 2796154 commit a19632e

27 files changed

+572
-99
lines changed

.circleci/config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
- image: "python:3.5-stretch"
1818
steps:
1919
- checkout
20+
# To test Jedi environments
21+
- run: python3 -m venv /tmp/pyenv
22+
- run: /tmp/pyenv/bin/python -m pip install loghub
2023
- run: pip install -e .[all] .[test]
2124
- run: py.test -v test/
2225

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ To enable pydocstyle for linting docstrings add the following setting in your LS
7272
"pyls.plugins.pydocstyle.enabled": true
7373
```
7474

75+
See `vscode-client/package.json`_ for the full set of supported configuration options.
76+
77+
.. _vscode-client/package.json: vscode-client/package.json
78+
7579
Language Server Features
7680
------------------------
7781

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ install:
2121
- "%PYTHON%/python.exe -m pip install .[all] .[test]"
2222

2323
test_script:
24-
- "%PYTHON%/Scripts/pytest.exe test/"
24+
- "%PYTHON%/Scripts/pytest.exe -v test/"
2525

2626
# on_finish:
2727
# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

pyls/__main__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import argparse
3-
import json
43
import logging
54
import logging.config
65
import sys
7-
from .python_ls import start_io_lang_server, start_tcp_lang_server, PythonLanguageServer
6+
7+
try:
8+
import ujson as json
9+
except Exception: # pylint: disable=broad-except
10+
import json
11+
12+
from .python_ls import (PythonLanguageServer, start_io_lang_server,
13+
start_tcp_lang_server)
814

915
LOG_FORMAT = "%(asctime)s UTC - %(levelname)s - %(name)s - %(message)s"
1016

pyls/hookspecs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ def pyls_initialize(config, workspace):
9595
pass
9696

9797

98+
@hookspec
99+
def pyls_initialized():
100+
pass
101+
102+
98103
@hookspec
99104
def pyls_lint(config, workspace, document, is_saved):
100105
pass

pyls/plugins/flake8_lint.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright 2019 Palantir Technologies, Inc.
22
"""Linter pluging for flake8"""
33
import logging
4+
from os import path
45
import re
56
from subprocess import Popen, PIPE
67
from pyls import hookimpl, lsp
@@ -20,6 +21,7 @@ def pyls_lint(config, document):
2021
log.debug("Got flake8 settings: %s", settings)
2122

2223
opts = {
24+
'config': settings.get('config'),
2325
'exclude': settings.get('exclude'),
2426
'filename': settings.get('filename'),
2527
'hang-closing': settings.get('hangClosing'),
@@ -28,6 +30,14 @@ def pyls_lint(config, document):
2830
'select': settings.get('select'),
2931
}
3032

33+
# flake takes only absolute path to the config. So we should check and
34+
# convert if necessary
35+
if opts.get('config') and not path.isabs(opts.get('config')):
36+
opts['config'] = path.abspath(path.expanduser(path.expandvars(
37+
opts.get('config')
38+
)))
39+
log.debug("using flake8 with config: %s", opts['config'])
40+
3141
# Call the flake8 utility then parse diagnostics from stdout
3242
args = build_args(opts, document.path)
3343
output = run_flake8(args)
@@ -64,16 +74,17 @@ def build_args(options, doc_path):
6474
"""
6575
args = [doc_path]
6676
for arg_name, arg_val in options.items():
77+
if arg_val is None:
78+
continue
6779
arg = None
6880
if isinstance(arg_val, list):
6981
arg = '--{}={}'.format(arg_name, ','.join(arg_val))
7082
elif isinstance(arg_val, bool):
7183
if arg_val:
7284
arg = '--{}'.format(arg_name)
73-
elif isinstance(arg_val, int):
85+
else:
7486
arg = '--{}={}'.format(arg_name, arg_val)
75-
if arg:
76-
args.append(arg)
87+
args.append(arg)
7788
return args
7889

7990

pyls/plugins/folding.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,50 @@ def __check_if_node_is_valid(node):
102102
valid = True
103103
if isinstance(node, tree_nodes.PythonNode):
104104
kind = node.type
105-
valid = kind not in {'decorated', 'parameters'}
105+
valid = kind not in {'decorated', 'parameters', 'dictorsetmaker',
106+
'testlist_comp'}
106107
if kind == 'suite':
107108
if isinstance(node.parent, tree_nodes.Function):
108109
valid = False
109110
return valid
110111

111112

113+
def __handle_skip(stack, skip):
114+
body = stack[skip]
115+
children = [body]
116+
if hasattr(body, 'children'):
117+
children = body.children
118+
stack = stack[:skip] + children + stack[skip + 1:]
119+
node = body
120+
end_line, _ = body.end_pos
121+
return node, end_line
122+
123+
124+
def __handle_flow_nodes(node, end_line, stack):
125+
from_keyword = False
126+
if isinstance(node, tree_nodes.Keyword):
127+
from_keyword = True
128+
if node.value in {'if', 'elif', 'with', 'while'}:
129+
node, end_line = __handle_skip(stack, 2)
130+
elif node.value in {'except'}:
131+
first_node = stack[0]
132+
if isinstance(first_node, tree_nodes.Operator):
133+
node, end_line = __handle_skip(stack, 1)
134+
else:
135+
node, end_line = __handle_skip(stack, 2)
136+
elif node.value in {'for'}:
137+
node, end_line = __handle_skip(stack, 4)
138+
elif node.value in {'else'}:
139+
node, end_line = __handle_skip(stack, 1)
140+
return end_line, from_keyword, node, stack
141+
142+
112143
def __compute_start_end_lines(node, stack):
113144
start_line, _ = node.start_pos
114145
end_line, _ = node.end_pos
146+
modified = False
147+
end_line, from_keyword, node, stack = __handle_flow_nodes(
148+
node, end_line, stack)
115149

116150
last_leaf = node.get_last_leaf()
117151
last_newline = isinstance(last_leaf, tree_nodes.Newline)
@@ -121,8 +155,7 @@ def __compute_start_end_lines(node, stack):
121155

122156
end_line -= 1
123157

124-
modified = False
125-
if isinstance(node.parent, tree_nodes.PythonNode):
158+
if isinstance(node.parent, tree_nodes.PythonNode) and not from_keyword:
126159
kind = node.type
127160
if kind in {'suite', 'atom', 'atom_expr', 'arglist'}:
128161
if len(stack) > 0:
@@ -133,7 +166,7 @@ def __compute_start_end_lines(node, stack):
133166
modified = True
134167
if not last_newline and not modified and not last_operator:
135168
end_line += 1
136-
return start_line, end_line
169+
return start_line, end_line, stack
137170

138171

139172
def __compute_folding_ranges(tree, lines):
@@ -158,7 +191,8 @@ def __compute_folding_ranges(tree, lines):
158191
elif not isinstance(node, SKIP_NODES):
159192
valid = __check_if_node_is_valid(node)
160193
if valid:
161-
start_line, end_line = __compute_start_end_lines(node, stack)
194+
start_line, end_line, stack = __compute_start_end_lines(
195+
node, stack)
162196
if end_line > start_line:
163197
current_end = folding_ranges.get(start_line, -1)
164198
folding_ranges[start_line] = max(current_end, end_line)

pyls/plugins/highlight.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import logging
3-
from pyls import hookimpl, lsp, uris
3+
from pyls import hookimpl, lsp
44

55
log = logging.getLogger(__name__)
66

@@ -13,7 +13,7 @@ def is_valid(definition):
1313
return definition.line is not None and definition.column is not None
1414

1515
def local_to_document(definition):
16-
return not definition.module_path or uris.uri_with(document.uri, path=definition.module_path) == document.uri
16+
return not definition.module_path or definition.module_path == document.path
1717

1818
return [{
1919
'range': {

pyls/plugins/hover.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def pyls_hover(document, position):
1616
# Find first exact matching definition
1717
definition = next((x for x in definitions if x.name == word), None)
1818

19+
# Ensure a definition is used if only one is available
20+
# even if the word doesn't match. An example of this case is 'np'
21+
# where 'numpy' doesn't match with 'np'. Same for NumPy ufuncs
22+
if len(definitions) == 1:
23+
definition = definitions[0]
24+
1925
if not definition:
2026
return {'contents': ''}
2127

pyls/plugins/jedi_completion.py

Lines changed: 94 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# Copyright 2017 Palantir Technologies, Inc.
22
import logging
3-
import time
3+
import os.path as osp
4+
import parso
45
from pyls import hookimpl, lsp, _utils
5-
from contextlib import contextmanager
6-
import signal
76

87
log = logging.getLogger(__name__)
98

@@ -24,6 +23,7 @@
2423
'builtinfunction': lsp.CompletionItemKind.Function,
2524
'module': lsp.CompletionItemKind.Module,
2625
'file': lsp.CompletionItemKind.File,
26+
'path': lsp.CompletionItemKind.Text,
2727
'xrange': lsp.CompletionItemKind.Class,
2828
'slice': lsp.CompletionItemKind.Class,
2929
'traceback': lsp.CompletionItemKind.Class,
@@ -43,29 +43,24 @@
4343

4444
COMPLETION_CACHE = {}
4545

46-
@contextmanager
47-
def timeout(time):
48-
# Register a function to raise a TimeoutError on the signal.
49-
signal.signal(signal.SIGALRM, raise_timeout)
50-
# Schedule the signal to be sent after ``time``.
51-
signal.setitimer(signal.SIGALRM, time)
52-
53-
try:
54-
yield
55-
except TimeoutError:
56-
pass
57-
finally:
58-
# Unregister the signal so it won't be triggered
59-
# if the timeout is not reached.
60-
signal.signal(signal.SIGALRM, signal.SIG_IGN)
46+
# Types of parso nodes for which snippet is not included in the completion
47+
_IMPORTS = ('import_name', 'import_from')
6148

62-
63-
def raise_timeout(signum, frame):
64-
raise TimeoutError
49+
# Types of parso node for errors
50+
_ERRORS = ('error_node', )
6551

6652
@hookimpl
6753
def pyls_completions(config, document, position):
68-
definitions = document.jedi_script(position).completions()
54+
try:
55+
definitions = document.jedi_script(position).completions()
56+
except AttributeError as e:
57+
if 'CompiledObject' in str(e):
58+
# Needed to handle missing CompiledObject attribute
59+
# 'sub_modules_dict'
60+
definitions = None
61+
else:
62+
raise e
63+
6964
if not definitions:
7065
return None
7166

@@ -77,9 +72,57 @@ def pyls_completions(config, document, position):
7772

7873
settings = config.plugin_settings('jedi_completion', document_path=document.path)
7974
should_include_params = settings.get('include_params')
75+
include_params = snippet_support and should_include_params and use_snippets(document, position)
76+
return [_format_completion(d, include_params) for d in definitions] or None
77+
78+
def is_exception_class(name):
79+
"""
80+
Determine if a class name is an instance of an Exception.
81+
82+
This returns `False` if the name given corresponds with a instance of
83+
the 'Exception' class, `True` otherwise
84+
"""
85+
try:
86+
return name in [cls.__name__ for cls in Exception.__subclasses__()]
87+
except AttributeError:
88+
# Needed in case a class don't uses new-style
89+
# class definition in Python 2
90+
return False
91+
8092

81-
result = [_format_completion(d, i, snippet_support and should_include_params) for i, d in enumerate(definitions)] or None
82-
return result
93+
def use_snippets(document, position):
94+
"""
95+
Determine if it's necessary to return snippets in code completions.
96+
97+
This returns `False` if a completion is being requested on an import
98+
statement, `True` otherwise.
99+
"""
100+
line = position['line']
101+
lines = document.source.split('\n', line)
102+
act_lines = [lines[line][:position['character']]]
103+
line -= 1
104+
last_character = ''
105+
while line > -1:
106+
act_line = lines[line]
107+
if (act_line.rstrip().endswith('\\') or
108+
act_line.rstrip().endswith('(') or
109+
act_line.rstrip().endswith(',')):
110+
act_lines.insert(0, act_line)
111+
line -= 1
112+
if act_line.rstrip().endswith('('):
113+
# Needs to be added to the end of the code before parsing
114+
# to make it valid, otherwise the node type could end
115+
# being an 'error_node' for multi-line imports that use '('
116+
last_character = ')'
117+
else:
118+
break
119+
if '(' in act_lines[-1].strip():
120+
last_character = ')'
121+
code = '\n'.join(act_lines).split(';')[-1].strip() + last_character
122+
tokens = parso.parse(code)
123+
expr_type = tokens.children[0].type
124+
return (expr_type not in _IMPORTS and
125+
not (expr_type in _ERRORS and 'import' in code))
83126

84127
@hookimpl
85128
def pyls_completion_detail(config, item):
@@ -108,18 +151,33 @@ def _format_completion(d, i, include_params=True):
108151
'sortText': '', #_sort_text(d),
109152
'insertText': d.name
110153
}
111-
# if include_params and hasattr(d, 'params') and d.params:
112-
# positional_args = [param for param in d.params if '=' not in param.description]
113-
114-
# # For completions with params, we can generate a snippet instead
115-
# completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
116-
# snippet = d.name + '('
117-
# for i, param in enumerate(positional_args):
118-
# snippet += '${%s:%s}' % (i + 1, param.name)
119-
# if i < len(positional_args) - 1:
120-
# snippet += ', '
121-
# snippet += ')$0'
122-
# completion['insertText'] = snippet
154+
155+
if d.type == 'path':
156+
path = osp.normpath(d.name)
157+
path = path.replace('\\', '\\\\')
158+
path = path.replace('/', '\\/')
159+
completion['insertText'] = path
160+
161+
if (include_params and hasattr(d, 'params') and d.params and
162+
not is_exception_class(d.name)):
163+
positional_args = [param for param in d.params
164+
if '=' not in param.description and
165+
param.name not in {'/', '*'}]
166+
167+
if len(positional_args) > 1:
168+
# For completions with params, we can generate a snippet instead
169+
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
170+
snippet = d.name + '('
171+
for i, param in enumerate(positional_args):
172+
snippet += '${%s:%s}' % (i + 1, param.name)
173+
if i < len(positional_args) - 1:
174+
snippet += ', '
175+
snippet += ')$0'
176+
completion['insertText'] = snippet
177+
elif len(positional_args) == 1:
178+
completion['insertText'] = d.name + '($0)'
179+
else:
180+
completion['insertText'] = d.name + '()'
123181

124182
return completion
125183

0 commit comments

Comments
 (0)