From 08289207f9f36640959ee9ee1961c816cd533f8c Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sat, 20 Jun 2026 11:24:09 +0200 Subject: [PATCH 1/3] Colorize commands separately in the REPL --- Lib/_colorize.py | 1 + Lib/_pyrepl/utils.py | 8 ++++++++ Lib/test/test_pyrepl/test_utils.py | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 5e0c0124e597b89..c662026fd1e5dbc 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -407,6 +407,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/utils.py b/Lib/_pyrepl/utils.py index 230dae35af665ab..e3d78a49ab8fb90 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -24,6 +24,7 @@ IDENTIFIERS_AFTER = frozenset({"def", "class"}) KEYWORD_CONSTANTS = frozenset({"True", "False", "None"}) BUILTINS = frozenset({str(name) for name in dir(builtins) if not name.startswith('_')}) +COMMANDS = frozenset({"exit", "quit", "copyright", "help", "clear"}) def THEME(**kwargs): @@ -235,6 +236,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 + and 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..100ed4ad60dff62 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -159,3 +159,28 @@ 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 + ("exit", [("exit", "command")]), + ("quit", [("quit", "command")]), + ("copyright", [("copyright", "command")]), + ("help", [("help", "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) From 3f9966c9d982ca549f339387acdfac6cbbedabed Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 21 Jun 2026 02:06:00 +0200 Subject: [PATCH 2/3] Add news entry --- .../Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-21-00-00-57.gh-issue-151822.bOC2G56F.rst 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. From a4dd50849e39f01e52b7ebde3fe1da15f5ed1f4a Mon Sep 17 00:00:00 2001 From: johnslavik Date: Sun, 21 Jun 2026 02:09:55 +0200 Subject: [PATCH 3/3] Fix type check error --- Lib/_pyrepl/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index e3d78a49ab8fb90..1e5fa25a0b5c2f5 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -239,7 +239,7 @@ def gen_colors_from_token_stream( elif ( token.string in COMMANDS and not prev_token - and next_token.type == T.NEWLINE + and (not next_token or next_token.type == T.NEWLINE) ): span = Span.from_token(token, line_lengths) yield ColorSpan(span, "command")