bump to 0.27 and document Request.query/Request.form helpers

Commit 457848d · patx · 2026-02-12T01:46:18-05:00

Changeset
457848d7d45c699da058e1e2a00e120c321b0fac
Parents
14bbaa42bd6e9715536527c90bc3c2411ee77be6

View source at this commit

Comments

No comments yet.

Log in to comment

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 @@
 [![Logo](https://patx.github.io/micropie/logo.png)](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."""