/
comment.py
310 lines (250 loc) · 11 KB
/
comment.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
"""Provide the Comment class."""
from ...exceptions import ClientException
from ...util.cache import cachedproperty
from ..comment_forest import CommentForest
from .base import RedditBase
from .mixins import (
FullnameMixin,
InboxableMixin,
ThingModerationMixin,
UserContentMixin,
)
from .redditor import Redditor
class Comment(InboxableMixin, UserContentMixin, FullnameMixin, RedditBase):
"""A class that represents a reddit comments.
**Typical Attributes**
This table describes attributes that typically belong to objects of this
class. Since attributes are dynamically provided (see
:ref:`determine-available-attributes-of-an-object`), there is not a
guarantee that these attributes will always be present, nor is this list
comprehensive in any way.
======================= ===================================================
Attribute Description
======================= ===================================================
``author`` Provides an instance of :class:`.Redditor`.
``body`` The body of the comment.
``created_utc`` Time the comment was created, represented in
`Unix Time`_.
``distinguished`` Whether or not the comment is distinguished.
``edited`` Whether or not the comment has been edited.
``id`` The ID of the comment.
``is_submitter`` Whether or not the comment author is also the
author of the submission.
``link_id`` The submission ID that the comment belongs to.
``parent_id`` The ID of the parent comment. If it is a top-level
comment, this returns the submission ID instead
(prefixed with 't3').
``permalink`` A permalink for the comment.
``replies`` Provides an instance of :class:`.CommentForest`.
``score`` The number of upvotes for the comment.
``stickied`` Whether or not the comment is stickied.
``submission`` Provides an instance of :class:`.Submission`. The
submission that the comment belongs to.
``subreddit`` Provides an instance of :class:`.Subreddit`. The
subreddit that the comment belongs to.
``subreddit_id`` The subreddit ID that the comment belongs to.
======================= ===================================================
.. _Unix Time: https://en.wikipedia.org/wiki/Unix_time
"""
MISSING_COMMENT_MESSAGE = (
"This comment does not appear to be in the comment tree"
)
STR_FIELD = "id"
@staticmethod
def id_from_url(url):
"""Get the ID of a comment from the full URL."""
parts = RedditBase._url_parts(url)
try:
comment_index = parts.index("comments")
except ValueError:
raise ClientException("Invalid URL: {}".format(url))
if len(parts) - 4 != comment_index:
raise ClientException("Invalid URL: {}".format(url))
return parts[-1]
@property
def kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["comment"]
@property
def is_root(self):
"""Return True when the comment is a top level comment."""
parent_type = self.parent_id.split("_", 1)[0]
return parent_type == self._reddit.config.kinds["submission"]
@cachedproperty
def mod(self):
"""Provide an instance of :class:`.CommentModeration`."""
return CommentModeration(self)
@property
def replies(self):
"""Provide an instance of :class:`.CommentForest`.
This property may return an empty list if the comment
has not been refreshed with :meth:`.refresh()`
Sort order and reply limit can be set with the ``reply_sort`` and
``reply_limit`` attributes before replies are fetched, including
any call to :meth:`.refresh`:
.. code:: python
comment.reply_sort = 'new'
comment.refresh()
replies = comment.replies
"""
if isinstance(self._replies, list):
self._replies = CommentForest(self.submission, self._replies)
return self._replies
@property
def submission(self):
"""Return the Submission object this comment belongs to."""
if not self._submission: # Comment not from submission
self._submission = self._reddit.submission(
self._extract_submission_id()
)
return self._submission
@submission.setter
def submission(self, submission):
"""Update the Submission associated with the Comment."""
submission._comments_by_id[self.name] = self
self._submission = submission
# pylint: disable=not-an-iterable
for reply in getattr(self, "replies", []):
reply.submission = submission
def __init__(
self,
reddit,
id=None, # pylint: disable=redefined-builtin
url=None,
_data=None,
):
"""Construct an instance of the Comment object."""
if [id, url, _data].count(None) != 2:
raise TypeError(
"Exactly one of `id`, `url`, or `_data` must be provided."
)
self._replies = self._submission = None
super(Comment, self).__init__(reddit, _data=_data)
if id:
self.id = id # pylint: disable=invalid-name
elif url:
self.id = self.id_from_url(url)
else:
self._fetched = True
def __setattr__(self, attribute, value):
"""Objectify author, replies, and subreddit."""
if attribute == "author":
value = Redditor.from_data(self._reddit, value)
elif attribute == "replies":
if value == "":
value = []
else:
value = self._reddit._objector.objectify(value).children
attribute = "_replies"
elif attribute == "subreddit":
value = self._reddit.subreddit(value)
super(Comment, self).__setattr__(attribute, value)
def _extract_submission_id(self):
if "context" in self.__dict__:
return self.context.rsplit("/", 4)[1]
return self.link_id.split("_", 1)[1]
def parent(self):
"""Return the parent of the comment.
The returned parent will be an instance of either
:class:`.Comment`, or :class:`.Submission`.
If this comment was obtained through a :class:`.Submission`, then its
entire ancestry should be immediately available, requiring no extra
network requests. However, if this comment was obtained through other
means, e.g., ``reddit.comment('COMMENT_ID')``, or
``reddit.inbox.comment_replies``, then the returned parent may be a
lazy instance of either :class:`.Comment`, or :class:`.Submission`.
Lazy Comment Example:
.. code:: python
comment = reddit.comment('cklhv0f')
parent = comment.parent()
# `replies` is empty until the comment is refreshed
print(parent.replies) # Output: []
parent.refresh()
print(parent.replies) # Output is at least: [Comment(id='cklhv0f')]
.. warning:: Successive calls to :meth:`.parent()` may result in a
network request per call when the comment is not obtained through a
:class:`.Submission`. See below for an example of how to minimize
requests.
If you have a deeply nested comment and wish to most efficiently
discover its top-most :class:`.Comment` ancestor you can chain
successive calls to :meth:`.parent()` with calls to :meth:`.refresh()`
at every 9 levels. For example:
.. code:: python
comment = reddit.comment('dkk4qjd')
ancestor = comment
refresh_counter = 0
while not ancestor.is_root:
ancestor = ancestor.parent()
if refresh_counter % 9 == 0:
ancestor.refresh()
refresh_counter += 1
print('Top-most Ancestor: {}'.format(ancestor))
The above code should result in 5 network requests to Reddit. Without
the calls to :meth:`.refresh()` it would make at least 31 network
requests.
"""
# pylint: disable=no-member
if self.parent_id == self.submission.fullname:
return self.submission
if self.parent_id in self.submission._comments_by_id:
# The Comment already exists, so simply return it
return self.submission._comments_by_id[self.parent_id]
# pylint: enable=no-member
parent = Comment(self._reddit, self.parent_id.split("_", 1)[1])
parent._submission = self.submission
return parent
def refresh(self):
"""Refresh the comment's attributes.
If using :meth:`.Reddit.comment` this method must be called in order to
obtain the comment's replies.
Example usage:
.. code:: python
comment = reddit.comment('dkk4qjd')
comment.refresh()
"""
if "context" in self.__dict__: # Using hasattr triggers a fetch
comment_path = self.context.split("?", 1)[0]
else:
comment_path = "{}_/{}".format(
self.submission._info_path(), # pylint: disable=no-member
self.id,
)
# The context limit appears to be 8, but let's ask for more anyway.
params = {"context": 100}
if "reply_limit" in self.__dict__:
params["limit"] = self.reply_limit
if "reply_sort" in self.__dict__:
params["sort"] = self.reply_sort
comment_list = self._reddit.get(comment_path, params=params)[
1
].children
if not comment_list:
raise ClientException(self.MISSING_COMMENT_MESSAGE)
# With context, the comment may be nested so we have to find it
comment = None
queue = comment_list[:]
while queue and (comment is None or comment.id != self.id):
comment = queue.pop()
if isinstance(comment, Comment):
queue.extend(comment._replies)
if comment.id != self.id:
raise ClientException(self.MISSING_COMMENT_MESSAGE)
if self._submission is not None:
del comment.__dict__["_submission"] # Don't replace if set
self.__dict__.update(comment.__dict__)
for reply in comment_list:
reply.submission = self.submission
return self
class CommentModeration(ThingModerationMixin):
"""Provide a set of functions pertaining to Comment moderation.
Example usage:
.. code:: python
comment = reddit.comment('dkk4qjd')
comment.mod.approve()
"""
REMOVAL_MESSAGE_API = "removal_comment_message"
def __init__(self, comment):
"""Create a CommentModeration instance.
:param comment: The comment to moderate.
"""
self.thing = comment