Fix skipping Jupyter cells with unknown %% magic (#4462)
This commit is contained in:
parent
bbfdba3a5e
commit
8d9d18c033
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
- Fix crashes involving comments in parenthesised return types or `X | Y` style unions.
|
- Fix crashes involving comments in parenthesised return types or `X | Y` style unions.
|
||||||
(#4453)
|
(#4453)
|
||||||
|
- Fix skipping Jupyter cells with unknown `%%` magic (#4462)
|
||||||
|
|
||||||
### Preview style
|
### Preview style
|
||||||
|
|
||||||
|
@ -53,12 +53,12 @@
|
|||||||
)
|
)
|
||||||
from black.handle_ipynb_magics import (
|
from black.handle_ipynb_magics import (
|
||||||
PYTHON_CELL_MAGICS,
|
PYTHON_CELL_MAGICS,
|
||||||
TRANSFORMED_MAGICS,
|
|
||||||
jupyter_dependencies_are_installed,
|
jupyter_dependencies_are_installed,
|
||||||
mask_cell,
|
mask_cell,
|
||||||
put_trailing_semicolon_back,
|
put_trailing_semicolon_back,
|
||||||
remove_trailing_semicolon,
|
remove_trailing_semicolon,
|
||||||
unmask_cell,
|
unmask_cell,
|
||||||
|
validate_cell,
|
||||||
)
|
)
|
||||||
from black.linegen import LN, LineGenerator, transform_line
|
from black.linegen import LN, LineGenerator, transform_line
|
||||||
from black.lines import EmptyLineTracker, LinesBlock
|
from black.lines import EmptyLineTracker, LinesBlock
|
||||||
@ -1084,32 +1084,6 @@ def format_file_contents(
|
|||||||
return dst_contents
|
return dst_contents
|
||||||
|
|
||||||
|
|
||||||
def validate_cell(src: str, mode: Mode) -> None:
|
|
||||||
"""Check that cell does not already contain TransformerManager transformations,
|
|
||||||
or non-Python cell magics, which might cause tokenizer_rt to break because of
|
|
||||||
indentations.
|
|
||||||
|
|
||||||
If a cell contains ``!ls``, then it'll be transformed to
|
|
||||||
``get_ipython().system('ls')``. However, if the cell originally contained
|
|
||||||
``get_ipython().system('ls')``, then it would get transformed in the same way:
|
|
||||||
|
|
||||||
>>> TransformerManager().transform_cell("get_ipython().system('ls')")
|
|
||||||
"get_ipython().system('ls')\n"
|
|
||||||
>>> TransformerManager().transform_cell("!ls")
|
|
||||||
"get_ipython().system('ls')\n"
|
|
||||||
|
|
||||||
Due to the impossibility of safely roundtripping in such situations, cells
|
|
||||||
containing transformed magics will be ignored.
|
|
||||||
"""
|
|
||||||
if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
|
|
||||||
raise NothingChanged
|
|
||||||
if (
|
|
||||||
src[:2] == "%%"
|
|
||||||
and src.split()[0][2:] not in PYTHON_CELL_MAGICS | mode.python_cell_magics
|
|
||||||
):
|
|
||||||
raise NothingChanged
|
|
||||||
|
|
||||||
|
|
||||||
def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
|
def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
|
||||||
"""Format code in given cell of Jupyter notebook.
|
"""Format code in given cell of Jupyter notebook.
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import ast
|
import ast
|
||||||
import collections
|
import collections
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
@ -14,6 +15,7 @@
|
|||||||
else:
|
else:
|
||||||
from typing_extensions import TypeGuard
|
from typing_extensions import TypeGuard
|
||||||
|
|
||||||
|
from black.mode import Mode
|
||||||
from black.output import out
|
from black.output import out
|
||||||
from black.report import NothingChanged
|
from black.report import NothingChanged
|
||||||
|
|
||||||
@ -64,6 +66,34 @@ def jupyter_dependencies_are_installed(*, warn: bool) -> bool:
|
|||||||
return installed
|
return installed
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cell(src: str, mode: Mode) -> None:
|
||||||
|
"""Check that cell does not already contain TransformerManager transformations,
|
||||||
|
or non-Python cell magics, which might cause tokenizer_rt to break because of
|
||||||
|
indentations.
|
||||||
|
|
||||||
|
If a cell contains ``!ls``, then it'll be transformed to
|
||||||
|
``get_ipython().system('ls')``. However, if the cell originally contained
|
||||||
|
``get_ipython().system('ls')``, then it would get transformed in the same way:
|
||||||
|
|
||||||
|
>>> TransformerManager().transform_cell("get_ipython().system('ls')")
|
||||||
|
"get_ipython().system('ls')\n"
|
||||||
|
>>> TransformerManager().transform_cell("!ls")
|
||||||
|
"get_ipython().system('ls')\n"
|
||||||
|
|
||||||
|
Due to the impossibility of safely roundtripping in such situations, cells
|
||||||
|
containing transformed magics will be ignored.
|
||||||
|
"""
|
||||||
|
if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
|
||||||
|
raise NothingChanged
|
||||||
|
|
||||||
|
line = _get_code_start(src)
|
||||||
|
if line.startswith("%%") and (
|
||||||
|
line.split(maxsplit=1)[0][2:]
|
||||||
|
not in PYTHON_CELL_MAGICS | mode.python_cell_magics
|
||||||
|
):
|
||||||
|
raise NothingChanged
|
||||||
|
|
||||||
|
|
||||||
def remove_trailing_semicolon(src: str) -> tuple[str, bool]:
|
def remove_trailing_semicolon(src: str) -> tuple[str, bool]:
|
||||||
"""Remove trailing semicolon from Jupyter notebook cell.
|
"""Remove trailing semicolon from Jupyter notebook cell.
|
||||||
|
|
||||||
@ -276,6 +306,21 @@ def unmask_cell(src: str, replacements: list[Replacement]) -> str:
|
|||||||
return src
|
return src
|
||||||
|
|
||||||
|
|
||||||
|
def _get_code_start(src: str) -> str:
|
||||||
|
"""Provides the first line where the code starts.
|
||||||
|
|
||||||
|
Iterates over lines of code until it finds the first line that doesn't
|
||||||
|
contain only empty spaces and comments. It removes any empty spaces at the
|
||||||
|
start of the line and returns it. If such line doesn't exist, it returns an
|
||||||
|
empty string.
|
||||||
|
"""
|
||||||
|
for match in re.finditer(".+", src):
|
||||||
|
line = match.group(0).lstrip()
|
||||||
|
if line and not line.startswith("#"):
|
||||||
|
return line
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
|
def _is_ipython_magic(node: ast.expr) -> TypeGuard[ast.Attribute]:
|
||||||
"""Check if attribute is IPython magic.
|
"""Check if attribute is IPython magic.
|
||||||
|
|
||||||
|
@ -208,6 +208,22 @@ def test_cell_magic_with_custom_python_magic(
|
|||||||
assert result == expected_output
|
assert result == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"src",
|
||||||
|
(
|
||||||
|
" %%custom_magic \nx=2",
|
||||||
|
"\n\n%%custom_magic\nx=2",
|
||||||
|
"# comment\n%%custom_magic\nx=2",
|
||||||
|
"\n \n # comment with %%time\n\t\n %%custom_magic # comment \nx=2",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_cell_magic_with_custom_python_magic_after_spaces_and_comments_noop(
|
||||||
|
src: str,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(NothingChanged):
|
||||||
|
format_cell(src, fast=True, mode=JUPYTER_MODE)
|
||||||
|
|
||||||
|
|
||||||
def test_cell_magic_nested() -> None:
|
def test_cell_magic_nested() -> None:
|
||||||
src = "%%time\n%%time\n2+2"
|
src = "%%time\n%%time\n2+2"
|
||||||
result = format_cell(src, fast=True, mode=JUPYTER_MODE)
|
result = format_cell(src, fast=True, mode=JUPYTER_MODE)
|
||||||
|
Loading…
Reference in New Issue
Block a user