Conversation
`%%time`, `%%timeit`, `%%capture` and `%%prun` lexed their body with
`using(Python3Lexer)`, which is the bare Python lexer and knows nothing about
IPython syntax. A shell escape inside one of those bodies therefore came out as
`Token.Error` for the `!` instead of being highlighted as a command:
%%time
!cmd # '!' was Token.Error
The body of each of those four magics is IPython input, not plain Python, which
is why the `!` reaches the system shell at runtime. Verified against IPython
9.17.1 by executing each magic with a shell escape in the body: `%%time`,
`%%timeit`, `%%prun` and `%%capture` all run it, while `%%python` does not.
Delegate those four to `using(this)`, the current lexer's own root state, which
already carries the `!`-escape, line-magic and everything else in
`ipython_tokens`. Using `this` rather than a concrete class keeps the rule
correct for both the Python 2 and Python 3 IPython lexers, which share this
token table.
The other Python-running magics keep delegating to `Python3Lexer`, because
their bodies are not IPython input: `%%debug` runs the body under pdb,
`%%python`/`%%python2`/`%%python3`/`%%pypy` run it in a separate interpreter, and
`%%writefile`/`%%file` treat it as literal file content.
Closes ipython#11
|
The only failing check is |
|
Friendly ping from the author: this PR is code-complete and self-contained, with no maintainer change requests outstanding. The GitHub Actions checks are currently waiting for maintainer approval to run on this fork PR ( |
Fixes the case in the issue: a shell escape inside a cell magic whose body is IPython input was highlighted as an error instead of as a command.
The bug
%%time,%%timeit,%%captureand%%prunlexed their body withusing(Python3Lexer)— the bare Python lexer, which knows nothing about IPython syntax. So the!of a shell escape came out asToken.Error:!!cmdToken.Operator%%time(before)Token.Error%%time(after)Token.OperatorWhich magics get the fix, and why
I checked this against IPython itself rather than guessing, by executing each magic with a shell escape in the body (IPython 9.17.1):
!echo …reaches the shell?%%time%%timeit%%capturecaptured.stdout)%%prun%%debug%%python,%%python2,%%python3,%%pypy%%writefile,%%fileThat is why the change is limited to four rules: the other Python-running magics genuinely do lex plain Python, and the existing test for
%%writefilestill pins that.The change
Those four now delegate to
using(this)— the current lexer's own root state, which already contains the!-escape rule, the line-magic rules and everything else inipython_tokens. Usingthisrather than naming a class keeps the rule correct for both the Python 2 and Python 3 IPython lexers, which share this token table.Testing
The CI matrix is Python 3.8–3.13; nothing added uses syntax newer than 3.8.
Eight test cases are added, in the style of the existing
test_shell_commands:test_shell_commands_inside_ipython_input_cell_magics(4 params) — the body tokenizes as the shell escape it is.test_no_shell_commands_where_the_body_is_not_ipython_input(3 params) —%%debug,%%pythonand%%writefilekeep plain-Python highlighting, so the fix cannot silently widen to magics whose bodies are not IPython input.test_python_still_lexes_inside_a_timing_magic— delegating to this lexer's root keeps ordinary Python highlighting intact.Red/green: with only the test changes applied, the four parametrizations of the first test fail and the other eleven pass — so the failure is specific to the reported behaviour, and the guards are genuinely pinning unchanged behaviour rather than passing by accident.
Final-interface output from the reporter's snippet, through the shipped
ipython3entry point withHtmlFormatter(nowrap=True):and unchanged for a magic where the body is not IPython input:
Notes
Open PR Keep exception message continuation lines as text #13 (also mine) edits
ipython_pygments_lexers.py, but around line 232 inIPythonPartialTracebackLexer; this change is confined to lines 53–150 (ipython_tokens), so the two do not overlap textually.Closes failure on shell commands after
%%time#11I did not change how
%%writefilelexes its body. It treats the body as literal text, so Python highlighting of it is arguably also imprecise, but that is a separate behaviour from the one reported here.This PR description was written with AI assistance.