Skip to content

Commit 20872fa

Browse files
committed
wip
1 parent 017aafe commit 20872fa

7 files changed

Lines changed: 556 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ src/_black_version.py
1818
*.swp
1919
.hypothesis/
2020
venv/
21+
.ipynb_checkpoints/

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def get_long_description() -> str:
8787
"colorama": ["colorama>=0.4.3"],
8888
"python2": ["typed-ast>=1.4.2"],
8989
"uvloop": ["uvloop>=0.15.2"],
90+
"jupyter": ["ipython>=7.8.0", "tokenize-rt>=3.2.0"],
9091
},
9192
test_suite="tests.test_black",
9293
classifiers=[

src/black/__init__.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import json
23
from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor
34
from contextlib import contextmanager
45
from datetime import datetime
@@ -46,6 +47,12 @@
4647
from black.files import wrap_stream_for_windows
4748
from black.parsing import InvalidInput # noqa F401
4849
from 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+
851921
def format_str(src_contents: str, *, mode: Mode) -> FileContent:
852922
"""Reformat a string and return new contents.
853923

0 commit comments

Comments
 (0)