docs for muutils v0.6.17
View Source on GitHub

# muutils.nbutils.convert_ipynb_to_script

fast conversion of Jupyter Notebooks to scripts, with some basic and hacky filtering and formatting.

View Source
View on GitHub

      1"""fast conversion of Jupyter Notebooks to scripts, with some basic and hacky filtering and formatting."""
      2
      3from __future__ import annotations
      4
      5import argparse
      6import json
      7import os
      8import sys
      9import typing
     10import warnings
     11
     12from muutils.spinner import SpinnerContext
     13
     14DISABLE_PLOTS: dict[str, list[str]] = {
     15    "matplotlib": [
     16        """
     17# ------------------------------------------------------------
     18# Disable matplotlib plots, done during processing by `convert_ipynb_to_script.py`
     19import matplotlib.pyplot as plt
     20plt.show = lambda: None
     21# ------------------------------------------------------------
     22"""
     23    ],
     24    "circuitsvis": [
     25        """
     26# ------------------------------------------------------------
     27# Disable circuitsvis plots, done during processing by `convert_ipynb_to_script.py`
     28from circuitsvis.utils.convert_props import PythonProperty, convert_props
     29from circuitsvis.utils.render import RenderedHTML, render, render_cdn, render_local
     30
     31def new_render(
     32    react_element_name: str,
     33    **kwargs: PythonProperty
     34) -> RenderedHTML:
     35    "return a visualization as raw HTML"
     36    local_src = render_local(react_element_name, **kwargs)
     37    cdn_src = render_cdn(react_element_name, **kwargs)
     38    # return as string instead of RenderedHTML for CI
     39    return str(RenderedHTML(local_src, cdn_src))
     40
     41render = new_render
     42# ------------------------------------------------------------
     43"""
     44    ],
     45    "muutils": [
     46        """import muutils.nbutils.configure_notebook as nb_conf
     47nb_conf.CONVERSION_PLOTMODE_OVERRIDE = "ignore"
     48"""
     49    ],
     50}
     51
     52DISABLE_PLOTS_WARNING: list[str] = [
     53    """
     54# ------------------------------------------------------------
     55# WARNING: this script is auto-generated by `convert_ipynb_to_script.py`
     56# showing plots has been disabled, so this is presumably in a temp dict for CI or something
     57# so don't modify this code, it will be overwritten!
     58# ------------------------------------------------------------
     59""".lstrip()
     60]
     61
     62
     63def disable_plots_in_script(script_lines: list[str]) -> list[str]:
     64    """Disable plots in a script by adding cursed things after the import statements"""
     65    result_str_TEMP: str = "\n\n".join(script_lines)
     66    script_lines_new: list[str] = script_lines
     67
     68    if "muutils" in result_str_TEMP:
     69        script_lines_new = DISABLE_PLOTS["muutils"] + script_lines_new
     70
     71    if "matplotlib" in result_str_TEMP:
     72        assert (
     73            "import matplotlib.pyplot as plt" in result_str_TEMP
     74        ), "matplotlib.pyplot must be imported as plt"
     75
     76        # find the last import statement involving matplotlib, and the first line that uses plt
     77        mpl_last_import_index: int = -1
     78        mpl_first_usage_index: int = -1
     79        for i, line in enumerate(script_lines_new):
     80            if "matplotlib" in line and (("import" in line) or ("from" in line)):
     81                mpl_last_import_index = i
     82
     83            if "configure_notebook" in line:
     84                mpl_last_import_index = i
     85
     86            if "plt." in line:
     87                mpl_first_usage_index = i
     88
     89        assert (
     90            mpl_last_import_index != -1
     91        ), f"matplotlib imports not found! see line {mpl_last_import_index}"
     92        if mpl_first_usage_index != -1:
     93            assert (
     94                mpl_first_usage_index > mpl_last_import_index
     95            ), f"matplotlib plots created before import! see lines {mpl_first_usage_index}, {mpl_last_import_index}"
     96        else:
     97            warnings.warn(
     98                "could not find where matplotlib is used, plot disabling might not work!"
     99            )
    100
    101        # insert the cursed things
    102        script_lines_new = (
    103            script_lines_new[: mpl_last_import_index + 1]
    104            + DISABLE_PLOTS["matplotlib"]
    105            + script_lines_new[mpl_last_import_index + 1 :]
    106        )
    107        result_str_TEMP = "\n\n".join(script_lines_new)
    108
    109    if "circuitsvis" in result_str_TEMP:
    110        # find the last import statement involving circuitsvis, and the first line that uses it
    111        cirv_last_import_index: int = -1
    112        cirv_first_usage_index: int = -1
    113
    114        for i, line in enumerate(script_lines_new):
    115            if "circuitsvis" in line:
    116                if (("import" in line) or ("from" in line)) and "circuitsvis" in line:
    117                    cirv_last_import_index = i
    118                else:
    119                    cirv_first_usage_index = i
    120
    121                if "configure_notebook" in line:
    122                    mpl_last_import_index = i
    123
    124                if "render" in line:
    125                    cirv_first_usage_index = i
    126
    127        assert (
    128            cirv_last_import_index != -1
    129        ), f"circuitsvis imports not found! see line {cirv_last_import_index}"
    130        if cirv_first_usage_index != -1:
    131            assert (
    132                cirv_first_usage_index > cirv_last_import_index
    133            ), f"circuitsvis plots created before import! see lines {cirv_first_usage_index}, {cirv_last_import_index}"
    134        else:
    135            warnings.warn(
    136                "could not find where circuitsvis is used, plot disabling might not work!"
    137            )
    138
    139        # insert the cursed things
    140        script_lines_new = (
    141            script_lines_new[: cirv_last_import_index + 1]
    142            + DISABLE_PLOTS["circuitsvis"]
    143            + script_lines_new[cirv_last_import_index + 1 :]
    144        )
    145        result_str_TEMP = "\n\n".join(script_lines_new)
    146
    147    return script_lines_new
    148
    149
    150def convert_ipynb(
    151    notebook: dict,
    152    strip_md_cells: bool = False,
    153    header_comment: str = r"#%%",
    154    disable_plots: bool = False,
    155    filter_out_lines: str | typing.Sequence[str] = (
    156        "%",
    157        "!",
    158    ),  # ignore notebook magic commands and shell commands
    159) -> str:
    160    """Convert Jupyter Notebook to a script, doing some basic filtering and formatting.
    161
    162    # Arguments
    163        - `notebook: dict`: Jupyter Notebook loaded as json.
    164        - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    165        - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    166        - `disable_plots: bool = False`: Disable plots in the output script.
    167        - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
    168            if a string is passed, it will be split by char and each char will be treated as a separate filter.
    169
    170    # Returns
    171        - `str`: Converted script.
    172    """
    173
    174    if isinstance(filter_out_lines, str):
    175        filter_out_lines = tuple(filter_out_lines)
    176    filter_out_lines_set: set = set(filter_out_lines)
    177
    178    result: list[str] = []
    179
    180    all_cells: list[dict] = notebook["cells"]
    181
    182    for cell in all_cells:
    183        cell_type: str = cell["cell_type"]
    184
    185        if not strip_md_cells and cell_type == "markdown":
    186            result.append(f'{header_comment}\n"""\n{"".join(cell["source"])}\n"""')
    187        elif cell_type == "code":
    188            source: list[str] = cell["source"]
    189            if filter_out_lines:
    190                source = [
    191                    (
    192                        f"#{line}"
    193                        if any(
    194                            line.startswith(filter_prefix)
    195                            for filter_prefix in filter_out_lines_set
    196                        )
    197                        else line
    198                    )
    199                    for line in source
    200                ]
    201            result.append(f'{header_comment}\n{"".join(source)}')
    202
    203    if disable_plots:
    204        result = disable_plots_in_script(result)
    205        result = DISABLE_PLOTS_WARNING + result
    206
    207    return "\n\n".join(result)
    208
    209
    210def process_file(
    211    in_file: str,
    212    out_file: str | None = None,
    213    strip_md_cells: bool = False,
    214    header_comment: str = r"#%%",
    215    disable_plots: bool = False,
    216    filter_out_lines: str | typing.Sequence[str] = ("%", "!"),
    217):
    218    print(f"\tProcessing {in_file}...", file=sys.stderr)
    219    assert os.path.exists(in_file), f"File {in_file} does not exist."
    220    assert os.path.isfile(in_file), f"Path {in_file} is not a file."
    221    assert in_file.endswith(".ipynb"), f"File {in_file} is not a Jupyter Notebook."
    222
    223    with open(in_file, "r") as file:
    224        notebook: dict = json.load(file)
    225
    226    try:
    227        converted_script: str = convert_ipynb(
    228            notebook=notebook,
    229            strip_md_cells=strip_md_cells,
    230            header_comment=header_comment,
    231            disable_plots=disable_plots,
    232            filter_out_lines=filter_out_lines,
    233        )
    234    except AssertionError as e:
    235        print(f"Error converting {in_file}: {e}", file=sys.stderr)
    236        raise e
    237
    238    if out_file:
    239        with open(out_file, "w") as file:
    240            file.write(converted_script)
    241    else:
    242        print(converted_script)
    243
    244
    245def process_dir(
    246    input_dir: str,
    247    output_dir: str,
    248    strip_md_cells: bool = False,
    249    header_comment: str = r"#%%",
    250    disable_plots: bool = False,
    251    filter_out_lines: str | typing.Sequence[str] = ("%", "!"),
    252):
    253    """Convert all Jupyter Notebooks in a directory to scripts.
    254
    255    # Arguments
    256        - `input_dir: str`: Input directory.
    257        - `output_dir: str`: Output directory.
    258        - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    259        - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    260        - `disable_plots: bool = False`: Disable plots in the output script.
    261        - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
    262            if a string is passed, it will be split by char and each char will be treated as a separate filter.
    263    """
    264
    265    assert os.path.exists(input_dir), f"Directory {input_dir} does not exist."
    266    assert os.path.isdir(input_dir), f"Path {input_dir} is not a directory."
    267
    268    if not os.path.exists(output_dir):
    269        os.makedirs(output_dir, exist_ok=True)
    270
    271    filenames: list[str] = [
    272        fname for fname in os.listdir(input_dir) if fname.endswith(".ipynb")
    273    ]
    274
    275    assert filenames, f"Directory {input_dir} does not contain any Jupyter Notebooks."
    276    n_files: int = len(filenames)
    277    print(f"Converting {n_files} notebooks:", file=sys.stderr)
    278
    279    with SpinnerContext(
    280        spinner_chars="braille",
    281        update_interval=0.01,
    282        format_string_when_updated=True,
    283        output_stream=sys.stderr,
    284    ) as spinner:
    285        for idx, fname in enumerate(filenames):
    286            spinner.update_value(f"\tConverting {idx+1}/{n_files}: {fname}")
    287            in_file: str = os.path.join(input_dir, fname)
    288            out_file: str = os.path.join(output_dir, fname.replace(".ipynb", ".py"))
    289
    290            with open(in_file, "r", encoding="utf-8") as file_in:
    291                notebook: dict = json.load(file_in)
    292
    293            try:
    294                converted_script: str = convert_ipynb(
    295                    notebook=notebook,
    296                    strip_md_cells=strip_md_cells,
    297                    header_comment=header_comment,
    298                    disable_plots=disable_plots,
    299                    filter_out_lines=filter_out_lines,
    300                )
    301            except AssertionError as e:
    302                spinner.stop()
    303                raise Exception(f"Error converting {in_file}") from e
    304
    305            with open(out_file, "w", encoding="utf-8") as file_out:
    306                file_out.write(converted_script)
    307
    308
    309if __name__ == "__main__":
    310    parser = argparse.ArgumentParser(
    311        description="Convert Jupyter Notebook to a script with cell separators."
    312    )
    313    parser.add_argument(
    314        "in_path",
    315        type=str,
    316        help="Input Jupyter Notebook file (.ipynb) or directory of files.",
    317    )
    318    parser.add_argument(
    319        "--out_file",
    320        type=str,
    321        help="Output script file. If not specified, the result will be printed to stdout.",
    322    )
    323    parser.add_argument(
    324        "--output_dir", type=str, help="Output directory for converted script files."
    325    )
    326    parser.add_argument(
    327        "--strip_md_cells",
    328        action="store_true",
    329        help="Remove markdown cells from the output script.",
    330    )
    331    parser.add_argument(
    332        "--header_comment",
    333        type=str,
    334        default=r"#%%",
    335        help="Comment string to separate cells in the output script.",
    336    )
    337    parser.add_argument(
    338        "--disable_plots",
    339        action="store_true",
    340        help="Disable plots in the output script. Useful for testing in CI.",
    341    )
    342    parser.add_argument(
    343        "--filter_out_lines",
    344        type=str,
    345        default="%",
    346        help="Comment out lines starting with these characters.",
    347    )
    348
    349    args = parser.parse_args()
    350
    351    if args.output_dir:
    352        assert not args.out_file, "Cannot specify both --out_file and --output_dir."
    353        process_dir(
    354            input_dir=args.in_path,
    355            output_dir=args.output_dir,
    356            strip_md_cells=args.strip_md_cells,
    357            header_comment=args.header_comment,
    358            disable_plots=args.disable_plots,
    359            filter_out_lines=args.filter_out_lines,
    360        )
    361
    362    else:
    363        process_file(
    364            in_file=args.in_path,
    365            out_file=args.out_file,
    366            strip_md_cells=args.strip_md_cells,
    367            header_comment=args.header_comment,
    368            disable_plots=args.disable_plots,
    369            filter_out_lines=args.filter_out_lines,
    370        )
    371
    372
    373print("convert_ipynb_to_script.py loaded.")

DISABLE_PLOTS: dict[str, list[str]] = {'matplotlib': ['\n# ------------------------------------------------------------\n# Disable matplotlib plots, done during processing by `convert_ipynb_to_script.py`\nimport matplotlib.pyplot as plt\nplt.show = lambda: None\n# ------------------------------------------------------------\n'], 'circuitsvis': ['\n# ------------------------------------------------------------\n# Disable circuitsvis plots, done during processing by `convert_ipynb_to_script.py`\nfrom circuitsvis.utils.convert_props import PythonProperty, convert_props\nfrom circuitsvis.utils.render import RenderedHTML, render, render_cdn, render_local\n\ndef new_render(\n react_element_name: str,\n **kwargs: PythonProperty\n) -> RenderedHTML:\n "return a visualization as raw HTML"\n local_src = render_local(react_element_name, **kwargs)\n cdn_src = render_cdn(react_element_name, **kwargs)\n # return as string instead of RenderedHTML for CI\n return str(RenderedHTML(local_src, cdn_src))\n\nrender = new_render\n# ------------------------------------------------------------\n'], 'muutils': ['import muutils.nbutils.configure_notebook as nb_conf\nnb_conf.CONVERSION_PLOTMODE_OVERRIDE = "ignore"\n']}
DISABLE_PLOTS_WARNING: list[str] = ["# ------------------------------------------------------------\n# WARNING: this script is auto-generated by `convert_ipynb_to_script.py`\n# showing plots has been disabled, so this is presumably in a temp dict for CI or something\n# so don't modify this code, it will be overwritten!\n# ------------------------------------------------------------\n"]
def disable_plots_in_script(script_lines: list[str]) -> list[str]:
View Source
View on GitHub

     64def disable_plots_in_script(script_lines: list[str]) -> list[str]:
     65    """Disable plots in a script by adding cursed things after the import statements"""
     66    result_str_TEMP: str = "\n\n".join(script_lines)
     67    script_lines_new: list[str] = script_lines
     68
     69    if "muutils" in result_str_TEMP:
     70        script_lines_new = DISABLE_PLOTS["muutils"] + script_lines_new
     71
     72    if "matplotlib" in result_str_TEMP:
     73        assert (
     74            "import matplotlib.pyplot as plt" in result_str_TEMP
     75        ), "matplotlib.pyplot must be imported as plt"
     76
     77        # find the last import statement involving matplotlib, and the first line that uses plt
     78        mpl_last_import_index: int = -1
     79        mpl_first_usage_index: int = -1
     80        for i, line in enumerate(script_lines_new):
     81            if "matplotlib" in line and (("import" in line) or ("from" in line)):
     82                mpl_last_import_index = i
     83
     84            if "configure_notebook" in line:
     85                mpl_last_import_index = i
     86
     87            if "plt." in line:
     88                mpl_first_usage_index = i
     89
     90        assert (
     91            mpl_last_import_index != -1
     92        ), f"matplotlib imports not found! see line {mpl_last_import_index}"
     93        if mpl_first_usage_index != -1:
     94            assert (
     95                mpl_first_usage_index > mpl_last_import_index
     96            ), f"matplotlib plots created before import! see lines {mpl_first_usage_index}, {mpl_last_import_index}"
     97        else:
     98            warnings.warn(
     99                "could not find where matplotlib is used, plot disabling might not work!"
    100            )
    101
    102        # insert the cursed things
    103        script_lines_new = (
    104            script_lines_new[: mpl_last_import_index + 1]
    105            + DISABLE_PLOTS["matplotlib"]
    106            + script_lines_new[mpl_last_import_index + 1 :]
    107        )
    108        result_str_TEMP = "\n\n".join(script_lines_new)
    109
    110    if "circuitsvis" in result_str_TEMP:
    111        # find the last import statement involving circuitsvis, and the first line that uses it
    112        cirv_last_import_index: int = -1
    113        cirv_first_usage_index: int = -1
    114
    115        for i, line in enumerate(script_lines_new):
    116            if "circuitsvis" in line:
    117                if (("import" in line) or ("from" in line)) and "circuitsvis" in line:
    118                    cirv_last_import_index = i
    119                else:
    120                    cirv_first_usage_index = i
    121
    122                if "configure_notebook" in line:
    123                    mpl_last_import_index = i
    124
    125                if "render" in line:
    126                    cirv_first_usage_index = i
    127
    128        assert (
    129            cirv_last_import_index != -1
    130        ), f"circuitsvis imports not found! see line {cirv_last_import_index}"
    131        if cirv_first_usage_index != -1:
    132            assert (
    133                cirv_first_usage_index > cirv_last_import_index
    134            ), f"circuitsvis plots created before import! see lines {cirv_first_usage_index}, {cirv_last_import_index}"
    135        else:
    136            warnings.warn(
    137                "could not find where circuitsvis is used, plot disabling might not work!"
    138            )
    139
    140        # insert the cursed things
    141        script_lines_new = (
    142            script_lines_new[: cirv_last_import_index + 1]
    143            + DISABLE_PLOTS["circuitsvis"]
    144            + script_lines_new[cirv_last_import_index + 1 :]
    145        )
    146        result_str_TEMP = "\n\n".join(script_lines_new)
    147
    148    return script_lines_new

Disable plots in a script by adding cursed things after the import statements

def convert_ipynb( notebook: dict, strip_md_cells: bool = False, header_comment: str = '#%%', disable_plots: bool = False, filter_out_lines: Union[str, Sequence[str]] = ('%', '!')) -> str:
View Source
View on GitHub

    151def convert_ipynb(
    152    notebook: dict,
    153    strip_md_cells: bool = False,
    154    header_comment: str = r"#%%",
    155    disable_plots: bool = False,
    156    filter_out_lines: str | typing.Sequence[str] = (
    157        "%",
    158        "!",
    159    ),  # ignore notebook magic commands and shell commands
    160) -> str:
    161    """Convert Jupyter Notebook to a script, doing some basic filtering and formatting.
    162
    163    # Arguments
    164        - `notebook: dict`: Jupyter Notebook loaded as json.
    165        - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    166        - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    167        - `disable_plots: bool = False`: Disable plots in the output script.
    168        - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
    169            if a string is passed, it will be split by char and each char will be treated as a separate filter.
    170
    171    # Returns
    172        - `str`: Converted script.
    173    """
    174
    175    if isinstance(filter_out_lines, str):
    176        filter_out_lines = tuple(filter_out_lines)
    177    filter_out_lines_set: set = set(filter_out_lines)
    178
    179    result: list[str] = []
    180
    181    all_cells: list[dict] = notebook["cells"]
    182
    183    for cell in all_cells:
    184        cell_type: str = cell["cell_type"]
    185
    186        if not strip_md_cells and cell_type == "markdown":
    187            result.append(f'{header_comment}\n"""\n{"".join(cell["source"])}\n"""')
    188        elif cell_type == "code":
    189            source: list[str] = cell["source"]
    190            if filter_out_lines:
    191                source = [
    192                    (
    193                        f"#{line}"
    194                        if any(
    195                            line.startswith(filter_prefix)
    196                            for filter_prefix in filter_out_lines_set
    197                        )
    198                        else line
    199                    )
    200                    for line in source
    201                ]
    202            result.append(f'{header_comment}\n{"".join(source)}')
    203
    204    if disable_plots:
    205        result = disable_plots_in_script(result)
    206        result = DISABLE_PLOTS_WARNING + result
    207
    208    return "\n\n".join(result)

Convert Jupyter Notebook to a script, doing some basic filtering and formatting.

# Arguments

    - `notebook: dict`: Jupyter Notebook loaded as json.
    - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    - `disable_plots: bool = False`: Disable plots in the output script.
    - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
        if a string is passed, it will be split by char and each char will be treated as a separate filter.

# Returns

    - `str`: Converted script.

def process_file( in_file: str, out_file: str | None = None, strip_md_cells: bool = False, header_comment: str = '#%%', disable_plots: bool = False, filter_out_lines: Union[str, Sequence[str]] = ('%', '!')):
View Source
View on GitHub

    211def process_file(
    212    in_file: str,
    213    out_file: str | None = None,
    214    strip_md_cells: bool = False,
    215    header_comment: str = r"#%%",
    216    disable_plots: bool = False,
    217    filter_out_lines: str | typing.Sequence[str] = ("%", "!"),
    218):
    219    print(f"\tProcessing {in_file}...", file=sys.stderr)
    220    assert os.path.exists(in_file), f"File {in_file} does not exist."
    221    assert os.path.isfile(in_file), f"Path {in_file} is not a file."
    222    assert in_file.endswith(".ipynb"), f"File {in_file} is not a Jupyter Notebook."
    223
    224    with open(in_file, "r") as file:
    225        notebook: dict = json.load(file)
    226
    227    try:
    228        converted_script: str = convert_ipynb(
    229            notebook=notebook,
    230            strip_md_cells=strip_md_cells,
    231            header_comment=header_comment,
    232            disable_plots=disable_plots,
    233            filter_out_lines=filter_out_lines,
    234        )
    235    except AssertionError as e:
    236        print(f"Error converting {in_file}: {e}", file=sys.stderr)
    237        raise e
    238
    239    if out_file:
    240        with open(out_file, "w") as file:
    241            file.write(converted_script)
    242    else:
    243        print(converted_script)

def process_dir( input_dir: str, output_dir: str, strip_md_cells: bool = False, header_comment: str = '#%%', disable_plots: bool = False, filter_out_lines: Union[str, Sequence[str]] = ('%', '!')):
View Source
View on GitHub

    246def process_dir(
    247    input_dir: str,
    248    output_dir: str,
    249    strip_md_cells: bool = False,
    250    header_comment: str = r"#%%",
    251    disable_plots: bool = False,
    252    filter_out_lines: str | typing.Sequence[str] = ("%", "!"),
    253):
    254    """Convert all Jupyter Notebooks in a directory to scripts.
    255
    256    # Arguments
    257        - `input_dir: str`: Input directory.
    258        - `output_dir: str`: Output directory.
    259        - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    260        - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    261        - `disable_plots: bool = False`: Disable plots in the output script.
    262        - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
    263            if a string is passed, it will be split by char and each char will be treated as a separate filter.
    264    """
    265
    266    assert os.path.exists(input_dir), f"Directory {input_dir} does not exist."
    267    assert os.path.isdir(input_dir), f"Path {input_dir} is not a directory."
    268
    269    if not os.path.exists(output_dir):
    270        os.makedirs(output_dir, exist_ok=True)
    271
    272    filenames: list[str] = [
    273        fname for fname in os.listdir(input_dir) if fname.endswith(".ipynb")
    274    ]
    275
    276    assert filenames, f"Directory {input_dir} does not contain any Jupyter Notebooks."
    277    n_files: int = len(filenames)
    278    print(f"Converting {n_files} notebooks:", file=sys.stderr)
    279
    280    with SpinnerContext(
    281        spinner_chars="braille",
    282        update_interval=0.01,
    283        format_string_when_updated=True,
    284        output_stream=sys.stderr,
    285    ) as spinner:
    286        for idx, fname in enumerate(filenames):
    287            spinner.update_value(f"\tConverting {idx+1}/{n_files}: {fname}")
    288            in_file: str = os.path.join(input_dir, fname)
    289            out_file: str = os.path.join(output_dir, fname.replace(".ipynb", ".py"))
    290
    291            with open(in_file, "r", encoding="utf-8") as file_in:
    292                notebook: dict = json.load(file_in)
    293
    294            try:
    295                converted_script: str = convert_ipynb(
    296                    notebook=notebook,
    297                    strip_md_cells=strip_md_cells,
    298                    header_comment=header_comment,
    299                    disable_plots=disable_plots,
    300                    filter_out_lines=filter_out_lines,
    301                )
    302            except AssertionError as e:
    303                spinner.stop()
    304                raise Exception(f"Error converting {in_file}") from e
    305
    306            with open(out_file, "w", encoding="utf-8") as file_out:
    307                file_out.write(converted_script)

Convert all Jupyter Notebooks in a directory to scripts.

# Arguments

    - `input_dir: str`: Input directory.
    - `output_dir: str`: Output directory.
    - `strip_md_cells: bool = False`: Remove markdown cells from the output script.
    - `header_comment: str = r'#%%'`: Comment string to separate cells in the output script.
    - `disable_plots: bool = False`: Disable plots in the output script.
    - `filter_out_lines: str|typing.Sequence[str] = ('%', '!')`: comment out lines starting with these strings (in code blocks).
        if a string is passed, it will be split by char and each char will be treated as a separate filter.