From 2b08d632a9675bc76a4d996e63cbaaa5519fbdf2 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 00:01:49 -0700 Subject: [PATCH 1/8] add use_connection hook --- src/idom/__init__.py | 6 +- src/idom/backend/default.py | 12 +--- src/idom/backend/fastapi.py | 10 +-- src/idom/backend/flask.py | 76 ++++++++++----------- src/idom/backend/hooks.py | 26 ++++++++ src/idom/backend/sanic.py | 105 ++++++++++++++++-------------- src/idom/backend/starlette.py | 51 ++++++++------- src/idom/backend/tornado.py | 59 +++++++---------- src/idom/backend/types.py | 29 +++++++-- src/idom/types.py | 7 +- tests/test_backend/test_common.py | 45 +++++++++++-- 11 files changed, 248 insertions(+), 178 deletions(-) create mode 100644 src/idom/backend/hooks.py diff --git a/src/idom/__init__.py b/src/idom/__init__.py index bf47f17e9..afbc83eed 100644 --- a/src/idom/__init__.py +++ b/src/idom/__init__.py @@ -1,4 +1,5 @@ from . import backend, config, html, logging, sample, types, web +from .backend.hooks import use_connection, use_location, use_scope from .backend.utils import run from .core import hooks from .core.component import component @@ -25,6 +26,7 @@ __version__ = "0.40.2" # DO NOT MODIFY __all__ = [ + "backend", "component", "config", "create_context", @@ -38,16 +40,18 @@ "Ref", "run", "sample", - "backend", "Stop", "types", "use_callback", + "use_connection", "use_context", "use_debug_value", "use_effect", + "use_location", "use_memo", "use_reducer", "use_ref", + "use_scope", "use_state", "vdom", "web", diff --git a/src/idom/backend/default.py b/src/idom/backend/default.py index 0cd593388..1f8db0126 100644 --- a/src/idom/backend/default.py +++ b/src/idom/backend/default.py @@ -5,7 +5,7 @@ from idom.types import RootComponentConstructor -from .types import BackendImplementation, Location +from .types import BackendImplementation from .utils import all_implementations @@ -35,16 +35,6 @@ async def serve_development_app( ) -def use_scope() -> Any: - """Return the current ASGI/WSGI scope""" - return _default_implementation().use_scope() - - -def use_location() -> Location: - """Return the current route as a string""" - return _default_implementation().use_location() - - _DEFAULT_IMPLEMENTATION: BackendImplementation[Any] | None = None diff --git a/src/idom/backend/fastapi.py b/src/idom/backend/fastapi.py index cc8c84580..2e68d705a 100644 --- a/src/idom/backend/fastapi.py +++ b/src/idom/backend/fastapi.py @@ -8,16 +8,10 @@ serve_development_app = starlette.serve_development_app """Alias for :func:`idom.backend.starlette.serve_development_app`""" -# see: https://github.com/idom-team/flake8-idom-hooks/issues/12 -use_location = starlette.use_location # noqa: ROH101 +use_connection = starlette.use_connection """Alias for :func:`idom.backend.starlette.use_location`""" -# see: https://github.com/idom-team/flake8-idom-hooks/issues/12 -use_scope = starlette.use_scope # noqa: ROH101 -"""Alias for :func:`idom.backend.starlette.use_scope`""" - -# see: https://github.com/idom-team/flake8-idom-hooks/issues/12 -use_websocket = starlette.use_websocket # noqa: ROH101 +use_websocket = starlette.use_websocket """Alias for :func:`idom.backend.starlette.use_websocket`""" Options = starlette.Options diff --git a/src/idom/backend/flask.py b/src/idom/backend/flask.py index 55e20618b..9de22ba81 100644 --- a/src/idom/backend/flask.py +++ b/src/idom/backend/flask.py @@ -22,11 +22,13 @@ from flask_cors import CORS from flask_sock import Sock from simple_websocket import Server as WebSocket +from werkzeug.local import LocalProxy from werkzeug.serving import BaseWSGIServer, make_server import idom -from idom.backend.types import Location -from idom.core.hooks import Context, create_context, use_context +from idom.backend.hooks import ConnectionContext +from idom.backend.hooks import use_connection as _use_connection +from idom.backend.types import Connection, Location from idom.core.layout import LayoutEvent, LayoutUpdate from idom.core.serve import serve_json_patch from idom.core.types import ComponentType, RootComponentConstructor @@ -37,8 +39,6 @@ logger = logging.getLogger(__name__) -ConnectionContext: Context[Connection | None] = create_context(None) - def configure( app: Flask, component: RootComponentConstructor, options: Options | None = None @@ -107,45 +107,25 @@ def run_server() -> None: raise RuntimeError("Failed to shutdown server.") -def use_location() -> Location: - """Get the current route as a string""" - conn = use_connection() - search = conn.request.query_string.decode() - return Location(pathname="/" + conn.path, search="?" + search if search else "") - - -def use_scope() -> dict[str, Any]: - """Get the current WSGI environment""" - return use_request().environ +def use_websocket() -> WebSocket: + """A handle to the current websocket""" + return use_connection().carrier.websocket -def use_request() -> Request: +def use_request() -> LocalProxy[Request]: """Get the current ``Request``""" - return use_connection().request + return use_connection().carrier.request -def use_connection() -> Connection: +def use_connection() -> Connection[_FlaskCarrier]: """Get the current :class:`Connection`""" - connection = use_context(ConnectionContext) - if connection is None: - raise RuntimeError( # pragma: no cover - "No connection. Are you running with a Flask server?" + conn = _use_connection() + if not isinstance(conn.carrier, _FlaskCarrier): + raise TypeError( # pragma: no cover + f"Connection has unexpected carrier {conn.carrier}. " + "Are you running with a Flask server?" ) - return connection - - -@dataclass -class Connection: - """A simple wrapper for holding connection information""" - - request: Request - """The current request object""" - - websocket: WebSocket - """A handle to the current websocket""" - - path: str - """The current path being served""" + return conn @dataclass @@ -230,11 +210,20 @@ async def recv_coro() -> Any: return await async_recv_queue.get() async def main() -> None: + search = request.query_string.decode() await serve_json_patch( idom.Layout( ConnectionContext( - component, value=Connection(request, websocket, path) - ) + component, + value=Connection( + scope=request.environ, + location=Location( + pathname=f"/{path}", + search=f"?{search}" if search else "", + ), + carrier=_FlaskCarrier(request, websocket), + ), + ), ), send_coro, recv_coro, @@ -283,3 +272,14 @@ class _DispatcherThreadInfo(NamedTuple): dispatch_future: "asyncio.Future[Any]" thread_send_queue: "ThreadQueue[LayoutUpdate]" async_recv_queue: "AsyncQueue[LayoutEvent]" + + +@dataclass +class _FlaskCarrier: + """A simple wrapper for holding a Flask request and WebSocket""" + + request: Request + """The current request object""" + + websocket: WebSocket + """A handle to the current websocket""" diff --git a/src/idom/backend/hooks.py b/src/idom/backend/hooks.py new file mode 100644 index 000000000..c5b5d7c9a --- /dev/null +++ b/src/idom/backend/hooks.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from typing import Any, MutableMapping + +from idom.core.hooks import Context, create_context, use_context + +from .types import Connection, Location + + +# backend implementations should establish this context at the root of an app +ConnectionContext: Context[Connection[Any] | None] = create_context(None) + + +def use_connection() -> Connection[Any]: + conn = use_context(ConnectionContext) + if conn is None: + raise RuntimeError("No backend established a connection.") # pragma: no cover + return conn + + +def use_scope() -> MutableMapping[str, Any]: + return use_connection().scope + + +def use_location() -> Location: + return use_connection().location diff --git a/src/idom/backend/sanic.py b/src/idom/backend/sanic.py index aa0b45405..29b692344 100644 --- a/src/idom/backend/sanic.py +++ b/src/idom/backend/sanic.py @@ -4,18 +4,17 @@ import json import logging from dataclasses import dataclass -from typing import Any, Dict, Tuple, Union +from typing import Any, Dict, MutableMapping, Tuple, Union from urllib import parse as urllib_parse from uuid import uuid4 from sanic import Blueprint, Sanic, request, response from sanic.config import Config from sanic.models.asgi import ASGIScope +from sanic.server.websockets.connection import WebSocketConnection from sanic_cors import CORS -from websockets.legacy.protocol import WebSocketCommonProtocol -from idom.backend.types import Location -from idom.core.hooks import Context, create_context, use_context +from idom.backend.types import Connection, Location from idom.core.layout import Layout, LayoutEvent from idom.core.serve import ( RecvCoroutine, @@ -27,13 +26,13 @@ from idom.core.types import RootComponentConstructor from ._asgi import serve_development_asgi +from .hooks import ConnectionContext +from .hooks import use_connection as _use_connection from .utils import safe_client_build_dir_path, safe_web_modules_dir_path logger = logging.getLogger(__name__) -ConnectionContext: Context[Connection | None] = create_context(None) - def configure( app: Sanic, component: RootComponentConstructor, options: Options | None = None @@ -65,50 +64,25 @@ async def serve_development_app( await serve_development_asgi(app, host, port, started) -def use_location() -> Location: - """Get the current route as a string""" - conn = use_connection() - search = conn.request.query_string - return Location(pathname="/" + conn.path, search="?" + search if search else "") - - -def use_scope() -> ASGIScope: - """Get the current ASGI scope""" - app = use_request().app - try: - asgi_app = app._asgi_app - except AttributeError: # pragma: no cover - raise RuntimeError("No scope. Sanic may not be running with an ASGI server") - return asgi_app.transport.scope - - def use_request() -> request.Request: """Get the current ``Request``""" - return use_connection().request - - -def use_connection() -> Connection: - """Get the current :class:`Connection`""" - connection = use_context(ConnectionContext) - if connection is None: - raise RuntimeError( # pragma: no cover - "No connection. Are you running with a Sanic server?" - ) - return connection + return use_connection().carrier.request -@dataclass -class Connection: - """A simple wrapper for holding connection information""" +def use_websocket() -> WebSocketConnection: + """Get the current websocket""" + return use_connection().carrier.websocket - request: request.Request - """The current request object""" - websocket: WebSocketCommonProtocol - """A handle to the current websocket""" - - path: str - """The current path being served""" +def use_connection() -> Connection[_SanicCarrier]: + """Get the current :class:`Connection`""" + conn = _use_connection() + if not isinstance(conn.carrier, _SanicCarrier): + raise TypeError( # pragma: no cover + f"Connection has unexpected carrier {conn.carrier}. " + "Are you running with a Sanic server?" + ) + return conn @dataclass @@ -165,12 +139,36 @@ def _setup_single_view_dispatcher_route( blueprint: Blueprint, constructor: RootComponentConstructor ) -> None: async def model_stream( - request: request.Request, socket: WebSocketCommonProtocol, path: str = "" + request: request.Request, socket: WebSocketConnection, path: str = "" ) -> None: + app = request.app + try: + asgi_app = app._asgi_app + except AttributeError: # pragma: no cover + logger.warning("No scope. Sanic may not be running with an ASGI server") + scope: MutableMapping[str, Any] = {} + else: + scope = asgi_app.transport.scope + send, recv = _make_send_recv_callbacks(socket) - conn = Connection(request, socket, path) await serve_json_patch( - Layout(ConnectionContext(constructor(), value=conn)), + Layout( + ConnectionContext( + constructor(), + value=Connection( + scope=scope, + location=Location( + pathname=f"/{path}", + search=( + f"?{request.query_string}" + if request.query_string + else "" + ), + ), + carrier=_SanicCarrier(request, socket), + ), + ) + ), send, recv, ) @@ -180,7 +178,7 @@ async def model_stream( def _make_send_recv_callbacks( - socket: WebSocketCommonProtocol, + socket: WebSocketConnection, ) -> Tuple[SendCoroutine, RecvCoroutine]: async def sock_send(value: VdomJsonPatch) -> None: await socket.send(json.dumps(value)) @@ -192,3 +190,14 @@ async def sock_recv() -> LayoutEvent: return LayoutEvent(**json.loads(data)) return sock_send, sock_recv + + +@dataclass +class _SanicCarrier: + """A simple wrapper for holding connection information""" + + request: request.Request + """The current request object""" + + websocket: WebSocketConnection + """A handle to the current websocket""" diff --git a/src/idom/backend/starlette.py b/src/idom/backend/starlette.py index ebee12dd0..c5c622979 100644 --- a/src/idom/backend/starlette.py +++ b/src/idom/backend/starlette.py @@ -12,9 +12,9 @@ from starlette.types import Receive, Scope, Send from starlette.websockets import WebSocket, WebSocketDisconnect -from idom.backend.types import Location +from idom.backend.hooks import ConnectionContext +from idom.backend.types import Connection, Location from idom.config import IDOM_WEB_MODULES_DIR -from idom.core.hooks import Context, create_context, use_context from idom.core.layout import Layout, LayoutEvent from idom.core.serve import ( RecvCoroutine, @@ -25,13 +25,13 @@ from idom.core.types import RootComponentConstructor from ._asgi import serve_development_asgi +from .hooks import ConnectionContext +from .hooks import use_connection as _use_connection from .utils import CLIENT_BUILD_DIR, safe_client_build_dir_path logger = logging.getLogger(__name__) -WebSocketContext: Context[WebSocket | None] = create_context(None) - def configure( app: Starlette, @@ -68,27 +68,19 @@ async def serve_development_app( await serve_development_asgi(app, host, port, started) -def use_location() -> Location: - """Get the current route as a string""" - scope = use_scope() - pathname = "/" + scope["path_params"].get("path", "") - search = scope["query_string"].decode() - return Location(pathname, "?" + search if search else "") - - -def use_scope() -> Scope: - """Get the current ASGI scope dictionary""" - return use_websocket().scope - - def use_websocket() -> WebSocket: """Get the current WebSocket object""" - websocket = use_context(WebSocketContext) - if websocket is None: - raise RuntimeError( # pragma: no cover - "No websocket. Are you running with a Starllette server?" + return use_connection().carrier + + +def use_connection() -> Connection[WebSocket]: + conn = _use_connection() + if not isinstance(conn.carrier, WebSocket): + raise TypeError( # pragma: no cover + f"Connection has unexpected carrier {conn.carrier}. " + "Are you running with a Flask server?" ) - return websocket + return conn @dataclass @@ -154,9 +146,22 @@ def _setup_single_view_dispatcher_route( async def model_stream(socket: WebSocket) -> None: await socket.accept() send, recv = _make_send_recv_callbacks(socket) + + pathname = "/" + socket.scope["path_params"].get("path", "") + search = socket.scope["query_string"].decode() + try: await serve_json_patch( - Layout(WebSocketContext(constructor(), value=socket)), + Layout( + ConnectionContext( + constructor(), + value=Connection( + scope=socket.scope, + location=Location(pathname, f"?{search}" if search else ""), + carrier=socket, + ), + ) + ), send, recv, ) diff --git a/src/idom/backend/tornado.py b/src/idom/backend/tornado.py index febd4db3a..c70c07f51 100644 --- a/src/idom/backend/tornado.py +++ b/src/idom/backend/tornado.py @@ -16,19 +16,17 @@ from tornado.websocket import WebSocketHandler from tornado.wsgi import WSGIContainer -from idom.backend.types import Location +from idom.backend.types import Connection, Location from idom.config import IDOM_WEB_MODULES_DIR -from idom.core.hooks import Context, create_context, use_context from idom.core.layout import Layout, LayoutEvent from idom.core.serve import VdomJsonPatch, serve_json_patch from idom.core.types import ComponentConstructor +from .hooks import ConnectionContext +from .hooks import use_connection as _use_connection from .utils import CLIENT_BUILD_DIR, safe_client_build_dir_path -ConnectionContext: Context[Connection | None] = create_context(None) - - def configure( app: Application, component: ComponentConstructor, @@ -84,41 +82,19 @@ async def serve_development_app( await server.close_all_connections() -def use_location() -> Location: - """Get the current route as a string""" - conn = use_connection() - search = conn.request.query - return Location(pathname="/" + conn.path, search="?" + search if search else "") - - -def use_scope() -> dict[str, Any]: - """Get the current WSGI environment dictionary""" - return WSGIContainer.environ(use_request()) - - def use_request() -> HTTPServerRequest: """Get the current ``HTTPServerRequest``""" - return use_connection().request + return use_connection().carrier -def use_connection() -> Connection: - connection = use_context(ConnectionContext) - if connection is None: - raise RuntimeError( # pragma: no cover - "No connection. Are you running with a Tornado server?" +def use_connection() -> Connection[HTTPServerRequest]: + conn = _use_connection() + if not isinstance(conn.carrier, HTTPServerRequest): + raise TypeError( # pragma: no cover + f"Connection has unexpected carrier {conn.carrier}. " + "Are you running with a Flask server?" ) - return connection - - -@dataclass -class Connection: - """A simple wrapper for holding connection information""" - - request: HTTPServerRequest - """The current request object""" - - path: str - """The current path being served""" + return conn @dataclass @@ -217,7 +193,18 @@ async def recv() -> LayoutEvent: Layout( ConnectionContext( self._component_constructor(), - value=Connection(self.request, path), + value=Connection( + scope=WSGIContainer.environ(self.request), + location=Location( + pathname=f"/{path}", + search=( + f"?{self.request.query}" + if self.request.query + else "" + ), + ), + carrier=self.request, + ), ) ), send, diff --git a/src/idom/backend/types.py b/src/idom/backend/types.py index 8a793b4f1..efb2a9d3f 100644 --- a/src/idom/backend/types.py +++ b/src/idom/backend/types.py @@ -2,7 +2,7 @@ import asyncio from dataclasses import dataclass -from typing import Any, MutableMapping, TypeVar +from typing import Any, Generic, MutableMapping, TypeVar from typing_extensions import Protocol, runtime_checkable @@ -36,11 +36,25 @@ async def serve_development_app( ) -> None: """Run an application using a development server""" - def use_scope(self) -> MutableMapping[str, Any]: - """Get an ASGI scope or WSGI environment dictionary""" - def use_location(self) -> Location: - """Get the current location (URL)""" +_Carrier = TypeVar("_Carrier") + + +@dataclass +class Connection(Generic[_Carrier]): + """Represents a connection with a client""" + + scope: MutableMapping[str, Any] + """An ASGI scope or WSGI environment dictionary""" + + location: Location + """The current location (URL)""" + + carrier: _Carrier + """How the connection is mediated. For example, a request or websocket. + + This typically depends on the backend implementation. + """ @dataclass @@ -55,4 +69,7 @@ class Location: """the path of the URL for the location""" search: str = "" - """A search or query string - a '?' followed by the parameters of the URL.""" + """A search or query string - a '?' followed by the parameters of the URL. + + If there are no search parameters this should be an empty string + """ diff --git a/src/idom/types.py b/src/idom/types.py index 80670ea35..ecb5732b7 100644 --- a/src/idom/types.py +++ b/src/idom/types.py @@ -4,7 +4,8 @@ - :mod:`idom.backend.types` """ -from .backend.types import BackendImplementation, Location +from .backend.types import BackendImplementation, Connection, Location +from .core.component import Component from .core.hooks import Context from .core.types import ( ComponentConstructor, @@ -27,8 +28,11 @@ __all__ = [ + "BackendImplementation", + "Component", "ComponentConstructor", "ComponentType", + "Connection", "Context", "EventHandlerDict", "EventHandlerFunc", @@ -45,5 +49,4 @@ "VdomChildren", "VdomDict", "VdomJson", - "BackendImplementation", ] diff --git a/tests/test_backend/test_common.py b/tests/test_backend/test_common.py index cefeaa185..f2c66f08f 100644 --- a/tests/test_backend/test_common.py +++ b/tests/test_backend/test_common.py @@ -1,11 +1,11 @@ -from typing import MutableMapping +from typing import MutableMapping, get_type_hints import pytest import idom from idom import html from idom.backend import default as default_implementation -from idom.backend.types import Location +from idom.backend.types import Connection, Location from idom.backend.utils import all_implementations from idom.testing import BackendFixture, DisplayFixture, poll @@ -64,12 +64,26 @@ async def test_module_from_template(display: DisplayFixture): await display.page.wait_for_selector(".VictoryContainer") +async def test_use_connection(display: DisplayFixture): + conn = idom.Ref() + + @idom.component + def ShowScope(): + conn.current = idom.use_connection() + return html.pre({"id": "scope"}, str(conn.current)) + + await display.show(ShowScope) + + await display.page.wait_for_selector("#scope") + assert isinstance(conn.current, Connection) + + async def test_use_scope(display: DisplayFixture): scope = idom.Ref() @idom.component def ShowScope(): - scope.current = display.backend.implementation.use_scope() + scope.current = idom.use_scope() return html.pre({"id": "scope"}, str(scope.current)) await display.show(ShowScope) @@ -88,8 +102,8 @@ async def poll_location(): @idom.component def ShowRoute(): - location.current = display.backend.implementation.use_location() - return html.pre({"id": "scope"}, str(location.current)) + location.current = idom.use_location() + return html.pre(str(location.current)) await display.show(ShowRoute) @@ -105,3 +119,24 @@ def ShowRoute(): ]: await display.goto(loc.pathname + loc.search) await poll_location.until_equals(loc) + + +@pytest.mark.parametrize("hook_name", ["use_request", "use_websocket"]) +async def test_use_request(display: DisplayFixture, hook_name): + hook = getattr(display.backend.implementation, hook_name, None) + if hook is None: + pytest.skip(f"{display.backend.implementation} has no '{hook_name}' hook") + + hook_val = idom.Ref() + + @idom.component + def ShowRoute(): + hook_val.current = hook() + return html.pre({"id": "hook"}, str(hook_val.current)) + + await display.show(ShowRoute) + + await display.page.wait_for_selector("#hook") + + # we can't easily narrow this check + assert hook_val.current is not None From 9dfaec8bb26df012280d92783fc1822e789bda34 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 13:08:05 -0700 Subject: [PATCH 2/8] add changelog entry --- docs/source/about/changelog.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index b1f8c805e..2940723de 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -23,7 +23,18 @@ more info, see the :ref:`Contributor Guide `. Unreleased ---------- -No changes. +**Changed** + +- :pull:`823` - The hooks ``use_location`` and ``use_scope`` are no longer + implementation specific and are now available as top-level imports. Instead of each + backend defining these hooks, backends establish a ``ConnectionContext`` with this + information. + +**Added** + +- :pull:`823` - There is a new ``use_connection`` hook which returns a ``Connection`` + object. This ``Connection`` object contains a ``location`` and ``scope``, along with + a ``carrier`` which is unique to each backend implementation. v0.40.2 From 4b9836259cff899b4da5c50e43f2e7db784aa1bb Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 13:13:30 -0700 Subject: [PATCH 3/8] upgrade uvicorn --- requirements/pkg-extras.txt | 4 ++-- src/idom/backend/_asgi.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/pkg-extras.txt b/requirements/pkg-extras.txt index 2b2079706..7d7a59bdb 100644 --- a/requirements/pkg-extras.txt +++ b/requirements/pkg-extras.txt @@ -1,6 +1,6 @@ # extra=starlette starlette >=0.13.6 -uvicorn[standard] >=0.13.4 +uvicorn[standard] >=0.19.0 # extra=sanic sanic >=21 @@ -8,7 +8,7 @@ sanic-cors # extra=fastapi fastapi >=0.63.0 -uvicorn[standard] >=0.13.4 +uvicorn[standard] >=0.19.0 # extra=flask flask diff --git a/src/idom/backend/_asgi.py b/src/idom/backend/_asgi.py index 9e01a21e7..94eaa2b88 100644 --- a/src/idom/backend/_asgi.py +++ b/src/idom/backend/_asgi.py @@ -21,7 +21,7 @@ async def serve_development_asgi( host=host, port=port, loop="asyncio", - debug=True, + reload=True, ) ) From 1186f5f5674bd9512824af2dc5e5b8a89d2e3a6f Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 13:19:57 -0700 Subject: [PATCH 4/8] fix type annotation --- src/idom/backend/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idom/backend/flask.py b/src/idom/backend/flask.py index 9de22ba81..c1f20cd86 100644 --- a/src/idom/backend/flask.py +++ b/src/idom/backend/flask.py @@ -112,7 +112,7 @@ def use_websocket() -> WebSocket: return use_connection().carrier.websocket -def use_request() -> LocalProxy[Request]: +def use_request() -> Request: """Get the current ``Request``""" return use_connection().carrier.request From a35048fa325c98d10673b5c2e327d24c023fa7ff Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 19 Oct 2022 13:26:03 -0700 Subject: [PATCH 5/8] remove unused imports --- src/idom/backend/flask.py | 1 - src/idom/backend/sanic.py | 1 - tests/test_backend/test_common.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/idom/backend/flask.py b/src/idom/backend/flask.py index c1f20cd86..56db466b4 100644 --- a/src/idom/backend/flask.py +++ b/src/idom/backend/flask.py @@ -22,7 +22,6 @@ from flask_cors import CORS from flask_sock import Sock from simple_websocket import Server as WebSocket -from werkzeug.local import LocalProxy from werkzeug.serving import BaseWSGIServer, make_server import idom diff --git a/src/idom/backend/sanic.py b/src/idom/backend/sanic.py index 29b692344..1a58c5d1c 100644 --- a/src/idom/backend/sanic.py +++ b/src/idom/backend/sanic.py @@ -10,7 +10,6 @@ from sanic import Blueprint, Sanic, request, response from sanic.config import Config -from sanic.models.asgi import ASGIScope from sanic.server.websockets.connection import WebSocketConnection from sanic_cors import CORS diff --git a/tests/test_backend/test_common.py b/tests/test_backend/test_common.py index f2c66f08f..93a40081f 100644 --- a/tests/test_backend/test_common.py +++ b/tests/test_backend/test_common.py @@ -1,4 +1,4 @@ -from typing import MutableMapping, get_type_hints +from typing import MutableMapping import pytest From 7a3c50ff3b52662568b69a39a9af78e2d22b8439 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 29 Oct 2022 13:41:44 -0700 Subject: [PATCH 6/8] update docstring --- src/idom/backend/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idom/backend/types.py b/src/idom/backend/types.py index efb2a9d3f..7ecf43350 100644 --- a/src/idom/backend/types.py +++ b/src/idom/backend/types.py @@ -51,7 +51,7 @@ class Connection(Generic[_Carrier]): """The current location (URL)""" carrier: _Carrier - """How the connection is mediated. For example, a request or websocket. + """How the connection is mediated. For example, a websocket. This typically depends on the backend implementation. """ From 493c0683cbbaa717791573d4039627b4fdee4801 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sat, 29 Oct 2022 13:52:56 -0700 Subject: [PATCH 7/8] increase type delay --- tests/test_widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index b557c8f93..2f06cd533 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -86,13 +86,13 @@ def SomeComponent(): input_1 = await display.page.wait_for_selector("#i_1") input_2 = await display.page.wait_for_selector("#i_2") - await input_1.type("hello", delay=20) + await input_1.type("hello", delay=50) assert (await input_1.evaluate("e => e.value")) == "hello" assert (await input_2.evaluate("e => e.value")) == "hello" await input_2.focus() - await input_2.type(" world", delay=20) + await input_2.type(" world", delay=50) assert (await input_1.evaluate("e => e.value")) == "hello world" assert (await input_2.evaluate("e => e.value")) == "hello world" From 23f4eaedd28090f80d0900911d3ab96f2fbdf172 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Sun, 30 Oct 2022 12:05:49 -0700 Subject: [PATCH 8/8] configure default delay --- docs/source/_custom_js/package-lock.json | 6 +++--- tests/test_client.py | 3 ++- tests/test_core/test_events.py | 3 ++- tests/test_core/test_hooks.py | 3 ++- tests/test_widgets.py | 9 +++++---- tests/tooling/common.py | 2 ++ 6 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 tests/tooling/common.py diff --git a/docs/source/_custom_js/package-lock.json b/docs/source/_custom_js/package-lock.json index e6bf270c5..b648d4992 100644 --- a/docs/source/_custom_js/package-lock.json +++ b/docs/source/_custom_js/package-lock.json @@ -19,7 +19,7 @@ } }, "../../../src/client/packages/idom-client-react": { - "version": "0.39.0", + "version": "0.40.2", "integrity": "sha512-pIK5eNwFSHKXg7ClpASWFVKyZDYxz59MSFpVaX/OqJFkrJaAxBuhKGXNTMXmuyWOL5Iyvb/ErwwDRxQRzMNkfQ==", "license": "MIT", "dependencies": { @@ -27,7 +27,7 @@ "htm": "^3.0.3" }, "devDependencies": { - "jsdom": "16.3.0", + "jsdom": "16.5.0", "lodash": "^4.17.21", "prettier": "^2.5.1", "uvu": "^0.5.1" @@ -604,7 +604,7 @@ "requires": { "fast-json-patch": "^3.0.0-1", "htm": "^3.0.3", - "jsdom": "16.3.0", + "jsdom": "16.5.0", "lodash": "^4.17.21", "prettier": "^2.5.1", "uvu": "^0.5.1" diff --git a/tests/test_client.py b/tests/test_client.py index ff7d826e2..0e48e3390 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,6 +7,7 @@ import idom from idom.backend.utils import find_available_port from idom.testing import BackendFixture, DisplayFixture +from tests.tooling.common import DEFAULT_TYPE_DELAY JS_DIR = Path(__file__).parent / "js" @@ -121,6 +122,6 @@ async def handle_change(event): await display.show(SomeComponent) inp = await display.page.wait_for_selector("#test-input") - await inp.type("hello") + await inp.type("hello", delay=DEFAULT_TYPE_DELAY) assert (await inp.evaluate("node => node.value")) == "hello" diff --git a/tests/test_core/test_events.py b/tests/test_core/test_events.py index 9b8cd2ae0..89f1dfa4c 100644 --- a/tests/test_core/test_events.py +++ b/tests/test_core/test_events.py @@ -8,6 +8,7 @@ to_event_handler_function, ) from idom.testing import DisplayFixture, poll +from tests.tooling.common import DEFAULT_TYPE_DELAY def test_event_handler_repr(): @@ -155,7 +156,7 @@ async def on_key_down(value): await display.show(Input) inp = await display.page.wait_for_selector("#input") - await inp.type("hello") + await inp.type("hello", delay=DEFAULT_TYPE_DELAY) # the default action of updating the element's value did not take place assert (await inp.evaluate("node => node.value")) == "" diff --git a/tests/test_core/test_hooks.py b/tests/test_core/test_hooks.py index e0bcba2e7..78fce87a4 100644 --- a/tests/test_core/test_hooks.py +++ b/tests/test_core/test_hooks.py @@ -15,6 +15,7 @@ from idom.testing import DisplayFixture, HookCatcher, assert_idom_did_log, poll from idom.testing.logs import assert_idom_did_not_log from idom.utils import Ref +from tests.tooling.common import DEFAULT_TYPE_DELAY async def test_must_be_rendering_in_layout_to_use_hooks(): @@ -246,7 +247,7 @@ async def on_change(event): await display.show(Input) button = await display.page.wait_for_selector("#input") - await button.type("this is a test") + await button.type("this is a test", delay=DEFAULT_TYPE_DELAY) await display.page.wait_for_selector("#complete") assert message_ref.current == "this is a test" diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 2f06cd533..2df28c656 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -3,6 +3,7 @@ import idom from idom.testing import DisplayFixture, poll +from tests.tooling.common import DEFAULT_TYPE_DELAY HERE = Path(__file__).parent @@ -86,13 +87,13 @@ def SomeComponent(): input_1 = await display.page.wait_for_selector("#i_1") input_2 = await display.page.wait_for_selector("#i_2") - await input_1.type("hello", delay=50) + await input_1.type("hello", delay=DEFAULT_TYPE_DELAY) assert (await input_1.evaluate("e => e.value")) == "hello" assert (await input_2.evaluate("e => e.value")) == "hello" await input_2.focus() - await input_2.type(" world", delay=50) + await input_2.type(" world", delay=DEFAULT_TYPE_DELAY) assert (await input_1.evaluate("e => e.value")) == "hello world" assert (await input_2.evaluate("e => e.value")) == "hello world" @@ -114,14 +115,14 @@ def SomeComponent(): input_1 = await display.page.wait_for_selector("#i_1") input_2 = await display.page.wait_for_selector("#i_2") - await input_1.type("hello", delay=20) + await input_1.type("hello", delay=DEFAULT_TYPE_DELAY) poll_value = poll(lambda: value.current) await poll_value.until_equals("hello") await input_2.focus() - await input_2.type(" world", delay=20) + await input_2.type(" world", delay=DEFAULT_TYPE_DELAY) await poll_value.until_equals("hello world") diff --git a/tests/tooling/common.py b/tests/tooling/common.py new file mode 100644 index 000000000..c995eacde --- /dev/null +++ b/tests/tooling/common.py @@ -0,0 +1,2 @@ +# see: https://github.com/microsoft/playwright-python/issues/1614 +DEFAULT_TYPE_DELAY = 100 # miliseconds