Skip to content

Commit 8c1cf78

Browse files
committed
Add tests for completer and implement completion of variable names
1 parent dc3111a commit 8c1cf78

2 files changed

Lines changed: 124 additions & 4 deletions

File tree

IPython/core/completer.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,16 @@ def complete(self, text, state):
11271127
except IndexError:
11281128
return None
11291129

1130-
def global_matches(self, text):
1130+
def global_matches(self, text: str):
1131+
"""Compute matches when text is a simple name.
1132+
1133+
Return a list of all keywords, built-in functions and names currently
1134+
defined in self.namespace or self.global_namespace that match.
1135+
1136+
"""
1137+
return self._global_matches(text)
1138+
1139+
def _global_matches(self, text: str, context: Optional[CompletionContext] = None):
11311140
"""Compute matches when text is a simple name.
11321141
11331142
Return a list of all keywords, built-in functions and names currently
@@ -1137,12 +1146,41 @@ def global_matches(self, text):
11371146
matches = []
11381147
match_append = matches.append
11391148
n = len(text)
1140-
for lst in [
1149+
1150+
search_lists = [
11411151
keyword.kwlist,
11421152
builtin_mod.__dict__.keys(),
11431153
list(self.namespace.keys()),
11441154
list(self.global_namespace.keys()),
1145-
]:
1155+
]
1156+
if context and context.full_text.count("\n") > 1:
1157+
# try to evaluate on full buffer
1158+
previous_lines = "\n".join(
1159+
context.full_text.split("\n")[: context.cursor_line]
1160+
)
1161+
if previous_lines:
1162+
all_code_lines_before_cursor = (
1163+
self._extract_code(previous_lines) + "\n" + text
1164+
)
1165+
context = EvaluationContext(
1166+
globals=self.global_namespace,
1167+
locals=self.namespace,
1168+
evaluation=self.evaluation,
1169+
auto_import=self._auto_import,
1170+
policy_overrides=self.policy_overrides,
1171+
)
1172+
try:
1173+
obj = guarded_eval(
1174+
all_code_lines_before_cursor,
1175+
context,
1176+
)
1177+
except Exception as e:
1178+
if self.debug:
1179+
warnings.warn(f"Evaluation exception {e}")
1180+
1181+
search_lists.append(list(context.transient_locals.keys()))
1182+
1183+
for lst in search_lists:
11461184
for word in lst:
11471185
if word[:n] == text and word != "__builtins__":
11481186
match_append(word)
@@ -1157,6 +1195,7 @@ def global_matches(self, text):
11571195
for word in shortened.keys():
11581196
if word[:n] == text and word != "__builtins__":
11591197
match_append(shortened[word])
1198+
11601199
return matches
11611200

11621201
def attr_matches(self, text):
@@ -2718,7 +2757,7 @@ def python_matcher(self, context: CompletionContext) -> SimpleMatcherResult:
27182757
# catches <undefined attributes>.<tab>
27192758
return SimpleMatcherResult(completions=[], suppress=False)
27202759
else:
2721-
matches = self.global_matches(context.token)
2760+
matches = self._global_matches(context.token, context)
27222761
# TODO: maybe distinguish between functions, modules and just "variables"
27232762
return SimpleMatcherResult(
27242763
completions=[

tests/test_completer.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,6 +1974,87 @@ def _(expected):
19741974
_(["completion_a"])
19751975

19761976

1977+
@pytest.mark.parametrize(
1978+
"use_jedi,evaluation",
1979+
[
1980+
[True, "minimal"],
1981+
[False, "limited"],
1982+
],
1983+
)
1984+
@pytest.mark.parametrize(
1985+
"code,insert_text",
1986+
[
1987+
[
1988+
"\n".join(
1989+
[
1990+
"class NotYetDefined:",
1991+
" def my_method(self) -> str:",
1992+
" return 1",
1993+
"my_instance = NotYetDefined()",
1994+
"my_insta",
1995+
]
1996+
),
1997+
"my_instance",
1998+
],
1999+
[
2000+
"\n".join(
2001+
[
2002+
"class NotYetDefined:",
2003+
" def my_method(self) -> str:",
2004+
" return 1",
2005+
"instance = NotYetDefined()",
2006+
"instance.",
2007+
]
2008+
),
2009+
"my_method",
2010+
],
2011+
[
2012+
"\n".join(
2013+
[
2014+
"class NotYetDefined:",
2015+
" def my_method(self) -> str:",
2016+
" return 1",
2017+
"my_instance = NotYetDefined()",
2018+
"my_instance.my_method().",
2019+
]
2020+
),
2021+
"capitalize",
2022+
],
2023+
[
2024+
"\n".join(
2025+
[
2026+
"my_instance = 1.1",
2027+
"assert my_instance.",
2028+
]
2029+
),
2030+
"as_integer_ratio",
2031+
],
2032+
[
2033+
"\n".join(
2034+
[
2035+
"def my_test() -> float:",
2036+
" pass",
2037+
"my_test().",
2038+
]
2039+
),
2040+
"as_integer_ratio",
2041+
],
2042+
],
2043+
)
2044+
def test_undefined_variables(use_jedi, evaluation, code, insert_text):
2045+
offset = len(code)
2046+
ip.Completer.use_jedi = use_jedi
2047+
ip.Completer.evaluation = evaluation
2048+
2049+
with provisionalcompleter():
2050+
completions = list(ip.Completer.completions(text=code, offset=offset))
2051+
match = [c for c in completions if c.text.lstrip(".") == insert_text]
2052+
message_on_fail = (
2053+
f"{insert_text} not found among {[c.text for c in completions]}"
2054+
)
2055+
assert len(match) == 1, message_on_fail
2056+
2057+
19772058
@pytest.mark.parametrize(
19782059
"line,expected",
19792060
[

0 commit comments

Comments
 (0)