Source code for synapse.lib.autodoc

import logging

import synapse.common as s_common

import synapse.lib.config as s_config

logger = logging.getLogger(__name__)

rstlvls = [
    ('#', {'over': True}),
    ('*', {'over': True}),
    ('=', {}),
    ('-', {}),
    ('^', {}),
]

stormtype_doc_schema = {
    'definitions': {

        'stormType': {
            'type': ['string', 'array', 'object'],
            'items': {'type': 'string'},
            'properties': {
                'name': {'type': 'string',
                         'description': 'For a function argument, the name of the argument.'},
                'desc': {'type': 'string',
                         'description': 'For a function argument or return value, the description of the value.'},
                'type': {'$ref': '#/definitions/stormType'},
                'args': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/stormType'},
                    'description': 'Arguments to document.',
                },
                'returns': {'$ref': '#/definitions/stormType',
                            'description': 'Function return types to document'},
                'default': {'type': ['boolean', 'integer', 'string', 'null'],
                            'description': 'For a function argument, the default value, if applicable.'},
            },
            'required': ['type'],
            'description': 'A multi-purpose container for holding types information. If this '
                           'is a string or list of strings, it represents simple return types.'
                           ' If it is a object, it should represent a function to generate '
                           'documentation for.',
            'additionalProperties': False,
        },

        'stormtypeDoc': {
            'type': 'object',
            'properties': {
                'name': {'type': 'string',
                         'description': 'The name of the object.'},
                'desc': {'type': 'string',
                         'description': 'The docstring of the object.'},
                'type': {'$ref': '#/definitions/stormType'}
            },
            'additionalProperties': False,
        },

    },
    'type': 'object',
    'properties': {
        'path': {
            'type': 'array',
            'items': {
                'type': 'string'
            },
            'minItems': 1,
            'description': 'The path of the object.'
        },
        'desc': {
            'type': 'string',
            'description': 'The doc for the object itself.'
        },
        'locals': {
            'type': 'array',
            'items': {'$ref': '#/definitions/stormtypeDoc'},
            'description': 'A list of attributes, functions, getters, and setters to document.',
        },
        'additionalProperties': False,
    },
}
reqValidStormTypeDoc = s_config.getJsValidator(stormtype_doc_schema)

[docs]class RstHelp: def __init__(self): self.lines = []
[docs] def addHead(self, name, lvl=0, link=None): char, info = rstlvls[lvl] under = char * len(name) lines = [] lines.append('') if link: lines.append('') lines.append(link) lines.append('') if info.get('over'): lines.append(under) lines.append(name) lines.append(under) lines.append('') self.addLines(*lines)
[docs] def addLines(self, *lines): self.lines.extend(lines)
[docs] def getRstText(self): return '\n'.join(self.lines)
[docs]def ljuster(ilines): '''Helper to lstrip lines of whitespace an appropriate amount.''' baseline = ilines[0] assert baseline != '' newbaseline = baseline.lstrip() assert newbaseline != '' diff = len(baseline) - len(newbaseline) assert diff >= 0 newlines = [line[diff:] for line in ilines] return newlines
[docs]def scrubLines(lines): '''Remove any empty lines until we encounter non-empty linee''' newlines = [] for line in lines: if line == '' and not newlines: continue newlines.append(line) return newlines
[docs]def prepareRstLines(doc): '''Prepare a desc string for RST lines.''' lines = doc.split('\n') lines = scrubLines(lines) lines = ljuster(lines) return lines
[docs]def getArgLines(rtype): lines = [] args = rtype.get('args', ()) assert args is not None if args == (): # Zero args return lines lines.append('\n') lines.append('Args:') for arg in args: name = arg.get('name') desc = arg.get('desc') atyp = arg.get('type') assert name is not None assert desc is not None assert atyp is not None name = name.replace('*', '\\*') if isinstance(atyp, str): line = f' {name} ({atyp}): {desc}' elif isinstance(atyp, (list, tuple)): assert len(atyp) > 1 for obj in atyp: assert isinstance(obj, str) tdata = ', '.join([f'``{obj}``' for obj in atyp]) rline = f'The input type may one one of the following: {tdata}.' line = f' {name}: {desc} {rline}' elif isinstance(atyp, dict): logger.warning('Fully declarative input types are not yet supported.') rline = f"The input type is derived from the declarative type ``{atyp}``." line = f' {name}: {desc} {rline}' else: raise AssertionError(f'unknown argtype: {atyp}') lines.extend((line, '\n')) return lines
[docs]def genCallsig(rtype): items = [] args = rtype.get('args', ()) assert args is not None for arg in args: name = arg.get('name') defv = arg.get('default', s_common.novalu) item = name if defv is not s_common.novalu: item = f'{item}={defv}' items.append(item) ret = f"({', '.join(items)})" return ret
[docs]def getRtypeStr(rtype, known_types, types_prefix, suffix): if rtype in known_types: rtype = getLink(rtype, types_prefix, ref=True, suffix=suffix) else: rtype = f'``{rtype}``' return rtype
[docs]def getReturnLines(rtype, known_types=None, types_prefix=None, suffix=None, isstor=False): # Allow someone to plumb in name=Yields as a return type. lines = [''] whitespace = ' ' if known_types is None: known_types = set() if isinstance(rtype, str): lines.append('Returns:') lines.append(f' The type is {getRtypeStr(rtype, known_types, types_prefix, suffix)}.') elif isinstance(rtype, (list, tuple)): assert len(rtype) > 1 tdata = ', '.join([f'{getRtypeStr(obj, known_types, types_prefix, suffix)}' for obj in rtype]) lines.append('Returns:') lines.append(f' The type may be one of the following: {tdata}.') elif isinstance(rtype, dict): returns = rtype.get('returns') assert returns is not None, f'Invalid returns for {rtype}' name = returns.get('name', 'Returns') desc = returns.get('desc') rettype = returns.get('type') lines.append(f'{name}:') # Now switch on the type. parts = [whitespace] if desc: parts.append(desc) if isinstance(rettype, str): parts.append(f"The return type is {getRtypeStr(rettype, known_types, types_prefix, suffix)}.") elif isinstance(rettype, (list, tuple)): assert len(rettype) > 1 tdata = ', '.join([f'{getRtypeStr(obj, known_types, types_prefix, suffix)}' for obj in rettype]) rline = f'The return type may be one of the following: {tdata}.' parts.append(rline) elif isinstance(rettype, dict): logger.warning('Fully declarative input types are not yet supported.') rline = f"The return type is derived from the declarative type ``{rettype}``." parts.append(rline) else: raise AssertionError(f'unknown return type: {rettype}') line = ' '.join(parts) lines.append(line) if isstor: line = f'{whitespace} When this is used to set the value, it does not have a return type.' lines.append(line) return lines
[docs]def docStormTypes(page, docinfo, linkprefix, islib=False, lvl=1, known_types=None, types_prefix=None, types_suffix=None, ): ''' Process a list of StormTypes doc information to add them to a a RstHelp object. Notes This will create internal hyperlink link targets for each header item. The link prefix string must be given with the ``linkprefix`` argument. Args: page (RstHelp): The RST page to add . docinfo (dict): A Stormtypes Doc. linkprefix (str): The RST link prefix string to use. islib (bool): Treat the data as a library. This will preface the header and attribute values with ``$`` and use full paths for attributes. lvl (int): The base header level to use when adding headers to the page. Returns: None ''' if known_types is None: known_types = set() for info in docinfo: reqValidStormTypeDoc(info) path = info.get('path') sname = '.'.join(path) safesname = sname.replace(':', '\\:') if islib: link = getLink(sname, linkprefix) page.addHead(f"${safesname}", lvl=lvl, link=link) else: link = getLink(sname, linkprefix, suffix=types_suffix) page.addHead(safesname, lvl=lvl, link=link) typedoc = info.get('desc') lines = prepareRstLines(typedoc) page.addLines(*lines) locls = info.get('locals', ()) locls = sorted(locls, key=lambda x: x.get('name')) for locl in locls: name = locl.get('name') loclname = '.'.join((sname, name)) desc = locl.get('desc') rtype = locl.get('type') assert desc is not None assert rtype is not None link = f'.. _{linkprefix}-{loclname.replace(":", ".").replace(".", "-")}:' if isinstance(rtype, dict): rname = rtype.get('type') if isinstance(rname, dict): raise AssertionError(f'rname as dict not supported loclname={loclname} rname={rname}') isstor = False isfunc = False isgtor = False isctor = False if rname == 'ctor' or 'ctor' in rname: isctor = True if rname == 'function' or 'function' in rname: isfunc = True if rname == 'gtor' or 'gtor' in rname: isgtor = True if rname == 'stor' or 'stor' in rname: isstor = True lines = prepareRstLines(desc) arglines = getArgLines(rtype) lines.extend(arglines) retlines = getReturnLines(rtype, known_types=known_types, types_prefix=types_prefix, suffix=types_suffix, isstor=isstor) lines.extend(retlines) callsig = '' if isfunc: callsig = genCallsig(rtype) header = f'{name}{callsig}' header = header.replace('*', r'\*') else: header = name lines = prepareRstLines(desc) retlines = getReturnLines(rtype, known_types=known_types, types_prefix=types_prefix, suffix=types_suffix) lines.extend(retlines) if islib: header = '.'.join((safesname, header)) header = f'${header}' page.addHead(header, lvl=lvl + 1, link=link) page.addLines(*lines)