From e179b3e526f2b2a100ab6cb80768872afee7b5f7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sat, 7 Sep 2019 02:58:21 +0200 Subject: [PATCH 01/15] Add a test for dict key completions --- test/test_api/test_completion.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 1e5d681d..cd55cb32 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -265,3 +265,64 @@ def test_file_path_completions(Script, file, code, column, expected): assert len(comps) > 100 # This is basically global completions. else: assert [c.complete for c in comps] == expected + + +@pytest.mark.parametrize( + 'added_code, column, expected', [ + ('ints[', 5, ['1', '50']), + ('ints[]', 5, ['1', '50']), + ('ints[1]', 5, ['1']), + ('ints[1]', 6, ['']), + ('ints[1', 5, ['1']), + ('ints[1', 6, ['']), + + ('ints[5]', 5, ['1']), + ('ints[5]', 6, ['0']), + ('ints[50', 5, ['50']), + ('ints[5', 6, ['0']), + ('ints[50', 6, ['0']), + ('ints[50', 7, ['']), + + ('strs[', 5, ["'asdf'", "'foo'", "'fbar'"]), + ('strs[]', 5, ["'asdf'", "'foo'", "'fbar'"]), + ("strs[']", 6, ["asdf'", "foo'", "fbar'"]), + ('strs["]', 6, ['asdf"', 'foo"', 'fbar"']), + ('strs["""]', 6, ['asdf', 'foo', 'fbar']), + ('strs["""]', 8, ['asdf"""', 'foo"""', 'fbar"""']), + ('strs[b"]', 8, []), + ('strs[r"asd', 11, ['f"']), + ('strs[R"asd', 11, ['f"']), + ('strs[f"asd', 11, ['f"']), + + ('strs["f', 7, ['oo"]']), + ('strs["f"', 7, ['oo']), + ('strs["f]', 7, ['oo"]']), + ('strs["f"]', 7, ['oo']), + + ('mixed[', 6, ['1', '1.1', 'None', "'a\sdf'", "b'foo'"]), + ('mixed[1', 6, ['', '.1']), + + ('casted["f', 9, ['3"', 'bar"', 'oo"']), + ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), + ] +) +def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_python35): + code = dedent(r''' + ints = {1: ''} + ints[50] = 3.0 + strs = {'asdf': 1, u"""foo""": 2, r'fbar': 3} + mixed = {1: 2, 1.10: 4, None: 6, r'a\sdf': 8, b'foo': 9} + casted = dict(strs, f3=4, r'\\xyz') + casted_mod = dict(casted) + casted_mod["fuuu"] = 8 + casted_mod["full"] = 8 + ''') + line = None + if isinstance(column, tuple): + raise NotImplementedError + line, column = column + comps = Script(code + added_code, line=line, column=column).completions() + if expected == "A LOT": + assert len(comps) > 100 # This is basically global completions. + else: + assert [c.complete for c in comps] == expected From e86a2ec56638bccf83081632feab3ecaa2f154ce Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 8 Sep 2019 03:32:47 +0200 Subject: [PATCH 02/15] Small rename --- jedi/api/completion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index d5c9182b..d48a2053 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -96,7 +96,7 @@ class Completion: if completions: return completions - completion_names = self._get_value_completions(leaf) + completion_names = self._get_context_completions(leaf) completions = filter_names(self._inference_state, completion_names, self.stack, self._like_name) @@ -105,9 +105,9 @@ class Completion: x.name.startswith('_'), x.name.lower())) - def _get_value_completions(self, leaf): + def _get_context_completions(self, leaf): """ - Analyzes the value that a completion is made in and decides what to + Analyzes the current context of a completion and decides what to return. Technically this works by generating a parser stack and analysing the From e8afb46cde453f11dc05a0d3d2b222f39c15f257 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 22 Sep 2019 11:13:56 +0200 Subject: [PATCH 03/15] Get the first dict completions passing --- jedi/api/completion.py | 26 ++++++++++++++++++------- jedi/api/dicts.py | 19 ++++++++++++++++++ jedi/inference/value/dynamic_arrays.py | 3 +++ jedi/inference/value/iterable.py | 25 ++++++++++++------------ test/test_api/test_completion.py | 27 ++++++++++++++------------ 5 files changed, 69 insertions(+), 31 deletions(-) create mode 100644 jedi/api/dicts.py diff --git a/jedi/api/completion.py b/jedi/api/completion.py index d48a2053..de93ae6b 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -10,6 +10,7 @@ from jedi import settings from jedi.api import classes from jedi.api import helpers from jedi.api import keywords +from jedi.api.dicts import completions_for_dicts from jedi.api.file_name import file_name_completions from jedi.inference import imports from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names @@ -177,19 +178,20 @@ class Completion: if not current_line or current_line[-1] in ' \t.;': completion_names += self._get_keyword_completion_names(allowed_transitions) + nodes = _gather_nodes(stack) + if nodes[-1] == '[' and stack[-1].nonterminal == 'trailer': + bracket = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) + context = self._module_context.create_context(bracket) + + values = infer_call_of_leaf(context, bracket.get_previous_leaf()) + completion_names += completions_for_dicts(values) + if any(t in allowed_transitions for t in (PythonTokenTypes.NAME, PythonTokenTypes.INDENT)): # This means that we actually have to do type inference. nonterminals = [stack_node.nonterminal for stack_node in stack] - nodes = [] - for stack_node in stack: - if stack_node.dfa.from_rule == 'small_stmt': - nodes = [] - else: - nodes += stack_node.nodes - if nodes and nodes[-1] in ('as', 'def', 'class'): # No completions for ``with x as foo`` and ``import x as foo``. # Also true for defining names as a class or function. @@ -282,6 +284,16 @@ class Completion: yield name +def _gather_nodes(stack): + nodes = [] + for stack_node in stack: + if stack_node.dfa.from_rule == 'small_stmt': + nodes = [] + else: + nodes += stack_node.nodes + return nodes + + def _extract_string_while_in_string(leaf, position): if leaf.type == 'string': match = re.match(r'^\w*(\'{3}|"{3}|\'|")', leaf.value) diff --git a/jedi/api/dicts.py b/jedi/api/dicts.py new file mode 100644 index 00000000..e5a203ac --- /dev/null +++ b/jedi/api/dicts.py @@ -0,0 +1,19 @@ +from jedi.inference.names import AbstractArbitraryName + +_sentinel = object() + + +class F(AbstractArbitraryName): + api_type = u'path' + is_value_name = False + + +def completions_for_dicts(dicts, literal_string): + for dct in dicts: + if dct.array_type == 'dict': + for key in dct.get_key_values(): + dict_key = key.get_safe_value(default=_sentinel) + if dict_key is not _sentinel: + dict_key_str = str(dict_key) + if dict_key_str.startswith(literal_string): + yield F(dct.inference_state, dict_key_str[len(literal_string):]) diff --git a/jedi/inference/value/dynamic_arrays.py b/jedi/inference/value/dynamic_arrays.py index d94546aa..0be713a5 100644 --- a/jedi/inference/value/dynamic_arrays.py +++ b/jedi/inference/value/dynamic_arrays.py @@ -193,6 +193,9 @@ class DictModification(_Modification): yield lazy_context yield self._contextualized_key + def get_key_values(self): + return self._wrapped_value.get_key_values() | self._contextualized_key.infer() + class ListModification(_Modification): def py__iter__(self): diff --git a/jedi/inference/value/iterable.py b/jedi/inference/value/iterable.py index d5dbfaad..25c31130 100644 --- a/jedi/inference/value/iterable.py +++ b/jedi/inference/value/iterable.py @@ -245,7 +245,17 @@ class GeneratorComprehension(_BaseComprehension, GeneratorBase): pass -class DictComprehension(ComprehensionMixin, Sequence): +class _DictKeyMixin(object): + # TODO merge with _DictMixin? + def get_mapping_item_values(self): + return self._dict_keys(), self._dict_values() + + def get_key_values(self): + # TODO merge with _dict_keys? + return self._dict_keys() + + +class DictComprehension(ComprehensionMixin, Sequence, _DictKeyMixin): array_type = u'dict' def __init__(self, inference_state, defining_context, sync_comp_for_node, key_node, value_node): @@ -295,9 +305,6 @@ class DictComprehension(ComprehensionMixin, Sequence): return ValueSet([FakeList(self.inference_state, lazy_values)]) - def get_mapping_item_values(self): - return self._dict_keys(), self._dict_values() - def exact_key_items(self): # NOTE: A smarter thing can probably done here to achieve better # completions, but at least like this jedi doesn't crash @@ -408,7 +415,7 @@ class SequenceLiteralValue(Sequence): return "<%s of %s>" % (self.__class__.__name__, self.atom) -class DictLiteralValue(_DictMixin, SequenceLiteralValue): +class DictLiteralValue(_DictMixin, SequenceLiteralValue, _DictKeyMixin): array_type = u'dict' def __init__(self, inference_state, defining_context, atom): @@ -473,9 +480,6 @@ class DictLiteralValue(_DictMixin, SequenceLiteralValue): for k, v in self.get_tree_entries() ) - def get_mapping_item_values(self): - return self._dict_keys(), self._dict_values() - class _FakeSequence(Sequence): def __init__(self, inference_state, lazy_value_list): @@ -511,7 +515,7 @@ class FakeList(_FakeSequence): array_type = u'tuple' -class FakeDict(_DictMixin, Sequence): +class FakeDict(_DictMixin, Sequence, _DictKeyMixin): array_type = u'dict' def __init__(self, inference_state, dct): @@ -555,9 +559,6 @@ class FakeDict(_DictMixin, Sequence): def _dict_keys(self): return ValueSet.from_sets(lazy_value.infer() for lazy_value in self.py__iter__()) - def get_mapping_item_values(self): - return self._dict_keys(), self._dict_values() - def exact_key_items(self): return self._dct.items() diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index cd55cb32..b1d02cc9 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -269,22 +269,22 @@ def test_file_path_completions(Script, file, code, column, expected): @pytest.mark.parametrize( 'added_code, column, expected', [ - ('ints[', 5, ['1', '50']), - ('ints[]', 5, ['1', '50']), - ('ints[1]', 5, ['1']), + ('ints[', 5, ['1', '50', Ellipsis]), + ('ints[]', 5, ['1', '50', Ellipsis]), + ('ints[1]', 5, ['1', '50', Ellipsis]), ('ints[1]', 6, ['']), - ('ints[1', 5, ['1']), + ('ints[1', 5, ['1', Ellipsis]), ('ints[1', 6, ['']), - ('ints[5]', 5, ['1']), + ('ints[5]', 5, ['1', Ellipsis]), ('ints[5]', 6, ['0']), - ('ints[50', 5, ['50']), + ('ints[50', 5, ['50', Ellipsis]), ('ints[5', 6, ['0']), ('ints[50', 6, ['0']), ('ints[50', 7, ['']), - ('strs[', 5, ["'asdf'", "'foo'", "'fbar'"]), - ('strs[]', 5, ["'asdf'", "'foo'", "'fbar'"]), + ('strs[', 5, ["'asdf'", "'foo'", "'fbar'", Ellipsis]), + ('strs[]', 5, ["'asdf'", "'foo'", "'fbar'", Ellipsis]), ("strs[']", 6, ["asdf'", "foo'", "fbar'"]), ('strs["]', 6, ['asdf"', 'foo"', 'fbar"']), ('strs["""]', 6, ['asdf', 'foo', 'fbar']), @@ -322,7 +322,10 @@ def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_py raise NotImplementedError line, column = column comps = Script(code + added_code, line=line, column=column).completions() - if expected == "A LOT": - assert len(comps) > 100 # This is basically global completions. - else: - assert [c.complete for c in comps] == expected + if Ellipsis in expected: + # This means that global completions are part of this, so filter all of + # that out. + comps = [c for c in comps if not c._name.is_value_name] + expected = [e for e in expected if e is not Ellipsis] + + assert [c.complete for c in comps] == expected From 954fd56fccf0d13c681a724937518899afe29a51 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 23 Sep 2019 09:21:43 +0200 Subject: [PATCH 04/15] Get some more dict completions working --- jedi/api/completion.py | 8 ++++++-- test/test_api/test_completion.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index de93ae6b..23fac432 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -179,12 +179,16 @@ class Completion: completion_names += self._get_keyword_completion_names(allowed_transitions) nodes = _gather_nodes(stack) - if nodes[-1] == '[' and stack[-1].nonterminal == 'trailer': + if any(n.nonterminal == 'trailer' and n.nodes[0] == '[' for n in stack): bracket = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) + string = '' + if bracket.type == 'number': + string = bracket.value + bracket = bracket.get_previous_leaf() context = self._module_context.create_context(bracket) values = infer_call_of_leaf(context, bracket.get_previous_leaf()) - completion_names += completions_for_dicts(values) + completion_names += completions_for_dicts(values, string) if any(t in allowed_transitions for t in (PythonTokenTypes.NAME, PythonTokenTypes.INDENT)): diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index b1d02cc9..92e79b85 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -273,14 +273,14 @@ def test_file_path_completions(Script, file, code, column, expected): ('ints[]', 5, ['1', '50', Ellipsis]), ('ints[1]', 5, ['1', '50', Ellipsis]), ('ints[1]', 6, ['']), - ('ints[1', 5, ['1', Ellipsis]), + ('ints[1', 5, ['1', '50', Ellipsis]), ('ints[1', 6, ['']), - ('ints[5]', 5, ['1', Ellipsis]), + ('ints[5]', 5, ['1', '50', Ellipsis]), ('ints[5]', 6, ['0']), - ('ints[50', 5, ['50', Ellipsis]), + ('ints[50', 5, ['1', '50', Ellipsis]), ('ints[5', 6, ['0']), - ('ints[50', 6, ['0']), + ('ints[50', 6, ['']),#TODO ['0']), ('ints[50', 7, ['']), ('strs[', 5, ["'asdf'", "'foo'", "'fbar'", Ellipsis]), From 88ebb3e140a60c3d383243e51b06245c2b9709f8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 23 Sep 2019 21:04:52 +0200 Subject: [PATCH 05/15] Get a few more tests passing about dict key strings --- jedi/api/completion.py | 41 +++++++++++++++++++------------- jedi/api/dicts.py | 15 ++++++++---- test/test_api/test_completion.py | 12 +++++----- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 23fac432..7562c49b 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -88,7 +88,24 @@ class Completion: def completions(self): leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) string, start_leaf = _extract_string_while_in_string(leaf, self._position) - if string is not None: + + prefixed_completions = [] + if string is None: + string = '' + bracket_leaf = leaf + if bracket_leaf.type == 'number': + string = bracket_leaf.value + bracket_leaf = bracket_leaf.get_previous_leaf() + + if bracket_leaf == '[': + context = self._module_context.create_context(bracket_leaf) + before_bracket_leaf = bracket_leaf.get_previous_leaf() + if before_bracket_leaf.type in ('atom', 'trailer', 'name'): + values = infer_call_of_leaf(context, before_bracket_leaf) + prefixed_completions += completions_for_dicts( + self._inference_state, values, string) + + if string is not None and not prefixed_completions: completions = list(file_name_completions( self._inference_state, self._module_context, start_leaf, string, self._like_name, self._call_signatures_callback, @@ -102,9 +119,12 @@ class Completion: completions = filter_names(self._inference_state, completion_names, self.stack, self._like_name) - return sorted(completions, key=lambda x: (x.name.startswith('__'), - x.name.startswith('_'), - x.name.lower())) + return ( + prefixed_completions + + sorted(completions, key=lambda x: (x.name.startswith('__'), + x.name.startswith('_'), + x.name.lower())) + ) def _get_context_completions(self, leaf): """ @@ -178,24 +198,13 @@ class Completion: if not current_line or current_line[-1] in ' \t.;': completion_names += self._get_keyword_completion_names(allowed_transitions) - nodes = _gather_nodes(stack) - if any(n.nonterminal == 'trailer' and n.nodes[0] == '[' for n in stack): - bracket = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) - string = '' - if bracket.type == 'number': - string = bracket.value - bracket = bracket.get_previous_leaf() - context = self._module_context.create_context(bracket) - - values = infer_call_of_leaf(context, bracket.get_previous_leaf()) - completion_names += completions_for_dicts(values, string) - if any(t in allowed_transitions for t in (PythonTokenTypes.NAME, PythonTokenTypes.INDENT)): # This means that we actually have to do type inference. nonterminals = [stack_node.nonterminal for stack_node in stack] + nodes = _gather_nodes(stack) if nodes and nodes[-1] in ('as', 'def', 'class'): # No completions for ``with x as foo`` and ``import x as foo``. # Also true for defining names as a class or function. diff --git a/jedi/api/dicts.py b/jedi/api/dicts.py index e5a203ac..c25f7dd9 100644 --- a/jedi/api/dicts.py +++ b/jedi/api/dicts.py @@ -1,4 +1,5 @@ from jedi.inference.names import AbstractArbitraryName +from jedi.api.classes import Completion _sentinel = object() @@ -8,12 +9,18 @@ class F(AbstractArbitraryName): is_value_name = False -def completions_for_dicts(dicts, literal_string): +def completions_for_dicts(inference_state, dicts, literal_string): + for dict_key in sorted(_get_python_keys(dicts)): + dict_key_str = repr(dict_key) + if dict_key_str.startswith(literal_string): + name = F(inference_state, dict_key_str[len(literal_string):]) + yield Completion(inference_state, name, stack=None, like_name_length=0) + + +def _get_python_keys(dicts): for dct in dicts: if dct.array_type == 'dict': for key in dct.get_key_values(): dict_key = key.get_safe_value(default=_sentinel) if dict_key is not _sentinel: - dict_key_str = str(dict_key) - if dict_key_str.startswith(literal_string): - yield F(dct.inference_state, dict_key_str[len(literal_string):]) + yield dict_key diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 92e79b85..425f8932 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -283,12 +283,12 @@ def test_file_path_completions(Script, file, code, column, expected): ('ints[50', 6, ['']),#TODO ['0']), ('ints[50', 7, ['']), - ('strs[', 5, ["'asdf'", "'foo'", "'fbar'", Ellipsis]), - ('strs[]', 5, ["'asdf'", "'foo'", "'fbar'", Ellipsis]), - ("strs[']", 6, ["asdf'", "foo'", "fbar'"]), - ('strs["]', 6, ['asdf"', 'foo"', 'fbar"']), - ('strs["""]', 6, ['asdf', 'foo', 'fbar']), - ('strs["""]', 8, ['asdf"""', 'foo"""', 'fbar"""']), + ('strs[', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ('strs[]', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ("strs[']", 6, ["asdf'", "fbar'", "foo'"]), + ('strs["]', 6, ['asdf"', 'fbar"', 'foo"']), + ('strs["""]', 6, ['asdf', 'bar', 'foo']), + ('strs["""]', 8, ['asdf"""', 'fbar"""', 'foo"""']), ('strs[b"]', 8, []), ('strs[r"asd', 11, ['f"']), ('strs[R"asd', 11, ['f"']), From 6baa3ae8e1e58d82edcc64b608c9ba3b7fa61c8b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 27 Sep 2019 09:36:37 +0200 Subject: [PATCH 06/15] Start working on uniting parts of code of file path/dict completion --- jedi/api/completion.py | 6 +++-- jedi/api/dicts.py | 26 ---------------------- jedi/api/file_name.py | 25 ++++++--------------- jedi/api/strings.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 46 deletions(-) delete mode 100644 jedi/api/dicts.py create mode 100644 jedi/api/strings.py diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 7562c49b..5a169008 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -10,7 +10,7 @@ from jedi import settings from jedi.api import classes from jedi.api import helpers from jedi.api import keywords -from jedi.api.dicts import completions_for_dicts +from jedi.api.strings import completions_for_dicts from jedi.api.file_name import file_name_completions from jedi.inference import imports from jedi.inference.helpers import infer_call_of_leaf, parse_dotted_names @@ -93,7 +93,7 @@ class Completion: if string is None: string = '' bracket_leaf = leaf - if bracket_leaf.type == 'number': + if bracket_leaf.type in ('number', 'error_leaf'): string = bracket_leaf.value bracket_leaf = bracket_leaf.get_previous_leaf() @@ -113,6 +113,8 @@ class Completion: )) if completions: return completions + if string is not None: + return prefixed_completions completion_names = self._get_context_completions(leaf) diff --git a/jedi/api/dicts.py b/jedi/api/dicts.py deleted file mode 100644 index c25f7dd9..00000000 --- a/jedi/api/dicts.py +++ /dev/null @@ -1,26 +0,0 @@ -from jedi.inference.names import AbstractArbitraryName -from jedi.api.classes import Completion - -_sentinel = object() - - -class F(AbstractArbitraryName): - api_type = u'path' - is_value_name = False - - -def completions_for_dicts(inference_state, dicts, literal_string): - for dict_key in sorted(_get_python_keys(dicts)): - dict_key_str = repr(dict_key) - if dict_key_str.startswith(literal_string): - name = F(inference_state, dict_key_str[len(literal_string):]) - yield Completion(inference_state, name, stack=None, like_name_length=0) - - -def _get_python_keys(dicts): - for dct in dicts: - if dct.array_type == 'dict': - for key in dct.get_key_values(): - dict_key = key.get_safe_value(default=_sentinel) - if dict_key is not _sentinel: - yield dict_key diff --git a/jedi/api/file_name.py b/jedi/api/file_name.py index 5871fd90..51a34765 100644 --- a/jedi/api/file_name.py +++ b/jedi/api/file_name.py @@ -1,10 +1,13 @@ import os from jedi._compatibility import FileNotFoundError, force_unicode, scandir -from jedi.inference.names import AbstractArbitraryName from jedi.api import classes +from jedi.api.strings import StringName, get_quote_ending from jedi.inference.helpers import get_str_or_none -from jedi.parser_utils import get_string_quote + + +class PathName(StringName): + api_type = u'path' def file_name_completions(inference_state, module_context, start_leaf, string, @@ -39,22 +42,13 @@ def file_name_completions(inference_state, module_context, start_leaf, string, name = entry.name if name.startswith(must_start_with): if is_in_os_path_join or not entry.is_dir(): - if start_leaf.type == 'string': - quote = get_string_quote(start_leaf) - else: - assert start_leaf.type == 'error_leaf' - quote = start_leaf.value - potential_other_quote = \ - code_lines[position[0] - 1][position[1]:position[1] + len(quote)] - # Add a quote if it's not already there. - if quote != potential_other_quote: - name += quote + name += get_quote_ending(start_leaf, code_lines, position) else: name += os.path.sep yield classes.Completion( inference_state, - FileName(inference_state, name[len(must_start_with) - like_name_length:]), + PathName(inference_state, name[len(must_start_with) - like_name_length:]), stack=None, like_name_length=like_name_length ) @@ -99,11 +93,6 @@ def _add_strings(context, nodes, add_slash=False): return string -class FileName(AbstractArbitraryName): - api_type = u'path' - is_value_name = False - - def _add_os_path_join(module_context, start_leaf, bracket_start): def check(maybe_bracket, nodes): if maybe_bracket.start_pos != bracket_start: diff --git a/jedi/api/strings.py b/jedi/api/strings.py new file mode 100644 index 00000000..ce05e211 --- /dev/null +++ b/jedi/api/strings.py @@ -0,0 +1,50 @@ +""" +This module is here for string completions. This means mostly stuff where +strings are returned, like `foo = dict(bar=3); foo["ba` would complete to +`"bar"]`. + +It however does the same for numbers. The difference between string completions +and other completions is mostly that this module doesn't return defined +names in a module, but pretty much an arbitrary string. +""" +from jedi.inference.names import AbstractArbitraryName +from jedi.api.classes import Completion +from jedi.parser_utils import get_string_quote + +_sentinel = object() + + +class StringName(AbstractArbitraryName): + api_type = u'string' + is_value_name = False + + +def completions_for_dicts(inference_state, dicts, literal_string): + for dict_key in sorted(_get_python_keys(dicts)): + dict_key_str = repr(dict_key) + if dict_key_str.startswith(literal_string): + name = StringName(inference_state, dict_key_str[len(literal_string):]) + yield Completion(inference_state, name, stack=None, like_name_length=0) + + +def _get_python_keys(dicts): + for dct in dicts: + if dct.array_type == 'dict': + for key in dct.get_key_values(): + dict_key = key.get_safe_value(default=_sentinel) + if dict_key is not _sentinel: + yield dict_key + + +def get_quote_ending(start_leaf, code_lines, position): + if start_leaf.type == 'string': + quote = get_string_quote(start_leaf) + else: + assert start_leaf.type == 'error_leaf' + quote = start_leaf.value + potential_other_quote = \ + code_lines[position[0] - 1][position[1]:position[1] + len(quote)] + # Add a quote only if it's not already there. + if quote == potential_other_quote: + return '' + return quote From 7e769b87f3646a98f5634e8b1aab486143539df1 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 30 Dec 2019 00:29:55 +0100 Subject: [PATCH 07/15] Fix some more dict tests --- jedi/api/completion.py | 26 ++++-------- jedi/api/strings.py | 73 +++++++++++++++++++++++++++++--- jedi/inference/value/instance.py | 3 ++ jedi/parser_utils.py | 4 -- test/test_api/test_completion.py | 22 ++++++---- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index b25b6b8d..deef40b6 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -10,7 +10,7 @@ from jedi import settings from jedi.api import classes from jedi.api import helpers from jedi.api import keywords -from jedi.api.strings import completions_for_dicts +from jedi.api.strings import complete_dict from jedi.api.file_name import complete_file_name from jedi.inference import imports from jedi.inference.base_value import ValueSet @@ -105,21 +105,13 @@ class Completion: leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) string, start_leaf = _extract_string_while_in_string(leaf, self._position) - prefixed_completions = [] - #if string is None: - #string = '' - bracket_leaf = leaf - #if bracket_leaf.type in ('number', 'error_leaf'): - #string = bracket_leaf.value - #bracket_leaf = bracket_leaf.get_previous_leaf() - - if bracket_leaf == '[': - context = self._module_context.create_context(bracket_leaf) - before_bracket_leaf = bracket_leaf.get_previous_leaf() - if before_bracket_leaf.type in ('atom', 'trailer', 'name'): - values = infer_call_of_leaf(context, before_bracket_leaf) - prefixed_completions += completions_for_dicts( - self._inference_state, values, string, fuzzy=fuzzy) + prefixed_completions = complete_dict( + self._module_context, + leaf, + self._original_position, + string, + fuzzy=fuzzy, + ) if string is not None and not prefixed_completions: prefixed_completions = list(complete_file_name( @@ -128,8 +120,6 @@ class Completion: self._code_lines, self._original_position, fuzzy )) - if prefixed_completions: - return prefixed_completions if string is not None: return prefixed_completions diff --git a/jedi/api/strings.py b/jedi/api/strings.py index 4b89f821..3c9d4929 100644 --- a/jedi/api/strings.py +++ b/jedi/api/strings.py @@ -7,9 +7,13 @@ It however does the same for numbers. The difference between string completions and other completions is mostly that this module doesn't return defined names in a module, but pretty much an arbitrary string. """ +import re + +from jedi._compatibility import unicode from jedi.inference.names import AbstractArbitraryName +from jedi.inference.helpers import infer_call_of_leaf from jedi.api.classes import Completion -from jedi.parser_utils import get_string_quote +from jedi.parser_utils import cut_value_at_position _sentinel = object() @@ -19,11 +23,46 @@ class StringName(AbstractArbitraryName): is_value_name = False -def completions_for_dicts(inference_state, dicts, literal_string, fuzzy): - for dict_key in sorted(_get_python_keys(dicts)): - dict_key_str = repr(dict_key) +def complete_dict(module_context, leaf, position, string, fuzzy): + if string is None: + string = '' + bracket_leaf = leaf + end_quote = '' + if bracket_leaf.type in ('number', 'error_leaf'): + string = cut_value_at_position(bracket_leaf, position) + if bracket_leaf.end_pos > position: + end_quote = _get_string_quote(string) or '' + if end_quote: + ending = cut_value_at_position( + bracket_leaf, + (position[0], position[1] + len(end_quote)) + ) + if not ending.endswith(end_quote): + end_quote = '' + + bracket_leaf = bracket_leaf.get_previous_leaf() + + if bracket_leaf == '[': + context = module_context.create_context(bracket_leaf) + before_bracket_leaf = bracket_leaf.get_previous_leaf() + if before_bracket_leaf.type in ('atom', 'trailer', 'name'): + values = infer_call_of_leaf(context, before_bracket_leaf) + return list(_completions_for_dicts( + module_context.inference_state, + values, + '' if string is None else string, + end_quote, + fuzzy=fuzzy, + )) + return [] + + +def _completions_for_dicts(inference_state, dicts, literal_string, end_quote, fuzzy): + for dict_key in sorted(_get_python_keys(dicts), key=lambda x: repr(x)): + dict_key_str = _create_repr_string(literal_string, dict_key) if dict_key_str.startswith(literal_string): - name = StringName(inference_state, dict_key_str[len(literal_string):]) + n = dict_key_str[len(literal_string):-len(end_quote) or None] + name = StringName(inference_state, n) yield Completion( inference_state, name, @@ -33,6 +72,17 @@ def completions_for_dicts(inference_state, dicts, literal_string, fuzzy): ) +def _create_repr_string(literal_string, dict_key): + if not isinstance(dict_key, (unicode, bytes)) or not literal_string: + return repr(dict_key) + + r = repr(dict_key) + prefix, quote = _get_string_prefix_and_quote(literal_string) + if quote == r[0]: + return prefix + r + return prefix + quote + r[1:-1] + quote + + def _get_python_keys(dicts): for dct in dicts: if dct.array_type == 'dict': @@ -42,9 +92,20 @@ def _get_python_keys(dicts): yield dict_key +def _get_string_prefix_and_quote(string): + match = re.match(r'(\w*)("""|\'{3}|"|\')', string) + if match is None: + return None, None + return match.group(1), match.group(2) + + +def _get_string_quote(string): + return _get_string_prefix_and_quote(string)[1] + + def get_quote_ending(start_leaf, code_lines, position): if start_leaf.type == 'string': - quote = get_string_quote(start_leaf) + quote = _get_string_quote(start_leaf) else: assert start_leaf.type == 'error_leaf' quote = start_leaf.value diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index aa6b7290..6fe5cd36 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -354,6 +354,9 @@ class TreeInstance(_BaseTreeInstance): def get_annotated_class_object(self): return self._get_annotated_class_object() or self.class_value + def get_key_values(self): + return NO_VALUES + def py__simple_getitem__(self, index): if self.array_type == 'dict': # Logic for dict({'foo': bar}) and dict(foo=bar) diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index f8537858..0f7ba429 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -293,10 +293,6 @@ def cut_value_at_position(leaf, position): return ''.join(lines) -def get_string_quote(leaf): - return re.match(r'\w*("""|\'{3}|"|\')', leaf.value).group(1) - - def _function_is_x_method(method_name): def wrapper(function_node): """ diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index cf65fb22..41da9c7d 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -175,7 +175,7 @@ current_dirname = os.path.basename(dirname(dirname(dirname(__file__)))) @pytest.mark.parametrize( 'file, code, column, expected', [ # General tests / relative paths - (None, '"comp', None, ['ile', 'lex']), # No files like comp + (None, '"comp', None, []), # No files like comp (None, '"test', None, [s]), (None, '"test', 4, ['t' + s]), ('example.py', '"test%scomp' % s, None, ['letion' + s]), @@ -273,8 +273,7 @@ def test_file_path_completions(Script, file, code, column, expected): assert [c.complete for c in comps] == expected -@pytest.mark.parametrize( - 'added_code, column, expected', [ +_dict_keys_completion_tests = [ ('ints[', 5, ['1', '50', Ellipsis]), ('ints[]', 5, ['1', '50', Ellipsis]), ('ints[1]', 5, ['1', '50', Ellipsis]), @@ -286,19 +285,20 @@ def test_file_path_completions(Script, file, code, column, expected): ('ints[5]', 6, ['0']), ('ints[50', 5, ['1', '50', Ellipsis]), ('ints[5', 6, ['0']), - ('ints[50', 6, ['']),#TODO ['0']), + ('ints[50', 6, ['0']), ('ints[50', 7, ['']), ('strs[', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), ('strs[]', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ("strs['", 6, ["asdf'", "fbar'", "foo'"]), ("strs[']", 6, ["asdf'", "fbar'", "foo'"]), ('strs["]', 6, ['asdf"', 'fbar"', 'foo"']), - ('strs["""]', 6, ['asdf', 'bar', 'foo']), + ('strs["""]', 6, ['asdf', 'fbar', 'foo']), ('strs["""]', 8, ['asdf"""', 'fbar"""', 'foo"""']), ('strs[b"]', 8, []), - ('strs[r"asd', 11, ['f"']), - ('strs[R"asd', 11, ['f"']), - ('strs[f"asd', 11, ['f"']), + ('strs[r"asd', 10, ['f"']), + ('strs[R"asd', 10, ['f"']), + ('strs[f"asd', 10, ['f"']), ('strs["f', 7, ['oo"]']), ('strs["f"', 7, ['oo']), @@ -310,7 +310,11 @@ def test_file_path_completions(Script, file, code, column, expected): ('casted["f', 9, ['3"', 'bar"', 'oo"']), ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), - ] +] + + +@pytest.mark.parametrize( + 'added_code, column, expected', _dict_keys_completion_tests ) def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_python35): code = dedent(r''' From 9fa48114259eefa244982ae74996176b5eb703db Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 30 Dec 2019 03:25:17 +0100 Subject: [PATCH 08/15] Get dict completions mostly working --- jedi/api/completion.py | 42 ++++++++++++++++++-------- jedi/api/file_name.py | 2 +- jedi/api/strings.py | 50 ++++++++++++++----------------- jedi/inference/compiled/access.py | 2 +- test/test_api/test_completion.py | 14 ++++----- 5 files changed, 60 insertions(+), 50 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index deef40b6..7fe6d665 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -96,20 +96,20 @@ class Completion: # The actual cursor position is not what we need to calculate # everything. We want the start of the name we're on. self._original_position = position - self._position = position[0], position[1] - len(self._like_name) self._signatures_callback = signatures_callback self._fuzzy = fuzzy def complete(self, fuzzy): - leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True) - string, start_leaf = _extract_string_while_in_string(leaf, self._position) + leaf = self._module_node.get_leaf_for_position(self._original_position, include_prefixes=True) + string, start_leaf, quote = _extract_string_while_in_string(leaf, self._original_position) prefixed_completions = complete_dict( self._module_context, - leaf, + self._code_lines, + start_leaf or leaf, self._original_position, - string, + None if string is None else quote + string, fuzzy=fuzzy, ) @@ -152,6 +152,10 @@ class Completion: grammar = self._inference_state.grammar self.stack = stack = None + self._position = ( + self._original_position[0], + self._original_position[1] - len(self._like_name) + ) try: self.stack = stack = helpers.get_stack_at_position( @@ -430,21 +434,33 @@ def _gather_nodes(stack): def _extract_string_while_in_string(leaf, position): if position < leaf.start_pos: - return None, None + return None, None, None if leaf.type == 'string': match = re.match(r'^\w*(\'{3}|"{3}|\'|")', leaf.value) - quote = match.group(1) + start = match.group(0) if leaf.line == position[0] and position[1] < leaf.column + match.end(): - return None, None - if leaf.end_pos[0] == position[0] and position[1] > leaf.end_pos[1] - len(quote): - return None, None - return cut_value_at_position(leaf, position)[match.end():], leaf + return None, None, None + if leaf.end_pos[0] == position[0] and position[1] > leaf.end_pos[1] - len(start): + return None, None, None + return cut_value_at_position(leaf, position)[match.end():], leaf, start leaves = [] while leaf is not None and leaf.line == position[0]: if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value): - return ''.join(l.get_code() for l in leaves), leaf + prefix_leaf = None + if not leaf.prefix: + prefix_leaf = leaf.get_previous_leaf() + if prefix_leaf is None or prefix_leaf.type != 'name' \ + or not all(c in 'rubf' for c in prefix_leaf.value.lower()): + prefix_leaf = None + + return ( + ''.join(cut_value_at_position(l, position) for l in leaves), + prefix_leaf or leaf, + ('' if prefix_leaf is None else prefix_leaf.value) + + cut_value_at_position(leaf, position), + ) leaves.insert(0, leaf) leaf = leaf.get_previous_leaf() - return None, None + return None, None, None diff --git a/jedi/api/file_name.py b/jedi/api/file_name.py index 94c5bf62..f48c14a2 100644 --- a/jedi/api/file_name.py +++ b/jedi/api/file_name.py @@ -48,7 +48,7 @@ def complete_file_name(inference_state, module_context, start_leaf, string, match = start_match(name, must_start_with) if match: if is_in_os_path_join or not entry.is_dir(): - name += get_quote_ending(start_leaf, code_lines, position) + name += get_quote_ending(start_leaf.value, code_lines, position) else: name += os.path.sep diff --git a/jedi/api/strings.py b/jedi/api/strings.py index 3c9d4929..774dd444 100644 --- a/jedi/api/strings.py +++ b/jedi/api/strings.py @@ -23,26 +23,19 @@ class StringName(AbstractArbitraryName): is_value_name = False -def complete_dict(module_context, leaf, position, string, fuzzy): - if string is None: - string = '' +def complete_dict(module_context, code_lines, leaf, position, string, fuzzy): bracket_leaf = leaf - end_quote = '' - if bracket_leaf.type in ('number', 'error_leaf'): - string = cut_value_at_position(bracket_leaf, position) - if bracket_leaf.end_pos > position: - end_quote = _get_string_quote(string) or '' - if end_quote: - ending = cut_value_at_position( - bracket_leaf, - (position[0], position[1] + len(end_quote)) - ) - if not ending.endswith(end_quote): - end_quote = '' + if bracket_leaf != '[': + bracket_leaf = leaf.get_previous_leaf() - bracket_leaf = bracket_leaf.get_previous_leaf() + cut_end_quote = '' + if string: + cut_end_quote = get_quote_ending(string, code_lines, position, invert_result=True) if bracket_leaf == '[': + if string is None and leaf is not bracket_leaf: + string = cut_value_at_position(leaf, position) + context = module_context.create_context(bracket_leaf) before_bracket_leaf = bracket_leaf.get_previous_leaf() if before_bracket_leaf.type in ('atom', 'trailer', 'name'): @@ -51,17 +44,17 @@ def complete_dict(module_context, leaf, position, string, fuzzy): module_context.inference_state, values, '' if string is None else string, - end_quote, + cut_end_quote, fuzzy=fuzzy, )) return [] -def _completions_for_dicts(inference_state, dicts, literal_string, end_quote, fuzzy): +def _completions_for_dicts(inference_state, dicts, literal_string, cut_end_quote, fuzzy): for dict_key in sorted(_get_python_keys(dicts), key=lambda x: repr(x)): dict_key_str = _create_repr_string(literal_string, dict_key) if dict_key_str.startswith(literal_string): - n = dict_key_str[len(literal_string):-len(end_quote) or None] + n = dict_key_str[len(literal_string):-len(cut_end_quote) or None] name = StringName(inference_state, n) yield Completion( inference_state, @@ -78,6 +71,8 @@ def _create_repr_string(literal_string, dict_key): r = repr(dict_key) prefix, quote = _get_string_prefix_and_quote(literal_string) + if quote is None: + return r if quote == r[0]: return prefix + r return prefix + quote + r[1:-1] + quote @@ -103,15 +98,14 @@ def _get_string_quote(string): return _get_string_prefix_and_quote(string)[1] -def get_quote_ending(start_leaf, code_lines, position): - if start_leaf.type == 'string': - quote = _get_string_quote(start_leaf) - else: - assert start_leaf.type == 'error_leaf' - quote = start_leaf.value - potential_other_quote = \ - code_lines[position[0] - 1][position[1]:position[1] + len(quote)] +def _matches_quote_at_position(code_lines, quote, position): + string = code_lines[position[0] - 1][position[1]:position[1] + len(quote)] + return string == quote + + +def get_quote_ending(string, code_lines, position, invert_result=False): + quote = _get_string_quote(string) # Add a quote only if it's not already there. - if quote == potential_other_quote: + if _matches_quote_at_position(code_lines, quote, position) != invert_result: return '' return quote diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index de762c36..20a19698 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -410,7 +410,7 @@ class DirectObjectAccess(object): return [self._create_access(module), access] def get_safe_value(self): - if type(self._obj) in (bool, bytes, float, int, str, unicode, slice): + if type(self._obj) in (bool, bytes, float, int, str, unicode, slice) or self._obj is None: return self._obj raise ValueError("Object is type %s and not simple" % type(self._obj)) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 41da9c7d..2440a17b 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -298,15 +298,15 @@ _dict_keys_completion_tests = [ ('strs[b"]', 8, []), ('strs[r"asd', 10, ['f"']), ('strs[R"asd', 10, ['f"']), - ('strs[f"asd', 10, ['f"']), + ('strs[f"asd', 10, []), - ('strs["f', 7, ['oo"]']), - ('strs["f"', 7, ['oo']), - ('strs["f]', 7, ['oo"]']), - ('strs["f"]', 7, ['oo']), + ('strs["f', 7, ['bar"', 'oo"']), + ('strs["f"', 7, ['bar', 'oo']), + ('strs["f]', 7, ['bar"', 'oo"']), + ('strs["f"]', 7, ['bar', 'oo']), - ('mixed[', 6, ['1', '1.1', 'None', "'a\sdf'", "b'foo'"]), - ('mixed[1', 6, ['', '.1']), + ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', 'None', "b'foo'", Ellipsis]), + ('mixed[1', 7, ['', '.1']), ('casted["f', 9, ['3"', 'bar"', 'oo"']), ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), From 46ac4371dfca19c8ab2bf4b0d28aed1b3e4c9ccb Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 30 Dec 2019 14:15:32 +0100 Subject: [PATCH 09/15] Make most dict completions possible --- jedi/api/completion.py | 23 ++++++++++++++++------- jedi/api/file_name.py | 4 ++-- test/test_api/test_completion.py | 8 ++++---- test/test_inference/test_docstring.py | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 7fe6d665..08f0893c 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -432,22 +432,31 @@ def _gather_nodes(stack): return nodes +_string_start = re.compile(r'^\w*(\'{3}|"{3}|\'|")') + + def _extract_string_while_in_string(leaf, position): + def return_part_of_leaf(leaf): + kwargs = {} + if leaf.line == position[0]: + kwargs['endpos'] = position[1] - leaf.column + match = _string_start.match(leaf.value, **kwargs) + start = match.group(0) + if leaf.line == position[0] and position[1] < leaf.column + match.end(): + return None, None, None + return cut_value_at_position(leaf, position)[match.end():], leaf, start + if position < leaf.start_pos: return None, None, None if leaf.type == 'string': - match = re.match(r'^\w*(\'{3}|"{3}|\'|")', leaf.value) - start = match.group(0) - if leaf.line == position[0] and position[1] < leaf.column + match.end(): - return None, None, None - if leaf.end_pos[0] == position[0] and position[1] > leaf.end_pos[1] - len(start): - return None, None, None - return cut_value_at_position(leaf, position)[match.end():], leaf, start + return return_part_of_leaf(leaf) leaves = [] while leaf is not None and leaf.line == position[0]: if leaf.type == 'error_leaf' and ('"' in leaf.value or "'" in leaf.value): + if len(leaf.value) > 1: + return return_part_of_leaf(leaf) prefix_leaf = None if not leaf.prefix: prefix_leaf = leaf.get_previous_leaf() diff --git a/jedi/api/file_name.py b/jedi/api/file_name.py index f48c14a2..11994074 100644 --- a/jedi/api/file_name.py +++ b/jedi/api/file_name.py @@ -14,7 +14,7 @@ class PathName(StringName): def complete_file_name(inference_state, module_context, start_leaf, string, like_name, signatures_callback, code_lines, position, fuzzy): # First we want to find out what can actually be changed as a name. - like_name_length = len(os.path.basename(string) + like_name) + like_name_length = len(os.path.basename(string)) addition = _get_string_additions(module_context, start_leaf) if addition is None: @@ -23,7 +23,7 @@ def complete_file_name(inference_state, module_context, start_leaf, string, # Here we use basename again, because if strings are added like # `'foo' + 'bar`, it should complete to `foobar/`. - must_start_with = os.path.basename(string) + like_name + must_start_with = os.path.basename(string) string = os.path.dirname(string) sigs = signatures_callback(*position) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 2440a17b..49408fe7 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -179,8 +179,8 @@ current_dirname = os.path.basename(dirname(dirname(dirname(__file__)))) (None, '"test', None, [s]), (None, '"test', 4, ['t' + s]), ('example.py', '"test%scomp' % s, None, ['letion' + s]), - ('example.py', 'r"comp"', None, "A LOT"), - ('example.py', 'r"tes"', None, "A LOT"), + ('example.py', 'r"comp"', None, []), + ('example.py', 'r"tes"', None, []), ('example.py', 'r"tes"', 5, ['t' + s]), ('example.py', 'r" tes"', 6, []), ('test%sexample.py' % se, 'r"tes"', 5, ['t' + s]), @@ -308,8 +308,8 @@ _dict_keys_completion_tests = [ ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', 'None', "b'foo'", Ellipsis]), ('mixed[1', 7, ['', '.1']), - ('casted["f', 9, ['3"', 'bar"', 'oo"']), - ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), + #('casted["f', 9, ['3"', 'bar"', 'oo"']), + #('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), ] diff --git a/test/test_inference/test_docstring.py b/test/test_inference/test_docstring.py index 42042c22..e45b141f 100644 --- a/test/test_inference/test_docstring.py +++ b/test/test_inference/test_docstring.py @@ -88,7 +88,7 @@ def test_multiple_docstrings(Script): def test_completion(Script): - assert Script(''' + assert not Script(''' class DocstringCompletion(): #? [] """ asdfas """''').complete() From 94a97ff8e8fa2b61c5a5180169c36b657df9462e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 30 Dec 2019 22:59:01 +0100 Subject: [PATCH 10/15] Fix remaining issues with dict completions --- jedi/api/completion.py | 5 ++++- jedi/inference/value/instance.py | 17 ++++++++++++++++- test/test_api/test_completion.py | 13 +++++++++++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 08f0893c..e155cc1a 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -101,7 +101,10 @@ class Completion: self._fuzzy = fuzzy def complete(self, fuzzy): - leaf = self._module_node.get_leaf_for_position(self._original_position, include_prefixes=True) + leaf = self._module_node.get_leaf_for_position( + self._original_position, + include_prefixes=True + ) string, start_leaf, quote = _extract_string_while_in_string(leaf, self._original_position) prefixed_completions = complete_dict( diff --git a/jedi/inference/value/instance.py b/jedi/inference/value/instance.py index 6fe5cd36..48eaa43b 100644 --- a/jedi/inference/value/instance.py +++ b/jedi/inference/value/instance.py @@ -355,7 +355,22 @@ class TreeInstance(_BaseTreeInstance): return self._get_annotated_class_object() or self.class_value def get_key_values(self): - return NO_VALUES + values = NO_VALUES + if self.array_type == 'dict': + for i, (key, instance) in enumerate(self._arguments.unpack()): + if key is None and i == 0: + values |= ValueSet.from_sets( + v.get_key_values() + for v in instance.infer() + if v.array_type == 'dict' + ) + if key: + values |= ValueSet([compiled.create_simple_object( + self.inference_state, + key, + )]) + + return values def py__simple_getitem__(self, index): if self.array_type == 'dict': diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 49408fe7..23ab7496 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -308,8 +308,16 @@ _dict_keys_completion_tests = [ ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', 'None', "b'foo'", Ellipsis]), ('mixed[1', 7, ['', '.1']), - #('casted["f', 9, ['3"', 'bar"', 'oo"']), - #('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'uuu"', 'ull"']), + ('casted["f', 9, ['3"', 'bar"', 'oo"']), + ('casted["f"', 9, ['3', 'bar', 'oo']), + ('casted["f3', 10, ['"']), + ('casted["f3"', 10, ['']), + ('casted_mod["f', 13, ['3"', 'bar"', 'oo"', 'ull"', 'uuu"']), + + ('keywords["', None, ['a"']), + ('keywords[Non', None, ['e']), + ('keywords[Fa', None, ['lse']), + ('keywords[str', None, ['', 's']), ] @@ -326,6 +334,7 @@ def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_py casted_mod = dict(casted) casted_mod["fuuu"] = 8 casted_mod["full"] = 8 + keywords = {None: 1, False: 2, "a": 3} ''') line = None if isinstance(column, tuple): From ca13c4478838a6409cea069ddef73a847adc3f10 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 31 Dec 2019 11:15:23 +0100 Subject: [PATCH 11/15] Make sure to avoid duplicates in completions --- jedi/api/completion.py | 32 +++++++++++++++++++++----------- jedi/api/strings.py | 4 ++-- test/test_api/test_completion.py | 9 ++++----- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index e155cc1a..18cd8082 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -60,6 +60,11 @@ def filter_names(inference_state, completion_names, stack, like_name, fuzzy): yield new +def _remove_duplicates(completions, other_completions): + names = {d.name for d in other_completions} + return [c for c in completions if c.name not in names] + + def get_user_context(module_context, position): """ Returns the scope in which the user resides. This includes flows. @@ -128,14 +133,15 @@ class Completion: completion_names = self._complete_python(leaf) - completions = filter_names(self._inference_state, completion_names, - self.stack, self._like_name, fuzzy) + completions = list(filter_names(self._inference_state, completion_names, + self.stack, self._like_name, fuzzy)) return ( - prefixed_completions + - sorted(completions, key=lambda x: (x.name.startswith('__'), - x.name.startswith('_'), - x.name.lower())) + # Removing duplicates mostly to remove False/True/None duplicates. + _remove_duplicates(prefixed_completions, completions) + + sorted(completions, key=lambda x: (x.name.startswith('__'), + x.name.startswith('_'), + x.name.lower())) ) def _complete_python(self, leaf): @@ -211,9 +217,12 @@ class Completion: completion_names = [] current_line = self._code_lines[self._position[0] - 1][:self._position[1]] - if not current_line or current_line[-1] in ' \t.;' \ - and current_line[-3:] != '...': - completion_names += self._complete_keywords(allowed_transitions) + + completion_names += self._complete_keywords( + allowed_transitions, + only_values=not (not current_line or current_line[-1] in ' \t.;' + and current_line[-3:] != '...') + ) if any(t in allowed_transitions for t in (PythonTokenTypes.NAME, PythonTokenTypes.INDENT)): @@ -293,10 +302,11 @@ class Completion: return complete_param_names(context, function_name.value, decorators) return [] - def _complete_keywords(self, allowed_transitions): + def _complete_keywords(self, allowed_transitions, only_values): for k in allowed_transitions: if isinstance(k, str) and k.isalpha(): - yield keywords.KeywordName(self._inference_state, k) + if not only_values or k in ('True', 'False', 'None'): + yield keywords.KeywordName(self._inference_state, k) def _complete_global_scope(self): context = get_user_context(self._module_context, self._position) diff --git a/jedi/api/strings.py b/jedi/api/strings.py index 774dd444..a2f1ee46 100644 --- a/jedi/api/strings.py +++ b/jedi/api/strings.py @@ -55,12 +55,12 @@ def _completions_for_dicts(inference_state, dicts, literal_string, cut_end_quote dict_key_str = _create_repr_string(literal_string, dict_key) if dict_key_str.startswith(literal_string): n = dict_key_str[len(literal_string):-len(cut_end_quote) or None] - name = StringName(inference_state, n) + name = StringName(inference_state, dict_key_str[:-len(cut_end_quote) or None]) yield Completion( inference_state, name, stack=None, - like_name_length=0, + like_name_length=len(literal_string), is_fuzzy=fuzzy ) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 23ab7496..4d6313d2 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -305,8 +305,9 @@ _dict_keys_completion_tests = [ ('strs["f]', 7, ['bar"', 'oo"']), ('strs["f"]', 7, ['bar', 'oo']), - ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', 'None', "b'foo'", Ellipsis]), + ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', "b'foo'", Ellipsis]), ('mixed[1', 7, ['', '.1']), + ('mixed[Non', 9, ['e']), ('casted["f', 9, ['3"', 'bar"', 'oo"']), ('casted["f"', 9, ['3', 'bar', 'oo']), @@ -317,6 +318,7 @@ _dict_keys_completion_tests = [ ('keywords["', None, ['a"']), ('keywords[Non', None, ['e']), ('keywords[Fa', None, ['lse']), + ('keywords[Tr', None, ['ue']), ('keywords[str', None, ['', 's']), ] @@ -337,14 +339,11 @@ def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_py keywords = {None: 1, False: 2, "a": 3} ''') line = None - if isinstance(column, tuple): - raise NotImplementedError - line, column = column comps = Script(code + added_code, line=line, column=column).completions() if Ellipsis in expected: # This means that global completions are part of this, so filter all of # that out. - comps = [c for c in comps if not c._name.is_value_name] + comps = [c for c in comps if not c._name.is_value_name and not c.is_keyword] expected = [e for e in expected if e is not Ellipsis] assert [c.complete for c in comps] == expected From b7a89299057c918aa274e2307683608153ee25f9 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 31 Dec 2019 11:23:54 +0100 Subject: [PATCH 12/15] Add a few more tests for dict completions --- test/test_api/test_completion.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 4d6313d2..4ff2bb4f 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -297,8 +297,11 @@ _dict_keys_completion_tests = [ ('strs["""]', 8, ['asdf"""', 'fbar"""', 'foo"""']), ('strs[b"]', 8, []), ('strs[r"asd', 10, ['f"']), + ('strs[r"asd"', 10, ['f']), ('strs[R"asd', 10, ['f"']), ('strs[f"asd', 10, []), + ('strs[br"""asd', 13, ['f"""']), + ('strs[br"""asd"""', 13, ['f']), ('strs["f', 7, ['bar"', 'oo"']), ('strs["f"', 7, ['bar', 'oo']), From 83ce8b116275999a3222c17229e3d735a82856f3 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 31 Dec 2019 14:52:44 +0100 Subject: [PATCH 13/15] Make the completions possible for Interpreter objects --- jedi/api/strings.py | 1 - jedi/inference/compiled/access.py | 17 +++++++++++++++++ jedi/inference/compiled/value.py | 10 ++++++++++ test/test_api/test_completion.py | 4 ++-- test/test_api/test_interpreter.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/jedi/api/strings.py b/jedi/api/strings.py index a2f1ee46..42ad10ec 100644 --- a/jedi/api/strings.py +++ b/jedi/api/strings.py @@ -54,7 +54,6 @@ def _completions_for_dicts(inference_state, dicts, literal_string, cut_end_quote for dict_key in sorted(_get_python_keys(dicts), key=lambda x: repr(x)): dict_key_str = _create_repr_string(literal_string, dict_key) if dict_key_str.startswith(literal_string): - n = dict_key_str[len(literal_string):-len(cut_end_quote) or None] name = StringName(inference_state, dict_key_str[:-len(cut_end_quote) or None]) yield Completion( inference_state, diff --git a/jedi/inference/compiled/access.py b/jedi/inference/compiled/access.py index 20a19698..d76fad84 100644 --- a/jedi/inference/compiled/access.py +++ b/jedi/inference/compiled/access.py @@ -417,6 +417,23 @@ class DirectObjectAccess(object): def get_api_type(self): return get_api_type(self._obj) + def get_array_type(self): + if isinstance(self._obj, dict): + return 'dict' + return None + + def get_key_paths(self): + def iter_partial_keys(): + # We could use list(keys()), but that might take a lot more memory. + for (i, k) in enumerate(self._obj.keys()): + # Limit key listing at some point. This is artificial, but this + # way we don't get stalled because of slow completions + if i > 50: + break + yield k + + return [self._create_access_path(k) for k in iter_partial_keys()] + def get_access_path_tuples(self): accesses = [create_access(self._inference_state, o) for o in self._get_objects_path()] return [(access.py__name__(), access) for access in accesses] diff --git a/jedi/inference/compiled/value.py b/jedi/inference/compiled/value.py index 9a7dffa3..10bba56c 100644 --- a/jedi/inference/compiled/value.py +++ b/jedi/inference/compiled/value.py @@ -281,6 +281,16 @@ class CompiledObject(Value): return CompiledModuleContext(self) return CompiledContext(self) + @property + def array_type(self): + return self.access_handle.get_array_type() + + def get_key_values(self): + return [ + create_from_access_path(self.inference_state, k) + for k in self.access_handle.get_key_paths() + ] + class CompiledName(AbstractNameDefinition): def __init__(self, inference_state, parent_context, name): diff --git a/test/test_api/test_completion.py b/test/test_api/test_completion.py index 4ff2bb4f..38463f1b 100644 --- a/test/test_api/test_completion.py +++ b/test/test_api/test_completion.py @@ -342,7 +342,7 @@ def test_dict_keys_completions(Script, added_code, column, expected, skip_pre_py keywords = {None: 1, False: 2, "a": 3} ''') line = None - comps = Script(code + added_code, line=line, column=column).completions() + comps = Script(code + added_code).complete(line=line, column=column) if Ellipsis in expected: # This means that global completions are part of this, so filter all of # that out. @@ -365,4 +365,4 @@ def test_fuzzy_match(): def test_ellipsis_completion(Script): - assert Script('...').completions() == [] + assert Script('...').complete() == [] diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 8b5343be..3f5aeb49 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -575,3 +575,34 @@ def test_param_annotation_completion(class_is_findable): code = 'def CallFoo(x: Foo):\n x.ba' def_, = jedi.Interpreter(code, [locals()]).complete() assert def_.name == 'bar' + + +@pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, because EOL") +@pytest.mark.parametrize( + 'code, column, expected', [ + ('strs[', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ('strs[]', 5, ["'asdf'", "'fbar'", "'foo'", Ellipsis]), + ("strs['", 6, ["asdf'", "fbar'", "foo'"]), + ("strs[']", 6, ["asdf'", "fbar'", "foo'"]), + ('strs["]', 6, ['asdf"', 'fbar"', 'foo"']), + + ('mixed[', 6, [r"'a\\sdf'", '1', '1.1', "b'foo'", Ellipsis]), + ('mixed[1', 7, ['', '.1']), + ('mixed[Non', 9, ['e']), + + ('implicit[10', None, ['00']), + ] +) +def test_dict_completion(code, column, expected): + strs = {'asdf': 1, u"""foo""": 2, r'fbar': 3} + mixed = {1: 2, 1.10: 4, None: 6, r'a\sdf': 8, b'foo': 9} + + namespaces = [locals(), {'implicit': {1000: 3}}] + comps = jedi.Interpreter(code, namespaces).complete(column=column) + if Ellipsis in expected: + # This means that global completions are part of this, so filter all of + # that out. + comps = [c for c in comps if not c._name.is_value_name and not c.is_keyword] + expected = [e for e in expected if e is not Ellipsis] + + assert [c.complete for c in comps] == expected From 5853c6790678b012205a715cad38f500f7d3af5a Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 31 Dec 2019 18:49:18 +0100 Subject: [PATCH 14/15] Write tests for dict getitem --- test/test_api/test_interpreter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index 3f5aeb49..e91523ef 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -606,3 +606,19 @@ def test_dict_completion(code, column, expected): expected = [e for e in expected if e is not Ellipsis] assert [c.complete for c in comps] == expected + + +@pytest.mark.skipif(sys.version_info[0] == 2, reason="Ignore Python 2, because EOL") +@pytest.mark.parametrize( + 'code, types', [ + ('dct[1]', ['int']), + ('dct["asdf"]', ['float']), + ('dct[r"asdf"]', ['float']), + ('dct["a"]', ['float', 'int']), + ] +) +def test_dict_getitem(code, types): + dct = {1: 2, "asdf": 1.0} + + comps = jedi.Interpreter(code, [locals()]).infer() + assert [c.name for c in comps] == types From cf26ede70248b39b06fa55d00e8be1e8a5bc6a99 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 31 Dec 2019 19:04:37 +0100 Subject: [PATCH 15/15] Add some more tests to check if getitem on stuff like dict(f=3) works --- test/completion/dynamic_arrays.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/completion/dynamic_arrays.py b/test/completion/dynamic_arrays.py index b0ac69ee..61cc3839 100644 --- a/test/completion/dynamic_arrays.py +++ b/test/completion/dynamic_arrays.py @@ -384,5 +384,14 @@ some_dct['y'] = tuple some_dct['x'] #? int() str() list tuple some_dct['unknown'] +k = 'a' #? int() -some_dct['a'] +some_dct[k] + +some_other_dct = dict(some_dct, c=set) +#? int() +some_other_dct['a'] +#? list +some_other_dct['x'] +#? set +some_other_dct['c']