Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pyls/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ def clip_column(column, lines, line_number):
return min(column, max_column)


def position_to_jedi_linecolumn(document, position):
"""Convert the format 'line', 'character' to 'line', 'column'"""
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

Expand Down
8 changes: 5 additions & 3 deletions pyls/plugins/definition.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# 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_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 [
{
Expand Down
5 changes: 3 additions & 2 deletions pyls/plugins/highlight.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 8 additions & 1 deletion pyls/plugins/hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@

@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': ''}

Expand Down
23 changes: 13 additions & 10 deletions pyls/plugins/jedi_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@
@hookimpl
def pyls_completions(config, document, position):
try:
definitions = document.jedi_script(position).completions()
code_position = _utils.position_to_jedi_linecolumn(document, position)
completions = document.jedi_script().complete(**code_position)
except AttributeError as e:
if 'CompiledObject' in str(e):
# Needed to handle missing CompiledObject attribute
# 'sub_modules_dict'
definitions = None
# TODO: probably not needed for new Complete objects
completions = None
else:
raise e

if not definitions:
if not completions:
return None

completion_capabilities = config.capabilities.get('textDocument', {}).get('completion', {})
Expand All @@ -69,7 +71,7 @@ def pyls_completions(config, document, position):
settings = config.plugin_settings('jedi_completion', document_path=document.path)
should_include_params = settings.get('include_params')
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
return [_format_completion(c, include_params) for c in completions] or None


def is_exception_class(name):
Expand Down Expand Up @@ -138,9 +140,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
Copy link

@ccordoba12 ccordoba12 Apr 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you taking here only the first signature? Also, could you provide an example of an object with multiple signatures?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because PyLS expects only one for completion, but the newer Jedi provides a list of signatures.
https://jedi.readthedocs.io/en/latest/docs/api.html#jedi.Script.get_signatures

I am pretty sure there are cases with multiple signatures for a completion.
https://jedi.readthedocs.io/en/latest/docs/api-classes.html#jedi.api.classes.BaseName.get_signatures

    def get_signatures(self):
        """
        Returns all potential signatures for a function or a class. Multiple
        signatures are typical if you use Python stubs with ``@overload``.

        :rtype: list of :class:`BaseSignature`
        """
        return [
            BaseSignature(self._inference_state, s)
            for s in self._get_signatures()
        ]

if '=' not in param.description and
param.name not in {'/', '*'}]

Expand All @@ -163,8 +165,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])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, why sig[0]?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

return '{}({})'.format(definition.name, params)

return definition.name
Expand All @@ -173,7 +176,7 @@ def _label(definition):
def _detail(definition):
try:
return definition.parent().full_name or ''
except AttributeError:
except (AttributeError, TypeError):
return definition.full_name or ''


Expand Down
6 changes: 3 additions & 3 deletions pyls/plugins/references.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion pyls/plugins/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': []}
Expand Down
23 changes: 9 additions & 14 deletions pyls/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +216 to +218

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you ditch environment here?

Copy link
Author

@bnavigator bnavigator Apr 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because environment is already pulled from the configuration in self.jedi_script() and is passed to the jedi.Script() constructor there. The .get_names() method does not have an environment parameter.


def jedi_script(self, position=None):
extra_paths = []
Expand All @@ -237,15 +230,17 @@ def jedi_script(self, position=None):
environment = self.get_enviroment(environment_path) if environment_path else None

kwargs = {
'source': self.source,
# 'source' is deprecated but 'code' was only introduced in 0.17.0
'source' if jedi.__version__ < "0.17.0" else 'code': self.source,
'path': self.path,
'sys_path': sys_path,
'environment': environment,
'project': jedi.api.Project(os.path.dirname(self.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)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.16.0',
'python-jsonrpc-server>=0.3.2',
'pluggy',
'ujson<=1.35; platform_system!="Windows"'
Expand Down
3 changes: 2 additions & 1 deletion test/plugins/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ def test_jedi_completion(config):
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})
Expand Down
39 changes: 39 additions & 0 deletions test/plugins/test_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,45 @@ def main():
pass
"""

NUMPY_DOC = """

import numpy as np
np.sin

"""


def test_numpy_hover():
# Over the blank line
no_hov_position = {'line': 1, 'character': 0}
# Over 'numpy' in import numpy as np
numpy_hov_position_1 = {'line': 2, 'character': 8}
# Over 'np' in import numpy as np
numpy_hov_position_2 = {'line': 2, 'character': 17}
# Over 'np' in np.sin
numpy_hov_position_3 = {'line': 3, 'character': 1}
# Over 'sin' in np.sin
numpy_sin_hov_position = {'line': 3, 'character': 4}

doc = Document(DOC_URI, NUMPY_DOC)

if LooseVersion(_utils.JEDI_VERSION) >= LooseVersion('0.15.0'):
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_2)['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]


def test_hover():
# Over 'main' in def main():
Expand Down
8 changes: 5 additions & 3 deletions test/plugins/test_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def test_references_builtin(tmp_workspace): # pylint: disable=redefined-outer-n
doc2 = Document(doc2_uri)

refs = pyls_references(doc2, position)
assert len(refs) == 1
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