Skip to content

Commit 8d9d18c

Browse files
authored
Fix skipping Jupyter cells with unknown %% magic (#4462)
1 parent bbfdba3 commit 8d9d18c

File tree

4 files changed

+63
-27
lines changed

4 files changed

+63
-27
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
- Fix crashes involving comments in parenthesised return types or `X | Y` style unions.
2323
(#4453)
24+
- Fix skipping Jupyter cells with unknown `%%` magic (#4462)
2425

2526
### Preview style
2627

src/black/__init__.py

+1-27
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@
5353
)
5454
from black.handle_ipynb_magics import (
5555
PYTHON_CELL_MAGICS,
56-
TRANSFORMED_MAGICS,
5756
jupyter_dependencies_are_installed,
5857
mask_cell,
5958
put_trailing_semicolon_back,
6059
remove_trailing_semicolon,
6160
unmask_cell,
61+
validate_cell,
6262
)
6363
from black.linegen import LN, LineGenerator, transform_line
6464
from black.lines import EmptyLineTracker, LinesBlock
@@ -1084,32 +1084,6 @@ def format_file_contents(
10841084
return dst_contents
10851085

10861086

1087-
def validate_cell(src: str, mode: Mode) -> None:
1088-
"""Check that cell does not already contain TransformerManager transformations,
1089-
or non-Python cell magics, which might cause tokenizer_rt to break because of
1090-
indentations.
1091-
1092-
If a cell contains ``!ls``, then it'll be transformed to
1093-
``get_ipython().system('ls')``. However, if the cell originally contained
1094-
``get_ipython().system('ls')``, then it would get transformed in the same way:
1095-
1096-
>>> TransformerManager().transform_cell("get_ipython().system('ls')")
1097-
"get_ipython().system('ls')\n"
1098-
>>> TransformerManager().transform_cell("!ls")
1099-
"get_ipython().system('ls')\n"
1100-
1101-
Due to the impossibility of safely roundtripping in such situations, cells
1102-
containing transformed magics will be ignored.
1103-
"""
1104-
if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
1105-
raise NothingChanged
1106-
if (
1107-
src[:2] == "%%"
1108-
and src.split()[0][2:] not in PYTHON_CELL_MAGICS | mode.python_cell_magics
1109-
):
1110-
raise NothingChanged
1111-
1112-
11131087
def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
11141088
"""Format code in given cell of Jupyter notebook.
11151089

src/black/handle_ipynb_magics.py

+45
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ast
44
import collections
55
import dataclasses
6+
import re
67
import secrets
78
import sys
89
from functools import lru_cache
@@ -14,6 +15,7 @@
1415
else:
1516
from typing_extensions import TypeGuard
1617

18+
from black.mode import Mode
1719
from black.output import out
1820
from black.report import NothingChanged
1921

@@ -64,6 +66,34 @@ def jupyter_dependencies_are_installed(*, warn: bool) -> bool:
6466
return installed
6567

6668

69+
def validate_cell(src: str, mode: Mode) -> None:
70+
"""Check that cell does not already contain TransformerManager transformations,
71+
or non-Python cell magics, which might cause tokenizer_rt to break because of
72+
indentations.
73+
74+
If a cell contains ``!ls``, then it'll be transformed to
75+
``get_ipython().system('ls')``. However, if the cell originally contained
76+
``get_ipython().system('ls')``, then it would get transformed in the same way:
77+
78+
>>> TransformerManager().transform_cell("get_ipython().system('ls')")
79+
"get_ipython().system('ls')\n"
80+
>>> TransformerManager().transform_cell("!ls")
81+
"get_ipython().system('ls')\n"
82+
83+
Due to the impossibility of safely roundtripping in such situations, cells
84+
containing transformed magics will be ignored.
85+
"""
86+
if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
87+
raise NothingChanged
88+
89+
line = _get_code_start(src)
90+
if line.startswith("%%") and (
91+
line.split(maxsplit=1)[0][2:]
92+
not in PYTHON_CELL_MAGICS | mode.python_cell_magics
93+
):
94+
raise NothingChanged
95+
96+
6797
def remove_trailing_semicolon(src: str) -> tuple[str, bool]:
6898
"""Remove trailing semicolon from Jupyter notebook cell.
6999
@@ -276,6 +306,21 @@ def unmask_cell(src: str, replacements: list[Replacement]) -> str:
276306
return src
277307

278308

309+
def _get_code_start(src: str) -> str:
310+
"""Provides the first line where the code starts.
311+
312+
Iterates over lines of code until it finds the first line that doesn't
313+
contain only empty spaces and comments. It removes any empty spaces at the
314+
start of the line and returns it. If such line doesn't exist, it returns an
315+
empty string.
316+
"""
317+
for match in re.finditer(".+", src):
318+
line = match.group(0).lstrip()
319+
if line and not line.startswith("#"):
320+
return line
321+
return ""
322+
323+
279324
def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
280325
"""Check if attribute is IPython magic.
281326

tests/test_ipynb.py

+16
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,22 @@ def test_cell_magic_with_custom_python_magic(
208208
assert result == expected_output
209209

210210

211+
@pytest.mark.parametrize(
212+
"src",
213+
(
214+
" %%custom_magic \nx=2",
215+
"\n\n%%custom_magic\nx=2",
216+
"# comment\n%%custom_magic\nx=2",
217+
"\n \n # comment with %%time\n\t\n %%custom_magic # comment \nx=2",
218+
),
219+
)
220+
def test_cell_magic_with_custom_python_magic_after_spaces_and_comments_noop(
221+
src: str,
222+
) -> None:
223+
with pytest.raises(NothingChanged):
224+
format_cell(src, fast=True, mode=JUPYTER_MODE)
225+
226+
211227
def test_cell_magic_nested() -> None:
212228
src = "%%time\n%%time\n2+2"
213229
result = format_cell(src, fast=True, mode=JUPYTER_MODE)

0 commit comments

Comments
 (0)