Docments

Document parameters using comments.

A docment is a comment beside a parameter in a function signature. It keeps the parameter’s description next to its name, type and default. docments(f) reads these comments for documentation tools and CLIs such as fastcore.script:

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.

NumPy-style docstrings have formatting rules, including where to put spaces before colons. That formatting is a pain to write and maintain, and awkward to read in code. Here’s an example 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 reads NumPy-style docstrings. You can combine them with parameter comments. The helpers below read and parse the docstrings.


source

docstring

def docstring(
    sym
):

Get documentation from an object, its constructor, or its callable implementation

docstring reads the function’s prose independently of its parameter comments.

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

Classes without their own prose can use a documented parent constructor. An undocumented class does not inherit the generic object.__init__ description.

class Named:
    def __init__(self, name):
        "Create a named object."
        self.name = name

class NamedChild(Named): pass
class Undocumented: pass

test_eq(docstring(NamedChild), "Create a named object.")
test_eq(docstring(Undocumented), '')
docstring(NamedChild)
'Create a named object.'

source

parse_docstring

def parse_docstring(
    sym
):

Parse a numpy-style docstring in sym

NumPy parameter sections become named records with a type and description. The summary remains separate from those parameter descriptions.

npdocs = parse_docstring(add_np)
test_eq(npdocs.Parameters['a'].type, 'int')
npdocs.Parameters
{'a': Parameter(name='a', type='int', desc=['the 1st number to add']),
 'b': Parameter(name='b', type='int', desc=['the 2nd number to add (default: 0)'])}

source

get_source

def get_source(
    s
):

Get source for a function, callable implementation, dataclass, or source string


source

get_dataclass_source

def get_dataclass_source(
    s
):

Get source code for dataclass s


source

isdataclass

def isdataclass(
    s
):

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

get_source unwraps decorated functions and partials to find their implementation. Passing source text leaves it unchanged.

source = get_source(add)
test_eq(get_source(partial(add, 1)), source)
test_eq(get_source(source), source)
PrettyString(source)
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

source

get_name

def get_name(
    obj
):

Get the name of obj


source

qual_name

def qual_name(
    obj
):

Get the qualified name of obj

get_name uses the short function or method name. qual_name retains its enclosing class or module name.

test_eq(get_name(L.map), 'map')
test_eq(qual_name(L.map), 'L.map')
test_eq(qual_name(docscrape), 'fastcore.docscrape')
get_name(L.map), qual_name(L.map)
('map', 'L.map')

Extracting parameter documentation

Follow one delegated function through comment extraction. _d supplies its own documentation for b; a and z come from the functions it delegates to:

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

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

@delegates(_c)
def _d(
    c:int, # First
    b:str, # Second
    **kwargs
)->int: # Return an int
    return c, _c(b, **kwargs)

Python’s tokenizer retains the comments that the syntax tree omits. First collect each comment by source line:

s = _d
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'}

Parameter locations use one-based source line numbers. The return annotation also has a location when present.

parms = _param_locs(s, returns=True, args_kwargs=True) or {}
parms
{3: 'c', 4: 'b', 5: 'kwargs', 6: 'return'}

A parameter uses its trailing comment, or consecutive comment lines immediately above it. Comments belonging to another parameter are not included.

docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}
test_eq(docs['c'], 'First')
docs
{'c': 'First', 'b': 'Second', 'kwargs': None, 'return': 'Return an int'}

Full docments retain the signature’s annotation and default alongside the comment.

sig = signature(s, eval_str=True)
res = {name:_get_full(p, docs) for name,p in sig.parameters.items()}
test_is(res['c'].anno, int)
res
{'c': {'docment': 'First',
  'anno': int,
  'default': inspect._empty,
  'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
 'b': {'docment': 'Second',
  'anno': str,
  'default': inspect._empty,
  'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
 'a': {'docment': None,
  'anno': int,
  'default': 2,
  'kind': <_ParameterKind.KEYWORD_ONLY: 3>},
 'z': {'docment': None,
  'anno': str,
  'default': 'b',
  'kind': <_ParameterKind.KEYWORD_ONLY: 3>}}

Add the return annotation and its comment to res. _d.__delwrap__ points to _c, where the next pass looks for descriptions of delegated parameters.

res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty)
test_eq(res['return'].docment, 'Return an int')
res['return']
{ 'anno': <class 'int'>,
  'default': <class 'inspect._empty'>,
  'docment': 'Return an int'}
_d.__delwrap__
<function __main__._c(b: str, a: int = 2, *, z: str = 'b')>

Docments


source

ann_parts

def ann_parts(
    anno
):

The underlying type and metadata tuple of an Annotated, else (anno, ())

ann_parts splits an annotation into its underlying type and its Annotated metadata, returning an empty tuple for plain annotations. An Annotated[None, ...] reports its type as None (used for CLI params whose argparse action takes no value).

test_eq(ann_parts(int), (int, ()))
test_eq(ann_parts(Annotated[int, "doc", dict(nargs='+')]), (int, ("doc", dict(nargs='+'))))
test_eq(ann_parts(Annotated[None, "ver"]), (None, ("ver",)))

source

docments

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

Get parameter and return documentation from comments, Annotated metadata, and NumPy docstrings

def add(
    a:int,  # The first operand
    b=1,    # The second operand
) -> int:   # The sum
    "Add `a` to `b`"
    return a + b
test_eq(docments(add), {'a': 'The first operand', 'b': 'The second operand', 'return': 'The sum'})

For longer descriptions, put comment lines above the parameter. docments also reads typing.Annotated string metadata and NumPy-style docstrings. Pass full=True to retrieve each parameter’s default, type and docment together.

docs = docments(_d)
test_eq(docs, {'c':'First', 'b':'Second', 'a':None, 'z':'Last', 'return':'Return an int'})
docs
{'a': None, 'b': 'Second', 'c': 'First', 'return': 'Return an int', 'z': 'Last'}
full = docments(_d, full=True)
test_eq({k:v.docment for k,v in full.items()}, docs)
test_eq(full.a.default, 2)
full
{ 'a': { 'anno': <class 'int'>,
         'default': 2,
         'docment': None,
         'kind': <_ParameterKind.KEYWORD_ONLY: 3>},
  'b': { 'anno': <class 'str'>,
         'default': <class 'inspect._empty'>,
         'docment': 'Second',
         'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
  'c': { 'anno': <class 'int'>,
         'default': <class 'inspect._empty'>,
         'docment': 'First',
         'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
  'return': { 'anno': <class 'int'>,
              'default': <class 'inspect._empty'>,
              'docment': 'Return an int'},
  'z': { 'anno': <class 'str'>,
         'default': 'b',
         'docment': 'Last',
         'kind': <_ParameterKind.KEYWORD_ONLY: 3>}}

Editing a module on disk can leave a live object’s source locations out of date. If that source no longer parses, docments returns signature information without comments. Restart the interpreter to load the edited module. Invalid source text passed directly to docments still raises a syntax error:

def _fn(a:int): return a
_real = get_source
def get_source(s): return '    "stale"\nx = 1'
try: test_eq(docments(_fn), {'a': None, 'return': None})
finally: get_source = _real
test_fail(lambda: _parses('    "stale"\nx = 1'), contains='indent')

Instead of a comment, you can document a parameter or return value with typing.Annotated metadata. The first string in the metadata is used as the docment. A trailing comment wins if both are present. Generated APIs can carry these descriptions without Python source:

def _g(
    a:Annotated[int, "the first"],
    b:Annotated[str, "ignored"]='x' # the second
) -> Annotated[list[int], "the results"]: ...

test_eq(docments(_g), {'a': 'the first', 'b': 'the second', 'return': 'the results'})

docments returns a dict mapping parameter names to descriptions. The return key holds the return description. Pass returns=False to omit that entry. Here it is for add:

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',
         'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
  'args': { 'anno': <class 'inspect._empty'>,
            'default': <class 'inspect._empty'>,
            'docment': 'some args',
            'kind': <_ParameterKind.VAR_POSITIONAL: 2>},
  'b': { 'anno': <class 'int'>,
         'default': 0,
         'docment': 'the 2nd number to add',
         'kind': <_ParameterKind.KEYWORD_ONLY: 3>},
  'kwargs': { 'anno': <class 'inspect._empty'>,
              'default': <class 'inspect._empty'>,
              'docment': None,
              'kind': <_ParameterKind.VAR_KEYWORD: 4>},
  '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',
  'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}
docments(add, full=True)['a']
{ 'anno': <class 'int'>,
  'default': <class 'inspect._empty'>,
  'docment': 'the 1st number to add',
  'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}

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`'}

NumPy descriptions fill gaps in the parameter comments. When both document the same parameter, the comment takes precedence:

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

Parameters
----------
a : int
    description overridden by the parameter comment
b : int
    the 2nd number to add (default: 0)"""
    return a+b
mixed = docments(add_mixed, full=True)
test_eq(mixed.a.docment, 'the first number to add')
test_eq(mixed.b.docment, 'the 2nd number to add (default: 0)')
mixed
{ 'a': { 'anno': <class 'int'>,
         'default': <class 'inspect._empty'>,
         'docment': 'the first number to add',
         'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
  'b': { 'anno': <class 'inspect._empty'>,
         'default': <class 'inspect._empty'>,
         'docment': 'the 2nd number to add (default: 0)',
         'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},
  'return': { 'anno': <class 'int'>,
              'default': <class 'inspect._empty'>,
              'docment': 'the result'}}

docments reads dataclass field comments from the class’s source file. For a dataclass defined in a notebook, export it to a module and import the class from there.

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.

sig_source preserves the original source lines through the signature. A body written on the same line is included; a body on subsequent lines is omitted.

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
source = sig_source(multi)
assert '          c=2,' in source
assert 'return a' not in source
PrettyString(source)
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

extract_docstrings reads source without executing it. It records public functions and methods, module prose, and a class’s constructor parameters. A constructor docstring supplies class documentation when the class has none.

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


source

DocmentTbl

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

Compute the docment table string

DocmentTbl renders parameter documentation as a Markdown table. String annotations can be displayed without resolving their types:

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

_dm = DocmentTbl(_f)
assert 'description of param a' in str(_dm)
assert 'Returns' in str(_dm)
assert 'Result' in _dm.params_str
_dm
Type Default Details
a description of param a
b bool True description of param b
c Result None
Returns int

Markdown table cells escape pipes and footnote markers. Existing escapes are preserved, and line breaks become <br>.

def compile_pattern(
    # Alternatives: a | b
    # Escaped form: a \| b
    # See ^[syntax]
    pattern:str
): return re.compile(pattern)

table = DocmentTbl(compile_pattern)
assert r'Alternatives: a \| b<br>Escaped form: a \| b<br>See \^[syntax]' in str(table)
table
Type Details
pattern str Alternatives: a | b
Escaped form: a | b
See ^[syntax]

Columns without any values are omitted. This function has no defaults or documented return value, so neither the Default column nor the Returns row appears.

def _f(
    a,
    b:int, #param b
    c:str  #param c
):
    "Do a thing"
    ...
_dm2 = DocmentTbl(_f)
assert 'Default' not in str(_dm2)
assert 'Returns' not in str(_dm2)
assert 'param b' in str(_dm2)
_dm2
Type Details
a
b int param b
c str param c

By default, passing a class to DocmentTbl shows its constructor parameters:

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:

method_table = DocmentTbl(_Test.foo)
assert 'description of param c' in str(method_table)
assert 'self' not in method_table.dm
method_table
Type Default Details
c int description of param c
d bool True description of param d

source

DocmentList

def DocmentList(
    obj
):

DocmentList renders each parameter as a bullet containing its type, default, and description.

listing = DocmentList(_f)
assert 'param b' in str(listing)
listing
  • a
  • b:int   param b
  • c:str   param c
  • return

Signature formatting starts a new line after each documented parameter. Undocumented parameters share a line when they fit.

formatted = _fmt_sig('foo', [('a:int', 'first'), ('b:str', None), ('c', 'third')], ')->int:', 80)
assert 'a:int, # first\n' in formatted
assert 'b:str, c, # third' in formatted
PrettyString(formatted)
def foo(
    a:int, # first
    b:str, c, # third
)->int:

source

DocmentText

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

source

DocmentText.params

def params():

DocmentText keeps formatted parameters separate from their descriptions before assembling the signature.

parameters = DocmentText(_f).params
test_eq(parameters[1], ('b:int', 'param b'))
parameters
[('a', None), ('b:int', 'param b'), ('c:str', 'param c')]

source

DocmentText.__str__

def __str__():

Return str(self).

DocmentText assembles those parameters into a Python signature with docments beside them.

text = DocmentText(_f)
assert 'b:int, # param b' in str(text)
text
def _f(
    a, b:int, # param b
    c:str, # param c
):
    "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:

A partial binds arguments without losing the remaining parameters’ docments.

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

Non-callable objects render as their type and value, rather than pretending to be functions:

test_eq(str(DocmentText(3, docstring=False)), 'int instance: 3')

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

Signatures retain positional-only / and keyword-only * separators. Varargs keep their */** prefixes, whether annotated or not:

def _va(
    a:int, # First value
    /, *ids:str, limit:Annotated[int, 'Maximum results']=10, **kw
) -> list[str]: pass

sigtext = str(sig2str(_va))
assert all(s in sigtext for s in ('First value', '/', '*ids:str', 'limit:int=10', 'Maximum results', '**kw', 'list[str]'))
def _vb(a, /, *, b=1): pass
assert ', /, *,' in str(sig2str(_vb))
sig2str(_va)
def _va(
    a:int, # First value
    /, *ids:str, limit:int=10, # Maximum results
    **kw
)->list[str]:

Documentation For An Object

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

NumPy parameter and return sections are omitted from the remaining prose when their types agree with the signature. Their descriptions will appear beside the parameters instead.

remaining = _docstring(add_np, docments(add_np, full=True))
assert 'Parameters' not in remaining
assert 'The sum of two numbers.' in remaining
PrettyString(remaining)
The sum of two numbers.

    Used to demonstrate numpy-style docstrings.

source

can_render

def can_render(
    sym
):

Check if sym has a renderable signature


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

NumPy-style sections remain part of the full documentation. Parameter and return descriptions appear beside the signature; notes, errors, and examples stay below it.

def bounded_add(a:int, b:int=0) -> int:
    """Add two non-negative numbers.

    Parameters
    ----------
    a : int
        First operand.
    b : int
        Second operand.

    Returns
    -------
    int
        Their sum.

    Notes
    -----
    Both operands must be non-negative.

    Raises
    ------
    ValueError
        An operand is negative.

    Examples
    --------
    >>> bounded_add(2, 3)
    5
    """
    if min(a, b) < 0: raise ValueError('Negative operand')
    return a+b

rendered = str(MarkdownRenderer(bounded_add))
assert all(s in rendered for s in ('First operand.', 'Their sum.', 'Both operands', 'ValueError', 'bounded_add(2, 3)'))
assert 'Parameters\n' not in rendered and 'Returns\n' not in rendered
MarkdownRenderer(bounded_add)
def bounded_add(
    a:int, # First operand.
    b:int=0, # Second operand.
)->int: # Their sum.

Add two non-negative numbers.

Notes

Both operands must be non-negative.

Raises

ValueError An operand is negative.

Examples

bounded_add(2, 3) 5

If a symbol’s source is unreadable, the renderer shows its signature without docments and warns with the symbol name and error:

def _stale(): pass
_stale.__code__ = _stale.__code__.replace(co_firstlineno=99999)
with warnings.catch_warnings(record=True) as w:
    warnings.simplefilter('always')
    r = MarkdownRenderer(_stale)
assert 'docments unavailable' in str(w[0].message)
assert '_stale' in str(r)
r
_stale()
def _f(
    a,
    b:callable=print, #param b
    c:str='foo'  #param c
)->str: # Result of doing it
    "Do a thing"
    ...

MarkdownRenderer(_f)
def _f(
    a, b:<built-in function callable>=print, # param b
    c:str='foo', # param c
)->str: # Result of doing it

Do a thing

print(MarkdownRenderer(_f))
def _f(
    a, b:<built-in function callable>=print, # param b
    c:str='foo', # param c
)->str: # Result of doing it"""Do a thing"""

Delegated signatures retain the wrapper’s own docments and the delegated parameters.

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
):
MarkdownRenderer(add_async)
async def add_async(
    a:int, # The first operand
    b: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.
)->int: # The result is calculated using Python's builtin `+` operator.

Add a to b

Some builtins have no inspectable signature. Their original docstring still supplies the calling convention and behavior.

builtin_docs = MarkdownRenderer(next)
test_is(builtin_docs.sig, None)
assert 'StopIteration' in builtin_docs.docs
builtin_docs
def next():

next(iterator[, default])

Return the next item from the iterator. If default is given and the iterator is exhausted, it is returned instead of raising StopIteration.

class Foo:
    "A callable calculator"
    async def __call__(self,
        x:int, # Input value
    ) -> int:
        "Calculate asynchronously"
        return x

foo = Foo()
rendered = str(MarkdownRenderer(foo))
assert all(s in rendered for s in ('async def', 'Input value', 'Calculate asynchronously'))
MarkdownRenderer(foo)
async def Foo.__call__(
    x:int, # Input value
)->int:

Calculate asynchronously

Fastgit’s Git resolves arbitrary subcommands through __getattr__, including names such as fget. Probing for fget can mistake such an instance for a property. The renderer uses isinstance to identify properties. Here it documents the instance’s own __call__, not a partial returned by __getattr__:

class Bar:
    def __call__(self, x):
        "Call bar"
        return x
    def __getattr__(self, nm):
        if nm.startswith('_'): raise AttributeError(nm)
        return partial(self, nm)

r = MarkdownRenderer(Bar())
assert 'Bar.__call__' in str(r)
r
def Bar.__call__(
    x
):

Call bar