Skip to content

Instantly share code, notes, and snippets.

@evnchn
Created November 22, 2025 12:52
Show Gist options
  • Select an option

  • Save evnchn/35b4e68ef3e42776bc69c2578e16d2aa to your computer and use it in GitHub Desktop.

Select an option

Save evnchn/35b4e68ef3e42776bc69c2578e16d2aa to your computer and use it in GitHub Desktop.
Tabbed interface for NiceGUI documentation (works but need UI work)
from __future__ import annotations
import inspect
from typing import Callable
from nicegui import binding, ui
from nicegui.elements.markdown import remove_indentation
from ..style import create_anchor_name, subheading
from .custom_restructured_text import CustomRestructuredText as custom_restructured_text
def generate_class_doc(class_obj: type, part_title: str) -> None:
"""Generate documentation for a class including all its methods and properties."""
doc = class_obj.__doc__ or class_obj.__init__.__doc__
if doc and ':param' in doc:
subheading('Initializer', anchor_name=create_anchor_name(part_title.replace('Reference', 'Initializer')))
description = remove_indentation(doc.split('\n', 1)[-1])
lines = [line.replace(':param ', ':') for line in description.splitlines() if ':param' in line]
custom_restructured_text('\n'.join(lines)).classes('bold-links arrow-links rst-param-tables')
mro = [base for base in class_obj.__mro__ if base.__module__.startswith('nicegui.')]
ancestors = mro[1:]
attributes: dict[str, tuple[type, object | None]] = {}
for base in reversed(mro):
for name in dir(base):
if not name.startswith('_') and _is_method_or_property(base, name):
attributes[name] = (base, getattr(base, name, None))
properties = {name: (ancestor, attribute) for name, (ancestor, attribute)
in attributes.items() if not callable(attribute)}
methods = {name: (ancestor, attribute) for name, (ancestor, attribute) in attributes.items() if callable(attribute)}
def render_section(items: dict[str, tuple[type, object | None]], is_method: bool) -> None:
sorted_items = sorted(items.items())
native = [(n, o, a) for n, (o, a) in sorted_items if o is class_obj]
inherited_items = [(n, o, a) for n, (o, a) in sorted_items if o is not class_obj]
# Group inherited items by owner class
inherited_by_owner = {}
for name, owner, attr in inherited_items:
if owner not in inherited_by_owner:
inherited_by_owner[owner] = []
inherited_by_owner[owner].append((name, attr))
def render_item(name: str, owner: type, attr: object | None) -> None:
if is_method:
decorator = ''
owner_attr = owner.__dict__.get(name)
if isinstance(owner_attr, staticmethod):
decorator += '`@staticmethod`<br />'
if isinstance(owner_attr, classmethod):
decorator += '`@classmethod`<br />'
ui.markdown(f'{decorator}**`{name}`**`{_generate_method_signature_description(attr)}`') \
.classes('w-full overflow-x-auto')
else:
ui.markdown(f'**`{name}`**`{_generate_property_signature_description(attr)}`')
docstring = getattr(attr, '__doc__', None)
if docstring:
_render_docstring(docstring).classes('ml-8')
def render_items_in_tab(item_list: list[tuple[str, type, object | None]]) -> None:
with ui.column().classes('gap-2 w-full overflow-x-auto'):
for name, owner, attr in item_list:
render_item(name, owner, attr)
if native or inherited_by_owner:
def get_inheritance_level(cls: type) -> int:
if cls is class_obj:
return 0
return class_obj.__mro__.index(cls)
sorted_owners = sorted(inherited_by_owner.keys(),
key=lambda x: (get_inheritance_level(x), x.__name__))
tab_names = ['Non-inherited'] + [owner.__name__ for owner in sorted_owners]
with ui.tabs().classes('w-full font-mono').props('no-caps') as tabs_container:
tabs = [ui.tab(name) for name in tab_names]
with ui.tab_panels(tabs_container, value=tabs[0]).classes('w-full bg-transparent'):
with ui.tab_panel(tabs[0]):
if native:
render_items_in_tab(native)
else:
item_type = 'methods' if is_method else 'properties'
ui.markdown(
f'No direct {item_type} defined here. Check the inherited tabs for available functionality.')
for i, owner in enumerate(sorted_owners, 1):
with ui.tab_panel(tabs[i]):
render_items_in_tab([(name, owner, attr) for name, attr in inherited_by_owner[owner]])
if properties:
subheading('Properties', anchor_name=create_anchor_name(part_title.replace('Reference', 'Properties')))
with ui.column().classes('gap-2 w-full overflow-x-auto'):
render_section(properties, is_method=False)
if methods:
subheading('Methods', anchor_name=create_anchor_name(part_title.replace('Reference', 'Methods')))
with ui.column().classes('gap-2 w-full overflow-x-auto'):
render_section(methods, is_method=True)
if ancestors:
subheading('Inheritance', anchor_name=create_anchor_name(part_title.replace('Reference', 'Inheritance')))
ui.markdown('\n'.join(f'- `{ancestor.__name__}`' for ancestor in ancestors))
def _is_method_or_property(cls: type, attribute_name: str) -> bool:
attribute = cls.__dict__.get(attribute_name, None)
return (
inspect.isfunction(attribute) or
inspect.ismethod(attribute) or
isinstance(attribute, (
staticmethod,
classmethod,
property,
binding.BindableProperty,
))
)
def _generate_property_signature_description(property_: property | None) -> str:
description = ''
if property_ is None:
return ': BindableProperty'
if property_.fget:
return_annotation = inspect.signature(property_.fget).return_annotation
if return_annotation != inspect.Parameter.empty:
return_type = inspect.formatannotation(return_annotation)
description += f': {return_type}'
if property_.fset:
description += ' (settable)'
if property_.fdel:
description += ' (deletable)'
return description
def _generate_method_signature_description(method: Callable) -> str:
param_strings = []
for param in inspect.signature(method).parameters.values():
param_string = param.name
if param_string == 'self':
continue
if param.annotation != inspect.Parameter.empty:
param_type = inspect.formatannotation(param.annotation)
param_string += f''': {param_type.strip("'")}'''
if param.default != inspect.Parameter.empty:
param_string += ' = [...]' if callable(param.default) else f' = {param.default!r}'
if param.kind == inspect.Parameter.VAR_POSITIONAL:
param_string = f'*{param_string}'
param_strings.append(param_string)
method_signature = ', '.join(param_strings)
description = f'({method_signature})'
return_annotation = inspect.signature(method).return_annotation
if return_annotation != inspect.Parameter.empty:
return_type = inspect.formatannotation(return_annotation)
description += f''' -> {return_type.strip("'").replace("typing_extensions.", "").replace("typing.", "")}'''
return description
def _render_docstring(doc: str) -> custom_restructured_text:
doc = _remove_indentation_from_docstring(doc)
return custom_restructured_text(doc).classes('bold-links arrow-links rst-param-tables')
def _remove_indentation_from_docstring(text: str) -> str:
lines = text.splitlines()
if not lines:
return ''
if len(lines) == 1:
return lines[0]
indentation = min(len(line) - len(line.lstrip()) for line in lines[1:] if line.strip())
return lines[0] + '\n' + '\n'.join(line[indentation:] for line in lines[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment