1
0

refreshable.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. from __future__ import annotations
  2. from dataclasses import dataclass, field
  3. from typing import Any, Awaitable, Callable, ClassVar, Dict, Generic, List, Optional, Tuple, TypeVar, Union, cast
  4. from typing_extensions import Concatenate, ParamSpec, Self
  5. from .. import background_tasks, core
  6. from ..client import Client
  7. from ..dataclasses import KWONLY_SLOTS
  8. from ..element import Element
  9. from ..helpers import is_coroutine_function
  10. _S = TypeVar('_S')
  11. _T = TypeVar('_T')
  12. _P = ParamSpec('_P')
  13. @dataclass(**KWONLY_SLOTS)
  14. class RefreshableTarget:
  15. container: RefreshableContainer
  16. refreshable: refreshable
  17. instance: Any
  18. args: Tuple[Any, ...]
  19. kwargs: Dict[str, Any]
  20. current_target: ClassVar[Optional[RefreshableTarget]] = None
  21. locals: List[Any] = field(default_factory=list)
  22. next_index: int = 0
  23. def run(self, func: Callable[..., Union[_T, Awaitable[_T]]]) -> Union[_T, Awaitable[_T]]:
  24. """Run the function and return the result."""
  25. RefreshableTarget.current_target = self
  26. self.next_index = 0
  27. # pylint: disable=no-else-return
  28. if is_coroutine_function(func):
  29. async def wait_for_result() -> Any:
  30. with self.container:
  31. if self.instance is None:
  32. result = func(*self.args, **self.kwargs)
  33. else:
  34. result = func(self.instance, *self.args, **self.kwargs)
  35. assert isinstance(result, Awaitable)
  36. return await result
  37. return wait_for_result()
  38. else:
  39. with self.container:
  40. if self.instance is None:
  41. return func(*self.args, **self.kwargs)
  42. else:
  43. return func(self.instance, *self.args, **self.kwargs)
  44. class RefreshableContainer(Element, component='refreshable.js'):
  45. pass
  46. class refreshable(Generic[_P, _T]):
  47. def __init__(self, func: Callable[_P, Union[_T, Awaitable[_T]]]) -> None:
  48. """Refreshable UI functions
  49. The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
  50. This method will automatically delete all elements created by the function and recreate them.
  51. """
  52. self.func = func
  53. self.instance = None
  54. self.targets: List[RefreshableTarget] = []
  55. def __get__(self, instance, _) -> Self:
  56. self.instance = instance
  57. return self
  58. def __getattribute__(self, __name: str) -> Any:
  59. attribute = object.__getattribute__(self, __name)
  60. if __name == 'refresh':
  61. def refresh(*args: Any, _instance=self.instance, **kwargs: Any) -> None:
  62. self.instance = _instance
  63. attribute(*args, **kwargs)
  64. return refresh
  65. return attribute
  66. def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> Union[_T, Awaitable[_T]]:
  67. self.prune()
  68. target = RefreshableTarget(container=RefreshableContainer(), refreshable=self, instance=self.instance,
  69. args=args, kwargs=kwargs)
  70. self.targets.append(target)
  71. return target.run(self.func)
  72. def refresh(self, *args: Any, **kwargs: Any) -> None:
  73. """Refresh the UI elements created by this function.
  74. This method accepts the same arguments as the function itself or a subset of them.
  75. It will combine the arguments passed to the function with the arguments passed to this method.
  76. """
  77. self.prune()
  78. for target in self.targets:
  79. if target.instance != self.instance:
  80. continue
  81. target.container.clear()
  82. target.args = args or target.args
  83. target.kwargs.update(kwargs)
  84. try:
  85. result = target.run(self.func)
  86. except TypeError as e:
  87. if 'got multiple values for argument' in str(e):
  88. function = str(e).split()[0].split('.')[-1]
  89. parameter = str(e).split()[-1]
  90. raise TypeError(f'{parameter} needs to be consistently passed to {function} '
  91. 'either as positional or as keyword argument') from e
  92. raise
  93. if is_coroutine_function(self.func):
  94. assert isinstance(result, Awaitable)
  95. if core.loop and core.loop.is_running():
  96. background_tasks.create(result)
  97. else:
  98. core.app.on_startup(result)
  99. def prune(self) -> None:
  100. """Remove all targets that are no longer on a page with a client connection.
  101. This method is called automatically before each refresh.
  102. """
  103. self.targets = [
  104. target
  105. for target in self.targets
  106. if target.container.client.id in Client.instances and target.container.id in target.container.client.elements
  107. ]
  108. class refreshable_method(Generic[_S, _P, _T], refreshable[_P, _T]):
  109. def __init__(self, func: Callable[Concatenate[_S, _P], Union[_T, Awaitable[_T]]]) -> None:
  110. """Refreshable UI methods
  111. The `@ui.refreshable_method` decorator allows you to create methods that have a `refresh` method.
  112. This method will automatically delete all elements created by the function and recreate them.
  113. """
  114. super().__init__(func) # type: ignore
  115. def state(value: Any) -> Tuple[Any, Callable[[Any], None]]:
  116. """Create a state variable that automatically updates its refreshable UI container.
  117. :param value: The initial value of the state variable.
  118. :return: A tuple containing the current value and a function to update the value.
  119. """
  120. target = cast(RefreshableTarget, RefreshableTarget.current_target)
  121. if target.next_index >= len(target.locals):
  122. target.locals.append(value)
  123. else:
  124. value = target.locals[target.next_index]
  125. def set_value(new_value: Any, index=target.next_index) -> None:
  126. if target.locals[index] == new_value:
  127. return
  128. target.locals[index] = new_value
  129. target.refreshable.refresh()
  130. target.next_index += 1
  131. return value, set_value