diff --git a/nbdev/_modidx.py b/nbdev/_modidx.py index c3a1109b5..22b2de700 100644 --- a/nbdev/_modidx.py +++ b/nbdev/_modidx.py @@ -49,6 +49,7 @@ 'nbdev.config.bump_version': ('api/config.html#bump_version', 'nbdev/config.py'), 'nbdev.config.create_output': ('api/config.html#create_output', 'nbdev/config.py'), 'nbdev.config.get_config': ('api/config.html#get_config', 'nbdev/config.py'), + 'nbdev.config.import_obj': ('api/config.html#import_obj', 'nbdev/config.py'), 'nbdev.config.is_nbdev': ('api/config.html#is_nbdev', 'nbdev/config.py'), 'nbdev.config.nbdev_create_config': ('api/config.html#nbdev_create_config', 'nbdev/config.py'), 'nbdev.config.read_version': ('api/config.html#read_version', 'nbdev/config.py'), @@ -98,10 +99,7 @@ 'nbdev.export.ExportModuleProc._export_': ('api/export.html#exportmoduleproc._export_', 'nbdev/export.py'), 'nbdev.export.ExportModuleProc._exporti_': ('api/export.html#exportmoduleproc._exporti_', 'nbdev/export.py'), 'nbdev.export.ExportModuleProc.begin': ('api/export.html#exportmoduleproc.begin', 'nbdev/export.py'), - 'nbdev.export.black_format': ('api/export.html#black_format', 'nbdev/export.py'), - 'nbdev.export.nb_export': ('api/export.html#nb_export', 'nbdev/export.py'), - 'nbdev.export.optional_procs': ('api/export.html#optional_procs', 'nbdev/export.py'), - 'nbdev.export.scrub_magics': ('api/export.html#scrub_magics', 'nbdev/export.py')}, + 'nbdev.export.nb_export': ('api/export.html#nb_export', 'nbdev/export.py')}, 'nbdev.extract_attachments': {}, 'nbdev.frontmatter': { 'nbdev.frontmatter.FrontmatterProc': ('api/frontmatter.html#frontmatterproc', 'nbdev/frontmatter.py'), 'nbdev.frontmatter.FrontmatterProc._update': ( 'api/frontmatter.html#frontmatterproc._update', @@ -222,7 +220,6 @@ 'nbdev.processors._default_exp': ('api/processors.html#_default_exp', 'nbdev/processors.py'), 'nbdev.processors._do_eval': ('api/processors.html#_do_eval', 'nbdev/processors.py'), 'nbdev.processors._get_nm': ('api/processors.html#_get_nm', 'nbdev/processors.py'), - 'nbdev.processors._import_obj': ('api/processors.html#_import_obj', 'nbdev/processors.py'), 'nbdev.processors._is_showdoc': ('api/processors.html#_is_showdoc', 'nbdev/processors.py'), 'nbdev.processors._re_hideline': ('api/processors.html#_re_hideline', 'nbdev/processors.py'), 'nbdev.processors._show_docs': ('api/processors.html#_show_docs', 'nbdev/processors.py'), @@ -333,6 +330,7 @@ 'nbdev.release.release_pypi': ('api/release.html#release_pypi', 'nbdev/release.py'), 'nbdev.release.write_conda_meta': ('api/release.html#write_conda_meta', 'nbdev/release.py'), 'nbdev.release.write_requirements': ('api/release.html#write_requirements', 'nbdev/release.py')}, + 'nbdev.scrubmagics': {}, 'nbdev.serve': { 'nbdev.serve._is_qpy': ('api/serve.html#_is_qpy', 'nbdev/serve.py'), 'nbdev.serve._proc_file': ('api/serve.html#_proc_file', 'nbdev/serve.py'), 'nbdev.serve.proc_nbs': ('api/serve.html#proc_nbs', 'nbdev/serve.py')}, diff --git a/nbdev/config.py b/nbdev/config.py index 757f06df3..deb284a90 100644 --- a/nbdev/config.py +++ b/nbdev/config.py @@ -5,7 +5,7 @@ # %% auto #0 __all__ = ['pyproject_nm', 'pyproject_tmpl', 'nbdev_defaults', 'pyproj_tmpl', 'nbdev_create_config', 'ConfigToml', 'get_config', 'is_nbdev', 'create_output', 'show_src', 'read_version', 'set_version', 'bump_version', 'update_version', - 'update_proj', 'add_init', 'write_cells'] + 'update_proj', 'add_init', 'import_obj', 'write_cells'] # %% ../nbs/api/01_config.ipynb #6fd14ecd from datetime import datetime @@ -16,7 +16,7 @@ from fastcore.style import * from fastcore.xdg import * -import ast,warnings +import ast,importlib,warnings from IPython.display import Markdown from execnb.nbio import read_nb,NbCell from urllib.error import HTTPError @@ -167,7 +167,8 @@ def _find_nbdev_pyproject(path=None): # %% ../nbs/api/01_config.ipynb #3dac70e0 nbdev_defaults = dict(nbs_path='nbs', doc_path='_docs', tst_flags='notest', recursive=True, readme_nb='index.ipynb', - clean_ids=True, clear_all=False, put_version_in_init=True, jupyter_hooks=False, black_formatting=False, branch='main') + clean_ids=True, clear_all=False, put_version_in_init=True, jupyter_hooks=False, branch='main', + doc_procs=[], export_procs=[]) _path_keys = 'lib_path', 'nbs_path', 'doc_path' @@ -337,6 +338,13 @@ def add_init(path=None): if get_config().get('put_version_in_init', True): update_version(path) if get_config().get('update_pyproject', True): update_proj(path.parent) +# %% ../nbs/api/01_config.ipynb #95cebda6 +def import_obj(s): + "Import and return `module:obj` string" + mod_nm, obj_nm = s.split(':') + mod = importlib.import_module(mod_nm) + return getattr(mod, obj_nm) + # %% ../nbs/api/01_config.ipynb #cdd05b4c def write_cells(cells, hdr, file, solo_nb=False): "Write `cells` to `file` along with header `hdr` (mainly for nbdev internal use)." diff --git a/nbdev/doclinks.py b/nbdev/doclinks.py index 9c46021f4..d0198073b 100644 --- a/nbdev/doclinks.py +++ b/nbdev/doclinks.py @@ -143,17 +143,17 @@ def nbglob_cli( @delegates(nbglob_cli) def nbdev_export( path:str=None, # Path or filename - procs:Param("tokens naming the export processors to use.", nargs="*", choices=optional_procs())="black_format", + procs:str='', # Space-separated `module:name` processors to use (or set `export_procs` in pyproject.toml) **kwargs): "Export notebooks in `path` to Python modules" if os.environ.get('IN_TEST',0): return if not is_nbdev(): raise Exception('`nbdev_export` must be called from a directory within a nbdev project.') - if procs: - import nbdev.export - procs = [getattr(nbdev.export, p) for p in L(procs)] + cfg = get_config() + procs = procs.split() if procs else cfg.get('export_procs', []) + procs = [import_obj(p) for p in procs] if procs else None files = nbglob(path=path, as_path=True, **kwargs).sorted('name') for f in files: nb_export(f, procs=procs) - add_init(get_config().lib_path) + add_init(cfg.lib_path) _build_modidx() # %% ../nbs/api/05_doclinks.ipynb #3134c22b diff --git a/nbdev/export.py b/nbdev/export.py index 0288452d0..44dd48ac2 100644 --- a/nbdev/export.py +++ b/nbdev/export.py @@ -3,7 +3,7 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/04_export.ipynb. # %% auto #0 -__all__ = ['ExportModuleProc', 'black_format', 'scrub_magics', 'optional_procs', 'nb_export'] +__all__ = ['ExportModuleProc', 'nb_export'] # %% ../nbs/api/04_export.ipynb #3b932371 from .config import * @@ -33,37 +33,6 @@ def __call__(self, cell): if cell.cell_type=='markdown' and src.startswith('# '): self.modules['#'].append(cell) _exports_=_export_ -# %% ../nbs/api/04_export.ipynb #6f524839 -def black_format(cell, # Cell to format - force=False): # Turn black formatting on regardless of pyproject.toml - "Processor to format code with `black`" - cfg = get_config() - if (not cfg.black_formatting and not force) or cell.cell_type != 'code': return - try: import black - except: raise ImportError("You must install black: `pip install black` if you wish to use black formatting with nbdev") - else: - _format_str = partial(black.format_str, mode = black.Mode()) - try: cell.source = _format_str(cell.source).strip() - except: pass - -# %% ../nbs/api/04_export.ipynb #aed6a875 -# includes the newline, because calling .strip() would affect all cells. -_magics_pattern = re.compile(r'^\s*(%%|%).*\n?', re.MULTILINE) - -def scrub_magics(cell): # Cell to format - "Processor to remove cell magics from exported code" - cfg = get_config() - if cell.cell_type != 'code': return - try: cell.source = _magics_pattern.sub('', cell.source) - except: pass - -# %% ../nbs/api/04_export.ipynb #d4a5fd8c -import nbdev.export -def optional_procs(): - "An explicit list of processors that could be used by `nb_export`" - return L([p for p in nbdev.export.__all__ - if p not in ["nb_export", "nb_export_cli", "ExportModuleProc", "optional_procs"]]) - # %% ../nbs/api/04_export.ipynb #76717e36 def nb_export(nbname:str, # Filename of notebook lib_path:str=None, # Path to destination library. If not in a nbdev project, defaults to current directory. diff --git a/nbdev/processors.py b/nbdev/processors.py index ed57ad14f..4a514736d 100644 --- a/nbdev/processors.py +++ b/nbdev/processors.py @@ -9,7 +9,6 @@ # %% ../nbs/api/10_processors.ipynb #2398f5ef-06d3-4890-8a54-7cf4f81f3894 import ast -import importlib from .config import * from .imports import * @@ -261,18 +260,12 @@ def end(self): widgets = {**old, **new, 'state': {**old.get('state', {}), **new['state']}} self.nb.metadata['widgets'] = {mimetype: widgets} -# %% ../nbs/api/10_processors.ipynb #a761e07c -def _import_obj(s): - mod_nm, obj_nm = s.split(':') - mod = importlib.import_module(mod_nm) - return getattr(mod, obj_nm) - # %% ../nbs/api/10_processors.ipynb #4b450cff class FilterDefaults: "Override `FilterDefaults` to change which notebook processors are used" def xtra_procs(self): - imps = get_config().get('procs', '').split() - return [_import_obj(o) for o in imps] + imps = get_config().get('doc_procs', '').split() + return [import_obj(o) for o in imps] def base_procs(self): return [FrontmatterProc, populate_language, add_show_docs, insert_warning, diff --git a/nbdev/scrubmagics.py b/nbdev/scrubmagics.py new file mode 100644 index 000000000..19fdb7802 --- /dev/null +++ b/nbdev/scrubmagics.py @@ -0,0 +1,13 @@ +"""Scrub Jupyter magics from exported code""" + +__all__ = ['scrub_magics'] + +import re + +_magics_pattern = re.compile(r'^\s*(%%|%).*\n?', re.MULTILINE) + +def scrub_magics(cell): + "Remove Jupyter magic lines (e.g. %%time, %matplotlib) from exported code cells" + if cell.cell_type != 'code': return + try: cell.source = _magics_pattern.sub('', cell.source) + except: pass diff --git a/nbs/api/01_config.ipynb b/nbs/api/01_config.ipynb index b60033f5b..d209b5c88 100644 --- a/nbs/api/01_config.ipynb +++ b/nbs/api/01_config.ipynb @@ -38,7 +38,7 @@ "from fastcore.style import *\n", "from fastcore.xdg import *\n", "\n", - "import ast,warnings\n", + "import ast,importlib,warnings\n", "from IPython.display import Markdown\n", "from execnb.nbio import read_nb,NbCell\n", "from urllib.error import HTTPError\n", @@ -355,7 +355,8 @@ "source": [ "#| export\n", "nbdev_defaults = dict(nbs_path='nbs', doc_path='_docs', tst_flags='notest', recursive=True, readme_nb='index.ipynb',\n", - " clean_ids=True, clear_all=False, put_version_in_init=True, jupyter_hooks=False, black_formatting=False, branch='main')\n", + " clean_ids=True, clear_all=False, put_version_in_init=True, jupyter_hooks=False, branch='main',\n", + " doc_procs=[], export_procs=[])\n", "\n", "_path_keys = 'lib_path', 'nbs_path', 'doc_path'" ] @@ -851,6 +852,21 @@ " for e in [d, d/'a', d/'a/b']: assert (e/_init).exists(),f\"Missing init in {e}\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "95cebda6", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def import_obj(s):\n", + " \"Import and return `module:obj` string\"\n", + " mod_nm, obj_nm = s.split(':')\n", + " mod = importlib.import_module(mod_nm)\n", + " return getattr(mod, obj_nm)" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/nbs/api/04_export.ipynb b/nbs/api/04_export.ipynb index 23e5e3703..e757b94bd 100644 --- a/nbs/api/04_export.ipynb +++ b/nbs/api/04_export.ipynb @@ -107,136 +107,6 @@ "assert 'h_n' in exp.in_all['some.thing'][0].source" ] }, - { - "cell_type": "markdown", - "id": "41ae5ee7", - "metadata": {}, - "source": [ - "### Optional export processors" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f524839", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "def black_format(cell, # Cell to format\n", - " force=False): # Turn black formatting on regardless of pyproject.toml\n", - " \"Processor to format code with `black`\"\n", - " cfg = get_config()\n", - " if (not cfg.black_formatting and not force) or cell.cell_type != 'code': return\n", - " try: import black\n", - " except: raise ImportError(\"You must install black: `pip install black` if you wish to use black formatting with nbdev\")\n", - " else:\n", - " _format_str = partial(black.format_str, mode = black.Mode())\n", - " try: cell.source = _format_str(cell.source).strip()\n", - " except: pass" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "06f422ea", - "metadata": {}, - "outputs": [], - "source": [ - "_cell = read_nb('../../tests/export_procs.ipynb')['cells'][0]\n", - "black_format(_cell, force=True)\n", - "test_eq(_cell.source, 'j = [1, 2, 3]')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aed6a875", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "# includes the newline, because calling .strip() would affect all cells.\n", - "_magics_pattern = re.compile(r'^\\s*(%%|%).*\\n?', re.MULTILINE)\n", - "\n", - "def scrub_magics(cell): # Cell to format\n", - " \"Processor to remove cell magics from exported code\"\n", - " cfg = get_config()\n", - " if cell.cell_type != 'code': return\n", - " try: cell.source = _magics_pattern.sub('', cell.source)\n", - " except: pass" - ] - }, - { - "cell_type": "markdown", - "id": "2edc2c07", - "metadata": {}, - "source": [ - "`scrub_magics` is a processor that scrubs the jupyter \"magics\" lines out of exported cells. This can be helpful when using tools like [sparkmagic](https://github.com/jupyter-incubator/sparkmagic) or just [Jupyter's builtin magics](https://ipython.readthedocs.io/en/stable/interactive/magics.html) in an nbdev project.\n", - "\n", - "Usage: \n", - "This behavior can be enabled by passing `scrub_magics` into the `--procs` flag of the `nbdev_export` command.\n", - "- `nbdev_export --procs scrub_magics`\n", - "- `nbdev_export --procs 'scrub_magics black_format'`\n", - "\n", - "Example:\n", - "\n", - "A cell like below could export the line `\"hello nbdev\"` into the `bar` module. And the `%%spark` magic line would be omitted.\n", - "\n", - "```python\n", - "%%spark\n", - "#| export bar\n", - "\"hello nbdev\"\n", - "```\n", - "\n", - "It will export as something similar to this:\n", - "\n", - "```python\n", - "# %% ../path/to/01_bar.ipynb 1\n", - "\"hello nbdev\"\n", - "```\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0b94a034", - "metadata": {}, - "outputs": [], - "source": [ - "_cell = read_nb('../../tests/export_procs.ipynb')['cells'][2]\n", - "scrub_magics(_cell)\n", - "test_eq(_cell.source, '''#| export bar\n", - "\"hello nbdev\"''')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d4a5fd8c", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "import nbdev.export\n", - "def optional_procs():\n", - " \"An explicit list of processors that could be used by `nb_export`\"\n", - " return L([p for p in nbdev.export.__all__\n", - " if p not in [\"nb_export\", \"nb_export_cli\", \"ExportModuleProc\", \"optional_procs\"]])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3f1b5e4a", - "metadata": {}, - "outputs": [], - "source": [ - "# every optional processor should be explicitly listed here\n", - "test_eq(optional_procs(), ['black_format', 'scrub_magics'])" - ] - }, { "cell_type": "markdown", "id": "94eb949b", diff --git a/nbs/api/05_doclinks.ipynb b/nbs/api/05_doclinks.ipynb index a113bd61e..88b314a79 100644 --- a/nbs/api/05_doclinks.ipynb +++ b/nbs/api/05_doclinks.ipynb @@ -387,30 +387,20 @@ "@delegates(nbglob_cli)\n", "def nbdev_export(\n", " path:str=None, # Path or filename\n", - " procs:Param(\"tokens naming the export processors to use.\", nargs=\"*\", choices=optional_procs())=\"black_format\",\n", + " procs:str='', # Space-separated `module:name` processors to use (or set `export_procs` in pyproject.toml)\n", " **kwargs):\n", " \"Export notebooks in `path` to Python modules\"\n", " if os.environ.get('IN_TEST',0): return\n", " if not is_nbdev(): raise Exception('`nbdev_export` must be called from a directory within a nbdev project.')\n", - " if procs:\n", - " import nbdev.export\n", - " procs = [getattr(nbdev.export, p) for p in L(procs)]\n", + " cfg = get_config()\n", + " procs = procs.split() if procs else cfg.get('export_procs', [])\n", + " procs = [import_obj(p) for p in procs] if procs else None\n", " files = nbglob(path=path, as_path=True, **kwargs).sorted('name')\n", " for f in files: nb_export(f, procs=procs)\n", - " add_init(get_config().lib_path)\n", + " add_init(cfg.lib_path)\n", " _build_modidx()" ] }, - { - "cell_type": "markdown", - "id": "7dc0b73e", - "metadata": {}, - "source": [ - "`procs` names the optional processors you wish to run on the exported cells of your notebook.\n", - "\n", - "N.B.: the `black_format` processor is passed in by default. But it is a no-op, unless `black_formatting = true` is set in `[tool.nbdev]` in your `pyproject.toml`. You can omit it from `nbdev_export` on the command line by passing in `--procs`." - ] - }, { "cell_type": "markdown", "id": "94ba347f", diff --git a/nbs/api/10_processors.ipynb b/nbs/api/10_processors.ipynb index 694436360..47bf11314 100644 --- a/nbs/api/10_processors.ipynb +++ b/nbs/api/10_processors.ipynb @@ -29,7 +29,6 @@ "source": [ "#| export\n", "import ast\n", - "import importlib\n", "\n", "from nbdev.config import *\n", "from nbdev.imports import *\n", @@ -743,20 +742,6 @@ "assert res" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "a761e07c", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "def _import_obj(s):\n", - " mod_nm, obj_nm = s.split(':')\n", - " mod = importlib.import_module(mod_nm)\n", - " return getattr(mod, obj_nm)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -768,8 +753,8 @@ "class FilterDefaults:\n", " \"Override `FilterDefaults` to change which notebook processors are used\"\n", " def xtra_procs(self):\n", - " imps = get_config().get('procs', '').split()\n", - " return [_import_obj(o) for o in imps]\n", + " imps = get_config().get('doc_procs', '').split()\n", + " return [import_obj(o) for o in imps]\n", "\n", " def base_procs(self):\n", " return [FrontmatterProc, populate_language, add_show_docs, insert_warning,\n", @@ -817,13 +802,7 @@ "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/tutorials/extensions.ipynb b/nbs/tutorials/extensions.ipynb index 09c7469e3..98d7c32b1 100644 --- a/nbs/tutorials/extensions.ipynb +++ b/nbs/tutorials/extensions.ipynb @@ -31,6 +31,27 @@ "id": "521da717-490e-473c-8d4c-c0ae0db3061d", "metadata": {}, "source": [ + "## Built-in export processor: `scrub_magics`\n", + "\n", + "nbdev includes one export processor out of the box: `scrub_magics`. It removes Jupyter magic lines (like `%%time`, `%matplotlib inline`, `%%spark`) from your exported `.py` files.\n", + "\n", + "To enable it, add to your `pyproject.toml`:\n", + "\n", + "```toml\n", + "[tool.nbdev]\n", + "export_procs = [\"nbdev.scrubmagics:scrub_magics\"]\n", + "```\n", + "\n", + "For example, a cell like this:\n", + "\n", + "```python\n", + "%%time\n", + "#| export\n", + "def my_func(): return 42\n", + "```\n", + "\n", + "Will export without the `%%time` line.\n", + "\n", "## What will this cover?\n", "\n", "With `nbdev`, it's possible to customize and extend it further beyond the standard capabilities through a well thoughtout and scalable framework. Does your particular library or need require you to inject custom quarto additives in certain cells? What about if you want to do something more trivial such as finding shortcuts to replace complicated quarto directives more easily (such as replacing `::: {.column-margin}` with `#| margin`)? \n", @@ -595,16 +616,16 @@ "source": [ "## How to enable the plugin on your project\n", "\n", - "This requires adding a `procs` key to `[tool.nbdev]` in your `pyproject.toml`.\n", + "This requires adding a `doc_procs` key to `[tool.nbdev]` in your `pyproject.toml`.\n", "\n", "For example, if this were code that lived in `nbdev`, we can specify where the processor comes from:\n", "\n", "```toml\n", "[tool.nbdev]\n", - "procs = [\"nbdev.extensions:LayoutProc\"]\n", + "doc_procs = [\"nbdev.extensions:LayoutProc\"]\n", "```\n", "\n", - "It follows the format of `library.module:processor_name`\n", + "It follows the format of `library.module:ProcessorClass`\n", "\n", "If this were being used from an external library (such as how this processor is based on the one that lives in [nbdev-extensions](https://muellerzr.github.io/nbdev-extensions)), you should add that to the dependencies of your project:\n", "\n", @@ -616,6 +637,71 @@ "And you're done! Now when calling `nbdev_docs` or `nbdev_preview` the processor we just made will be *automatically* applied to your notebooks and perform this conversion!" ] }, + { + "cell_type": "markdown", + "id": "477c189a", + "metadata": {}, + "source": [ + "## Export processors\n", + "\n", + "The `Processor` class shown above is for **doc processors** — they transform notebooks when building documentation with `nbdev_docs`.\n", + "\n", + "There's a simpler kind of processor for **export processors** — they transform code cells when exporting to `.py` files with `nbdev_export`. These are just plain functions, not classes.\n", + "\n", + "### Writing an export processor\n", + "\n", + "An export processor is a function that receives a notebook cell and modifies it in place:\n", + "\n", + "```python\n", + "def my_export_proc(cell):\n", + " \"Transform code cells during export\"\n", + " if cell.cell_type != 'code': return\n", + " # Modify cell.source as needed\n", + " cell.source = transform(cell.source)\n", + "```\n", + "\n", + "For example, here's a formatter using `black`:\n", + "\n", + "```python\n", + "# nbdev_black/__init__.py\n", + "def black_format(cell):\n", + " \"Format code cells with black\"\n", + " if cell.cell_type != 'code': return\n", + " try:\n", + " import black\n", + " cell.source = black.format_str(cell.source, mode=black.Mode())\n", + " except: pass # Leave malformed code unchanged\n", + "```\n", + "\n", + "### Enabling export processors\n", + "\n", + "Add `export_procs` to your `pyproject.toml`:\n", + "\n", + "```toml\n", + "[tool.nbdev]\n", + "export_procs = [\"nbdev_black:black_format\"]\n", + "```\n", + "\n", + "You can chain multiple processors — they run in order:\n", + "\n", + "```toml\n", + "[tool.nbdev]\n", + "export_procs = [\n", + " \"nbdev_black:black_format\",\n", + " \"my_project.procs:my_custom_proc\",\n", + "]\n", + "```\n", + "\n", + "### Key differences from doc processors\n", + "\n", + "| | Doc processors | Export processors |\n", + "|---|---|---|\n", + "| **Used by** | `nbdev_docs` | `nbdev_export` |\n", + "| **Interface** | `Processor` class with `cell()` method | Simple function |\n", + "| **Config key** | `doc_procs` | `export_procs` |\n", + "| **Purpose** | Transform notebooks for documentation | Transform code for `.py` files |" + ] + }, { "cell_type": "markdown", "id": "52cad971-b227-4426-880e-c4c1fc09f482",