api.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. from __future__ import annotations
  2. import inspect
  3. import sys
  4. import types
  5. from copy import deepcopy
  6. from pathlib import Path
  7. from types import ModuleType
  8. from typing import Any, Callable, Dict, Optional, Union, overload
  9. import nicegui
  10. from nicegui import app as nicegui_app
  11. from nicegui import ui as nicegui_ui
  12. from nicegui.functions.navigate import Navigate
  13. from nicegui.elements.markdown import remove_indentation
  14. from .page import DocumentationPage
  15. from .part import Demo, DocumentationPart
  16. registry: Dict[str, DocumentationPage] = {}
  17. redirects: Dict[str, str] = {}
  18. def get_page(documentation: ModuleType) -> DocumentationPage:
  19. """Return the documentation page for the given documentation module."""
  20. target_name = _removesuffix(documentation.__name__.split('.')[-1], '_documentation')
  21. assert target_name in registry, f'Documentation page {target_name} does not exist'
  22. return registry[target_name]
  23. def _get_current_page() -> DocumentationPage:
  24. frame = sys._getframe(2) # pylint: disable=protected-access
  25. module = inspect.getmodule(frame)
  26. assert module is not None and module.__file__ is not None
  27. name = _removesuffix(Path(module.__file__).stem, '_documentation')
  28. if name == 'overview':
  29. name = ''
  30. if name not in registry:
  31. registry[name] = DocumentationPage(name=name)
  32. return registry[name]
  33. def title(title_: Optional[str] = None, subtitle: Optional[str] = None) -> None:
  34. """Set the title of the current documentation page."""
  35. page = _get_current_page()
  36. page.title = title_
  37. page.subtitle = subtitle
  38. def text(title_: str, description: str) -> None:
  39. """Add a text block to the current documentation page."""
  40. _get_current_page().parts.append(DocumentationPart(title=title_, description=description))
  41. @overload
  42. def demo(title_: str,
  43. description: str, /, *,
  44. tab: Optional[Union[str, Callable]] = None,
  45. lazy: bool = True,
  46. ) -> Callable[[Callable], Callable]:
  47. ...
  48. @overload
  49. def demo(element: type, /,
  50. tab: Optional[Union[str, Callable]] = None,
  51. lazy: bool = True,
  52. ) -> Callable[[Callable], Callable]:
  53. ...
  54. @overload
  55. def demo(function: Union[Callable, Navigate], /,
  56. tab: Optional[Union[str, Callable]] = None,
  57. lazy: bool = True,
  58. ) -> Callable[[Callable], Callable]:
  59. ...
  60. def demo(*args, **kwargs) -> Callable[[Callable], Callable]:
  61. """Add a demo section to the current documentation page."""
  62. if len(args) == 2:
  63. element = None
  64. title_, description = args
  65. is_markdown = True
  66. else:
  67. element = args[0]
  68. doc = element.__doc__
  69. if isinstance(element, type) and not doc:
  70. doc = element.__init__.__doc__ # type: ignore
  71. title_, description = doc.split('\n', 1)
  72. title_ = title_.rstrip('.')
  73. is_markdown = False
  74. description = remove_indentation(description)
  75. page = _get_current_page()
  76. def decorator(function: Callable) -> Callable:
  77. if not page.parts and element:
  78. name = getattr(element, '__name__', None) or element.__class__.__name__
  79. ui_name = _find_attribute(nicegui_ui, name)
  80. app_name = _find_attribute(nicegui_app, name)
  81. if ui_name:
  82. page.title = f'ui.*{ui_name}*'
  83. elif app_name:
  84. page.title = f'app.*{app_name}*'
  85. page.parts.append(DocumentationPart(
  86. title=title_,
  87. description=description,
  88. description_format='md' if is_markdown else 'rst',
  89. demo=Demo(function=function, lazy=kwargs.get('lazy', True), tab=kwargs.get('tab')),
  90. ))
  91. return function
  92. return decorator
  93. def part(title_: str) -> Callable:
  94. """Add a custom part with arbitrary UI and descriptive markdown elements to the current documentation page.
  95. The content of any contained markdown elements will be used for search indexing.
  96. """
  97. page = _get_current_page()
  98. def decorator(function: Callable) -> Callable:
  99. with nicegui_ui.element() as container:
  100. function()
  101. elements = nicegui.ElementFilter(kind=nicegui.ui.markdown, local_scope=True)
  102. description = ''.join(e.content for e in elements if '```' not in e.content)
  103. container.delete()
  104. page.parts.append(DocumentationPart(title=title_, search_text=description, ui=function))
  105. return function
  106. return decorator
  107. def ui(function: Callable) -> Callable:
  108. """Add arbitrary UI to the current documentation page."""
  109. _get_current_page().parts.append(DocumentationPart(ui=function))
  110. return function
  111. def intro(documentation: types.ModuleType) -> None:
  112. """Add an intro section to the current documentation page."""
  113. current_page = _get_current_page()
  114. target_page = get_page(documentation)
  115. target_page.back_link = current_page.name
  116. part = deepcopy(target_page.parts[0])
  117. part.link = target_page.name
  118. current_page.parts.append(part)
  119. def reference(element: type, *,
  120. title: str = 'Reference', # pylint: disable=redefined-outer-name
  121. ) -> None:
  122. """Add a reference section to the current documentation page."""
  123. _get_current_page().parts.append(DocumentationPart(title=title, reference=element))
  124. def extra_column(function: Callable) -> Callable:
  125. """Add an extra column to the current documentation page."""
  126. _get_current_page().extra_column = function
  127. return function
  128. def _find_attribute(obj: Any, name: str) -> Optional[str]:
  129. for attr in dir(obj):
  130. if attr.lower().replace('_', '') == name.lower().replace('_', ''):
  131. return attr
  132. return None
  133. def _removesuffix(string: str, suffix: str) -> str:
  134. # NOTE: Remove this once we drop Python 3.8 support
  135. if string.endswith(suffix):
  136. return string[:-len(suffix)]
  137. return string