Repository: falconry/falcon
Pull request #1687 (2020-02-27 14:28:50)

refactor(asgi.Request): Change the media property to an alias of get_media()
# Summary of Changes

This patch adds back support for the media property to help make the ASGI
and WSGI interfaces as similar as possible.

# Related Issues

#1358 

codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=h1) Report
> Merging [#1687](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/3c5c938e41474b024bf4b2f7da5bd6d1ae385de4?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1687/graphs/tree.svg?width=650&token=zOtkfNQ3h6&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1687   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          47      47           
  Lines        3526    3526           
  Branches      548     548           
======================================
  Hits         3526    3526
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=footer). Last update [3c5c938...9f4229e](https://codecov.io/gh/falconry/falcon/pull/1687?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

vytas7: This looks good, I'm thinking to adopt the same thinking for multipart media, the async flavour.

Patch:
diff --git a/falcon/asgi/request.py b/falcon/asgi/request.py
index c1e2d69..4c09325 100644
--- a/falcon/asgi/request.py
+++ b/falcon/asgi/request.py
@@ -262,6 +262,13 @@ class Request(falcon.request.Request):
             the body of the request, if any.
 
             See also: :class:`falcon.asgi.BoundedStream`
+        media (object): An awaitable property that acts as an alias for
+            :meth:`~.get_media`. This can be used to ease the porting of a
+            a WSGI app to ASGI, although the ``await`` keyword must still be
+            added when referencing the property::
+
+                deserialized_media = await req.media
+
         expect (str): Value of the Expect header, or ``None`` if the
             header is missing.
         range (tuple of int): A 2-member ``tuple`` parsed from the value of the
@@ -681,13 +688,6 @@ class Request(falcon.request.Request):
 
         return netloc_value
 
-    @property
-    def media(self):
-        raise errors.UnsupportedError(
-            'The media property is not supported for ASGI requests. '
-            'Please use the Request.get_media() coroutine function instead.'
-        )
-
     async def get_media(self):
         """Returns a deserialized form of the request stream.
 
@@ -732,6 +732,8 @@ class Request(falcon.request.Request):
 
         return self._media
 
+    media = property(get_media)
+
     @property
     def if_match(self):
         # TODO(kgriffs): It may make sense at some point to create a
diff --git a/falcon/request.py b/falcon/request.py
index 0592041..37a2a5a 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -331,8 +331,6 @@ class Request:
 
                 doc = json.load(req.bounded_stream)
 
-        expect (str): Value of the Expect header, or ``None`` if the
-            header is missing.
         media (object): Returns a deserialized form of the request stream.
             When called, it will attempt to deserialize the request stream
             using the Content-Type header as well as the media-type handlers
@@ -345,6 +343,9 @@ class Request:
                 it's called and cache the results. Follow-up calls will just
                 retrieve a cached version of the object.
 
+        expect (str): Value of the Expect header, or ``None`` if the
+            header is missing.
+
         range (tuple of int): A 2-member ``tuple`` parsed from the value of the
             Range header.
 


Test patch:
diff --git a/tests/asgi/test_request_asgi.py b/tests/asgi/test_request_asgi.py
index 6a84bd8..e674010 100644
--- a/tests/asgi/test_request_asgi.py
+++ b/tests/asgi/test_request_asgi.py
@@ -1,6 +1,6 @@
 import pytest
 
-from falcon import testing, UnsupportedError
+from falcon import testing
 
 
 def test_missing_server_in_scope():
@@ -13,9 +13,3 @@ def test_log_error_not_supported():
     req = testing.create_asgi_req()
     with pytest.raises(NotImplementedError):
         req.log_error('Boink')
-
-
-def test_media_prop_not_supported():
-    req = testing.create_asgi_req()
-    with pytest.raises(UnsupportedError):
-        req.media
diff --git a/tests/test_media_handlers.py b/tests/test_media_handlers.py
index 6cbb2b3..bb5cec0 100644
--- a/tests/test_media_handlers.py
+++ b/tests/test_media_handlers.py
@@ -154,7 +154,10 @@ def test_deserialization_raises(asgi):
             resp.media = {}
 
         async def on_post(self, req, resp):
-            await req.get_media()
+            # NOTE(kgriffs): In this one case we use the property
+            #   instead of get_media(), in order to test that the
+            #   alias works as expected.
+            await req.media
 
     app.add_route('/', ResourceAsync() if asgi else Resource())