Skip to content

feat: plaintext support #1499

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions nbdev/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
'nbdev.migrate.nbdev_migrate': ('api/migrate.html#nbdev_migrate', 'nbdev/migrate.py')},
'nbdev.process': { 'nbdev.process.NBProcessor': ('api/process.html#nbprocessor', 'nbdev/process.py'),
'nbdev.process.NBProcessor.__init__': ('api/process.html#nbprocessor.__init__', 'nbdev/process.py'),
'nbdev.process.NBProcessor._handle_nb': ('api/process.html#nbprocessor._handle_nb', 'nbdev/process.py'),
'nbdev.process.NBProcessor._proc': ('api/process.html#nbprocessor._proc', 'nbdev/process.py'),
'nbdev.process.NBProcessor._process_cell': ( 'api/process.html#nbprocessor._process_cell',
'nbdev/process.py'),
Expand Down
12 changes: 8 additions & 4 deletions nbdev/doclinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .maker import *
from .export import *
from .imports import *
from .process import plaintext_file_formats

from fastcore.script import *
from fastcore.utils import *
Expand Down Expand Up @@ -120,15 +121,18 @@ def nbglob(path=None, skip_folder_re = '^[_.]', file_glob='*.ipynb', skip_file_r
"Find all files in a directory matching an extension given a config key."
path = Path(path or get_config()[key])
recursive=get_config().recursive
res = globtastic(path, file_glob=file_glob, skip_folder_re=skip_folder_re,
skip_file_re=skip_file_re, recursive=recursive, **kwargs)
if type(file_glob) != list: file_glob = [file_glob]
res = []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a list comprehension.

for _file_glob in file_glob:
res += globtastic(path, file_glob=_file_glob, skip_folder_re=skip_folder_re,
skip_file_re=skip_file_re, recursive=recursive, **kwargs)
return res.map(Path) if as_path else res

# %% ../nbs/api/05_doclinks.ipynb
def nbglob_cli(
path:str=None, # Path to notebooks
symlinks:bool=False, # Follow symlinks?
file_glob:str='*.ipynb', # Only include files matching glob
file_glob:Union[str, List[str]]=['*.ipynb'] + [f"*.{ext}" for ext in plaintext_file_formats], # Only include files matching glob
file_re:str=None, # Only include files matching regex
folder_re:str=None, # Only enter folders matching regex
skip_file_glob:str=None, # Skip files matching glob
Expand All @@ -142,7 +146,7 @@ def nbglob_cli(
@call_parse
@delegates(nbglob_cli)
def nbdev_export(
path:str=None, # Path or filename
path:str=None, # Path or filename,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a mistake?

procs:Param("tokens naming the export processors to use.", nargs="*", choices=optional_procs())="black_format",
**kwargs):
"Export notebooks in `path` to Python modules"
Expand Down
3 changes: 2 additions & 1 deletion nbdev/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ def nb_export(nbname:str, # Filename of notebook
name:str=None, # Name of python script {name}.py to create.
mod_maker=ModuleMaker,
debug:bool=False, # Debug mode
fmt:str=None, # Format to export to
solo_nb:bool=False # Export single notebook outside of an nbdev project.
):
"Create module(s) from notebook"
if lib_path is None: lib_path = get_config().lib_path if is_nbdev() else '.'
exp = ExportModuleProc()
nb = NBProcessor(nbname, [exp]+L(procs), debug=debug)
nb = NBProcessor(nbname, [exp]+L(procs), debug=debug, fmt=fmt)
nb.process()
for mod,cells in exp.modules.items():
if first(1 for o in cells if o.cell_type=='code'):
Expand Down
38 changes: 36 additions & 2 deletions nbdev/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@

from collections import defaultdict

try:
import nbformat
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imports can be on one line

import jupytext
import tempfile
plaintext_supported = True
except ImportError:
plaintext_supported = False
Comment on lines +24 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
except ImportError:
plaintext_supported = False
except ImportError: plaintext_supported = False


# %% ../nbs/api/03_process.ipynb
# from https://github.com/quarto-dev/quarto-cli/blob/main/src/resources/jupyter/notebook.py
langs = defaultdict(
Expand Down Expand Up @@ -88,17 +96,43 @@ def _mk_procs(procs, nb): return L(procs).map(instantiate, nb=nb)
# %% ../nbs/api/03_process.ipynb
def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_'

# %% ../nbs/api/03_process.ipynb
plaintext_file_formats = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be on one line.

"pct.py": "py:percent",
"lgt.py": "py:light",
"spx.py": "py:sphinx",
"myst.md": "md:myst",
"pandoc.md": "md:pandoc",
}

# %% ../nbs/api/03_process.ipynb
class NBProcessor:
"Process cells and nbdev comments in a notebook"
def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False):
self.nb = read_nb(path) if nb is None else nb
def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False, fmt=None):
self._handle_nb(path, nb, fmt)
self.lang = nb_lang(self.nb)
for cell in self.nb.cells: cell.directives_ = extract_directives(cell, remove=rm_directives, lang=self.lang)
self.procs = _mk_procs(procs, nb=self.nb)
self.debug,self.rm_directives = debug,rm_directives
if process: self.process()

def _handle_nb(self, path, nb, fmt):
path = str(path)
if any(path.endswith(ext) for ext in plaintext_file_formats) or fmt is not None:
fmt = plaintext_file_formats[".".join(path.rsplit('.', 2)[-2:])] if fmt is None else fmt
if fmt in plaintext_file_formats.values():
if not plaintext_supported:
raise ValueError(f"File {path} has a supported extension, but plaintext conversion is not supported. Please install jupytext and nbformat to use this feature.")
nb_converted = jupytext.read(path, fmt=fmt)
with tempfile.NamedTemporaryFile(delete=True, suffix=".ipynb") as temp_file:
nbformat.write(nb_converted, temp_file.name)
self.nb = read_nb(temp_file.name) if nb is None else nb
return
if fmt is None or fmt == "ipynb":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each part here can be on one line.

self.nb = read_nb(path) if nb is None else nb
else:
raise ValueError(f"Invalid format: {fmt}")

def _process_cell(self, proc, cell):
if not hasattr(cell,'source'): return
if cell.cell_type=='code' and cell.directives_:
Expand Down
48 changes: 45 additions & 3 deletions nbs/api/03_process.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@
"from fastcore.script import *\n",
"from fastcore.imports import *\n",
"\n",
"from collections import defaultdict"
"from collections import defaultdict\n",
"\n",
"try:\n",
" import nbformat\n",
" import jupytext\n",
" import tempfile\n",
" plaintext_supported = True\n",
"except ImportError:\n",
" plaintext_supported = False"
]
},
{
Expand Down Expand Up @@ -353,6 +361,23 @@
"def _is_direc(f): return getattr(f, '__name__', '-')[-1]=='_'"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16dc34e7",
"metadata": {},
"outputs": [],
"source": [
"#|exporti\n",
"plaintext_file_formats = {\n",
" \"pct.py\": \"py:percent\",\n",
" \"lgt.py\": \"py:light\",\n",
" \"spx.py\": \"py:sphinx\",\n",
" \"myst.md\": \"md:myst\",\n",
" \"pandoc.md\": \"md:pandoc\",\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand All @@ -363,14 +388,31 @@
"#|export\n",
"class NBProcessor:\n",
" \"Process cells and nbdev comments in a notebook\"\n",
" def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False):\n",
" self.nb = read_nb(path) if nb is None else nb\n",
" def __init__(self, path=None, procs=None, nb=None, debug=False, rm_directives=True, process=False, fmt=None):\n",
" self._handle_nb(path, nb, fmt)\n",
" self.lang = nb_lang(self.nb)\n",
" for cell in self.nb.cells: cell.directives_ = extract_directives(cell, remove=rm_directives, lang=self.lang)\n",
" self.procs = _mk_procs(procs, nb=self.nb)\n",
" self.debug,self.rm_directives = debug,rm_directives\n",
" if process: self.process()\n",
"\n",
" def _handle_nb(self, path, nb, fmt): \n",
" path = str(path)\n",
" if any(path.endswith(ext) for ext in plaintext_file_formats) or fmt is not None:\n",
" fmt = plaintext_file_formats[\".\".join(path.rsplit('.', 2)[-2:])] if fmt is None else fmt\n",
" if fmt in plaintext_file_formats.values():\n",
" if not plaintext_supported:\n",
" raise ValueError(f\"File {path} has a supported extension, but plaintext conversion is not supported. Please install jupytext and nbformat to use this feature.\")\n",
" nb_converted = jupytext.read(path, fmt=fmt)\n",
" with tempfile.NamedTemporaryFile(delete=True, suffix=\".ipynb\") as temp_file:\n",
" nbformat.write(nb_converted, temp_file.name)\n",
" self.nb = read_nb(temp_file.name) if nb is None else nb\n",
" return\n",
" if fmt is None or fmt == \"ipynb\":\n",
" self.nb = read_nb(path) if nb is None else nb\n",
" else:\n",
" raise ValueError(f\"Invalid format: {fmt}\")\n",
"\n",
" def _process_cell(self, proc, cell):\n",
" if not hasattr(cell,'source'): return\n",
" if cell.cell_type=='code' and cell.directives_:\n",
Expand Down
3 changes: 2 additions & 1 deletion nbs/api/04_export.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,13 @@
" name:str=None, # Name of python script {name}.py to create.\n",
" mod_maker=ModuleMaker,\n",
" debug:bool=False, # Debug mode\n",
" fmt:str=None, # Format to export to\n",
" solo_nb:bool=False # Export single notebook outside of an nbdev project.\n",
" ):\n",
" \"Create module(s) from notebook\"\n",
" if lib_path is None: lib_path = get_config().lib_path if is_nbdev() else '.'\n",
" exp = ExportModuleProc()\n",
" nb = NBProcessor(nbname, [exp]+L(procs), debug=debug)\n",
" nb = NBProcessor(nbname, [exp]+L(procs), debug=debug, fmt=fmt)\n",
" nb.process()\n",
" for mod,cells in exp.modules.items():\n",
" if first(1 for o in cells if o.cell_type=='code'):\n",
Expand Down
26 changes: 21 additions & 5 deletions nbs/api/05_doclinks.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"from nbdev.maker import *\n",
"from nbdev.export import *\n",
"from nbdev.imports import *\n",
"from nbdev.process import plaintext_file_formats\n",
"\n",
"from fastcore.script import *\n",
"from fastcore.utils import *\n",
Expand Down Expand Up @@ -325,8 +326,11 @@
" \"Find all files in a directory matching an extension given a config key.\"\n",
" path = Path(path or get_config()[key])\n",
" recursive=get_config().recursive\n",
" res = globtastic(path, file_glob=file_glob, skip_folder_re=skip_folder_re,\n",
" skip_file_re=skip_file_re, recursive=recursive, **kwargs)\n",
" if type(file_glob) != list: file_glob = [file_glob]\n",
" res = []\n",
" for _file_glob in file_glob:\n",
" res += globtastic(path, file_glob=_file_glob, skip_folder_re=skip_folder_re,\n",
" skip_file_re=skip_file_re, recursive=recursive, **kwargs)\n",
" return res.map(Path) if as_path else res"
]
},
Expand All @@ -340,7 +344,7 @@
"def nbglob_cli(\n",
" path:str=None, # Path to notebooks\n",
" symlinks:bool=False, # Follow symlinks?\n",
" file_glob:str='*.ipynb', # Only include files matching glob\n",
" file_glob:Union[str, List[str]]=['*.ipynb'] + [f\"*.{ext}\" for ext in plaintext_file_formats], # Only include files matching glob\n",
" file_re:str=None, # Only include files matching regex\n",
" folder_re:str=None, # Only enter folders matching regex\n",
" skip_file_glob:str=None, # Skip files matching glob\n",
Expand All @@ -361,7 +365,7 @@
"@call_parse\n",
"@delegates(nbglob_cli)\n",
"def nbdev_export(\n",
" path:str=None, # Path or filename\n",
" path:str=None, # Path or filename,\n",
" procs:Param(\"tokens naming the export processors to use.\", nargs=\"*\", choices=optional_procs())=\"black_format\",\n",
" **kwargs):\n",
" \"Export notebooks in `path` to Python modules\"\n",
Expand Down Expand Up @@ -874,7 +878,19 @@
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"outputs": [
{
"ename": "AttributeError",
"evalue": "'NoneType' object has no attribute 'startswith'",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[0;32mIn[30], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[43mc\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mdoc\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mnumpy.array\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mstartswith\u001b[49m(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mhttp\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m c\u001b[38;5;241m.\u001b[39mdoc(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mNbdevLookup\u001b[39m\u001b[38;5;124m'\u001b[39m)\u001b[38;5;241m.\u001b[39mendswith(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124m#nbdevlookup\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m c\u001b[38;5;241m.\u001b[39mdoc(\u001b[38;5;124m'\u001b[39m\u001b[38;5;124marray\u001b[39m\u001b[38;5;124m'\u001b[39m)\n",
"\u001b[0;31mAttributeError\u001b[0m: 'NoneType' object has no attribute 'startswith'"
]
}
],
"source": [
"assert c.doc('numpy.array').startswith('http')\n",
"assert not c.doc('numpy.Array')\n",
Expand Down
2 changes: 1 addition & 1 deletion nbs/tutorials/tutorial.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Since we created a repo named `nbev-hello-world` with the `fastai` user, we can clone it as follows:\n",
"Since we created a repo named `nbdev-hello-world` with the `fastai` user, we can clone it as follows:\n",
"\n",
"```sh\n",
"git clone https://github.com/fastai/nbdev-hello-world.git\n",
Expand Down
Loading