refreshable.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. from dataclasses import dataclass
  2. from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
  3. from typing_extensions import Self
  4. from .. import background_tasks, globals
  5. from ..element import Element
  6. from ..helpers import KWONLY_SLOTS, is_coroutine_function
  7. @dataclass(**KWONLY_SLOTS)
  8. class RefreshableTarget:
  9. container: Element
  10. instance: Any
  11. args: Tuple[Any, ...]
  12. kwargs: Dict[str, Any]
  13. def run(self, func: Callable[..., Any]) -> Union[None, Awaitable]:
  14. if is_coroutine_function(func):
  15. async def wait_for_result() -> None:
  16. with self.container:
  17. if self.instance is None:
  18. await func(*self.args, **self.kwargs)
  19. else:
  20. await func(self.instance, *self.args, **self.kwargs)
  21. return wait_for_result()
  22. else:
  23. with self.container:
  24. if self.instance is None:
  25. func(*self.args, **self.kwargs)
  26. else:
  27. func(self.instance, *self.args, **self.kwargs)
  28. return None # required by mypy
  29. class RefreshableContainer(Element, component='refreshable.js'):
  30. pass
  31. class refreshable:
  32. def __init__(self, func: Callable[..., Any]) -> None:
  33. """Refreshable UI functions
  34. The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
  35. This method will automatically delete all elements created by the function and recreate them.
  36. """
  37. self.func = func
  38. self.instance = None
  39. self.targets: List[RefreshableTarget] = []
  40. def __get__(self, instance, _) -> Self:
  41. self.instance = instance
  42. return self
  43. def __getattribute__(self, __name: str) -> Any:
  44. attribute = object.__getattribute__(self, __name)
  45. if __name == 'refresh':
  46. def refresh(*args: Any, _instance=self.instance, **kwargs: Any) -> None:
  47. self.instance = _instance
  48. attribute(*args, **kwargs)
  49. return refresh
  50. return attribute
  51. def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
  52. self.prune()
  53. target = RefreshableTarget(container=RefreshableContainer(), instance=self.instance, args=args, kwargs=kwargs)
  54. self.targets.append(target)
  55. return target.run(self.func)
  56. def refresh(self, *args: Any, **kwargs: Any) -> None:
  57. self.prune()
  58. for target in self.targets:
  59. if target.instance != self.instance:
  60. continue
  61. target.container.clear()
  62. target.args = args or target.args
  63. target.kwargs.update(kwargs)
  64. try:
  65. result = target.run(self.func)
  66. except TypeError as e:
  67. if 'got multiple values for argument' in str(e):
  68. function = str(e).split()[0]
  69. parameter = str(e).split()[-1]
  70. raise Exception(f'{parameter} needs to be consistently passed to {function} '
  71. 'either as positional or as keyword argument') from e
  72. raise
  73. if is_coroutine_function(self.func):
  74. assert result is not None
  75. if globals.loop and globals.loop.is_running():
  76. background_tasks.create(result)
  77. else:
  78. globals.app.on_startup(result)
  79. def prune(self) -> None:
  80. self.targets = [
  81. target
  82. for target in self.targets
  83. if target.container.client.id in globals.clients and target.container.id in target.container.client.elements
  84. ]