From 568e32d2a535117005db58025a42eaa7651e9571 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Sat, 23 May 2026 22:27:02 -0500 Subject: [PATCH 1/4] Add support for the Python 3.15+ `lazy` keyword New in Python 3.15, there is a `lazy` keyword which allows for deferred imports. Also update the demo to make use of `lazy`, type annotations, and `None`, all of which make the demo a bit more complete. --- lib/rouge/demos/python | 13 +++++++++++++ lib/rouge/lexers/python.rb | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/rouge/demos/python b/lib/rouge/demos/python index 77a5cb3111..2f783f0c53 100644 --- a/lib/rouge/demos/python +++ b/lib/rouge/demos/python @@ -1,6 +1,19 @@ +lazy import re + + def fib(n): # write Fibonacci series up to n """Print a Fibonacci series up to n.""" a, b = 0, 1 while a < n: print a, a, b = b, a+b + + +_ANSI_RE = None + +def strip_ansi(s: str) -> str: + """Remove ANSI escapes from a string.""" + if _ANSI_RE is None: + global _ANSI_RE + _ANSI_RE = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + return _ANSI_RE.sub("", s) diff --git a/lib/rouge/lexers/python.rb b/lib/rouge/lexers/python.rb index 39619363f2..1a727ac1af 100644 --- a/lib/rouge/lexers/python.rb +++ b/lib/rouge/lexers/python.rb @@ -21,7 +21,7 @@ def self.keywords assert break continue del elif else except exec finally for global if lambda pass print raise return try while yield as with from import - async await nonlocal + async await nonlocal lazy ) end From 8536b75ebb68ae7cf38e4270062514d0d69c635e Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 29 May 2026 17:36:00 -0500 Subject: [PATCH 2/4] Fix handling of lazy soft-keyword Account for the fact that `lazy` is also a valid identifier. Capture that you know it's a keyword *iff* the next token is `from` or `import`. Since there's no valid context in Python for putting two identifiers adjacent without an operator, we don't need to worry about what comes after `from` or `import`. --- lib/rouge/lexers/python.rb | 7 ++++++- spec/visual/samples/python | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/rouge/lexers/python.rb b/lib/rouge/lexers/python.rb index 1a727ac1af..9f8b540086 100644 --- a/lib/rouge/lexers/python.rb +++ b/lib/rouge/lexers/python.rb @@ -21,7 +21,7 @@ def self.keywords assert break continue del elif else except exec finally for global if lambda pass print raise return try while yield as with from import - async await nonlocal lazy + async await nonlocal ) end @@ -115,6 +115,11 @@ def current_string rule %r/class\b/, Keyword, :classname + # handle `lazy import` and `from ... lazy import ...` soft keyword usage + # since `import` and `from` are hard keywords, we know that this usage + # has to be the soft-keyword case + rule %r/lazy(?=#{inline_ws}(from|import))/, Keyword + rule %r/`.*?`/, Str::Backtick rule %r/([rtfbu]{0,2})('''|"""|['"])/i do |m| groups Str::Affix, Str::Heredoc diff --git a/spec/visual/samples/python b/spec/visual/samples/python index af07996312..f28436dda0 100644 --- a/spec/visual/samples/python +++ b/spec/visual/samples/python @@ -262,3 +262,13 @@ match (100, 200): thing \ : other + + +lazy = "deferred" +lazy import requests +lazy \ + import \ + re +from typing lazy import Iterator +from typing lazy \ + import Generator From 05f0e8614035a4b519380451c0b9a6ffdd16f4d4 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Fri, 29 May 2026 17:48:54 -0500 Subject: [PATCH 3/4] Fix `lazy` keyword breaking `from import` styling The `from_import` state lexing now accounts for `lazy import` being the next tokens. --- lib/rouge/lexers/python.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/rouge/lexers/python.rb b/lib/rouge/lexers/python.rb index 9f8b540086..7080a95bf7 100644 --- a/lib/rouge/lexers/python.rb +++ b/lib/rouge/lexers/python.rb @@ -115,10 +115,10 @@ def current_string rule %r/class\b/, Keyword, :classname - # handle `lazy import` and `from ... lazy import ...` soft keyword usage - # since `import` and `from` are hard keywords, we know that this usage - # has to be the soft-keyword case - rule %r/lazy(?=#{inline_ws}(from|import))/, Keyword + # handle `lazy import` soft keyword usage + # since `import` is a hard keyword, we know that this usage has to be + # the soft-keyword case + rule %r/lazy(?=#{inline_ws}import)/, Keyword rule %r/`.*?`/, Str::Backtick rule %r/([rtfbu]{0,2})('''|"""|['"])/i do |m| @@ -178,6 +178,7 @@ def current_string # import after from, meaning we don't push the :import state state :from_import do mixin :inline_whitespace + rule %r/lazy\b/, Keyword::Namespace rule %r/import\b/, Keyword::Namespace, :pop! rule(//) { pop! } end From 00dea0b238845282152a7a640624067c2e631f2b Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Thu, 4 Jun 2026 16:25:13 -0500 Subject: [PATCH 4/4] Fix word boundaries for Python lazy keyword Co-authored-by: Jeanine Adkisson <225017+jneen@users.noreply.github.com> --- lib/rouge/lexers/python.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rouge/lexers/python.rb b/lib/rouge/lexers/python.rb index 7080a95bf7..884bb23e4f 100644 --- a/lib/rouge/lexers/python.rb +++ b/lib/rouge/lexers/python.rb @@ -118,7 +118,7 @@ def current_string # handle `lazy import` soft keyword usage # since `import` is a hard keyword, we know that this usage has to be # the soft-keyword case - rule %r/lazy(?=#{inline_ws}import)/, Keyword + rule %r/lazy\b(?=#{inline_ws}import\b)/, Keyword rule %r/`.*?`/, Str::Backtick rule %r/([rtfbu]{0,2})('''|"""|['"])/i do |m|