Docments

Document parameters using comments.

docments provides programmatic access to comments in function parameters and return types. It can be used to create more developer-friendly documentation, CLI, etc tools.

Why?

Without docments, if you want to document your parameters, you have to repeat param names in docstrings, since they’re already in the function signature. The parameters have to be kept synchronized in the two places as you change your code. Readers of your code have to look back and forth between two places to understand what’s happening. So it’s more work for you, and for your users.

Furthermore, to have parameter documentation formatted nicely without docments, you have to use special magic docstring formatting, often with odd quirks, which is a pain to create and maintain, and awkward to read in code. For instance, using numpy-style documentation:

def add_np(a:int, b:int=0)->int:
    """The sum of two numbers.
    
    Used to demonstrate numpy-style docstrings.

Parameters
----------
a : int
    the 1st number to add
b : int
    the 2nd number to add (default: 0)

Returns
-------
int
    the result of adding `a` to `b`"""
    return a+b

By comparison, here’s the same thing using docments:

def add(
    a:int, # the 1st number to add
    b=0,   # the 2nd number to add
)->int:    # the result of adding `a` to `b`
    "The sum of two numbers."
    return a+b

Numpy docstring helper functions

docments also supports numpy-style docstrings, or a mix or numpy-style and docments parameter documentation. The functions in this section help get and parse this information.


source

docstring


def docstring(
    sym
):

Get docstring for sym for functions ad classes

test_eq(docstring(add), "The sum of two numbers.")

source

parse_docstring


def parse_docstring(
    sym
):

Parse a numpy-style docstring in sym

# parse_docstring(add_np)

source

isdataclass


def isdataclass(
    s
):

Check if s is a dataclass but not a dataclass’ instance


source

get_dataclass_source


def get_dataclass_source(
    s
):

Get source code for dataclass s


source

get_source


def get_source(
    s
):

Get source code for string, function object or dataclass s

parms = _param_locs(add)
parms
{2: 'a', 3: 'b', 4: 'return'}
_get_comment(2, 'a', {2: ' the 1st number to add'}, parms)
'the 1st number to add'

source

get_name


def get_name(
    obj
):

Get the name of obj

test_eq(get_name(in_ipython), 'in_ipython')
test_eq(get_name(L.map), 'map')

source

qual_name


def qual_name(
    obj
):

Get the qualified name of obj

assert qual_name(docscrape) == 'fastcore.docscrape'

Docments

Let’s manually go through each step of _docments to see what it does:

def _b(
    z:str='b', # Last
):
    return b, a

@delegates(_b)
def _c(
    b:str, # Ignore
    a:int=2
): return b, a # Third

@delegates(_c)
def _d(
    c:int, # First
    b:str, # Second
    **kwargs
)->int: # Return an int
    return c, _c(b, **kwargs)
s = _d
nps = parse_docstring(s)
if isclass(s) and not is_dataclass(s): s = s.__init__
comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT}
comments
{3: ' First', 4: ' Second', 6: ' Return an int'}
parms = _param_locs(s, returns=True, args_kwargs=True) or {}
parms
{3: 'c', 4: 'b', 5: 'kwargs', 6: 'return'}
docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}
docs
{'c': 'First', 'b': 'Second', 'kwargs': None, 'return': 'Return an int'}
sig = signature(s, eval_str=True)
res = {name:_get_full(p, docs) for name,p in sig.parameters.items()}
res
{'c': {'docment': 'First', 'anno': int, 'default': inspect._empty},
 'b': {'docment': 'Second', 'anno': str, 'default': inspect._empty},
 'a': {'docment': None, 'anno': int, 'default': 2},
 'z': {'docment': None, 'anno': str, 'default': 'b'}}
res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty)
res = _merge_docs(res, nps)
res
{'c': {'docment': 'First', 'anno': int, 'default': inspect._empty},
 'b': {'docment': 'Second', 'anno': str, 'default': inspect._empty},
 'a': {'docment': None, 'anno': int, 'default': 2},
 'z': {'docment': None, 'anno': str, 'default': 'b'},
 'return': {'docment': 'Return an int',
  'anno': int,
  'default': inspect._empty}}
_d.__delwrap__
<function __main__._c(b: str, a: int = 2, *, z: str = 'b')>

source

docments


def docments(
    s, full:bool=False, eval_str:bool=False, returns:bool=True, args_kwargs:bool=False
):

Get docments for s

docments(_d)
{'a': None, 'b': 'Second', 'c': 'First', 'return': 'Return an int', 'z': 'Last'}
docments(_d, full=True)
{ 'a': {'anno': <class 'int'>, 'default': 2, 'docment': None},
  'b': { 'anno': <class 'str'>,
         'default': <class 'inspect._empty'>,
         'docment': 'Second'},
  'c': { 'anno': <class 'int'>,
         'default': <class 'inspect._empty'>,
         'docment': 'First'},
  'return': { 'anno': <class 'int'>,
              'default': <class 'inspect._empty'>,
              'docment': 'Return an int'},
  'z': {'anno': <class 'str'>, 'default': 'b', 'docment': 'Last'}}

The returned dict has parameter names as keys, docments as values. The return value comment appears in the return, unless returns=False. Using the add definition above, we get:

def add(
    a:int, # the 1st number to add
    b=0,   # the 2nd number to add
)->int:    # the result of adding `a` to `b`
    "The sum of two numbers."
    return a+b
docments(add)
{ 'a': 'the 1st number to add',
  'b': 'the 2nd number to add',
  'return': 'the result of adding `a` to `b`'}

args_kwargs=True adds args and kwargs docs too:

def add(
    a:int, # the 1st number to add
    *args, # some args
    b=0,   # the 2nd number to add
    **kwargs, # Passed to the `example` function
)->int:    # the result of adding `a` to `b`
    "The sum of two numbers."
    return a+b
docments(add, args_kwargs=True)
{ 'a': 'the 1st number to add',
  'args': 'some args',
  'b': 'the 2nd number to add',
  'kwargs': 'Passed to the `example` function',
  'return': 'the result of adding `a` to `b`'}

If you pass full=True, the values are dict of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied. (Note that for full, args_kwargs=True is always set too.)

docments(add, full=True)
{ 'a': { 'anno': <class 'int'>,
         'default': <class 'inspect._empty'>,
         'docment': 'the 1st number to add'},
  'args': { 'anno': <_ParameterKind.VAR_POSITIONAL: 2>,
            'default': <class 'inspect._empty'>,
            'docment': 'some args'},
  'b': { 'anno': <class 'int'>,
         'default': 0,
         'docment': 'the 2nd number to add'},
  'kwargs': { 'anno': <_ParameterKind.VAR_KEYWORD: 4>,
              'default': <class 'inspect._empty'>,
              'docment': None},
  'return': { 'anno': <class 'int'>,
              'default': <class 'inspect._empty'>,
              'docment': 'the result of adding `a` to `b`'}}

To evaluate stringified annotations (from python 3.10), use eval_str:

docments(add, full=True, eval_str=True)['a']
{ 'anno': <class 'int'>,
  'default': <class 'inspect._empty'>,
  'docment': 'the 1st number to add'}
docments(add, full=True)['a']
{ 'anno': <class 'int'>,
  'default': <class 'inspect._empty'>,
  'docment': 'the 1st number to add'}

If you need more space to document a parameter, place one or more lines of comments above the parameter, or above the return type. You can mix-and-match these docment styles:

def add(
    # The first operand
    a:int,
    # This is the second of the operands to the *addition* operator.
    # Note that passing a negative value here is the equivalent of the *subtraction* operator.
    b:int,
)->int: # The result is calculated using Python's builtin `+` operator.
    "Add `a` to `b`"
    return a+b
docments(add)
{ 'a': 'The first operand',
  'b': 'This is the second of the operands to the *addition* operator.\n'
       'Note that passing a negative value here is the equivalent of the '
       '*subtraction* operator.',
  'return': "The result is calculated using Python's builtin `+` operator."}

Docments works with async functions, too:

async def add_async(
    # The first operand
    a:int,
    # This is the second of the operands to the *addition* operator.
    # Note that passing a negative value here is the equivalent of the *subtraction* operator.
    b:int,
)->int: # The result is calculated using Python's builtin `+` operator.
    "Add `a` to `b`"
    return a+b
test_eq(docments(add_async), docments(add))

You can also use docments with classes and methods:

class Adder:
    "An addition calculator"
    def __init__(self,
        a:int, # First operand
        b:int, # 2nd operand
    ): self.a,self.b = a,b
    
    def calculate(self
                 )->int: # Integral result of addition operator
        "Add `a` to `b`"
        return a+b
docments(Adder)
{'a': 'First operand', 'b': '2nd operand', 'return': None, 'self': None}
docments(Adder.calculate)
{'return': 'Integral result of addition operator', 'self': None}

docments can also be extracted from numpy-style docstrings:

print(add_np.__doc__)
The sum of two numbers.

    Used to demonstrate numpy-style docstrings.

Parameters
----------
a : int
    the 1st number to add
b : int
    the 2nd number to add (default: 0)

Returns
-------
int
    the result of adding `a` to `b`
docments(add_np)
{ 'a': 'the 1st number to add',
  'b': 'the 2nd number to add (default: 0)',
  'return': 'the result of adding `a` to `b`'}

You can even mix and match docments and numpy parameters:

def add_mixed(a:int, # the first number to add
              b
             )->int: # the result
    """The sum of two numbers.

Parameters
----------
b : int
    the 2nd number to add (default: 0)"""
    return a+b
docments(add_mixed, full=True)
{ 'a': { 'anno': <class 'int'>,
         'default': <class 'inspect._empty'>,
         'docment': 'the first number to add'},
  'b': { 'anno': <class 'inspect._empty'>,
         'default': <class 'inspect._empty'>,
         'docment': 'the 2nd number to add (default: 0)'},
  'return': { 'anno': <class 'int'>,
              'default': <class 'inspect._empty'>,
              'docment': 'the result'}}

You can use docments with dataclasses, however if the class was defined in online notebook, docments will not contain parameters’ comments. This is because the source code is not available in the notebook. After converting the notebook to a module, the docments will be available. Thus, documentation will have correct parameters’ comments.

Docments even works with delegates:

from fastcore.meta import delegates
def _a(a:str=None): return a # First

@delegates(_a)
def _b(b:str, # Second
       **kwargs
      ): # Return nothing
    return b, (_a(**kwargs)) 

docments(_b)
{'a': 'First', 'b': 'Second', 'return': None}
docments(_b, full=True)
{ 'a': {'anno': <class 'str'>, 'default': None, 'docment': 'First'},
  'b': { 'anno': <class 'str'>,
         'default': <class 'inspect._empty'>,
         'docment': 'Second'},
  'return': { 'anno': <class 'inspect._empty'>,
              'default': <class 'inspect._empty'>,
              'docment': None}}

Builtins just return an empty dict:

docments(str)
{'args': None, 'kwargs': None, 'return': None, 'self': None}

Extract docstrings


source

sig_source


def sig_source(
    obj
):

Full source of signature line(s) for a function or class.

print(sig_source(flexiclass))
def flexiclass(
        cls # The class to convert
    ) -> dataclass:
def simple(x: dict[str, int]): return x
print(sig_source(simple))
def simple(x: dict[str, int]): return x
def multi(a, b=1,
          c=2,
          d=3):
    return a
print(sig_source(multi))
def multi(a, b=1,
          c=2,
          d=3):

source

extract_docstrings


def extract_docstrings(
    code
):

Create a dict from function/class/method names to tuples of docstrings and param lists

sample_code = """
"This is a module."

def top_func(a, b, *args, **kw):
    "This is top-level."
    pass

class SampleClass:
    "This is a class."

    def __init__(self, x, y):
        "Constructor for SampleClass."
        pass

    def method1(self, param1):
        "This is method1."
        pass

    def _private_method(self):
        "This should not be included."
        pass

class AnotherClass:
    def __init__(self, a, b):
        "This class has no separate docstring."
        pass"""

exp = {'_module': ('This is a module.', ''),
       'top_func': ('This is top-level.', 'a, b, *args, **kw'),
       'SampleClass': ('This is a class.', 'self, x, y'),
       'SampleClass.method1': ('This is method1.', 'self, param1'),
       'AnotherClass': ('This class has no separate docstring.', 'self, a, b')}
test_eq(extract_docstrings(sample_code), exp)

Rendering docment Tables

Render nicely formatted tables that shows docments for any function or method.


source

DocmentTbl


def DocmentTbl(
    obj, verbose:bool=True, returns:bool=True
):

Compute the docment table string

DocmentTbl can render a markdown table showing docments if appropriate. This is an example of how a docments table will render for a function:

def _f(a,      # description of param a
       b=True, # description of param b
       c:str=None
       ) -> int: ...

_dm = DocmentTbl(_f)
_dm
Type Default Details
a description of param a
b bool True description of param b
c str None
Returns int

If one column in the table has no information, for example because there are no default values, that column will not be shown. In the below example, the Default column, will not be shown. Additionally, if the return of the function is not annotated the Returns row will not be rendered:

def _f(a,
        b:int, #param b
        c:str='foo'  #param c
       )->str: # Result of doing it
    "Do a thing"
    ...
_dm2 = DocmentTbl(_f)
_dm2
Type Default Details
a
b int param b
c str foo param c
Returns str Result of doing it

DocmentTbl also works on classes. By default, the __init__ will be rendered:

class _Test:
    def __init__(self,
                 a,      # description of param a
                 b=True, # description of param b
                 c:str=None):
        ...

    def foo(self,
            c:int,      # description of param c
            d=True, # description of param d
           ):
        ...
DocmentTbl(_Test)
Type Default Details
a description of param a
b bool True description of param b
c str None

You can also pass a method to be rendered as well:

DocmentTbl(_Test.foo)
Type Default Details
c int description of param c
d bool True description of param d

source

DocmentList


def DocmentList(
    obj
):

Initialize self. See help(type(self)) for accurate signature.

DocmentList(_f)
  • a
  • b:int   param b
  • c:str=foo   param c
  • return:str   Result of doing it

source

DocmentText


def DocmentText(
    obj, maxline:int=110, docstring:bool=True
):

Initialize self. See help(type(self)) for accurate signature.

DocmentText(_f).params
[('a', None), ('b:int', 'param b'), ("c:str='foo'", 'param c')]
DocmentText(_f)
def _f(
    a, b:int, # param b
    c:str='foo', # param c
)->str: # Result of doing it
    "Do a thing"
def _g(
    a, b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int, cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int, # hi
    c:str='foo'
)->str:
    "Do a thing"

DocmentText(_g, maxline=80, docstring=False)
def _g(
    a, b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int,
    cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int, # hi
    c:str='foo'
)->str:
DocmentText(partial(_g, 1), maxline=80, docstring=False)
def _g[partial: 1](
    b:int, cccccccccccccccccccc:int, ccccccccdccccccccccc:int,
    cccccccccccecccccccc:int, cccccccfcccccccccc:int, ccccccccccccgccccc:int,
    c:str='foo'
)->str:

source

sig2str


def sig2str(
    func, maxline:int=110
):

Generate function signature with docments as comments

print(sig2str(_d))
def _d(
    c:int, # First
    b:str, # Second
    a:int=2, z:str='b', # Last
)->int: # Return an int

Documentation For An Object

Render the signature as well as the docments to show complete documentation for an object.


source

ShowDocRenderer


def ShowDocRenderer(
    sym, name:str | None=None, title_level:int=3, maxline:int=110
):

Show documentation for sym


source

MarkdownRenderer


def MarkdownRenderer(
    sym, name:str | None=None, title_level:int=3, maxline:int=110
):

Markdown renderer for show_doc

def _f(a,
        b:int, #param b
        c:str='foo'  #param c
       )->str: # Result of doing it
    "Do a thing"
    ...

MarkdownRenderer(_f)

def _f(
    a, b:int, # param b
    c:str='foo', # param c
)->str: # Result of doing it

Do a thing

def f(a:int=0 # aa
): pass

@delegates(f)
def g(
    b:int|str, # bb
    **kwargs
): return kwargs
MarkdownRenderer(g)

def g(
    b:int | str, # bb
    a:int=0, # aa
):