patx/micropie
Add `Request.json` helper method and update the docs and README to reflect
Commit c7af233 · patx · 2026-02-12T19:24:26-05:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 9ea53b0..287c6c3 100644
--- a/README.md
+++ b/README.md
@@ -108,18 +108,16 @@ Access your app at [http://127.0.0.1:8000](http://127.0.0.1:8000).
### **Route Handlers**
-MicroPie's route handlers map URLs to methods in your `App` subclass, handling HTTP requests with flexible parameter mapping and response formats.
+MicroPie's route handlers map URLs to methods in your `App` subclass. Handler input can come from automatic argument binding or request helper methods.
#### **Key Points**
- **Automatic Mapping**: URLs map to method names (e.g., `/greet` → `greet`, `/` → `index`).
- **Private Methods**: Methods starting with `_` (e.g., `_private_method`) are private and inaccessible via URLs, returning 404. **Security Note**: Use `_` for sensitive methods to prevent external access.
-- **Parameters**: Automatically populated from:
- - Path segments (e.g., `/greet/Alice` → `name="Alice"`).
- - Query strings (e.g., `?name=Alice`).
- - Form data (POST/PUT/PATCH).
- - Session data (`self.request.session`).
- - File uploads (`self.request.files`).
- - Default values in method signatures.
+- **Automatic Argument Binding**: Handler args are populated from path/query/body data by parameter name.
+- **Request Helpers**:
+ - `self.request.query(name, default)` for query-string values.
+ - `self.request.form(name, default)` for form/body values.
+ - `self.request.json()` for full JSON payloads, or `self.request.json(name, default)` for a key lookup.
- **HTTP Methods**: Handlers support all methods (GET, POST, etc.). Check `self.request.method` to handle specific methods.
- **Responses**:
- String, bytes, or JSON-serializable object.
@@ -131,36 +129,45 @@ MicroPie's route handlers map URLs to methods in your `App` subclass, handling H
- **Errors**: Auto-handled 404/400; customize via middleware.
- **Dynamic Params**: Use `*args` for multiple path parameters.
-#### **Flexible HTTP Routing for GET Requests**
-MicroPie automatically maps URLs to methods within your `App` class. Routes can be defined as either synchronous or asynchronous functions, offering good flexibility.
-
-For GET requests, pass data through query strings or URL path segments, automatically mapped to method arguments.
+#### **Automatic Argument Binding**
+MicroPie can bind handler parameters directly from incoming request data:
```python
class MyApp(App):
async def greet(self, name="Guest"):
return f"Hello, {name}!"
- async def hello(self):
- name = self.request.query("name")
- return f"Hello {name}!"
+ async def submit(self, username="Anonymous"):
+ return f"Submitted by: {username}"
```
**Access:**
-- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`, same as [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`.
-- [http://127.0.0.1:8000/hello/Alice](http://127.0.0.1:8000/hello/Alice) returns a `500 Internal Server Error` because it is expecting [http://127.0.0.1:8000/hello?name=Alice](http://127.0.0.1:8000/hello?name=Alice), which returns `Hello Alice!`
+- [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`.
+- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) also returns `Hello, Alice!`.
+- POST `application/x-www-form-urlencoded` to `/submit` with `username=bob` returns `Submitted by: bob`.
+- POST `application/json` to `/submit` with `{"username": "bob"}` also returns `Submitted by: bob`.
-#### **Flexible HTTP POST Request Handling**
-MicroPie also supports handling form data submitted via HTTP POST requests. Form data is automatically mapped to method arguments. It is able to handle default values and raw/JSON POST data:
+#### **Helper-Based Request Access**
+Use helper methods for query, form, and JSON payload access:
```python
class MyApp(App):
- async def submit_default_values(self, username="Anonymous"):
- return f"Form submitted by: {username}"
+ async def greet(self):
+ name = self.request.query("name", "Guest")
+ return f"Hello, {name}!"
- async def submit_catch_all(self):
+ async def submit(self):
username = self.request.form("username", "Anonymous")
return f"Submitted by: {username}"
+
+ async def submit_json(self):
+ data = self.request.json()
+ username = self.request.json("username", "Anonymous")
+ return {"submitted_by": username, "raw": data}
```
+**Access:**
+- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`.
+- POST `application/x-www-form-urlencoded` to `/submit` with `username=bob` returns `Submitted by: bob`.
+- POST `application/json` to `/submit_json` with `{"username": "bob"}` returns JSON including `submitted_by: "bob"`.
-By default, MicroPie's route handlers can accept any request method, it's up to you how to handle any incoming requests! You can check the request method (and a number of other things specific to the current request state) in the handler with `self.request.method`. You can see how to handle POST JSON data at [examples/json_api](https://github.com/patx/micropie/tree/main/examples/json_api).
+By default, MicroPie's route handlers can accept any request method. Check `self.request.method` in a handler when route behavior differs by method. For lower-level request internals such as `query_params`, `body_params`, and `get_json`, see `docs/apidocs/reference/request.rst`.
### **Lifecycle Event Handling**
MicroPie supports ASGI lifespan events, allowing you to register asynchronous handlers for application startup and shutdown. This is useful for tasks like initializing database connections or cleaning up resources.
diff --git a/docs/apidocs/reference/request.rst b/docs/apidocs/reference/request.rst
index 33c6e6c..4f752f4 100644
--- a/docs/apidocs/reference/request.rst
+++ b/docs/apidocs/reference/request.rst
@@ -35,7 +35,8 @@ Request class
.. attribute:: query_params
A ``dict`` mapping each query parameter to a list of values.
- For convenience, use :meth:`~micropie.Request.query` to obtain
+ This is the raw parsed mapping from the query string. For
+ convenience, use :meth:`~micropie.Request.query` to obtain
the first value.
.. method:: query(name, default=None)
@@ -48,7 +49,8 @@ Request class
A ``dict`` mapping form field names to lists of values. For
JSON requests, body parameters are derived from the top‑level
object; for ``application/x-www-form-urlencoded`` forms they are
- parsed using :func:`urllib.parse.parse_qs`.
+ parsed using :func:`urllib.parse.parse_qs`. This is the raw
+ mapping used by MicroPie for argument binding.
.. method:: form(name, default=None)
@@ -61,6 +63,16 @@ Request class
the request contains valid JSON with content type
``application/json``.
+ .. method:: json(name=None, default=None)
+
+ Return the parsed JSON payload or a value from a top-level JSON
+ object.
+
+ If *name* is omitted, this method returns the full parsed payload
+ (equivalent to :attr:`~micropie.Request.get_json`). If *name* is
+ provided and the payload is an object, the value for that key is
+ returned; otherwise *default* is returned.
+
.. attribute:: session
A ``dict`` for storing per‑client data across requests. See
diff --git a/docs/apidocs/tutorial/routing.rst b/docs/apidocs/tutorial/routing.rst
index 77418dd..58d5588 100644
--- a/docs/apidocs/tutorial/routing.rst
+++ b/docs/apidocs/tutorial/routing.rst
@@ -65,6 +65,36 @@ sources in the following order:
If MicroPie cannot determine a value for a required parameter, it
returns a ``400 Bad Request``.
+Helpers and raw request data
+----------------------------
+
+In handlers, you can access request input through helper methods or
+through raw parsed attributes:
+
+* Helpers:
+
+ * ``self.request.query(name, default=None)`` returns the first query
+ value.
+ * ``self.request.form(name, default=None)`` returns the first
+ form/body value.
+ * ``self.request.json(name=None, default=None)`` returns either the
+ full parsed JSON payload or a key from a top-level JSON object.
+
+* Raw attributes:
+
+ * ``self.request.path_params``: list of positional path segments.
+ * ``self.request.query_params``: ``dict[str, list[str]]`` parsed from
+ the query string.
+ * ``self.request.body_params``: ``dict[str, list[str]]`` parsed from
+ form-urlencoded payloads, or mirrored from top-level JSON objects.
+ * ``self.request.get_json``: full parsed JSON payload.
+ * ``self.request.session``: session dictionary.
+ * ``self.request.files``: uploaded multipart files.
+
+The helper APIs are convenient when you need a single value. The raw
+attributes are useful when you need full multi-value access or exact
+payload inspection.
+
Examples
--------
diff --git a/micropie.py b/micropie.py
index 22ae68d..01cb3ad 100644
--- a/micropie.py
+++ b/micropie.py
@@ -163,6 +163,20 @@ class Request:
return values[0]
return default
+ def json(self, name: Optional[str] = None, default: Any = None) -> Any:
+ """
+ Return the parsed JSON body or a value from a top-level JSON object.
+
+ Args:
+ name: Optional key from the top-level JSON object.
+ default: Value returned when key is missing or payload is not an object.
+ """
+ if name is None:
+ return self.get_json
+ if isinstance(self.get_json, dict):
+ return self.get_json.get(name, default)
+ return default
+
class WebSocketRequest(Request):
"""Represents a WebSocket request in the MicroPie framework."""
diff --git a/tests.py b/tests.py
index 3a0d345..f1d815d 100644
--- a/tests.py
+++ b/tests.py
@@ -112,6 +112,34 @@ class TestRequest(MicroPieTestCase):
self.assertIsNone(request.form("missing"))
self.assertEqual(request.form("missing", "fallback"), "fallback")
+ async def test_request_json_helper(self):
+ """Verify JSON helper returns payloads, keys, and defaults."""
+ request = Request(
+ {
+ "type": "http",
+ "method": "POST",
+ "path": "/json",
+ "headers": [],
+ "get_json": {"username": "alice", "age": 42},
+ }
+ )
+ self.assertEqual(request.json(), {"username": "alice", "age": 42})
+ self.assertEqual(request.json("username"), "alice")
+ self.assertIsNone(request.json("missing"))
+ self.assertEqual(request.json("missing", "fallback"), "fallback")
+
+ list_payload_request = Request(
+ {
+ "type": "http",
+ "method": "POST",
+ "path": "/json",
+ "headers": [],
+ "get_json": ["a", "b"],
+ }
+ )
+ self.assertEqual(list_payload_request.json(), ["a", "b"])
+ self.assertEqual(list_payload_request.json("username", "fallback"), "fallback")
+
class TestSession(MicroPieTestCase):
"""Tests for session management and cookie parsing."""