1
0
forked from VimPlug/jedi

Get doctest completions working, fixes #860

This commit is contained in:
Dave Halter
2020-01-01 00:24:58 +01:00
parent 8914bbbcc3
commit 50c5eb5786
5 changed files with 99 additions and 10 deletions

View File

@@ -204,9 +204,9 @@ class Script(object):
with debug.increase_indent_cm('complete'): with debug.increase_indent_cm('complete'):
completion = Completion( completion = Completion(
self._inference_state, self._get_module_context(), self._code_lines, self._inference_state, self._get_module_context(), self._code_lines,
(line, column), self.find_signatures (line, column), self.find_signatures, fuzzy=fuzzy,
) )
return completion.complete(fuzzy) return completion.complete()
def completions(self, fuzzy=False): def completions(self, fuzzy=False):
# Deprecated, will be removed. # Deprecated, will be removed.

View File

@@ -1,8 +1,10 @@
import re import re
from textwrap import dedent
from parso.python.token import PythonTokenTypes from parso.python.token import PythonTokenTypes
from parso.python import tree from parso.python import tree
from parso.tree import search_ancestor, Leaf from parso.tree import search_ancestor, Leaf
from parso import split_lines
from jedi._compatibility import Parameter from jedi._compatibility import Parameter
from jedi import debug from jedi import debug
@@ -16,7 +18,7 @@ from jedi.inference import imports
from jedi.inference.base_value import ValueSet from jedi.inference.base_value import ValueSet
from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names
from jedi.inference.context import get_global_filters from jedi.inference.context import get_global_filters
from jedi.inference.value import TreeInstance from jedi.inference.value import TreeInstance, ModuleValue
from jedi.inference.gradual.conversion import convert_values from jedi.inference.gradual.conversion import convert_values
from jedi.parser_utils import cut_value_at_position from jedi.parser_utils import cut_value_at_position
from jedi.plugins import plugin_manager from jedi.plugins import plugin_manager
@@ -105,7 +107,7 @@ class Completion:
self._fuzzy = fuzzy self._fuzzy = fuzzy
def complete(self, fuzzy): def complete(self):
leaf = self._module_node.get_leaf_for_position( leaf = self._module_node.get_leaf_for_position(
self._original_position, self._original_position,
include_prefixes=True include_prefixes=True
@@ -118,7 +120,7 @@ class Completion:
start_leaf or leaf, start_leaf or leaf,
self._original_position, self._original_position,
None if string is None else quote + string, None if string is None else quote + string,
fuzzy=fuzzy, fuzzy=self._fuzzy,
) )
if string is not None and not prefixed_completions: if string is not None and not prefixed_completions:
@@ -126,15 +128,18 @@ class Completion:
self._inference_state, self._module_context, start_leaf, string, self._inference_state, self._module_context, start_leaf, string,
self._like_name, self._signatures_callback, self._like_name, self._signatures_callback,
self._code_lines, self._original_position, self._code_lines, self._original_position,
fuzzy self._fuzzy
)) ))
if string is not None: if string is not None:
if not prefixed_completions and '\n' in string:
# Complete only multi line strings
prefixed_completions = self._complete_in_string(start_leaf, string)
return prefixed_completions return prefixed_completions
completion_names = self._complete_python(leaf) completion_names = self._complete_python(leaf)
completions = list(filter_names(self._inference_state, completion_names, completions = list(filter_names(self._inference_state, completion_names,
self.stack, self._like_name, fuzzy)) self.stack, self._like_name, self._fuzzy))
return ( return (
# Removing duplicates mostly to remove False/True/None duplicates. # Removing duplicates mostly to remove False/True/None duplicates.
@@ -434,6 +439,41 @@ class Completion:
if (name.api_type == 'function') == is_function: if (name.api_type == 'function') == is_function:
yield name yield name
def _complete_in_string(self, start_leaf, string):
"""
To make it possible for people to have completions in doctests or
generally in "Python" code in docstrings, we use the following
heuristic:
- Either
"""
string = dedent(string)
code_lines = split_lines(string, keepends=True)
if code_lines[-1].startswith('>>>') or code_lines[-1].startswith(' '):
code_lines = [
re.sub(r'^(>>> ?| +)', '', l)
for l in code_lines
if l.startswith('>>>') or l.startswith(' ')
]
module_node = self._inference_state.grammar.parse(''.join(code_lines))
module_value = ModuleValue(
self._inference_state,
module_node,
file_io=None,
string_names=None,
code_lines=code_lines,
)
module_value.parent_context = self._module_context
return Completion(
self._inference_state,
module_value.as_context(),
code_lines=code_lines,
position=module_node.end_pos,
signatures_callback=lambda *args, **kwargs: [],
fuzzy=self._fuzzy
).complete()
return []
def _gather_nodes(stack): def _gather_nodes(stack):
nodes = [] nodes = []
@@ -466,7 +506,7 @@ def _extract_string_while_in_string(leaf, position):
return return_part_of_leaf(leaf) return return_part_of_leaf(leaf)
leaves = [] leaves = []
while leaf is not None and leaf.line == position[0]: while leaf is not None:
if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value): if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value):
if len(leaf.value) > 1: if len(leaf.value) > 1:
return return_part_of_leaf(leaf) return return_part_of_leaf(leaf)
@@ -483,6 +523,12 @@ def _extract_string_while_in_string(leaf, position):
('' if prefix_leaf is None else prefix_leaf.value) ('' if prefix_leaf is None else prefix_leaf.value)
+ cut_value_at_position(leaf, position), + cut_value_at_position(leaf, position),
) )
if leaf.line != position[0]:
# Multi line strings are always simple error leaves and contain the
# whole string, single line error leaves are atherefore important
# now and since the line is different, it's not really a single
# line string anymore.
break
leaves.insert(0, leaf) leaves.insert(0, leaf)
leaf = leaf.get_previous_leaf() leaf = leaf.get_previous_leaf()
return None, None, None return None, None, None

View File

@@ -468,7 +468,7 @@ def get_global_filters(context, until_position, origin_scope):
until_position=until_position, until_position=until_position,
origin_scope=origin_scope): origin_scope=origin_scope):
yield filter yield filter
if isinstance(context, BaseFunctionExecutionContext): if isinstance(context, (BaseFunctionExecutionContext, ModuleContext)):
# The position should be reset if the current scope is a function. # The position should be reset if the current scope is a function.
until_position = None until_position = None

View File

@@ -252,3 +252,46 @@ def import_issues(foo):
""" """
#? datetime.datetime() #? datetime.datetime()
foo foo
# -----------------
# Doctest completions
# -----------------
def doctest_with_gt():
"""
x
>>> somewhere_in_docstring = 3
#? ['import_issues']
>>> import_issu
#? ['somewhere_in_docstring']
>>> somewhere_
blabla
>>> haha = 3
#? ['haha']
>>> hah
#? ['doctest_with_space']
>>> doctest_with_sp
"""
def doctest_with_space():
"""
x
#? ['import_issues']
import_issu
"""
def doctest_without_ending():
"""
#? []
import_issu
ha
no_ending = False
#? ['import_issues']
import_issu
#? ['no_ending']
no_endin

View File

@@ -23,7 +23,7 @@ def test_string_literals(Script):
script = Script(dedent(source)) script = Script(dedent(source))
assert script._get_module_context().tree_node.end_pos == (6, 0) assert script._get_module_context().tree_node.end_pos == (6, 0)
assert script.complete() assert not script.complete()
def test_incomplete_function(Script): def test_incomplete_function(Script):