<jupyter_start><jupyter_text>Command line functions> Console commands added by the nbdev library<jupyter_code>%nbdev_export
from nbdev.imports import *
from nbdev.export import *
from nbdev.sync import *
from nbdev.merge import *
from nbdev.export2html import *
from nbdev.test import *
from fastscript import call_parse,Param,bool_arg<jupyter_output><empty_output><jupyter_text>`nbdev` comes with the following commands. To use any of them, you must be in one of the subfolders of your project: they will search for the `settings.ini` recursively in the parent directory but need to access it to be able to work. Their names all begin with nbdev so you can easily get a list with tab completion.- `nbdev_build_docs` builds the documentation from the notebooks- `nbdev_build_lib` builds the library from the notebooks- `nbdev_bump_version` increments version in `settings.py` by one- `nbdev_clean_nbs` removes all superfluous metadata form the notebooks, to avoid merge conflicts- `nbdev_detach` exports cell attachments to `dest` and updates references- `nbdev_diff_nbs` gives you the diff between the notebooks and the exported library- `nbdev_fix_merge` will fix merge conflicts in a notebook file- `nbdev_install_git_hooks` installs the git hooks that use the last two command automatically on each commit/merge- `nbdev_nb2md` converts a notebook to a markdown file- `nbdev_new` creates a new nbdev project- `nbdev_read_nbs` reads all notebooks to make sure none are broken- `nbdev_test_nbs` runs tests in notebooks- `nbdev_trust_nbs` trusts all notebooks (so that the HTML content is shown)- `nbdev_update_lib` propagates any change in the library back to the notebooks- `nbdev_upgrade` updates an existing nbdev project to use the latest features  Migrate from comment flags to magic flags<jupyter_code>%nbdev_export
import re,nbformat
from nbdev.export import _mk_flag_re, _re_all_def
from nbdev.flags import parse_line<jupyter_output><empty_output><jupyter_text>Migrating notebooksRun `nbdev_upgrade` from the command line to update code cells in notebooks that use comment flags like```pythonexport special.module```to use magic flags```python%nbdev_export special.module```To make the magic flags work, `nbdev_upgrade` might need to add a new code cell to the top of the notebook```pythonfrom nbdev import *``` Hiding the `from nbdev import *` cellnbdev does not treat `from nbdev import *` as special, but this cell can be hidden from the docs by combining it with `%nbdev_default_export`. e.g. ```pythonfrom nbdev import *%nbdev_default_export my_module```this works because nbdev will hide any code cell containing the `%nbdev_default_export` flag.If you don't need `%nbdev_default_export` in your notebook you can: use the hide input [jupyter extension](https://github.com/ipython-contrib/jupyter_contrib_nbextensions) or edit cell metadata to include `"hide_input": true`<jupyter_code>%nbdev_export_internal
def _code_patterns_and_replace_fns():
    "Return a list of pattern/function tuples that can migrate flags used in code cells"
    patterns_and_replace_fns = []

    def _replace_fn(magic, m):
        "Return a magic flag for a comment flag matched in `m`"
        if not m.groups() or not m.group(1): return f'%{magic}'
        return f'%{magic}' if m.group(1) is None else f'%{magic} {m.group(1)}'

    def _add_pattern_and_replace_fn(comment_flag, magic_flag, n_params=(0,1)):
        "Add a pattern/function tuple to go from comment to magic flag"
        pattern = _mk_flag_re(False, comment_flag, n_params, "")
        # note: fn has to be single arg so we can use it in `pattern.sub` calls later
        patterns_and_replace_fns.append((pattern, partial(_replace_fn, magic_flag)))

    _add_pattern_and_replace_fn('default_exp', 'nbdev_default_export', 1)
    _add_pattern_and_replace_fn('exports', 'nbdev_export_and_show')
    _add_pattern_and_replace_fn('exporti', 'nbdev_export_internal')
    _add_pattern_and_replace_fn('export', 'nbdev_export')
    _add_pattern_and_replace_fn('hide_input', 'nbdev_hide_input', 0)
    _add_pattern_and_replace_fn('hide_output', 'nbdev_hide_output', 0)
    _add_pattern_and_replace_fn('hide', 'nbdev_hide', 0) # keep at index 6 - see _migrate2magic
    _add_pattern_and_replace_fn('default_cls_lvl', 'nbdev_default_class_level', 1)
    _add_pattern_and_replace_fn('collapse[_-]output', 'nbdev_collapse_output', 0)
    _add_pattern_and_replace_fn('collapse[_-]show', 'nbdev_collapse_input open', 0)
    _add_pattern_and_replace_fn('collapse[_-]hide', 'nbdev_collapse_input', 0)
    _add_pattern_and_replace_fn('collapse', 'nbdev_collapse_input', 0)
    for flag in Config().get('tst_flags', '').split('|'):
        if flag.strip():
            _add_pattern_and_replace_fn(f'all_{flag}', f'nbdev_{flag}_test all', 0)
            _add_pattern_and_replace_fn(flag, f'nbdev_{flag}_test', 0)
    patterns_and_replace_fns.append(
        (_re_all_def, lambda m: '%nbdev_add2all ' + ','.join(parse_line(m.group(1)))))
    return patterns_and_replace_fns
%nbdev_export_internal
class CellMigrator():
    "Can migrate a cell using `patterns_and_replace_fns`"
    def __init__(self, patterns_and_replace_fns):
        self.patterns_and_replace_fns,self.upd_count,self.first_cell=patterns_and_replace_fns,0,None
    def __call__(self, cell):
        if self.first_cell is None: self.first_cell = cell
        for pattern, replace_fn in self.patterns_and_replace_fns:
            source=cell.source
            cell.source=pattern.sub(replace_fn, source)
            if source!=cell.source: self.upd_count+=1
%nbdev_export_internal
def _migrate2magic(nb):
    "Migrate a single notebook"
    # migrate #hide in markdown
    m=CellMigrator(_code_patterns_and_replace_fns()[6:7])
    [m(cell) for cell in nb.cells if cell.cell_type=='markdown']
    # migrate everything in code_patterns_and_replace_fns in code cells
    m=CellMigrator(_code_patterns_and_replace_fns())
    [m(cell) for cell in nb.cells if cell.cell_type=='code']
    imp,fc='from nbdev import *',m.first_cell
    if m.upd_count!=0 and fc is not None and imp not in fc.get('source', ''):
        nb.cells.insert(0, nbformat.v4.new_code_cell(imp, metadata={'hide_input': True}))
    NotebookNotary().sign(nb)
    return nb
%nbdev_hide
def remove_line(starting_with, from_string):
    result=[]
    for line in from_string.split('\n'):
        if not line.strip().startswith(starting_with):
            result.append(line)
    return '\n'.join(result)

test_lines = 'line1\n%magic\n#comment\n123\n %magic\n # comment'
test_eq('line1\n%magic\n123\n %magic', remove_line('#', test_lines))
test_eq('line1\n123', remove_line('%', 'line1\n%magic\n123\n %magic'))
%nbdev_hide
def remove_comments_and_magics(string):
    return remove_line('#', remove_line('%', string))

test_eq('line1\n123', remove_comments_and_magics(test_lines))
%nbdev_hide
def test_migrate2magic(fname):
    "Check that nothing other that comments and magics in code cells have been changed"
    nb=read_nb(fname)
    nb_migrated=_migrate2magic(read_nb(fname))
    test_eq(len(nb.cells)+1, len(nb_migrated.cells))
    test_eq('from nbdev import *', nb_migrated.cells[0].source)
    for i in range(len(nb.cells)):
        cell, cell_migrated=nb.cells[i], nb_migrated.cells[i+1]
        if cell.cell_type=='code':
            cell.source=remove_comments_and_magics(cell.source)
            cell_migrated.source=remove_comments_and_magics(cell_migrated.source)
        test_eq(cell, cell_migrated)
        
test_migrate2magic('../test/00_export.ipynb')
test_migrate2magic('../test/07_clean.ipynb')

def test_migrate2magic_noop(fname):
    "Check that nothing is changed if there are no comment flags in a notebook"
    nb=read_nb(fname)
    nb_migrated=_migrate2magic(read_nb(fname))
    test_eq(nb, nb_migrated)
        
test_migrate2magic_noop('99_search.ipynb')
%nbdev_hide
sources=['#export aaa\nimport io,sys,json,glob\n#collapse-OUTPUT\nfrom fastscript ...',
        '%nbdev_export aaa\nimport io,sys,json,glob\n%nbdev_collapse_output\nfrom fastscript ...',
        '#EXPORT\n # collapse\nimport io,sys,json,glob',
        '%nbdev_export\n%nbdev_collapse_input\nimport io,sys,json,glob',
        '#exportS\nimport io,sys,json,glob\n#colLApse_show',
        '%nbdev_export_and_show\nimport io,sys,json,glob\n%nbdev_collapse_input open',
        ' # collapse-show \n#exporti\nimport io,sys,json,glob',
        '%nbdev_collapse_input open\n%nbdev_export_internal\nimport io,sys,json,glob',
        '#export\t\tspecial.module  \nimport io,sys,json,glob',
        '%nbdev_export special.module\nimport io,sys,json,glob',
        '#exports special.module\nimport io,sys,json,glob',
        '%nbdev_export_and_show special.module\nimport io,sys,json,glob',
        '#EXPORT   special.module\nimport io,sys,json,glob',
        '%nbdev_export special.module\nimport io,sys,json,glob',
        '#exportI \t \tspecial.module\n# collapse_hide  \nimport io,sys,json,glob',
        '%nbdev_export_internal special.module\n%nbdev_collapse_input\nimport io,sys,json,glob',
        '# export \nimport io,sys,json,glob',
        '%nbdev_export\nimport io,sys,json,glob',
        ' # export\nimport io,sys,json,glob',
        '%nbdev_export\nimport io,sys,json,glob',
        '#default_cls_lvl ',
        '#default_cls_lvl ',
        '#default_cls_lvl 3 another',
        '#default_cls_lvl 3 another',
        '#default_cls_lvl 3',
        '%nbdev_default_class_level 3',
        'from nbdev import *\n # default_cls_lvl 3',
        'from nbdev import *\n%nbdev_default_class_level 3',
        ' #  Collapse-Hide \n # EXPORTS\nimport io,sys,json,glob',
        '%nbdev_collapse_input\n%nbdev_export_and_show\nimport io,sys,json,glob',
        ' # exporti\nimport io,sys,json,glob',
        '%nbdev_export_internal\nimport io,sys,json,glob',
        'import io,sys,json,glob\n#export aaa\nfrom fastscript import call_pars...',
        'import io,sys,json,glob\n%nbdev_export aaa\nfrom fastscript import call_pars...',
        '#fastai2\nsome test code',
        '%nbdev_fastai2_test\nsome test code',
        '#fastai2 extra_comment\nsome test code', # test flags with "parameters" won't get migrated
        '#fastai2 extra_comment\nsome test code',
        '# all_fastai2  \nsome test code',
        '%nbdev_fastai2_test all\nsome test code',
        '# all_fastai2 extra_comment \nsome test code',
        '# all_fastai2 extra_comment \nsome test code',
        '#COLLAPSE_OUTPUT\nprint("lotsofoutput")',
        '%nbdev_collapse_output\nprint("lotsofoutput")',
        ' # hide\n#fastai2\ndef some_test():\n...',
        '%nbdev_hide\n%nbdev_fastai2_test\ndef some_test():\n...',
        '#comment\n# export \n_all_ = ["a",bb, "CCC" , \'d\'] \nmore code',
        '#comment\n%nbdev_export\n%nbdev_add2all "a",bb,"CCC",\'d\' \nmore code']
nb=nbformat.v4.new_notebook()
nb_expected=nbformat.v4.new_notebook()
nb_expected.cells.append(nbformat.v4.new_code_cell('from nbdev import *', metadata={'hide_input': True}))
for cells, source in zip([nb.cells,nb_expected.cells]*(len(sources)//2), sources):
    cells.append(nbformat.v4.new_code_cell(source))
nb_migrated=_migrate2magic(nb)
for cell_expected, cell_migrated in zip(nb_expected.cells, nb_migrated.cells):
    test_eq(cell_expected, cell_migrated)
%nbdev_hide
# hide is the only flag that we migrate in markdown
sources=['#export aaa\nimport io,sys,json,glob\n#collapse-OUTPUT\nfrom fastscript ...',
        '#export aaa\nimport io,sys,json,glob\n#collapse-OUTPUT\nfrom fastscript ...',
        '#exportI \t \tspecial.module\n# collapse_hide  \nimport io,sys,json,glob',
        '#exportI \t \tspecial.module\n# collapse_hide  \nimport io,sys,json,glob',
        'some text\n#hide\nwill still be hidden',
        'some text\n%nbdev_hide\nwill still be hidden',
        ' #  Collapse-Hide \n # EXPORTS\nimport io,sys,json,glob',
        ' #  Collapse-Hide \n # EXPORTS\nimport io,sys,json,glob',
        ' # hide\n\n\n#fastai2\ndef some_test():\n...',
        '%nbdev_hide\n\n\n#fastai2\ndef some_test():\n...']
nb=nbformat.v4.new_notebook()
nb_expected=nbformat.v4.new_notebook()
for cells, source in zip([nb.cells,nb_expected.cells]*(len(sources)//2), sources):
    cells.append(nbformat.v4.new_markdown_cell(source))
nb_migrated=_migrate2magic(nb)
for cell_expected, cell_migrated in zip(nb_expected.cells, nb_migrated.cells):
    test_eq(cell_expected, cell_migrated)<jupyter_output><empty_output><jupyter_text>Add css for "collapse" componentsIf you want to use collapsable cells in your HTML docs, you need to style the details tag in customstyles.css. `_add_collapse_css` will do this for you, if the details tag is not already styled.<jupyter_code>%nbdev_export_internal
_details_description_css = """\n
/*Added by nbdev add_collapse_css*/
details.description[open] summary::after {
    content: attr(data-open);
}

details.description:not([open]) summary::after {
    content: attr(data-close);
}

details.description summary {
    text-align: right;
    font-size: 15px;
    color: #337ab7;
    cursor: pointer;
}

details + div.output_wrapper {
    /* show/hide code */
    margin-top: 25px;
}

div.input + details {
    /* show/hide output */
    margin-top: -25px;
}
/*End of section added by nbdev add_collapse_css*/"""

def _add_collapse_css(doc_path=None):
    "Update customstyles.css so that collapse components can be used in HTML pages"
    fn = (Path(doc_path) if doc_path else Config().doc_path/'css')/'customstyles.css'
    with open(fn) as f:
        if 'details.description' in f.read():
            print('details.description already styled in customstyles.css, no changes made')
        else:
            with open(fn, 'a') as f: f.write(_details_description_css)
            print('details.description styles added to customstyles.css')
%nbdev_hide
with open('/tmp/customstyles.css', 'w') as f:
    f.write('/*test file*/')
_add_collapse_css('/tmp') # details.description styles added ...
with open('/tmp/customstyles.css') as f:
    test_eq(''.join(['/*test file*/', _details_description_css]), f.read())
with open('/tmp/customstyles.css', 'a') as f:
    f.write('\nmore things added after')
_add_collapse_css('/tmp') # details.description already styled ...
with open('/tmp/customstyles.css') as f:
    test_eq(''.join(['/*test file*/', _details_description_css, '\nmore things added after']), f.read())<jupyter_output>details.description styles added to customstyles.css
details.description already styled in customstyles.css, no changes made<jupyter_text>Upgrading existing nbdev projects to use new features<jupyter_code>%nbdev_export
@call_parse
def nbdev_upgrade(migrate2magic:Param("Migrate all notebooks in `nbs_path` to use magic flags", bool_arg)=True, 
                  add_collapse_css:Param("Add css for \"#collapse\" components", bool_arg)=True):
    "Update an existing nbdev project to use the latest features"
    if migrate2magic:
        for fname in Config().nbs_path.glob('*.ipynb'):
            print('Migrating', fname)
            nbformat.write(_migrate2magic(read_nb(fname)), str(fname), version=4)
    if add_collapse_css: _add_collapse_css()<jupyter_output><empty_output><jupyter_text>- `migrate2magic` reads *all* notebooks in `nbs_path` and migrates them in-place- `add_collapse_css` updates `customstyles.css` so that "collapse" components can be used in HTML pages  Navigating from notebooks to script and back<jupyter_code>%nbdev_export
@call_parse
def nbdev_build_lib(fname:Param("A notebook name or glob to convert", str)=None):
    "Export notebooks matching `fname` to python modules"
    write_tmpls()
    notebook2script(fname=fname)<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the whole library is built from the notebooks in the `lib_folder` set in your `settings.ini`.<jupyter_code>%nbdev_export
@call_parse
def nbdev_update_lib(fname:Param("A notebook name or glob to convert", str)=None):
    "Propagates any change in the modules matching `fname` to the notebooks that created them"
    script2notebook(fname=fname)<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the whole library is treated. Note that this tool is only designed for small changes such as typo or small bug fixes. You can't add new cells in notebook from the library.<jupyter_code>%nbdev_export
@call_parse
def nbdev_diff_nbs(): 
    "Prints the diff between an export of the library in notebooks and the actual modules"
    diff_nb_script()<jupyter_output><empty_output><jupyter_text>Extracting tests<jupyter_code>%nbdev_export
def _test_one(fname, flags=None, verbose=True):
    print(f"testing: {fname}")
    start = time.time()
    try: 
        test_nb(fname, flags=flags)
        return True,time.time()-start
    except Exception as e: 
        if "Kernel died before replying to kernel_info" in str(e):
            time.sleep(random.random())
            _test_one(fname, flags=flags)
        if verbose: print(f'Error in {fname}:\n{e}')
        return False,time.time()-start
%nbdev_export
@call_parse
def nbdev_test_nbs(fname:Param("A notebook name or glob to convert", str)=None,
                   flags:Param("Space separated list of flags", str)=None,
                   n_workers:Param("Number of workers to use", int)=None,
                   verbose:Param("Print errors along the way", bool)=True,
                   timing:Param("Timing each notebook to see the ones are slow", bool)=False):
    "Test in parallel the notebooks matching `fname`, passing along `flags`"
    if flags is not None: flags = flags.split(' ')
    if fname is None: 
        files = [f for f in Config().nbs_path.glob('*.ipynb') if not f.name.startswith('_')]
    else: files = glob.glob(fname)
    files = [Path(f).absolute() for f in sorted(files)]
    if len(files)==1 and n_workers is None: n_workers=0
    # make sure we are inside the notebook folder of the project
    os.chdir(Config().nbs_path)
    results = parallel(_test_one, files, flags=flags, verbose=verbose, n_workers=n_workers)
    passed,times = [r[0] for r in results],[r[1] for r in results]
    if all(passed): print("All tests are passing!")
    else:
        msg = "The following notebooks failed:\n"
        raise Exception(msg + '\n'.join([f.name for p,f in zip(passed,files) if not p]))
    if timing:
        for i,t in sorted(enumerate(times), key=lambda o:o[1], reverse=True): 
            print(f"Notebook {files[i].name} took {int(t)} seconds")<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the whole library is tested from the notebooks in the `lib_folder` set in your `settings.ini`.  Building documentation The following functions complete the ones in `export2html` to fully build the documentation of your library.<jupyter_code>%nbdev_export
_re_index = re.compile(r'^(?:\d*_|)index\.ipynb$')
%nbdev_export
def make_readme():
    "Convert the index notebook to README.md"
    index_fn = None
    for f in Config().nbs_path.glob('*.ipynb'):
        if _re_index.match(f.name): index_fn = f
    assert index_fn is not None, "Could not locate index notebook"
    print(f"converting {index_fn} to README.md")
    convert_md(index_fn, Config().config_file.parent, jekyll=False)
    n = Config().config_file.parent/index_fn.with_suffix('.md').name
    shutil.move(n, Config().config_file.parent/'README.md')
    if Path(Config().config_file.parent/'PRE_README.md').is_file():
        with open(Config().config_file.parent/'README.md', 'r') as f: readme = f.read()
        with open(Config().config_file.parent/'PRE_README.md', 'r') as f: pre_readme = f.read()
        with open(Config().config_file.parent/'README.md', 'w') as f: f.write(f'{pre_readme}\n{readme}')
%nbdev_export
@call_parse
def nbdev_build_docs(fname:Param("A notebook name or glob to convert", str)=None,
                     force_all:Param("Rebuild even notebooks that haven't changed", bool)=False,
                     mk_readme:Param("Also convert the index notebook to README", bool)=True,
                     n_workers:Param("Number of workers to use", int)=None):
    "Build the documentation by converting notebooks mathing `fname` to html"
    notebook2html(fname=fname, force_all=force_all, n_workers=n_workers)
    if fname is None: make_sidebar()
    if mk_readme: make_readme()<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the whole documentation is build from the notebooks in the `lib_folder` set in your `settings.ini`, only converting the ones that have been modified since the their corresponding html was last touched unless you pass `force_all=True`. The index is also converted to make the README file, unless you pass along `mk_readme=False`.<jupyter_code>%nbdev_export
@call_parse
def nbdev_nb2md(fname:Param("A notebook file name to convert", str),
                dest:Param("The destination folder", str)='.',
                img_path:Param("Folder to export images to")="",
                jekyll:Param("To use jekyll metadata for your markdown file or not", bool_arg)=False,):
    "Convert the notebook in `fname` to a markdown file"
    nb_detach_cells(fname, dest=img_path)
    convert_md(fname, dest, jekyll=jekyll, img_path=img_path)
%nbdev_export
@call_parse
def nbdev_detach(path_nb:Param("Path to notebook"),
                 dest:Param("Destination folder", str)="",
                 use_img:Param("Convert markdown images to img tags", bool_arg)=False):
    "Export cell attachments to `dest` and update references"
    nb_detach_cells(path_nb, dest=dest, use_img=use_img)<jupyter_output><empty_output><jupyter_text>Other utils<jupyter_code>%nbdev_export
@call_parse
def nbdev_read_nbs(fname:Param("A notebook name or glob to convert", str)=None):
    "Check all notebooks matching `fname` can be opened"
    files = Config().nbs_path.glob('**/*.ipynb') if fname is None else glob.glob(fname)
    for nb in files:
        try: _ = read_nb(nb)
        except Exception as e:
            print(f"{nb} is corrupted and can't be opened.")
            raise e<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the all the notebooks in `lib_folder` are checked.<jupyter_code>%nbdev_export
@call_parse
def nbdev_trust_nbs(fname:Param("A notebook name or glob to convert", str)=None,
                    force_all:Param("Trust even notebooks that haven't changed", bool)=False):
    "Trust noteboks matching `fname`"
    check_fname = Config().nbs_path/".last_checked"
    last_checked = os.path.getmtime(check_fname) if check_fname.exists() else None
    files = Config().nbs_path.glob('**/*.ipynb') if fname is None else glob.glob(fname)
    for fn in files:
        if last_checked and not force_all:
            last_changed = os.path.getmtime(fn)
            if last_changed < last_checked: continue
        nb = read_nb(fn)
        if not NotebookNotary().check_signature(nb): NotebookNotary().sign(nb)
    check_fname.touch(exist_ok=True)<jupyter_output><empty_output><jupyter_text>By default (`fname` left to `None`), the all the notebooks in `lib_folder` are trusted. To speed things up, only the ones touched since the last time this command was run are trusted unless you pass along `force_all=True`.<jupyter_code>%nbdev_export
@call_parse
def nbdev_fix_merge(fname:Param("A notebook filename to fix", str),
                    fast:Param("Fast fix: automatically fix the merge conflicts in outputs or metadata", bool)=True,
                    trust_us:Param("Use local outputs/metadata when fast mergning", bool)=True):
    "Fix merge conflicts in notebook `fname`"
    fix_conflicts(fname, fast=fast, trust_us=trust_us)<jupyter_output><empty_output><jupyter_text>When you have merge conflicts after a `git pull`, the notebook file will be broken and won't open in jupyter notebook anymore. This command fixes this by changing the notebook to a proper json file again and add markdown cells to signal the conflict, you just have to open that notebook again and look for `>>>>>>>` to see those conflicts and manually fix them. The old broken file is copied with a `.ipynb.bak` extension, so is still accessible in case the merge wasn't sucessful.Moreover, if `fast=True`, conflicts in outputs and metadata will automatically be fixed by using the local version if `trust_us=True`, the remote one if `trust_us=False`. With this option, it's very likely you won't have anything to do, unless there is a real conflict.<jupyter_code>%nbdev_export
def bump_version(version, part=2):
    version = version.split('.')
    version[part] = str(int(version[part]) + 1)
    for i in range(part+1, 3): version[i] = '0'
    return '.'.join(version)
test_eq(bump_version('0.1.1'   ), '0.1.2')
test_eq(bump_version('0.1.1', 1), '0.2.0')
%nbdev_export
@call_parse
def nbdev_bump_version(part:Param("Part of version to bump", int)=2):
    "Increment version in `settings.py` by one"
    cfg = Config()
    print(f'Old version: {cfg.version}')
    cfg.d['version'] = bump_version(Config().version, part)
    cfg.save()
    update_version()
    print(f'New version: {cfg.version}')<jupyter_output><empty_output><jupyter_text>Git hooks<jupyter_code>%nbdev_export
import subprocess
%nbdev_export
@call_parse
def nbdev_install_git_hooks():
    "Install git hooks to clean/trust notebooks automatically"
    try: path = Config().config_file.parent
    except: path = Path.cwd()
    fn = path/'.git'/'hooks'/'post-merge'
    #Trust notebooks after merge
    with open(fn, 'w') as f:
        f.write("""#!/bin/bash
echo "Trusting notebooks"
nbdev_trust_nbs
"""
        )
    os.chmod(fn, os.stat(fn).st_mode | stat.S_IEXEC)
    #Clean notebooks on commit/diff
    with open(path/'.gitconfig', 'w') as f:
        f.write("""# Generated by nbdev_install_git_hooks
#
# If you need to disable this instrumentation do:
#
# git config --local --unset include.path
#
# To restore the filter
#
# git config --local include.path .gitconfig
#
# If you see notebooks not stripped, checked the filters are applied in .gitattributes
#
[filter "clean-nbs"]
        clean = nbdev_clean_nbs --read_input_stream True
        smudge = cat
        required = true
[diff "ipynb"]
        textconv = nbdev_clean_nbs --disp True --fname
""")
    cmd = "git config --local include.path ../.gitconfig"
    print(f"Executing: {cmd}")
    result = subprocess.run(cmd.split(), shell=False, check=False, stderr=subprocess.PIPE)
    if result.returncode == 0:
        print("Success: hooks are installed and repo's .gitconfig is now trusted")
    else:
        print("Failed to trust repo's .gitconfig")
        if result.stderr: print(f"Error: {result.stderr.decode('utf-8')}")
    try: nb_path = Config().nbs_path
    except: nb_path = Path.cwd()
    with open(nb_path/'.gitattributes', 'w') as f:
        f.write("""**/*.ipynb filter=clean-nbs
**/*.ipynb diff=ipynb
"""
               )<jupyter_output><empty_output><jupyter_text>This command installs git hooks to make sure notebooks are cleaned before you commit them to GitHub and automatically trusted at each merge. To be more specific, this creates:- an executable '.git/hooks/post-merge' file that contains the command `nbdev_trust_nbs`- a `.gitconfig` file that uses `nbev_clean_nbs` has a filter/diff on all notebook files inside `nbs_folder` and a `.gitattributes` file generated in this folder (copy this file in other folders where you might have notebooks you want cleaned as well)  Starting a new project<jupyter_code>%nbdev_export
_template_git_repo = "https://github.com/fastai/nbdev_template.git"
%nbdev_export
@call_parse
def nbdev_new(name: Param("A directory to create the project in", str),
              template_git_repo: Param("url to template repo", str)=_template_git_repo):
    "Create a new nbdev project with a given name."
    
    path = Path(f"./{name}").absolute()
    
    if path.is_dir():
        print(f"Directory {path} already exists. Aborting.")
        return
    
    print(f"Creating a new nbdev project {name}.")
    
    def rmtree_onerror(func, path, exc_info):
        "Use with `shutil.rmtree` when you need to delete files/folders that might be read-only."
        os.chmod(path, stat.S_IWRITE)
        func(path)
    
    try:
        subprocess.run(['git', 'clone', f'{template_git_repo}', f'{path}'], check=True, timeout=5000)
        # Note: on windows, .git is created with a read-only flag 
        shutil.rmtree(path/".git", onerror=rmtree_onerror)
        subprocess.run("git init".split(), cwd=path, check=True)
        subprocess.run("git add .".split(), cwd=path, check=True)
        subprocess.run("git commit -am \"Initial\"".split(), cwd=path, check=True)
        
        print(f"Created a new repo for project {name}. Please edit settings.ini and run nbdev_build_lib to get started.")
    except Exception as e:
        print("An error occured while copying nbdev project template:")
        print(e)
        if os.path.isdir(path): 
            try:
                shutil.rmtree(path, onerror=rmtree_onerror)
            except Exception as e2:
                print(f"An error occured while cleaning up. Failed to delete {path}:")
                print(e2)<jupyter_output><empty_output><jupyter_text>`nbdev_new` is a command line tool that creates a new nbdev project based on the [nbdev_template repo](https://github.com/fastai/nbdev_template). You can use a custom template by passing in `template_git_repo`. It'll initialize a new git repository and commit the new project.After you run `nbdev_new`, please edit `settings.ini` and run `nbdev_build_lib`.  Export -<jupyter_code>%nbdev_hide
notebook2script()<jupyter_output>Converted 00_export.ipynb.
Converted 01_sync.ipynb.
Converted 02_showdoc.ipynb.
Converted 03_export2html.ipynb.
Converted 04_test.ipynb.
Converted 05_merge.ipynb.
Converted 06_cli.ipynb.
Converted 07_clean.ipynb.
Converted 08_flag_tests.ipynb.
Converted 99_search.ipynb.
Converted index.ipynb.
Converted tutorial.ipynb.
{'fraction_non_alphanumeric': 0.11514685735289204, 'fraction_numerical': 0.005680489190363217, 'mean_word_length': 4.183235192241081, 'pattern_counts': {'":': 1, '<': 72, '<?xml version=': 0, '>': 79, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '5523', 'license': 'Apache-2.0', 'n_tokens_mistral': 9956, 'n_tokens_neox': 9814, 'n_words': 2623, 'path': 'nbs/06_cli.ipynb', 'repo_name': 'jcornford/nbdev', 'size': '29927'}