Using nbdev for a library with a single class

Hi,

I have an experimental library that I really want to finalise and publish using nbdev. However, the library is currently a single .py file with one main class, and most of the library functionalities are implemented as methods of this class.

Is there any natural way to do something like this in nbdev? AFAIK, the class needs to be defined within a single cell, which means I can’t really “interleave” the text, explanations, examples etc, nbdev-style between methods. Is there any way to use nbdev for a project that is mostly built around a single class?

1 Like

You can use the patch method from fastcore to add new methods to your class in new cells. That’s what we do in fastai v2 for very long classes.

3 Likes

Also note that methods that aren’t patched need their own show_doc cells for documentation. So you can interleave examples and prose there.

3 Likes

Thanks for your answers! And sorry for the long delay, this is a side project and it got neglected for a while…

@patch seems to be indeed very helpful. Does this mean I need to add fastcore as a dependency to my package, and export from fastcore.foundation import patch to foo.py?

1 Like

There’s a ton of useful stuff in fastcore, so I’d consider using it! But if you really just want to use patching it’s actually a very nice and slim implementation with only two built-in dependencies:

import functools
from types import FunctionType

def copy_func(f):
    "Copy a non-builtin function (NB `copy.copy` does not work for this)"
    if not isinstance(f,FunctionType): return copy(f)
    fn = FunctionType(f.__code__, f.__globals__, f.__name__, f.__defaults__, f.__closure__)
    fn.__dict__.update(f.__dict__)
    return fn

def patch_to(cls, as_prop=False):
    "Decorator: add `f` to `cls`"
    if not isinstance(cls, (tuple,list)): cls=(cls,)
    def _inner(f):
        for c_ in cls:
            nf = copy_func(f)
            # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually
            for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o))
            nf.__qualname__ = f"{c_.__name__}.{f.__name__}"
            setattr(c_, f.__name__, property(nf) if as_prop else nf)
        return f
    return _inner

def patch(f):
    "Decorator: add `f` to the first parameter's class (based on f's type annotations)"
    cls = next(iter(f.__annotations__.values()))
    return patch_to(cls)(f)

That’s the only code needed:

class MyClass():
    def __init__(self):
        pass
    
@patch
def new_fun(self:MyClass):
    print("I'm a patched function!")
    
MyInstance = MyClass()
MyInstance.new_fun()
"I'm a patched function!"

EDIT: Oh I forgot to mention that fastcore itself is extremely low weight: The only external library used is numpy (and dataclasses if your python is < 3.7).

8 Likes

I had the same problem (long classes in one cell) and found this forum post, but I don’t see the patch function in fastcore anymore. Has it moved, or been renamed?

[EDIT] I went through the fastcore history on GitHub and found it has recently been moved to fastcore.basics. I’m all set then. FYI, the search function at fastcore.fast.ai can’t find it in a search for “patch”:

4 Likes