pythonnative 0.16.0__py3-none-any.whl → 0.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pythonnative/animated.py CHANGED
@@ -1,46 +1,42 @@
1
1
  """Animated values + animation drivers + animated component wrappers.
2
2
 
3
- Modeled on React Native's `Animated` API. The core primitives are:
3
+ Modeled on React Native's ``Animated`` API but with an
4
+ ``async``-aware completion contract. The core primitives are:
4
5
 
5
6
  - [`AnimatedValue`][pythonnative.animated.AnimatedValue]: a numeric
6
7
  cell with subscribers; animations mutate it over time.
7
8
  - ``Animated.timing`` / ``Animated.spring`` / ``Animated.decay``:
8
- animation factories.
9
+ animation factories. The objects they return implement
10
+ ``__await__``, so you can write ``await Animated.timing(v, to=1.0)``
11
+ to suspend until the animation finishes.
9
12
  - ``Animated.sequence`` / ``Animated.parallel`` / ``Animated.delay``:
10
- composition.
13
+ composition; also awaitable.
11
14
  - ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``:
12
15
  components whose ``style`` may contain ``AnimatedValue`` instances.
13
- The component subscribes to the value during mount and forwards
14
- changes directly to the underlying native handler's
15
- ``set_animated_property`` hook (bypassing the reconciler so
16
- per-frame work doesn't go through full Python reconciliation).
17
16
 
18
17
  Driver:
19
18
 
20
- - A single background thread ticks at ~60 Hz, advancing every
21
- active animation by ``dt``. When an animation finishes it removes
22
- itself from the active set; the thread sleeps when nothing is
23
- running.
24
- - For platforms that have a native easing/animation API,
25
- ``AnimatedValue`` *also* sends a one-shot
26
- ``set_animated_property(view, prop, target, duration_ms, easing)``
27
- call when the animation starts, so UIKit / Android can interpolate
28
- at GPU 60 Hz without per-frame Python work. The Python ticker
29
- then keeps the reactive ``AnimatedValue.value`` reading
30
- approximately synchronized for any non-native consumers.
19
+ - A single background thread ticks at ~60 Hz, advancing every active
20
+ animation by ``dt``.
21
+ - Animations expose two APIs:
22
+ - ``handle.start()`` — fire-and-forget. Returns ``self``.
23
+ - ``await handle`` (or ``await handle.run()``) wait for the
24
+ animation to complete; cancellation cancels the animation.
31
25
 
32
26
  Example:
33
27
  ```python
34
28
  import pythonnative as pn
35
29
 
30
+
36
31
  @pn.component
37
32
  def FadeIn():
38
- opacity = pn.use_memo(lambda: pn.Animated.Value(0.0), [])
33
+ opacity = pn.use_animated_value(0.0)
39
34
 
40
- def fade_in():
41
- pn.Animated.timing(opacity, to=1.0, duration=400).start()
35
+ async def fade_in():
36
+ await pn.Animated.timing(opacity, to=1.0, duration=400)
37
+ await pn.Animated.timing(opacity, to=0.5, duration=200)
42
38
 
43
- pn.use_effect(fade_in, [])
39
+ pn.use_async_effect(fade_in, [])
44
40
 
45
41
  return pn.Animated.View(
46
42
  pn.Text("Hello!"),
@@ -51,6 +47,7 @@ Example:
51
47
 
52
48
  from __future__ import annotations
53
49
 
50
+ import asyncio
54
51
  import math
55
52
  import threading
56
53
  import time
@@ -58,14 +55,13 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
58
55
 
59
56
  from .element import Element
60
57
  from .hooks import use_effect, use_ref
58
+ from .runtime import resolve_future
61
59
  from .style import StyleProp, resolve_style
62
60
 
63
61
  # Maximum frame rate at which the Python ticker drives animations.
64
- # We aim for 60 Hz but back off when no animation is active.
65
62
  _TARGET_FPS = 60.0
66
63
  _FRAME_DT = 1.0 / _TARGET_FPS
67
64
 
68
- # Easing functions: t in [0, 1] -> [0, 1].
69
65
  _EASINGS: Dict[str, Callable[[float], float]] = {
70
66
  "linear": lambda t: t,
71
67
  "ease_in": lambda t: t * t,
@@ -104,13 +100,14 @@ def _resolve_easing(name: Any) -> Callable[[float], float]:
104
100
  class AnimatedValue:
105
101
  """A subscribable numeric cell driven by animations.
106
102
 
107
- Direct mutation via [`set_value`][pythonnative.animated.AnimatedValue.set_value]
108
- fires subscribers immediately; animations call `set_value` from
103
+ Direct mutation via
104
+ [`set_value`][pythonnative.animated.AnimatedValue.set_value]
105
+ fires subscribers immediately; animations call ``set_value`` from
109
106
  the ticker thread.
110
107
 
111
108
  Subscribers are ``(prop_name, callback)`` tuples. Each animated
112
- component (e.g., `Animated.View`) subscribes once per
113
- AnimatedValue prop in its style during mount.
109
+ component (e.g., ``Animated.View``) subscribes once per
110
+ ``AnimatedValue`` prop in its style during mount.
114
111
  """
115
112
 
116
113
  __slots__ = ("_value", "_subscribers", "_lock")
@@ -126,11 +123,7 @@ class AnimatedValue:
126
123
  return self._value
127
124
 
128
125
  def set_value(self, new_value: float) -> None:
129
- """Set the value immediately and fire all subscribers.
130
-
131
- Used by user code for instant snaps; animations also call this
132
- once per tick to update the value.
133
- """
126
+ """Set the value immediately and fire all subscribers."""
134
127
  new_value = float(new_value)
135
128
  with self._lock:
136
129
  self._value = new_value
@@ -146,8 +139,7 @@ class AnimatedValue:
146
139
 
147
140
  Returns an unsubscribe callable. ``prop`` is metadata only —
148
141
  it lets the subscriber differentiate this binding from others
149
- on the same AnimatedValue (the value can be bound to
150
- multiple props on multiple views).
142
+ on the same ``AnimatedValue``.
151
143
  """
152
144
  with self._lock:
153
145
  self._subscribers.append((prop, callback))
@@ -176,9 +168,9 @@ class AnimatedValue:
176
168
  class _AnimationManager:
177
169
  """Single-threaded driver for all currently-running animations.
178
170
 
179
- Holds a list of ``(animation, advance_callback)`` pairs and
180
- ticks them at ~60 Hz. The thread starts on first use and idles
181
- (releases the GIL via ``time.sleep``) when nothing is active.
171
+ Holds a list of ``_RunningAnimation`` instances and ticks them at
172
+ ~60 Hz. The thread starts on first use and idles when nothing is
173
+ active.
182
174
  """
183
175
 
184
176
  def __init__(self) -> None:
@@ -214,7 +206,6 @@ class _AnimationManager:
214
206
  with self._lock:
215
207
  active = list(self._animations)
216
208
  if not active:
217
- # Idle: sleep longer until something starts.
218
209
  time.sleep(0.05)
219
210
  last = time.monotonic()
220
211
  continue
@@ -237,21 +228,28 @@ _manager = _AnimationManager()
237
228
 
238
229
 
239
230
  class _RunningAnimation:
240
- """Base class for in-flight animations; advance() returns True when done."""
231
+ """Base class for in-flight animations; ``advance()`` returns True when done."""
241
232
 
242
- def __init__(self, value: AnimatedValue, on_complete: Optional[Callable[[], None]]) -> None:
233
+ def __init__(self, value: AnimatedValue) -> None:
243
234
  self.value = value
244
- self._on_complete = on_complete
235
+ self._completion_futures: List[asyncio.Future[None]] = []
236
+ self._completed = False
237
+
238
+ def add_completion_future(self, future: asyncio.Future[None]) -> None:
239
+ """Register ``future`` to be resolved when the animation ends."""
240
+ self._completion_futures.append(future)
241
+ if self._completed:
242
+ resolve_future(future, None)
245
243
 
246
244
  def advance(self, dt: float) -> bool:
247
245
  raise NotImplementedError
248
246
 
249
247
  def _finish(self) -> None:
250
- if self._on_complete is not None:
251
- try:
252
- self._on_complete()
253
- except Exception:
254
- pass
248
+ if self._completed:
249
+ return
250
+ self._completed = True
251
+ for fut in self._completion_futures:
252
+ resolve_future(fut, None)
255
253
 
256
254
 
257
255
  class _TimingAnimation(_RunningAnimation):
@@ -261,9 +259,8 @@ class _TimingAnimation(_RunningAnimation):
261
259
  to: float,
262
260
  duration: float,
263
261
  easing: Callable[[float], float],
264
- on_complete: Optional[Callable[[], None]],
265
262
  ) -> None:
266
- super().__init__(value, on_complete)
263
+ super().__init__(value)
267
264
  self._from = value.value
268
265
  self._to = float(to)
269
266
  self._duration = max(0.001, float(duration) / 1000.0)
@@ -292,9 +289,8 @@ class _SpringAnimation(_RunningAnimation):
292
289
  stiffness: float,
293
290
  damping: float,
294
291
  mass: float,
295
- on_complete: Optional[Callable[[], None]],
296
292
  ) -> None:
297
- super().__init__(value, on_complete)
293
+ super().__init__(value)
298
294
  self._to = float(to)
299
295
  self._velocity = 0.0
300
296
  self._stiffness = float(stiffness)
@@ -316,20 +312,13 @@ class _SpringAnimation(_RunningAnimation):
316
312
 
317
313
 
318
314
  class _DecayAnimation(_RunningAnimation):
319
- def __init__(
320
- self,
321
- value: AnimatedValue,
322
- velocity: float,
323
- deceleration: float,
324
- on_complete: Optional[Callable[[], None]],
325
- ) -> None:
326
- super().__init__(value, on_complete)
315
+ def __init__(self, value: AnimatedValue, velocity: float, deceleration: float) -> None:
316
+ super().__init__(value)
327
317
  self._velocity = float(velocity)
328
318
  self._deceleration = float(deceleration)
329
319
  self._rest_threshold = 0.001
330
320
 
331
321
  def advance(self, dt: float) -> bool:
332
- # Exponential decay of velocity.
333
322
  self._velocity *= math.exp(-self._deceleration * dt * 1000.0)
334
323
  new_x = self.value.value + self._velocity * dt
335
324
  self.value.set_value(new_x)
@@ -339,94 +328,150 @@ class _DecayAnimation(_RunningAnimation):
339
328
  return False
340
329
 
341
330
 
342
- class _CompositeAnimation:
343
- """Wraps a list of animations played in sequence or in parallel."""
331
+ class _DelayAnimation(_RunningAnimation):
332
+ def __init__(self, duration_ms: float) -> None:
333
+ super().__init__(AnimatedValue(0.0))
334
+ self._elapsed = 0.0
335
+ self._duration = max(0.001, duration_ms / 1000.0)
344
336
 
345
- def __init__(self, items: List[Any], mode: str) -> None:
346
- self._items = list(items)
347
- self._mode = mode
337
+ def advance(self, dt: float) -> bool:
338
+ self._elapsed += dt
339
+ if self._elapsed >= self._duration:
340
+ self._finish()
341
+ return True
342
+ return False
348
343
 
349
- def start(self, on_complete: Optional[Callable[[], None]] = None) -> None:
350
- if self._mode == "parallel":
351
- remaining = [len(self._items)]
352
- lock = threading.Lock()
353
-
354
- def _one_done() -> None:
355
- with lock:
356
- remaining[0] -= 1
357
- if remaining[0] <= 0 and on_complete is not None:
358
- try:
359
- on_complete()
360
- except Exception:
361
- pass
362
-
363
- for item in self._items:
364
- if item is None:
365
- _one_done()
366
- continue
367
- try:
368
- item.start(_one_done)
369
- except Exception:
370
- _one_done()
371
- return
372
344
 
373
- # Sequence
374
- index = [0]
345
+ # ======================================================================
346
+ # Public animation handles
347
+ # ======================================================================
375
348
 
376
- def _next() -> None:
377
- i = index[0]
378
- if i >= len(self._items):
379
- if on_complete is not None:
380
- try:
381
- on_complete()
382
- except Exception:
383
- pass
384
- return
385
- item = self._items[i]
386
- index[0] += 1
387
- if item is None:
388
- _next()
389
- return
390
- try:
391
- item.start(_next)
392
- except Exception:
393
- _next()
394
349
 
395
- _next()
350
+ class _AwaitableAnimation:
351
+ """Base for awaitable animation handles.
352
+
353
+ Subclasses implement :meth:`start` and :meth:`stop`. Awaiting the
354
+ handle (``await handle``) starts the animation if necessary and
355
+ suspends until it completes. Cancelling the awaiting task calls
356
+ :meth:`stop`.
357
+
358
+ Calling :meth:`start` returns ``self`` so handles can be chained
359
+ or stashed: ``handle = pn.Animated.timing(...).start()``.
360
+ """
361
+
362
+ def start(self) -> "_AwaitableAnimation":
363
+ raise NotImplementedError
396
364
 
397
365
  def stop(self) -> None:
398
- for item in self._items:
366
+ raise NotImplementedError
367
+
368
+ def run(self) -> "_AwaitableAnimation":
369
+ """Return ``self`` for explicit ``await handle.run()`` style.
370
+
371
+ Equivalent to ``await handle`` directly; provided because some
372
+ readers prefer the slightly more explicit form, particularly
373
+ when storing the awaitable before resolving it.
374
+ """
375
+ return self
376
+
377
+ async def _drive(self) -> None:
378
+ raise NotImplementedError
379
+
380
+ def __await__(self) -> Any:
381
+ try:
382
+ asyncio.get_running_loop()
383
+ except RuntimeError as exc:
384
+ raise RuntimeError(
385
+ "Animations can only be awaited from inside an asyncio task; "
386
+ "use handle.start() to fire-and-forget instead."
387
+ ) from exc
388
+
389
+ async def _runner() -> None:
399
390
  try:
400
- item.stop()
401
- except Exception:
402
- pass
391
+ await self._drive()
392
+ except asyncio.CancelledError:
393
+ self.stop()
394
+ raise
403
395
 
396
+ return _runner().__await__()
404
397
 
405
- class _AnimationHandle:
406
- """Public handle returned by `Animated.timing` / `.spring` / `.decay`.
407
398
 
408
- Wraps a `_RunningAnimation` factory so each ``.start()`` call creates
409
- a fresh in-flight animation (matches RN the `Animated.timing`
410
- return value is reusable).
399
+ class _AnimationHandle(_AwaitableAnimation):
400
+ """Public handle returned by ``Animated.timing`` / ``.spring`` / ``.decay``.
401
+
402
+ Wraps a ``_RunningAnimation`` factory so each ``.start()`` call
403
+ creates a fresh in-flight animation (matches React Native — the
404
+ ``Animated.timing`` return value is reusable).
411
405
  """
412
406
 
413
- def __init__(self, factory: Callable[[Optional[Callable[[], None]]], _RunningAnimation]) -> None:
407
+ def __init__(self, factory: Callable[[], _RunningAnimation]) -> None:
414
408
  self._factory = factory
415
409
  self._current: Optional[_RunningAnimation] = None
416
410
 
417
- def start(self, on_complete: Optional[Callable[[], None]] = None) -> None:
418
- """Begin the animation, optionally invoking ``on_complete`` at the end."""
411
+ def start(self) -> "_AnimationHandle":
412
+ """Begin the animation. Returns ``self`` for chaining."""
419
413
  self.stop()
420
- anim = self._factory(on_complete)
414
+ anim = self._factory()
421
415
  self._current = anim
422
416
  _manager.add(anim)
417
+ return self
423
418
 
424
419
  def stop(self) -> None:
425
420
  """Cancel the running instance (no-op if not running)."""
426
421
  if self._current is not None:
422
+ self._current._finish()
427
423
  _manager.remove(self._current)
428
424
  self._current = None
429
425
 
426
+ async def _drive(self) -> None:
427
+ if self._current is None:
428
+ self.start()
429
+ loop = asyncio.get_running_loop()
430
+ future: asyncio.Future[None] = loop.create_future()
431
+ assert self._current is not None
432
+ self._current.add_completion_future(future)
433
+ await future
434
+
435
+
436
+ class _CompositeAnimation(_AwaitableAnimation):
437
+ """Run a list of animations in sequence or in parallel."""
438
+
439
+ def __init__(self, items: List[Any], mode: str) -> None:
440
+ self._items = list(items)
441
+ self._mode = mode
442
+
443
+ def start(self) -> "_CompositeAnimation":
444
+ """Schedule the composite on the framework runtime, fire-and-forget."""
445
+ from .runtime import run_async
446
+
447
+ run_async(self._drive())
448
+ return self
449
+
450
+ def stop(self) -> None:
451
+ for item in self._items:
452
+ try:
453
+ item.stop()
454
+ except Exception:
455
+ pass
456
+
457
+ async def _drive(self) -> None:
458
+ if self._mode == "parallel":
459
+ await asyncio.gather(*(self._await_item(item) for item in self._items))
460
+ return
461
+ for item in self._items:
462
+ await self._await_item(item)
463
+
464
+ @staticmethod
465
+ async def _await_item(item: Any) -> None:
466
+ if item is None:
467
+ return
468
+ if isinstance(item, _AwaitableAnimation):
469
+ await item
470
+ else:
471
+ # Plain awaitables and coroutines are supported too — lets
472
+ # users mix in ``asyncio.sleep`` or other awaitables.
473
+ await item
474
+
430
475
 
431
476
  # ======================================================================
432
477
  # Animated component wrappers
@@ -434,10 +479,10 @@ class _AnimationHandle:
434
479
 
435
480
 
436
481
  def _resolve_style_with_values(style: StyleProp) -> Tuple[Dict[str, Any], Dict[str, AnimatedValue]]:
437
- """Return ``(plain_style, animated_bindings)``.
482
+ """Split ``style`` into a plain dict and animated bindings.
438
483
 
439
- AnimatedValue entries in the style are replaced with their
440
- current numeric value in ``plain_style`` and recorded in
484
+ AnimatedValue entries in the style are replaced with their current
485
+ numeric value in ``plain_style`` and recorded in
441
486
  ``animated_bindings`` so the wrapping component can subscribe
442
487
  after mount.
443
488
  """
@@ -457,11 +502,7 @@ def _make_animated_factory(
457
502
  element_type: str,
458
503
  accept_children: bool,
459
504
  ) -> Callable[..., Element]:
460
- """Build an animated wrapper for ``element_type``.
461
-
462
- The returned factory is used as the public
463
- ``Animated.View`` / ``Animated.Text`` / ``Animated.Image``.
464
- """
505
+ """Build an animated wrapper for ``element_type``."""
465
506
  from .hooks import component # local import to avoid cycle
466
507
 
467
508
  @component
@@ -482,9 +523,9 @@ def _make_animated_factory(
482
523
  return lambda: None
483
524
 
484
525
  for prop, value in bindings.items():
485
- # Capture into closure via default arg.
526
+
486
527
  def _on_change(new_val: float, _prop: str = prop, _view: Any = view) -> None:
487
- handler = _get_handler_for(view)
528
+ handler = _get_handler_for(_view)
488
529
  if handler is None:
489
530
  return
490
531
  setter = getattr(handler, "set_animated_property", None)
@@ -516,7 +557,6 @@ def _make_animated_factory(
516
557
  if element_type == "Image":
517
558
  source = args[0] if args else kwargs.pop("source", "")
518
559
  return _Image(source, style=plain_style, ref=ref)
519
- # View
520
560
  children = list(args) if accept_children else []
521
561
  return _View(*children, style=plain_style, ref=ref)
522
562
 
@@ -524,29 +564,19 @@ def _make_animated_factory(
524
564
 
525
565
 
526
566
  def _animated_prop_name(prop: str) -> str:
527
- """Map a style key to the name expected by `set_animated_property`."""
567
+ """Map a style key to the name expected by ``set_animated_property``."""
528
568
  if prop == "opacity":
529
569
  return "opacity"
530
570
  if prop == "background_color":
531
571
  return "background_color"
532
- # Transform shorthand keys: ``translate_x``, ``translate_y``,
533
- # ``scale``, ``scale_x``, ``scale_y``, ``rotate``.
534
572
  if prop in ("translate_x", "translate_y", "scale", "scale_x", "scale_y", "rotate"):
535
573
  return prop
536
574
  return prop
537
575
 
538
576
 
539
577
  def _get_handler_for(native_view: Any) -> Any:
540
- """Best-effort lookup of the registered handler for ``native_view``.
541
-
542
- Animated bindings need a handler reference to call
543
- `set_animated_property`. Since the registry is keyed by element
544
- type and we only have the native view, we fall back to looking
545
- up the most recently registered "View" handler — works in
546
- practice because all animated targets are flex containers,
547
- images, or text views, and every iOS/Android handler subclass
548
- inherits the same `set_animated_property` from the base.
549
- """
578
+ """Best-effort lookup of the registered handler for ``native_view``."""
579
+ del native_view
550
580
  try:
551
581
  from .native_views import get_registry
552
582
 
@@ -570,8 +600,8 @@ def _get_handler_for(native_view: Any) -> Any:
570
600
  class _AnimatedNamespace:
571
601
  """Public ``Animated`` namespace.
572
602
 
573
- Exposes the `Value`, animation factories, composers, and
574
- component wrappers (`View`, `Text`, `Image`).
603
+ Exposes the ``Value`` type, animation factories, composers, and
604
+ component wrappers (``View``, ``Text``, ``Image``).
575
605
  """
576
606
 
577
607
  Value = AnimatedValue
@@ -586,8 +616,8 @@ class _AnimatedNamespace:
586
616
  ) -> _AnimationHandle:
587
617
  """Linearly interpolate ``value`` to ``to`` over ``duration`` ms."""
588
618
 
589
- def _factory(on_complete: Optional[Callable[[], None]]) -> _RunningAnimation:
590
- return _TimingAnimation(value, to, duration, _resolve_easing(easing), on_complete)
619
+ def _factory() -> _RunningAnimation:
620
+ return _TimingAnimation(value, to, duration, _resolve_easing(easing))
591
621
 
592
622
  return _AnimationHandle(_factory)
593
623
 
@@ -602,8 +632,8 @@ class _AnimatedNamespace:
602
632
  ) -> _AnimationHandle:
603
633
  """Run a damped harmonic spring toward ``to``."""
604
634
 
605
- def _factory(on_complete: Optional[Callable[[], None]]) -> _RunningAnimation:
606
- return _SpringAnimation(value, to, stiffness, damping, mass, on_complete)
635
+ def _factory() -> _RunningAnimation:
636
+ return _SpringAnimation(value, to, stiffness, damping, mass)
607
637
 
608
638
  return _AnimationHandle(_factory)
609
639
 
@@ -616,8 +646,8 @@ class _AnimatedNamespace:
616
646
  ) -> _AnimationHandle:
617
647
  """Decelerate ``value`` from its current velocity until it rests."""
618
648
 
619
- def _factory(on_complete: Optional[Callable[[], None]]) -> _RunningAnimation:
620
- return _DecayAnimation(value, velocity, deceleration, on_complete)
649
+ def _factory() -> _RunningAnimation:
650
+ return _DecayAnimation(value, velocity, deceleration)
621
651
 
622
652
  return _AnimationHandle(_factory)
623
653
 
@@ -635,21 +665,8 @@ class _AnimatedNamespace:
635
665
  def delay(duration: float) -> _AnimationHandle:
636
666
  """Wait ``duration`` ms before continuing in a sequence."""
637
667
 
638
- def _factory(on_complete: Optional[Callable[[], None]]) -> _RunningAnimation:
639
- class _Delay(_RunningAnimation):
640
- def __init__(self, on_complete: Optional[Callable[[], None]]) -> None:
641
- super().__init__(AnimatedValue(0.0), on_complete)
642
- self._elapsed = 0.0
643
- self._duration = max(0.001, duration / 1000.0)
644
-
645
- def advance(self, dt: float) -> bool:
646
- self._elapsed += dt
647
- if self._elapsed >= self._duration:
648
- self._finish()
649
- return True
650
- return False
651
-
652
- return _Delay(on_complete)
668
+ def _factory() -> _RunningAnimation:
669
+ return _DelayAnimation(duration)
653
670
 
654
671
  return _AnimationHandle(_factory)
655
672
 
@@ -662,7 +679,7 @@ Animated = _AnimatedNamespace()
662
679
 
663
680
 
664
681
  def use_animated_value(initial: float = 0.0) -> AnimatedValue:
665
- """Return an [`AnimatedValue`][pythonnative.AnimatedValue] with a stable identity across renders.
682
+ """Return an [`AnimatedValue`][pythonnative.AnimatedValue] that is stable across renders.
666
683
 
667
684
  Convenience wrapper for the common pattern
668
685
  ``pn.use_memo(lambda: AnimatedValue(initial), [])``. The same
@@ -679,14 +696,15 @@ def use_animated_value(initial: float = 0.0) -> AnimatedValue:
679
696
  ```python
680
697
  import pythonnative as pn
681
698
 
699
+
682
700
  @pn.component
683
701
  def FadeIn():
684
702
  opacity = pn.use_animated_value(0.0)
685
703
 
686
- def fade_in():
687
- pn.Animated.timing(opacity, to=1.0, duration=300).start()
704
+ async def fade_in():
705
+ await pn.Animated.timing(opacity, to=1.0, duration=300)
688
706
 
689
- pn.use_effect(lambda: fade_in(), [])
707
+ pn.use_async_effect(fade_in, [])
690
708
  return pn.Animated.View(
691
709
  pn.Text("Hello"),
692
710
  style=pn.style(opacity=opacity),