diff --git a/doc/content/end-user-documentation/views.rst b/doc/content/end-user-documentation/views.rst index 8c96f60b..554e220d 100644 --- a/doc/content/end-user-documentation/views.rst +++ b/doc/content/end-user-documentation/views.rst @@ -341,6 +341,48 @@ JSON Responses } +Binary Responses +~~~~~~~~~~~~~~~~ + +.. note:: + + Binary responses are only available in non interactive views + +.. code-block:: python + + from lona import LonaView + + + class MyLonaView(LonaView): + def handle_request(self, request): + return { + 'content_type': 'application/pdf', + 'body': open('foo.pdf', 'rb').read(), + } + + +Custom Headers +~~~~~~~~~~~~~~ + +.. note:: + + Custom headers are only available in non interactive views + +.. code-block:: python + + from lona import LonaView + + + class MyLonaView(LonaView): + def handle_request(self, request): + return { + 'headers': { + 'foo': 'bar', + }, + 'text': 'foo', + } + + View Hooks ---------- @@ -1106,8 +1148,8 @@ Server.get_view_class\(route=None, import_string=None, url=None\) Only one argument can be set at a time. -Server.reverse\(url_name, \*\*url_args\) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Server.reverse\(route_name, \*\*url_args\) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Returns a routing reverse match as string. diff --git a/lona/html/node_list.py b/lona/html/node_list.py index 1373687f..02091ae9 100644 --- a/lona/html/node_list.py +++ b/lona/html/node_list.py @@ -65,6 +65,11 @@ def append(self, node): ], ) + def extend(self, nodes): + with self._node.lock: + for node in nodes: + self.append(node) + def remove(self, node): with self._node.lock: self._nodes.remove(node) @@ -113,6 +118,10 @@ def clear(self): payload=[], ) + def index(self, node): + with self._node.lock: + return self._nodes.index(node) + def __getitem__(self, index): with self._node.lock: return self._nodes[index] diff --git a/lona/html/widgets.py b/lona/html/widgets.py index 3d7a4def..8c12f29e 100644 --- a/lona/html/widgets.py +++ b/lona/html/widgets.py @@ -41,12 +41,18 @@ def insert(self, *args, **kwargs): def append(self, *args, **kwargs): return self.nodes.append(*args, **kwargs) + def extend(self, *args, **kwargs): + return self.nodes.extend(*args, **kwargs) + def remove(self, *args, **kwargs): return self.nodes.remove(*args, **kwargs) def clear(self, *args, **kwargs): return self.nodes.clear(*args, **kwargs) + def index(self, *args, **kwargs): + return self.nodes.index(*args, **kwargs) + def __getitem__(self, *args, **kwargs): return self.nodes.__getitem__(*args, **kwargs) diff --git a/lona/response_parser.py b/lona/response_parser.py index da7d2c28..0eb6dd57 100644 --- a/lona/response_parser.py +++ b/lona/response_parser.py @@ -43,7 +43,9 @@ def render_response_dict(self, raw_response_dict, view_name): response_dict = { 'status': 200, 'content_type': 'text/html', - 'text': '', + 'text': None, + 'body': None, + 'headers': None, 'file': '', 'redirect': '', 'http_redirect': '', @@ -51,6 +53,7 @@ def render_response_dict(self, raw_response_dict, view_name): key_words = { 'text', + 'body', 'redirect', 'http_redirect', 'template', diff --git a/lona/routing.py b/lona/routing.py index 41944861..04d18999 100644 --- a/lona/routing.py +++ b/lona/routing.py @@ -134,6 +134,15 @@ def clear_reverse_cache_info(self): # routes ################################################################## def add_route(self, route): + # check if route name already exists + if route.name: + for _route in self.routes: + if route.name == _route.name: + logger.warning( + "route name '%s' already exists", + route.name, + ) + self.routes.append(route) def add_routes(self, *routes): @@ -160,17 +169,17 @@ def resolve(self, *args, **kwargs): return self._resolve_lru_cache(*args, **kwargs) # reverse ################################################################# - def _reverse(self, name, *args, **kwargs): + def _reverse(self, route_name, *args, **kwargs): route = None - for i in self.routes: - if i.name == name: - route = i + for _route in self.routes: + if _route.name == route_name: + route = _route break if not route: - raise ValueError(f"no route named '{name}' found") + raise ValueError(f"no route named '{route_name}' found") if route.path: return route.path diff --git a/lona/server.py b/lona/server.py index a4a62f0d..2cf4886a 100644 --- a/lona/server.py +++ b/lona/server.py @@ -23,7 +23,6 @@ from lona.templating import TemplatingEngine from lona.imports import acquire as _acquire from lona.server_state import ServerState -from lona.shell.shell import embed_shell from lona.view_loader import ViewLoader from lona.connection import Connection from lona.settings import Settings @@ -305,14 +304,20 @@ def _render_response(self, response_dict): if response_dict['http_redirect']: return HTTPFound(response_dict['http_redirect']) + default_headers = { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + } + + headers = response_dict['headers'] or default_headers + response = Response( status=response_dict['status'], content_type=response_dict['content_type'], text=response_dict['text'], + body=response_dict['body'], + headers=headers, ) - response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' - return response # handle http requests #################################################### @@ -525,13 +530,6 @@ def state(self): return self._state # helper ################################################################## - def embed_shell(self, _locals=None): - if _locals is None: - _locals = {} - _locals['server'] = self - - return embed_shell(self, locals=_locals) - def get_running_views_count(self, *args, **kwargs): return self.view_runtime_controller.get_running_views_count( *args, diff --git a/lona/view.py b/lona/view.py index d8499add..6f6f605a 100644 --- a/lona/view.py +++ b/lona/view.py @@ -1,23 +1,18 @@ from __future__ import annotations from typing import TYPE_CHECKING, overload, TypeVar, Union, cast -from collections.abc import Awaitable, Iterator, Callable -from concurrent.futures import CancelledError +from collections.abc import Awaitable, Callable import threading -import warnings import asyncio from typing_extensions import Literal from jinja2.nodes import Template from lona.view_runtime import VIEW_RUNTIME_STATE, ViewRuntime -from lona.exceptions import ServerStop, UserAbort from lona.html.abstract_node import AbstractNode from lona.events.input_event import InputEvent from lona.static_files import StaticFile -from lona.shell.shell import embed_shell from lona.connection import Connection -from lona.errors import ClientError from lona.request import Request # avoid import cycles @@ -33,8 +28,6 @@ class LonaView: STATIC_FILES: list[StaticFile] = [] - _server: LonaServer # TODO: remove after 1.8 - def __init__( self, server: LonaServer, @@ -45,24 +38,6 @@ def __init__( self._view_runtime: ViewRuntime = view_runtime self._request: Request = request - # objects ################################################################# - @classmethod - def iter_objects(cls: type[V]) -> Iterator[V]: - # TODO: remove after 1.8 - - warnings.warn( - 'LonaView.iter_objects() will be removed in 1.8', - category=DeprecationWarning, - ) - - view_runtime_controller = cls._server.view_runtime_controller - - for view_runtime in view_runtime_controller.iter_view_runtimes(): - if view_runtime.view_class != cls: - continue - - yield view_runtime.view - # properties ############################################################## @property def server(self) -> LonaServer: @@ -315,14 +290,6 @@ def ping(self) -> Literal['pong']: return 'pong' - # helper ################################################################## - def embed_shell(self, _locals: None | dict = None) -> None: - if _locals is None: - _locals = {} - _locals['self'] = self - - embed_shell(server=self.server, locals=_locals) - # hooks ################################################################### def handle_request(self, request: Request) -> None | str | AbstractNode | dict: # NOQA: LN001 return '' @@ -344,21 +311,6 @@ def handle_input_event( def on_view_event(self, view_event: 'ViewEvent') -> dict | None: pass - def on_shutdown( - self, - reason: Union[ - None, - UserAbort, - ServerStop, - CancelledError, - ClientError, - Exception, - ], - ) -> None: - # TODO: remove after 1.8 - - pass - def on_stop(self, reason: Exception | None) -> None: pass diff --git a/lona/view_loader.py b/lona/view_loader.py index ff63bf19..95484bcb 100644 --- a/lona/view_loader.py +++ b/lona/view_loader.py @@ -1,7 +1,6 @@ from __future__ import annotations from typing import Type, cast -import warnings import logging import inspect import asyncio @@ -49,7 +48,6 @@ def _run_checks(self, route: Route, view: type[LonaView]) -> None: 'handle_input_event_root', 'handle_input_event', 'on_view_event', - 'on_shutdown', 'on_stop', 'on_cleanup', ] @@ -66,15 +64,6 @@ def _run_checks(self, route: Route, view: type[LonaView]) -> None: hook_name, ) - # TODO: remove after 1.8 - if(isinstance(view, type) and - view.on_shutdown is not LonaView.on_shutdown): - - warnings.warn( - 'LonaView.on_shutdown() will be removed in 1.8', - category=DeprecationWarning, - ) - def _generate_acquiring_error_view( self, exception: Exception, @@ -108,10 +97,6 @@ def _cache_view( view_class = self._acquire(view) - # TODO: remove after 1.8 - if isinstance(view_class, type) and issubclass(view_class, LonaView): - view_class._server = self.server - if route: self._run_checks(route, view_class) diff --git a/lona/view_runtime.py b/lona/view_runtime.py index 5fe5814b..be30b5e0 100644 --- a/lona/view_runtime.py +++ b/lona/view_runtime.py @@ -236,29 +236,6 @@ def run_middlewares(self, connection, window_id, url): ) # start and stop ########################################################## - def run_shutdown_hook(self): - # TODO: remove after 1.8 - - logger.debug( - 'running %s with stop reason %s', - self.view.on_shutdown, - self.stop_reason, - ) - - stop_reason = self.stop_reason - - if not isinstance(stop_reason, (ServerStop, CancelledError)): - stop_reason = None - - try: - self.view.on_shutdown(stop_reason) - - except Exception: - logger.exception( - 'Exception raised while running %s', - self.view.on_shutdown, - ) - def run_stop_hook(self): logger.debug( 'running %s with stop reason %s', @@ -336,10 +313,12 @@ def start(self): if(self.route and self.route.interactive and isinstance(raw_response_dict, dict) and ( 'json' in raw_response_dict or - 'file' in raw_response_dict)): + 'file' in raw_response_dict or + 'headers' in raw_response_dict or + 'body' in raw_response_dict)): raise RuntimeError( - 'JSON and file responses are only available in non-interactive mode', + 'JSON, binary and file responses and headers are only available in non-interactive mode', ) return self.handle_raw_response_dict(raw_response_dict) @@ -398,8 +377,6 @@ def start(self): self.send_view_stop() self.run_stop_hook() - self.run_shutdown_hook() - def stop(self, reason=UserAbort, clean_up=True): self.stop_reason = reason diff --git a/tests/test_view_iter_objects.py b/tests/test_view_iter_objects.py deleted file mode 100644 index 69d20278..00000000 --- a/tests/test_view_iter_objects.py +++ /dev/null @@ -1,95 +0,0 @@ -from lona.pytest import eventually -from lona import LonaView - - -async def test_view_iter_objects(lona_app_context): - """ - This test tests LonaView.iter_objects() with theese steps: - - - Creating two views - - Opening 3 tabs on view 1 - - Opening 2 tabs on view 2 - - Count view objects of view 1 and 2 and check view classes - - Closing 1 tab on view 1 - - Closing 1 tab on view 2 - - Count view objects of view 1 and 2 and check view classes - """ - - from playwright.async_api import async_playwright - - # setup views - class View1(LonaView): - pass - - class View2(LonaView): - pass - - def setup_app(app): - app.route('/view-1/')(View1) - app.route('/view-2/')(View2) - - context = await lona_app_context(setup_app) - - async with async_playwright() as playwright: - browser = await playwright.chromium.launch() - browser_context = await browser.new_context() - - # view 1 - view1_page1 = await browser_context.new_page() - view1_page2 = await browser_context.new_page() - view1_page3 = await browser_context.new_page() - - await view1_page1.goto(context.make_url('/view-1/')) - await view1_page2.goto(context.make_url('/view-1/')) - await view1_page3.goto(context.make_url('/view-1/')) - - # view 2 - view2_page1 = await browser_context.new_page() - view2_page2 = await browser_context.new_page() - - await view2_page1.goto(context.make_url('/view-2/')) - await view2_page2.goto(context.make_url('/view-2/')) - - # count view objects - for attempt in eventually(): - async with attempt: - - # view 1 - view_objects = list(View1.iter_objects()) - - assert len(view_objects) == 3 - - for view_object in view_objects: - assert view_object.__class__ == View1 - - # view 2 - view_objects = list(View2.iter_objects()) - - assert len(view_objects) == 2 - - for view_object in view_objects: - assert view_object.__class__ == View2 - - # close two tabs - await view1_page3.goto(context.make_url('/')) - await view2_page2.goto(context.make_url('/')) - - # recount view objects - for attempt in eventually(): - async with attempt: - - # view 1 - view_objects = list(View1.iter_objects()) - - assert len(view_objects) == 2 - - for view_object in view_objects: - assert view_object.__class__ == View1 - - # view 2 - view_objects = list(View2.iter_objects()) - - assert len(view_objects) == 1 - - for view_object in view_objects: - assert view_object.__class__ == View2