]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151822: Colorize commands separately in the REPL (#151823)
authorBartosz Sławecki <bartosz@ilikepython.com>
Sat, 18 Jul 2026 11:17:51 +0000 (13:17 +0200)
committerGitHub <noreply@github.com>
Sat, 18 Jul 2026 11:17:51 +0000 (13:17 +0200)
Lib/_colorize.py
Lib/_pyrepl/simple_interact.py
Lib/_pyrepl/utils.py
Lib/test/test_pyrepl/test_utils.py
Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst [new file with mode: 0644]

index 27eb7f13baca97163e88bfc86fa6242e880a7696..5f44a3aa05eb8f654edf04dba04c4bdbd43c2bc8 100644 (file)
@@ -405,6 +405,7 @@ class Syntax(ThemeSection):
     keyword: str = ANSIColors.BOLD_BLUE
     keyword_constant: str = ANSIColors.BOLD_BLUE
     builtin: str = ANSIColors.CYAN
+    command: str = ANSIColors.BOLD_CYAN
     comment: str = ANSIColors.RED
     string: str = ANSIColors.GREEN
     number: str = ANSIColors.YELLOW
index c169d0191bd8333c9aca83e42d124b56587bc9c6..da557b254e52c48404e900989a69a6be438a0d9d 100644 (file)
@@ -68,6 +68,7 @@ def _clear_screen():
     reader.scheduled_commands.append("clear_screen")
 
 
+# Keep this in sync with _pyrepl.utils.COMMANDS
 REPL_COMMANDS = {
     "exit": _sitebuiltins.Quitter('exit', ''),
     "quit": _sitebuiltins.Quitter('quit' ,''),
index 230dae35af665ab0226393a4c1db729e6eab46a7..8d68f4f66f2980ee4bb8715784c71334eeb0198f 100644 (file)
@@ -24,6 +24,8 @@ ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})
 IDENTIFIERS_AFTER = frozenset({"def", "class"})
 KEYWORD_CONSTANTS = frozenset({"True", "False", "None"})
 BUILTINS = frozenset({str(name) for name in dir(builtins) if not name.startswith('_')})
+# Keep this in sync with _pyrepl.simple_interact.REPL_COMMANDS
+COMMANDS = frozenset({"exit", "quit", "copyright", "help", "clear"})
 
 
 def THEME(**kwargs):
@@ -235,6 +237,13 @@ def gen_colors_from_token_stream(
                 ):
                     span = Span.from_token(token, line_lengths)
                     yield ColorSpan(span, "soft_keyword")
+                elif (
+                    token.string in COMMANDS
+                    and (not prev_token or prev_token.type == T.INDENT)
+                    and (not next_token or next_token.type == T.NEWLINE)
+                ):
+                    span = Span.from_token(token, line_lengths)
+                    yield ColorSpan(span, "command")
                 elif (
                     token.string in BUILTINS
                     and not (prev_token and prev_token.exact_type == T.DOT)
index ebbd06213c69affac2a4860388ddf7abff653b01..e87d14304c82257f3f25fce8abb30b42bc9cad82 100644 (file)
@@ -159,3 +159,29 @@ class TestUtils(TestCase):
                     span_text = code[color.span.start:color.span.end + 1]
                     actual_highlights.append((span_text, color.tag))
                 self.assertEqual(actual_highlights, expected_highlights)
+
+    def test_gen_colors_command_highlighting(self):
+        cases = [
+            # highlights bare command names (after stripping whitespaces)
+            ("exit", [("exit", "command")]),
+            ("quit", [("quit", "command")]),
+            ("copyright", [("copyright", "command")]),
+            ("help", [("help", "command")]),
+            ("clear", [("clear", "command")]),
+            ("  clear ", [("clear", "command")]),
+            # no highlight when not the only token on the line
+            ("x = exit", [("=", "op"), ("exit", "builtin")]),
+            ("obj.exit", [(".", "op")]),
+            # falls through to builtin when called as function or used in expression
+            ("exit()", [("exit", "builtin"), ("(", "op"), (")", "op")]),
+            ("quit(0)", [("quit", "builtin"), ("(", "op"), ("0", "number"), (")", "op")]),
+            ("print(exit)", [("print", "builtin"), ("(", "op"), ("exit", "builtin"), (")", "op")]),
+        ]
+        for code, expected_highlights in cases:
+            with self.subTest(code=code):
+                colors = list(gen_colors(code))
+                actual_highlights = []
+                for color in colors:
+                    span_text = code[color.span.start:color.span.end + 1]
+                    actual_highlights.append((span_text, color.tag))
+                self.assertEqual(actual_highlights, expected_highlights)
diff --git a/Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst b/Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst
new file mode 100644 (file)
index 0000000..70e7323
--- /dev/null
@@ -0,0 +1 @@
+Colorize ``exit``, ``quit``, ``copyright``, ``help``, and ``clear`` as commands in the :term:`REPL` when typed alone on a line. Patch by Bartosz Sławecki.