From 2c113f5267e410e88ce9879091398c9495bd6258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Thu, 5 Dec 2024 15:56:11 +0100 Subject: [PATCH] fix: type tests --- README.rst | 9 +++- setup.py | 1 + tutordash/server/app.py | 94 +++++++++++++++++++++++++---------------- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/README.rst b/README.rst index 7f438b4..a5058e2 100644 --- a/README.rst +++ b/README.rst @@ -21,13 +21,18 @@ Usage Development *********** +Install locally:: + + pip install -e .[dev] + Install npm requirements:: npm clean-install -Compile SCSS files and watch for changes:: +Compile SCSS files:: - make scss-watch + make scss # compile once + make scss-watch # compile and watch for changes Run a development server:: diff --git a/setup.py b/setup.py index d66d8a2..2f3b7dc 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ setup( extras_require={ "dev": [ "tutor[dev]>=18.0.0,<19.0.0", + "types-aiofiles" ] }, entry_points={"tutor.plugin.v1": ["dash = tutordash.plugin"]}, diff --git a/tutordash/server/app.py b/tutordash/server/app.py index 0d0c104..5328aca 100644 --- a/tutordash/server/app.py +++ b/tutordash/server/app.py @@ -1,5 +1,6 @@ import asyncio from contextlib import contextmanager +import logging import os import shlex import subprocess @@ -11,42 +12,54 @@ import aiofiles import tutor.env -from quart import Quart, render_template, request, websocket, redirect, url_for +from quart import ( + Quart, + render_template, + request, + websocket, + redirect, + url_for, +) +from quart.helpers import WerkzeugResponse from tutor.exceptions import TutorError from tutor import fmt, hooks from tutor.types import Config import tutor.utils from tutor.commands.cli import cli +logger = logging.getLogger(__name__) + class TutorProject: """ - TODO This big god class is not very elegant. + Provide access to the current Tutor project root and configuration. """ CONFIG: dict[str, t.Any] = {} ROOT: str = "" - @staticmethod - def installed_plugins() -> list[str]: - return sorted(set(hooks.Filters.PLUGINS_INSTALLED.iterate())) - - @staticmethod - def enabled_plugins() -> list[str]: - return sorted(set(hooks.Filters.PLUGINS_LOADED.iterate())) - - @hooks.Actions.CONFIG_LOADED.add() - @staticmethod - def _dash_update_tutor_config(config: Config) -> None: - TutorProject.CONFIG = config - - @hooks.Actions.PROJECT_ROOT_READY.add() - @staticmethod - def _dash_update_tutor_root(root: str) -> None: - TutorProject.ROOT = root + @classmethod + def connect(cls, root: str) -> None: + """ + Call whenever we are ready to connect to the Tutor hooks API. + """ + if not cls.ROOT: + # Hook up TutorProject with Tutor hooks -- just once + hooks.Actions.PROJECT_ROOT_READY.add()(cls._dash_on_project_root_ready) + hooks.Actions.CONFIG_LOADED.add()(cls._dash_on_config_loaded) + hooks.Actions.CORE_READY.do() # discover plugins + hooks.Actions.PROJECT_ROOT_READY.do(root) @classmethod - def tutor_stdout_path(cls): + def _dash_on_project_root_ready(cls, root: str) -> None: + cls.ROOT = root + + @classmethod + def _dash_on_config_loaded(cls, config: Config) -> None: + cls.CONFIG = config + + @classmethod + def tutor_stdout_path(cls) -> str: return tutor.env.data_path(cls.ROOT, "dash", "tutor.log") @@ -58,19 +71,21 @@ app = Quart( def run(root: str, **app_kwargs: t.Any) -> None: - hooks.Actions.CORE_READY.do() # discover plugins - hooks.Actions.PROJECT_ROOT_READY.do(root) + """ + Bootstrap the Quart app and run it. + """ + TutorProject.connect(root) app.logger.info("Dash tutor logs location: %s", TutorProject.tutor_stdout_path()) app.run(**app_kwargs) @app.get("/") -async def home(): +async def home() -> str: return await render_template("index.html") @app.get("/sidebar/plugins") -async def sidebar_plugins(): +async def sidebar_plugins() -> str: # TODO get rid of this view and render from home() return await render_template( "sidebar/_plugins.html", @@ -79,14 +94,14 @@ async def sidebar_plugins(): @app.get("/plugin/") -async def plugin(name: str): +async def plugin(name: str) -> str: # TODO check that plugin exists - is_enabled = name in TutorProject.enabled_plugins() + is_enabled = name in hooks.Filters.PLUGINS_LOADED.iterate() return await render_template("plugin.html", plugin_name=name, is_enabled=is_enabled) @app.post("/plugin//toggle") -async def toggle_plugin(name: str): +async def toggle_plugin(name: str) -> dict[str, str]: # TODO check plugin exists form = await request.form enabled = form.get("enabled") @@ -95,11 +110,14 @@ async def toggle_plugin(name: str): # as pydantic or a rest framework? return {} + # TODO actually toggle plugin + logger.info("Toggling plugin %s", name) + return {} @app.post("/tutor/cli") -async def tutor_cli(): +async def tutor_cli() -> WerkzeugResponse: # Run command asynchronously # TODO return 400 if thread is active # TODO parse command from JSON request body @@ -135,17 +153,19 @@ def run_tutor_cli(args: list[str]) -> None: ): try: # Call tutor command - cli(args) + cli(args) # pylint: disable=no-value-for-parameter except TutorError as e: with open(TutorProject.tutor_stdout_path(), "a", encoding="utf8") as stdout: stdout.write(e.args[0]) - except SystemExit as e: + except SystemExit: # TODO what to do with e.code? pass @contextmanager -def patch_objects(refs): +def patch_objects( + refs: list[tuple[object, str, t.Callable[[t.Any], t.Any]]] +) -> t.Iterator[None]: old_objects = [] for module, object_name, new_object in refs: # backup old object @@ -153,20 +173,20 @@ def patch_objects(refs): # override object setattr(module, object_name, new_object) try: - yield + yield None finally: # restore old objects for module, object_name, old_object in old_objects: setattr(module, object_name, old_object) -def click_echo(text, **kwargs): +def click_echo(text: str, **_kwargs: t.Any) -> None: with open(TutorProject.tutor_stdout_path(), "a", encoding="utf8") as stdout: stdout.write(text) stdout.write("\n") -def click_style(text, **kwargs): +def click_style(text: str, **_kwargs: t.Any) -> str: """ Strip ANSI colors @@ -195,12 +215,12 @@ def execute(*command: str) -> int: @app.get("/tutor/logs") -async def tutor_logs(): +async def tutor_logs() -> str: return await render_template("tutor_logs.html") @app.websocket("/tutor/logs/stream") -async def tutor_logs_stream(): +async def tutor_logs_stream() -> None: while True: async for content in stream_file(TutorProject.tutor_stdout_path()): try: @@ -211,7 +231,7 @@ async def tutor_logs_stream(): await asyncio.sleep(0.1) -async def stream_file(path: str) -> t.Iterator[str]: +async def stream_file(path: str) -> t.AsyncGenerator[str]: """ Async stream content from file.