Skip to content

lazily create asyncio objects #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions src/textual_dev/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import sys
from typing import Any, Generic, TypeVar, overload

__all__ = ["cached_property"]

if sys.version_info >= (3, 12):
from functools import cached_property
else:
# based on the code from Python 3.14:
# https://github.com/python/cpython/blob/
# 5507eff19c757a908a2ff29dfe423e35595fda00/Lib/functools.py#L1089-L1138
# Copyright (C) 2006 Python Software Foundation.
# vendored under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 because
# prior to Python 3.12 cached_property used a threading.Lock, which makes
# it very slow.
_T_co = TypeVar("_T_co", covariant=True)
_NOT_FOUND = object()

class cached_property(Generic[_T_co]):
def __init__(self, func: Callable[[Any, _T_co]]) -> None:
self.func = func
self.attrname = None
self.__doc__ = func.__doc__
self.__module__ = func.__module__

def __set_name__(self, owner: type[any], name: str) -> None:
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)

@overload
def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ...

@overload
def __get__(
self, instance: object, owner: type[Any] | None = None
) -> _T_co: ...

def __get__(
self, instance: object, owner: type[Any] | None = None
) -> _T_co | Self:
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it."
)
try:
cache = instance.__dict__
except (
AttributeError
): # not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
6 changes: 3 additions & 3 deletions src/textual_dev/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None:
self.log_queue: Queue[str | bytes | Type[ClientShutdown]] | None = None
self.spillover: int = 0
self.verbose: bool = False
self._ready_event: asyncio.Event = asyncio.Event()

async def connect(self) -> None:
"""Connect to the devtools server.
Expand All @@ -124,6 +123,7 @@ async def connect(self) -> None:

log_queue = self.log_queue
websocket = self.websocket
ready_event = asyncio.Event()

async def update_console() -> None:
"""Coroutine function scheduled as a Task, which listens on
Expand All @@ -141,7 +141,7 @@ async def update_console() -> None:
self.console.width = payload["width"]
self.console.height = payload["height"]
self.verbose = payload.get("verbose", False)
self._ready_event.set()
ready_event.set()

async def send_queued_logs():
"""Coroutine function which is scheduled as a Task, which consumes
Expand All @@ -162,7 +162,7 @@ async def send_queued_logs():
async def server_info_received() -> None:
"""Wait for the first server info message to be received and handled."""
try:
await asyncio.wait_for(self._ready_event.wait(), timeout=READY_TIMEOUT)
await asyncio.wait_for(ready_event.wait(), timeout=READY_TIMEOUT)
except asyncio.TimeoutError:
return

Expand Down
6 changes: 5 additions & 1 deletion src/textual_dev/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from textual._time import time

from textual_dev.renderables import DevConsoleHeader, DevConsoleLog, DevConsoleNotice
from textual_dev._compat import cached_property

QUEUEABLE_TYPES = {"client_log", "client_spillover"}

Expand All @@ -44,9 +45,12 @@ def __init__(
self.verbose = verbose
self.exclude = {name.upper() for name in exclude} if exclude else set()
self.console = Console()
self.shutdown_event = asyncio.Event()
self.clients: list[ClientHandler] = []

@cached_property
def shutdown_event(self) -> asyncio.Event:
return asyncio.Event()

async def start(self) -> None:
"""Starts devtools tasks"""
self.size_poll_task = asyncio.create_task(self._console_size_poller())
Expand Down
14 changes: 12 additions & 2 deletions tests/devtools/test_devtools_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import types
from asyncio import Queue
import asyncio
from datetime import datetime

import msgpack
Expand All @@ -23,6 +23,16 @@ def test_devtools_client_initialize_defaults():
assert devtools.url == f"ws://127.0.0.1:{DEVTOOLS_PORT}"


def test_devtools_client_initialize_defaults_after_asyncio_run():
async def amain():
pass

asyncio.run(amain())

devtools = DevtoolsClient()
assert devtools.url == f"ws://127.0.0.1:{DEVTOOLS_PORT}"


async def test_devtools_client_is_connected(devtools):
assert devtools.is_connected

Expand Down Expand Up @@ -59,7 +69,7 @@ async def test_devtools_log_places_encodes_and_queues_many_logs_as_string(devtoo
async def test_devtools_log_spillover(devtools):
# Give the devtools an intentionally small max queue size
await devtools._stop_log_queue_processing()
devtools.log_queue = Queue(maxsize=2)
devtools.log_queue = asyncio.Queue(maxsize=2)

# Force spillover of 2
devtools.log(DevtoolsLog((Panel("hello, world"),), CALLER))
Expand Down