#hide #default_exp export #default_cls_lvl 3 from nbdev.showdoc import show_doc #export from nbdev.imports import * from fastcore.script import * from fastcore.foundation import * from keyword import iskeyword import nbformat show_doc(Config, title_level=3) create_config("github", "nbdev", user='fastai', path='..', tst_flags='tst', cfg_name='test_settings.ini', recursive='False') cfg = Config(cfg_name='test_settings.ini') test_eq(cfg.lib_name, 'nbdev') test_eq(cfg.git_url, "https://github.com/fastai/nbdev/tree/master/") # test_eq(cfg.path("lib_path"), Path.cwd().parent/'nbdev') # test_eq(cfg.path("nbs_path"), Path.cwd()) # test_eq(cfg.path("doc_path"), Path.cwd().parent/'docs') test_eq(cfg.custom_sidebar, 'False') test_eq(cfg.recursive, 'False') #export def read_nb(fname): "Read the notebook in `fname`." with open(Path(fname),'r', encoding='utf8') as f: return nbformat.reads(f.read(), as_version=4) test_nb = read_nb('00_export.ipynb') test_nb.keys() test_nb['metadata'] f"{test_nb['nbformat']}.{test_nb['nbformat_minor']}" test_nb['cells'][0] #export def check_re(cell, pat, code_only=True): "Check if `cell` contains a line with regex `pat`" if code_only and cell['cell_type'] != 'code': return if isinstance(pat, str): pat = re.compile(pat, re.IGNORECASE | re.MULTILINE) cell_source = cell['source'].replace('\r', '') # Eliminate \r\n result = pat.search(cell_source) return result cell = test_nb['cells'][1].copy() assert check_re(cell, '#export') is not None assert check_re(cell, re.compile('#export')) is not None assert check_re(cell, '# bla') is None cell['cell_type'] = 'markdown' assert check_re(cell, '#export') is None assert check_re(cell, '#export', code_only=False) is not None #export def check_re_multi(cell, pats, code_only=True): "Check if `cell` contains a line matching any regex in `pats`, returning the first match found" return L(pats).map_first(partial(check_re, cell, code_only=code_only)) cell = test_nb['cells'][0].copy() cell['source'] = "a b c" assert check_re(cell, 'a') is not None assert check_re(cell, 'd') is None # show that searching with patterns ['d','b','a'] will match 'b' # i.e. 'd' is not found and we don't search for 'a' assert check_re_multi(cell, ['d','b','a']).span() == (2,3) #export def _mk_flag_re(body, n_params, comment): "Compiles a regex for finding nbdev flags" assert body!=True, 'magics no longer supported' prefix = r"\s*\#\s*" param_group = "" if n_params == -1: param_group = r"[ \t]+(.+)" if n_params == 1: param_group = r"[ \t]+(\S+)" if n_params == (0,1): param_group = r"(?:[ \t]+(\S+))?" return re.compile(rf""" # {comment}: ^ # beginning of line (since re.MULTILINE is passed) {prefix} {body} {param_group} [ \t]* # any number of spaces and/or tabs $ # end of line (since re.MULTILINE is passed) """, re.MULTILINE | re.VERBOSE) #hide re_blank_test = _mk_flag_re('export[si]?', 0, "test") re_mod_test = _mk_flag_re('export[si]?', 1, "test") re_opt_test = _mk_flag_re('export[si]?', (0,1), "test") for f in ['export', 'exports', 'exporti']: cell = nbformat.v4.new_code_cell(f'#{f} \n some code') assert check_re(cell, re_blank_test) is not None assert check_re(cell, re_mod_test) is None assert check_re(cell, re_opt_test) is not None test_eq(check_re(cell, re_opt_test).groups()[0], None) cell.source = f'#{f} special.module \n some code' assert check_re(cell, re_blank_test) is None assert check_re(cell, re_mod_test) is not None test_eq(check_re(cell, re_mod_test).groups()[0], 'special.module') assert check_re(cell, re_opt_test) is not None test_eq(check_re(cell, re_opt_test).groups()[0], 'special.module') #export _re_blank_export = _mk_flag_re("export[si]?", 0, "Matches any line with #export, #exports or #exporti without any module name") #export _re_mod_export = _mk_flag_re("export[si]?", 1, "Matches any line with #export, #exports or #exporti with a module name and catches it in group 1") #export _re_internal_export = _mk_flag_re("exporti", (0,1), "Matches any line with #exporti with or without a module name") #exporti def _is_external_export(tst): "Check if a cell is an external or internal export. `tst` is an re match" return _re_internal_export.search(tst.string) is None #export def is_export(cell, default): "Check if `cell` is to be exported and returns the name of the module to export it if provided" tst = check_re(cell, _re_blank_export) if tst: if default is None: print(f"No export destination, ignored:\n{cell['source']}") return default, _is_external_export(tst) tst = check_re(cell, _re_mod_export) if tst: return os.path.sep.join(tst.groups()[0].split('.')), _is_external_export(tst) else: return None cell = test_nb['cells'][1].copy() test_eq(is_export(cell, 'export'), ('export', True)) cell['source'] = "# exports" test_eq(is_export(cell, 'export'), ('export', True)) cell['source'] = "# exporti" test_eq(is_export(cell, 'export'), ('export', False)) cell['source'] = "# export mod" test_eq(is_export(cell, 'export'), ('mod', True)) #hide cell['source'] = "# export mod.file" test_eq(is_export(cell, 'export'), (f'mod{os.path.sep}file', True)) cell['source'] = "# exporti mod.file" test_eq(is_export(cell, 'export'), (f'mod{os.path.sep}file', False)) cell['source'] = "# expt mod.file" assert is_export(cell, 'export') is None cell['source'] = "# exportmod.file" assert is_export(cell, 'export') is None cell['source'] = "# exportsmod.file" assert is_export(cell, 'export') is None cell['source'] = "# exporti mod file" assert is_export(cell, 'export') is None #export _re_default_exp = _mk_flag_re('default_exp', 1, "Matches any line with #default_exp with a module name") #export def find_default_export(cells): "Find in `cells` the default export module." res = L(cells).map_first(check_re, pat=_re_default_exp) return res.groups()[0] if res else None test_eq(find_default_export(test_nb['cells']), 'export') assert find_default_export(test_nb['cells'][2:]) is None #hide mods = [f'mod{i}' for i in range(3)] cells = [{'cell_type': 'code', 'source': f'#default_exp {mod}'} for mod in mods] for i, mod in enumerate(mods): test_eq(mod, find_default_export(cells[i:])) #export _re_patch_func = re.compile(r""" # Catches any function decorated with @patch, its name in group 1 and the patched class in group 2 @patch # At any place in the cell, something that begins with @patch (?:\s*@.*)* # Any other decorator applied to the function \s*def # Any number of whitespace (including a new line probably) followed by def \s+ # One whitespace or more ([^\(\s]+) # Catch a group composed of anything but whitespace or an opening parenthesis (name of the function) \s*\( # Any number of whitespace followed by an opening parenthesis [^:]* # Any number of character different of : (the name of the first arg that is type-annotated) :\s* # A column followed by any number of whitespace (?: # Non-catching group with either ([^,\s\(\)]*) # a group composed of anything but a comma, a parenthesis or whitespace (name of the class) | # or (\([^\)]*\))) # a group composed of something between parenthesis (tuple of classes) \s* # Any number of whitespace (?:,|\)) # Non-catching group with either a comma or a closing parenthesis """, re.VERBOSE) tst = _re_patch_func.search(""" @patch @log_args(a=1) def func(obj:Class):""") tst, tst.groups() #hide tst = _re_patch_func.search(""" @patch def func(obj:Class):""") test_eq(tst.groups(), ("func", "Class", None)) tst = _re_patch_func.search(""" @patch def func (obj:Class, a)""") test_eq(tst.groups(), ("func", "Class", None)) tst = _re_patch_func.search(""" @patch def func (obj:(Class1, Class2), a)""") test_eq(tst.groups(), ("func", None, "(Class1, Class2)")) tst = _re_patch_func.search(""" @patch def func (obj:(Class1, Class2), a:int)->int:""") test_eq(tst.groups(), ("func", None, "(Class1, Class2)")) tst = _re_patch_func.search(""" @patch @log_args(but='a,b') @funcs_kwargs def func (obj:(Class1, Class2), a:int)->int:""") test_eq(tst.groups(), ("func", None, "(Class1, Class2)")) tst = _re_patch_func.search(""" @patch @contextmanager def func (obj:Class, a:int)->int:""") test_eq(tst.groups(), ("func", "Class", None)) #export _re_typedispatch_func = re.compile(r""" # Catches any function decorated with @typedispatch (@typedispatch # At any place in the cell, catch a group with something that begins with @typedispatch \s*def # Any number of whitespace (including a new line probably) followed by def \s+ # One whitespace or more [^\(]+ # Anything but whitespace or an opening parenthesis (name of the function) \s*\( # Any number of whitespace followed by an opening parenthesis [^\)]* # Any number of character different of ) \)[\s\S]*:) # A closing parenthesis followed by any number of characters and whitespace (type annotation) and : """, re.VERBOSE) #hide assert _re_typedispatch_func.search("@typedispatch\ndef func(a, b):").groups() == ('@typedispatch\ndef func(a, b):',) assert (_re_typedispatch_func.search("@typedispatch\ndef func(a:str, b:bool)->int:").groups() == ('@typedispatch\ndef func(a:str, b:bool)->int:',)) #export _re_class_func_def = re.compile(r""" # Catches any 0-indented function or class definition with its name in group 1 ^ # Beginning of a line (since re.MULTILINE is passed) (?:async\sdef|def|class) # Non-catching group for def or class \s+ # One whitespace or more ([^\(\s]+) # Catching group with any character except an opening parenthesis or a whitespace (name) \s* # Any number of whitespace (?:\(|:) # Non-catching group with either an opening parenthesis or a : (classes don't need ()) """, re.MULTILINE | re.VERBOSE) #hide test_eq(_re_class_func_def.search("class Class:").groups(), ('Class',)) test_eq(_re_class_func_def.search("def func(a, b):").groups(), ('func',)) test_eq(_re_class_func_def.search("def func(a:str, b:bool)->int:").groups(), ('func',)) test_eq(_re_class_func_def.search("async def func(a, b):").groups(), ('func',)) #export _re_obj_def = re.compile(r""" # Catches any 0-indented object definition (bla = thing) with its name in group 1 ^ # Beginning of a line (since re.MULTILINE is passed) ([_a-zA-Z]+[a-zA-Z0-9_\.]*) # Catch a group which is a valid python variable name \s* # Any number of whitespace (?::\s*\S.*|)= # Non-catching group of either a colon followed by a type annotation, or nothing; followed by an = """, re.MULTILINE | re.VERBOSE) #hide test_eq(_re_obj_def.search("a = 1").groups(), ('a',)) test_eq(_re_obj_def.search("a.b = 1").groups(), ('a.b',)) test_eq(_re_obj_def.search("_aA1=1").groups(), ('_aA1',)) test_eq(_re_obj_def.search("a : int =1").groups(), ('a',)) test_eq(_re_obj_def.search("a:f(':=')=1").groups(), ('a',)) assert _re_obj_def.search("@abc=2") is None assert _re_obj_def.search("a a=2") is None #export def _not_private(n): for t in n.split('.'): if (t.startswith('_') and not t.startswith('__')) or t.startswith('@'): return False return '\\' not in t and '^' not in t and '[' not in t and t != 'else' def export_names(code, func_only=False): "Find the names of the objects, functions or classes defined in `code` that are exported." #Format monkey-patches with @patch def _f(gps): nm, cls, t = gps.groups() if cls is not None: return f"def {cls}.{nm}():" return '\n'.join([f"def {c}.{nm}():" for c in re.split(', *', t[1:-1])]) code = _re_typedispatch_func.sub('', code) code = _re_patch_func.sub(_f, code) names = _re_class_func_def.findall(code) if not func_only: names += _re_obj_def.findall(code) return [n for n in names if _not_private(n) and not iskeyword(n)] test_eq(export_names("def my_func(x):\n pass\nclass MyClass():"), ["my_func", "MyClass"]) #hide #Indented funcs are ignored (funcs inside a class) test_eq(export_names(" def my_func(x):\n pass\nclass MyClass():"), ["MyClass"]) #Private funcs are ignored, dunder are not test_eq(export_names("def _my_func():\n pass\nclass MyClass():"), ["MyClass"]) test_eq(export_names("__version__ = 1:\n pass\nclass MyClass():"), ["MyClass", "__version__"]) #trailing spaces test_eq(export_names("def my_func ():\n pass\nclass MyClass():"), ["my_func", "MyClass"]) #class without parenthesis test_eq(export_names("def my_func ():\n pass\nclass MyClass:"), ["my_func", "MyClass"]) #object and funcs test_eq(export_names("def my_func ():\n pass\ndefault_bla=[]:"), ["my_func", "default_bla"]) test_eq(export_names("def my_func ():\n pass\ndefault_bla=[]:", func_only=True), ["my_func"]) #Private objects are ignored test_eq(export_names("def my_func ():\n pass\n_default_bla = []:"), ["my_func"]) #Objects with dots are privates if one part is private test_eq(export_names("def my_func ():\n pass\ndefault.bla = []:"), ["my_func", "default.bla"]) test_eq(export_names("def my_func ():\n pass\ndefault._bla = []:"), ["my_func"]) #Monkey-path with @patch are properly renamed test_eq(export_names("@patch\ndef my_func(x:Class):\n pass"), ["Class.my_func"]) test_eq(export_names("@patch\ndef my_func(x:Class):\n pass", func_only=True), ["Class.my_func"]) test_eq(export_names("some code\n@patch\ndef my_func(x:Class, y):\n pass"), ["Class.my_func"]) test_eq(export_names("some code\n@patch\ndef my_func(x:(Class1,Class2), y):\n pass"), ["Class1.my_func", "Class2.my_func"]) #Check delegates test_eq(export_names("@delegates(keep=True)\nclass someClass:\n pass"), ["someClass"]) #Typedispatch decorated functions shouldn't be added test_eq(export_names("@patch\ndef my_func(x:Class):\n pass\n@typedispatch\ndef func(x: TensorImage): pass"), ["Class.my_func"]) #try, except and other keywords should not be picked up (these can look like object def with type annotation) test_eq(export_names("try:\n a=1\nexcept:\n b=2"), []) test_eq(export_names("try:\n this_might_work\nexcept:\n b=2"), []) #export _re_all_def = re.compile(r""" # Catches a cell with defines \_all\_ = [\*\*] and get that \*\* in group 1 ^_all_ # Beginning of line (since re.MULTILINE is passed) \s*=\s* # Any number of whitespace, =, any number of whitespace \[ # Opening [ ([^\n\]]*) # Catching group with anything except a ] or newline \] # Closing ] """, re.MULTILINE | re.VERBOSE) #Same with __all__ _re__all__def = re.compile(r'^__all__\s*=\s*\[([^\]]*)\]', re.MULTILINE) #export def extra_add(flags, code): "Catch adds to `__all__` required by a cell with `_all_=`" m = check_re({'source': code}, _re_all_def, False) if m: code = m.re.sub('#nbdev_' + 'comment \g<0>', code) code = re.sub(r'([^\n]|^)\n*$', r'\1', code) if not m: return [], code def clean_quotes(s): "Return `s` enclosed in single quotes, removing double quotes if needed" if s.startswith("'") and s.endswith("'"): return s if s.startswith('"') and s.endswith('"'): s = s[1:-1] return f"'{s}'" return [clean_quotes(s) for s in parse_line(m.group(1))], code #hide for code, expected in [ ['_all_ = ["func", "func1", "func2"]', (["'func'", "'func1'", "'func2'"],'#nbdev_comment _all_ = ["func", "func1", "func2"]')], ['_all_=[func, func1, func2]', (["'func'", "'func1'", "'func2'"],'#nbdev_comment _all_=[func, func1, func2]')], ["_all_ = ['func', 'func1', 'func2']", (["'func'", "'func1'", "'func2'"],"#nbdev_comment _all_ = ['func', 'func1', 'func2']")], ['_all_ = ["func", "func1" , "func2"]', (["'func'", "'func1'", "'func2'"],'#nbdev_comment _all_ = ["func", "func1" , "func2"]')], ["_all_ = ['func','func1', 'func2']\n", (["'func'", "'func1'", "'func2'"],"#nbdev_comment _all_ = ['func','func1', 'func2']")], ["_all_ = ['func']\n_all_ = ['func1', 'func2']\n", (["'func'"],"#nbdev_comment _all_ = ['func']\n#nbdev_comment _all_ = ['func1', 'func2']")], ['code\n\n_all_ = ["func", "func1", "func2"]', (["'func'", "'func1'", "'func2'"],'code\n\n#nbdev_comment _all_ = ["func", "func1", "func2"]')], ['code\n\n_all_ = [func]\nmore code', (["'func'"],'code\n\n#nbdev_comment _all_ = [func]\nmore code')]]: test_eq(extra_add('', code), expected) # line breaks within the list of names means _all_ is ignored test_eq(extra_add('', "_all_ = ['func',\n'func1', 'func2']\n"), ([],"_all_ = ['func',\n'func1', 'func2']\n")) #export _re_from_future_import = re.compile(r"^from[ \t]+__future__[ \t]+import.*$", re.MULTILINE) def _from_future_import(fname, flags, code, to_dict=None): "Write `__future__` imports to `fname` and return `code` with `__future__` imports commented out" from_future_imports = _re_from_future_import.findall(code) if from_future_imports: code = _re_from_future_import.sub('#nbdev' + '_comment \g<0>', code) else: from_future_imports = _re_from_future_import.findall(flags) if not from_future_imports or to_dict is not None: return code with open(fname, 'r', encoding='utf8') as f: text = f.read() start = _re__all__def.search(text).start() with open(fname, 'w', encoding='utf8') as f: f.write('\n'.join([text[:start], *from_future_imports, '\n', text[start:]])) return code #hide txt = """ # AUTOHEADER ... File to edit: mod.ipynb (unless otherwise specified). __all__ = [my_file, MyClas] # Cell def valid_code(): pass""" expected_txt = """ # AUTOHEADER ... File to edit: mod.ipynb (unless otherwise specified). from __future__ import annotations from __future__ import generator_stop __all__ = [my_file, MyClas] # Cell def valid_code(): pass""" flags="# export" code = """ # comment from __future__ import annotations valid_code = False # but _from_future_import will work anyway from __future__ import generator_stop from __future__ import not_zero_indented valid_code = True """ expected_code = """ # comment #nbdev_comment from __future__ import annotations valid_code = False # but _from_future_import will work anyway #nbdev_comment from __future__ import generator_stop from __future__ import not_zero_indented valid_code = True """ def _run_from_future_import_test(): fname = 'test_from_future_import.txt' with open(fname, 'w', encoding='utf8') as f: f.write(txt) actual_code=_from_future_import(fname, flags, code, {}) test_eq(expected_code, actual_code) with open(fname, 'r', encoding='utf8') as f: test_eq(f.read(), txt) actual_code=_from_future_import(fname, flags, code) test_eq(expected_code, actual_code) with open(fname, 'r', encoding='utf8') as f: test_eq(f.read(), expected_txt) os.remove(fname) _run_from_future_import_test() flags="""from __future__ import annotations from __future__ import generator_stop #export""" code = "" expected_code = "" fname = 'test_from_future_import.txt' _run_from_future_import_test() #export def _add2all(fname, names, line_width=120): if len(names) == 0: return with open(fname, 'r', encoding='utf8') as f: text = f.read() tw = TextWrapper(width=120, initial_indent='', subsequent_indent=' '*11, break_long_words=False) re_all = _re__all__def.search(text) start,end = re_all.start(),re_all.end() text_all = tw.wrap(f"{text[start:end-1]}{'' if text[end-2]=='[' else ', '}{', '.join(names)}]") with open(fname, 'w', encoding='utf8') as f: f.write(text[:start] + '\n'.join(text_all) + text[end:]) #hide fname = 'test_add.txt' with open(fname, 'w', encoding='utf8') as f: f.write("Bla\n__all__ = [my_file, MyClas]\nBli") _add2all(fname, ['new_function']) with open(fname, 'r', encoding='utf8') as f: test_eq(f.read(), "Bla\n__all__ = [my_file, MyClas, new_function]\nBli") _add2all(fname, [f'new_function{i}' for i in range(10)]) with open(fname, 'r', encoding='utf8') as f: test_eq(f.read(), """Bla __all__ = [my_file, MyClas, new_function, new_function0, new_function1, new_function2, new_function3, new_function4, new_function5, new_function6, new_function7, new_function8, new_function9] Bli""") os.remove(fname) #export def relative_import(name, fname): "Convert a module `name` to a name relative to `fname`" mods = name.split('.') splits = str(fname).split(os.path.sep) if mods[0] not in splits: return name i=len(splits)-1 while i>0 and splits[i] != mods[0]: i-=1 splits = splits[i:] while len(mods)>0 and splits[0] == mods[0]: splits,mods = splits[1:],mods[1:] return '.' * (len(splits)) + '.'.join(mods) test_eq(relative_import('nbdev.core', Path.cwd()/'nbdev'/'data.py'), '.core') test_eq(relative_import('nbdev.core', Path('nbdev')/'vision'/'data.py'), '..core') test_eq(relative_import('nbdev.vision.transform', Path('nbdev')/'vision'/'data.py'), '.transform') test_eq(relative_import('nbdev.notebook.core', Path('nbdev')/'data'/'external.py'), '..notebook.core') test_eq(relative_import('nbdev.vision', Path('nbdev')/'vision'/'learner.py'), '.') #export _re_import = ReLibName(r'^(\s*)from (LIB_NAME\.\S*) import (.*)$') #export def _deal_import(code_lines, fname): def _replace(m): sp,mod,obj = m.groups() return f"{sp}from {relative_import(mod, fname)} import {obj}" return [_re_import.re.sub(_replace,line) for line in code_lines] #hide lines = ["from nbdev.core import *", "nothing to see", " from nbdev.vision import bla1, bla2", "from nbdev.vision import models", "import nbdev.vision"] test_eq(_deal_import(lines, Path.cwd()/'nbdev'/'data.py'), [ "from .core import *", "nothing to see", " from .vision import bla1, bla2", "from .vision import models", "import nbdev.vision" ]) #export _re_index_custom = re.compile(r'def custom_doc_links\(name\):(.*)$', re.DOTALL) #export def reset_nbdev_module(): "Create a skeleton for _nbdev" fname = Config().path("lib_path")/'_nbdev.py' fname.parent.mkdir(parents=True, exist_ok=True) sep = '\n'* (int(Config().get('cell_spacing', '1'))+1) if fname.is_file(): with open(fname, 'r') as f: search = _re_index_custom.search(f.read()) else: search = None prev_code = search.groups()[0] if search is not None else ' return None\n' with open(fname, 'w') as f: f.write(f"# AUTOGENERATED BY NBDEV! DO NOT EDIT!") f.write('\n\n__all__ = ["index", "modules", "custom_doc_links", "git_url"]') f.write('\n\nindex = {}') f.write('\n\nmodules = []') f.write(f'\n\ndoc_url = "{Config().doc_host}{Config().doc_baseurl}"') f.write(f'\n\ngit_url = "{Config().git_url}"') f.write(f'{sep}def custom_doc_links(name):{prev_code}') #export class _EmptyModule(): def __init__(self): self.index,self.modules = {},[] self.doc_url,self.git_url = f"{Config().doc_host}{Config().doc_baseurl}",Config().git_url def custom_doc_links(self, name): return None #export def get_nbdev_module(): "Reads _nbdev" try: spec = importlib.util.spec_from_file_location(f"{Config().lib_name}._nbdev", Config().path("lib_path")/'_nbdev.py') mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod except: return _EmptyModule() #export _re_index_idx = re.compile(r'index\s*=\s*{[^}]*}') _re_index_mod = re.compile(r'modules\s*=\s*\[[^\]]*\]') #export def save_nbdev_module(mod): "Save `mod` inside _nbdev" fname = Config().path("lib_path")/'_nbdev.py' with open(fname, 'r') as f: code = f.read() t = r',\n '.join([f'"{k}": "{v}"' for k,v in mod.index.items()]) code = _re_index_idx.sub("index = {"+ t +"}", code) t = r',\n '.join(['"' + f.replace('\\','/') + '"' for f in mod.modules]) code = _re_index_mod.sub(f"modules = [{t}]", code) with open(fname, 'w') as f: f.write(code) #hide ind,ind_bak = Config().path("lib_path")/'_nbdev.py',Config().path("lib_path")/'_nbdev.bak' if ind.exists(): shutil.move(ind, ind_bak) try: reset_nbdev_module() mod = get_nbdev_module() test_eq(mod.index, {}) test_eq(mod.modules, []) mod.index = {'foo':'bar'} mod.modules.append('lala.bla') save_nbdev_module(mod) mod = get_nbdev_module() test_eq(mod.index, {'foo':'bar'}) test_eq(mod.modules, ['lala.bla']) finally: if ind_bak.exists(): shutil.move(ind_bak, ind) #export def split_flags_and_code(cell, return_type=list): "Splits the `source` of a cell into 2 parts and returns (flags, code)" source_str = cell['source'].replace('\r', '') code_lines = source_str.split('\n') split_pos = 0 if code_lines[0].strip().startswith('#') else -1 for i, line in enumerate(code_lines): if not line.startswith('#') and line.strip() and not _re_from_future_import.match(line): break split_pos+=1 res = code_lines[:split_pos], code_lines[split_pos:] if return_type is list: return res return tuple('\n'.join(r) for r in res) def _test_split_flags_and_code(expected_flags, expected_code): cell = nbformat.v4.new_code_cell('\n'.join(expected_flags + expected_code)) test_eq((expected_flags, expected_code), split_flags_and_code(cell)) expected=('\n'.join(expected_flags), '\n'.join(expected_code)) test_eq(expected, split_flags_and_code(cell, str)) _test_split_flags_and_code([ '#export'], ['# TODO: write this function', 'def func(x): pass']) #export def create_mod_file(fname, nb_path, bare=False): "Create a module file for `fname`." bare = str(Config().get('bare', bare)) == 'True' fname.parent.mkdir(parents=True, exist_ok=True) file_path = os.path.relpath(nb_path, Config().config_file.parent).replace('\\', '/') with open(fname, 'w') as f: if not bare: f.write(f"# AUTOGENERATED! DO NOT EDIT! File to edit: {file_path} (unless otherwise specified).") f.write('\n\n__all__ = []') #export def create_mod_files(files, to_dict=False, bare=False): "Create mod files for default exports found in `files`" modules = [] for f in sorted(files): fname = Path(f) nb = read_nb(fname) default = find_default_export(nb['cells']) if default is not None: default = os.path.sep.join(default.split('.')) modules.append(default) if not to_dict: create_mod_file(Config().path("lib_path")/f'{default}.py', Config().path("nbs_path")/f'{fname}', bare=bare) return modules #export def _notebook2script(fname, modules, silent=False, to_dict=None, bare=False): "Finds cells starting with `#export` and puts them into a module created by `create_mod_files`" bare = str(Config().get('bare', bare)) == 'True' if os.environ.get('IN_TEST',0): return # don't export if running tests sep = '\n'* (int(Config().get('cell_spacing', '1'))+1) fname = Path(fname) nb = read_nb(fname) default = find_default_export(nb['cells']) if default is not None: default = os.path.sep.join(default.split('.')) mod = get_nbdev_module() exports = [is_export(c, default) for c in nb['cells']] cells = [(i,c,e) for i,(c,e) in enumerate(zip(nb['cells'],exports)) if e is not None] for i,c,(e,a) in cells: if e not in modules: print(f'Warning: Exporting to "{e}.py" but this module is not part of this build') fname_out = Config().path("lib_path")/f'{e}.py' if bare: orig = "\n" else: orig = (f'# {"" if a else "Internal "}C' if e==default else f'# Comes from {fname.name}, c') + 'ell\n' flag_lines,code_lines = split_flags_and_code(c) code_lines = _deal_import(code_lines, fname_out) code = sep + orig + '\n'.join(code_lines) names = export_names(code) flags = '\n'.join(flag_lines) extra,code = extra_add(flags, code) code = _from_future_import(fname_out, flags, code, to_dict) if a: if to_dict is None: _add2all(fname_out, [f"'{f}'" for f in names if '.' not in f and len(f) > 0] + extra) mod.index.update({f: fname.name for f in names}) code = re.sub(r' +$', '', code, flags=re.MULTILINE) if code != sep + orig[:-1]: if to_dict is not None: to_dict[e].append((i, fname, code)) else: with open(fname_out, 'a', encoding='utf8') as f: f.write(code) if f'{e}.py' not in mod.modules: mod.modules.append(f'{e}.py') save_nbdev_module(mod) if not silent: print(f"Converted {fname.name}.") return to_dict #hide if not os.environ.get('IN_TEST',0): modules = create_mod_files(glob.glob('00_export.ipynb')) _notebook2script('00_export.ipynb', modules) #hide with open(Config().path("lib_path")/('export.py')) as f: l = f.readline() test_eq(l, '# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/00_export.ipynb (unless otherwise specified).\n') #export def add_init(path): "Add `__init__.py` in all subdirs of `path` containing python files if it's not there already" for p,d,f in os.walk(path): for f_ in f: if f_.endswith('.py'): if not (Path(p)/'__init__.py').exists(): (Path(p)/'__init__.py').touch() break with tempfile.TemporaryDirectory() as d: os.makedirs(Path(d)/'a', exist_ok=True) (Path(d)/'a'/'f.py').touch() os.makedirs(Path(d)/'a/b', exist_ok=True) (Path(d)/'a'/'b'/'f.py').touch() add_init(d) assert not (Path(d)/'__init__.py').exists() for e in [Path(d)/'a', Path(d)/'a/b']: assert (e/'__init__.py').exists() #export _re_version = re.compile('^__version__\s*=.*$', re.MULTILINE) #export def update_version(): "Add or update `__version__` in the main `__init__.py` of the library" fname = Config().path("lib_path")/'__init__.py' if not fname.exists(): fname.touch() version = f'__version__ = "{Config().version}"' with open(fname, 'r') as f: code = f.read() if _re_version.search(code) is None: code = version + "\n" + code else: code = _re_version.sub(version, code) with open(fname, 'w') as f: f.write(code) #export _re_baseurl = re.compile('^baseurl\s*:.*$', re.MULTILINE) #export def update_baseurl(): "Add or update `baseurl` in `_config.yml` for the docs" fname = Config().path("doc_path")/'_config.yml' if not fname.exists(): return with open(fname, 'r') as f: code = f.read() if _re_baseurl.search(code) is None: code = code + f"\nbaseurl: {Config().doc_baseurl}" else: code = _re_baseurl.sub(f"baseurl: {Config().doc_baseurl}", code) with open(fname, 'w') as f: f.write(code) #export def nbglob(fname=None, recursive=None, extension='.ipynb', config_key='nbs_path') -> L: "Find all files in a directory matching an extension given a `config_key`. Ignores hidden directories and filenames starting with `_`. If argument `recursive` is not set to `True` or `False`, this value is retreived from settings.ini with a default of `False`." if recursive == None: recursive=Config().get('recursive', 'False').lower() == 'true' fname = Config().path(config_key) if fname is None else Path(fname) if fname.is_dir(): fname = f'{fname.absolute()}/**/*{extension}' if recursive else f'{fname.absolute()}/*{extension}' fls = L(glob.glob(str(fname), recursive=recursive)).filter(lambda x: '/.' not in x).map(Path) return fls.filter(lambda x: not x.name.startswith('_') and x.name.endswith(extension)) #hide with tempfile.TemporaryDirectory() as d: os.makedirs(Path(d)/'a', exist_ok=True) (Path(d)/'a'/'a.ipynb').touch() (Path(d)/'a'/'fake_a.ipynb').touch() os.makedirs(Path(d)/'a/b', exist_ok=True) (Path(d)/'a'/'b'/'fake_b.ipynb').touch() os.makedirs(Path(d)/'a/b/c', exist_ok=True) (Path(d)/'a'/'b'/'c'/'fake_c.ipynb').touch() (Path(d)/'a'/'b'/'c'/'foo_c.ipynb').touch() if sys.platform != "win32": assert len(nbglob(str(Path(f'{str(d)}/**/foo*')), recursive=True)) == 1 assert len(nbglob(d, recursive=True)) == 5 assert len(nbglob(d, recursive=False)) == 0 assert len(nbglob(str(Path(f'{d}/a')), recursive=False)) == 2 #hide if sys.platform != "win32": assert len(nbglob('*')) > 1 assert len(nbglob('*')) > len(nbglob('0*')) assert not nbglob().filter(lambda x: '.ipynb_checkpoints' in str(x)) #hide fnames = nbglob() test_eq(len(fnames) > 0, True) fnames = nbglob(fnames[0]) test_eq(len(fnames), 1) #hide fnames = nbglob(extension='.py', config_key='lib_path') test_eq(len(fnames) > 1, True) #export def notebook2script(fname=None, silent=False, to_dict=False, bare=False): "Convert notebooks matching `fname` to modules" # initial checks if os.environ.get('IN_TEST',0): return # don't export if running tests if fname is None: reset_nbdev_module() update_version() update_baseurl() files = nbglob(fname=fname) d = collections.defaultdict(list) if to_dict else None modules = create_mod_files(files, to_dict, bare=bare) for f in sorted(files): d = _notebook2script(f, modules, silent=silent, to_dict=d, bare=bare) if to_dict: return d else: add_init(Config().path("lib_path")) #export class DocsTestClass: "for tests only" def test(): pass #hide #exporti #for tests only def update_lib_with_exporti_testfn(): pass #hide notebook2script()