Skip to content

Bug Report: nbdev Parser IndexError on @patch_to with Keyword Arguments #1596

Description

@Mr-Ruben

Bug Report: nbdev Parser IndexError on @patch_to with Keyword Arguments

Description

The nbdev AST parser fails with an IndexError when it encounters the @patch_to decorator used with a keyword argument (e.g., cls=ClassName) instead of a positional argument. This occurs during processes like nbdev_export that involve indexing symbols.

Root Cause

In nbdev/doclinks.py, the patch_name function attempts to extract the class name being patched. It explicitly assumes the class is the first positional argument in the decorator's AST representation.

File: nbdev/doclinks.py
Code Snippet:

elif nm=='patch_to': a = d.args[0]  # <--- CRASHES HERE

If d.args is empty (which happens when a keyword argument is used), d.args[0] raises an IndexError.

Reproduction Proof

As demonstrated in the reproduction script above:

  • Input: @patch_to(MyClass) -> d.args contains 1 element. Result: Success.
  • Input: @patch_to(cls=MyClass) -> d.args is empty; d.keywords contains the data. Result: IndexError.

Impact

Users cannot use standard Python keyword argument syntax for the @patch_to decorator without breaking the nbdev export/documentation pipeline.

Recommended Fix

The parser should check both d.args and d.keywords. A robust fix would be:

elif nm=='patch_to': 
    a = d.args[0] if d.args else [k.value for k in d.keywords if k.arg=='cls'][0]

Reproduce with

import os
import nbformat as nbf
import traceback
from nbdev.doclinks import nbdev_export

# 1. Setup a clean minimal nbdev project structure
!rm -rf /content/reproduction_project
!mkdir -p /content/reproduction_project/reproduction_project
%cd /content/reproduction_project

# 2. Manually create pyproject.toml
with open('pyproject.toml', 'w') as f:
    f.write("""[project]\nname = \"reproduction_project\"\nauthors = [{name=\"Test\"}]\n\n[tool.nbdev]\nlib_name = \"reproduction_project\"\nuser = \"testuser\"\nlib_path = \"reproduction_project\"\nnbs_path = \".\"\nrecursive = false\ntst_flags = \"notest\"\n""")

# 3. Create a notebook with the bug-triggering code and required default_exp
nb = nbf.v4.new_notebook()
# Adding default_exp to satisfy nbdev requirements
code = """#| default_exp core\n#| export\nfrom fastcore.utils import patch_to\n\nclass MyClass: pass\n\n@patch_to(cls=MyClass)\ndef my_method(self): \n    return \"Hello World\"\n"""
nb['cells'] = [nbf.v4.new_code_cell(code)]

with open('00_core.ipynb', 'w') as f:
    nbf.write(nb, f)

print("\n--- Attempting nbdev_export (Expected to trigger IndexError) ---")
try:
    nbdev_export()
except Exception as e:
    print(f"\nCaught Error: {type(e).__name__}: {e}")
    traceback.print_exc()

I get


--- Attempting nbdev_export (Expected to trigger IndexError) ---

Caught Error: IndexError: list index out of range
Traceback (most recent call last):
  File "/tmp/ipykernel_2846/2623198774.py", line 26, in <cell line: 0>
    nbdev_export()
  File "/usr/local/lib/python3.12/dist-packages/fastcore/script.py", line 161, in _f
    if not mod: return func(*args, **kwargs)
                       ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 156, in nbdev_export
    _build_modidx()
  File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 112, in _build_modidx
    try: res['syms'].update(_get_modidx((dest.parent/file).resolve(), code_root, nbs_path=nbs_path))
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 91, in _get_modidx
    if isinstance(tree, _def_types): _stor(patch_name(tree))
                                           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 51, in patch_name
    elif nm=='patch_to': a = d.args[0]
                             ~~~~~~^^^
IndexError: list index out of range

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions