Skip to content

Commit 10ee163

Browse files
authored
Infra: Add custom lexers for PEP 823 + 824 (#5125)
1 parent fbb2bc3 commit 10ee163

6 files changed

Lines changed: 128 additions & 25 deletions

File tree

‎pep_sphinx_extensions/__init__.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
create_rss_feed,
1414
get_from_doctree,
1515
)
16+
from pep_sphinx_extensions.lexers import get_custom_lexers
1617
from pep_sphinx_extensions.pep_processor.html import (
1718
pep_html_builder,
1819
pep_html_translator,
@@ -110,6 +111,10 @@ def setup(app: Sphinx) -> dict[str, bool]:
110111
app.add_directive("superseded", pep_banner_directive.SupersededBanner)
111112
app.add_directive("withdrawn", pep_banner_directive.WithdrawnBanner)
112113

114+
# Register custom lexers
115+
for lexer in get_custom_lexers():
116+
app.add_lexer(lexer.name, lexer)
117+
113118
# Register event callbacks
114119
app.connect(
115120
"builder-inited", _update_config_for_builder
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# This file is placed in the public domain or under the
2+
# CC0-1.0-Universal license, whichever is more permissive.
3+
4+
from __future__ import annotations
5+
6+
import importlib
7+
import pkgutil
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from pygments.lexer import Lexer
12+
13+
14+
def get_custom_lexers() -> list[type[Lexer]]:
15+
lexers: list[type[Lexer]] = []
16+
for module_info in pkgutil.walk_packages(__path__, prefix=f"{__name__}."):
17+
module = importlib.import_module(module_info.name)
18+
if (register_func := getattr(module, "register", None)) is None:
19+
continue
20+
lexers.extend(register_func())
21+
return lexers
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# This file is placed in the public domain or under the
2+
# CC0-1.0-Universal license, whichever is more permissive.
3+
4+
"""Custom lexer for PEP 823."""
5+
6+
# This lexer is most useful while PEP 823 is being discussed.
7+
# If it breaks in the future, it can be safely replaced by plain
8+
# Python or text lexers.
9+
10+
from pygments.lexer import DelegatingLexer, inherit
11+
from pygments.lexers.python import (
12+
PythonLexer,
13+
PythonTracebackLexer,
14+
_PythonConsoleLexerBase,
15+
)
16+
from pygments.token import Operator, Other
17+
18+
19+
class Py823Lexer(PythonLexer):
20+
name = "py823"
21+
22+
tokens = {
23+
"expr": [
24+
(r"maybe\b", Operator.Word),
25+
(r"\?", Operator),
26+
inherit,
27+
],
28+
}
29+
30+
31+
class Py823ConsoleLexer(DelegatingLexer):
32+
name = "py823-console"
33+
34+
def __init__(self, **options):
35+
pylexer = Py823Lexer
36+
tblexer = PythonTracebackLexer
37+
38+
class _ReplaceInnerCode(DelegatingLexer):
39+
def __init__(self, **options):
40+
super().__init__(
41+
pylexer, _PythonConsoleLexerBase, Other.Code, **options
42+
)
43+
44+
super().__init__(tblexer, _ReplaceInnerCode, Other.Traceback, **options)
45+
46+
47+
def register():
48+
return [Py823Lexer, Py823ConsoleLexer]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# This file is placed in the public domain or under the
2+
# CC0-1.0-Universal license, whichever is more permissive.
3+
4+
"""Custom lexer for PEP 824."""
5+
6+
# This lexer is most useful while PEP 824 is being discussed.
7+
# If it breaks in the future, it can be safely replaced by plain
8+
# Python or text lexers.
9+
10+
from pygments.lexer import inherit
11+
from pygments.lexers.python import PythonLexer
12+
from pygments.token import Operator
13+
14+
15+
class Py824Lexer(PythonLexer):
16+
name = "py824"
17+
18+
tokens = {
19+
"expr": [
20+
(r"\?\?(?=\s)", Operator.Word),
21+
(r"otherwise\b", Operator.Word),
22+
(r"\?", Operator),
23+
inherit,
24+
],
25+
}
26+
27+
28+
def register():
29+
return [Py824Lexer]

‎peps/pep-0823.rst‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ kinds of expressions much simpler while being predictable and doing
149149
the correct things intuitively. Using these operators, the function
150150
could instead be written as:
151151

152-
::
152+
.. code-block:: py823
153153
154154
def get_customer_name(data: Data) -> str | None:
155155
return data.customer?.user?.name.lower()
@@ -198,7 +198,7 @@ least for dictionaries a useful helper method is ``dict.get(key)``.
198198
199199
Writing it using ``?.`` and ``?[ ]`` would look like this:
200200

201-
::
201+
.. code-block:: py823
202202
203203
def get_customer_name(data: Data) -> str | None:
204204
return data.get("customer")?["user"]?["name"].lower()
@@ -221,14 +221,14 @@ hide in plain sight. Attribute and function names have been shortened.
221221
If code relied on this property, the expression cannot necessarily
222222
be replaced with ``?.`` or ``?[ ]``.
223223

224-
::
224+
.. code-block:: py823
225225
226226
# In assignments
227227
228228
x = a.b if (a is not None) else None
229229
x = a?.b
230230
231-
::
231+
.. code-block:: py823
232232
233233
# In if statements often used as guard clause with early
234234
# return or raising of an exception
@@ -242,7 +242,7 @@ hide in plain sight. Attribute and function names have been shortened.
242242
if a is None or a.b is None: ...
243243
if a?.b is None: ...
244244
245-
::
245+
.. code-block:: py823
246246
247247
# Misc expressions
248248
@@ -336,7 +336,7 @@ for trying to get a subscript of ``None`` are omitted. It is therefore
336336
not necessary to change subsequent ``.`` or ``[ ]`` on the right-hand
337337
side just because a ``?.`` or ``?[ ]`` is used prior.
338338

339-
::
339+
.. code-block:: py823-console
340340
341341
>>> a = None
342342
>>> print(a?.b.c[0].some_function())
@@ -348,7 +348,7 @@ their ``None``-aware counterparts, and call expressions). As a rule of
348348
thumb, short-circuiting is broken once an operator other than
349349
``.``, ``[ ]``, ``?.``, ``?[ ]`` is reached.
350350

351-
::
351+
.. code-block:: py823-console
352352
353353
>>> a = None
354354
>>> print(a?.b.c)
@@ -370,7 +370,7 @@ be broken. For example function arguments or subscripts are evaluated
370370
on their own and would not short-circuit the remaining ``tail`` of the
371371
outer expression.
372372

373-
::
373+
.. code-block:: py823
374374
375375
# func(a?.b).c[d?.e]
376376
@@ -389,7 +389,7 @@ if ``a is None``. This is conceptually identical to extracting the group
389389
contents and storing the result in a temporary variable before
390390
substituting it back into the original expression.
391391

392-
::
392+
.. code-block:: py823
393393
394394
# (a?.b).c
395395
@@ -400,7 +400,7 @@ Common use cases for ``None``-aware access operators in groups are
400400
boolean or conditional expressions which can provide a fallback value
401401
in case the first part evaluates to ``None``.
402402

403-
::
403+
.. code-block:: py823
404404
405405
(a.b?.c or d).e?.func()
406406
@@ -419,7 +419,7 @@ Assignments
419419
``None``-aware expressions may only be used in a ``Load`` context.
420420
Assignments are not permitted and will raise a ``SyntaxError``.
421421

422-
::
422+
.. code-block:: py823-console
423423
424424
>>> a?.b = 1
425425
File "<python-input-1>", line 1
@@ -437,7 +437,7 @@ This does not apply if the ``None``-aware expressions is only part
437437
of a larger expression and evaluated on its own, for example as a
438438
function argument.
439439

440-
::
440+
.. code-block:: py823-console
441441
442442
>>> a = None
443443
>>> def f(a):
@@ -512,7 +512,7 @@ their needs, especially code formatters might prefer a style which
512512
conforms better to their existing preferences. An example of what
513513
is possible:
514514

515-
::
515+
.. code-block:: py823
516516
517517
def get_customer_name(data: Data) -> str | None:
518518
return (
@@ -675,7 +675,7 @@ because it might be too difficult to understand. Developers should
675675
instead change any subsequent attribute access or subscript to their
676676
``None``-aware variants.
677677

678-
::
678+
.. code-block:: py823
679679
680680
# before
681681
a.b.optional?.c.d.e
@@ -706,7 +706,7 @@ instead of two new operators, it may also be **too general**, in a sense
706706
that it can be combine with any other operator. For example it is not
707707
clear what the following expressions would mean:
708708

709-
::
709+
.. code-block:: py823-console
710710
711711
>>> x? + 1
712712
>>> x? -= 1
@@ -717,7 +717,7 @@ clear what the following expressions would mean:
717717
Even if a default meaning of ``is not None else None`` is assumed, the
718718
expressions are likely to raise errors at some point.
719719

720-
::
720+
.. code-block:: py823-console
721721
722722
>>> x? + 1
723723
>>> (_t1 if ((_t1 := x) is not None) else None) + 1
@@ -896,7 +896,7 @@ the substitution principle. An expression ``(a?.b).c`` should behave
896896
the same whether or not ``a?.b`` is written inline inside a group or
897897
defined as a separate variable.
898898

899-
::
899+
.. code-block:: py823
900900
901901
(a?.b).c
902902
@@ -1053,7 +1053,7 @@ for an ``optional`` value evaluates to ``None``, the result will be
10531053
will be skipped. In the example below, if ``a.b`` is ``None``, so will
10541054
be ``a.b?.c``:
10551055

1056-
::
1056+
.. code-block:: py823
10571057
10581058
a.b?.c
10591059
^^^

‎peps/pep-0824.rst‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Using the "``None``-coalescing" operator ``??`` instead, helps to keep
111111
the expression short and predictable while still clearly communicating
112112
the intent.
113113

114-
::
114+
.. code-block:: py824
115115
116116
def show_user_age(user: User):
117117
age = user.age ?? "unknown"
@@ -134,7 +134,7 @@ Using the "``None``-coalesce assignment" operator ``??=`` helps to
134134
avoid repeating the expression. Especially for more complex once,
135135
this will make it easier to read and write.
136136

137-
::
137+
.. code-block:: py824
138138
139139
def fix_user_name(user: User):
140140
user.name ??= "unknown"
@@ -157,7 +157,7 @@ and assign the fallback value inside the function itself.
157157
158158
This could be rewritten as:
159159

160-
::
160+
.. code-block:: py824
161161
162162
def show_user_name(user: User | None):
163163
user ??= create_default_user()
@@ -188,7 +188,7 @@ conditional expressions. Parentheses can be added as necessary to
188188
modify the precedence of individual expressions. A few examples of
189189
how implicit parentheses would be placed:
190190

191-
::
191+
.. code-block:: py824
192192
193193
# x or y ?? 2
194194
(x or y) ?? 2
@@ -335,7 +335,7 @@ The following is therefore merely meant as a suggestion.
335335
+---------------------------+--------------------------+----------------------------+
336336
| Code | Pattern | Example |
337337
+===========================+==========================+============================+
338-
| :: | "... or ... if None" | "user dot age ``or`` |
338+
| .. code-block:: py824 | "... or ... if None" | "user dot age ``or`` |
339339
| | | unknown ``if None``" |
340340
+ user.age ?? "unknown" +--------------------------+----------------------------+
341341
| | "... coalesce with ..." | "user dot age |
@@ -348,7 +348,7 @@ The following is therefore merely meant as a suggestion.
348348
+-----------------------------+------------------------------+------------------------------------+
349349
| Code | Pattern | Example |
350350
+=============================+==============================+====================================+
351-
| :: | "if ... is None, assign ..." | "``if`` user dot name ``is None``, |
351+
| .. code-block:: py824 | "if ... is None, assign ..." | "``if`` user dot name ``is None``, |
352352
| | | ``assign`` unknown" |
353353
+ user.name ??= "unknown" +------------------------------+------------------------------------+
354354
| | "assign ... to ... if None" | "``assign`` unknown ``to`` user |
@@ -416,7 +416,7 @@ programming languages.
416416
Lastly, using a (soft-) keyword for the "``None``-coalescing assignment"
417417
operator poses additional questions and readability concerns.
418418

419-
::
419+
.. code-block:: py824
420420
421421
a = otherwise b
422422

0 commit comments

Comments
 (0)