diff --git a/.idea/dictionaries/roman.xml b/.idea/dictionaries/roman.xml
index 58cf7470..941fd83d 100644
--- a/.idea/dictionaries/roman.xml
+++ b/.idea/dictionaries/roman.xml
@@ -3,7 +3,9 @@
gamename
maxlen
+ outdirname
pagename
+ pythondir
unhashable
diff --git a/assets/src/ba_data/python/bastd/ui/popup.py b/assets/src/ba_data/python/bastd/ui/popup.py
index d89374b2..8a50f8fb 100644
--- a/assets/src/ba_data/python/bastd/ui/popup.py
+++ b/assets/src/ba_data/python/bastd/ui/popup.py
@@ -15,7 +15,9 @@ if TYPE_CHECKING:
class PopupWindow:
- """A transient window that positions and scales itself for visibility."""
+ """A transient window that positions and scales itself for visibility.
+
+ Category: UI Classes"""
def __init__(self,
position: tuple[float, float],
diff --git a/tools/batools/docs.py b/tools/batools/docs.py
index 2653d528..a436eab1 100755
--- a/tools/batools/docs.py
+++ b/tools/batools/docs.py
@@ -2,20 +2,15 @@
#
"""Documentation generation functionality."""
-# pylint: disable=too-many-lines
-
from __future__ import annotations
-import importlib
+import sys
import os
-import datetime
import inspect
-import subprocess
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Union, cast
+from pathlib import Path
+from typing import TYPE_CHECKING
from enum import Enum
-import types
-import pydoc
from efro.error import CleanError
from efro.terminal import Clr
@@ -367,1059 +362,23 @@ def _add_inner_classes(class_objs: Sequence[type],
(cls.__module__ + '.' + cls.__name__ + '.' + name, obj))
-class BaseGenerator:
- """Base class which handles docs generation.
-
- Some generation options and target module set up in inherited classes."""
-
- # This should be overridden by inherited classes.
-
- # Top-level module (e.g. 'ba').
- top_module_name: str
-
- # Modules NOT to generate docs for. Example: ['ba.internal'].
- ignored_modules: list[str]
-
- # Docs file header.
- header: str = ''
-
- def __init__(self) -> None:
- self._index_keys: list[str] = []
-
- # All modules where we will search functions and classes to
- # generate docs for (including top-level module).
- self._submodules: set[ModuleType] = set()
-
- # Make a list of missing stuff so we can warn about it in one
- # big chunk at the end (so the user can batch their corrections).
- self._errors: list[Any] = []
- self._index: dict[str, tuple[str, Union[ClassInfo, FunctionInfo,
- AttributeInfo]]] = {}
- self._out = ''
- self._classes: list[ClassInfo] = []
- self._class_names: set[str] = set()
- self._functions: list[FunctionInfo] = []
- self._function_names: set[str] = set()
- self._merged_categories: list[tuple[str, str,
- list[Union[ClassInfo,
- FunctionInfo]]]] = []
-
- def name_variants(self, name: str) -> list[str]:
- """Return variants of a word (such as plural) for linking."""
- # Do 'ies' plural for words ending in y.
- # (but not things like foo.y or display or prey)
- if (len(name) > 1 and name.endswith('y') and name[-2].isalpha()
- and name[-2] not in {'a', 'e', 'i', 'o', 'u'}):
- return [name, f'{name[:-1]}ies']
- # Otherwise assume plural just ends with s:
- return [name, f'{name}s']
-
- def _add_index_links(self,
- docs: str,
- ignore_links: Optional[list[str]] = None) -> str:
- """Add links to indexed classes/methods/etc found in a docstr."""
- sub_num = 0
- subs = {}
-
- # Ok now replace any names found in our index with links.
- for index_entry in self._index_keys:
- if ignore_links is not None and index_entry in ignore_links:
- continue
-
- for index_entry_actual in self.name_variants(index_entry):
- bits = docs.split(index_entry_actual)
- docs = bits[0]
-
- # Look at the first char after each split; if its
- # not alphanumeric, lets replace.
- for i in range(1, len(bits)):
- bit = bits[i]
- if not bit:
- valid = True
- else:
- valid = not bit[:1].isalnum()
- if valid:
-
- # Strip out this name and replace it with a funky
- # string to prevent further replacements from
- # applying to it.. we'll then swap it back at the end.
- sub_name = '<__SUB' + str(sub_num) + '__>'
- subs[sub_name] = index_entry_actual
- sub_num += 1
-
- # Sub in link.
- docs += ('' + sub_name + '')
- else:
- docs += index_entry_actual # Keep original.
- docs += bits[i]
-
- for sub_name, sub_val in list(subs.items()):
- docs = docs.replace(sub_name, sub_val)
- return docs
-
- def _get_all_attrs_for_class(self, cls: type,
- docs: str) -> tuple[str, list[AttributeInfo]]:
- """
- if there's an 'Attributes' section in the docs, strip it out and
- create attributes entries out of it.
- Returns the stripped down docs as well as detected attrs.
- """
- attrs: list[AttributeInfo] = []
-
- # Start by pulling any type info we find in the doc str.
- # (necessary in many non-property cases since there's no other way
- # to annotate attrs)
- docs = parse_docs_attrs(attrs, docs)
-
- # In some cases we document an attr in the class doc-string but
- # provide an annotation for it at the type level.
- # (this is the case for simple class/instance attributes since we
- # can't provide docstrings along with those)
- self._get_class_level_types_for_doc_attrs(cls, attrs)
-
- # Now pull info on properties, which can have doc-strings and
- # annotations all in the same place; yay!
- self._get_property_attrs_for_class(cls, attrs)
-
- return docs, attrs
-
- def _get_class_level_types_for_doc_attrs(
- self, cls: type, attrs: list[AttributeInfo]) -> None:
- # Take note of all the attrs that we're aware of already;
- # these are the ones we can potentially provide type info for.
- existing_attrs_by_name = {a.name: a for a in attrs}
-
- cls_annotations = getattr(cls, '__annotations__', {})
-
- for aname, aval in cls_annotations.items():
- # (we expect __future__ annotations to always be on, which makes
- # these strings)
- assert isinstance(aval, str)
- if aname in existing_attrs_by_name:
- # Complain if there's a type in both the docs and the type.
- if existing_attrs_by_name[aname].attr_type is not None:
- print('FOUND', existing_attrs_by_name[aname], aval)
- self._errors.append(
- f'attr {aname} for class {cls}'
- 'has both a docstring and class level annotation;'
- ' should just have one')
- existing_attrs_by_name[aname].attr_type = aval
-
- def _get_property_attrs_for_class(self, cls: type,
- attrs: list[AttributeInfo]) -> None:
- for attrname in dir(cls):
- attr = getattr(cls, attrname)
-
- if isinstance(attr, property):
- if any(a.name == attrname for a in attrs):
- raise Exception(f'attr "{attrname}" has both a'
- f' class docs and property entry')
-
- # Pull its docs.
- attrdocs = getattr(attr, '__doc__', None)
- if attrdocs is None:
- self._errors.append(
- f'property \'{attrname}\' on class {cls}')
- attrdocs = '(no docs)'
- else:
- attrdocs = attrdocs.strip()
-
- # Pull type annotations.
- attr_annotations = getattr(attr.fget, '__annotations__')
- if (not isinstance(attr_annotations, dict)
- or 'return' not in attr_annotations
- or not isinstance(attr_annotations['return'], str)):
- raise Exception('property type annotation not found')
- attrtype = attr_annotations['return']
-
- if '(internal)' not in attrdocs:
- attrs.append(
- AttributeInfo(name=attrname,
- docs=attrdocs,
- attr_type=attrtype))
-
- def _get_base_docs_for_class(self, cls: type) -> str:
- if cls.__doc__ is not None:
- docs = cls.__doc__
- docs_lines = docs.splitlines()
- min_indent = 9999
- for i, line in enumerate(docs_lines):
- if line != '':
- spaces = 0
- while line and line[0] == ' ':
- line = line[1:]
- spaces += 1
- if spaces < min_indent:
- min_indent = spaces
- if min_indent == 9999:
- min_indent = 0
-
- for i, line in enumerate(docs_lines):
- if line != '':
- if not line.startswith(' ' * min_indent):
- raise Exception("expected opening whitespace: '" +
- line + "'; class " + str(cls))
- docs_lines[i] = line[min_indent:]
- docs = '\n'.join(docs_lines)
-
- else:
- docs = '(no docs)'
- self._errors.append(f'base docs for class {cls}')
- return docs
-
- def _get_enum_values_for_class(self, cls: type) -> Optional[list[str]]:
- if issubclass(cls, Enum):
- return [val.name for val in cls]
- return None
-
- def _get_methods_for_class(
- self, cls: type) -> tuple[list[FunctionInfo], list[FunctionInfo]]:
-
- method_types = [
- types.MethodDescriptorType, types.FunctionType, types.MethodType
- ]
- methods_raw = [
- getattr(cls, name) for name in dir(cls)
- if any(isinstance(getattr(cls, name), t)
- for t in method_types) and (
- not name.startswith('_') or name == '__init__') and
- '_no_init' not in name and '_no_init_or_replace_init' not in name
- ]
-
- methods: list[FunctionInfo] = []
- inherited_methods: list[FunctionInfo] = []
- for mth in methods_raw:
-
- # Protocols seem to give this...
- if mth.__name__ in {'_no_init', '_no_init_or_replace_init'}:
- continue
-
- # Keep a list of inherited methods but don't do a full
- # listing of them.
- if _is_inherited(cls, mth.__name__):
- dcls = _get_defining_class(cls, mth.__name__)
- assert dcls is not None
- inherited_methods.append(
- FunctionInfo(name=mth.__name__,
- method_class=dcls.__module__ + '.' +
- dcls.__name__))
- continue
-
- # Use pydoc stuff for python methods since it includes args.
-
- # Its a c-defined method.
- if isinstance(mth, types.MethodDescriptorType):
- if mth.__doc__ is not None:
- mdocs = mth.__doc__
- else:
- mdocs = '(no docs)'
- self._errors.append(mth)
- is_class_method = False
-
- # Its a python method.
- else:
- mdocs, is_class_method = self._python_method_docs(cls, mth)
- if '(internal)' not in mdocs:
- methods.append(
- FunctionInfo(name=mth.__name__,
- docs=mdocs,
- is_class_method=is_class_method))
- return methods, inherited_methods
-
- def _python_method_docs(self, cls: type,
- mth: Callable) -> tuple[str, bool]:
- mdocs_lines = pydoc.plain(pydoc.render_doc(mth)).splitlines()[2:]
-
- # Remove ugly 'method of builtins.type instance'
- # on classmethods.
- mdocs_lines = [
- l.replace('method of builtins.type instance', '')
- for l in mdocs_lines
- ]
-
- # Pydoc indents all lines but the first 4 spaces;
- # undo that.
- for i, line in enumerate(mdocs_lines):
- if i != 0:
- if not line.startswith(' '):
- raise Exception('UNEXPECTED')
- mdocs_lines[i] = line[4:]
-
- # Class-methods will show up as bound methods when we pull
- # them out of the type (with the type as the object).
- # Regular methods just show up as normal functions in
- # python 3 (no more unbound methods).
- is_class_method = inspect.ismethod(mth)
-
- # If this only gave us 1 line, it means there's no docs
- # (the one line is just the call signature).
- # In that case lets try parent classes to see if they
- # have docs.
- if len(mdocs_lines) == 1:
- mdocs_lines = self._handle_single_line_method_docs(
- cls, mdocs_lines, mth)
-
- # Add an empty line after the first.
- mdocs_lines = [mdocs_lines[0]] + [''] + mdocs_lines[1:]
- if len(mdocs_lines) == 2:
- # Special case: we allow dataclass types to have no __init__ docs
- # since they generate their own init (and their attributes tell
- # pretty much the whole story about them anyway).
- if (hasattr(cls, '__dataclass_fields__')
- and mth.__name__ == '__init__'):
- pass
- else:
- self._errors.append((cls, mth))
- mdocs = '\n'.join(mdocs_lines)
- return mdocs, is_class_method
-
- def _handle_single_line_method_docs(self, cls: type,
- mdocs_lines: list[str],
- mth: Callable) -> list[str]:
- for testclass in cls.mro()[1:]:
- testm = getattr(testclass, mth.__name__, None)
- if testm is not None:
- mdocs_lines_test = pydoc.plain(
- pydoc.render_doc(testm)).splitlines()[2:]
-
- # Split before "unbound method" or "method".
- if 'unbound' in mdocs_lines_test[0]:
- if len(mdocs_lines_test[0].split('unbound')) > 2:
- raise Exception('multi-unbounds')
- mdocs_lines_test[0] = \
- mdocs_lines_test[0].split('unbound')[0]
- else:
- if len(mdocs_lines_test[0].split('method')) > 2:
- raise Exception('multi-methods')
- mdocs_lines_test[0] = \
- mdocs_lines_test[0].split('method')[0]
-
- # If this one has more info in it but its
- # first line (call signature) matches ours,
- # go ahead and use its docs in place of ours.
- if (len(mdocs_lines_test) > 1
- and mdocs_lines_test[0] == mdocs_lines[0]):
- mdocs_lines = mdocs_lines_test
- return mdocs_lines
-
- def _create_index(self) -> None:
- # Create an index of everything we can link to in classes and
- # functions.
- for cls in self._classes:
- key = cls.name
- if key in self._index:
- print(f'WARNING: duplicate index entry: {key}')
- self._index[key] = (_get_class_href(cls.name), cls)
- self._index_keys.append(key)
-
- # Add in methods.
- for mth in cls.methods:
- key = cls.name + '.' + mth.name
- if key in self._index:
- print(f'WARNING: duplicate index entry: {key}')
- self._index[key] = (_get_method_href(cls.name, mth.name), mth)
- self._index_keys.append(key)
-
- # Add in attributes.
- for attr in cls.attributes:
- key = cls.name + '.' + attr.name
- if key in self._index:
- print(f'WARNING: duplicate index entry: {key}')
- self._index[key] = (_get_attribute_href(cls.name,
- attr.name), attr)
- self._index_keys.append(key)
-
- # Add in functions.
- for fnc in self._functions:
- key = fnc.name
- if key in self._index:
- print(f'WARNING: duplicate index entry: {key}')
- self._index[key] = (_get_function_href(fnc.name), fnc)
- self._index_keys.append(key)
-
- # Reverse this so when we replace things with links our longest
- # ones are searched first (such as nested classes like ba.Foo.Bar).
- self._index_keys.reverse()
-
- def _write_inherited_attrs(self, inherited_attrs: dict[str, str]) -> None:
- style = (' style="padding-left: 0px;"' if DO_STYLES else '')
- self._out += f'
Attributes Inherited:
\n'
- style = (' style="padding-left: 30px;"' if DO_STYLES else '')
- self._out += f''
- inherited_attrs_sorted = list(inherited_attrs.items())
- inherited_attrs_sorted.sort(key=lambda x: x[0].lower())
- for i, attr in enumerate(inherited_attrs_sorted):
- if i != 0:
- self._out += ', '
- aname = attr[0]
- self._out += ('' + aname + '')
- self._out += '
\n'
-
- def _write_attrs(self, cls: ClassInfo,
- attributes: list[AttributeInfo]) -> None:
- # Include a block of links to our attrs if we have more
- # than one.
- if len(attributes) > 1:
- self._out += f''
- for i, attr in enumerate(attributes):
- if i != 0:
- self._out += ', '
- aname = attr.name
- self._out += ('' +
- aname + '')
- self._out += '
\n'
-
- self._out += '\n'
- for attr in attributes:
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
- self._out += (f'- \n')
-
- # If we've got a type for the attr, spit that out.
- if attr.attr_type is not None:
- # Add links to any types we cover.
- typestr = self._add_index_links(attr.attr_type)
- style2 = (' style="color: #666677;"' if DO_STYLES else '')
- self._out += (f'
'
- f'' + typestr + '
\n')
- else:
- self._errors.append(f"Attr '{attr.name}' on {cls.name} "
- 'has no type annotation.')
-
- if attr.docs is not None:
- self._out += self._filter_docs(attr.docs, 'attribute')
- self._out += ' \n'
- self._out += '
\n'
-
- def _write_class_attrs_all(self, cls: ClassInfo,
- attributes: list[AttributeInfo],
- inherited_attrs: dict[str, str]) -> None:
- # If this class has no non-inherited attrs, just print a link to
- # the base class instead of repeating everything.
- # Nevermind for now; we never have many attrs so this isn't as
- # helpful as with methods.
- if bool(False):
- if cls.parents:
- self._out += f'Attributes:
\n'
- par = cls.parents[0]
- self._out += (f'<'
- 'all attributes inherited from ' + '' + par + '' +
- '>
\n')
- else:
- # Dump inherited attrs.
- if inherited_attrs:
- self._write_inherited_attrs(inherited_attrs)
-
- # Dump attributes.
- if attributes:
- if inherited_attrs:
- self._out += (f''
- 'Attributes Defined Here:
\n')
- else:
- self._out += f'Attributes:
\n'
-
- self._write_attrs(cls, attributes)
-
- def _write_enum_vals(self, cls: ClassInfo) -> None:
- if cls.enum_values is None:
- return
- self._out += f'Values:
\n'
- self._out += '\n'
- for val in cls.enum_values:
- self._out += '- ' + val + '
\n'
- self._out += '
\n'
-
- def _write_inherited_methods_for_class(self, cls: ClassInfo) -> None:
- """Dump inherited methods for a class."""
- if cls.inherited_methods:
- # If we inherit directly from a builtin class,
- # lets not print inherited methods at all since
- # we don't have docs for them.
- if (len(cls.parents) == 1
- and cls.parents[0].startswith('builtins.')):
- pass
- else:
- self._out += f'Methods Inherited:
\n'
- self._out += f''
- for i, method in enumerate(cls.inherited_methods):
- if i != 0:
- self._out += ', '
- mname = method.name + '()'
- if mname == '__init__()':
- mname = '<constructor>'
- assert method.method_class is not None
- self._out += (
- '' + mname + '')
- self._out += '
\n'
-
- def _write_methods_for_class(self, cls: ClassInfo,
- methods: list[FunctionInfo]) -> None:
- """Dump methods for a class."""
- if cls.methods:
- # Just say "methods" if we had no inherited ones.
- if cls.inherited_methods:
- self._out += (f''
- 'Methods Defined or Overridden:
\n')
- else:
- self._out += f'Methods:
\n'
-
- # Include a block of links to our methods if we have more
- # than one.
- if len(methods) > 1:
- self._out += f''
- for i, method in enumerate(methods):
- if i != 0:
- self._out += ', '
- mname = method.name + '()'
- if mname == '__init__()':
- mname = '<constructor>'
- self._out += ('' + mname + '')
- self._out += '
\n'
-
- self._out += '\n'
- for mth in cls.methods:
- self._write_method(cls, mth)
- self._out += '
\n'
-
- def _write_method(self, cls: ClassInfo, mth: FunctionInfo) -> None:
- name = mth.name + '()'
- ignore_links = []
- if name == '__init__()':
- ignore_links.append(cls.name)
- name = '<constructor>'
-
- # If we have a 3 part name such as
- # 'ba.Spaz.DeathMessage',
- # ignore the first 2 components ('ba.Spaz').
- dot_splits = cls.name.split('.')
- if len(dot_splits) == 3:
- ignore_links.append(dot_splits[0] + '.' + dot_splits[1])
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
-
- self._out += (f'\n')
- if mth.docs is not None:
- mdocslines = mth.docs.splitlines()
-
- # Hmm should we be pulling from the class docs
- # as python suggests?.. hiding the suggestion to do so.
- mdocslines = [
- l for l in mdocslines
- if 'Initialize self. See help(type(self))'
- ' for accurate signature' not in l
- ]
-
- # Kill any '-> None' on inits.
- if '__init__' in mdocslines[0]:
- mdocslines[0] = mdocslines[0].split(' -> ')[0]
-
- # Let's display '__init__(self, foo)' as 'Name(foo)'.
- mdocslines[0] = mdocslines[0].replace('__init__(self, ',
- cls.name + '(')
- mdocslines[0] = mdocslines[0].replace('__init__(self)',
- cls.name + '()')
-
- if mth.is_class_method:
- style2 = (' style="color: #CC6600;"' if DO_STYLES else '')
- self._out += (f''
- f''
- '<class method>'
- '
\n')
- self._out += self._filter_docs('\n'.join(mdocslines),
- 'method',
- ignore_links=ignore_links)
- self._out += '\n'
-
- def _get_type_display(self, name: str) -> str:
- """Given a string such as 'ba.ClassName', returns link/txt for it."""
- # Special-case; don't do links for built in classes.
- if name.startswith('builtins.'):
- shortname = name.replace('builtins.', '')
-
- # Show handy links for some builtin python types
- if shortname in {
- 'Exception', 'BaseException', 'RuntimeError', 'ValueError'
- }:
- return (f'{shortname}')
- if name.startswith('ba.'):
- return ('' + name +
- '')
-
- # Show handy links for various standard library types.
- if name in {'typing.Generic', 'typing.Protocol'}:
- return (f'{name}')
- if name in {'enum.Enum'}:
- return (f'{name}')
-
- return name
-
- def _write_class_inheritance(self, cls: ClassInfo) -> None:
- if cls.parents:
- self._out += f'Inherits from: '
- for i, par in enumerate(cls.parents):
-
- if i != 0:
- self._out += ', '
-
- self._out += self._get_type_display(par)
- else:
- self._out += (f'
'
- '<top level class>\n')
-
- def _get_inherited_attrs(self, cls: ClassInfo,
- attr_name_set: set[str]) -> dict[str, str]:
- inherited_attrs: dict[str, str] = {}
- for par in cls.parents:
- if par in self._index:
- parent_class = self._index[par][1]
- assert isinstance(parent_class, ClassInfo)
- for attr in parent_class.attributes:
- if (attr.name not in attr_name_set
- and attr.name not in inherited_attrs):
- inherited_attrs[attr.name] = (_get_attribute_href(
- parent_class.name, attr.name))
- self._out += '
\n'
- return inherited_attrs
-
- def _write_classes(self) -> None:
- # Now write out the docs for each class.
- for cls in self._classes:
- self._out += '
\n'
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
- self._out += (f'' + cls.name +
- '\n')
- self._write_class_inheritance(cls)
- methods = cls.methods
- methods.sort(key=lambda x: x.name.lower())
- attributes = cls.attributes
- attributes.sort(key=lambda x: x.name.lower())
-
- attr_name_set = set()
- for attr in attributes:
- attr_name_set.add(attr.name)
-
- # Go through parent classes' attributes, including any
- # that aren't in our set in the 'inherited attributes' list.
-
- inherited_attrs = self._get_inherited_attrs(cls, attr_name_set)
-
- if cls.docs is not None:
- self._out += self._filter_docs(cls.docs, 'class')
-
- self._write_class_attrs_all(cls, attributes, inherited_attrs)
-
- self._write_enum_vals(cls)
-
- # If this class has no non-inherited methods, just print a link to
- # the base class instead of repeating everything.
- if cls.inherited_methods and not cls.methods:
- if cls.parents:
- self._out += f'Methods:
\n'
- par = cls.parents[0]
- self._out += (f'
<'
- 'all methods inherited from ' +
- self._get_type_display(par) + '>
\n')
- else:
- self._write_inherited_methods_for_class(cls)
- self._write_methods_for_class(cls, methods)
-
- def _collect_submodules(self, module: ModuleType) -> None:
- # In case we somehow met one module twice.
- if module in self._submodules:
- return
-
- if module.__name__ in self.ignored_modules:
- return
-
- if not module.__name__.startswith(self.top_module_name):
- return
-
- self._submodules.add(module)
-
- names = dir(module)
- objects = [
- getattr(module, name) for name in names if not name.startswith('_')
- ]
- submodules = [
- obj for obj in objects if isinstance(obj, types.ModuleType)
- ]
- for submodule in submodules:
- self._collect_submodules(submodule)
-
- def _gather_funcs(self, module: ModuleType) -> None:
- func_types = [types.FunctionType, types.BuiltinMethodType]
-
- names = dir(module)
- objects = [
- getattr(module, name) for name in names if not name.startswith('_')
- ]
- funcs = [
- obj for obj in objects if any(
- isinstance(obj, t) for t in func_types)
- ]
- # Or should we take care of this in modules itself?..
- funcs = [
- f for f in funcs if f.__name__.startswith(self.top_module_name)
- ]
-
- for fnc in funcs:
- # For non-builtin funcs, use the pydoc rendering since it includes
- # args.
- # Chop off the first line which is just "Python Library
- # Documentation: and the second which is blank.
- docs = None
- if isinstance(fnc, types.FunctionType):
- docslines = pydoc.plain(pydoc.render_doc(fnc)).splitlines()[2:]
-
- # Pydoc indents all lines but the first 4 spaces; undo that.
- for i, line in enumerate(docslines):
- if i != 0:
- if not line.startswith(' '):
- raise Exception('UNEXPECTED')
- docslines[i] = line[4:]
-
- # Add an empty line after the first.
- docslines = [docslines[0]] + [''] + docslines[1:]
-
- if len(docslines) == 2:
- docslines.append('(no docs)')
- self._errors.append(fnc)
- docs = '\n'.join(docslines)
- else:
- if fnc.__doc__ is not None:
- docs = fnc.__doc__
- else:
- fnc.__doc__ = '(no docs)'
- self._errors.append(fnc)
- assert docs is not None
-
- f_info = FunctionInfo(name=fnc.__module__ + '.' + fnc.__name__,
- category=_get_category(
- docs, CategoryType.FUNCTION),
- docs=docs)
- if '(internal)' not in docs and f_info.name not in self._function_names:
- self._functions.append(f_info)
- self._function_names.add(f_info.name)
-
- def _process_classes(self, module: ModuleType) -> None:
- classes_by_name = _get_module_classes(module)
- for c_name, cls in classes_by_name:
- docs = self._get_base_docs_for_class(cls)
- bases = _get_bases(cls)
- methods, inherited_methods = self._get_methods_for_class(cls)
- enum_values = self._get_enum_values_for_class(cls)
- docs, attrs = self._get_all_attrs_for_class(cls, docs)
-
- c_info = ClassInfo(name=c_name,
- parents=bases,
- docs=docs,
- enum_values=enum_values,
- methods=methods,
- inherited_methods=inherited_methods,
- category=_get_category(docs,
- CategoryType.CLASS),
- attributes=attrs)
- if c_info.name not in self._class_names:
- self._classes.append(c_info)
- self._class_names.add(c_info.name)
-
- def _write_category_list(self) -> None:
- for cname, ctype, cmembers in self._merged_categories:
- if ctype == 'class':
- assert (isinstance(i, ClassInfo) for i in cmembers)
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
- self._out += (f'\n')
- classes_sorted = cast(list[ClassInfo], cmembers)
- classes_sorted.sort(key=lambda x: x.name.lower())
- pcc = _print_child_classes(classes_sorted, '', 0)
- self._out += pcc
- elif ctype == 'function':
- assert (isinstance(i, FunctionInfo) for i in cmembers)
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
- self._out += (f'\n'
- '\n')
- funcs = cast(list[FunctionInfo], cmembers)
- funcs.sort(key=lambda x: x.name.lower())
- for fnc in funcs:
- self._out += (' - ' +
- fnc.name + '()
\n')
- self._out += '
\n'
- else:
- raise Exception('invalid ctype')
-
- def _filter_docs(self,
- docs: str,
- filter_type: str,
- ignore_links: list[str] = None) -> str:
- get_category_href_func, indent = _filter_type_settings(filter_type)
- docs = docs.replace('>', '>')
- docs = docs.replace('<', '<')
-
- docs_lines = docs.splitlines()
-
- # Make sure empty lines are actually empty (so we can search for
- # '\n\n' and not get thrown off by something like '\n \n').
- for i, line in enumerate(docs_lines):
- if line.strip() == '':
- docs_lines[i] = ''
-
- # If a line starts with 'Category:', make it a link to that category.
- for i, line in enumerate(docs_lines):
- if line.lower().strip().startswith(CATEGORY_STRING.lower()):
- if get_category_href_func is None:
- raise Exception('cant do category for filter_type ' +
- filter_type)
- cat = line.strip()[len(CATEGORY_STRING):].strip()
- docs_lines[i] = (CATEGORY_STRING + ' ' + cat +
- '')
- docs = '\n'.join(docs_lines)
- docs = _split_into_paragraphs(docs, filter_type, indent)
- docs = self._add_index_links(docs, ignore_links)
- return docs
-
- def get_top_module(self) -> ModuleType:
- """Returns top-level module we generating docs for"""
- return importlib.import_module(self.top_module_name)
-
- def run(self, outfilename: str) -> None:
- """Generate docs from within the game."""
- # pylint: disable=too-many-locals
-
- import ba
-
- print(f"Generating docs file: '{Clr.BLU}{outfilename}{Clr.RST}'...")
-
- # Collect everything we want to generate docs for.
- self._collect_submodules(self.get_top_module())
- submodules = list(sorted(self._submodules, key=lambda x: x.__name__))
- print(f'{self.top_module_name} submodules:\n -->',
- '\n --> '.join([sm.__name__ for sm in submodules]))
- for module in submodules:
- self._gather_funcs(module)
- self._process_classes(module)
-
- # Remove duplicates. This probably should be handled at
- # self._gather_funcs/self._process_classes level (i.e. just not to add
- # already existing things), but the main problem is unhashable
- # ClassInfo dataclass which leads to O(N^2) asymptotic (though I didn't
- # check how exactly slow it would be).
- # funcs = self._functions
- # funcs.sort(key=lambda x: x.name)
- # f_last: Optional[FunctionInfo] = None
- # self._functions = []
- # for func in funcs:
- # if func.name == f_last.name:
- # continue
- # f_last = func
- # self._functions.append(func)
-
- self._functions.sort(key=lambda x: x.name)
- self._classes.sort(key=lambda x: x.name)
-
- # Start with our list of classes and functions.
- app = ba.app
- self._out += ('last updated on ' + str(datetime.date.today()) +
- ' for Ballistica version ' + app.version + ' build ' +
- str(app.build_number) + '
\n')
- self._out += self.header
- self._out += '
\n'
- self._out += 'Table of Contents
\n'
-
- self._create_index()
-
- # Build a sorted list of class categories.
- c_categories: dict[str, list[Union[ClassInfo, FunctionInfo]]] = {}
- self._classes.sort(key=lambda x: x.name.lower())
- for cls in self._classes:
- assert cls.category is not None
- category = cls.category
- if category not in c_categories:
- c_categories[category] = []
- c_categories[category].append(cls)
-
- self._merged_categories = [(cname, 'class', cval)
- for cname, cval in c_categories.items()]
-
- # Build sorted function category list.
- f_categories: dict[str, list[FunctionInfo]] = {}
- for fnc in self._functions:
- if fnc.category is not None:
- category = fnc.category
- else:
- category = 'Misc'
- if category not in f_categories:
- f_categories[category] = []
- f_categories[category].append(fnc)
-
- self._merged_categories += [(cname, 'function',
- cast(list[Union[ClassInfo, FunctionInfo]],
- cval))
- for cname, cval in f_categories.items()]
-
- def sort_func(entry: tuple[str, str, Any]) -> str:
- name = entry[0].lower()
-
- # Sort a few recognized categories somewhat manually.
- overrides = {
- 'gameplay classes': 'aaaa',
- 'gameplay functions': 'aaab',
- 'general utility classes': 'aaac',
- 'general utility functions': 'aaad',
- 'asset classes': 'aaae',
- 'asset functions': 'aaaf',
- 'message classes': 'aaag',
- 'app classes': 'aaah',
- 'app functions': 'aaai',
- 'user interface classes': 'aaaj',
- 'user interface functions': 'aaak',
- }
- return overrides.get(name, name)
-
- self._merged_categories.sort(key=sort_func)
-
- # Write out our category listings.
- self._write_category_list()
- self._write_classes()
-
- # Now write docs for each function.
- for fnc in self._functions:
- self._out += '
\n'
- cssclass = ' class="offsanchor"' if DO_CSS_CLASSES else ''
- self._out += (f'' + fnc.name +
- '()\n')
- if fnc.docs is not None:
- self._out += self._filter_docs(fnc.docs, 'function')
-
- # If we've hit any errors along the way, complain.
- if self._errors:
- max_displayed = 10
- print(
- len(self._errors), 'ISSUES FOUND GENERATING DOCS:\n' +
- '\n'.join(self._errors[:max_displayed]))
- clipped = max(0, len(self._errors) - max_displayed)
- if clipped:
- print(f'(and {clipped} more)')
- raise Exception(
- str(len(self._errors)) + ' docs generation issues.')
-
- with open(outfilename, 'w', encoding='utf-8') as outfile:
- outfile.write(self._out)
-
- print(f"Generated docs file: '{Clr.BLU}{outfilename}{Clr.RST}'.")
-
-
-class BaModuleGenerator(BaseGenerator):
- """Generates docs for 'ba' module."""
-
- top_module_name = 'ba'
- ignored_modules = ['ba.internal', 'ba.ui']
- header = ('
This page documents the Python classes'
- ' and functions in the \'ba\' module,\n'
- ' which are the ones most relevant to modding in Ballistica.'
- ' If you come across something you feel'
- ' should be included here or could'
- ' be better explained, please '
- ''
- 'let me know. Happy modding!
\n')
-
- def _add_index_links(self,
- docs: str,
- ignore_links: Optional[list[str]] = None) -> str:
- docs = super()._add_index_links(docs, ignore_links=ignore_links)
-
- # Misc replacements:
- docs = docs.replace(
- 'General message handling; can be passed any message object.',
- 'General message handling; can be passed any message object.')
-
- return docs
-
-
-class BastdModuleGenerator(BaseGenerator):
- """Generates docs for 'bastd' module and all submodules."""
-
- top_module_name = 'bastd'
- # Mypy complains if there is no type annotation
- # (though it is in base class).
- ignored_modules: list[str] = []
- header = ('This page documents the Python classes'
- ' and functions in the \'bastd\' module,\n'
- ' which are also often needed to modding.'
- ' If you come across something you feel'
- ' should be included here or could'
- ' be better explained, please '
- ''
- 'let me know. Happy modding!
\n')
-
-
def generate(projroot: str) -> None:
"""Main entry point."""
- toolsdir = os.path.abspath(os.path.join(projroot, 'tools'))
+ import pdoc
# Make sure we're running from the dir above this script.
os.chdir(projroot)
- ba_out_file_name = os.path.abspath('build/docs_ba.html')
- bastd_out_file_name = os.path.abspath('build/docs_bastd.html')
-
- # Let's build the cmake version; no sandboxing issues to contend
- # with there. Also going with the headless build; will need to revisit
- # if there's ever any functionality not available in that build.
- subprocess.run(['make', 'cmake-server-build'], check=True)
-
- # Launch ballisticacore and exec ourself from within it.
- print('Launching ballisticacore to generate docs...')
+ pythondir = str(
+ Path(projroot, 'assets', 'src', 'ba_data', 'python').absolute())
+ sys.path.append(pythondir)
+ outdirname = Path('build', 'docs_html').absolute()
try:
- subprocess.run(
- [
- './ballisticacore',
- '-exec',
- f'try:\n'
- f' import sys\n'
- f' import ba\n'
- f' sys.path.append("{toolsdir}")\n'
- f' import batools.docs\n'
- f' batools.docs.BaModuleGenerator().run("{ba_out_file_name}")\n'
- f' batools.docs.BastdModuleGenerator().run("{bastd_out_file_name}")\n'
- f' ba.quit()\n'
- f'except Exception:\n'
- f' import sys\n'
- f' import traceback\n'
- f' print("ERROR GENERATING DOCS")\n'
- f' traceback.print_exc()\n'
- f' sys.exit(255)\n',
- ],
- cwd='build/cmake/server-debug/dist',
- check=True,
- )
- except Exception as exc2:
- # Keep our error simple here; we want focus to be on what went
- # wrong withing BallisticaCore.
- raise CleanError('BallisticaCore docs generation failed.') from exc2
+ pdoc.pdoc('ba', 'bastd', output_directory=outdirname)
+ except Exception as exc:
+ import traceback
+ traceback.print_exc()
+ raise CleanError('Docs generation failed') from exc
- print('Docs generation complete.')
+ print(f'{Clr.GRN}Docs generation complete.{Clr.RST}')