version dev-.30: remove get_json in favor of json(), improve docs

Commit c2c1101 · patx · 2026-05-28T17:49:56-04:00

Changeset
c2c11013ab714c01a78f9e1e5ac126b972e4c9a6
Parents
2f426f02508282b684cde37d50d20406c97659ee

View source at this commit

Comments

No comments yet.

Log in to comment

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