diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 560a9db192169e..2e943a33f0d0ce 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -254,20 +254,53 @@ 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 == ":" + # A stack to keep track of string delimiters (quotes). Push a quote when + # entering a string, and pop it when the string ends. When the stack is + # empty, we're not inside a string. If encounter a '#' while not inside a + # string, it's a comment start; otherwise, it's just a '#' character within + # a string. + in_string: list[str] = [] + in_comment = False + char_line_indent_start = None + char_line_indent = 0 + lastchar_line_indent = 0 + cursor_line_indent = 0 + + i = -1 + while i < pos - 1: + i += 1 + char = buffer[i] + + # update last_char + if char == "#": + if in_string: + last_char = char # '#' inside a string is just a character + else: + in_comment = True + elif char == "\n": + # newline ends a comment + in_comment = False + if i < pos - 1 and buffer[i + 1] in " \t": + char_line_indent_start = i + 1 + else: + char_line_indent_start = None # clear last line's line_indent_start + char_line_indent = 0 + elif char not in " \t": + if char_line_indent_start is not None: + char_line_indent = i - char_line_indent_start + if not in_comment and not in_string: + # update last_char with non-whitespace chars outside comments and strings + last_char = char + lastchar_line_indent = char_line_indent + + # update stack + if char in "\"'" and (i == 0 or buffer[i - 1] != "\\"): + if in_string and in_string[-1] == char: + in_string.pop() + else: + in_string.append(char) + cursor_line_indent = char_line_indent + return last_char == ":" and cursor_line_indent <= lastchar_line_indent class maybe_accept(commands.Command): diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index fc8114891d12dd..ec5a30945f95e5 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -572,6 +572,46 @@ def test_auto_indent_with_comment(self): output = multiline_input(reader) self.assertEqual(output, output_code) + # fmt: off + events = code_to_events( + "def f():\n" + "# foo\n" + "pass\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) + + # 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( @@ -605,6 +645,43 @@ def test_auto_indent_ignore_comments(self): output = multiline_input(reader) self.assertEqual(output, output_code) + def test_auto_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_auto_indent_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):