# Released under the MIT License. See LICENSE for details. # """Documentation generation functionality.""" from __future__ import annotations import sys import os import inspect from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING from enum import Enum from efro.error import CleanError from efro.terminal import Clr if TYPE_CHECKING: from types import ModuleType from typing import Optional, Callable, Any, Sequence CATEGORY_STRING = 'Category:' DO_STYLES = False DO_CSS_CLASSES = False STYLE_PAD_L0 = ' style="padding-left: 0px;"' if DO_STYLES else '' STYLE_PAD_L30 = ' style="padding-left: 30px;"' if DO_STYLES else '' STYLE_PAD_L60 = ' style="padding-left: 60px;"' if DO_STYLES else '' class CategoryType(Enum): """Self explanatory.""" FUNCTION = 0 CLASS = 1 @dataclass class AttributeInfo: """Info about an attribute of a class.""" name: str attr_type: Optional[str] = None docs: Optional[str] = None @dataclass class FunctionInfo: """Info about a function/method.""" name: str category: Optional[str] = None method_class: Optional[str] = None docs: Optional[str] = None is_class_method: bool = False @dataclass class ClassInfo: """Info about a class of functions/classes.""" name: str category: str methods: list[FunctionInfo] inherited_methods: list[FunctionInfo] attributes: list[AttributeInfo] parents: list[str] docs: Optional[str] enum_values: Optional[list[str]] def parse_docs_attrs(attrs: list[AttributeInfo], docs: str) -> str: """Given a docs str, parses attribute descriptions contained within.""" docs_lines = docs.splitlines() attr_line = None for i, line in enumerate(docs_lines): if line.strip() in ['Attributes:', 'Attrs:']: attr_line = i break if attr_line is not None: # Docs is now everything *up to* this. docs = '\n'.join(docs_lines[:attr_line]) # Go through remaining lines creating attrs and docs for each. cur_attr: Optional[AttributeInfo] = None for i in range(attr_line + 1, len(docs_lines)): line = docs_lines[i].strip() # A line with a single alphanumeric word preceding a colon # is a new attr. splits = line.split(':') if (len(splits) in (1, 2) and splits[0] and splits[0].replace('_', '').isalnum()): if cur_attr is not None: attrs.append(cur_attr) cur_attr = AttributeInfo(name=splits[0]) if len(splits) == 2: cur_attr.attr_type = splits[1] # Any other line gets tacked onto the current attr. else: if cur_attr is not None: if cur_attr.docs is None: cur_attr.docs = '' cur_attr.docs += line + '\n' # Finish out last. if cur_attr is not None: attrs.append(cur_attr) for attr in attrs: if attr.docs is not None: attr.docs = attr.docs.strip() return docs def _get_defining_class(cls: type, name: str) -> Optional[type]: for i in cls.mro()[1:]: if hasattr(i, name): return i return None def _get_bases(cls: type) -> list[str]: bases = [] for par in cls.mro()[1:]: if par is not object: bases.append(par.__module__ + '.' + par.__name__) return bases def _split_into_paragraphs(docs: str, filter_type: str, indent: int) -> str: indent_str = str(indent) + 'px' # Ok, now break into paragraphs (2 newlines denotes a new paragraph). paragraphs = docs.split('\n\n') docs = '' for i, par in enumerate(paragraphs): # For function/method signatures, indent lines after the first so # our big multi-line function signatures are readable. if (filter_type in ['function', 'method'] and i == 0 and len(par.split('(')) > 1 and par.strip().split('(')[0].replace('.', '').replace( '_', '').isalnum()): style = (' style="padding-left: ' + str(indent + 50) + 'px; text-indent: -50px;"') if DO_STYLES else '' style2 = ' style="color: #666677;"' if DO_STYLES else '' docs += f'
' # Also, signatures seem to have quotes around annotations. # Let's just strip them all out. This will look wrong if # we have a string as a default value though, so don't # in that case. if " = '" not in par and ' = "' not in par: par = par.replace("'", '') docs += par docs += '
\n\n' # Emphasize a few specific lines. elif par.strip() in [ 'Conditions:', 'Available Conditions:', 'Actions:', 'Available Actions:', 'Play Types:', 'Available Setting Options:', 'Available Values:', 'Usage:' ]: style = (' style="padding-left: ' + indent_str + ';"' if DO_STYLES else '') docs += f'' docs += par docs += '
\n\n' elif par.lower().strip().startswith(CATEGORY_STRING.lower()): style = (' style="padding-left: ' + indent_str + ';"' if DO_STYLES else '') docs += f'' docs += par docs += '
\n\n' elif par.strip().startswith('#'): p_lines = par.split('\n') for it2, line in enumerate(p_lines): if line.strip().startswith('#'): style = (' style="color: #008800;"' if DO_STYLES else '') p_lines[it2] = (f'' + line + '') par = '\n'.join(p_lines) style = (' style="padding-left: ' + indent_str + ';"' if DO_STYLES else '') docs += f''
docs += par
docs += '\n\n'
else:
style = (' style="padding-left: ' + indent_str +
';"' if DO_STYLES else '')
docs += f'' docs += par docs += '
\n\n' return docs def _filter_type_settings(filter_type: str) -> tuple[Optional[Callable], int]: get_category_href_func = None if filter_type == 'class': indent = 30 get_category_href_func = _get_class_category_href elif filter_type == 'method': indent = 60 elif filter_type == 'function': indent = 30 get_category_href_func = _get_function_category_href elif filter_type == 'attribute': indent = 60 else: raise Exception('invalid filter_type: ' + str(filter_type)) return get_category_href_func, indent def _get_defining_class_backwards(cls: type, name: str) -> Optional[type]: mro = cls.mro() mro.reverse() for i in mro: if hasattr(i, name): return i return None def _get_module_classes(module: ModuleType) -> list[tuple[str, type]]: names = dir(module) # Look for all public classes in the provided module. class_objs = [ getattr(module, name) for name in names if not name.startswith('_') ] class_objs = [ c for c in class_objs if str(c).startswith('