Skip to content

Commit 83cb2fa

Browse files
krassowskimeeseeksmachine
authored andcommitted
Backport PR ipython#14898: Fix attribute completion for expressions with comparison operators
1 parent 73bc525 commit 83cb2fa

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

IPython/core/completer.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1145,16 +1145,61 @@ def attr_matches(self, text):
11451145
# we simple attribute matching with normal identifiers.
11461146
_ATTR_MATCH_RE = re.compile(r"(.+)\.(\w*)$")
11471147

1148+
def _strip_code_before_operator(self, code: str) -> str:
1149+
o_parens = {"(", "[", "{"}
1150+
c_parens = {")", "]", "}"}
1151+
1152+
# Dry-run tokenize to catch errors
1153+
try:
1154+
_ = list(tokenize.generate_tokens(iter(code.splitlines()).__next__))
1155+
except tokenize.TokenError:
1156+
# Try trimming the expression and retrying
1157+
trimmed_code = self._trim_expr(code)
1158+
try:
1159+
_ = list(
1160+
tokenize.generate_tokens(iter(trimmed_code.splitlines()).__next__)
1161+
)
1162+
code = trimmed_code
1163+
except tokenize.TokenError:
1164+
return code
1165+
1166+
tokens = _parse_tokens(code)
1167+
encountered_operator = False
1168+
after_operator = []
1169+
nesting_level = 0
1170+
1171+
for t in tokens:
1172+
if t.type == tokenize.OP:
1173+
if t.string in o_parens:
1174+
nesting_level += 1
1175+
elif t.string in c_parens:
1176+
nesting_level -= 1
1177+
elif t.string != "." and nesting_level == 0:
1178+
encountered_operator = True
1179+
after_operator = []
1180+
continue
1181+
1182+
if encountered_operator:
1183+
after_operator.append(t.string)
1184+
1185+
if encountered_operator:
1186+
return "".join(after_operator)
1187+
else:
1188+
return code
1189+
11481190
def _attr_matches(
11491191
self, text: str, include_prefix: bool = True
11501192
) -> Tuple[Sequence[str], str]:
11511193
m2 = self._ATTR_MATCH_RE.match(text)
11521194
if not m2:
11531195
return [], ""
11541196
expr, attr = m2.group(1, 2)
1197+
try:
1198+
expr = self._strip_code_before_operator(expr)
1199+
except tokenize.TokenError:
1200+
pass
11551201

11561202
obj = self._evaluate_expr(expr)
1157-
11581203
if obj is not_found:
11591204
return [], ""
11601205

IPython/core/tests/test_completer.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,9 @@ def test_greedy_completions(self):
595595
"""
596596
ip = get_ipython()
597597
ip.ex("a=list(range(5))")
598+
ip.ex("b,c = 1, 1.2")
598599
ip.ex("d = {'a b': str}")
600+
ip.ex("x=y='a'")
599601
_, c = ip.complete(".", line="a[0].")
600602
self.assertFalse(".real" in c, "Shouldn't have completed on a[0]: %s" % c)
601603

@@ -653,6 +655,41 @@ def _(line, cursor_pos, expect, message, completion):
653655
"Should have completed on `a.app`: %s",
654656
Completion(2, 4, "append"),
655657
)
658+
_(
659+
"x.upper() == y.",
660+
15,
661+
".upper",
662+
"Should have completed on `x.upper() == y.`: %s",
663+
Completion(15, 15, "upper"),
664+
)
665+
_(
666+
"(x.upper() == y.",
667+
16,
668+
".upper",
669+
"Should have completed on `(x.upper() == y.`: %s",
670+
Completion(16, 16, "upper"),
671+
)
672+
_(
673+
"(x.upper() == y).",
674+
17,
675+
".bit_length",
676+
"Should have completed on `(x.upper() == y).`: %s",
677+
Completion(17, 17, "bit_length"),
678+
)
679+
_(
680+
"{'==', 'abc'}.",
681+
14,
682+
".add",
683+
"Should have completed on `{'==', 'abc'}.`: %s",
684+
Completion(14, 14, "add"),
685+
)
686+
_(
687+
"b + c.",
688+
6,
689+
".hex",
690+
"Should have completed on `b + c.`: %s",
691+
Completion(6, 6, "hex"),
692+
)
656693

657694
def test_omit__names(self):
658695
# also happens to test IPCompleter as a configurable

0 commit comments

Comments
 (0)