#|hide
#|default_exp read
Reading a notebook, and initial bootstrapping for notebook exporting
#|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"}]
nbdev uses a settings.ini
file in the root of the project to store all configuration details. This file is in ConfigParser
format, and can be read and written conveniently using fastcore's Config
class.
#|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)
This is a wrapper for fastcore
's save_config_file
which sets some nbdev
defaults. It is also installed as a CLI command.
#|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)
get_config
searches for settings.ini
in the current directory, and then in all parent directories, stopping when it is found.
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()
Python modules require a __init.py__
file in all directories that are modules. We assume that all directories containing a python file (including in subdirectories of any depth) is a module, and therefore add a __init__.py
to each.
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')
This is a simple exporter with just enough functionality to correctly export this notebook, in order to bootstrap the creation of nbdev itself.
#|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__