Created
November 22, 2025 12:52
-
-
Save evnchn/35b4e68ef3e42776bc69c2578e16d2aa to your computer and use it in GitHub Desktop.
Tabbed interface for NiceGUI documentation (works but need UI work)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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