We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Hard-coded limit in stream_generator() function.
## Issue Description

When I'm create a subreddit read stream, I cannot configure how many items from the past to take. The value 100 is hard-coded in the function stream_generator(). As a result, if I want to start reading comments in subreddit from the current time, I still get up to 100 previous ones and I need to filter them. In my opinion, this value should be passed in the kwargs of function stream_generator().


## System Information

    PRAW Version: 5.4.0
  Python Version: 3.6
Operating System: Debian 9


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `praw/models/util.py`
Content:
```
1 """Provide helper classes used by other models."""
2 import random
3 import time
4 
5 
6 class BoundedSet(object):
7     """A set with a maximum size that evicts the oldest items when necessary.
8 
9     This class does not implement the complete set interface.
10     """
11 
12     def __init__(self, max_items):
13         """Construct an instance of the BoundedSet."""
14         self.max_items = max_items
15         self._fifo = []
16         self._set = set()
17 
18     def __contains__(self, item):
19         """Test if the BoundedSet contains item."""
20         return item in self._set
21 
22     def add(self, item):
23         """Add an item to the set discarding the oldest item if necessary."""
24         if len(self._set) == self.max_items:
25             self._set.remove(self._fifo.pop(0))
26         self._fifo.append(item)
27         self._set.add(item)
28 
29 
30 class ExponentialCounter(object):
31     """A class to provide an exponential counter with jitter."""
32 
33     def __init__(self, max_counter):
34         """Initialize an instance of ExponentialCounter.
35 
36         :param max_counter: The maximum base value. Note that the computed
37             value may be 3.125% higher due to jitter.
38         """
39         self._base = 1
40         self._max = max_counter
41 
42     def counter(self):
43         """Increment the counter and return the current value with jitter."""
44         max_jitter = self._base / 16.
45         value = self._base + random.random() * max_jitter - max_jitter / 2
46         self._base = min(self._base * 2, self._max)
47         return value
48 
49     def reset(self):
50         """Reset the counter to 1."""
51         self._base = 1
52 
53 
54 def permissions_string(permissions, known_permissions):
55     """Return a comma separated string of permission changes.
56 
57     :param permissions: A list of strings, or ``None``. These strings can
58        exclusively contain ``+`` or ``-`` prefixes, or contain no prefixes at
59        all. When prefixed, the resulting string will simply be the joining of
60        these inputs. When not prefixed, all permissions are considered to be
61        additions, and all permissions in the ``known_permissions`` set that
62        aren't provided are considered to be removals. When None, the result is
63        ``+all``.
64     :param known_permissions: A set of strings representing the available
65        permissions.
66 
67     """
68     to_set = []
69     if permissions is None:
70         to_set = ['+all']
71     else:
72         to_set = ['-all']
73         omitted = sorted(known_permissions - set(permissions))
74         to_set.extend('-{}'.format(x) for x in omitted)
75         to_set.extend('+{}'.format(x) for x in permissions)
76     return ','.join(to_set)
77 
78 
79 def stream_generator(function, pause_after=None):
80     """Yield new items from ListingGenerators and ``None`` when paused.
81 
82     :param function: A callable that returns a ListingGenerator, e.g.
83        ``subreddit.comments`` or ``subreddit.new``.
84 
85     :param pause_after: An integer representing the number of requests that
86         result in no new items before this function yields ``None``,
87         effectively introducing a pause into the stream. A negative value
88         yields ``None`` after items from a single response have been yielded,
89         regardless of number of new items obtained in that response. A value of
90         ``0`` yields ``None`` after every response resulting in no new items,
91         and a value of ``None`` never introduces a pause (default: None).
92 
93     .. note:: This function internally uses an exponential delay with jitter
94        between subsequent responses that contain no new results, up to a
95        maximum delay of just over a 16 seconds. In practice that means that the
96        time before pause for ``pause_after=N+1`` is approximately twice the
97        time before pause for ``pause_after=N``.
98 
99     For example, to create a stream of comment replies, try:
100 
101     .. code:: python
102 
103        reply_function = reddit.inbox.comment_replies
104        for reply in praw.models.util.stream_generator(reply_function):
105            print(reply)
106 
107     To pause a comment stream after six responses with no new
108     comments, try:
109 
110     .. code:: python
111 
112        subreddit = reddit.subreddit('redditdev')
113        for comment in subreddit.stream.comments(pause_after=6):
114            if comment is None:
115                break
116            print(comment)
117 
118     To resume fetching comments after a pause, try:
119 
120     .. code:: python
121 
122        subreddit = reddit.subreddit('help')
123        comment_stream = subreddit.stream.comments(pause_after=5)
124 
125        for comment in comment_stream:
126            if comment is None:
127                break
128            print(comment)
129        # Do any other processing, then try to fetch more data
130        for comment in comment_stream:
131            if comment is None:
132                break
133            print(comment)
134 
135     To bypass the internal exponential backoff, try the following. This
136     approach is useful if you are monitoring a subreddit with infrequent
137     activity, and you want the to consistently learn about new items from the
138     stream as soon as possible, rather than up to a delay of just over sixteen
139     seconds.
140 
141     .. code:: python
142 
143        subreddit = reddit.subreddit('help')
144        for comment in subreddit.stream.comments(pause_after=0):
145            if comment is None:
146                continue
147            print(comment)
148 
149     """
150     before_fullname = None
151     exponential_counter = ExponentialCounter(max_counter=16)
152     seen_fullnames = BoundedSet(301)
153     without_before_counter = 0
154     responses_without_new = 0
155     valid_pause_after = pause_after is not None
156     while True:
157         found = False
158         newest_fullname = None
159         limit = 100
160         if before_fullname is None:
161             limit -= without_before_counter
162             without_before_counter = (without_before_counter + 1) % 30
163         for item in reversed(list(function(
164                 limit=limit, params={'before': before_fullname}))):
165             if item.fullname in seen_fullnames:
166                 continue
167             found = True
168             seen_fullnames.add(item.fullname)
169             newest_fullname = item.fullname
170             yield item
171         before_fullname = newest_fullname
172         if valid_pause_after and pause_after < 0:
173             yield None
174         elif found:
175             exponential_counter.reset()
176             responses_without_new = 0
177         else:
178             responses_without_new += 1
179             if valid_pause_after and responses_without_new > pause_after:
180                 exponential_counter.reset()
181                 responses_without_new = 0
182                 yield None
183             else:
184                 time.sleep(exponential_counter.counter())
185
```

Path: `praw/objector.py`
Content:
```
1 """Provides the Objector class."""
2 import re
3 
4 from .exceptions import APIException
5 
6 
7 class Objector(object):
8     """The objector builds :class:`.RedditBase` objects."""
9 
10     @staticmethod
11     def _camel_to_snake(name):
12         """Return `name` converted from camelCase to snake_case.
13 
14         Code from http://stackoverflow.com/a/1176023/.
15 
16         """
17         first_break_replaced = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
18         return re.sub(
19             '([a-z0-9])([A-Z])', r'\1_\2', first_break_replaced).lower()
20 
21     @classmethod
22     def _snake_case_keys(cls, dictionary):
23         """Return a copy of dictionary with keys converted to snake_case.
24 
25         :param dictionary: The dict to be corrected.
26 
27         """
28         return {cls._camel_to_snake(k): v for k, v in dictionary.items()}
29 
30     def __init__(self, reddit):
31         """Initialize an Objector instance.
32 
33         :param reddit: An instance of :class:`~.Reddit`.
34 
35         """
36         self.parsers = {}
37         self._reddit = reddit
38 
39     def kind(self, instance):
40         """Return the kind from the instance class.
41 
42         :param instance: An instance of a subclass of RedditBase.
43 
44         """
45         for key in self.parsers:
46             if isinstance(instance, self.parsers[key]):
47                 return key
48 
49     def _objectify_dict(self, data):
50         """Create RedditBase objects from dicts.
51 
52         :param data: The structured data, assumed to be a dict.
53         :returns: An instance of :class:`~.RedditBase`.
54 
55         """
56         if ({'conversation', 'messages', 'modActions'}.issubset(data)):
57             parser = self.parsers['ModmailConversation']
58         elif {'actionTypeId', 'author', 'date'}.issubset(data):
59             # Modmail mod action
60             data = self._snake_case_keys(data)
61             parser = self.parsers['ModmailAction']
62         elif {'bodyMarkdown', 'isInternal'}.issubset(data):
63             # Modmail message
64             data = self._snake_case_keys(data)
65             parser = self.parsers['ModmailMessage']
66         elif {'isAdmin', 'isDeleted'}.issubset(data):
67             # Modmail author
68             data = self._snake_case_keys(data)
69             # Prevent clobbering base-36 id
70             del data['id']
71             data['is_subreddit_mod'] = data.pop('is_mod')
72             parser = self.parsers[self._reddit.config.kinds['redditor']]
73         elif {'banStatus', 'muteStatus', 'recentComments'}.issubset(data):
74             # Modmail user
75             data = self._snake_case_keys(data)
76             data['created_string'] = data.pop('created')
77             parser = self.parsers[self._reddit.config.kinds['redditor']]
78         elif {'displayName', 'id', 'type'}.issubset(data):
79             # Modmail subreddit
80             data = self._snake_case_keys(data)
81             parser = self.parsers[self._reddit.config.kinds[data['type']]]
82         elif ({'date', 'id', 'name'}.issubset(data)
83               or {'id', 'name', 'permissions'}.issubset(data)):
84             parser = self.parsers[self._reddit.config.kinds['redditor']]
85         else:
86             if 'user' in data:
87                 parser = self.parsers[self._reddit.config.kinds['redditor']]
88                 data['user'] = parser.parse({'name': data['user']},
89                                             self._reddit)
90             return data
91         return parser.parse(data, self._reddit)
92 
93     def objectify(self, data):
94         """Create RedditBase objects from data.
95 
96         :param data: The structured data.
97         :returns: An instance of :class:`~.RedditBase`, or ``None`` if
98             given ``data`` is ``None``.
99 
100         """
101         # pylint: disable=too-many-return-statements
102         if data is None:  # 204 no content
103             return
104         if isinstance(data, list):
105             return [self.objectify(item) for item in data]
106         if 'kind' in data and data['kind'] in self.parsers:
107             parser = self.parsers[data['kind']]
108             return parser.parse(data['data'], self._reddit)
109         elif 'json' in data and 'data' in data['json']:
110             if 'things' in data['json']['data']:  # Submission.reply
111                 return self.objectify(data['json']['data']['things'])
112             if 'url' in data['json']['data']:  # Subreddit.submit
113                 # The URL is the URL to the submission, so it's removed.
114                 del data['json']['data']['url']
115                 parser = self.parsers[self._reddit.config.kinds['submission']]
116             else:
117                 parser = self.parsers['LiveUpdateEvent']
118             return parser.parse(data['json']['data'], self._reddit)
119         elif 'json' in data and 'errors' in data['json']:
120             errors = data['json']['errors']
121             if len(errors) == 1:
122                 raise APIException(*errors[0])
123             assert not errors
124 
125         elif isinstance(data, dict):
126             return self._objectify_dict(data)
127 
128         return data
129 
130     def register(self, kind, cls):
131         """Register a class for a given kind.
132 
133         :param kind: The kind in the parsed data to map to ``cls``.
134         :param cls: A RedditBase class.
135 
136         """
137         self.parsers[kind] = cls
138
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/praw/models/util.py b/praw/models/util.py
--- a/praw/models/util.py
+++ b/praw/models/util.py
@@ -76,7 +76,7 @@
     return ','.join(to_set)
 
 
-def stream_generator(function, pause_after=None):
+def stream_generator(function, pause_after=None, skip_existing=False):
     """Yield new items from ListingGenerators and ``None`` when paused.
 
     :param function: A callable that returns a ListingGenerator, e.g.
@@ -90,6 +90,10 @@
         ``0`` yields ``None`` after every response resulting in no new items,
         and a value of ``None`` never introduces a pause (default: None).
 
+    :param skip_existing: When True does not yield any results from the first
+        request thereby skipping any items that existed in the stream prior to
+        starting the stream (default: False).
+
     .. note:: This function internally uses an exponential delay with jitter
        between subsequent responses that contain no new results, up to a
        maximum delay of just over a 16 seconds. In practice that means that the
@@ -159,8 +163,10 @@
             found = True
             seen_fullnames.add(item.fullname)
             newest_fullname = item.fullname
-            yield item
+            if not skip_existing:
+                yield item
         before_fullname = newest_fullname
+        skip_existing = False
         if valid_pause_after and pause_after < 0:
             yield None
         elif found:
diff --git a/praw/objector.py b/praw/objector.py
--- a/praw/objector.py
+++ b/praw/objector.py
@@ -42,9 +42,12 @@
         :param instance: An instance of a subclass of RedditBase.
 
         """
+        retval = None
         for key in self.parsers:
             if isinstance(instance, self.parsers[key]):
-                return key
+                retval = key
+                break
+        return retval
 
     def _objectify_dict(self, data):
         """Create RedditBase objects from dicts.
@@ -100,7 +103,7 @@
         """
         # pylint: disable=too-many-return-statements
         if data is None:  # 204 no content
-            return
+            return None
         if isinstance(data, list):
             return [self.objectify(item) for item in data]
         if 'kind' in data and data['kind'] in self.parsers:

{"golden_diff": "diff --git a/praw/models/util.py b/praw/models/util.py\n--- a/praw/models/util.py\n+++ b/praw/models/util.py\n@@ -76,7 +76,7 @@\n     return ','.join(to_set)\n \n \n-def stream_generator(function, pause_after=None):\n+def stream_generator(function, pause_after=None, skip_existing=False):\n     \"\"\"Yield new items from ListingGenerators and ``None`` when paused.\n \n     :param function: A callable that returns a ListingGenerator, e.g.\n@@ -90,6 +90,10 @@\n         ``0`` yields ``None`` after every response resulting in no new items,\n         and a value of ``None`` never introduces a pause (default: None).\n \n+    :param skip_existing: When True does not yield any results from the first\n+        request thereby skipping any items that existed in the stream prior to\n+        starting the stream (default: False).\n+\n     .. note:: This function internally uses an exponential delay with jitter\n        between subsequent responses that contain no new results, up to a\n        maximum delay of just over a 16 seconds. In practice that means that the\n@@ -159,8 +163,10 @@\n             found = True\n             seen_fullnames.add(item.fullname)\n             newest_fullname = item.fullname\n-            yield item\n+            if not skip_existing:\n+                yield item\n         before_fullname = newest_fullname\n+        skip_existing = False\n         if valid_pause_after and pause_after < 0:\n             yield None\n         elif found:\ndiff --git a/praw/objector.py b/praw/objector.py\n--- a/praw/objector.py\n+++ b/praw/objector.py\n@@ -42,9 +42,12 @@\n         :param instance: An instance of a subclass of RedditBase.\n \n         \"\"\"\n+        retval = None\n         for key in self.parsers:\n             if isinstance(instance, self.parsers[key]):\n-                return key\n+                retval = key\n+                break\n+        return retval\n \n     def _objectify_dict(self, data):\n         \"\"\"Create RedditBase objects from dicts.\n@@ -100,7 +103,7 @@\n         \"\"\"\n         # pylint: disable=too-many-return-statements\n         if data is None:  # 204 no content\n-            return\n+            return None\n         if isinstance(data, list):\n             return [self.objectify(item) for item in data]\n         if 'kind' in data and data['kind'] in self.parsers:\n", "issue": "Hard-coded limit in stream_generator() function.\n## Issue Description\r\n\r\nWhen I'm create a subreddit read stream, I cannot configure how many items from the past to take. The value 100 is hard-coded in the function stream_generator(). As a result, if I want to start reading comments in subreddit from the current time, I still get up to 100 previous ones and I need to filter them. In my opinion, this value should be passed in the kwargs of function stream_generator().\r\n\r\n\r\n## System Information\r\n\r\n    PRAW Version: 5.4.0\r\n  Python Version: 3.6\r\nOperating System: Debian 9\r\n\n", "before_files": [{"content": "\"\"\"Provide helper classes used by other models.\"\"\"\nimport random\nimport time\n\n\nclass BoundedSet(object):\n    \"\"\"A set with a maximum size that evicts the oldest items when necessary.\n\n    This class does not implement the complete set interface.\n    \"\"\"\n\n    def __init__(self, max_items):\n        \"\"\"Construct an instance of the BoundedSet.\"\"\"\n        self.max_items = max_items\n        self._fifo = []\n        self._set = set()\n\n    def __contains__(self, item):\n        \"\"\"Test if the BoundedSet contains item.\"\"\"\n        return item in self._set\n\n    def add(self, item):\n        \"\"\"Add an item to the set discarding the oldest item if necessary.\"\"\"\n        if len(self._set) == self.max_items:\n            self._set.remove(self._fifo.pop(0))\n        self._fifo.append(item)\n        self._set.add(item)\n\n\nclass ExponentialCounter(object):\n    \"\"\"A class to provide an exponential counter with jitter.\"\"\"\n\n    def __init__(self, max_counter):\n        \"\"\"Initialize an instance of ExponentialCounter.\n\n        :param max_counter: The maximum base value. Note that the computed\n            value may be 3.125% higher due to jitter.\n        \"\"\"\n        self._base = 1\n        self._max = max_counter\n\n    def counter(self):\n        \"\"\"Increment the counter and return the current value with jitter.\"\"\"\n        max_jitter = self._base / 16.\n        value = self._base + random.random() * max_jitter - max_jitter / 2\n        self._base = min(self._base * 2, self._max)\n        return value\n\n    def reset(self):\n        \"\"\"Reset the counter to 1.\"\"\"\n        self._base = 1\n\n\ndef permissions_string(permissions, known_permissions):\n    \"\"\"Return a comma separated string of permission changes.\n\n    :param permissions: A list of strings, or ``None``. These strings can\n       exclusively contain ``+`` or ``-`` prefixes, or contain no prefixes at\n       all. When prefixed, the resulting string will simply be the joining of\n       these inputs. When not prefixed, all permissions are considered to be\n       additions, and all permissions in the ``known_permissions`` set that\n       aren't provided are considered to be removals. When None, the result is\n       ``+all``.\n    :param known_permissions: A set of strings representing the available\n       permissions.\n\n    \"\"\"\n    to_set = []\n    if permissions is None:\n        to_set = ['+all']\n    else:\n        to_set = ['-all']\n        omitted = sorted(known_permissions - set(permissions))\n        to_set.extend('-{}'.format(x) for x in omitted)\n        to_set.extend('+{}'.format(x) for x in permissions)\n    return ','.join(to_set)\n\n\ndef stream_generator(function, pause_after=None):\n    \"\"\"Yield new items from ListingGenerators and ``None`` when paused.\n\n    :param function: A callable that returns a ListingGenerator, e.g.\n       ``subreddit.comments`` or ``subreddit.new``.\n\n    :param pause_after: An integer representing the number of requests that\n        result in no new items before this function yields ``None``,\n        effectively introducing a pause into the stream. A negative value\n        yields ``None`` after items from a single response have been yielded,\n        regardless of number of new items obtained in that response. A value of\n        ``0`` yields ``None`` after every response resulting in no new items,\n        and a value of ``None`` never introduces a pause (default: None).\n\n    .. note:: This function internally uses an exponential delay with jitter\n       between subsequent responses that contain no new results, up to a\n       maximum delay of just over a 16 seconds. In practice that means that the\n       time before pause for ``pause_after=N+1`` is approximately twice the\n       time before pause for ``pause_after=N``.\n\n    For example, to create a stream of comment replies, try:\n\n    .. code:: python\n\n       reply_function = reddit.inbox.comment_replies\n       for reply in praw.models.util.stream_generator(reply_function):\n           print(reply)\n\n    To pause a comment stream after six responses with no new\n    comments, try:\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('redditdev')\n       for comment in subreddit.stream.comments(pause_after=6):\n           if comment is None:\n               break\n           print(comment)\n\n    To resume fetching comments after a pause, try:\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('help')\n       comment_stream = subreddit.stream.comments(pause_after=5)\n\n       for comment in comment_stream:\n           if comment is None:\n               break\n           print(comment)\n       # Do any other processing, then try to fetch more data\n       for comment in comment_stream:\n           if comment is None:\n               break\n           print(comment)\n\n    To bypass the internal exponential backoff, try the following. This\n    approach is useful if you are monitoring a subreddit with infrequent\n    activity, and you want the to consistently learn about new items from the\n    stream as soon as possible, rather than up to a delay of just over sixteen\n    seconds.\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('help')\n       for comment in subreddit.stream.comments(pause_after=0):\n           if comment is None:\n               continue\n           print(comment)\n\n    \"\"\"\n    before_fullname = None\n    exponential_counter = ExponentialCounter(max_counter=16)\n    seen_fullnames = BoundedSet(301)\n    without_before_counter = 0\n    responses_without_new = 0\n    valid_pause_after = pause_after is not None\n    while True:\n        found = False\n        newest_fullname = None\n        limit = 100\n        if before_fullname is None:\n            limit -= without_before_counter\n            without_before_counter = (without_before_counter + 1) % 30\n        for item in reversed(list(function(\n                limit=limit, params={'before': before_fullname}))):\n            if item.fullname in seen_fullnames:\n                continue\n            found = True\n            seen_fullnames.add(item.fullname)\n            newest_fullname = item.fullname\n            yield item\n        before_fullname = newest_fullname\n        if valid_pause_after and pause_after < 0:\n            yield None\n        elif found:\n            exponential_counter.reset()\n            responses_without_new = 0\n        else:\n            responses_without_new += 1\n            if valid_pause_after and responses_without_new > pause_after:\n                exponential_counter.reset()\n                responses_without_new = 0\n                yield None\n            else:\n                time.sleep(exponential_counter.counter())\n", "path": "praw/models/util.py"}, {"content": "\"\"\"Provides the Objector class.\"\"\"\nimport re\n\nfrom .exceptions import APIException\n\n\nclass Objector(object):\n    \"\"\"The objector builds :class:`.RedditBase` objects.\"\"\"\n\n    @staticmethod\n    def _camel_to_snake(name):\n        \"\"\"Return `name` converted from camelCase to snake_case.\n\n        Code from http://stackoverflow.com/a/1176023/.\n\n        \"\"\"\n        first_break_replaced = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\n        return re.sub(\n            '([a-z0-9])([A-Z])', r'\\1_\\2', first_break_replaced).lower()\n\n    @classmethod\n    def _snake_case_keys(cls, dictionary):\n        \"\"\"Return a copy of dictionary with keys converted to snake_case.\n\n        :param dictionary: The dict to be corrected.\n\n        \"\"\"\n        return {cls._camel_to_snake(k): v for k, v in dictionary.items()}\n\n    def __init__(self, reddit):\n        \"\"\"Initialize an Objector instance.\n\n        :param reddit: An instance of :class:`~.Reddit`.\n\n        \"\"\"\n        self.parsers = {}\n        self._reddit = reddit\n\n    def kind(self, instance):\n        \"\"\"Return the kind from the instance class.\n\n        :param instance: An instance of a subclass of RedditBase.\n\n        \"\"\"\n        for key in self.parsers:\n            if isinstance(instance, self.parsers[key]):\n                return key\n\n    def _objectify_dict(self, data):\n        \"\"\"Create RedditBase objects from dicts.\n\n        :param data: The structured data, assumed to be a dict.\n        :returns: An instance of :class:`~.RedditBase`.\n\n        \"\"\"\n        if ({'conversation', 'messages', 'modActions'}.issubset(data)):\n            parser = self.parsers['ModmailConversation']\n        elif {'actionTypeId', 'author', 'date'}.issubset(data):\n            # Modmail mod action\n            data = self._snake_case_keys(data)\n            parser = self.parsers['ModmailAction']\n        elif {'bodyMarkdown', 'isInternal'}.issubset(data):\n            # Modmail message\n            data = self._snake_case_keys(data)\n            parser = self.parsers['ModmailMessage']\n        elif {'isAdmin', 'isDeleted'}.issubset(data):\n            # Modmail author\n            data = self._snake_case_keys(data)\n            # Prevent clobbering base-36 id\n            del data['id']\n            data['is_subreddit_mod'] = data.pop('is_mod')\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        elif {'banStatus', 'muteStatus', 'recentComments'}.issubset(data):\n            # Modmail user\n            data = self._snake_case_keys(data)\n            data['created_string'] = data.pop('created')\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        elif {'displayName', 'id', 'type'}.issubset(data):\n            # Modmail subreddit\n            data = self._snake_case_keys(data)\n            parser = self.parsers[self._reddit.config.kinds[data['type']]]\n        elif ({'date', 'id', 'name'}.issubset(data)\n              or {'id', 'name', 'permissions'}.issubset(data)):\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        else:\n            if 'user' in data:\n                parser = self.parsers[self._reddit.config.kinds['redditor']]\n                data['user'] = parser.parse({'name': data['user']},\n                                            self._reddit)\n            return data\n        return parser.parse(data, self._reddit)\n\n    def objectify(self, data):\n        \"\"\"Create RedditBase objects from data.\n\n        :param data: The structured data.\n        :returns: An instance of :class:`~.RedditBase`, or ``None`` if\n            given ``data`` is ``None``.\n\n        \"\"\"\n        # pylint: disable=too-many-return-statements\n        if data is None:  # 204 no content\n            return\n        if isinstance(data, list):\n            return [self.objectify(item) for item in data]\n        if 'kind' in data and data['kind'] in self.parsers:\n            parser = self.parsers[data['kind']]\n            return parser.parse(data['data'], self._reddit)\n        elif 'json' in data and 'data' in data['json']:\n            if 'things' in data['json']['data']:  # Submission.reply\n                return self.objectify(data['json']['data']['things'])\n            if 'url' in data['json']['data']:  # Subreddit.submit\n                # The URL is the URL to the submission, so it's removed.\n                del data['json']['data']['url']\n                parser = self.parsers[self._reddit.config.kinds['submission']]\n            else:\n                parser = self.parsers['LiveUpdateEvent']\n            return parser.parse(data['json']['data'], self._reddit)\n        elif 'json' in data and 'errors' in data['json']:\n            errors = data['json']['errors']\n            if len(errors) == 1:\n                raise APIException(*errors[0])\n            assert not errors\n\n        elif isinstance(data, dict):\n            return self._objectify_dict(data)\n\n        return data\n\n    def register(self, kind, cls):\n        \"\"\"Register a class for a given kind.\n\n        :param kind: The kind in the parsed data to map to ``cls``.\n        :param cls: A RedditBase class.\n\n        \"\"\"\n        self.parsers[kind] = cls\n", "path": "praw/objector.py"}], "after_files": [{"content": "\"\"\"Provide helper classes used by other models.\"\"\"\nimport random\nimport time\n\n\nclass BoundedSet(object):\n    \"\"\"A set with a maximum size that evicts the oldest items when necessary.\n\n    This class does not implement the complete set interface.\n    \"\"\"\n\n    def __init__(self, max_items):\n        \"\"\"Construct an instance of the BoundedSet.\"\"\"\n        self.max_items = max_items\n        self._fifo = []\n        self._set = set()\n\n    def __contains__(self, item):\n        \"\"\"Test if the BoundedSet contains item.\"\"\"\n        return item in self._set\n\n    def add(self, item):\n        \"\"\"Add an item to the set discarding the oldest item if necessary.\"\"\"\n        if len(self._set) == self.max_items:\n            self._set.remove(self._fifo.pop(0))\n        self._fifo.append(item)\n        self._set.add(item)\n\n\nclass ExponentialCounter(object):\n    \"\"\"A class to provide an exponential counter with jitter.\"\"\"\n\n    def __init__(self, max_counter):\n        \"\"\"Initialize an instance of ExponentialCounter.\n\n        :param max_counter: The maximum base value. Note that the computed\n            value may be 3.125% higher due to jitter.\n        \"\"\"\n        self._base = 1\n        self._max = max_counter\n\n    def counter(self):\n        \"\"\"Increment the counter and return the current value with jitter.\"\"\"\n        max_jitter = self._base / 16.\n        value = self._base + random.random() * max_jitter - max_jitter / 2\n        self._base = min(self._base * 2, self._max)\n        return value\n\n    def reset(self):\n        \"\"\"Reset the counter to 1.\"\"\"\n        self._base = 1\n\n\ndef permissions_string(permissions, known_permissions):\n    \"\"\"Return a comma separated string of permission changes.\n\n    :param permissions: A list of strings, or ``None``. These strings can\n       exclusively contain ``+`` or ``-`` prefixes, or contain no prefixes at\n       all. When prefixed, the resulting string will simply be the joining of\n       these inputs. When not prefixed, all permissions are considered to be\n       additions, and all permissions in the ``known_permissions`` set that\n       aren't provided are considered to be removals. When None, the result is\n       ``+all``.\n    :param known_permissions: A set of strings representing the available\n       permissions.\n\n    \"\"\"\n    to_set = []\n    if permissions is None:\n        to_set = ['+all']\n    else:\n        to_set = ['-all']\n        omitted = sorted(known_permissions - set(permissions))\n        to_set.extend('-{}'.format(x) for x in omitted)\n        to_set.extend('+{}'.format(x) for x in permissions)\n    return ','.join(to_set)\n\n\ndef stream_generator(function, pause_after=None, skip_existing=False):\n    \"\"\"Yield new items from ListingGenerators and ``None`` when paused.\n\n    :param function: A callable that returns a ListingGenerator, e.g.\n       ``subreddit.comments`` or ``subreddit.new``.\n\n    :param pause_after: An integer representing the number of requests that\n        result in no new items before this function yields ``None``,\n        effectively introducing a pause into the stream. A negative value\n        yields ``None`` after items from a single response have been yielded,\n        regardless of number of new items obtained in that response. A value of\n        ``0`` yields ``None`` after every response resulting in no new items,\n        and a value of ``None`` never introduces a pause (default: None).\n\n    :param skip_existing: When True does not yield any results from the first\n        request thereby skipping any items that existed in the stream prior to\n        starting the stream (default: False).\n\n    .. note:: This function internally uses an exponential delay with jitter\n       between subsequent responses that contain no new results, up to a\n       maximum delay of just over a 16 seconds. In practice that means that the\n       time before pause for ``pause_after=N+1`` is approximately twice the\n       time before pause for ``pause_after=N``.\n\n    For example to pause a comment stream after six responses with no new\n    comments, try:\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('redditdev')\n       for comment in subreddit.stream.comments(pause_after=6):\n           if comment is None:\n               break\n           print(comment)\n\n    To resume fetching comments after a pause, try:\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('help')\n       comment_stream = subreddit.stream.comments(pause_after=5)\n\n       for comment in comment_stream:\n           if comment is None:\n               break\n           print(comment)\n       # Do any other processing, then try to fetch more data\n       for comment in comment_stream:\n           if comment is None:\n               break\n           print(comment)\n\n    To bypass the internal exponential backoff, try the following. This\n    approach is useful if you are monitoring a subreddit with infrequent\n    activity, and you want the to consistently learn about new items from the\n    stream as soon as possible, rather than up to a delay of just over sixteen\n    seconds.\n\n    .. code:: python\n\n       subreddit = reddit.subreddit('help')\n       for comment in subreddit.stream.comments(pause_after=0):\n           if comment is None:\n               continue\n           print(comment)\n\n    \"\"\"\n    before_fullname = None\n    exponential_counter = ExponentialCounter(max_counter=16)\n    seen_fullnames = BoundedSet(301)\n    without_before_counter = 0\n    responses_without_new = 0\n    valid_pause_after = pause_after is not None\n    while True:\n        found = False\n        newest_fullname = None\n        limit = 100\n        if before_fullname is None:\n            limit -= without_before_counter\n            without_before_counter = (without_before_counter + 1) % 30\n        for item in reversed(list(function(\n                limit=limit, params={'before': before_fullname}))):\n            if item.fullname in seen_fullnames:\n                continue\n            found = True\n            seen_fullnames.add(item.fullname)\n            newest_fullname = item.fullname\n            if not skip_existing:\n                yield item\n        before_fullname = newest_fullname\n        skip_existing = False\n        if valid_pause_after and pause_after < 0:\n            yield None\n        elif found:\n            exponential_counter.reset()\n            responses_without_new = 0\n        else:\n            responses_without_new += 1\n            if valid_pause_after and responses_without_new > pause_after:\n                exponential_counter.reset()\n                responses_without_new = 0\n                yield None\n            else:\n                time.sleep(exponential_counter.counter())\n", "path": "praw/models/util.py"}, {"content": "\"\"\"Provides the Objector class.\"\"\"\nimport re\n\nfrom .exceptions import APIException\n\n\nclass Objector(object):\n    \"\"\"The objector builds :class:`.RedditBase` objects.\"\"\"\n\n    @staticmethod\n    def _camel_to_snake(name):\n        \"\"\"Return `name` converted from camelCase to snake_case.\n\n        Code from http://stackoverflow.com/a/1176023/.\n\n        \"\"\"\n        first_break_replaced = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\n        return re.sub(\n            '([a-z0-9])([A-Z])', r'\\1_\\2', first_break_replaced).lower()\n\n    @classmethod\n    def _snake_case_keys(cls, dictionary):\n        \"\"\"Return a copy of dictionary with keys converted to snake_case.\n\n        :param dictionary: The dict to be corrected.\n\n        \"\"\"\n        return {cls._camel_to_snake(k): v for k, v in dictionary.items()}\n\n    def __init__(self, reddit):\n        \"\"\"Initialize an Objector instance.\n\n        :param reddit: An instance of :class:`~.Reddit`.\n\n        \"\"\"\n        self.parsers = {}\n        self._reddit = reddit\n\n    def kind(self, instance):\n        \"\"\"Return the kind from the instance class.\n\n        :param instance: An instance of a subclass of RedditBase.\n\n        \"\"\"\n        retval = None\n        for key in self.parsers:\n            if isinstance(instance, self.parsers[key]):\n                retval = key\n                break\n        return retval\n\n    def _objectify_dict(self, data):\n        \"\"\"Create RedditBase objects from dicts.\n\n        :param data: The structured data, assumed to be a dict.\n        :returns: An instance of :class:`~.RedditBase`.\n\n        \"\"\"\n        if ({'conversation', 'messages', 'modActions'}.issubset(data)):\n            parser = self.parsers['ModmailConversation']\n        elif {'actionTypeId', 'author', 'date'}.issubset(data):\n            # Modmail mod action\n            data = self._snake_case_keys(data)\n            parser = self.parsers['ModmailAction']\n        elif {'bodyMarkdown', 'isInternal'}.issubset(data):\n            # Modmail message\n            data = self._snake_case_keys(data)\n            parser = self.parsers['ModmailMessage']\n        elif {'isAdmin', 'isDeleted'}.issubset(data):\n            # Modmail author\n            data = self._snake_case_keys(data)\n            # Prevent clobbering base-36 id\n            del data['id']\n            data['is_subreddit_mod'] = data.pop('is_mod')\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        elif {'banStatus', 'muteStatus', 'recentComments'}.issubset(data):\n            # Modmail user\n            data = self._snake_case_keys(data)\n            data['created_string'] = data.pop('created')\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        elif {'displayName', 'id', 'type'}.issubset(data):\n            # Modmail subreddit\n            data = self._snake_case_keys(data)\n            parser = self.parsers[self._reddit.config.kinds[data['type']]]\n        elif ({'date', 'id', 'name'}.issubset(data)\n              or {'id', 'name', 'permissions'}.issubset(data)):\n            parser = self.parsers[self._reddit.config.kinds['redditor']]\n        else:\n            if 'user' in data:\n                parser = self.parsers[self._reddit.config.kinds['redditor']]\n                data['user'] = parser.parse({'name': data['user']},\n                                            self._reddit)\n            return data\n        return parser.parse(data, self._reddit)\n\n    def objectify(self, data):\n        \"\"\"Create RedditBase objects from data.\n\n        :param data: The structured data.\n        :returns: An instance of :class:`~.RedditBase`, or ``None`` if\n            given ``data`` is ``None``.\n\n        \"\"\"\n        # pylint: disable=too-many-return-statements\n        if data is None:  # 204 no content\n            return None\n        if isinstance(data, list):\n            return [self.objectify(item) for item in data]\n        if 'kind' in data and data['kind'] in self.parsers:\n            parser = self.parsers[data['kind']]\n            return parser.parse(data['data'], self._reddit)\n        elif 'json' in data and 'data' in data['json']:\n            if 'things' in data['json']['data']:  # Submission.reply\n                return self.objectify(data['json']['data']['things'])\n            if 'url' in data['json']['data']:  # Subreddit.submit\n                # The URL is the URL to the submission, so it's removed.\n                del data['json']['data']['url']\n                parser = self.parsers[self._reddit.config.kinds['submission']]\n            else:\n                parser = self.parsers['LiveUpdateEvent']\n            return parser.parse(data['json']['data'], self._reddit)\n        elif 'json' in data and 'errors' in data['json']:\n            errors = data['json']['errors']\n            if len(errors) == 1:\n                raise APIException(*errors[0])\n            assert not errors\n\n        elif isinstance(data, dict):\n            return self._objectify_dict(data)\n\n        return data\n\n    def register(self, kind, cls):\n        \"\"\"Register a class for a given kind.\n\n        :param kind: The kind in the parsed data to map to ``cls``.\n        :param cls: A RedditBase class.\n\n        \"\"\"\n        self.parsers[kind] = cls\n", "path": "praw/objector.py"}]}