11import asyncio
2+ import json
23from concurrent .futures import Executor , ThreadPoolExecutor , ProcessPoolExecutor
34from contextlib import contextmanager
45from datetime import datetime
4647from black .files import wrap_stream_for_windows
4748from black .parsing import InvalidInput # noqa F401
4849from black .parsing import lib2to3_parse , parse_ast , stringify_ast
50+ from black .handle_ipynb_magics import (
51+ mask_cell ,
52+ unmask_cell ,
53+ remove_trailing_semicolon ,
54+ put_trailing_semicolon_back ,
55+ )
4956
5057
5158# lib2to3 fork
@@ -196,6 +203,14 @@ def validate_regex(
196203 " when piping source on standard input)."
197204 ),
198205)
206+ @click .option (
207+ "--ipynb" ,
208+ is_flag = True ,
209+ help = (
210+ "Format all input files like ipynb notebooks regardless of file extension "
211+ "(useful when piping source on standard input)."
212+ ),
213+ )
199214@click .option (
200215 "-S" ,
201216 "--skip-string-normalization" ,
@@ -354,6 +369,7 @@ def main(
354369 color : bool ,
355370 fast : bool ,
356371 pyi : bool ,
372+ ipynb : bool ,
357373 skip_string_normalization : bool ,
358374 skip_magic_trailing_comma : bool ,
359375 experimental_string_processing : bool ,
@@ -390,6 +406,7 @@ def main(
390406 target_versions = versions ,
391407 line_length = line_length ,
392408 is_pyi = pyi ,
409+ is_ipynb = ipynb ,
393410 string_normalization = not skip_string_normalization ,
394411 magic_trailing_comma = not skip_magic_trailing_comma ,
395412 experimental_string_processing = experimental_string_processing ,
@@ -584,6 +601,8 @@ def reformat_one(
584601 if is_stdin :
585602 if src .suffix == ".pyi" :
586603 mode = replace (mode , is_pyi = True )
604+ elif src .suffix == ".ipynb" :
605+ mode = replace (mode , is_ipynb = True )
587606 if format_stdin_to_stdout (fast = fast , write_back = write_back , mode = mode ):
588607 changed = Changed .YES
589608 else :
@@ -732,6 +751,8 @@ def format_file_in_place(
732751 """
733752 if src .suffix == ".pyi" :
734753 mode = replace (mode , is_pyi = True )
754+ elif src .suffix == ".ipynb" :
755+ mode = replace (mode , is_ipynb = True )
735756
736757 then = datetime .utcfromtimestamp (src .stat ().st_mtime )
737758 with open (src , "rb" ) as buf :
@@ -825,6 +846,9 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo
825846 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
826847 `mode` is passed to :func:`format_str`.
827848 """
849+ if mode .is_ipynb :
850+ return format_ipynb_string (src_contents , mode = mode , fast = fast )
851+
828852 if not src_contents .strip ():
829853 raise NothingChanged
830854
@@ -848,6 +872,52 @@ def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileCo
848872 return dst_contents
849873
850874
875+ def format_cell (src : str , * , mode : Mode ) -> str :
876+ src_without_trailing_semicolon , has_trailing_semicolon = remove_trailing_semicolon (
877+ src
878+ )
879+ try :
880+ masked_cell , replacements = mask_cell (src_without_trailing_semicolon )
881+ except SyntaxError :
882+ # Don't format, might be automagic or multi-line magic.
883+ raise NothingChanged
884+ formatted_masked_cell = format_str (masked_cell , mode = mode )
885+ formatted_cell = unmask_cell (formatted_masked_cell , replacements )
886+ new_src = put_trailing_semicolon_back (formatted_cell , has_trailing_semicolon )
887+ new_src = new_src .rstrip ("\n " )
888+ if new_src == src :
889+ raise NothingChanged
890+ return new_src
891+
892+
893+ def format_ipynb_string (
894+ src_contents : str , * , mode : Mode , fast : bool = False
895+ ) -> FileContent :
896+ nb = json .loads (src_contents )
897+ trailing_newline = src_contents [- 1 ] == "\n "
898+ modified = False
899+ for _ , cell in enumerate (nb ["cells" ]):
900+ if cell .get ("cell_type" , None ) == "code" :
901+ try :
902+ src = "" .join (cell ["source" ])
903+ new_src = format_cell (src , mode = mode )
904+ except NothingChanged :
905+ pass
906+ else :
907+ cell ["source" ] = new_src .splitlines (keepends = True )
908+ modified = True
909+
910+ if modified :
911+ res = json .dumps (nb , indent = 1 , ensure_ascii = False )
912+ if trailing_newline :
913+ res = res + "\n "
914+ if res == src_contents :
915+ raise NothingChanged
916+ return res
917+ else :
918+ raise NothingChanged
919+
920+
851921def format_str (src_contents : str , * , mode : Mode ) -> FileContent :
852922 """Reformat a string and return new contents.
853923
0 commit comments