]>
crepu.dev Git - config.git/blob - djavu-asus/elpy/rpc-venv/lib/python3.11/site-packages/black/handle_ipynb_magics.py
1 """Functions to process IPython magics with."""
8 from functools
import lru_cache
9 from importlib
.util
import find_spec
10 from typing
import Dict
, List
, Optional
, Tuple
12 if sys
.version_info
>= (3, 10):
13 from typing
import TypeGuard
15 from typing_extensions
import TypeGuard
17 from black
.output
import out
18 from black
.report
import NothingChanged
20 TRANSFORMED_MAGICS
= frozenset(
22 "get_ipython().run_cell_magic",
23 "get_ipython().system",
24 "get_ipython().getoutput",
25 "get_ipython().run_line_magic",
28 TOKENS_TO_IGNORE
= frozenset(
39 PYTHON_CELL_MAGICS
= frozenset(
50 TOKEN_HEX
= secrets
.token_hex
53 @dataclasses.dataclass(frozen
=True)
60 def jupyter_dependencies_are_installed(*, warn
: bool) -> bool:
62 find_spec("tokenize_rt") is not None and find_spec("IPython") is not None
64 if not installed
and warn
:
66 "Skipping .ipynb files as Jupyter dependencies are not installed.\n"
67 'You can fix this by running ``pip install "black[jupyter]"``'
73 def remove_trailing_semicolon(src
: str) -> Tuple
[str, bool]:
74 """Remove trailing semicolon from Jupyter notebook cell.
78 fig, ax = plt.subplots()
79 ax.plot(x_data, y_data); # plot data
83 fig, ax = plt.subplots()
84 ax.plot(x_data, y_data) # plot data
86 Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
87 ``tokenize_rt`` so that round-tripping works fine.
89 from tokenize_rt
import reversed_enumerate
, src_to_tokens
, tokens_to_src
91 tokens
= src_to_tokens(src
)
92 trailing_semicolon
= False
93 for idx
, token
in reversed_enumerate(tokens
):
94 if token
.name
in TOKENS_TO_IGNORE
:
96 if token
.name
== "OP" and token
.src
== ";":
98 trailing_semicolon
= True
100 if not trailing_semicolon
:
102 return tokens_to_src(tokens
), True
105 def put_trailing_semicolon_back(src
: str, has_trailing_semicolon
: bool) -> str:
106 """Put trailing semicolon back if cell originally had it.
108 Mirrors the logic in `quiet` from `IPython.core.displayhook`, but uses
109 ``tokenize_rt`` so that round-tripping works fine.
111 if not has_trailing_semicolon
:
113 from tokenize_rt
import reversed_enumerate
, src_to_tokens
, tokens_to_src
115 tokens
= src_to_tokens(src
)
116 for idx
, token
in reversed_enumerate(tokens
):
117 if token
.name
in TOKENS_TO_IGNORE
:
119 tokens
[idx
] = token
._replace
(src
=token
.src
+ ";")
121 else: # pragma: nocover
122 raise AssertionError(
123 "INTERNAL ERROR: Was not able to reinstate trailing semicolon. "
124 "Please report a bug on https://github.com/psf/black/issues. "
126 return str(tokens_to_src(tokens
))
129 def mask_cell(src
: str) -> Tuple
[str, List
[Replacement
]]:
130 """Mask IPython magics so content becomes parseable Python code.
142 The replacements are returned, along with the transformed code.
144 replacements
: List
[Replacement
] = []
148 # Might have IPython magics, will process below.
151 # Syntax is fine, nothing to mask, early return.
152 return src
, replacements
154 from IPython
.core
.inputtransformer2
import TransformerManager
156 transformer_manager
= TransformerManager()
157 transformed
= transformer_manager
.transform_cell(src
)
158 transformed
, cell_magic_replacements
= replace_cell_magics(transformed
)
159 replacements
+= cell_magic_replacements
160 transformed
= transformer_manager
.transform_cell(transformed
)
161 transformed
, magic_replacements
= replace_magics(transformed
)
162 if len(transformed
.splitlines()) != len(src
.splitlines()):
163 # Multi-line magic, not supported.
165 replacements
+= magic_replacements
166 return transformed
, replacements
169 def get_token(src
: str, magic
: str) -> str:
170 """Return randomly generated token to mask IPython magic with.
172 For example, if 'magic' was `%matplotlib inline`, then a possible
173 token to mask it with would be `"43fdd17f7e5ddc83"`. The token
174 will be the same length as the magic, and we make sure that it was
175 not already present anywhere else in the cell.
178 nbytes
= max(len(magic
) // 2 - 1, 1)
179 token
= TOKEN_HEX(nbytes
)
182 token
= TOKEN_HEX(nbytes
)
185 raise AssertionError(
186 "INTERNAL ERROR: Black was not able to replace IPython magic. "
187 "Please report a bug on https://github.com/psf/black/issues. "
188 f
"The magic might be helpful: {magic}"
190 if len(token
) + 2 < len(magic
):
195 def replace_cell_magics(src
: str) -> Tuple
[str, List
[Replacement
]]:
196 """Replace cell magic with token.
198 Note that 'src' will already have been processed by IPython's
199 TransformerManager().transform_cell.
203 get_ipython().run_cell_magic('t', '-n1', 'ls =!ls\\n')
210 The replacement, along with the transformed code, is returned.
212 replacements
: List
[Replacement
] = []
214 tree
= ast
.parse(src
)
216 cell_magic_finder
= CellMagicFinder()
217 cell_magic_finder
.visit(tree
)
218 if cell_magic_finder
.cell_magic
is None:
219 return src
, replacements
220 header
= cell_magic_finder
.cell_magic
.header
221 mask
= get_token(src
, header
)
222 replacements
.append(Replacement(mask
=mask
, src
=header
))
223 return f
"{mask}\n{cell_magic_finder.cell_magic.body}", replacements
226 def replace_magics(src
: str) -> Tuple
[str, List
[Replacement
]]:
227 """Replace magics within body of cell.
229 Note that 'src' will already have been processed by IPython's
230 TransformerManager().transform_cell.
234 get_ipython().run_line_magic('matplotlib', 'inline')
242 The replacement, along with the transformed code, are returned.
245 magic_finder
= MagicFinder()
246 magic_finder
.visit(ast
.parse(src
))
248 for i
, line
in enumerate(src
.splitlines(), start
=1):
249 if i
in magic_finder
.magics
:
250 offsets_and_magics
= magic_finder
.magics
[i
]
251 if len(offsets_and_magics
) != 1: # pragma: nocover
252 raise AssertionError(
253 f
"Expecting one magic per line, got: {offsets_and_magics}\n"
254 "Please report a bug on https://github.com/psf/black/issues."
256 col_offset
, magic
= (
257 offsets_and_magics
[0].col_offset
,
258 offsets_and_magics
[0].magic
,
260 mask
= get_token(src
, magic
)
261 replacements
.append(Replacement(mask
=mask
, src
=magic
))
262 line
= line
[:col_offset
] + mask
263 new_srcs
.append(line
)
264 return "\n".join(new_srcs
), replacements
267 def unmask_cell(src
: str, replacements
: List
[Replacement
]) -> str:
268 """Remove replacements from cell.
280 for replacement
in replacements
:
281 src
= src
.replace(replacement
.mask
, replacement
.src
)
285 def _is_ipython_magic(node
: ast
.expr
) -> TypeGuard
[ast
.Attribute
]:
286 """Check if attribute is IPython magic.
288 Note that the source of the abstract syntax tree
289 will already have been processed by IPython's
290 TransformerManager().transform_cell.
293 isinstance(node
, ast
.Attribute
)
294 and isinstance(node
.value
, ast
.Call
)
295 and isinstance(node
.value
.func
, ast
.Name
)
296 and node
.value
.func
.id == "get_ipython"
300 def _get_str_args(args
: List
[ast
.expr
]) -> List
[str]:
303 assert isinstance(arg
, ast
.Str
)
304 str_args
.append(arg
.s
)
308 @dataclasses.dataclass(frozen
=True)
311 params
: Optional
[str]
315 def header(self
) -> str:
317 return f
"%%{self.name} {self.params}"
318 return f
"%%{self.name}"
321 # ast.NodeVisitor + dataclass = breakage under mypyc.
322 class CellMagicFinder(ast
.NodeVisitor
):
325 Note that the source of the abstract syntax tree
326 will already have been processed by IPython's
327 TransformerManager().transform_cell.
334 would have been transformed to
336 get_ipython().run_cell_magic('time', '', 'foo()\\n')
338 and we look for instances of the latter.
341 def __init__(self
, cell_magic
: Optional
[CellMagic
] = None) -> None:
342 self
.cell_magic
= cell_magic
344 def visit_Expr(self
, node
: ast
.Expr
) -> None:
345 """Find cell magic, extract header and body."""
347 isinstance(node
.value
, ast
.Call
)
348 and _is_ipython_magic(node
.value
.func
)
349 and node
.value
.func
.attr
== "run_cell_magic"
351 args
= _get_str_args(node
.value
.args
)
352 self
.cell_magic
= CellMagic(name
=args
[0], params
=args
[1], body
=args
[2])
353 self
.generic_visit(node
)
356 @dataclasses.dataclass(frozen
=True)
357 class OffsetAndMagic
:
362 # Unsurprisingly, subclassing ast.NodeVisitor means we can't use dataclasses here
363 # as mypyc will generate broken code.
364 class MagicFinder(ast
.NodeVisitor
):
365 """Visit cell to look for get_ipython calls.
367 Note that the source of the abstract syntax tree
368 will already have been processed by IPython's
369 TransformerManager().transform_cell.
375 would have been transformed to
377 get_ipython().run_line_magic('matplotlib', 'inline')
379 and we look for instances of the latter (and likewise for other
383 def __init__(self
) -> None:
384 self
.magics
: Dict
[int, List
[OffsetAndMagic
]] = collections
.defaultdict(list)
386 def visit_Assign(self
, node
: ast
.Assign
) -> None:
387 """Look for system assign magics.
391 black_version = !black --version
394 would have been (respectively) transformed to
396 black_version = get_ipython().getoutput('black --version')
397 env = get_ipython().run_line_magic('env', 'var')
399 and we look for instances of any of the latter.
401 if isinstance(node
.value
, ast
.Call
) and _is_ipython_magic(node
.value
.func
):
402 args
= _get_str_args(node
.value
.args
)
403 if node
.value
.func
.attr
== "getoutput":
405 elif node
.value
.func
.attr
== "run_line_magic":
410 raise AssertionError(
411 f
"Unexpected IPython magic {node.value.func.attr!r} found. "
412 "Please report a bug on https://github.com/psf/black/issues."
414 self
.magics
[node
.value
.lineno
].append(
415 OffsetAndMagic(node
.value
.col_offset
, src
)
417 self
.generic_visit(node
)
419 def visit_Expr(self
, node
: ast
.Expr
) -> None:
420 """Look for magics in body of cell.
429 would (respectively) get transformed to
431 get_ipython().system('ls')
432 get_ipython().getoutput('ls')
433 get_ipython().run_line_magic('pinfo', 'ls')
434 get_ipython().run_line_magic('pinfo2', 'ls')
436 and we look for instances of any of the latter.
438 if isinstance(node
.value
, ast
.Call
) and _is_ipython_magic(node
.value
.func
):
439 args
= _get_str_args(node
.value
.args
)
440 if node
.value
.func
.attr
== "run_line_magic":
441 if args
[0] == "pinfo":
443 elif args
[0] == "pinfo2":
449 elif node
.value
.func
.attr
== "system":
451 elif node
.value
.func
.attr
== "getoutput":
454 raise NothingChanged
# unsupported magic.
455 self
.magics
[node
.value
.lineno
].append(
456 OffsetAndMagic(node
.value
.col_offset
, src
)
458 self
.generic_visit(node
)