Repository: praw-dev/praw

Issue:
Trying to sticky a post which is already stickied causes PRAW to exit script
**Describe the bug**

If a post (submission) is already stickied, and `mod.sticky(state=True)` is called on that submission, a script using praw exits because of a HTTP 409 error. Traceback shown below in "Code/Logs" section

**To Reproduce**

1. Make a post and sticky it as a mod
2. Try to call mod.sticky(state=True) on that same submission, without doing a check to see if it is already stickied



**Expected behavior**

I would expect nothing to happen where the post stays stickied and praw doesn't cause a python script to exit. 


**Code/Logs**

```python
sub = subreddit.submit(...) #this would be post details
sub.mod.sticky(state=True) #sticky post 1st time
sub.mod.sticky(state=True) #attempt to sticky again
```

Traceback:
```
    sub.mod.sticky(state=True)
  File "/usr/local/lib/python3.7/site-packages/praw/models/reddit/submission.py", line 566, in sticky
    API_PATH["sticky_submission"], data=data
  File "/usr/local/lib/python3.7/site-packages/praw/reddit.py", line 523, in post
    "POST", path, data=data or {}, files=files, params=params
  File "/usr/local/lib/python3.7/site-packages/praw/reddit.py", line 577, in request
    method, path, data=data, files=files, params=params
  File "/usr/local/lib/python3.7/site-packages/prawcore/sessions.py", line 185, in request
    params=params, url=url)
  File "/usr/local/lib/python3.7/site-packages/prawcore/sessions.py", line 130, in _request_with_retries
    raise self.STATUS_EXCEPTIONS[response.status_code](response)
prawcore.exceptions.Conflict: received 409 HTTP response
```

**System Info**
 - OS: Mac OS
 - Python: 3.7
 - PRAW Version: Able to reproduce on 6.3.1, 6.4.0



Discussion:
I've confirmed this behavior. I feel like this is an issue on Reddit's end, but PRAW should still do something to prevent this scenario from raising an exception.
I probably should've added a check to see if the thread is stickied first, but anyways, yeah it seems like this could probably be on Reddit's end as I hadn't encountered this error before and I hadn't made any code changes/praw version changes for at least 3 months
As this is mostly a Reddit issue, I'll note the error on the method documentation. 
@pythoncoderAS, that wasn't what I meant when I suggested that PRAW do something about this. Perhaps we could catch and silence this specific exception?

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,11 +1,12 @@
 import os
 import sys
 
-from praw import __version__
-
+# Do not touch these. They use the local PRAW over the global PRAW.
 sys.path.insert(0, ".")
 sys.path.insert(1, "..")
 
+from praw import __version__
+
 
 copyright = "2020, Bryce Boe"
 exclude_patterns = ["_build"]
diff --git a/praw/models/reddit/submission.py b/praw/models/reddit/submission.py
--- a/praw/models/reddit/submission.py
+++ b/praw/models/reddit/submission.py
@@ -2,6 +2,8 @@
 from typing import Any, Dict, List, Optional, TypeVar, Union
 from urllib.parse import urljoin
 
+from prawcore import Conflict
+
 from ...const import API_PATH
 from ...exceptions import InvalidURL
 from ...util.cache import cachedproperty
@@ -255,7 +257,9 @@ def spoiler(self):
             API_PATH["spoiler"], data={"id": self.thing.fullname}
         )
 
-    def sticky(self, state: bool = True, bottom: bool = True):
+    def sticky(
+        self, state: bool = True, bottom: bool = True,
+    ):
         """Set the submission's sticky state in its subreddit.
 
         :param state: (boolean) True sets the sticky for the submission, false
@@ -264,7 +268,11 @@ def sticky(self, state: bool = True, bottom: bool = True):
             sticky. If no top sticky exists, this submission will become the
             top sticky regardless (default: True).
 
-        This submission will replace an existing stickied submission if one
+        .. note:: When a submission is stickied two or more times, the Reddit
+            API responds with a 409 error that is raises as a ``Conflict`` by
+            PRAWCore. The method suppresses these ``Conflict`` errors.
+
+        This submission will replace the second stickied submission if one
         exists.
 
         For example:
@@ -278,9 +286,12 @@ def sticky(self, state: bool = True, bottom: bool = True):
         data = {"id": self.thing.fullname, "state": state}
         if not bottom:
             data["num"] = 1
-        return self.thing._reddit.post(
-            API_PATH["sticky_submission"], data=data
-        )
+        try:
+            return self.thing._reddit.post(
+                API_PATH["sticky_submission"], data=data
+            )
+        except Conflict:
+            pass
 
     def suggested_sort(self, sort: str = "blank"):
         """Set the suggested sort for the comments of the submission.


Test patch:
diff --git a/tests/integration/cassettes/TestSubmissionModeration.test_sticky__ignore_conflicts.json b/tests/integration/cassettes/TestSubmissionModeration.test_sticky__ignore_conflicts.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/cassettes/TestSubmissionModeration.test_sticky__ignore_conflicts.json
@@ -0,0 +1,349 @@
+{
+  "http_interactions": [
+    {
+      "recorded_at": "2020-02-23T00:58:09",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "grant_type=password&password=<PASSWORD>&username=<USERNAME>"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "Basic <BASIC_AUTH>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "61"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://www.reddit.com/api/v1/access_token"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"access_token\": \"<ACCESS_TOKEN>\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "118"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Sun, 23 Feb 2020 00:58:09 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Set-Cookie": [
+            "edgebucket=TTlS1rT0aStFza8slf; Domain=reddit.com; Max-Age=63071999; Path=/;  secure"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lga21952-LGA"
+          ],
+          "X-Timer": [
+            "S1582419489.322549,VS0,VE291"
+          ],
+          "cache-control": [
+            "max-age=0, must-revalidate"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://www.reddit.com/api/v1/access_token"
+      }
+    },
+    {
+      "recorded_at": "2020-02-23T00:58:10",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "api_type=json&id=t3_f1iczt&num=1&state=True"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "43"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "Cookie": [
+            "edgebucket=TTlS1rT0aStFza8slf"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://oauth.reddit.com/api/set_subreddit_sticky/?raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"message\": \"Conflict\", \"error\": 409}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "37"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Sun, 23 Feb 2020 00:58:10 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lga21981-LGA"
+          ],
+          "X-Timer": [
+            "S1582419490.064442,VS0,VE74"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "loid=00000000005bn5wlvy.2.1577584524387.Z0FBQUFBQmVVYzRpM0JZeGN0ejQ3dlVUQURValE0TFk0Mklsb3lJYmFTTUhhS1FQb2k1YWZrWlZRZXpRZUR1Z29ldzlfQUJwYUt3SV9QQTlUc0o1UW9VemwyTVpyRUwtSXZDSmVIT2F6R2FRdWNha0E3bmxzWXk5TjRwQjZsUGJ5aG9XSi1WczZBN00; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Tue, 22-Feb-2022 00:58:10 GMT; secure",
+            "session_tracker=CWQIRulA5z2zyKHuBH.0.1582419490108.Z0FBQUFBQmVVYzRpZFM5RFJ5bWI0UGs1dW5jRGlsWDNhdGU2UnFkLWlXYTdxU0QwMUs5V1NsY2JHcVlpcTlSZUl3NHhIOFdYODZ0aDdzMEVlMFFkMlRGR2NpOUhUS0c4M25Sb0Z4Y3czbWYxUjFuSEtpbGl6S1F4cnhjU0dOSkNzYThaanNJZGtpMGI; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sun, 23-Feb-2020 02:58:10 GMT; secure",
+            "loid=00000000005r9efth1.2.1582419490132.Z0FBQUFBQmVVYzRpMzlQQkxsVkhFRU9Xc2ZLLXNseE42NmFvbVhnMzFBZXkxdVRnMnZ1U2RpbzRmd0hoTF9kRlJLaGlvb01ZZUl3aS1QMkoxQUJyZFgxYlRyX2tGVS1jZlREYXpvVUFSSjlFWkVWWFI2aFd1V2E3eHJBVE0xTkVLdE1VX1dXSHRmeHg; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Tue, 22-Feb-2022 00:58:10 GMT; secure",
+            "session_tracker=Md5oirlsJ9n5KSKK8R.0.1582419490132.Z0FBQUFBQmVVYzRpbTd1X2VXcnJQWHBzVThoMEM4UGtLTlVqZXF0cmJPQS1WdkpjMEs1QlV4T3pEVXo3TzJpb3QzcFRLeHZZdG9rNHBGM1hzR0RPd3FZOFdnRUE2eXNTRmNmeHo4R1c4UEdPdWpHRUNYcUkwd0ItQU4tLWdCZWpTbXRHc19haHctdU0; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sun, 23-Feb-2020 02:58:10 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "598.0"
+          ],
+          "x-ratelimit-reset": [
+            "110"
+          ],
+          "x-ratelimit-used": [
+            "2"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 409,
+          "message": "Conflict"
+        },
+        "url": "https://oauth.reddit.com/api/set_subreddit_sticky/?raw_json=1"
+      }
+    },
+    {
+      "recorded_at": "2020-02-23T00:58:10",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "api_type=json&id=t3_f1iczt&num=1&state=True"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "43"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "Cookie": [
+            "edgebucket=TTlS1rT0aStFza8slf; loid=00000000005r9efth1.2.1582419490132.Z0FBQUFBQmVVYzRpMzlQQkxsVkhFRU9Xc2ZLLXNseE42NmFvbVhnMzFBZXkxdVRnMnZ1U2RpbzRmd0hoTF9kRlJLaGlvb01ZZUl3aS1QMkoxQUJyZFgxYlRyX2tGVS1jZlREYXpvVUFSSjlFWkVWWFI2aFd1V2E3eHJBVE0xTkVLdE1VX1dXSHRmeHg; session_tracker=Md5oirlsJ9n5KSKK8R.0.1582419490132.Z0FBQUFBQmVVYzRpbTd1X2VXcnJQWHBzVThoMEM4UGtLTlVqZXF0cmJPQS1WdkpjMEs1QlV4T3pEVXo3TzJpb3QzcFRLeHZZdG9rNHBGM1hzR0RPd3FZOFdnRUE2eXNTRmNmeHo4R1c4UEdPdWpHRUNYcUkwd0ItQU4tLWdCZWpTbXRHc19haHctdU0"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://oauth.reddit.com/api/set_subreddit_sticky/?raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"message\": \"Conflict\", \"error\": 409}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "37"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Sun, 23 Feb 2020 00:58:10 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lga21981-LGA"
+          ],
+          "X-Timer": [
+            "S1582419490.208103,VS0,VE63"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "loid=00000000005bn5wlvy.2.1577584524387.Z0FBQUFBQmVVYzRpV01xZlZxZnEwQUZta181ZHdpMUFDbXVYVUtLb3FlaVdUZ0FRMmtYb1ZQSi1oVW5WVXltcjZEM3diRE1NWFM3R0ZGOXVYWmVUWi04NGVXMU03ZTctSFg1S3dsZVVtWVFuSkp4QVBxVTJYUHJKQ0preGdTRnBmTDFqTldwZTNVYnQ; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Tue, 22-Feb-2022 00:58:10 GMT; secure",
+            "session_tracker=Md5oirlsJ9n5KSKK8R.0.1582419490240.Z0FBQUFBQmVVYzRpRFZVOWt6ZVprbksyZVRFQWk4TlZQTEFkc3Q0czF4eUlTVExTcWt5aHh1SGFmbzdPWXpLcU53bXNCdFkyc3JzRmRIblBVU1ZXWXp0NllKdGxKOVZpS1dDLTlIV2JEMEJhYzZFWjFOaWE1TExwYV9tQjhYNG1wMnlJN2ZVc0haUUQ; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sun, 23-Feb-2020 02:58:10 GMT; secure",
+            "session_tracker=Md5oirlsJ9n5KSKK8R.0.1582419490264.Z0FBQUFBQmVVYzRpNUtqY2pKM0hJUEFWc00zU2ZQZnZDTlF4UExWU3dFQ1pXWDBBR3gzR04xR3M5NDZpbzhJdjhfa0dsblNISGlCSloyNE1vd0pIRHdRamNWU1M1WUhqWlFRWUkxWVJtWlhlN0RPNVlEMk1tZnRUbElCUG9OZ0RWTnFtdU11SVd1RlE; Domain=reddit.com; Max-Age=7199; Path=/; expires=Sun, 23-Feb-2020 02:58:10 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "597.0"
+          ],
+          "x-ratelimit-reset": [
+            "110"
+          ],
+          "x-ratelimit-used": [
+            "3"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 409,
+          "message": "Conflict"
+        },
+        "url": "https://oauth.reddit.com/api/set_subreddit_sticky/?raw_json=1"
+      }
+    }
+  ],
+  "recorded_with": "betamax/0.8.1"
+}
\ No newline at end of file
diff --git a/tests/integration/models/reddit/test_submission.py b/tests/integration/models/reddit/test_submission.py
--- a/tests/integration/models/reddit/test_submission.py
+++ b/tests/integration/models/reddit/test_submission.py
@@ -531,6 +531,15 @@ def test_sticky__top(self):
         ):
             Submission(self.reddit, "4s2idz").mod.sticky(bottom=False)
 
+    @mock.patch("time.sleep", return_value=None)
+    def test_sticky__ignore_conflicts(self, _):
+        self.reddit.read_only = False
+        with self.recorder.use_cassette(
+            "TestSubmissionModeration.test_sticky__ignore_conflicts"
+        ):
+            Submission(self.reddit, "f1iczt").mod.sticky(bottom=False)
+            Submission(self.reddit, "f1iczt").mod.sticky(bottom=False)
+
     def test_suggested_sort(self):
         self.reddit.read_only = False
         with self.recorder.use_cassette(