#|hide #|default_exp read #|export from datetime import datetime from fastcore.imports import * from fastcore.foundation import * from fastcore.basics import * from fastcore.imports import * from fastcore.script import * from fastcore.xtras import * import ast,functools from execnb.nbio import read_nb,NbCell from pprint import pformat,pprint #|hide from fastcore.test import * import tempfile #| export def mk_cell(text, cell_type='code'): "Create a `NbCell` containing `text`" assert cell_type in {'code', 'markdown', 'raw'} return NbCell(0, dict(cell_type=cell_type, metadata={}, source=text, directives_={})) #|export def create_output(txt, mime): "Add a cell output containing `txt` of the `mime` text MIME sub-type" return [{"data": { f"text/{mime}": str(txt).splitlines(True) }, "execution_count": 1, "metadata": {}, "output_type": "execute_result"}] #|export @call_parse def nbdev_create_config( user:str, # Repo username lib_name:str=None, # Name of library description='TODO fill me in', # Description for PyPI author='TODO fill me in', # Author for PyPI author_email='todo@example.org', # Email for PyPI path:str='.', # Path to create config file cfg_name:str='settings.ini', # Name of config file to create branch:str='master', # Repo branch host:str='github', # Repo hostname git_url:str="https://github.com/%(user)s/%(lib_name)s/tree/%(branch)s/", # Repo URL custom_sidebar:bool_arg=False, # Create custom sidebar? nbs_path:str='.', # Name of folder containing notebooks lib_path:str='%(lib_name)s', # Folder name of root module doc_path:str='_docs', # Folder name containing docs tst_flags:str='', # Test flags version:str='0.0.1', # Version number keywords='python', # Keywords for PyPI license='apache2', # License for PyPI copyright='', # Copyright for PyPI, defaults to author from current year status='3', # Status for PyPI min_python='3.6', # Minimum python version for PyPI audience='Developers', # Audience for PyPI language='English' # Language for PyPI ): "Create a config file" if lib_name is None: parent = Path.cwd().parent lib_name = parent.parent.name if parent.name=='nbs' else parent.name if not copyright: copyright = f'{datetime.now().year} ownwards, {author}' g = locals() config = {o:g[o] for o in 'host lib_name user branch nbs_path doc_path \ description author author_email keywords license tst_flags version custom_sidebar \ copyright status min_python audience language git_url lib_path'.split()} save_config_file(Path(path)/cfg_name, config) #|export @functools.lru_cache(maxsize=None) def get_config(cfg_name='settings.ini', path=None): "`Config` for ini file found in `path` (defaults to `cwd`)" cfg_path = Path.cwd() if path is None else Path(path) while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent return Config(cfg_path, cfg_name=cfg_name) nbdev_create_config('fastai', path='..', nbs_path='nbs', tst_flags='tst', cfg_name='test_settings.ini') cfg = get_config('test_settings.ini') test_eq(cfg.lib_name, 'nbdev') test_eq(cfg.git_url, "https://github.com/fastai/nbdev/tree/master/") cwd = Path.cwd() test_eq(cfg.config_path, cwd.parent.absolute()) test_eq(cfg.path('lib_path'), cwd.parent/'nbdev') test_eq(cfg.path('nbs_path'), cwd) test_eq(cfg.path('doc_path'), cwd.parent/'_docs') #|export def config_key(c, default=None, path=True, missing_ok=False): "Look for key `c` in settings.ini and fail gracefully if not found and no default provided" try: cfg = get_config() except FileNotFoundError: if missing_ok and default is not None: return default else: raise ValueError('settings.ini not found') res = cfg.path(c) if path else cfg.get(c, default=default) if res is None: raise ValueError(f'`{c}` not specified in settings.ini') return res #|export _init = '__init__.py' def _has_py(fs): return any(1 for f in fs if f.endswith('.py')) def add_init(path): "Add `__init__.py` in all subdirs of `path` containing python files if it's not there already" # we add the lowest-level `__init__.py` files first, which ensures _has_py succeeds for parent modules path = Path(path) path.mkdir(exist_ok=True) if not (path/_init).exists(): (path/_init).touch() for r,ds,fs in os.walk(path, topdown=False): r = Path(r) subds = (os.listdir(r/d) for d in ds) if _has_py(fs) or any(filter(_has_py, subds)) and not (r/_init).exists(): (r/_init).touch() with tempfile.TemporaryDirectory() as d: d = Path(d) (d/'a/b').mkdir(parents=True) (d/'a/b/f.py').touch() (d/'a/c').mkdir() add_init(d) assert not (d/'a/c'/_init).exists(), "Should not add init to dir without py file" for e in [d, d/'a', d/'a/b']: assert (e/_init).exists(),f"Missing init in {e}" #|export def write_cells(cells, hdr, file, offset=0): "Write `cells` to `file` along with header `hdr` starting at index `offset` (mainly for nbdev internal use)" for cell in cells: if cell.source.strip(): file.write(f'\n\n{hdr} {cell.idx_+offset}\n{cell.source}') #|export def basic_export_nb(fname, name, dest=None): "Basic exporter to bootstrap nbdev" if dest is None: dest = config_key('lib_path') fname,dest = Path(fname),Path(dest) nb = read_nb(fname) # grab the source from all the cells that have an `export` comment cells = L(cell for cell in nb.cells if re.match(r'#\s*\|export', cell.source)) # find all the exported functions, to create `__all__`: trees = cells.map(NbCell.parsed_).concat() funcs = trees.filter(risinstance((ast.FunctionDef,ast.ClassDef))).attrgot('name') exp_funcs = [f for f in funcs if f[0]!='_'] # write out the file with (dest/name).open('w') as f: f.write(f"# %% auto 0\n__all__ = {exp_funcs}") write_cells(cells, f"# %% {fname.relpath(dest)}", f) f.write('\n') #|hide #| eval: false path = Path('../nbdev') (path/'read.py').unlink(missing_ok=True) add_init(path) basic_export_nb("01_read.ipynb", 'read.py') g = exec_new('from nbdev import read') assert g['read'].add_init assert 'add_init' in g['read'].__all__