patx/wred
Default static file root to the static directory
Commit 7826c02 · patx · 2026-10-15T14:40:23-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 31b09a7..f53ecee 100644
--- a/README.md
+++ b/README.md
@@ -192,23 +192,20 @@ streaming, downloads, or SSE. Use ASGI middleware for behavior across routes.
## Static files
```python
-from pathlib import Path
-
-static_root = Path(__file__).parent / 'static'
-
@app.route('/')
async def index(request):
- return await app.static('index.html', root=static_root)
+ return await app.static('index.html')
@app.route('/static/{filename}')
async def asset(request, filename):
- return await app.static(filename, root=static_root)
+ return await app.static(filename)
```
-`static(filename, root=...)` returns `(bytes, content_type, status)`.
-The root is required and accepts a string or `Path`. MIME types are guessed;
-missing, unreadable, or outside-root files return plain-text 404, including
-symlink escapes. Files are read in a worker thread, entirely into memory.
+`static(filename, root='static')` returns `(bytes, content_type, status)`.
+Omitting `root`, passing `''`, or passing `None` uses the `static` directory in the
+current working directory. Set `root` to a string or `Path` to use another directory.
+MIME types are guessed; missing, unreadable, or outside-root files return
+plain-text 404, including symlink escapes. Files are read in a worker thread, entirely into memory.
Small files. Don't feed it a movie.
## Errors
diff --git a/tests/test_wred.py b/tests/test_wred.py
index f91c12f..c72b26b 100644
--- a/tests/test_wred.py
+++ b/tests/test_wred.py
@@ -1,5 +1,6 @@
import tempfile
import unittest
+from contextlib import chdir
from pathlib import Path
from unittest.mock import patch
@@ -115,13 +116,16 @@ class WredTests(unittest.IsolatedAsyncioTestCase):
(root / 'asset.unknownextension').write_bytes(b'\x00\xff')
(base / 'secret.txt').write_text('private')
(root / 'escape.txt').symlink_to(base / 'secret.txt')
+ custom_root = base / 'assets'
+ custom_root.mkdir()
+ (custom_root / 'index.html').write_bytes(b'<p>Custom root</p>')
app = App()
@app.route('/file', methods=['POST'])
async def file(request):
data = await request.json()
- return await app.static(data['filename'], root=root)
+ return await app.static(data['filename'], **root_options)
cases = [
('index.html', 200, b'text/html', b'<p>Hello</p>'),
@@ -133,17 +137,24 @@ class WredTests(unittest.IsolatedAsyncioTestCase):
('escape.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
('invalid\x00.txt', 404, b'text/plain; charset=utf-8', b'Not Found'),
]
- for filename, status, content_type, body in cases:
- with self.subTest(filename=filename):
- async def receive():
- return {'type': 'http.request', 'body': orjson.dumps({'filename': filename})}
-
- messages = await call_app(app, {'type': 'http', 'path': '/file', 'method': 'POST'}, receive)
- self.assertEqual(messages, [
- {'type': 'http.response.start', 'status': status,
- 'headers': [(b'content-type', content_type)]},
- {'type': 'http.response.body', 'body': body},
- ])
+ cases = [
+ (options, *case)
+ for options in ({}, {'root': ''}, {'root': None}, {'root': root}, {'root': str(root)})
+ for case in cases
+ ]
+ cases.append(({'root': custom_root}, 'index.html', 200, b'text/html', b'<p>Custom root</p>'))
+ with chdir(base):
+ for root_options, filename, status, content_type, body in cases:
+ with self.subTest(root=root_options, filename=filename):
+ async def receive():
+ return {'type': 'http.request', 'body': orjson.dumps({'filename': filename})}
+
+ messages = await call_app(app, {'type': 'http', 'path': '/file', 'method': 'POST'}, receive)
+ self.assertEqual(messages, [
+ {'type': 'http.response.start', 'status': status,
+ 'headers': [(b'content-type', content_type)]},
+ {'type': 'http.response.body', 'body': body},
+ ])
async def test_dictionary_and_list_responses_are_json(self):
app = App()
diff --git a/wred.py b/wred.py
index 2aa8788..a0b00ec 100644
--- a/wred.py
+++ b/wred.py
@@ -161,11 +161,11 @@ class App:
return None
return params
- async def static(self, filename: str | Path, *, root: str | Path) -> tuple[bytes, str, int]:
- """Serve a small file inside root; read its entire body off the event loop."""
+ async def static(self, filename: str | Path, *, root: str | Path | None = 'static') -> tuple[bytes, str, int]:
+ """Serve a small file inside root (default: ./static), off the event loop."""
def read() -> tuple[bytes, str, int]:
try:
- directory = Path(root).resolve()
+ directory = Path(root or 'static').resolve()
path = (directory / filename).resolve()
if not path.is_relative_to(directory) or not path.is_file():
return b'Not Found', 'text/plain; charset=utf-8', 404