Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 115 additions & 5 deletions IPython/core/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2615,8 +2615,11 @@ def _determine_completion_context(self, line):
if is_string and not is_in_expression:
return self._CompletionContextType.GLOBAL

# If we're in a template string expression, handle specially
if is_string and is_in_expression:
# If we're in a template string expression, handle specially.
# Note that ``is_string`` may be False here for nested template strings
# (``f'{f'a.``) as the naive quote tracking sees the inner opening quote
# as closing the outer one; the recursion below sorts this out.
if is_in_expression:
# Extract the expression part - look for the last { that isn't closed
expr_start = line.rfind("{")
if expr_start >= 0:
Expand All @@ -2631,12 +2634,119 @@ def _determine_completion_context(self, line):
return self._CompletionContextType.GLOBAL

# Handle all other attribute matches np.ran, d[0].k, (a,b).count, obj._private
chain_match = re.search(r".*(.+(?<!\s)\.(?:[a-zA-Z_]\w*)?)$", line)
if chain_match:
return self._CompletionContextType.ATTRIBUTE
# A trailing dot is only an attribute access if what precedes it is a
# valid Python expression; malformed input such as ``3a.`` or ``$).``
# is not, and falls back to global completion.
target = self._extract_attribute_target(line)
if target is not None:
try:
ast.parse(target, mode="eval")
except (SyntaxError, ValueError):
pass
else:
return self._CompletionContextType.ATTRIBUTE

return self._CompletionContextType.GLOBAL

def _extract_attribute_target(self, line: str) -> str | None:
"""Extract the expression a trailing ``.attr`` would be looked up on.

Scan *line* backwards from the last dot (which must be followed by at
most an identifier and nothing else) and return the primary expression
preceding it, or ``None`` if there is no trailing attribute access or
no plausible expression in front of it.
"""
match = re.search(r"\.(?:[a-zA-Z_]\w*)?$", line)
if match is None:
return None
dot = match.start()
i = dot - 1
while i >= 0:
char = line[i]
if char in ")]":
# A call or subscript trailer, or a parenthesized/list atom;
# whatever precedes it may still be part of the expression.
i = self._skip_backwards_over_brackets(line, i)
if i is None:
return None
continue
if char == "}":
# A dict/set display; nothing may precede it.
i = self._skip_backwards_over_brackets(line, i)
if i is None:
return None
break
if char in "\"'":
# A string literal; nothing but its prefix may precede it.
i = self._skip_backwards_over_string(line, i)
if i is None:
return None
break
if char.isalnum() or char == "_":
while i >= 0 and (line[i].isalnum() or line[i] == "_"):
i -= 1
# a name or number may itself be an attribute of something else
if i >= 0 and line[i] == ".":
i -= 1
continue
break
# anything else (operator, bracket, whitespace, ...) ends the
# expression
break
return line[i + 1 : dot] or None

def _skip_backwards_over_brackets(self, line: str, i: int) -> int | None:
"""Skip a bracketed group ending at index *i* (a closing bracket).

Return the index just before the matching opening bracket, or ``None``
if the brackets are unbalanced.
"""
depth = 0
while i >= 0:
char = line[i]
if char in "\"'":
skipped = self._skip_backwards_over_string(line, i)
if skipped is None:
return None
i = skipped
continue
if char in ")]}":
depth += 1
elif char in "([{":
depth -= 1
if depth == 0:
return i - 1
if depth < 0:
return None
i -= 1
return None

def _skip_backwards_over_string(self, line: str, i: int) -> int | None:
"""Skip a string literal ending at index *i* (its closing quote).

Return the index just before the opening quote (and any string prefix),
or ``None`` if no opening quote was found.
"""
quote = line[i]
j = i - 1
while j >= 0:
if line[j] == quote:
backslashes = 0
k = j - 1
while k >= 0 and line[k] == "\\":
backslashes += 1
k -= 1
if backslashes % 2 == 0:
break
j -= 1
else:
return None
j -= 1
# skip any string prefix (r, b, u, f, t and combinations thereof)
while j >= 0 and line[j] in "bBrRuUfFtT":
j -= 1
return j

def _is_completing_in_cli_context(self, text: str) -> bool:
"""
Determine if we are completing in a CLI alias, line magic, or bang expression context.
Expand Down
5 changes: 1 addition & 4 deletions tests/test_async_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,10 +434,7 @@ def test_memory_error():
iprc("(" * 200 + ")" * 200)


@pytest.mark.xfail(reason="fail on curio 1.6 and before on Python 3.12")
@pytest.mark.skip(
reason="skip_without(curio) fails on 3.12 for now even with other skip so must uncond skip"
)
@skip_without("curio")
def test_autoawait_curio():
iprc("%autoawait curio")

Expand Down
1 change: 0 additions & 1 deletion tests/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2829,7 +2829,6 @@ def test_completion_in_cli_context(line, expected, expected_after_assignment):
ip.user_ns.pop("test_alias", None)


@pytest.mark.xfail(reason="Completion context not yet supported")
@pytest.mark.parametrize(
"line, expected",
[
Expand Down
28 changes: 18 additions & 10 deletions tests/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,12 @@ def f():
)


# From 3.13, ``set_trace()``/``breakpoint()`` stop on the line where they are
# called instead of on the next one, so one extra ``step`` is needed to reach
# the line following the ``set_trace()`` call.
SET_TRACE_STOPS_ON_CALLING_LINE = sys.version_info >= (3, 13)


def _decorator_skip_setup():
import pexpect

Expand Down Expand Up @@ -484,7 +490,6 @@ def _decorator_skip_setup():
return child


@pytest.mark.skip(reason="recently fail for unknown reason on CI")
@skip_win32
def test_decorator_skip():
"""test that decorator frames can be skipped."""
Expand All @@ -496,6 +501,11 @@ def test_decorator_skip():
child.expect("ipdb>")

child.expect("ipdb>")
if SET_TRACE_STOPS_ON_CALLING_LINE:
child.sendline("step")
child.expect_exact("step")
child.expect_exact("----> 3 bar(3, 4)")
child.expect("ipdb>")
child.sendline("step")
child.expect_exact("step")
child.expect_exact("--Call--")
Expand All @@ -509,7 +519,6 @@ def test_decorator_skip():
child.close()


@pytest.mark.skip(reason="recently fail for unknown reason on CI")
@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="issues on PyPy")
@skip_win32
def test_decorator_skip_disabled():
Expand All @@ -519,7 +528,12 @@ def test_decorator_skip_disabled():

child.expect_exact("3 bar(3, 4)")

for input_, expected in [
if SET_TRACE_STOPS_ON_CALLING_LINE:
extra_step = [("step", "----> 3 bar(3, 4)")]
else:
extra_step = []

for input_, expected in extra_step + [
("skip_predicates debuggerskip False", ""),
("skip_predicates", "debuggerskip : False"),
("step", "---> 2 def wrapped_fn"),
Expand All @@ -538,7 +552,6 @@ def test_decorator_skip_disabled():
child.close()


@pytest.mark.skip(reason="recently fail for unknown reason on CI")
@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="issues on PyPy")
@skip_win32
def test_decorator_skip_with_breakpoint():
Expand Down Expand Up @@ -582,12 +595,7 @@ def test_decorator_skip_with_breakpoint():
child.expect_exact(line)
child.sendline("")

# From 3.13, set_trace()/breakpoint() stop on the line where they're
# called, instead of the next line.
if sys.version_info >= (3, 14):
child.expect_exact(" 46 ipdb.set_trace()")
extra_step = [("step", "--> 47 bar(3, 4)")]
elif sys.version_info >= (3, 13):
if SET_TRACE_STOPS_ON_CALLING_LINE:
child.expect_exact("--> 46 ipdb.set_trace()")
extra_step = [("step", "--> 47 bar(3, 4)")]
else:
Expand Down
1 change: 0 additions & 1 deletion tests/test_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
def test_instantiation_FileLink():
"""FileLink: Test class can be instantiated"""
fl = display.FileLink("example.txt")
# TODO: remove if when only Python >= 3.6 is supported
fl = display.FileLink(pathlib.PurePath("example.txt"))


Expand Down
Loading