From 8b79890b9f45da6f572043632f07497462e1a205 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 8 Apr 2024 16:01:43 +0200 Subject: [PATCH] - --- .gitignore | 3 -- jupyverse_api/jupyverse_api/main/__init__.py | 20 ++++++--- jupyverse_api/pyproject.toml | 2 + plugins/auth/fps_auth/main.py | 14 +++---- plugins/auth_fief/fps_auth_fief/main.py | 13 +++--- .../fps_auth_jupyterhub/main.py | 32 +++++++-------- plugins/contents/fps_contents/fileid.py | 20 +++++++-- plugins/contents/fps_contents/main.py | 8 ++-- plugins/frontend/fps_frontend/main.py | 2 +- plugins/jupyterlab/fps_jupyterlab/main.py | 14 +++---- .../fps_kernels/kernel_driver/connect.py | 2 + .../fps_kernels/kernel_driver/driver.py | 26 +++++++++--- plugins/kernels/fps_kernels/main.py | 16 ++++---- plugins/kernels/fps_kernels/routes.py | 6 ++- plugins/lab/fps_lab/main.py | 12 +++--- plugins/lab/fps_lab/routes.py | 5 +++ plugins/login/fps_login/main.py | 8 ++-- plugins/nbconvert/fps_nbconvert/main.py | 8 ++-- plugins/noauth/fps_noauth/main.py | 9 ++-- plugins/notebook/fps_notebook/main.py | 17 ++++---- .../resource_usage/fps_resource_usage/main.py | 8 ++-- plugins/terminals/fps_terminals/main.py | 10 ++--- plugins/terminals/fps_terminals/routes.py | 3 +- plugins/terminals/fps_terminals/server.py | 5 ++- plugins/webdav/fps_webdav/main.py | 6 +-- plugins/webdav/tests/test_webdav.py | 4 +- plugins/yjs/fps_yjs/main.py | 18 ++++---- plugins/yjs/fps_yjs/routes.py | 41 ++++++++++++++----- pyproject.toml | 17 ++------ tests/conftest.py | 5 +++ tests/test_app.py | 10 ++--- tests/test_auth.py | 40 +++++++++--------- tests/test_contents.py | 6 +-- tests/test_execute.py | 30 +++++++------- tests/test_kernels.py | 4 +- tests/test_server.py | 38 ++++++++--------- tests/test_settings.py | 6 +-- 37 files changed, 273 insertions(+), 215 deletions(-) diff --git a/.gitignore b/.gitignore index 70e4192c..d4751320 100644 --- a/.gitignore +++ b/.gitignore @@ -344,6 +344,3 @@ $RECYCLE.BIN/ .jupyter_ystore.db .jupyter_ystore.db-journal fps_cli_args.toml - -# pixi environments -.pixi diff --git a/jupyverse_api/jupyverse_api/main/__init__.py b/jupyverse_api/jupyverse_api/main/__init__.py index 0e40f4ee..9d4b0208 100644 --- a/jupyverse_api/jupyverse_api/main/__init__.py +++ b/jupyverse_api/jupyverse_api/main/__init__.py @@ -3,8 +3,9 @@ import webbrowser from typing import Any, Callable, Dict, Sequence, Tuple +from anyio import Event from asgiref.typing import ASGI3Application -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource, start_service_task from asphalt.web.fastapi import FastAPIComponent from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -23,10 +24,10 @@ def __init__( self.mount_path = mount_path async def start(self) -> None: - app = await request_resource(FastAPI) + app = await get_resource(FastAPI, wait=True) _app = App(app, mount_path=self.mount_path) - await add_resource(_app) + add_resource(_app) class JupyverseComponent(FastAPIComponent): @@ -64,6 +65,7 @@ def __init__( self.port = port self.open_browser = open_browser self.query_params = query_params + self.lifespan = Lifespan() async def start(self) -> None: query_params = QueryParams(d={}) @@ -71,12 +73,15 @@ async def start(self) -> None: if not host.startswith("http"): host = f"http://{host}" host_url = Host(url=f"{host}:{self.port}/") - await add_resource(query_params) - await add_resource(host_url) + add_resource(query_params) + add_resource(host_url) + add_resource(self.lifespan) await super().start() # at this point, the server has started + await start_service_task(self.lifespan.shutdown_request.wait, "Server lifespan notifier", teardown_action=self.lifespan.shutdown_request.set) + if self.open_browser: qp = query_params.d if self.query_params: @@ -91,3 +96,8 @@ class QueryParams(BaseModel): class Host(BaseModel): url: str + + +class Lifespan: + def __init__(self): + self.shutdown_request = Event() diff --git a/jupyverse_api/pyproject.toml b/jupyverse_api/pyproject.toml index 670c64bf..ee1ea543 100644 --- a/jupyverse_api/pyproject.toml +++ b/jupyverse_api/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] @@ -28,6 +29,7 @@ dependencies = [ "pydantic >=2,<3", "fastapi >=0.95.0,<1", "rich-click >=1.6.1,<2", + "importlib_metadata >=3.6; python_version<'3.10'", #"asphalt >=4.11.0,<5", #"asphalt-web[fastapi] >=1.1.0,<2", ] diff --git a/plugins/auth/fps_auth/main.py b/plugins/auth/fps_auth/main.py index 90fb3743..df086639 100644 --- a/plugins/auth/fps_auth/main.py +++ b/plugins/auth/fps_auth/main.py @@ -1,6 +1,6 @@ import logging -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from fastapi_users.exceptions import UserAlreadyExists from jupyverse_api.app import App @@ -19,13 +19,13 @@ def __init__(self, **kwargs): self.auth_config = _AuthConfig(**kwargs) async def start(self) -> None: - await add_resource(self.auth_config, types=AuthConfig) + add_resource(self.auth_config, types=AuthConfig) - app = await request_resource(App) - frontend_config = await request_resource(FrontendConfig) + app = await get_resource(App, wait=True) + frontend_config = await get_resource(FrontendConfig, wait=True) auth = auth_factory(app, self.auth_config, frontend_config) - await add_resource(auth, types=Auth) + add_resource(auth, types=Auth) await auth.db.create_db_and_tables() @@ -56,8 +56,8 @@ async def start(self) -> None: ) if self.auth_config.mode == "token": - query_params = await request_resource(QueryParams) - host = await request_resource(Host) + query_params = await get_resource(QueryParams, wait=True) + host = await get_resource(Host, wait=True) query_params.d["token"] = self.auth_config.token logger.info("") diff --git a/plugins/auth_fief/fps_auth_fief/main.py b/plugins/auth_fief/fps_auth_fief/main.py index 812882b9..04632df9 100644 --- a/plugins/auth_fief/fps_auth_fief/main.py +++ b/plugins/auth_fief/fps_auth_fief/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, Context +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth, AuthConfig @@ -11,13 +11,10 @@ class AuthFiefComponent(Component): def __init__(self, **kwargs): self.auth_fief_config = _AuthFiefConfig(**kwargs) - async def start( - self, - ctx: Context, - ) -> None: - await ctx.add_resource(self.auth_fief_config, types=AuthConfig) + async def start(self) -> None: + add_resource(self.auth_fief_config, types=AuthConfig) - app = await ctx.request_resource(App) + app = await get_resource(App, wait=True) auth_fief = auth_factory(app, self.auth_fief_config) - await ctx.add_resource(auth_fief, types=Auth) + add_resource(auth_fief, types=Auth) diff --git a/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py b/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py index 02f59451..ac421a70 100644 --- a/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py +++ b/plugins/auth_jupyterhub/fps_auth_jupyterhub/main.py @@ -1,4 +1,10 @@ -from asphalt.core import Component, ContainerComponent, Context +from asphalt.core import ( + Component, + ContainerComponent, + add_resource, + get_resource, + start_background_task, +) from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession from jupyverse_api.app import App @@ -11,18 +17,15 @@ class _AuthJupyterHubComponent(Component): #@context_teardown - async def start( - self, - ctx: Context, - ) -> None: - app = await ctx.request_resource(App) - db_session = await ctx.request_resource(AsyncSession) - db_engine = await ctx.request_resource(AsyncEngine) + async def start(self) -> None: + app = await get_resource(App, wait=True) + db_session = await get_resource(AsyncSession, wait=True) + db_engine = await get_resource(AsyncEngine, wait=True) http_client = httpx.AsyncClient() auth_jupyterhub = auth_factory(app, db_session) - await ctx.start_background_task(auth_jupyterhub.start, "JupyterHub Auth", auth_jupyterhub.stop) - await ctx.add_resource(auth_jupyterhub, types=Auth) + await start_background_task(auth_jupyterhub.start, "JupyterHub Auth", auth_jupyterhub.stop) + add_resource(auth_jupyterhub, types=Auth) async with db_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) @@ -37,14 +40,11 @@ def __init__(self, **kwargs): self.auth_jupyterhub_config = AuthJupyterHubConfig(**kwargs) super().__init__() - async def start( - self, - ctx: Context, - ) -> None: - await ctx.add_resource(self.auth_jupyterhub_config, types=AuthConfig) + async def start(self) -> None: + add_resource(self.auth_jupyterhub_config, types=AuthConfig) self.add_component( "sqlalchemy", url=self.auth_jupyterhub_config.db_url, ) self.add_component("auth_jupyterhub", type=_AuthJupyterHubComponent) - await super().start(ctx) + await super().start() diff --git a/plugins/contents/fps_contents/fileid.py b/plugins/contents/fps_contents/fileid.py index 2d815f81..720f99d6 100644 --- a/plugins/contents/fps_contents/fileid.py +++ b/plugins/contents/fps_contents/fileid.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from contextlib import AsyncExitStack from typing import Dict, List, Optional, Set @@ -41,6 +43,7 @@ def __init__(self, db_path: str = ".fileid.db"): self.initialized = Event() self.watchers = {} self.stop_watching_files = Event() + self.started_watching_files = Event() self.stopped_watching_files = Event() self.lock = Lock() self._task_group = None @@ -65,19 +68,27 @@ async def __aexit__(self, exc_type, exc_value, exc_tb): return await self._exit_stack.__aexit__(exc_type, exc_value, exc_tb) async def start(self) -> None: - self._db = await connect(self.db_path) - await self.watch_files() + async def _start(): + self._db = await connect(self.db_path) + await self.watch_files() + + async with create_task_group() as self._task_group: + self._task_group.start_soon(_start) async def stop(self) -> None: + print("fileid stopping") + self._task_group.cancel_scope.cancel() await self._db.close() self.stop_watching_files.set() - await self.stopped_watching_files.wait() + if self.started_watching_files.is_set(): + await self.stopped_watching_files.wait() + print("fileid stopped") async def get_id(self, path: str) -> Optional[str]: await self.initialized.wait() async with self.lock: cursor = await self._db.cursor() - await cur.execute("SELECT id FROM fileids WHERE path = ?", (path,)) + await cursor.execute("SELECT id FROM fileids WHERE path = ?", (path,)) for (idx,) in await cursor.fetchall(): return idx return None @@ -127,6 +138,7 @@ async def watch_files(self): await self._db.commit() self.initialized.set() + self.started_watching_files.set() async for changes in awatch(".", stop_event=self.stop_watching_files): async with self.lock: deleted_paths = set() diff --git a/plugins/contents/fps_contents/main.py b/plugins/contents/fps_contents/main.py index d5f61d79..13b94f86 100644 --- a/plugins/contents/fps_contents/main.py +++ b/plugins/contents/fps_contents/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -9,8 +9,8 @@ class ContentsComponent(Component): async def start(self) -> None: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) contents = _Contents(app, auth) - await add_resource(contents, types=Contents) + add_resource(contents, types=Contents) diff --git a/plugins/frontend/fps_frontend/main.py b/plugins/frontend/fps_frontend/main.py index c68bb73a..1b502de3 100644 --- a/plugins/frontend/fps_frontend/main.py +++ b/plugins/frontend/fps_frontend/main.py @@ -8,4 +8,4 @@ def __init__(self, **kwargs): self.frontend_config = FrontendConfig(**kwargs) async def start(self) -> None: - await add_resource(self.frontend_config, types=FrontendConfig) + add_resource(self.frontend_config, types=FrontendConfig) diff --git a/plugins/jupyterlab/fps_jupyterlab/main.py b/plugins/jupyterlab/fps_jupyterlab/main.py index 9bc9b98a..e9ea1877 100644 --- a/plugins/jupyterlab/fps_jupyterlab/main.py +++ b/plugins/jupyterlab/fps_jupyterlab/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -14,12 +14,12 @@ def __init__(self, **kwargs): self.jupyterlab_config = JupyterLabConfig(**kwargs) async def start(self) -> None: - await add_resource(self.jupyterlab_config, types=JupyterLabConfig) + add_resource(self.jupyterlab_config, types=JupyterLabConfig) - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore - frontend_config = await request_resource(FrontendConfig) - lab = await request_resource(Lab) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) + frontend_config = await get_resource(FrontendConfig, wait=True) + lab = await get_resource(Lab, wait=True) jupyterlab = _JupyterLab(app, self.jupyterlab_config, auth, frontend_config, lab) - await add_resource(jupyterlab, types=JupyterLab) + add_resource(jupyterlab, types=JupyterLab) diff --git a/plugins/kernels/fps_kernels/kernel_driver/connect.py b/plugins/kernels/fps_kernels/kernel_driver/connect.py index 0f54955d..c8da60ab 100644 --- a/plugins/kernels/fps_kernels/kernel_driver/connect.py +++ b/plugins/kernels/fps_kernels/kernel_driver/connect.py @@ -89,6 +89,8 @@ async def launch_kernel( else: stdout = None stderr = None + if not kernel_cwd: + kernel_cwd = None process = await open_process(cmd, stdout=stdout, stderr=stderr, cwd=kernel_cwd) return process diff --git a/plugins/kernels/fps_kernels/kernel_driver/driver.py b/plugins/kernels/fps_kernels/kernel_driver/driver.py index 8b190f47..5696eac3 100644 --- a/plugins/kernels/fps_kernels/kernel_driver/driver.py +++ b/plugins/kernels/fps_kernels/kernel_driver/driver.py @@ -3,7 +3,14 @@ import uuid from typing import Any, Dict, Optional, cast -from anyio import create_memory_object_stream, create_task_group, fail_after +from anyio import ( + TASK_STATUS_IGNORED, + Event, + create_memory_object_stream, + create_task_group, + fail_after, +) +from anyio.abc import TaskStatus from anyio.streams.stapled import StapledObjectStream from pycrdt import Array, Map @@ -45,7 +52,9 @@ def __init__( self.session_id = uuid.uuid4().hex self.msg_cnt = 0 self.execute_requests: Dict[str, Dict[str, StapledObjectStream]] = {} - self.comm_messages: StapledObjectStream = StapledObjectStream(create_memory_object_stream[dict](max_buffer_size=1024)) + self.comm_messages: StapledObjectStream = StapledObjectStream(*create_memory_object_stream[dict](max_buffer_size=1024)) + self.stop_event = Event() + self.stopped_event = Event() async def restart(self, startup_timeout: float = float("inf")) -> None: self.task_group.cancel_scope.cancel() @@ -61,7 +70,7 @@ async def restart(self, startup_timeout: float = float("inf")) -> None: async with create_task_group() as self.task_group: self.listen_channels() - async def start(self, startup_timeout: float = float("inf"), connect: bool = True) -> None: + async def start(self, startup_timeout: float = float("inf"), connect: bool = True, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED) -> None: async with create_task_group() as self.task_group: self.kernel_process = await launch_kernel( self.kernelspec_path, @@ -70,7 +79,10 @@ async def start(self, startup_timeout: float = float("inf"), connect: bool = Tru self.capture_kernel_output, ) if connect: - await self.connect(startup_timeout) + await self.connect() + task_status.started() + await self.stop_event.wait() + self.stopped_event.set() async def connect(self, startup_timeout: float = float("inf")) -> None: self.connect_channels() @@ -93,6 +105,8 @@ async def stop(self) -> None: await self.kernel_process.wait() await self.kernel_process.aclose() os.remove(self.connection_file_path) + self.stop_event.set() + await self.stopped_event.wait() self.task_group.cancel_scope.cancel() async def listen_iopub(self): @@ -132,8 +146,8 @@ async def execute( self.msg_cnt += 1 await send_message(msg, self.shell_channel, self.key, change_date_to_str=True) self.execute_requests[msg_id] = { - "iopub_msg": StapledObjectStream(create_memory_object_stream[dict](max_buffer_size=1024)), - "shell_msg": StapledObjectStream(create_memory_object_stream[dict](max_buffer_size=1024)), + "iopub_msg": StapledObjectStream(*create_memory_object_stream[dict](max_buffer_size=1024)), + "shell_msg": StapledObjectStream(*create_memory_object_stream[dict](max_buffer_size=1024)), } if wait_for_executed: deadline = time.time() + timeout diff --git a/plugins/kernels/fps_kernels/main.py b/plugins/kernels/fps_kernels/main.py index c1ebfc62..96a98c14 100644 --- a/plugins/kernels/fps_kernels/main.py +++ b/plugins/kernels/fps_kernels/main.py @@ -3,7 +3,7 @@ from collections.abc import AsyncGenerator from typing import Optional -from asphalt.core import Component, add_resource, request_resource, start_background_task +from asphalt.core import Component, add_resource, get_resource, start_service_task from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -19,17 +19,17 @@ def __init__(self, **kwargs): self.kernels_config = KernelsConfig(**kwargs) async def start(self) -> AsyncGenerator[None, Optional[BaseException]]: - await add_resource(self.kernels_config, types=KernelsConfig) + add_resource(self.kernels_config, types=KernelsConfig) - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore - frontend_config = await request_resource(FrontendConfig) + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) + frontend_config = await get_resource(FrontendConfig, wait=True) yjs = ( - await request_resource(Yjs) # type: ignore + await get_resource(Yjs, wait=True) if self.kernels_config.require_yjs else None ) kernels = _Kernels(app, self.kernels_config, auth, frontend_config, yjs) - await start_background_task(kernels.start, name="Kernels", teardown_action=kernels.stop) - await add_resource(kernels, types=Kernels) + await start_service_task(kernels.start, name="Kernels", teardown_action=kernels.stop) + add_resource(kernels, types=Kernels) diff --git a/plugins/kernels/fps_kernels/routes.py b/plugins/kernels/fps_kernels/routes.py index ecd92463..2a848389 100644 --- a/plugins/kernels/fps_kernels/routes.py +++ b/plugins/kernels/fps_kernels/routes.py @@ -67,12 +67,16 @@ async def start(self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED) -> await self.stop_event.wait() async def stop(self) -> None: + print("kernels stopping") for kernel in self.kernels.values(): await kernel["server"].stop() + if kernel["driver"] is not None: + await kernel["driver"].stop() if self.kernels_config.allow_external_kernels: self.stop_watching_files.set() await self.stopped_watching_files.wait() self.stop_event.set() + print("kernels stopped") async def get_status( self, @@ -300,7 +304,7 @@ async def execute_cell( connection_file=kernel["server"].connection_file_path, yjs=self.yjs, ) - await driver.connect() + await self.task_group.start(driver.start) driver = kernel["driver"] await driver.execute(ycell, wait_for_executed=False) diff --git a/plugins/lab/fps_lab/main.py b/plugins/lab/fps_lab/main.py index dccf669c..d73666c0 100644 --- a/plugins/lab/fps_lab/main.py +++ b/plugins/lab/fps_lab/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, get_resource, request_resource +from asphalt.core import Component, add_resource, get_resource, get_resource_nowait from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -11,10 +11,10 @@ class LabComponent(Component): async def start(self) -> None: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore - frontend_config = await request_resource(FrontendConfig) - jupyterlab_config = get_resource(JupyterLabConfig) + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) + frontend_config = await get_resource(FrontendConfig, wait=True) + jupyterlab_config = get_resource_nowait(JupyterLabConfig) lab = _Lab(app, auth, frontend_config, jupyterlab_config) - await add_resource(lab, types=Lab) + add_resource(lab, types=Lab) diff --git a/plugins/lab/fps_lab/routes.py b/plugins/lab/fps_lab/routes.py index 3f68f844..11667a35 100644 --- a/plugins/lab/fps_lab/routes.py +++ b/plugins/lab/fps_lab/routes.py @@ -7,6 +7,11 @@ from pathlib import Path from typing import List, Optional, Tuple +if sys.version_info < (3, 10): + from importlib_metadata import entry_points +else: + from importlib.metadata import entry_points + import json5 # type: ignore from babel import Locale from fastapi import Response, status diff --git a/plugins/login/fps_login/main.py b/plugins/login/fps_login/main.py index c484f105..95550e46 100644 --- a/plugins/login/fps_login/main.py +++ b/plugins/login/fps_login/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import AuthConfig @@ -9,8 +9,8 @@ class LoginComponent(Component): async def start(self) -> None: - app = await request_resource(App) - auth_config = await request_resource(AuthConfig) + app = await get_resource(App, wait=True) + auth_config = await get_resource(AuthConfig, wait=True) login = _Login(app, auth_config) - await add_resource(login, types=Login) + add_resource(login, types=Login) diff --git a/plugins/nbconvert/fps_nbconvert/main.py b/plugins/nbconvert/fps_nbconvert/main.py index 0d9f3c9d..9b498fd0 100644 --- a/plugins/nbconvert/fps_nbconvert/main.py +++ b/plugins/nbconvert/fps_nbconvert/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -9,8 +9,8 @@ class NbconvertComponent(Component): async def start(self) -> None: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) nbconvert = _Nbconvert(app, auth) - await add_resource(nbconvert, types=Nbconvert) + add_resource(nbconvert, types=Nbconvert) diff --git a/plugins/noauth/fps_noauth/main.py b/plugins/noauth/fps_noauth/main.py index 5bdbff00..92bf0846 100644 --- a/plugins/noauth/fps_noauth/main.py +++ b/plugins/noauth/fps_noauth/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, Context +from asphalt.core import Component, add_resource from jupyverse_api.auth import Auth @@ -6,9 +6,6 @@ class NoAuthComponent(Component): - async def start( - self, - ctx: Context, - ) -> None: + async def start(self) -> None: no_auth = _NoAuth() - await ctx.add_resource(no_auth, types=Auth) + add_resource(no_auth, types=Auth) diff --git a/plugins/notebook/fps_notebook/main.py b/plugins/notebook/fps_notebook/main.py index a540ea90..349f2c42 100644 --- a/plugins/notebook/fps_notebook/main.py +++ b/plugins/notebook/fps_notebook/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, Context +from asphalt.core import Component, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -10,14 +10,11 @@ class NotebookComponent(Component): - async def start( - self, - ctx: Context, - ) -> None: - app = await ctx.request_resource(App) - auth = await ctx.request_resource(Auth) # type: ignore - frontend_config = await ctx.request_resource(FrontendConfig) - lab = await ctx.request_resource(Lab) # type: ignore + async def start(self) -> None: + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) + frontend_config = await get_resource(FrontendConfig, wait=True) + lab = await get_resource(Lab, wait=True) notebook = _Notebook(app, auth, frontend_config, lab) - await ctx.add_resource(notebook, types=Notebook) + add_resource(notebook, types=Notebook) diff --git a/plugins/resource_usage/fps_resource_usage/main.py b/plugins/resource_usage/fps_resource_usage/main.py index 4eb3eab3..9163ceef 100644 --- a/plugins/resource_usage/fps_resource_usage/main.py +++ b/plugins/resource_usage/fps_resource_usage/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -12,8 +12,8 @@ def __init__(self, **kwargs): self.resource_usage_config = ResourceUsageConfig(**kwargs) async def start(self) -> None: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) resource_usage = _ResourceUsage(app, auth, self.resource_usage_config) - await add_resource(resource_usage, types=ResourceUsage) + add_resource(resource_usage, types=ResourceUsage) diff --git a/plugins/terminals/fps_terminals/main.py b/plugins/terminals/fps_terminals/main.py index 782fc84b..31b6fb45 100644 --- a/plugins/terminals/fps_terminals/main.py +++ b/plugins/terminals/fps_terminals/main.py @@ -1,7 +1,7 @@ import os from typing import Type -from asphalt.core import Component, add_resource, request_resource, start_background_task +from asphalt.core import Component, add_resource, get_resource, start_service_task from jupyverse_api.app import App from jupyverse_api.auth import Auth @@ -18,9 +18,9 @@ class TerminalsComponent(Component): async def start(self) -> None: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) terminals = _Terminals(app, auth, _TerminalServer) - await start_background_task(terminals.start, name="Terminals", teardown_action=terminals.stop) - await add_resource(terminals, types=Terminals) + await start_service_task(terminals.start, name="Terminals", teardown_action=terminals.stop) + add_resource(terminals, types=Terminals) diff --git a/plugins/terminals/fps_terminals/routes.py b/plugins/terminals/fps_terminals/routes.py index 40ba006f..35415899 100644 --- a/plugins/terminals/fps_terminals/routes.py +++ b/plugins/terminals/fps_terminals/routes.py @@ -22,10 +22,11 @@ async def start(self): await self.stopped.wait() async def stop(self): - print(f"stopping {TERMINALS}") + print("terminals stopping") for terminal in TERMINALS.values(): await terminal["server"].stop() self.stopped.set() + print("terminals stopped") async def get_terminals( self, diff --git a/plugins/terminals/fps_terminals/server.py b/plugins/terminals/fps_terminals/server.py index e8ca9dcc..ac46089e 100644 --- a/plugins/terminals/fps_terminals/server.py +++ b/plugins/terminals/fps_terminals/server.py @@ -41,7 +41,10 @@ def quit(self, websocket): async def stop(self) -> None: os.write(self.recv_stream.pipeout, b"0") self.p_out.close() - self.recv_stream.sel.unregister(self.p_out) + try: + self.recv_stream.sel.unregister(self.p_out) + except Exception: + pass async def serve(self, websocket, permissions): self.websocket = websocket diff --git a/plugins/webdav/fps_webdav/main.py b/plugins/webdav/fps_webdav/main.py index f340e7ab..7da847bd 100644 --- a/plugins/webdav/fps_webdav/main.py +++ b/plugins/webdav/fps_webdav/main.py @@ -1,4 +1,4 @@ -from asphalt.core import Component, add_resource, request_resource +from asphalt.core import Component, add_resource, get_resource from jupyverse_api.app import App @@ -11,7 +11,7 @@ def __init__(self, **kwargs): self.webdav_config = WebDAVConfig(**kwargs) async def start(self) -> None: - app = await request_resource(App) + app = await get_resource(App, wait=True) webdav = WebDAV(app, self.webdav_config) - await add_resource(webdav) + add_resource(webdav) diff --git a/plugins/webdav/tests/test_webdav.py b/plugins/webdav/tests/test_webdav.py index dc6ed856..9a172650 100644 --- a/plugins/webdav/tests/test_webdav.py +++ b/plugins/webdav/tests/test_webdav.py @@ -30,11 +30,11 @@ async def test_webdav(unused_tcp_port): components = configure( COMPONENTS, {"webdav": {"account_mapping": [{"username": "foo", "password": "bar"}]}} ) - async with Context() as ctx: + async with Context(): await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() webdav = easywebdav.connect( "127.0.0.1", port=unused_tcp_port, path="webdav", username="foo", password="bar" diff --git a/plugins/yjs/fps_yjs/main.py b/plugins/yjs/fps_yjs/main.py index de8eae2b..3b508169 100644 --- a/plugins/yjs/fps_yjs/main.py +++ b/plugins/yjs/fps_yjs/main.py @@ -3,11 +3,12 @@ from collections.abc import AsyncGenerator from typing import Optional -from asphalt.core import Component, add_resource, request_resource, start_background_task +from asphalt.core import Component, add_resource, get_resource, start_service_task from jupyverse_api.app import App from jupyverse_api.auth import Auth from jupyverse_api.contents import Contents +from jupyverse_api.main import Lifespan from jupyverse_api.yjs import Yjs from .routes import _Yjs @@ -15,12 +16,13 @@ class YjsComponent(Component): async def start(self) -> AsyncGenerator[None, Optional[BaseException]]: - app = await request_resource(App) - auth = await request_resource(Auth) # type: ignore - contents = await request_resource(Contents) # type: ignore + app = await get_resource(App, wait=True) + auth = await get_resource(Auth, wait=True) + contents = await get_resource(Contents, wait=True) + lifespan = await get_resource(Lifespan, wait=True) - yjs = _Yjs(app, auth, contents) - await add_resource(yjs, types=Yjs) + yjs = _Yjs(app, auth, contents, lifespan) + add_resource(yjs, types=Yjs) - await start_background_task(yjs.room_manager.start, "Room manager", teardown_action=yjs.room_manager.stop) - await start_background_task(contents.file_id_manager.start, "File ID manager", teardown_action=contents.file_id_manager.stop) + await start_service_task(yjs.room_manager.start, "Room manager", teardown_action=yjs.room_manager.stop) + await start_service_task(contents.file_id_manager.start, "File ID manager", teardown_action=contents.file_id_manager.stop) diff --git a/plugins/yjs/fps_yjs/routes.py b/plugins/yjs/fps_yjs/routes.py index 5561e617..84c828c4 100644 --- a/plugins/yjs/fps_yjs/routes.py +++ b/plugins/yjs/fps_yjs/routes.py @@ -21,6 +21,7 @@ from jupyverse_api.app import App from jupyverse_api.auth import Auth, User from jupyverse_api.contents import Contents +from jupyverse_api.main import Lifespan from jupyverse_api.yjs import Yjs from jupyverse_api.yjs.models import CreateDocumentSession @@ -47,10 +48,12 @@ def __init__( app: App, auth: Auth, contents: Contents, + lifespan: Lifespan, ) -> None: super().__init__(app=app, auth=auth) self.contents = contents - self.room_manager = RoomManager(contents) + self.lifespan = lifespan + self.room_manager = RoomManager(contents, lifespan) if Widgets is None: self.widgets = None else: @@ -66,7 +69,8 @@ async def collaboration_room_websocket( websocket, permissions = websocket_permissions await websocket.accept() ywebsocket = YWebsocket(websocket, path) - await self.room_manager.serve(ywebsocket, permissions) + async with create_task_group() as tg: + task = Task(self.room_manager.serve(ywebsocket, permissions), tg, self.lifespan.shutdown_request) async def create_roomid( self, @@ -141,6 +145,7 @@ async def recv(self): class RoomManager: contents: Contents + lifespan: Lifespan documents: Dict[str, YBaseDoc] watchers: Dict[str, Task] savers: Dict[str, Task] @@ -150,8 +155,9 @@ class RoomManager: lock: Lock _task_group: TaskGroup - def __init__(self, contents: Contents): + def __init__(self, contents: Contents, lifespan: Lifespan): self.contents = contents + self.lifespan = lifespan self.documents = {} # a dictionary of room_name:document self.watchers = {} # a dictionary of file_id:task self.savers = {} # a dictionary of file_id:task @@ -160,9 +166,17 @@ def __init__(self, contents: Contents): self.websocket_server = JupyterWebsocketServer(rooms_ready=False, auto_clean_rooms=False) self.lock = Lock() + async def on_shutdown(self): + await self.lifespan.shutdown_request.wait() + await self.websocket_server.stop() + print("on shutdown") + async def start(self): async with create_task_group() as self._task_group: await self._task_group.start(self.websocket_server.start) + self._task_group.start_soon(self.on_shutdown) + print("websocket server started") + print("websocket server stopped") async def stop(self): #for watcher in self.watchers.values(): @@ -171,7 +185,9 @@ async def stop(self): # saver.cancel() #for cleaner in self.cleaners.values(): # cleaner.cancel() - await self.websocket_server.stop() + print("room manager stopping") + #await self.websocket_server.stop() + print("room manager stopped") async def serve(self, websocket: YWebsocket, permissions) -> None: room = await self.websocket_server.get_room(websocket.path) @@ -384,23 +400,28 @@ async def get_room(self, ws_path: str, ydoc: Doc | None = None) -> YRoom: class Task: - def __init__(self, coro, task_group: TaskGroup): + def __init__(self, coro, task_group: TaskGroup, cancel_event: Event | None = None): self._coro = coro - self._stop_event = Event() + self._cancel_event = cancel_event + self.cancelled = Event() + self.finished = Event() task_group.start_soon(self.run) def cancel(self): - self._stop_event.set() + self.cancelled.set() async def run(self): async with create_task_group() as tg: tg.start_soon(self._run, tg) - tg.start_soon(self._check_stop, tg) + tg.start_soon(self._check_cancellation, self.cancelled, tg) + if self._cancel_event is not None: + tg.start_soon(self._check_cancellation, self._cancel_event, tg) + self.finished.set() async def _run(self, tg: TaskGroup): await self._coro tg.cancel_scope.cancel() - async def _check_stop(self, tg: TaskGroup): - await self._stop_event.wait() + async def _check_cancellation(self, cancel_event, tg: TaskGroup): + await cancel_event.wait() tg.cancel_scope.cancel() diff --git a/pyproject.toml b/pyproject.toml index f39b8c8d..e7e702a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,10 +65,10 @@ docs = [ "mkdocs", "mkdocs-material" ] # to the dependencies section pre-install-commands = [ "pip install git+https://github.com/asphalt-framework/asphalt.git@5.0", - "pip install git+https://github.com/davidbrochart/asphalt-web.git@asphalt-v5", + "pip install git+https://github.com/asphalt-framework/asphalt-web.git@asphalt5", "pip install asgiref", "pip install fastapi", - "pip install uvicorn", + "pip install hypercorn", "pip install -e ./jupyverse_api", "pip install -e ./plugins/contents", @@ -93,6 +93,7 @@ matrix.frontend.scripts = [ { key = "typecheck1", value = "typecheck0 ./plugins/jupyterlab", if = ["jupyterlab"] }, { key = "typecheck1", value = "typecheck0 ./plugins/notebook", if = ["notebook"] }, ] + matrix.auth.post-install-commands = [ { value = "pip install -e ./plugins/noauth", if = ["noauth"] }, { value = "pip install -e ./plugins/auth -e ./plugins/login", if = ["auth"] }, @@ -187,15 +188,3 @@ python_packages = [ [tool.hatch.version] path = "jupyverse/__init__.py" - -[tool.pytest.ini_options] -asyncio_mode = "strict" - -[tool.pixi.project] -name = "" -channels = ["conda-forge"] -platforms = ["linux-64"] - -[tool.pixi.dependencies] -pip = ">=24.0,<25" -python = "<3.12" diff --git a/tests/conftest.py b/tests/conftest.py index 748a3e70..f1b91e03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,11 @@ import requests +@pytest.fixture +def anyio_backend(): + return "asyncio" + + @pytest.fixture() def cwd(): return Path(__file__).parents[1] diff --git a/tests/test_app.py b/tests/test_app.py index dfd97365..ff62fc0a 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,5 @@ import pytest -from asphalt.core import Context +from asphalt.core import Context, get_resource from fastapi import APIRouter from httpx import AsyncClient from utils import configure @@ -9,7 +9,7 @@ from jupyverse_api.main import JupyverseComponent -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize( "mount_path", ( @@ -20,13 +20,13 @@ async def test_mount_path(mount_path, unused_tcp_port): components = configure({"app": {"type": "app"}}, {"app": {"mount_path": mount_path}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() - app = await ctx.request_resource(App) + app = await get_resource(App, wait=True) router = APIRouter() @router.get("/") diff --git a/tests/test_auth.py b/tests/test_auth.py index e8a3b5ed..9e35fa25 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,5 +1,5 @@ import pytest -from asphalt.core import Context +from asphalt.core import Context, get_resource from httpx import AsyncClient from httpx_ws import WebSocketUpgradeError, aconnect_ws from utils import authenticate_client, configure @@ -19,13 +19,13 @@ } -@pytest.mark.asyncio +@pytest.mark.anyio async def test_kernel_channels_unauthenticated(unused_tcp_port): - async with Context() as ctx: + async with Context(): await JupyverseComponent( components=COMPONENTS, port=unused_tcp_port, - ).start(ctx) + ).start() with pytest.raises(WebSocketUpgradeError): async with aconnect_ws( @@ -34,13 +34,13 @@ async def test_kernel_channels_unauthenticated(unused_tcp_port): pass -@pytest.mark.asyncio +@pytest.mark.anyio async def test_kernel_channels_authenticated(unused_tcp_port): - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=COMPONENTS, port=unused_tcp_port, - ).start(ctx) + ).start() await authenticate_client(http, unused_tcp_port) async with aconnect_ws( @@ -50,15 +50,15 @@ async def test_kernel_channels_authenticated(unused_tcp_port): pass -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth", "token", "user")) async def test_root_auth(auth_mode, unused_tcp_port): components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() response = await http.get(f"http://127.0.0.1:{unused_tcp_port}/") if auth_mode == "noauth": @@ -70,31 +70,31 @@ async def test_root_auth(auth_mode, unused_tcp_port): assert response.headers["content-type"] == "application/json" -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) async def test_no_auth(auth_mode, unused_tcp_port): components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() response = await http.get(f"http://127.0.0.1:{unused_tcp_port}/lab") assert response.status_code == 200 -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("token",)) async def test_token_auth(auth_mode, unused_tcp_port): components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() - auth_config = await ctx.request_resource(AuthConfig) + auth_config = await get_resource(AuthConfig, wait=True) # no token provided, should not work response = await http.get(f"http://127.0.0.1:{unused_tcp_port}/") @@ -104,7 +104,7 @@ async def test_token_auth(auth_mode, unused_tcp_port): assert response.status_code == 302 -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("user",)) @pytest.mark.parametrize( "permissions", @@ -115,11 +115,11 @@ async def test_token_auth(auth_mode, unused_tcp_port): ) async def test_permissions(auth_mode, permissions, unused_tcp_port): components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() await authenticate_client(http, unused_tcp_port, permissions=permissions) response = await http.get(f"http://127.0.0.1:{unused_tcp_port}/auth/user/me") diff --git a/tests/test_contents.py b/tests/test_contents.py index b44a4aac..1262bd6a 100644 --- a/tests/test_contents.py +++ b/tests/test_contents.py @@ -16,7 +16,7 @@ } -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) async def test_tree(auth_mode, tmp_path, unused_tcp_port): prev_dir = os.getcwd() @@ -65,11 +65,11 @@ async def test_tree(auth_mode, tmp_path, unused_tcp_port): ) components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() response = await http.get( f"http://127.0.0.1:{unused_tcp_port}/api/contents", params={"content": 1} diff --git a/tests/test_execute.py b/tests/test_execute.py index d423f1a1..2c11d416 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -1,9 +1,9 @@ -import asyncio import os from functools import partial from pathlib import Path import pytest +from anyio import Event, create_task_group, sleep from asphalt.core import Context from fps_yjs.ydocs import ydocs from fps_yjs.ywebsocket import WebsocketProvider @@ -55,7 +55,7 @@ async def recv(self) -> bytes: return bytes(b) -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) async def test_execute(auth_mode, unused_tcp_port): url = f"http://127.0.0.1:{unused_tcp_port}" @@ -67,7 +67,7 @@ async def test_execute(auth_mode, unused_tcp_port): await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() ws_url = url.replace("http", "ws", 1) name = "notebook1.ipynb" @@ -98,7 +98,7 @@ async def test_execute(auth_mode, unused_tcp_port): def callback(aevent, events, event): events.append(event) aevent.set() - aevent = asyncio.Event() + aevent = Event() events = [] ynb.ydoc.observe_subdocs(partial(callback, aevent, events)) async with aconnect_ws( @@ -106,7 +106,7 @@ def callback(aevent, events, event): ) as websocket, WebsocketProvider(ynb.ydoc, Websocket(websocket, document_id)): # connect to the shared notebook document # wait for file to be loaded and Y model to be created in server and client - await asyncio.sleep(0.5) + await sleep(0.5) # execute notebook for cell_idx in range(2): response = await http.post( @@ -125,15 +125,15 @@ def callback(aevent, events, event): guid = event.added[0] if guid is not None: break - task = asyncio.create_task(connect_ywidget(ws_url, guid)) - response = await http.post( - f"{url}/api/kernels/{kernel_id}/execute", - json={ - "document_id": document_id, - "cell_id": ynb.ycells[2]["id"], - } - ) - await task + async with create_task_group() as tg: + tg.start_soon(connect_ywidget, ws_url, guid) + response = await http.post( + f"{url}/api/kernels/{kernel_id}/execute", + json={ + "document_id": document_id, + "cell_id": ynb.ycells[2]["id"], + } + ) async def connect_ywidget(ws_url, guid): @@ -141,7 +141,7 @@ async def connect_ywidget(ws_url, guid): async with aconnect_ws( f"{ws_url}/api/collaboration/room/ywidget:{guid}" ) as websocket, WebsocketProvider(ywidget_doc, Websocket(websocket, guid)): - await asyncio.sleep(0.5) + await sleep(0.5) attrs = Map() model_name = Text() ywidget_doc["_attrs"] = attrs diff --git a/tests/test_kernels.py b/tests/test_kernels.py index dba726b9..f6403483 100644 --- a/tests/test_kernels.py +++ b/tests/test_kernels.py @@ -51,11 +51,11 @@ async def test_kernel_messages(auth_mode, capfd, unused_tcp_port): } components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient(): + async with Context(), AsyncClient(): await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() # block msg_type_0 msg["header"]["msg_id"] = str(int(msg["header"]["msg_id"]) + 1) diff --git a/tests/test_server.py b/tests/test_server.py index bc2325d4..cc3a2131 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,10 +1,10 @@ -import asyncio import json from functools import partial from pathlib import Path import pytest import requests +from anyio import Event, create_task_group, sleep from fps_yjs.ydocs import ydocs from fps_yjs.ywebsocket import WebsocketProvider from pycrdt import Array, Doc, Map, Text @@ -47,7 +47,7 @@ def test_settings_persistence_get(start_jupyverse): assert response.status_code == 204 -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) @pytest.mark.parametrize("clear_users", (False,)) async def test_rest_api(start_jupyverse): @@ -87,7 +87,7 @@ async def test_rest_api(start_jupyverse): ) as websocket, WebsocketProvider(ydoc, websocket): # connect to the shared notebook document # wait for file to be loaded and Y model to be created in server and client - await asyncio.sleep(0.5) + await sleep(0.5) ydoc["cells"] = ycells = Array() # execute notebook for cell_idx in range(3): @@ -101,7 +101,7 @@ async def test_rest_api(start_jupyverse): ), ) # wait for Y model to be updated - await asyncio.sleep(0.5) + await sleep(0.5) # retrieve cells cells = json.loads(str(ycells)) assert cells[0]["outputs"] == [ @@ -125,7 +125,7 @@ async def test_rest_api(start_jupyverse): ] -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) @pytest.mark.parametrize("clear_users", (False,)) async def test_ywidgets(start_jupyverse): @@ -164,7 +164,7 @@ async def test_ywidgets(start_jupyverse): def callback(aevent, events, event): events.append(event) aevent.set() - aevent = asyncio.Event() + aevent = Event() events = [] ynb.ydoc.observe_subdocs(partial(callback, aevent, events)) async with connect( @@ -172,7 +172,7 @@ def callback(aevent, events, event): ) as websocket, WebsocketProvider(ynb.ydoc, websocket): # connect to the shared notebook document # wait for file to be loaded and Y model to be created in server and client - await asyncio.sleep(0.5) + await sleep(0.5) # execute notebook for cell_idx in range(2): response = requests.post( @@ -193,17 +193,17 @@ def callback(aevent, events, event): guid = event.added[0] if guid is not None: break - task = asyncio.create_task(connect_ywidget(ws_url, guid)) - response = requests.post( - f"{url}/api/kernels/{kernel_id}/execute", - data=json.dumps( - { - "document_id": document_id, - "cell_id": ynb.ycells[2]["id"], - } - ), - ) - await task + async with create_task_group() as tg: + tg.start_soon(connect_ywidget, ws_url, guid) + response = requests.post( + f"{url}/api/kernels/{kernel_id}/execute", + data=json.dumps( + { + "document_id": document_id, + "cell_id": ynb.ycells[2]["id"], + } + ), + ) async def connect_ywidget(ws_url, guid): @@ -211,7 +211,7 @@ async def connect_ywidget(ws_url, guid): async with connect( f"{ws_url}/api/collaboration/room/ywidget:{guid}" ) as websocket, WebsocketProvider(ywidget_doc, websocket): - await asyncio.sleep(0.5) + await sleep(0.5) attrs = Map() model_name = Text() ywidget_doc["_attrs"] = attrs diff --git a/tests/test_settings.py b/tests/test_settings.py index 03cb6a60..a30aa7ed 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -21,15 +21,15 @@ } -@pytest.mark.asyncio +@pytest.mark.anyio @pytest.mark.parametrize("auth_mode", ("noauth",)) async def test_settings(auth_mode, unused_tcp_port): components = configure(COMPONENTS, {"auth": {"mode": auth_mode}}) - async with Context() as ctx, AsyncClient() as http: + async with Context(), AsyncClient() as http: await JupyverseComponent( components=components, port=unused_tcp_port, - ).start(ctx) + ).start() # get previous theme response = await http.get(