Skip to content

Commit bc96aac

Browse files
committed
move eagerly created asyncio.Event and asyncio.Queue objects into cached_properties
1 parent 5d9e3c0 commit bc96aac

File tree

5 files changed

+103
-12
lines changed

5 files changed

+103
-12
lines changed

src/textual/_animator.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from textual import _time
1212
from textual._callback import invoke
13+
from textual._compat import cached_property
1314
from textual._easing import DEFAULT_EASING, EASING
1415
from textual._types import AnimationLevel, CallbackType
1516
from textual.timer import Timer
@@ -242,11 +243,16 @@ def __init__(self, app: App, frames_per_second: int = 60) -> None:
242243
callback=self,
243244
pause=True,
244245
)
246+
247+
@cached_property
248+
def _idle_event(self) -> asyncio.Event:
245249
"""The timer that runs the animator."""
246-
self._idle_event = asyncio.Event()
250+
return asyncio.Event()
251+
252+
@cached_property
253+
def _complete_event(self) -> asyncio.Event:
247254
"""Flag if no animations are currently taking place."""
248-
self._complete_event = asyncio.Event()
249-
"""Flag if no animations are currently taking place and none are scheduled."""
255+
return asyncio.Event()
250256

251257
async def start(self) -> None:
252258
"""Start the animator task."""

src/textual/_compat.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from typing import Any, Generic, TypeVar, overload
5+
6+
if sys.version_info >= (3, 12):
7+
from functools import cached_property
8+
else:
9+
_T_co = TypeVar("_T_co", covariant=True)
10+
_NOT_FOUND = object()
11+
12+
class cached_property(Generic[_T_co]):
13+
def __init__(self, func: Callable[[Any, _T_co]]) -> None:
14+
self.func = func
15+
self.attrname = None
16+
self.__doc__ = func.__doc__
17+
self.__module__ = func.__module__
18+
19+
def __set_name__(self, owner: type[any], name: str) -> None:
20+
if self.attrname is None:
21+
self.attrname = name
22+
elif name != self.attrname:
23+
raise TypeError(
24+
"Cannot assign the same cached_property to two different names "
25+
f"({self.attrname!r} and {name!r})."
26+
)
27+
28+
@overload
29+
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...
30+
31+
@overload
32+
def __get__(
33+
self, instance: object, owner: type[Any] | None = None
34+
) -> _T_co: ...
35+
36+
def __get__(
37+
self, instance: object, owner: type[Any] | None = None
38+
) -> _T_co | Self:
39+
if instance is None:
40+
return self
41+
if self.attrname is None:
42+
raise TypeError(
43+
"Cannot use cached_property instance without calling __set_name__ on it."
44+
)
45+
try:
46+
cache = instance.__dict__
47+
except (
48+
AttributeError
49+
): # not all objects have __dict__ (e.g. class defines slots)
50+
msg = (
51+
f"No '__dict__' attribute on {type(instance).__name__!r} "
52+
f"instance to cache {self.attrname!r} property."
53+
)
54+
raise TypeError(msg) from None
55+
val = cache.get(self.attrname, _NOT_FOUND)
56+
if val is _NOT_FOUND:
57+
val = self.func(instance)
58+
try:
59+
cache[self.attrname] = val
60+
except TypeError:
61+
msg = (
62+
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
63+
f"does not support item assignment for caching {self.attrname!r} property."
64+
)
65+
raise TypeError(msg) from None
66+
return val

src/textual/app.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
from textual._ansi_sequences import SYNC_END, SYNC_START
7575
from textual._ansi_theme import ALABASTER, MONOKAI
7676
from textual._callback import invoke
77+
from textual._compat import cached_property
7778
from textual._compose import compose
7879
from textual._compositor import CompositorUpdate
7980
from textual._context import active_app, active_message_pump
@@ -648,9 +649,6 @@ def __init__(
648649
"""The unhandled exception which is leading to the app shutting down,
649650
or None if the app is still running with no unhandled exceptions."""
650651

651-
self._exception_event: asyncio.Event = asyncio.Event()
652-
"""An event that will be set when the first exception is encountered."""
653-
654652
self.title = (
655653
self.TITLE if self.TITLE is not None else f"{self.__class__.__name__}"
656654
)
@@ -844,6 +842,11 @@ def __init__(
844842
)
845843
)
846844

845+
@cached_property
846+
def _exception_event(self) -> asyncio.Event:
847+
"""An event that will be set when the first exception is encountered."""
848+
return asyncio.Event()
849+
847850
def __init_subclass__(cls, *args, **kwargs) -> None:
848851
for variable_name, screen_collection in (
849852
("SCREENS", cls.SCREENS),

src/textual/message_pump.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from __future__ import annotations
1212

1313
import asyncio
14+
import sys
1415
import threading
15-
from asyncio import CancelledError, Queue, QueueEmpty, Task, create_task
16+
from asyncio import CancelledError, QueueEmpty, Task, create_task
1617
from contextlib import contextmanager
1718
from functools import partial
1819
from time import perf_counter
@@ -22,15 +23,18 @@
2223
Awaitable,
2324
Callable,
2425
Generator,
26+
Generic,
2527
Iterable,
2628
Type,
2729
TypeVar,
2830
cast,
31+
overload,
2932
)
3033
from weakref import WeakSet
3134

3235
from textual import Logger, events, log, messages
3336
from textual._callback import invoke
37+
from textual._compat import cached_property
3438
from textual._context import NoActiveAppError, active_app, active_message_pump
3539
from textual._context import message_hook as message_hook_context_var
3640
from textual._context import prevent_message_types_stack
@@ -114,7 +118,6 @@ class MessagePump(metaclass=_MessagePumpMeta):
114118
"""Base class which supplies a message pump."""
115119

116120
def __init__(self, parent: MessagePump | None = None) -> None:
117-
self._message_queue: Queue[Message | None] = Queue()
118121
self._parent = parent
119122
self._running: bool = False
120123
self._closing: bool = False
@@ -125,7 +128,6 @@ def __init__(self, parent: MessagePump | None = None) -> None:
125128
self._timers: WeakSet[Timer] = WeakSet()
126129
self._last_idle: float = time()
127130
self._max_idle: float | None = None
128-
self._mounted_event = asyncio.Event()
129131
self._is_mounted = False
130132
"""Having this explicit Boolean is an optimization.
131133
@@ -143,6 +145,14 @@ def __init__(self, parent: MessagePump | None = None) -> None:
143145
144146
"""
145147

148+
@cached_property
149+
def _message_queue(self) -> asyncio.Queue[Message | None]:
150+
return asyncio.Queue()
151+
152+
@cached_property
153+
def _mounted_event(self) -> asyncio.Event:
154+
return asyncio.Event()
155+
146156
@property
147157
def _prevent_message_types_stack(self) -> list[set[type[Message]]]:
148158
"""The stack that manages prevented messages."""

src/textual/timer.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from textual import _time, events
1717
from textual._callback import invoke
18+
from textual._compat import cached_property
1819
from textual._context import active_app
1920
from textual._time import sleep
2021
from textual._types import MessageTarget
@@ -62,11 +63,16 @@ def __init__(
6263
self._callback = callback
6364
self._repeat = repeat
6465
self._skip = skip
65-
self._active = Event()
6666
self._task: Task | None = None
6767
self._reset: bool = False
68-
if not pause:
69-
self._active.set()
68+
self._original_pause = pause
69+
70+
@cached_property
71+
def _active(self) -> Event:
72+
event = Event()
73+
if not self._original_pause:
74+
event.set()
75+
return event
7076

7177
def __rich_repr__(self) -> Result:
7278
yield self._interval

0 commit comments

Comments
 (0)