{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"hide_input": false
},
"outputs": [],
"source": [
"#hide\n",
"#default_exp export\n",
"#default_cls_lvl 3\n",
"from nbdev.showdoc import show_doc"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"hide_input": false
},
"outputs": [],
"source": [
"#export\n",
"from nbdev.imports import *\n",
"from fastcore.script import *\n",
"from fastcore.foundation import *\n",
"from keyword import iskeyword\n",
"import nbformat"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Export to modules\n",
"\n",
"> The functions that transform notebooks in a library"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The most important function defined in this module is `notebooks2script`, so you may want to jump to it before scrolling though the rest, which explain the details behind the scenes of the conversion from notebooks to library. The main things to remember are:\n",
"- put `# export` on each cell you want exported\n",
"- put `# exports` on each cell you want exported with the source code shown in the docs \n",
"- put `# exporti` on each cell you want exported without it being added to `__all__`, and without it showing up in the docs.\n",
"- one cell should contain `# default_exp` followed by the name of the module (with points for submodules and without the py extension) everything should be exported in (if one specific cell needs to be exported in a different module, just indicate it after `#export`: `#export special.module`)\n",
"- all left members of an equality, functions and classes will be exported and variables that are not private will be put in the `__all__` automatically\n",
"- to add something to `__all__` if it's not picked automatically, write an exported cell with something like `#add2all \"my_name\"`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Examples of `export`\n",
"\n",
"See [these examples](https://github.com/fastai/nbdev_export_demo/blob/master/demo.ipynb) on different ways to use `#export` to export code in notebooks to modules. These include:\n",
"\n",
"- How to specify a default for exporting cells\n",
"- How to hide code and not export it at all\n",
"- How to export different cells to specific modules"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic foundations"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For bootstrapping `nbdev` we have a few basic foundations defined in imports
, which we test a show here. First, a simple config file class, `Config` that read the content of your `settings.ini` file and make it accessible:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"data": {
"text/markdown": [
"
Config
[source]Config
(**`cfg_name`**=*`'settings.ini'`*)\n",
"\n",
"Reading and writing `settings.ini`"
],
"text/plain": [
"_nbdev
inside your library, and the following function are used to define it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"_re_index_custom = re.compile(r'def custom_doc_links\\(name\\):(.*)$', re.DOTALL)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def reset_nbdev_module():\n",
" \"Create a skeleton for _nbdev
\"\n",
" fname = Config().path(\"lib_path\")/'_nbdev.py'\n",
" fname.parent.mkdir(parents=True, exist_ok=True)\n",
" sep = '\\n'* (int(Config().get('cell_spacing', '1'))+1)\n",
" if fname.is_file():\n",
" with open(fname, 'r') as f: search = _re_index_custom.search(f.read())\n",
" else: search = None\n",
" prev_code = search.groups()[0] if search is not None else ' return None\\n'\n",
" with open(fname, 'w') as f:\n",
" f.write(f\"# AUTOGENERATED BY NBDEV! DO NOT EDIT!\")\n",
" f.write('\\n\\n__all__ = [\"index\", \"modules\", \"custom_doc_links\", \"git_url\"]')\n",
" f.write('\\n\\nindex = {}')\n",
" f.write('\\n\\nmodules = []')\n",
" f.write(f'\\n\\ndoc_url = \"{Config().doc_host}{Config().doc_baseurl}\"')\n",
" f.write(f'\\n\\ngit_url = \"{Config().git_url}\"')\n",
" f.write(f'{sep}def custom_doc_links(name):{prev_code}')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"class _EmptyModule():\n",
" def __init__(self):\n",
" self.index,self.modules = {},[]\n",
" self.doc_url,self.git_url = f\"{Config().doc_host}{Config().doc_baseurl}\",Config().git_url\n",
"\n",
" def custom_doc_links(self, name): return None"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def get_nbdev_module():\n",
" \"Reads _nbdev
\"\n",
" try:\n",
" spec = importlib.util.spec_from_file_location(f\"{Config().lib_name}._nbdev\", Config().path(\"lib_path\")/'_nbdev.py')\n",
" mod = importlib.util.module_from_spec(spec)\n",
" spec.loader.exec_module(mod)\n",
" return mod\n",
" except: return _EmptyModule()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"_re_index_idx = re.compile(r'index\\s*=\\s*{[^}]*}')\n",
"_re_index_mod = re.compile(r'modules\\s*=\\s*\\[[^\\]]*\\]')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def save_nbdev_module(mod):\n",
" \"Save `mod` inside _nbdev
\"\n",
" fname = Config().path(\"lib_path\")/'_nbdev.py'\n",
" with open(fname, 'r') as f: code = f.read()\n",
" t = r',\\n '.join([f'\"{k}\": \"{v}\"' for k,v in mod.index.items()])\n",
" code = _re_index_idx.sub(\"index = {\"+ t +\"}\", code)\n",
" t = r',\\n '.join(['\"' + f.replace('\\\\','/') + '\"' for f in mod.modules])\n",
" code = _re_index_mod.sub(f\"modules = [{t}]\", code)\n",
" with open(fname, 'w') as f: f.write(code)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"ind,ind_bak = Config().path(\"lib_path\")/'_nbdev.py',Config().path(\"lib_path\")/'_nbdev.bak'\n",
"if ind.exists(): shutil.move(ind, ind_bak)\n",
"try:\n",
" reset_nbdev_module()\n",
" mod = get_nbdev_module()\n",
" test_eq(mod.index, {})\n",
" test_eq(mod.modules, [])\n",
"\n",
" mod.index = {'foo':'bar'}\n",
" mod.modules.append('lala.bla')\n",
" save_nbdev_module(mod)\n",
"\n",
" mod = get_nbdev_module()\n",
" test_eq(mod.index, {'foo':'bar'})\n",
" test_eq(mod.modules, ['lala.bla'])\n",
"finally:\n",
" if ind_bak.exists(): shutil.move(ind_bak, ind)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Create the modules"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def split_flags_and_code(cell, return_type=list):\n",
" \"Splits the `source` of a cell into 2 parts and returns (flags, code)\"\n",
" code_lines = cell['source'].split('\\n')\n",
" split_pos = 0 if code_lines[0].strip().startswith('#') else -1\n",
" for i, line in enumerate(code_lines):\n",
" if not line.startswith('#') and line.strip() and not _re_from_future_import.match(line): break\n",
" split_pos+=1\n",
" res = code_lines[:split_pos], code_lines[split_pos:]\n",
" if return_type is list: return res\n",
" return tuple('\\n'.join(r) for r in res)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`return_type` tells us if the tuple returned will contain `list`s of lines or `str`ings with line breaks. \n",
"\n",
"We treat the first comment line as a flag"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def _test_split_flags_and_code(expected_flags, expected_code):\n",
" cell = nbformat.v4.new_code_cell('\\n'.join(expected_flags + expected_code))\n",
" test_eq((expected_flags, expected_code), split_flags_and_code(cell))\n",
" expected=('\\n'.join(expected_flags), '\\n'.join(expected_code))\n",
" test_eq(expected, split_flags_and_code(cell, str))\n",
" \n",
"_test_split_flags_and_code([\n",
" '#export'],\n",
" ['# TODO: write this function',\n",
" 'def func(x): pass'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def create_mod_file(fname, nb_path, bare=False):\n",
" \"Create a module file for `fname`.\"\n",
" bare = str(Config().get('bare', bare)) == 'True'\n",
" fname.parent.mkdir(parents=True, exist_ok=True)\n",
" file_path = os.path.relpath(nb_path, Config().config_file.parent).replace('\\\\', '/')\n",
" with open(fname, 'w') as f:\n",
" if not bare: f.write(f\"# AUTOGENERATED! DO NOT EDIT! File to edit: {file_path} (unless otherwise specified).\")\n",
" f.write('\\n\\n__all__ = []')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A new module filename is created each time a notebook has a cell marked with `#default_exp`. In your collection of notebooks, you should only have one notebook that creates a given module since they are re-created each time you do a library build (to ensure the library is clean). Note that any file you create manually will never be overwritten (unless it has the same name as one of the modules defined in a `#default_exp` cell) so you are responsible to clean up those yourself.\n",
"\n",
"`fname` is the notebook that contained the `#default_exp` cell."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def create_mod_files(files, to_dict=False, bare=False):\n",
" \"Create mod files for default exports found in `files`\"\n",
" modules = []\n",
" for f in files:\n",
" fname = Path(f)\n",
" nb = read_nb(fname)\n",
" default = find_default_export(nb['cells'])\n",
" if default is not None:\n",
" default = os.path.sep.join(default.split('.'))\n",
" modules.append(default)\n",
" if not to_dict:\n",
" create_mod_file(Config().path(\"lib_path\")/f'{default}.py', Config().path(\"nbs_path\")/f'{fname}', bare=bare)\n",
" return modules"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create module files for all `#default_export` flags found in `files` and return a list containing the names of modules created. \n",
"\n",
"Note: The number if modules returned will be less that the number of files passed in if files do not `#default_export`.\n",
"\n",
"By creating all module files before calling `_notebook2script`, the order of execution no longer matters - so you can now export to a notebook that is run \"later\".\n",
"\n",
"You might still have problems when\n",
"- converting a subset of notebooks or\n",
"- exporting to a module that does not have a `#default_export` yet\n",
"\n",
"in which case `_notebook2script` will print warnings like;\n",
"```\n",
"Warning: Exporting to \"core.py\" but this module is not part of this build\n",
"```\n",
"\n",
"If you see a warning like this\n",
"- and the module file (e.g. \"core.py\") does not exist, you'll see a `FileNotFoundError`\n",
"- if the module file exists, the exported cell will be written - even if the exported cell is already in the module file"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def _notebook2script(fname, modules, silent=False, to_dict=None, bare=False):\n",
" \"Finds cells starting with `#export` and puts them into a module created by `create_mod_files`\"\n",
" bare = str(Config().get('bare', bare)) == 'True'\n",
" if os.environ.get('IN_TEST',0): return # don't export if running tests\n",
" sep = '\\n'* (int(Config().get('cell_spacing', '1'))+1)\n",
" fname = Path(fname)\n",
" nb = read_nb(fname)\n",
" default = find_default_export(nb['cells'])\n",
" if default is not None:\n",
" default = os.path.sep.join(default.split('.'))\n",
" mod = get_nbdev_module()\n",
" exports = [is_export(c, default) for c in nb['cells']]\n",
" cells = [(i,c,e) for i,(c,e) in enumerate(zip(nb['cells'],exports)) if e is not None]\n",
" for i,c,(e,a) in cells:\n",
" if e not in modules: print(f'Warning: Exporting to \"{e}.py\" but this module is not part of this build')\n",
" fname_out = Config().path(\"lib_path\")/f'{e}.py'\n",
" if bare: orig = \"\\n\"\n",
" else: orig = (f'# {\"\" if a else \"Internal \"}C' if e==default else f'# Comes from {fname.name}, c') + 'ell\\n'\n",
" flag_lines,code_lines = split_flags_and_code(c)\n",
" code_lines = _deal_import(code_lines, fname_out)\n",
" code = sep + orig + '\\n'.join(code_lines)\n",
" names = export_names(code)\n",
" flags = '\\n'.join(flag_lines)\n",
" extra,code = extra_add(flags, code)\n",
" code = _from_future_import(fname_out, flags, code, to_dict)\n",
" if a:\n",
" if to_dict is None: _add2all(fname_out, [f\"'{f}'\" for f in names if '.' not in f and len(f) > 0] + extra)\n",
" mod.index.update({f: fname.name for f in names})\n",
" code = re.sub(r' +$', '', code, flags=re.MULTILINE)\n",
" if code != sep + orig[:-1]:\n",
" if to_dict is not None: to_dict[e].append((i, fname, code))\n",
" else:\n",
" with open(fname_out, 'a', encoding='utf8') as f: f.write(code)\n",
" if f'{e}.py' not in mod.modules: mod.modules.append(f'{e}.py')\n",
" save_nbdev_module(mod)\n",
"\n",
" if not silent: print(f\"Converted {fname.name}.\")\n",
" return to_dict"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Converted 00_export.ipynb.\n"
]
}
],
"source": [
"#hide\n",
"if not os.environ.get('IN_TEST',0):\n",
" modules = create_mod_files(glob.glob('00_export.ipynb'))\n",
" _notebook2script('00_export.ipynb', modules)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"with open(Config().path(\"lib_path\")/('export.py')) as f: l = f.readline()\n",
"test_eq(l, '# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/00_export.ipynb (unless otherwise specified).\\n')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def add_init(path):\n",
" \"Add `__init__.py` in all subdirs of `path` containing python files if it's not there already\"\n",
" for p,d,f in os.walk(path):\n",
" for f_ in f:\n",
" if f_.endswith('.py'):\n",
" if not (Path(p)/'__init__.py').exists(): (Path(p)/'__init__.py').touch()\n",
" break"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"with tempfile.TemporaryDirectory() as d:\n",
" os.makedirs(Path(d)/'a', exist_ok=True)\n",
" (Path(d)/'a'/'f.py').touch()\n",
" os.makedirs(Path(d)/'a/b', exist_ok=True)\n",
" (Path(d)/'a'/'b'/'f.py').touch()\n",
" add_init(d)\n",
" assert not (Path(d)/'__init__.py').exists()\n",
" for e in [Path(d)/'a', Path(d)/'a/b']:\n",
" assert (e/'__init__.py').exists()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"_re_version = re.compile('^__version__\\s*=.*$', re.MULTILINE)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def update_version():\n",
" \"Add or update `__version__` in the main `__init__.py` of the library\"\n",
" fname = Config().path(\"lib_path\")/'__init__.py'\n",
" if not fname.exists(): fname.touch()\n",
" version = f'__version__ = \"{Config().version}\"'\n",
" with open(fname, 'r') as f: code = f.read()\n",
" if _re_version.search(code) is None: code = version + \"\\n\" + code\n",
" else: code = _re_version.sub(version, code)\n",
" with open(fname, 'w') as f: f.write(code)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"_re_baseurl = re.compile('^baseurl\\s*:.*$', re.MULTILINE)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def update_baseurl():\n",
" \"Add or update `baseurl` in `_config.yml` for the docs\"\n",
" fname = Config().path(\"doc_path\")/'_config.yml'\n",
" if not fname.exists(): return\n",
" with open(fname, 'r') as f: code = f.read()\n",
" if _re_baseurl.search(code) is None: code = code + f\"\\nbaseurl: {Config().doc_baseurl}\"\n",
" else: code = _re_baseurl.sub(f\"baseurl: {Config().doc_baseurl}\", code)\n",
" with open(fname, 'w') as f: f.write(code)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"#hide\n",
"def _get_paths(pth:str, names:list): return [Path(pth)/n for n in names if '/.' not in pth]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def nbglob(fname=None, recursive=None, extension='.ipynb', config_key='nbs_path') -> L:\n",
" \"Find all files in a directory matching an extension given a `config_key`. Ignores hidden directories and filenames starting with `_`\"\n",
" fname = Config().path(config_key) if fname is None else Path(fname)\n",
" if fname.is_file(): return L([fname])\n",
" if recursive: fls = L(os.walk(fname)).map(lambda x: _get_paths(x[0], x[2])).concat()\n",
" else: fls = fname.glob(f'*{extension}')\n",
" return L(fls).filter(lambda x: not x.name.startswith('_') and x.name.endswith(extension))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Optionally you can pass a `config_key` to dictate which directory you are pointing to. By default it's `nbs_path` as without any parameters passed in, it will check for notebooks. To have it instead find library files simply pass in `lib_path` instead.\n",
"\n",
"> Note: it will only search for paths in `Config().path`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"assert not nbglob().filter(lambda x: '.ipynb_checkpoints' in str(x))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"fnames = nbglob()\n",
"test_eq(len(fnames) > 0, True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"fnames = nbglob(fnames[0])\n",
"test_eq(len(fnames), 1)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"fnames = nbglob(extension='.py', config_key='lib_path')\n",
"test_eq(len(fnames) > 1, True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"def notebook2script(fname=None, silent=False, to_dict=False, bare=False, recursive=None):\n",
" \"Convert notebooks matching `fname` to modules\"\n",
" # initial checks\n",
" if os.environ.get('IN_TEST',0): return # don't export if running tests\n",
" if fname is None:\n",
" reset_nbdev_module()\n",
" update_version()\n",
" update_baseurl()\n",
" files = nbglob(fname=fname, recursive=recursive)\n",
" d = collections.defaultdict(list) if to_dict else None\n",
" modules = create_mod_files(files, to_dict, bare=bare)\n",
" for f in sorted(files): d = _notebook2script(f, modules, silent=silent, to_dict=d, bare=bare)\n",
" if to_dict: return d\n",
" else: add_init(Config().path(\"lib_path\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finds cells starting with `#export` and puts them into the appropriate module. If `fname` is not specified, this will convert all notebook not beginning with an underscore in the `nb_folder` defined in `setting.ini`. Otherwise `fname` can be a single filename or a glob expression.\n",
"\n",
"`silent` makes the command not print any statement and `to_dict` is used internally to convert the library to a dictionary. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#export\n",
"class DocsTestClass:\n",
" \"for tests only\"\n",
" def test(): pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#hide\n",
"#exporti\n",
"#for tests only\n",
"def update_lib_with_exporti_testfn(): pass"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Export -"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Converted 00_export.ipynb.\n",
"Converted 01_sync.ipynb.\n",
"Converted 02_showdoc.ipynb.\n",
"Converted 03_export2html.ipynb.\n",
"Converted 04_test.ipynb.\n",
"Converted 05_merge.ipynb.\n",
"Converted 06_cli.ipynb.\n",
"Converted 07_clean.ipynb.\n",
"Converted 99_search.ipynb.\n",
"Converted example.ipynb.\n",
"Converted index.ipynb.\n",
"Converted tutorial.ipynb.\n"
]
}
],
"source": [
"#hide\n",
"notebook2script()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"jupytext": {
"split_at_heading": true
},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}