Repository: praw-dev/praw
Pull request #1455 (2020-04-29 01:14:39)

Add json parameter for POST, PATCH, and PUT
This parameter gets passed through to `requests`, which automatically JSON-encodes the value and sets an appropriate Content-Type header.

Support for sending bodies that are actually JSON-encoded is needed for upcoming PRAW features such as polls.

This will help for a fix for #1452 by allowing us to use new endpoints.

jarhill0: I don't understand why flake8 is failing with 
```
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.ImageWidget' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.Menu' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.MenuLink' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.ModeratorsWidget' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.PostFlairWidget' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.RulesWidget' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.Submenu' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.SubredditWidgets' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.SubredditWidgetsModeration' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.TextArea' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.Widget' imported but unused
./build/lib/praw/models/__init__.py:25:1: F401 '.reddit.widgets.WidgetModeration' imported but unused
./build/lib/praw/models/__init__.py:47:1: F401 '.reddit.wikipage.WikiPage' imported but unused
./build/lib/praw/models/__init__.py:48:1: F401 '.redditors.Redditors' imported but unused
./build/lib/praw/models/__init__.py:49:1: F401 '.stylesheet.Stylesheet' imported but unused
./build/lib/praw/models/__init__.py:50:1: F401 '.subreddits.Subreddits' imported but unused
./build/lib/praw/models/__init__.py:51:1: F401 '.trophy.Trophy' imported but unused
./build/lib/praw/models/__init__.py:52:1: F401 '.user.User' imported but unused
./build/lib/praw/models/reddit/mixins/__init__.py:6:1: F401 '.fullname.FullnameMixin' imported but unused
./build/lib/praw/models/reddit/mixins/__init__.py:8:1: F401 '.inboxable.InboxableMixin' imported but unused
./build/lib/praw/models/reddit/mixins/__init__.py:10:1: F401 '.messageable.MessageableMixin' imported but unused
./build/lib/praw/models/listing/mixins/__init__.py:2:1: F401 '.base.BaseListingMixin' imported but unused
./build/lib/praw/models/listing/mixins/__init__.py:3:1: F401 '.redditor.RedditorListingMixin' imported but unused
./build/lib/praw/models/listing/mixins/__init__.py:4:1: F401 '.rising.RisingListingMixin' imported but unused
./build/lib/praw/models/listing/mixins/__init__.py:5:1: F401 '.submission.SubmissionListingMixin' imported but unused
./build/lib/praw/models/listing/mixins/__init__.py:6:1: F401 '.subreddit.SubredditListingMixin' imported but unused
63    F401 '.auth.Auth' imported but unused
```

It seems [`setup.cfg`](https://github.com/praw-dev/praw/blob/db459269616a188a21e663527d3ebc204a35cf51/setup.cfg) should provide an exception to this.
PythonCoderAS: > setup.cfg

Flake8 is failing in this regard to everything. I set a debug branch, it seems that the current directory is being copied over to a build directory.
jarhill0: Pushed requested changes. I'm sure CI will still fail because of the flake8 thing though.
jarhill0: Oops, I didn't actually commit the changes. That's fixed now.
PythonCoderAS: The ClientException is untested. Try making an UnitTest for it. 

Patch:
diff --git a/praw/reddit.py b/praw/reddit.py
index d704b93d..4481149f 100644
--- a/praw/reddit.py
+++ b/praw/reddit.py
@@ -553,6 +553,7 @@ class Reddit:
             Union[Dict[str, Union[str, Any]], bytes, IO, str]
         ] = None,
         files: Optional[Dict[str, IO]] = None,
+        json=None,
         method: str = "",
         params: Optional[Union[str, Dict[str, str]]] = None,
         path: str = "",
@@ -563,6 +564,9 @@ class Reddit:
             of the request (default: None).
         :param files: Dictionary, filename to file (like) object mapping
             (default: None).
+        :param json: JSON-serializable object to send in the body
+            of the request with a Content-Type header of application/json
+            (default: None). If ``json`` is provided, ``data`` should not be.
         :param method: The HTTP method (e.g., GET, POST, PUT, DELETE).
         :param params: The query parameters to add to the request (default:
             None).
@@ -571,7 +575,12 @@ class Reddit:
         """
         return self._objector.objectify(
             self.request(
-                data=data, files=files, method=method, params=params, path=path
+                data=data,
+                files=files,
+                json=json,
+                method=method,
+                params=params,
+                path=path,
             )
         )
 
@@ -597,15 +606,21 @@ class Reddit:
         data: Optional[
             Union[Dict[str, Union[str, Any]], bytes, IO, str]
         ] = None,
+        json=None,
     ) -> Any:
         """Return parsed objects returned from a PATCH request to ``path``.
 
         :param path: The path to fetch.
         :param data: Dictionary, bytes, or file-like object to send in the body
             of the request (default: None).
+        :param json: JSON-serializable object to send in the body
+            of the request with a Content-Type header of application/json
+            (default: None). If ``json`` is provided, ``data`` should not be.
 
         """
-        return self._objectify_request(data=data, method="PATCH", path=path)
+        return self._objectify_request(
+            data=data, method="PATCH", path=path, json=json
+        )
 
     def post(
         self,
@@ -615,6 +630,7 @@ class Reddit:
         ] = None,
         files: Optional[Dict[str, IO]] = None,
         params: Optional[Union[str, Dict[str, str]]] = None,
+        json=None,
     ) -> Any:
         """Return parsed objects returned from a POST request to ``path``.
 
@@ -625,13 +641,18 @@ class Reddit:
             (default: None).
         :param params: The query parameters to add to the request (default:
             None).
+        :param json: JSON-serializable object to send in the body
+            of the request with a Content-Type header of application/json
+            (default: None). If ``json`` is provided, ``data`` should not be.
 
         """
-        data = data or {}
+        if json is None:
+            data = data or {}
         try:
             return self._objectify_request(
                 data=data,
                 files=files,
+                json=json,
                 method="POST",
                 params=params,
                 path=path,
@@ -660,15 +681,21 @@ class Reddit:
         data: Optional[
             Union[Dict[str, Union[str, Any]], bytes, IO, str]
         ] = None,
+        json=None,
     ):
         """Return parsed objects returned from a PUT request to ``path``.
 
         :param path: The path to fetch.
         :param data: Dictionary, bytes, or file-like object to send in the body
             of the request (default: None).
+        :param json: JSON-serializable object to send in the body
+            of the request with a Content-Type header of application/json
+            (default: None). If ``json`` is provided, ``data`` should not be.
 
         """
-        return self._objectify_request(data=data, method="PUT", path=path,)
+        return self._objectify_request(
+            data=data, json=json, method="PUT", path=path
+        )
 
     def random_subreddit(self, nsfw: bool = False) -> Subreddit:
         """Return a random lazy instance of :class:`~.Subreddit`.
@@ -709,6 +736,7 @@ class Reddit:
             Union[Dict[str, Union[str, Any]], bytes, IO, str]
         ] = None,
         files: Optional[Dict[str, IO]] = None,
+        json=None,
     ) -> Any:
         """Return the parsed JSON data returned from a request to URL.
 
@@ -720,8 +748,15 @@ class Reddit:
             of the request (default: None).
         :param files: Dictionary, filename to file (like) object mapping
             (default: None).
+        :param json: JSON-serializable object to send in the body
+            of the request with a Content-Type header of application/json
+            (default: None). If ``json`` is provided, ``data`` should not be.
 
         """
+        if data and json:
+            raise ClientException(
+                "At most one of `data` and `json` is supported."
+            )
         try:
             return self._core.request(
                 method,
@@ -730,6 +765,7 @@ class Reddit:
                 files=files,
                 params=params,
                 timeout=self.config.timeout,
+                json=json,
             )
         except BadRequest as exception:
             try:


Test patch:
diff --git a/tests/unit/test_reddit.py b/tests/unit/test_reddit.py
index 9d8bd13a..d117cfae 100644
--- a/tests/unit/test_reddit.py
+++ b/tests/unit/test_reddit.py
@@ -269,6 +269,21 @@ class TestReddit(UnitTest):
             "Unexpected BadRequest without json body."
         )
 
+    def test_request__json_and_body(self):
+        reddit = Reddit(
+            client_id="dummy", client_secret="dummy", user_agent="dummy"
+        )
+        with pytest.raises(ClientException) as excinfo:
+            reddit.request(
+                method="POST",
+                path="/",
+                data={"key": "value"},
+                json={"key": "value"},
+            )
+        assert str(excinfo.value).startswith(
+            "At most one of `data` and `json` is supported."
+        )
+
     def test_submission(self):
         assert self.reddit.submission("2gmzqe").id == "2gmzqe"