forked from VimPlug/jedi
Get doctest completions working, fixes #860
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user