tools.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import importlib
  2. import inspect
  3. import re
  4. from typing import Callable, Optional, Union
  5. import docutils.core
  6. from nicegui import context, ui
  7. from nicegui.binding import BindableProperty
  8. from nicegui.elements.markdown import apply_tailwind, remove_indentation
  9. from .demo import demo
  10. SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
  11. def pascal_to_snake(name: str) -> str:
  12. return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
  13. def create_anchor_name(text: str) -> str:
  14. return SPECIAL_CHARACTERS.sub('_', text).lower()
  15. def get_menu() -> ui.left_drawer:
  16. return [element for element in context.get_client().elements.values() if isinstance(element, ui.left_drawer)][0]
  17. def heading(text: str, *, make_menu_entry: bool = True) -> None:
  18. ui.link_target(create_anchor_name(text))
  19. ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
  20. if make_menu_entry:
  21. with get_menu():
  22. ui.label(text).classes('font-bold mt-4')
  23. def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[str] = None) -> None:
  24. name = create_anchor_name(text)
  25. ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
  26. with ui.row().classes('gap-2 items-center relative'):
  27. if more_link:
  28. ui.link(text, f'documentation/{more_link}').classes('text-2xl')
  29. else:
  30. ui.label(text).classes('text-2xl')
  31. with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
  32. ui.icon('link', size='sm').classes('opacity-10 hover:opacity-80')
  33. if make_menu_entry:
  34. with get_menu() as menu:
  35. async def click():
  36. if await ui.run_javascript('!!document.querySelector("div.q-drawer__backdrop")', timeout=5.0):
  37. menu.hide()
  38. ui.open(f'#{name}')
  39. ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click, [])
  40. def render_docstring(doc: str, with_params: bool = True) -> ui.html:
  41. doc = remove_indentation(doc)
  42. doc = doc.replace('param ', '')
  43. html = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
  44. html = apply_tailwind(html)
  45. if not with_params:
  46. html = re.sub(r'<dl class=".* simple">.*?</dl>', '', html, flags=re.DOTALL)
  47. return ui.html(html).classes('documentation bold-links arrow-links')
  48. class text_demo:
  49. def __init__(self, title: str, explanation: str, *,
  50. tab: Optional[Union[str, Callable]] = None,
  51. more_link: Optional[str] = None,
  52. make_menu_entry: bool = True
  53. ) -> None:
  54. self.title = title
  55. self.explanation = explanation
  56. self.make_menu_entry = make_menu_entry
  57. self.tab = tab
  58. self.more_link = more_link
  59. def __call__(self, f: Callable) -> Callable:
  60. subheading(self.title, make_menu_entry=self.make_menu_entry, more_link=self.more_link)
  61. ui.markdown(self.explanation).classes('bold-links arrow-links')
  62. f.tab = self.tab
  63. return demo(f)
  64. class section_intro_demo(text_demo):
  65. def __init__(self, name: str, title: str, explanation: str) -> None:
  66. super().__init__(title, explanation, more_link=f'section_{name}', make_menu_entry=False)
  67. self.name = name
  68. with get_menu():
  69. ui.link(title, f'/documentation/section_{name}')
  70. def __call__(self, f: Callable) -> Callable:
  71. result = super().__call__(f)
  72. ui.markdown(f'[Read more...](/documentation/section_{self.name})').classes('bold-links arrow-links')
  73. return result
  74. class main_page_demo(text_demo):
  75. def __init__(self, title: str, explanation: str) -> None:
  76. super().__init__(title, explanation, make_menu_entry=False)
  77. class element_demo:
  78. def __init__(self, element_class: Union[Callable, type, str]) -> None:
  79. if isinstance(element_class, str):
  80. module = importlib.import_module(f'website.documentation.more.{element_class}_documentation')
  81. element_class = getattr(module, 'main_demo')
  82. self.element_class = element_class
  83. def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
  84. doc = f.__doc__ or self.element_class.__doc__ or self.element_class.__init__.__doc__
  85. title, documentation = doc.split('\n', 1)
  86. with ui.column().classes('w-full mb-8 gap-2'):
  87. if more_link:
  88. subheading(title, more_link=more_link)
  89. render_docstring(documentation, with_params=more_link is None)
  90. result = demo(f)
  91. if more_link:
  92. ui.markdown(f'See [more...](documentation/{more_link})').classes('bold-links arrow-links')
  93. return result
  94. def load_demo(api: Union[type, Callable, str]) -> None:
  95. name = api if isinstance(api, str) else pascal_to_snake(api.__name__)
  96. try:
  97. module = importlib.import_module(f'website.documentation.more.{name}_documentation')
  98. except ModuleNotFoundError:
  99. module = importlib.import_module(f'website.documentation.more.{name.replace("_", "")}_documentation')
  100. element_demo(api)(getattr(module, 'main_demo'), more_link=name)
  101. def is_method_or_property(cls: type, attribute_name: str) -> bool:
  102. attribute = cls.__dict__.get(attribute_name, None)
  103. return (
  104. inspect.isfunction(attribute) or
  105. inspect.ismethod(attribute) or
  106. isinstance(attribute, property) or
  107. isinstance(attribute, BindableProperty)
  108. )
  109. def generate_class_doc(class_obj: type) -> None:
  110. mro = [base for base in class_obj.__mro__ if base.__module__.startswith('nicegui.')]
  111. ancestors = mro[1:]
  112. attributes = {}
  113. for base in reversed(mro):
  114. for name in dir(base):
  115. if not name.startswith('_') and is_method_or_property(base, name):
  116. attributes[name] = getattr(base, name, None)
  117. properties = {name: attribute for name, attribute in attributes.items() if not callable(attribute)}
  118. methods = {name: attribute for name, attribute in attributes.items() if callable(attribute)}
  119. if properties:
  120. subheading('Properties')
  121. with ui.column().classes('gap-2'):
  122. for name, property in sorted(properties.items()):
  123. ui.markdown(f'**`{name}`**`{generate_property_signature_description(property)}`')
  124. if property.__doc__:
  125. render_docstring(property.__doc__).classes('ml-8')
  126. if methods:
  127. subheading('Methods')
  128. with ui.column().classes('gap-2'):
  129. for name, method in sorted(methods.items()):
  130. ui.markdown(f'**`{name}`**`{generate_method_signature_description(method)}`')
  131. if method.__doc__:
  132. render_docstring(method.__doc__).classes('ml-8')
  133. if ancestors:
  134. subheading('Inherited from')
  135. with ui.column().classes('gap-2'):
  136. for ancestor in ancestors:
  137. ui.markdown(f'- `{ancestor.__name__}`')
  138. def generate_method_signature_description(method: Callable) -> str:
  139. param_strings = []
  140. for param in inspect.signature(method).parameters.values():
  141. param_string = param.name
  142. if param_string == 'self':
  143. continue
  144. if param.annotation != inspect.Parameter.empty:
  145. param_type = inspect.formatannotation(param.annotation)
  146. param_string += f''': {param_type.strip("'")}'''
  147. if param.default != inspect.Parameter.empty:
  148. param_string += f' = [...]' if callable(param.default) else f' = {repr(param.default)}'
  149. if param.kind == inspect.Parameter.VAR_POSITIONAL:
  150. param_string = f'*{param_string}'
  151. param_strings.append(param_string)
  152. method_signature = ', '.join(param_strings)
  153. description = f'({method_signature})'
  154. return_annotation = inspect.signature(method).return_annotation
  155. if return_annotation != inspect.Parameter.empty:
  156. return_type = inspect.formatannotation(return_annotation)
  157. description += f''' -> {return_type.strip("'").replace("typing_extensions.", "").replace("typing.", "")}'''
  158. return description
  159. def generate_property_signature_description(property: Optional[property]) -> str:
  160. description = ''
  161. if property is None:
  162. return ': BindableProperty'
  163. if property.fget:
  164. return_annotation = inspect.signature(property.fget).return_annotation
  165. if return_annotation != inspect.Parameter.empty:
  166. return_type = inspect.formatannotation(return_annotation)
  167. description += f': {return_type}'
  168. if property.fset:
  169. description += ' (settable)'
  170. if property.fdel:
  171. description += ' (deletable)'
  172. return description