fastai / nbdev

Create delightful software with Jupyter Notebooks
https://nbdev.fast.ai/
Apache License 2.0
4.8k stars 484 forks source link

Python 2.3.12 `nbdev_preview` complains about `show_doc` even though `show_doc` works in Notebook? #1354

Closed dsm-72 closed 1 year ago

dsm-72 commented 1 year ago
#| default_exp nbs

#| hide
from nbdev.showdoc import *

#| export
import os, re, nbformat as nbf
from nbformat import NotebookNode
from typing import List, Optional, Union
from tqdm.auto import tqdm
import warnings

#| export
@dataclass
class NotebookPrefixer:
    path: Optional[str] = None
    name: Optional[str] = None
    maximal: Optional[bool] = False

    def __post_init__(self):
        self.path = self.path if self.path else os.getcwd()

    def int_to_prefix_int(self, i: int) -> str:
        if i < 10:
            return f'0{i}'
        else:
            return str(i)

    def prefix_int_to_int(self, prefix: str) -> int:
        if prefix == '00':
            return 0        
        return int(prefix.lstrip('0'))

    def prefix_from_filename(self, name:str):
        return self.prefix_int_to_int(name.split('_')[0])

    def filename_without_prefix(self, name:str):
        if '_' not in name:
            return name
        return '_'.join(name.split('_')[1:])

    def next_nb_prefix(
        self, path: Optional[str] = None, 
        name: Optional[str] = None, maximal: bool = False
    ):
        """
        Calculate the next missing value of the notebook prefixes in a directory.

        Parameters
        ----------
        path : str
            The path to the directory containing the notebooks.
        name : Optional[str], optional
            The name of the notebook to check, by default None
        maximal : bool, optional
            If True, return the maximum prefix + 1, otherwise return the smallest missing prefix, by default False

        Returns
        -------
        int
            The next missing value of the notebook prefixes.
        """
        # Get the list of notebooks in the directory
        path = path if path else self.path
        name = name if name else self.name
        maximal = maximal if maximal is not None else self.maximal

        notebooks = [file for file in os.listdir(path) if file.endswith('.ipynb')]

        # Filter out non-convention notebooks
        notebooks = [file for file in notebooks if re.match(r'\d{2,}_.*\.ipynb', file)]

        # Extract the prefixes
        prefixes = sorted([self.prefix_from_filename(file) for file in notebooks])
        # Check if a name is provided
        if name is not None:
            # Check if there is a notebook that matches the name
            for file in notebooks:
                cur_name = self.filename_without_prefix(file).replace('.ipynb', '')
                basename = self.filename_without_prefix(name).replace('.ipynb', '')
                if basename == cur_name:
                    prefix = self.prefix_from_filename(file)
                    prefix = self.int_to_prefix_int(prefix)
                    return prefix

        if maximal:
            # Return the maximum prefix + 1
            return self.int_to_prefix_int(max(prefixes) + 1)

        else:
            # Return the smallest missing prefix
            for i, prefix in enumerate(prefixes[:-1]):
                nextix = prefixes[i + 1]
                if nextix - prefix > 1:
                    return self.int_to_prefix_int(prefix + 1)

#| export 
#| exec_doc
NBDEV_DEFAULT_EXP = '#| default_exp'
AUTORELOAD = '%load_ext autoreload\n%autoreload 2'
NBDEV_SHOWDOC = '#| hide\nfrom nbdev.showdoc import *'
NBDEV_EXPORT = '#| hide\nimport nbdev; nbdev.nbdev_export()'

@dataclass
class NotebookAggregator:
    """
    A class to aggregate Jupyter notebooks.

    Parameters
    ----------
    path : str
        The path to the directory containing the notebooks.
    module : Optional[str], optional
        The name of the module, by default None
    output : Optional[str], optional
        The path to the output notebook, by default None
    ignore : List[str], optional
        A list of notebooks to ignore, by default []
    """
    path: str

    module: Optional[str] = None
    output: Optional[str] = None
    ignore: List[str] = field(default_factory=list)

    prefix: Optional[Union[bool, str]] = True
    prefix_dir: Optional[str] = None

    @property
    def basename(self):
        return os.path.basename(self.path)

    @property
    def dirname(self):
        return os.path.dirname(self.path)

    @property
    def notebooks(self):
        notebooks = [
            file for file in os.listdir(self.path) 
            if file.endswith('.ipynb') and file not in self.ignore
        ]
        notebooks = sorted(notebooks)
        return notebooks

    def __post_init__(self):
        if not self.module:
            self.module = self.basename

        if not self.prefix_dir:
            self.prefix_dir = self.dirname

        if not self.output:
            prefix = ''
            if self.prefix == True:
                prefix = f'{self.get_prefix()}_'

            elif isinstance(self.prefix, str):
                prefix = f'{self.prefix}_'

            self.output = os.path.join(self.dirname, f'{prefix}{self.module}.ipynb')

    def nb_as_href(self, name: str):
        return f'[{name}]({os.path.join(self.path, name)})'

    def format_header(self):
        res = f'# {self.module}\n'
        res += '> This notebook was generated from the following notebooks:\n\n'
        for name in self.notebooks:
            res += f'- {self.nb_as_href(name)}\n'
        return res

    def add_current_notebook_section(self, nb: NotebookNode, name: str):
        head = name.lstrip('_').rstrip('.ipynb').replace('_', ' ')
        if head.split()[0].isnumeric():
            head = ' '.join(head.split()[1:])

        nb['cells'].append(nbf.v4.new_markdown_cell(
            f'## {head.capitalize()}\n> This notebook was generated from {self.nb_as_href(name)})'
        ))
        return nb

    def write_nbdev_head(self, nb: NotebookNode):
        # Add the first four cells
        nb['cells'].append(nbf.v4.new_markdown_cell(self.format_header()))
        nb['cells'].append(nbf.v4.new_code_cell(f'{NBDEV_DEFAULT_EXP} {self.module}'))
        nb['cells'].append(nbf.v4.new_code_cell(AUTORELOAD))
        nb['cells'].append(nbf.v4.new_code_cell(NBDEV_SHOWDOC))
        return nb

    def write_nbdev_tail(self, nb: NotebookNode):  
        # Add the final cell      
        nb['cells'].append(nbf.v4.new_code_cell(NBDEV_EXPORT))
        return nb

    def get_prefix(self):
        prefixer = NotebookPrefixer(path=self.prefix_dir, name=self.module)
        prefix = prefixer.next_nb_prefix()
        return prefix

    def aggregate(self):
        new_nb = nbf.v4.new_notebook()
        new_nb['cells'] = []

        new_nb = self.write_nbdev_head(new_nb)

        # Iterate over the notebooks
        for nb_name in tqdm(self.notebooks, desc='Aggregating notebooks'):

            # Read the notebook
            with open(os.path.join(self.path, nb_name)) as f:
                nb = nbf.read(f, as_version=4)

            # Find the start and end cells
            start_cell = next(i for i, cell in enumerate(nb.cells) if cell.source.strip() == NBDEV_SHOWDOC)
            end_cell = next(i for i, cell in enumerate(nb.cells) if cell.source.strip() == NBDEV_EXPORT)

            self.add_current_notebook_section(new_nb, nb_name)

            # Add the cells to the new notebook
            new_nb.cells.extend(nb.cells[start_cell+1:end_cell])

        new_nb = self.write_nbdev_tail(new_nb)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            # Normalize the notebook
            changes, new_nb = nbf.validator.normalize(new_nb)

        # Write the new notebook
        with open(self.output, 'w') as f:
            nbf.write(new_nb, f)

#| eval: false
from nbdev import show_doc, __version__
show_doc(NotebookAggregator)

yields the following:

NotebookAggregator

 NotebookAggregator (path:str, module:Optional[str]=None,
                     output:Optional[str]=None,
                     ignore:List[str]=<factory>,
                     prefix:Union[bool,str,NoneType]=True,
                     prefix_dir:Optional[str]=None)

A class to aggregate Jupyter notebooks.

  | Type | Default | Details -- | -- | -- | -- path | str |   | The path to the directory containing the notebooks. module | Optional | None | The name of the module, by default None output | Optional | None | The path to the output notebook, by default None ignore | List |   | A list of notebooks to ignore, by default [] prefix | Union | True |   prefix_dir | Optional | None
and then: ``` nbdev_prepare ``` ``` nbdev_preview concurrent.futures.process._RemoteTraceback: """ Traceback (most recent call last): File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/IPython/core/interactiveshell.py", line 3508, in run_code exec(code_obj, self.user_global_ns, self.user_ns) File "", line 3, in show_doc(NotebookAggregator) ^^^^^^^^^^^^^^^^^^ NameError: name 'NotebookAggregator' is not defined The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/process.py", line 256, in _process_worker r = call_item.fn(*call_item.args, **call_item.kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/process.py", line 205, in _process_chunk return [fn(*args) for args in chunk] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/process.py", line 205, in return [fn(*args) for args in chunk] ^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/parallel.py", line 46, in _call return g(item) ^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/serve_drv.py", line 22, in main if src.suffix=='.ipynb': exec_nb(src, dst, x) ^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/serve_drv.py", line 16, in exec_nb cb()(nb) File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/processors.py", line 243, in __call__ def __call__(self, nb): return self.nb_proc(nb).process() ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/process.py", line 126, in process for proc in self.procs: self._proc(proc) ^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/process.py", line 119, in _proc for cell in self.nb.cells: self._process_cell(proc, cell) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/process.py", line 110, in _process_cell if callable(proc) and not _is_direc(proc): cell = opt_set(cell, proc(cell)) ^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/processors.py", line 205, in __call__ raise Exception(f"Error{' in notebook: '+title if title else ''} in cell {cell.idx_} :\n{cell.source}") from self.k.exc[1] Exception: Error in notebook: Notebooks in cell 32 : #| echo: false #| output: asis show_doc(NotebookAggregator) """ The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/USER/mambaforge/envs/MY_LIB/bin/nbdev_preview", line 10, in sys.exit(nbdev_preview()) ^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/script.py", line 119, in _f return tfunc(**merge(args, args_from_prog(func, xtra))) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/quarto.py", line 289, in nbdev_preview cache,cfg,path = _pre_docs(path, n_workers=n_workers, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/quarto.py", line 174, in _pre_docs cache = proc_nbs(path, n_workers=n_workers, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/nbdev/serve.py", line 77, in proc_nbs parallel(nbdev.serve_drv.main, files, n_workers=n_workers, pause=0.01, **kw) File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/parallel.py", line 117, in parallel return L(r) ^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/foundation.py", line 98, in __call__ return super().__call__(x, *args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/foundation.py", line 106, in __init__ items = listify(items, *rest, use_list=use_list, match=match) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/site-packages/fastcore/basics.py", line 66, in listify elif is_iter(o): res = list(o) ^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/process.py", line 597, in _chain_from_iterable_of_lists for element in iterable: File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/_base.py", line 619, in result_iterator yield _result_or_cancel(fs.pop()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/_base.py", line 317, in _result_or_cancel return fut.result(timeout) ^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/_base.py", line 456, in result return self.__get_result() ^^^^^^^^^^^^^^^^^^^ File "/Users/USER/mambaforge/envs/MY_LIB/lib/python3.11/concurrent/futures/_base.py", line 401, in __get_result raise self._exception Exception: Error in notebook: Notebooks in cell 32 : #| echo: false #| output: asis show_doc(NotebookAggregator) ```
dsm-72 commented 1 year ago

Figured it out! @jph00

I had to move:

#| export 
NBDEV_DEFAULT_EXP = '#| default_exp'
AUTORELOAD = '%load_ext autoreload\n%autoreload 2'
NBDEV_SHOWDOC = '#| hide\nfrom nbdev.showdoc import *'
NBDEV_EXPORT = '#| hide\nimport nbdev; nbdev.nbdev_export()'

to another cell >.<