patx/micropie
version dev-.30: remove get_json in favor of json(), improve docs
Commit c2c1101 · patx · 2026-05-28T17:49:56-04:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index fb51262..62c9cf4 100644
--- a/README.md
+++ b/README.md
@@ -168,7 +168,14 @@ class MyApp(App):
- 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. 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`.
+Choose the simplest input style that fits the route:
+- Use handler arguments when you only need named path/query/body values.
+- Use helpers when you want to explicitly read one query value, one body value, or a JSON payload.
+- Use raw attributes like `query_params`, `body_params`, `files`, `headers`, and `session` when you need repeated values, uploaded file streams, middleware-level inspection, or exact parsed structures.
+
+Do not `await` `self.request.query(...)`, `self.request.form(...)`, or `self.request.json(...)`. They are synchronous accessors over request data MicroPie has already parsed. Uploaded file content is the exception: file bodies are streamed through async queues in `self.request.files`.
+
+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` and `body_params`, 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/conf.py b/docs/apidocs/conf.py
index 0f3965d..d0e1923 100644
--- a/docs/apidocs/conf.py
+++ b/docs/apidocs/conf.py
@@ -2,7 +2,7 @@
project = "MicroPie"
author = "Harrison Erd"
-release = "0.28"
+release = "0.30"
extensions = [
"sphinx.ext.autodoc",
diff --git a/docs/apidocs/reference/request.rst b/docs/apidocs/reference/request.rst
index 4f752f4..67f8813 100644
--- a/docs/apidocs/reference/request.rst
+++ b/docs/apidocs/reference/request.rst
@@ -19,6 +19,11 @@ Request class
instantiate this class yourself. MicroPie creates one per
request and stores it in a context variable.
+ The helper methods on ``Request`` are synchronous. Use
+ ``request.query(...)``, ``request.form(...)`` and ``request.json(...)``
+ directly, even inside ``async def`` handlers. They return values that
+ MicroPie has already parsed for the current request.
+
.. attribute:: scope
The original ASGI scope for the request.
@@ -31,6 +36,8 @@ Request class
A list of positional path parameters. See
:doc:`../tutorial/routing` for details on parameter mapping.
+ You usually do not need this directly because MicroPie binds path
+ parameters into handler arguments first.
.. attribute:: query_params
@@ -39,39 +46,72 @@ Request class
convenience, use :meth:`~micropie.Request.query` to obtain
the first value.
+ Use this raw attribute when repeated query parameters matter:
+
+ .. code-block:: python
+
+ tags = request.query_params.get("tag", [])
+
.. method:: query(name, default=None)
Return the first value for query parameter *name*, or *default*
if missing.
+ This is the usual API for optional query-string inputs:
+
+ .. code-block:: python
+
+ page = int(request.query("page", "1"))
+
+ Do not await this method.
+
.. attribute:: body_params
A ``dict`` mapping form field names to lists of values. For
- JSON requests, body parameters are derived from the top‑level
+ 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`. This is the raw
mapping used by MicroPie for argument binding.
+ Use this raw attribute when you need all submitted values for a
+ field, or when middleware needs to inspect parsed request fields:
+
+ .. code-block:: python
+
+ selected = request.body_params.get("choice", [])
+
.. method:: form(name, default=None)
Return the first value for form/body field *name*, or *default*
if missing.
- .. attribute:: get_json
+ This helper reads from :attr:`body_params`, so it returns the
+ first parsed body value. It is commonly used for HTML form fields,
+ but top-level JSON object keys are also mirrored into
+ :attr:`body_params` for argument binding.
+
+ .. code-block:: python
+
+ username = request.form("username", "Anonymous")
- The JSON body parsed into a Python object. Only populated when
- the request contains valid JSON with content type
- ``application/json``.
+ Do not await this method.
.. 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.
+ If *name* is omitted, this method returns the full parsed payload.
+ If *name* is provided and the payload is an object, the value for
+ that key is returned; otherwise *default* is returned.
+
+ .. code-block:: python
+
+ payload = request.json()
+ username = request.json("username", "Anonymous")
+
+ Do not await this method. MicroPie parses JSON before calling the
+ route handler. Invalid JSON requests receive ``400 Bad Request``.
.. attribute:: session
@@ -80,12 +120,16 @@ Request class
.. attribute:: files
- A ``dict`` of uploaded files for multipart/form‑data requests.
+ A ``dict`` of uploaded files for multipart/form-data requests.
Each entry is a mapping containing keys ``filename``,
``content_type`` and ``content``, where ``content`` is an
``asyncio.Queue`` yielding file chunks as bytes. See
:doc:`../tutorial/routing` for information on awaiting file fields.
+ Unlike ``query()``, ``form()`` and ``json()``, file content is
+ streaming data. Read file chunks asynchronously from the
+ ``content`` queue.
+
.. attribute:: headers
A case‑insensitive mapping of header names to values, decoded as
diff --git a/docs/apidocs/tutorial/routing.rst b/docs/apidocs/tutorial/routing.rst
index 58d5588..cf5d1a7 100644
--- a/docs/apidocs/tutorial/routing.rst
+++ b/docs/apidocs/tutorial/routing.rst
@@ -65,35 +65,100 @@ 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
-----------------------------
+Choosing an input access style
+------------------------------
+
+MicroPie gives you three levels of request input access. Prefer the
+simplest level that fits the handler.
+
+1. **Handler arguments** are the most concise option. Use them when an
+ endpoint needs a small number of named values and the normal binding
+ order is acceptable.
+
+ .. code-block:: python
+
+ class MyApp(App):
+ async def search(self, q="", page="1"):
+ return {"q": q, "page": int(page)}
-In handlers, you can access request input through helper methods or
-through raw parsed attributes:
+ This works for ``/search?q=micropie&page=2`` without touching
+ ``self.request`` directly.
-* Helpers:
+2. **Helper methods** are best when the handler needs explicit control
+ over where a value comes from, a default value, or the full JSON
+ payload.
- * ``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.
+ .. code-block:: python
-* Raw attributes:
+ class MyApp(App):
+ async def search(self):
+ q = self.request.query("q", "")
+ page = int(self.request.query("page", "1"))
+ return {"q": q, "page": page}
+
+ async def submit(self):
+ username = self.request.form("username", "Anonymous")
+ return {"submitted_by": username}
+
+ async def api_submit(self):
+ payload = self.request.json()
+ username = self.request.json("username", "Anonymous")
+ return {"submitted_by": username, "raw": payload}
+
+ The helper methods are:
+
+ * ``self.request.query(name, default=None)``: first query-string
+ value for ``name``.
+ * ``self.request.form(name, default=None)``: first parsed body value
+ for ``name`` from form data, or from a top-level JSON object mirrored
+ into body parameters.
+ * ``self.request.json(name=None, default=None)``: full parsed JSON
+ payload when called with no name, or one top-level JSON object value
+ when ``name`` is provided.
+
+3. **Raw attributes** are useful when you need the lower-level parsed
+ structures, such as repeated query parameters, all submitted form
+ values, uploaded files, or middleware decisions.
+
+ * ``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, multipart text fields, or mirrored from
+ top-level JSON objects.
+ * ``self.request.session``: session dictionary.
+ * ``self.request.files``: uploaded multipart files.
+ * ``self.request.headers``: lower-case request header mapping.
+
+ For example, use ``query_params`` when repeated values matter:
- * ``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.
+ .. code-block:: python
-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.
+ class MyApp(App):
+ async def filter(self):
+ tags = self.request.query_params.get("tag", [])
+ return {"tags": tags}
+
+Do not await request helpers
+----------------------------
+
+The request helper methods are synchronous accessors over data MicroPie
+has already parsed for the current request. Do not use ``await`` with
+``query()``, ``form()`` or ``json()``:
+
+.. code-block:: python
+
+ class MyApp(App):
+ async def submit(self):
+ data = self.request.json()
+ username = self.request.form("username", "")
+ q = self.request.query("q", "")
+ return {"data": data, "username": username, "q": q}
+
+For JSON and URL-encoded form requests, the body has been read before
+your handler runs. Multipart file uploads are the main exception:
+uploaded file content is exposed as an ``asyncio.Queue`` in
+``self.request.files`` and should be read asynchronously.
Examples
--------
diff --git a/docs/apidocs/whats_new.rst b/docs/apidocs/whats_new.rst
index 06b6b88..c0bc2f0 100644
--- a/docs/apidocs/whats_new.rst
+++ b/docs/apidocs/whats_new.rst
@@ -11,6 +11,9 @@ releases, consult the `GitHub releases page <https://github.com/patx/micropie/re
Version highlights
------------------
+* **0.30** – Removes the public ``Request.get_json`` attribute. Use
+ ``Request.json()`` for the full parsed JSON payload or
+ ``Request.json(name, default)`` for top-level object lookups.
* **0.29** Performance upgrades, no more per request signature inspections
for routing. 24%-54% increase in req/sec.
* **0.28** – Adds ``Request.json`` helper for convenient JSON access and
diff --git a/docs/release_notes.md b/docs/release_notes.md
index 1c8b7c9..89fb853 100644
--- a/docs/release_notes.md
+++ b/docs/release_notes.md
@@ -1,6 +1,7 @@
[](https://patx.github.io/micropie)
## Releases Notes
+- **[0.30](https://github.com/patx/micropie/releases/tag/v0.30)** - Remove the public `Request.get_json` attribute. Use `Request.json()` or `Request.json(name, default)`.
- **[0.29](https://github.com/patx/micropie/releases/tag/v0.29)** - Performance upgrades, no more per request signature inspections for routing. 24%-54% increase in req/sec.
- **[0.28](https://github.com/patx/micropie/releases/tag/v0.28)** - Add `Request.json` helper
- **[0.27](https://github.com/patx/micropie/releases/tag/v0.27)** - Add `Request.query` and `Request.form` helpers
diff --git a/examples/blog/app.py b/examples/blog/app.py
index 36384f2..709043f 100644
--- a/examples/blog/app.py
+++ b/examples/blog/app.py
@@ -260,7 +260,7 @@ class BlogApp(App):
return guard
try:
- data = self.request.get_json
+ data = self.request.json()
except Exception:
return 400, {"error": "Invalid JSON payload."}
@@ -317,7 +317,7 @@ class BlogApp(App):
return guard
try:
- data = self.request.get_json
+ data = self.request.json()
except Exception:
return 400, {"error": "Invalid JSON payload."}
diff --git a/examples/explicit_routing/app.py b/examples/explicit_routing/app.py
index d71415d..07fa0df 100644
--- a/examples/explicit_routing/app.py
+++ b/examples/explicit_routing/app.py
@@ -9,7 +9,7 @@ class MyApp(ExplicitApp):
@route("/api/users/{user:str}/records", method=["POST"])
async def _create_record(self, user: str):
try:
- data = self.request.get_json
+ data = self.request.json()
return {"user": user, "record": data.get("record_id"), "created": True}
except Exception:
return {"error": f"Invalid JSON"}
diff --git a/examples/json_api/app.py b/examples/json_api/app.py
index 8b390e1..942f0fa 100644
--- a/examples/json_api/app.py
+++ b/examples/json_api/app.py
@@ -10,7 +10,7 @@ class Root(App):
if self.request.method == "GET":
return {"input": False, "extra": False}
- data = self.request.get_json
+ data = self.request.json()
return {"input": data, "extra": True}
async def example(self):
diff --git a/examples/middleware/csrf.py b/examples/middleware/csrf.py
index a5937d6..698d8a0 100644
--- a/examples/middleware/csrf.py
+++ b/examples/middleware/csrf.py
@@ -88,7 +88,7 @@ class CSRFMiddleware(HttpMiddleware):
if lst and isinstance(lst, list) and lst:
return lst[0]
- j = getattr(request, "get_json", None)
+ j = request.json()
if isinstance(j, dict):
tok = j.get(self.body_field)
if isinstance(tok, str):
diff --git a/examples/middleware/router.py b/examples/middleware/router.py
index 781aef1..5ab2874 100644
--- a/examples/middleware/router.py
+++ b/examples/middleware/router.py
@@ -88,7 +88,7 @@ class MyApp(App):
async def _create_record(self, user: str):
try:
- data = self.request.get_json
+ data = self.request.json()
return {"user": user, "record": data.get("record_id"), "created": True}
except Exception as e:
return {"error": f"Invalid JSON: {str(e)}"}
diff --git a/examples/server_sent_events/chat.py b/examples/server_sent_events/chat.py
index 8844b6d..a0793eb 100644
--- a/examples/server_sent_events/chat.py
+++ b/examples/server_sent_events/chat.py
@@ -51,7 +51,7 @@ class ChatApp(App):
return 200, html, [("Content-Type", "text/html")]
async def send(self):
- data = self.request.get_json
+ data = self.request.json()
username = data.get("username", "Anonymous")
message = data.get("message", "")
if message.strip():
diff --git a/examples/url_shortener/main.py b/examples/url_shortener/main.py
index 8ededda..e1a33bd 100644
--- a/examples/url_shortener/main.py
+++ b/examples/url_shortener/main.py
@@ -234,7 +234,7 @@ class ApiApp(App):
if self.request.method != "POST":
return 405, {"error": "Method Not Allowed"}
- data = self.request.get_json
+ data = self.request.json()
url_str = data.get("url")
if not isinstance(url_str, str):
diff --git a/examples/url_shortener/middlewares/csrf.py b/examples/url_shortener/middlewares/csrf.py
index 5cbe29e..5298604 100644
--- a/examples/url_shortener/middlewares/csrf.py
+++ b/examples/url_shortener/middlewares/csrf.py
@@ -95,7 +95,7 @@ class CSRFMiddleware(HttpMiddleware):
if lst and isinstance(lst, list) and lst:
return lst[0]
- j = getattr(request, "get_json", None)
+ j = request.json()
if isinstance(j, dict):
tok = j.get(self.body_field)
if isinstance(tok, str):
diff --git a/micropie.py b/micropie.py
index add903d..df9da6a 100644
--- a/micropie.py
+++ b/micropie.py
@@ -164,7 +164,7 @@ class Request:
self.path_params: List[str] = []
self.query_params: Dict[str, List[str]] = {}
self.body_params: Dict[str, List[str]] = scope.get("body_params", {})
- self.get_json: Any = scope.get("get_json", {})
+ self._json: Any = scope.get("json_body", {})
self.session: Dict[str, Any] = scope.get("session", {})
self.files: Dict[str, Any] = scope.get("files", {})
self.headers: Dict[str, str] = {
@@ -210,9 +210,9 @@ class Request:
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 self._json
+ if isinstance(self._json, dict):
+ return self._json.get(name, default)
return default
@@ -683,10 +683,10 @@ class App:
body_data = b"".join(body_chunks)
if "application/json" in content_type:
try:
- request.get_json = json.loads(body_data)
- if isinstance(request.get_json, dict):
+ request._json = json.loads(body_data)
+ if isinstance(request._json, dict):
request.body_params = {
- k: [str(v)] for k, v in request.get_json.items()
+ k: [str(v)] for k, v in request._json.items()
}
except Exception:
await _early_exit(400, "400 Bad Request: Bad JSON")
@@ -721,7 +721,7 @@ class App:
new_scope["root_path"] = scope.get("root_path", "")
new_scope["body_params"] = request.body_params
new_scope["body_parsed"] = request.body_parsed
- new_scope["get_json"] = getattr(request, "get_json", {})
+ new_scope["json_body"] = request._json
new_scope["files"] = request.files
new_scope["session"] = request.session
diff --git a/pyproject.toml b/pyproject.toml
index 02adb8b..c8a468b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi"
[project]
name = "micropie"
-version = "0.29"
+version = "0.30"
description = "An ultra micro ASGI web framework"
keywords = ["micropie", "asgi", "microframework", "http"]
readme = "docs/README.md"
diff --git a/tests.py b/tests.py
index a03ac12..02c7e4c 100644
--- a/tests.py
+++ b/tests.py
@@ -136,9 +136,10 @@ class TestRequest(MicroPieTestCase):
"method": "POST",
"path": "/json",
"headers": [],
- "get_json": {"username": "alice", "age": 42},
+ "json_body": {"username": "alice", "age": 42},
}
)
+ self.assertFalse(hasattr(request, "get_json"))
self.assertEqual(request.json(), {"username": "alice", "age": 42})
self.assertEqual(request.json("username"), "alice")
self.assertIsNone(request.json("missing"))
@@ -150,7 +151,7 @@ class TestRequest(MicroPieTestCase):
"method": "POST",
"path": "/json",
"headers": [],
- "get_json": ["a", "b"],
+ "json_body": ["a", "b"],
}
)
self.assertEqual(list_payload_request.json(), ["a", "b"])
@@ -580,7 +581,7 @@ class TestResponseHandling(MicroPieTestCase):
"""Test JSON request and response handling."""
async def json_handler(self):
- return self.request.get_json
+ return self.request.json()
setattr(self.app, "json_handler", json_handler.__get__(self.app, App))