diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 23b8fa6b9c7625..fd3d37fca8dd05 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -40,6 +40,7 @@ from .completing_reader import CompletingReader from .console import Console as ConsoleType from ._module_completer import ModuleCompleter, make_default_module_completer +from .utils import gen_colors Console: type[ConsoleType] _error: tuple[type[Exception], ...] | type[Exception] @@ -253,23 +254,22 @@ def _get_first_indentation(buffer: list[str]) -> str | None: def _should_auto_indent(buffer: list[str], pos: int) -> bool: - # check if last character before "pos" is a colon, ignoring - # whitespaces and comments. - last_char = None - while pos > 0: - pos -= 1 - if last_char is None: - if buffer[pos] not in " \t\n#": # ignore whitespaces and comments - last_char = buffer[pos] - else: - # even if we found a non-whitespace character before - # original pos, we keep going back until newline is reached - # to make sure we ignore comments - if buffer[pos] == "\n": - break - if buffer[pos] == "#": - last_char = None - return last_char == ":" + buffer_str = ''.join(buffer) + colors = tuple(gen_colors(buffer_str)) + string_spans = tuple(c.span for c in colors if c.tag == "string") + comment_spans = tuple(c.span for c in colors if c.tag == "comment") + def in_span(i, spans): + return any(s.start <= i <= s.end for s in spans) + i = pos - 1 + while i >= 0: + if buffer_str[i] in " \t\n": + i -= 1 + continue + if in_span(i, string_spans) or in_span(i, comment_spans): + i -= 1 + continue + break + return i >= 0 and buffer_str[i] == ":" class maybe_accept(commands.Command): diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 35a1733787e7a2..9980b70782f66f 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -594,6 +594,27 @@ def test_auto_indent_with_comment(self): output = multiline_input(reader) self.assertEqual(output, output_code) + # fmt: off + events = itertools.chain( + code_to_events("def f():\n"), + [ + Event(evt="key", data="backspace", raw=b"\x08"), + ], + code_to_events("# foo\npass\n\n") + ) + + output_code = ( + "def f():\n" + "# foo\n" + " pass\n" + " " + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + def test_auto_indent_with_multicomment(self): # fmt: off events = code_to_events( @@ -627,6 +648,43 @@ def test_auto_indent_ignore_comments(self): output = multiline_input(reader) self.assertEqual(output, output_code) + def test_dont_indent_hashtag(self): + # fmt: off + events = code_to_events( + "if ' ' == '#':\n" + "pass\n\n" + ) + + output_code = ( + "if ' ' == '#':\n" + " pass\n" + " " + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + + def test_dont_indent_in_multiline_string(self): + # fmt: off + events = code_to_events( + "s = '''\n" + "Note:\n" + "'''\n\n" + ) + + output_code = ( + "s = '''\n" + "Note:\n" + "'''" + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + class TestPyReplOutput(ScreenEqualMixin, TestCase): def prepare_reader(self, events): diff --git a/Misc/NEWS.d/next/Library/2025-10-28-23-14-16.gh-issue-133710.BzRnmu.rst b/Misc/NEWS.d/next/Library/2025-10-28-23-14-16.gh-issue-133710.BzRnmu.rst new file mode 100644 index 00000000000000..fd2bb3f0d98df6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-28-23-14-16.gh-issue-133710.BzRnmu.rst @@ -0,0 +1 @@ +Enhance auto-indent in :mod:`!_pyrepl`.