diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index e77001f6a1d10bb..77b1fd1eed81abe 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -266,14 +266,14 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: trace("is_soft_keyword_used{t}", t=tokens) match tokens: case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="match"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{" | "~" | "...") ): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="match"), TI(T.NAME, string=s) ): @@ -281,14 +281,14 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: return s in keyword_first_sets_match return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="case"), TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START) | TI(T.OP, string="(" | "*" | "-" | "+" | "[" | "{") ): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="case"), TI(T.NAME, string=s) ): @@ -298,13 +298,13 @@ def is_soft_keyword_used(*tokens: TI | None) -> bool: case (TI(string="case"), TI(string="_"), TI(string=":")): return True case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":"), TI(string="type"), TI(T.NAME, string=s) ): return not keyword.iskeyword(s) case ( - None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":" | ";"), + None | TI(T.NEWLINE | T.NL | T.INDENT | T.DEDENT) | TI(string=":" | ";"), TI(string="lazy"), TI(string="import") | TI(string="from") ): diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index e87d14304c82257..1cb9961e0082ede 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -160,6 +160,67 @@ def test_gen_colors_keyword_highlighting(self): actual_highlights.append((span_text, color.tag)) self.assertEqual(actual_highlights, expected_highlights) + def test_gen_colors_soft_keywords_after_blank_lines(self): + cases = [ + ( + "def f(): pass\n", + "match -1:\n case _: pass", + ["match", "case", "_"], + ), + ( + "def f(): pass\n", + "match value:\n case _: pass", + ["match", "case", "_"], + ), + ( + "match value:\n case 0: pass\n", + " case -1: pass", + ["match", "case", "case"], + ), + ( + "match value:\n case 0: pass\n", + " case other: pass", + ["match", "case", "case"], + ), + ("def f(): pass\n", "type Alias = int", ["type"]), + ("def f(): pass\n", "lazy import mod", ["lazy"]), + ("def f(): pass\n", "lazy from mod import name", ["lazy"]), + ] + for prefix, statement, expected in cases: + for blank_line in ("\n", " \n", "# comment\n"): + code = prefix + blank_line + statement + with self.subTest(code=code): + actual = [ + code[color.span.start:color.span.end + 1] + for color in gen_colors(code) + if color.tag == "soft_keyword" + ] + self.assertEqual(actual, expected) + + def test_gen_colors_match_after_dedent(self): + for subject in ("-1", "value"): + for separator in ("", "\n", "# comment\n"): + code = ( + "if x:\n pass\n" + f"{separator}match {subject}:\n case _: pass" + ) + with self.subTest(code=code): + actual = [ + code[color.span.start:color.span.end + 1] + for color in gen_colors(code) + if color.tag == "soft_keyword" + ] + self.assertEqual(actual, ["match", "case", "_"]) + + def test_gen_colors_soft_keyword_names_in_brackets(self): + for opening, closing in ("()", "[]", "{}"): + for name in ("match", "case"): + code = f"{opening}\n{name} - 1\n{closing}" + with self.subTest(code=code): + self.assertNotIn( + "soft_keyword", [color.tag for color in gen_colors(code)] + ) + def test_gen_colors_command_highlighting(self): cases = [ # highlights bare command names (after stripping whitespaces) diff --git a/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst new file mode 100644 index 000000000000000..506c71ca82bdbae --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-20-23-30-00.gh-issue-159973.Ks9nLm.rst @@ -0,0 +1,3 @@ +Fix soft keyword highlighting after blank lines, comment-only lines, and +dedents in the :ref:`interactive shell `. Patch by Bartosz +Sławecki.