diff --git a/aiohttp_devtools/logs.py b/aiohttp_devtools/logs.py
index 53683d26..3087d528 100644
--- a/aiohttp_devtools/logs.py
+++ b/aiohttp_devtools/logs.py
@@ -80,7 +80,7 @@ def formatMessage(self, record: logging.LogRecord) -> str:
def formatException(self, ei: _Ei) -> str:
sio = StringIO()
- traceback.print_exception(*ei, file=sio) # type: ignore[misc]
+ traceback.print_exception(*ei, file=sio)
stack = sio.getvalue()
sio.close()
if self.stream_is_tty and pyg_lexer:
diff --git a/aiohttp_devtools/runserver/serve.py b/aiohttp_devtools/runserver/serve.py
index 353bcce2..f5c36c2c 100644
--- a/aiohttp_devtools/runserver/serve.py
+++ b/aiohttp_devtools/runserver/serve.py
@@ -3,9 +3,10 @@
import json
import mimetypes
import sys
+import warnings
from errno import EADDRINUSE
from pathlib import Path
-from typing import Any, Iterator, Optional, NoReturn
+from typing import Any, Iterator, NoReturn, Optional, Set, Tuple
from aiohttp import WSMsgType, web
from aiohttp.hdrs import LAST_MODIFIED, CONTENT_LENGTH
@@ -23,19 +24,37 @@
from .log_handlers import AccessLogger
from .utils import MutableValue
+try:
+ from aiohttp_jinja2 import static_root_key
+except ImportError:
+ static_root_key = None
+
LIVE_RELOAD_HOST_SNIPPET = '\n\n'
LIVE_RELOAD_LOCAL_SNIPPET = b'\n\n'
HOST = '0.0.0.0'
+LIVERELOAD_SCRIPT = web.AppKey("LIVERELOAD_SCRIPT", bytes)
+STATIC_PATH = web.AppKey("STATIC_PATH", str)
+STATIC_URL = web.AppKey("STATIC_URL", str)
+WS = web.AppKey("WS", Set[Tuple[web.WebSocketResponse, str]])
+
def _set_static_url(app: web.Application, url: str) -> None:
- app["static_root_url"] = MutableValue(url)
+ if static_root_key is None: # TODO: Remove fallback
+ with warnings.catch_warnings():
+ app["static_root_url"] = MutableValue(url)
+ else:
+ app[static_root_key] = MutableValue(url)
for subapp in app._subapps:
_set_static_url(subapp, url)
def _change_static_url(app: web.Application, url: str) -> None:
- app["static_root_url"].change(url)
+ if static_root_key is None: # TODO: Remove fallback
+ with warnings.catch_warnings():
+ app["static_root_url"].change(url)
+ else:
+ app[static_root_key].change(url)
for subapp in app._subapps:
_change_static_url(subapp, url)
@@ -174,23 +193,20 @@ async def create_main_app(config: Config, app_factory: AppFactory) -> web.AppRun
modify_main_app(app, config)
await check_port_open(config.main_port)
- return web.AppRunner(app, access_log_class=AccessLogger)
+ return web.AppRunner(app, access_log_class=AccessLogger, shutdown_timeout=0.1)
async def start_main_app(runner: web.AppRunner, port: int) -> None:
await runner.setup()
- site = web.TCPSite(runner, host=HOST, port=port, shutdown_timeout=0.1)
+ site = web.TCPSite(runner, host=HOST, port=port)
await site.start()
-WS = 'websockets'
-
-
async def src_reload(app: web.Application, path: Optional[str] = None) -> int:
"""
prompt each connected browser to reload by sending websocket message.
- :param path: if supplied this must be a path relative to app['static_path'],
+ :param path: if supplied this must be a path relative to `static_path`,
eg. reload of a single file is only supported for static resources.
:return: number of sources reloaded
"""
@@ -200,7 +216,7 @@ async def src_reload(app: web.Application, path: Optional[str] = None) -> int:
is_html = None
if path:
- path = str(Path(app['static_url']) / Path(path).relative_to(app['static_path']))
+ path = str(Path(app[STATIC_URL]) / Path(path).relative_to(app[STATIC_PATH]))
is_html = mimetypes.guess_type(path)[0] == 'text/html'
reloads = 0
@@ -239,16 +255,15 @@ def create_auxiliary_app(
*, static_path: Optional[str], static_url: str = "/", livereload: bool = True,
browser_cache: bool = False) -> web.Application:
app = web.Application()
- app[WS] = set()
- app.update(
- static_path=static_path,
- static_url=static_url,
- )
+ ws: Set[Tuple[web.WebSocketResponse, str]] = set()
+ app[STATIC_PATH] = static_path or ""
+ app[STATIC_URL] = static_url
+ app[WS] = ws
app.on_shutdown.append(cleanup_aux_app)
if livereload:
lr_path = Path(__file__).resolve().parent / 'livereload.js'
- app['livereload_script'] = lr_path.read_bytes()
+ app[LIVERELOAD_SCRIPT] = lr_path.read_bytes()
app.router.add_route('GET', '/livereload.js', livereload_js)
app.router.add_route('GET', '/livereload', websocket_handler)
aux_logger.debug('enabling livereload on auxiliary app')
@@ -271,7 +286,7 @@ async def livereload_js(request: web.Request) -> web.Response:
if request.if_modified_since:
raise HTTPNotModified()
- lr_script = request.app['livereload_script']
+ lr_script = request.app[LIVERELOAD_SCRIPT]
return web.Response(body=lr_script, content_type='application/javascript',
headers={LAST_MODIFIED: 'Fri, 01 Jan 2016 00:00:00 GMT'})
diff --git a/aiohttp_devtools/runserver/watch.py b/aiohttp_devtools/runserver/watch.py
index be4e3a4a..fc8029af 100644
--- a/aiohttp_devtools/runserver/watch.py
+++ b/aiohttp_devtools/runserver/watch.py
@@ -14,7 +14,7 @@
from ..exceptions import AiohttpDevException
from ..logs import rs_dft_logger as logger
from .config import Config
-from .serve import WS, serve_main_app, src_reload
+from .serve import STATIC_PATH, WS, serve_main_app, src_reload
class WatchTask:
@@ -64,7 +64,7 @@ async def _run(self, live_checks: int = 150) -> None:
try:
self._start_dev_server()
- static_path = str(self._app['static_path'])
+ static_path = self._app[STATIC_PATH]
def is_static(changes: Iterable[Tuple[object, str]]) -> bool:
return all(str(c[1]).startswith(static_path) for c in changes)
diff --git a/requirements.txt b/requirements.txt
index c1777a07..163d153a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
aiohttp==3.8.5
+aiohttp-jinja2==1.6
click==8.1.7
coverage==7.3.2
devtools==0.12.2
diff --git a/setup.py b/setup.py
index 298ec0bf..b0ba738d 100644
--- a/setup.py
+++ b/setup.py
@@ -63,7 +63,7 @@
aiohttp-devtools=aiohttp_devtools.cli:cli
""",
install_requires=[
- 'aiohttp>=3.8.0',
+ 'aiohttp>=3.9',
'click>=6.6',
'devtools>=0.6',
'Pygments>=2.2.0',
diff --git a/tests/test_runserver_main.py b/tests/test_runserver_main.py
index e1581ef0..2a826c84 100644
--- a/tests/test_runserver_main.py
+++ b/tests/test_runserver_main.py
@@ -9,8 +9,8 @@
from aiohttp_devtools.runserver import runserver
from aiohttp_devtools.runserver.config import Config
-from aiohttp_devtools.runserver.serve import (create_auxiliary_app, create_main_app, modify_main_app, src_reload,
- start_main_app)
+from aiohttp_devtools.runserver.serve import (
+ WS, create_auxiliary_app, create_main_app, modify_main_app, src_reload, start_main_app)
from .conftest import SIMPLE_APP, forked
@@ -218,12 +218,12 @@ async def test_websocket_hello(aux_cli, smart_caplog):
async def test_websocket_info(aux_cli, event_loop):
- assert len(aux_cli.server.app['websockets']) == 0
+ assert len(aux_cli.server.app[WS]) == 0
ws = await aux_cli.session.ws_connect(aux_cli.make_url('/livereload'))
try:
await ws.send_json({'command': 'info', 'url': 'foobar', 'plugins': 'bang'})
await asyncio.sleep(0.05)
- assert len(aux_cli.server.app['websockets']) == 1
+ assert len(aux_cli.server.app[WS]) == 1
finally:
await ws.close()
diff --git a/tests/test_runserver_serve.py b/tests/test_runserver_serve.py
index dadeb7dc..31feb315 100644
--- a/tests/test_runserver_serve.py
+++ b/tests/test_runserver_serve.py
@@ -6,12 +6,15 @@
import pytest
from aiohttp.web import Application, Request, Response
+from aiohttp_jinja2 import static_root_key
from pytest_toolbox import mktree
from aiohttp_devtools.exceptions import AiohttpDevException
from aiohttp_devtools.runserver.config import Config
from aiohttp_devtools.runserver.log_handlers import fmt_size
-from aiohttp_devtools.runserver.serve import check_port_open, cleanup_aux_app, modify_main_app, src_reload
+from aiohttp_devtools.runserver.serve import (
+ STATIC_PATH, STATIC_URL, WS, check_port_open, cleanup_aux_app,
+ modify_main_app, src_reload)
from .conftest import SIMPLE_APP, create_future
@@ -33,11 +36,9 @@ async def test_aux_reload(smart_caplog):
aux_app = Application()
ws = MagicMock()
ws.send_str = MagicMock(return_value=create_future())
- aux_app.update(
- websockets=[(ws, '/foo/bar')],
- static_url='/static/',
- static_path='/path/to/static_files/'
- )
+ aux_app[STATIC_PATH] = "/path/to/static_files/"
+ aux_app[STATIC_URL] = "/static/"
+ aux_app[WS] = set(((ws, '/foo/bar'),)) # type: ignore[misc]
assert 1 == await src_reload(aux_app, '/path/to/static_files/the_file.js')
assert ws.send_str.call_count == 1
send_obj = json.loads(ws.send_str.call_args[0][0])
@@ -55,11 +56,9 @@ async def test_aux_reload_no_path():
aux_app = Application()
ws = MagicMock()
ws.send_str = MagicMock(return_value=create_future())
- aux_app.update(
- websockets=[(ws, '/foo/bar')],
- static_url='/static/',
- static_path='/path/to/static_files/'
- )
+ aux_app[STATIC_PATH] = "/path/to/static_files/"
+ aux_app[STATIC_URL] = "/static/"
+ aux_app[WS] = set(((ws, '/foo/bar'),)) # type: ignore[misc]
assert 1 == await src_reload(aux_app)
assert ws.send_str.call_count == 1
send_obj = json.loads(ws.send_str.call_args[0][0])
@@ -75,11 +74,9 @@ async def test_aux_reload_html_different():
aux_app = Application()
ws = MagicMock()
ws.send_str = MagicMock(return_value=create_future())
- aux_app.update(
- websockets=[(ws, '/foo/bar')],
- static_url='/static/',
- static_path='/path/to/static_files/'
- )
+ aux_app[STATIC_PATH] = "/path/to/static_files/"
+ aux_app[STATIC_URL] = "/static/"
+ aux_app[WS] = set(((ws, '/foo/bar'),)) # type: ignore[misc]
assert 0 == await src_reload(aux_app, '/path/to/static_files/foo/bar.html')
assert ws.send_str.call_count == 0
@@ -89,11 +86,9 @@ async def test_aux_reload_runtime_error(smart_caplog):
ws = MagicMock()
ws.send_str = MagicMock(return_value=create_future())
ws.send_str = MagicMock(side_effect=RuntimeError('foobar'))
- aux_app.update(
- websockets=[(ws, '/foo/bar')],
- static_url='/static/',
- static_path='/path/to/static_files/'
- )
+ aux_app[STATIC_PATH] = "/path/to/static_files/"
+ aux_app[STATIC_URL] = "/static/"
+ aux_app[WS] = set(((ws, '/foo/bar'),)) # type: ignore[misc]
assert 0 == await src_reload(aux_app)
assert ws.send_str.call_count == 1
assert 'adev.server.aux ERROR: Error broadcasting change to /foo/bar, RuntimeError: foobar\n' == smart_caplog
@@ -104,7 +99,7 @@ async def test_aux_cleanup(event_loop):
aux_app.on_cleanup.append(cleanup_aux_app)
ws = MagicMock()
ws.close = MagicMock(return_value=create_future())
- aux_app['websockets'] = [(ws, '/foo/bar')]
+ aux_app[WS] = set(((ws, '/foo/bar'),)) # type: ignore[misc]
aux_app.freeze()
await aux_app.cleanup()
assert ws.close.call_count == 1
@@ -144,8 +139,8 @@ def test_modify_main_app_all_off(tmpworkdir):
modify_main_app(app, config) # type: ignore[arg-type]
assert len(app.on_response_prepare) == 0
assert len(app.middlewares) == 0
- assert app['static_root_url'] == 'http://foobar.com:8001/static'
- assert subapp["static_root_url"] == "http://foobar.com:8001/static"
+ assert app[static_root_key] == 'http://foobar.com:8001/static'
+ assert subapp[static_root_key] == "http://foobar.com:8001/static"
assert app._debug is True
@@ -158,8 +153,8 @@ def test_modify_main_app_all_on(tmpworkdir):
modify_main_app(app, config) # type: ignore[arg-type]
assert len(app.on_response_prepare) == 1
assert len(app.middlewares) == 2
- assert app['static_root_url'] == 'http://localhost:8001/static'
- assert subapp['static_root_url'] == "http://localhost:8001/static"
+ assert app[static_root_key] == 'http://localhost:8001/static'
+ assert subapp[static_root_key] == "http://localhost:8001/static"
assert app._debug is True
diff --git a/tests/test_runserver_watch.py b/tests/test_runserver_watch.py
index 4136f379..cf60b895 100644
--- a/tests/test_runserver_watch.py
+++ b/tests/test_runserver_watch.py
@@ -1,10 +1,12 @@
import asyncio
from functools import partial
+from typing import Set, Tuple
from unittest.mock import MagicMock, call
from aiohttp import ClientSession
-from aiohttp.web import Application
+from aiohttp.web import Application, WebSocketResponse
+from aiohttp_devtools.runserver.serve import STATIC_PATH, WS
from aiohttp_devtools.runserver.watch import AppTask, LiveReloadTask
from .conftest import create_future
@@ -39,7 +41,7 @@ async def test_single_file_change(event_loop, mocker):
stop_mock = mocker.patch.object(app_task, "_stop_dev_server", autospec=True)
app = MagicMock()
await app_task.start(app)
- d = {'static_path': '/path/to/'}
+ d = {STATIC_PATH: "/path/to/"}
app.__getitem__.side_effect = d.__getitem__
assert app_task._task is not None
await app_task._task
@@ -79,13 +81,13 @@ async def test_python_no_server(event_loop, mocker):
stop_mock = mocker.patch.object(app_task, "_stop_dev_server", autospec=True)
mocker.patch.object(app_task, "_run", partial(app_task._run, live_checks=2))
app = Application()
- app['static_path'] = '/path/to/'
+ app[STATIC_PATH] = "/path/to/"
app.src_reload = MagicMock()
mock_ws = MagicMock()
f: asyncio.Future[int] = asyncio.Future()
f.set_result(1)
mock_ws.send_str = MagicMock(return_value=f)
- app['websockets'] = [(mock_ws, '/')]
+ app[WS] = set(((mock_ws, '/'),)) # type: ignore[misc]
await app_task.start(app)
assert app_task._task is not None
await app_task._task
@@ -98,7 +100,8 @@ async def test_python_no_server(event_loop, mocker):
async def test_reload_server_running(event_loop, aiohttp_client, mocker):
app = Application()
- app['websockets'] = [None]
+ ws: Set[Tuple[WebSocketResponse, str]] = set(((MagicMock(), "/foo"),))
+ app[WS] = ws
mock_src_reload = mocker.patch('aiohttp_devtools.runserver.watch.src_reload', return_value=create_future())
cli = await aiohttp_client(app)
config = MagicMock()