cleanup docs.py

This commit is contained in:
Roman Trapeznikov 2022-02-11 10:06:08 +03:00
parent 542b0ec131
commit 236a89e62b
No known key found for this signature in database
GPG Key ID: 89BED52F1E290F8D

View File

@ -6,33 +6,15 @@ from __future__ import annotations
import sys import sys
import os import os
import inspect
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from enum import Enum
from efro.error import CleanError from efro.error import CleanError
from efro.terminal import Clr from efro.terminal import Clr
if TYPE_CHECKING: if TYPE_CHECKING:
from types import ModuleType from typing import Optional
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 @dataclass
@ -43,29 +25,6 @@ class AttributeInfo:
docs: 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: def parse_docs_attrs(attrs: list[AttributeInfo], docs: str) -> str:
"""Given a docs str, parses attribute descriptions contained within.""" """Given a docs str, parses attribute descriptions contained within."""
docs_lines = docs.splitlines() docs_lines = docs.splitlines()
@ -112,256 +71,6 @@ def parse_docs_attrs(attrs: list[AttributeInfo], docs: str) -> str:
return docs 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'<p{style}><span{style2}>'
# 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 += '</span></p>\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'<p{style}><strong>'
docs += par
docs += '</strong></p>\n\n'
elif par.lower().strip().startswith(CATEGORY_STRING.lower()):
style = (' style="padding-left: ' + indent_str +
';"' if DO_STYLES else '')
docs += f'<p{style}>'
docs += par
docs += '</p>\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'<span{style}><em><small>' + line +
'</small></em></span>')
par = '\n'.join(p_lines)
style = (' style="padding-left: ' + indent_str +
';"' if DO_STYLES else '')
docs += f'<pre{style}>'
docs += par
docs += '</pre>\n\n'
else:
style = (' style="padding-left: ' + indent_str +
';"' if DO_STYLES else '')
docs += f'<p{style}>'
docs += par
docs += '</p>\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('<class ')
or str(c).startswith('<type ') or str(c).startswith('<enum ')
]
# Build a map of full names and their corresponding classes.
classes_by_name = [(c.__module__ + '.' + c.__name__, c)
for c in class_objs]
# Strip duplicates (for instance ba.Dep and ba.Dependency are the same
# thing at runtime)
classes_by_name = list(set(classes_by_name))
# And add those classes' inner classes.
_add_inner_classes(class_objs, classes_by_name)
return classes_by_name
def _is_inherited(cls: type, name: str) -> bool:
method = getattr(cls, name)
# Classmethods are already bound with the class as self.
# we need to just look at the im_func in that case
is_class_method = (inspect.ismethod(method) and method.__self__ is cls)
if is_class_method:
for mth in cls.mro()[1:]:
mth2 = getattr(mth, name, None)
if mth2 is not None:
if method.__func__ == mth2.__func__:
return True
else:
return any(method == getattr(i, name, None) for i in cls.mro()[1:])
return False
def _get_function_category_href(c_name: str) -> str:
"""Return a href for linking to the specified function category."""
return 'function_category_' + c_name.replace('.', '_').replace(' ', '_')
def _get_class_category_href(c_name: str) -> str:
"""Return a href for linking to the specified class category."""
return 'class_category_' + c_name.replace('.', '_').replace(' ', '_')
def _get_class_href(c_name: str) -> str:
"""Return a href for linking to the specified class."""
return 'class_' + c_name.replace('.', '_')
def _get_function_href(f_name: str) -> str:
"""Return a href for linking to the specified function."""
return 'function_' + f_name.replace('.', '_')
def _get_method_href(c_name: str, f_name: str) -> str:
"""Return a href for linking to the specified method."""
return 'method_' + c_name.replace('.', '_') + '__' + f_name.replace(
'.', '_')
def _get_attribute_href(c_name: str, a_name: str) -> str:
return 'attr_' + c_name.replace('.', '_') + '__' + a_name.replace('.', '_')
def _get_category(docs: str, category_type: CategoryType) -> str:
"""Parse the category name from a docstring."""
category_lines = [
l for l in docs.splitlines()
if l.lower().strip().startswith(CATEGORY_STRING.lower())
]
if category_lines:
category = category_lines[0].strip()[len(CATEGORY_STRING):].strip()
else:
category = {
CategoryType.CLASS: 'Misc Classes',
CategoryType.FUNCTION: 'Misc Functions'
}[category_type]
return category
def _print_child_classes(category_classes: list[ClassInfo], parent: str,
indent: int) -> str:
out = ''
valid_classes = []
for cls in category_classes:
if cls.parents:
c_parent = cls.parents[0]
else:
c_parent = ''
# If it has a parent not in this category, consider it to
# have no parent.
if c_parent != '':
found = False
for ctest in category_classes:
if c_parent == ctest.name:
found = True
break
if not found:
c_parent = ''
# Print only if its parent matches what we want.
if c_parent == parent:
valid_classes.append(cls)
if valid_classes:
out += ' ' * indent + '<ul>\n'
for cls in valid_classes:
out += (' ' * indent + ' <li><a href="#' +
_get_class_href(cls.name) + '">' + cls.name + '</a></li>\n')
out += _print_child_classes(category_classes, cls.name, indent + 1)
if valid_classes:
out += ' ' * indent + '</ul>\n'
return out
def _add_inner_classes(class_objs: Sequence[type],
classes_by_name: list[tuple[str, type]]) -> None:
# Ok, now go through all existing classes and look for classes
# defined within.
for cls in class_objs:
for name in dir(cls):
if name.startswith('_'):
continue
obj = getattr(cls, name)
if not inspect.isclass(obj):
continue
if _get_defining_class_backwards(cls, name) != cls:
continue
classes_by_name.append(
(cls.__module__ + '.' + cls.__name__ + '.' + name, obj))
def generate(projroot: str) -> None: def generate(projroot: str) -> None:
"""Main entry point.""" """Main entry point."""
import pdoc import pdoc