patx/micropie
bump to 0.27 and document Request.query/Request.form helpers
Commit 457848d · patx · 2026-02-12T01:46:18-05:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 3eb3e40..9ea53b0 100644
--- a/README.md
+++ b/README.md
@@ -141,7 +141,7 @@ class MyApp(App):
return f"Hello, {name}!"
async def hello(self):
- name = self.request.query_params.get("name", [None])[0]
+ name = self.request.query("name")
return f"Hello {name}!"
```
**Access:**
@@ -156,7 +156,7 @@ class MyApp(App):
return f"Form submitted by: {username}"
async def submit_catch_all(self):
- username = self.request.body_params.get("username", ["Anonymous"])[0]
+ username = self.request.form("username", "Anonymous")
return f"Submitted by: {username}"
```
diff --git a/docs/apidocs/conf.py b/docs/apidocs/conf.py
index fbb4517..481efd3 100644
--- a/docs/apidocs/conf.py
+++ b/docs/apidocs/conf.py
@@ -2,7 +2,7 @@
project = "MicroPie"
author = "Harrison Erd"
-release = "0.26"
+release = "0.27"
extensions = [
"sphinx.ext.autodoc",
diff --git a/docs/apidocs/reference/request.rst b/docs/apidocs/reference/request.rst
index 332b8e7..33c6e6c 100644
--- a/docs/apidocs/reference/request.rst
+++ b/docs/apidocs/reference/request.rst
@@ -35,8 +35,13 @@ Request class
.. attribute:: query_params
A ``dict`` mapping each query parameter to a list of values.
- For convenience, use ``self.request.query_params['name'][0]`` to
- obtain the first value.
+ For convenience, use :meth:`~micropie.Request.query` to obtain
+ the first value.
+
+ .. method:: query(name, default=None)
+
+ Return the first value for query parameter *name*, or *default*
+ if missing.
.. attribute:: body_params
@@ -45,6 +50,11 @@ Request class
object; for ``application/x-www-form-urlencoded`` forms they are
parsed using :func:`urllib.parse.parse_qs`.
+ .. method:: form(name, default=None)
+
+ Return the first value for form/body field *name*, or *default*
+ if missing.
+
.. attribute:: get_json
The JSON body parsed into a Python object. Only populated when
@@ -77,4 +87,4 @@ WebSocketRequest class
Inherits from :class:`~micropie.Request` and represents a WebSocket
connection request. All attributes of :class:`~micropie.Request`
apply. For WebSocket handlers the request is accessible via
- ``self.request`` inside the handler or via the context variable.
\ No newline at end of file
+ ``self.request`` inside the handler or via the context variable.
diff --git a/docs/apidocs/whats_new.rst b/docs/apidocs/whats_new.rst
index c36269a..6ae148b 100644
--- a/docs/apidocs/whats_new.rst
+++ b/docs/apidocs/whats_new.rst
@@ -11,6 +11,8 @@ releases, consult the `GitHub releases page <https://github.com/patx/micropie/re
Version highlights
------------------
+* **0.27** – Adds ``Request.query`` and ``Request.form`` helpers for more
+ direct access to query-string and form data.
* **0.26** – Makes sub-application handoff independent of middleware
ordering, improving reliability for mounted ASGI apps.
* **0.25** – Fixes Unicode redirect handling by percent-encoding
diff --git a/docs/release_notes.md b/docs/release_notes.md
index 7f6a175..f171322 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -1,6 +1,7 @@
[](https://patx.github.io/micropie)
## Releases Notes
+- **[0.27](https://github.com/patx/micropie/releases/tag/v0.27)** - Add `Request.query` and `Request.form` helpers
- **[0.26](https://github.com/patx/micropie/releases/tag/v0.26)** - Sub-app routing no longer depends on middleware ordering
- **[0.25](https://github.com/patx/micropie/releases/tag/v0.25)** - Fix unicode redirect handling. Percent-encode non-ASCII path segments before setting Location header. Prevents latin-1 header encoding errors and avoids double-encoding queries.
- **[0.24](https://github.com/patx/micropie/releases/tag/v0.24)** - Improve session handling. Expired sessions now clean up properly, and empty sessions delete stored data. Session saving also moved after `after_request` middleware.
diff --git a/examples/auth/app.py b/examples/auth/app.py
index f64debb..4168450 100644
--- a/examples/auth/app.py
+++ b/examples/auth/app.py
@@ -22,7 +22,7 @@ class Root(App):
)
async def callback(self):
- code = self.request.query_params.get("code")
+ code = self.request.query("code")
if not code:
return "Error: No code provided"
diff --git a/examples/blog/app.py b/examples/blog/app.py
index 0f731af..36384f2 100644
--- a/examples/blog/app.py
+++ b/examples/blog/app.py
@@ -203,7 +203,7 @@ class BlogApp(App):
GET → show login form
POST → authenticate and set session, then redirect
"""
- next_path = self.request.query_params.get("next", ["/"])[0]
+ next_path = self.request.query("next", "/")
if self.request.method == "GET":
return await self._render_template(
@@ -215,8 +215,8 @@ class BlogApp(App):
nav_active="login",
)
- username = self.request.body_params.get("username", [""])[0].strip()
- password = self.request.body_params.get("password", [""])[0].strip()
+ username = self.request.form("username", "").strip()
+ password = self.request.form("password", "").strip()
user = await self.users.find_one({"username": username})
if not user or user.get("password") != password:
diff --git a/examples/explicit_routing/ws.py b/examples/explicit_routing/ws.py
index ba53876..62129fa 100644
--- a/examples/explicit_routing/ws.py
+++ b/examples/explicit_routing/ws.py
@@ -10,7 +10,7 @@ class MyApp(ExplicitApp):
@ws_route("/ws/chat/{room:str}")
async def ws_chat(self, ws: WebSocket, room: str):
await ws.accept()
- user = self.request.query_params.get("user", ["anonymous"])[0]
+ user = self.request.query("user", "anonymous")
self.request.session["last_room"] = room
while True:
try:
diff --git a/examples/json_api/basic.py b/examples/json_api/basic.py
index 0f74420..3dfa297 100644
--- a/examples/json_api/basic.py
+++ b/examples/json_api/basic.py
@@ -9,7 +9,7 @@ class PasteApp(App):
async def paste(self, pid: str = None):
if self.request.method == "POST":
# Get content from JSON or form, depending on the Content-Type
- content = self.request.body_params.get("content")[0]
+ content = self.request.form("content")
pid = str(uuid4())
await db.aset(pid, content)
return {
diff --git a/examples/middleware/csrf.py b/examples/middleware/csrf.py
index f54e5ee..a5937d6 100644
--- a/examples/middleware/csrf.py
+++ b/examples/middleware/csrf.py
@@ -161,7 +161,7 @@ class Root(App):
async def submit(self):
if self.request.method == "POST":
- name = self.request.body_params.get("name", ["World"])[0]
+ name = self.request.form("name", "World")
return f"Hello {name}"
diff --git a/examples/middleware/subapp.py b/examples/middleware/subapp.py
index 5726fa2..cc26baa 100644
--- a/examples/middleware/subapp.py
+++ b/examples/middleware/subapp.py
@@ -24,7 +24,7 @@ class CSRFMiddleware(HttpMiddleware):
if request.method in ("POST", "PUT", "PATCH"):
print(f"Request body_params: {request.body_params}") # Debugging
- submitted_token = request.body_params.get("csrf_token", [""])[0]
+ submitted_token = request.form("csrf_token", "")
print(f"Submitted CSRF token: {submitted_token}") # Debugging
if not submitted_token:
return {"status_code": 403, "body": "Missing CSRF token"}
diff --git a/examples/twutr/app.py b/examples/twutr/app.py
index 31e2974..29e6a78 100644
--- a/examples/twutr/app.py
+++ b/examples/twutr/app.py
@@ -367,7 +367,7 @@ class Twutr(App):
if not self.request.session.get("logged_in"):
return self._redirect("/login")
if self.request.method == "POST":
- message = self.request.body_params.get("message", [""])[0]
+ message = self.request.form("message", "")
sanitized_message = convert_custom_syntax(message)
if not sanitized_message.strip():
return await self._render_template(
@@ -389,8 +389,8 @@ class Twutr(App):
if self.request.session.get("logged_in"):
return self._redirect("/")
if self.request.method == "POST":
- username = escape(self.request.body_params.get("username", [""])[0].strip())
- password = escape(self.request.body_params.get("password", [""])[0].strip())
+ username = escape(self.request.form("username", "").strip())
+ password = escape(self.request.form("password", "").strip())
if not username or not password:
return await self._render_template(
"login.html",
@@ -422,8 +422,8 @@ class Twutr(App):
if self.request.session.get("logged_in"):
return self._redirect("/")
if self.request.method == "POST":
- username = escape(self.request.body_params.get("username", [""])[0].strip())
- password = escape(self.request.body_params.get("password", [""])[0].strip())
+ username = escape(self.request.form("username", "").strip())
+ password = escape(self.request.form("password", "").strip())
if not username or not password:
return await self._render_template(
"login.html",
diff --git a/examples/websockets/app.py b/examples/websockets/app.py
index 6ac5580..005a964 100644
--- a/examples/websockets/app.py
+++ b/examples/websockets/app.py
@@ -9,7 +9,7 @@ class MyApp(App):
async def ws_chat(self, ws, room=None):
"""WebSocket handler for ws://localhost:8000/chat"""
await ws.accept()
- user = self.request.query_params.get("user", ["anonymous"])[0]
+ user = self.request.query("user", "anonymous")
self.request.session["last_room"] = room or "default"
while True:
try:
diff --git a/micropie.py b/micropie.py
index 52ecba3..22ae68d 100644
--- a/micropie.py
+++ b/micropie.py
@@ -137,6 +137,32 @@ class Request:
}
self.body_parsed: bool = scope.get("body_parsed", False)
+ def query(self, name: str, default: Optional[str] = None) -> Optional[str]:
+ """
+ Return the first value for a query parameter.
+
+ Args:
+ name: Query parameter name.
+ default: Value returned when the parameter is missing.
+ """
+ values = self.query_params.get(name)
+ if values:
+ return values[0]
+ return default
+
+ def form(self, name: str, default: Optional[str] = None) -> Optional[str]:
+ """
+ Return the first value for a form/body parameter.
+
+ Args:
+ name: Form field name.
+ default: Value returned when the parameter is missing.
+ """
+ values = self.body_params.get(name)
+ if values:
+ return values[0]
+ return default
+
class WebSocketRequest(Request):
"""Represents a WebSocket request in the MicroPie framework."""
diff --git a/pyproject.toml b/pyproject.toml
index cc8443e..fc019bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
[project]
name = "micropie"
-version = "0.26"
+version = "0.27"
description = "An ultra micro ASGI web framework"
keywords = ["micropie", "asgi", "microframework", "http"]
readme = "docs/README.md"
@@ -29,4 +29,3 @@ all = ["jinja2", "multipart", "orjson", "uvicorn"]
[project.urls]
Homepage = "https://patx.github.io/micropie"
Repository = "https://github.com/patx/micropie"
-
diff --git a/tests.py b/tests.py
index a55e937..3a0d345 100644
--- a/tests.py
+++ b/tests.py
@@ -89,6 +89,29 @@ class TestRequest(MicroPieTestCase):
"Query params should be parsed",
)
+ async def test_request_query_and_form_helpers(self):
+ """Verify helper accessors return first values and defaults."""
+ scope = {
+ "type": "http",
+ "method": "POST",
+ "path": "/test",
+ "headers": [],
+ "query_string": b"name=Alice&name=Bob",
+ "body_params": {"username": ["test-user"]},
+ }
+ request = Request(scope)
+ request.query_params = parse_qs(
+ scope.get("query_string", b"").decode("utf-8", "ignore")
+ )
+
+ self.assertEqual(request.query("name"), "Alice")
+ self.assertIsNone(request.query("missing"))
+ self.assertEqual(request.query("missing", "fallback"), "fallback")
+
+ self.assertEqual(request.form("username"), "test-user")
+ self.assertIsNone(request.form("missing"))
+ self.assertEqual(request.form("missing", "fallback"), "fallback")
+
class TestSession(MicroPieTestCase):
"""Tests for session management and cookie parsing."""