Skip to content

Commit 60d343a

Browse files
authored
bpo-44010: IDLE: colorize pattern-matching soft keywords (pythonGH-25851)
1 parent d798acc commit 60d343a

File tree

6 files changed

+345
-73
lines changed

6 files changed

+345
-73
lines changed

Doc/library/idle.rst

+6
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,12 @@ keywords, builtin class and function names, names following ``class`` and
613613
``def``, strings, and comments. For any text window, these are the cursor (when
614614
present), found text (when possible), and selected text.
615615

616+
IDLE also highlights the :ref:`soft keywords <soft-keywords>` :keyword:`match`,
617+
:keyword:`case <match>`, and :keyword:`_ <wildcard-patterns>` in
618+
pattern-matching statements. However, this highlighting is not perfect and
619+
will be incorrect in some rare cases, including some ``_``-s in ``case``
620+
patterns.
621+
616622
Text coloring is done in the background, so uncolorized text is occasionally
617623
visible. To change the color scheme, use the Configure IDLE dialog
618624
Highlighting tab. The marking of debugger breakpoint lines in the editor and

Doc/whatsnew/3.10.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,12 @@ Terry Jan Reedy in :issue:`37892`.)
10301030
We expect to backport these shell changes to a future 3.9 maintenance
10311031
release.
10321032
1033+
Highlight the new :ref:`soft keywords <soft-keywords>` :keyword:`match`,
1034+
:keyword:`case <match>`, and :keyword:`_ <wildcard-patterns>` in
1035+
pattern-matching statements. However, this highlighting is not perfect
1036+
and will be incorrect in some rare cases, including some ``_``-s in
1037+
``case`` patterns. (Contributed by Tal Einat in bpo-44010.)
1038+
10331039
importlib.metadata
10341040
------------------
10351041

Lib/idlelib/colorizer.py

+90-45
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,32 @@ def any(name, alternates):
1616

1717
def make_pat():
1818
kw = r"\b" + any("KEYWORD", keyword.kwlist) + r"\b"
19+
match_softkw = (
20+
r"^[ \t]*" + # at beginning of line + possible indentation
21+
r"(?P<MATCH_SOFTKW>match)\b" +
22+
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
23+
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
24+
# pattern-matching statement
25+
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
26+
]) +
27+
r"))"
28+
)
29+
case_default = (
30+
r"^[ \t]*" + # at beginning of line + possible indentation
31+
r"(?P<CASE_SOFTKW>case)" +
32+
r"[ \t]+(?P<CASE_DEFAULT_UNDERSCORE>_\b)"
33+
)
34+
case_softkw_and_pattern = (
35+
r"^[ \t]*" + # at beginning of line + possible indentation
36+
r"(?P<CASE_SOFTKW2>case)\b" +
37+
r"(?![ \t]*(?:" + "|".join([ # not followed by ...
38+
r"_\b", # a lone underscore
39+
r"[:,;=^&|@~)\]}]", # a character which means it can't be a
40+
# pattern-matching case
41+
r"\b(?:" + r"|".join(keyword.kwlist) + r")\b", # a keyword
42+
]) +
43+
r"))"
44+
)
1945
builtinlist = [str(name) for name in dir(builtins)
2046
if not name.startswith('_') and
2147
name not in keyword.kwlist]
@@ -27,12 +53,29 @@ def make_pat():
2753
sq3string = stringprefix + r"'''[^'\\]*((\\.|'(?!''))[^'\\]*)*(''')?"
2854
dq3string = stringprefix + r'"""[^"\\]*((\\.|"(?!""))[^"\\]*)*(""")?'
2955
string = any("STRING", [sq3string, dq3string, sqstring, dqstring])
30-
return (kw + "|" + builtin + "|" + comment + "|" + string +
31-
"|" + any("SYNC", [r"\n"]))
56+
prog = re.compile("|".join([
57+
builtin, comment, string, kw,
58+
match_softkw, case_default,
59+
case_softkw_and_pattern,
60+
any("SYNC", [r"\n"]),
61+
]),
62+
re.DOTALL | re.MULTILINE)
63+
return prog
3264

3365

34-
prog = re.compile(make_pat(), re.S)
35-
idprog = re.compile(r"\s+(\w+)", re.S)
66+
prog = make_pat()
67+
idprog = re.compile(r"\s+(\w+)")
68+
prog_group_name_to_tag = {
69+
"MATCH_SOFTKW": "KEYWORD",
70+
"CASE_SOFTKW": "KEYWORD",
71+
"CASE_DEFAULT_UNDERSCORE": "KEYWORD",
72+
"CASE_SOFTKW2": "KEYWORD",
73+
}
74+
75+
76+
def matched_named_groups(re_match):
77+
"Get only the non-empty named groups from an re.Match object."
78+
return ((k, v) for (k, v) in re_match.groupdict().items() if v)
3679

3780

3881
def color_config(text):
@@ -231,14 +274,10 @@ def recolorize(self):
231274
def recolorize_main(self):
232275
"Evaluate text and apply colorizing tags."
233276
next = "1.0"
234-
while True:
235-
item = self.tag_nextrange("TODO", next)
236-
if not item:
237-
break
238-
head, tail = item
239-
self.tag_remove("SYNC", head, tail)
240-
item = self.tag_prevrange("SYNC", head)
241-
head = item[1] if item else "1.0"
277+
while todo_tag_range := self.tag_nextrange("TODO", next):
278+
self.tag_remove("SYNC", todo_tag_range[0], todo_tag_range[1])
279+
sync_tag_range = self.tag_prevrange("SYNC", todo_tag_range[0])
280+
head = sync_tag_range[1] if sync_tag_range else "1.0"
242281

243282
chars = ""
244283
next = head
@@ -256,23 +295,8 @@ def recolorize_main(self):
256295
return
257296
for tag in self.tagdefs:
258297
self.tag_remove(tag, mark, next)
259-
chars = chars + line
260-
m = self.prog.search(chars)
261-
while m:
262-
for key, value in m.groupdict().items():
263-
if value:
264-
a, b = m.span(key)
265-
self.tag_add(key,
266-
head + "+%dc" % a,
267-
head + "+%dc" % b)
268-
if value in ("def", "class"):
269-
m1 = self.idprog.match(chars, b)
270-
if m1:
271-
a, b = m1.span(1)
272-
self.tag_add("DEFINITION",
273-
head + "+%dc" % a,
274-
head + "+%dc" % b)
275-
m = self.prog.search(chars, m.end())
298+
chars += line
299+
self._add_tags_in_section(chars, head)
276300
if "SYNC" in self.tag_names(next + "-1c"):
277301
head = next
278302
chars = ""
@@ -291,6 +315,40 @@ def recolorize_main(self):
291315
if DEBUG: print("colorizing stopped")
292316
return
293317

318+
def _add_tag(self, start, end, head, matched_group_name):
319+
"""Add a tag to a given range in the text widget.
320+
321+
This is a utility function, receiving the range as `start` and
322+
`end` positions, each of which is a number of characters
323+
relative to the given `head` index in the text widget.
324+
325+
The tag to add is determined by `matched_group_name`, which is
326+
the name of a regular expression "named group" as matched by
327+
by the relevant highlighting regexps.
328+
"""
329+
tag = prog_group_name_to_tag.get(matched_group_name,
330+
matched_group_name)
331+
self.tag_add(tag,
332+
f"{head}+{start:d}c",
333+
f"{head}+{end:d}c")
334+
335+
def _add_tags_in_section(self, chars, head):
336+
"""Parse and add highlighting tags to a given part of the text.
337+
338+
`chars` is a string with the text to parse and to which
339+
highlighting is to be applied.
340+
341+
`head` is the index in the text widget where the text is found.
342+
"""
343+
for m in self.prog.finditer(chars):
344+
for name, matched_text in matched_named_groups(m):
345+
a, b = m.span(name)
346+
self._add_tag(a, b, head, name)
347+
if matched_text in ("def", "class"):
348+
if m1 := self.idprog.match(chars, b):
349+
a, b = m1.span(1)
350+
self._add_tag(a, b, head, "DEFINITION")
351+
294352
def removecolors(self):
295353
"Remove all colorizing tags."
296354
for tag in self.tagdefs:
@@ -299,27 +357,14 @@ def removecolors(self):
299357

300358
def _color_delegator(parent): # htest #
301359
from tkinter import Toplevel, Text
360+
from idlelib.idle_test.test_colorizer import source
302361
from idlelib.percolator import Percolator
303362

304363
top = Toplevel(parent)
305364
top.title("Test ColorDelegator")
306365
x, y = map(int, parent.geometry().split('+')[1:])
307-
top.geometry("700x250+%d+%d" % (x + 20, y + 175))
308-
source = (
309-
"if True: int ('1') # keyword, builtin, string, comment\n"
310-
"elif False: print(0)\n"
311-
"else: float(None)\n"
312-
"if iF + If + IF: 'keyword matching must respect case'\n"
313-
"if'': x or'' # valid keyword-string no-space combinations\n"
314-
"async def f(): await g()\n"
315-
"# All valid prefixes for unicode and byte strings should be colored.\n"
316-
"'x', '''x''', \"x\", \"\"\"x\"\"\"\n"
317-
"r'x', u'x', R'x', U'x', f'x', F'x'\n"
318-
"fr'x', Fr'x', fR'x', FR'x', rf'x', rF'x', Rf'x', RF'x'\n"
319-
"b'x',B'x', br'x',Br'x',bR'x',BR'x', rb'x', rB'x',Rb'x',RB'x'\n"
320-
"# Invalid combinations of legal characters should be half colored.\n"
321-
"ur'x', ru'x', uf'x', fu'x', UR'x', ufr'x', rfu'x', xf'x', fx'x'\n"
322-
)
366+
top.geometry("700x550+%d+%d" % (x + 20, y + 175))
367+
323368
text = Text(top, background="white")
324369
text.pack(expand=1, fill="both")
325370
text.insert("insert", source)

Lib/idlelib/help.html

+19-8
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<head>
66
<meta charset="utf-8" />
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8-
<title>IDLE &#8212; Python 3.10.0a6 documentation</title>
8+
<title>IDLE &#8212; Python 3.11.0a0 documentation</title>
99
<link rel="stylesheet" href="../_static/pydoctheme.css" type="text/css" />
1010
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
1111

@@ -18,7 +18,7 @@
1818
<script src="../_static/sidebar.js"></script>
1919

2020
<link rel="search" type="application/opensearchdescription+xml"
21-
title="Search within Python 3.10.0a6 documentation"
21+
title="Search within Python 3.11.0a0 documentation"
2222
href="../_static/opensearch.xml"/>
2323
<link rel="author" title="About these documents" href="../about.html" />
2424
<link rel="index" title="Index" href="../genindex.html" />
@@ -71,7 +71,7 @@ <h3>Navigation</h3>
7171

7272

7373
<li id="cpython-language-and-version">
74-
<a href="../index.html">3.10.0a6 Documentation</a> &#187;
74+
<a href="../index.html">3.11.0a0 Documentation</a> &#187;
7575
</li>
7676

7777
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li>
@@ -102,7 +102,7 @@ <h3>Navigation</h3>
102102

103103
<div class="section" id="idle">
104104
<span id="id1"></span><h1>IDLE<a class="headerlink" href="#idle" title="Permalink to this headline"></a></h1>
105-
<p><strong>Source code:</strong> <a class="reference external" href="https://github.com/python/cpython/tree/master/Lib/idlelib/">Lib/idlelib/</a></p>
105+
<p><strong>Source code:</strong> <a class="reference external" href="https://github.com/python/cpython/tree/main/Lib/idlelib/">Lib/idlelib/</a></p>
106106
<hr class="docutils" id="index-0" />
107107
<p>IDLE is Python’s Integrated Development and Learning Environment.</p>
108108
<p>IDLE has the following features:</p>
@@ -581,6 +581,11 @@ <h3>Text colors<a class="headerlink" href="#text-colors" title="Permalink to thi
581581
keywords, builtin class and function names, names following <code class="docutils literal notranslate"><span class="pre">class</span></code> and
582582
<code class="docutils literal notranslate"><span class="pre">def</span></code>, strings, and comments. For any text window, these are the cursor (when
583583
present), found text (when possible), and selected text.</p>
584+
<p>IDLE also highlights the <a class="reference internal" href="../reference/lexical_analysis.html#soft-keywords"><span class="std std-ref">soft keywords</span></a> <a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">match</span></code></a>,
585+
<a class="reference internal" href="../reference/compound_stmts.html#match"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">case</span></code></a>, and <a class="reference internal" href="../reference/compound_stmts.html#wildcard-patterns"><code class="xref std std-keyword docutils literal notranslate"><span class="pre">_</span></code></a> in
586+
pattern-matching statements. However, this highlighting is not perfect and
587+
will be incorrect in some rare cases, including some <code class="docutils literal notranslate"><span class="pre">_</span></code>-s in <code class="docutils literal notranslate"><span class="pre">case</span></code>
588+
patterns.</p>
584589
<p>Text coloring is done in the background, so uncolorized text is occasionally
585590
visible. To change the color scheme, use the Configure IDLE dialog
586591
Highlighting tab. The marking of debugger breakpoint lines in the editor and
@@ -685,7 +690,7 @@ <h3>Running user code<a class="headerlink" href="#running-user-code" title="Perm
685690
directly with Python in a text-mode system console or terminal window.
686691
However, the different interface and operation occasionally affect
687692
visible results. For instance, <code class="docutils literal notranslate"><span class="pre">sys.modules</span></code> starts with more entries,
688-
and <code class="docutils literal notranslate"><span class="pre">threading.activeCount()</span></code> returns 2 instead of 1.</p>
693+
and <code class="docutils literal notranslate"><span class="pre">threading.active_count()</span></code> returns 2 instead of 1.</p>
689694
<p>By default, IDLE runs user code in a separate OS process rather than in
690695
the user interface process that runs the shell and editor. In the execution
691696
process, it replaces <code class="docutils literal notranslate"><span class="pre">sys.stdin</span></code>, <code class="docutils literal notranslate"><span class="pre">sys.stdout</span></code>, and <code class="docutils literal notranslate"><span class="pre">sys.stderr</span></code>
@@ -939,7 +944,7 @@ <h3>This Page</h3>
939944
<ul class="this-page-menu">
940945
<li><a href="../bugs.html">Report a Bug</a></li>
941946
<li>
942-
<a href="https://github.com/python/cpython/blob/master/Doc/library/idle.rst"
947+
<a href="https://github.com/python/cpython/blob/main/Doc/library/idle.rst"
943948
rel="nofollow">Show Source
944949
</a>
945950
</li>
@@ -971,7 +976,7 @@ <h3>Navigation</h3>
971976

972977

973978
<li id="cpython-language-and-version">
974-
<a href="../index.html">3.10.0a6 Documentation</a> &#187;
979+
<a href="../index.html">3.11.0a0 Documentation</a> &#187;
975980
</li>
976981

977982
<li class="nav-item nav-item-1"><a href="index.html" >The Python Standard Library</a> &#187;</li>
@@ -997,13 +1002,19 @@ <h3>Navigation</h3>
9971002
<div class="footer">
9981003
&copy; <a href="../copyright.html">Copyright</a> 2001-2021, Python Software Foundation.
9991004
<br />
1005+
This page is licensed under the Python Software Foundation License Version 2.
1006+
<br />
1007+
Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.
1008+
<br />
1009+
See <a href="">History and License</a> for more information.
1010+
<br /><br />
10001011

10011012
The Python Software Foundation is a non-profit corporation.
10021013
<a href="https://www.python.org/psf/donations/">Please donate.</a>
10031014
<br />
10041015
<br />
10051016

1006-
Last updated on Mar 29, 2021.
1017+
Last updated on May 11, 2021.
10071018
<a href="https://docs.python.org/3/bugs.html">Found a bug</a>?
10081019
<br />
10091020

0 commit comments

Comments
 (0)