https://github.com/python/cpython/commit/bc2fd447d20d6840f4d7feb3cb8c35ee4e2aaa06
commit: bc2fd447d20d6840f4d7feb3cb8c35ee4e2aaa06
branch: main
author: Bartosz SÅ‚awecki <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-07-18T13:17:51+02:00
summary:

gh-151822: Colorize commands separately in the REPL (#151823)

files:
A Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst
M Lib/_colorize.py
M Lib/_pyrepl/simple_interact.py
M Lib/_pyrepl/utils.py
M Lib/test/test_pyrepl/test_utils.py

diff --git a/Lib/_colorize.py b/Lib/_colorize.py
index 27eb7f13baca971..5f44a3aa05eb8f6 100644
--- a/Lib/_colorize.py
+++ b/Lib/_colorize.py
@@ -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
diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py
index c169d0191bd8333..da557b254e52c48 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -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' ,''),
diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py
index 230dae35af665ab..8d68f4f66f2980e 100644
--- a/Lib/_pyrepl/utils.py
+++ b/Lib/_pyrepl/utils.py
@@ -24,6 +24,8 @@
 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)
diff --git a/Lib/test/test_pyrepl/test_utils.py 
b/Lib/test/test_pyrepl/test_utils.py
index ebbd06213c69aff..e87d14304c82257 100644
--- a/Lib/test/test_pyrepl/test_utils.py
+++ b/Lib/test/test_pyrepl/test_utils.py
@@ -159,3 +159,29 @@ def test_gen_colors_keyword_highlighting(self):
                     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
index 000000000000000..70e7323c7e2ef6a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst
@@ -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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to