diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4e87845cb..a88de57e7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -58,5 +58,4 @@ repos:
- repo: https://github.com/LilSpazJoekp/docstrfmt
hooks:
- id: docstrfmt
- require_serial: true
rev: v1.5.1
diff --git a/CHANGES.rst b/CHANGES.rst
index c75bfe733..cb824aa54 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,7 +1,7 @@
Change Log
==========
-PRAW follows `semantic versioning `_.
+PRAW follows `semantic versioning `_.
Unreleased
----------
diff --git a/praw/config.py b/praw/config.py
index 5c0b6b3d5..a89f66d1c 100644
--- a/praw/config.py
+++ b/praw/config.py
@@ -6,6 +6,7 @@
import sys
from pathlib import Path
from threading import Lock
+from typing import Any
from .exceptions import ClientException
@@ -32,13 +33,13 @@ class Config:
}
@staticmethod
- def _config_boolean(item: bool | str) -> bool: # noqa: ANN001
+ def _config_boolean(item: bool | str) -> bool:
if isinstance(item, bool):
return item
return item.lower() in {"1", "yes", "true", "on"}
@classmethod
- def _load_config(cls, *, config_interpolation: str | None = None): # noqa: ANN001
+ def _load_config(cls, *, config_interpolation: str | None = None):
"""Attempt to load settings from various praw.ini files."""
if config_interpolation is not None:
interpolator_class = cls.INTERPOLATION_LEVEL[config_interpolation]()
@@ -97,17 +98,19 @@ def __init__(
self._initialize_attributes()
- def _fetch(self, key): # noqa: ANN001
+ def _fetch(self, key: str) -> Any:
value = self.custom[key]
del self.custom[key]
return value
- def _fetch_default(self, key, *, default=None): # noqa: ANN001
+ def _fetch_default(
+ self, key: str, *, default: bool | float | str | None = None
+ ) -> Any:
if key not in self.custom:
return default
return self._fetch(key)
- def _fetch_or_not_set(self, key): # noqa: ANN001
+ def _fetch_or_not_set(self, key: str) -> Any | _NotSet:
if key in self._settings: # Passed in values have the highest priority
return self._fetch(key)
@@ -117,7 +120,7 @@ def _fetch_or_not_set(self, key): # noqa: ANN001
# Environment variables have higher priority than praw.ini settings
return env_value or ini_value or self.CONFIG_NOT_SET
- def _initialize_attributes(self): # noqa: ANN001
+ def _initialize_attributes(self):
self._short_url = self._fetch_default("short_url") or self.CONFIG_NOT_SET
self.check_for_async = self._config_boolean(
self._fetch_default("check_for_async", default=True)
diff --git a/praw/endpoints.py b/praw/endpoints.py
index d1b6a8151..1c4f12df1 100644
--- a/praw/endpoints.py
+++ b/praw/endpoints.py
@@ -1,5 +1,4 @@
"""List of API endpoints PRAW knows about."""
-# flake8: noqa
# fmt: off
API_PATH = {
"about_edited": "r/{subreddit}/about/edited/",
diff --git a/praw/exceptions.py b/praw/exceptions.py
index 619ef070c..98b7dec02 100644
--- a/praw/exceptions.py
+++ b/praw/exceptions.py
@@ -9,12 +9,13 @@
"""
from __future__ import annotations
+from typing import Any
from warnings import warn
from .util import _deprecate_args
-class PRAWException(Exception): # noqa: N818
+class PRAWException(Exception):
"""The base PRAW Exception that all other exception classes extend."""
@@ -79,7 +80,7 @@ class ClientException(PRAWException):
class DuplicateReplaceException(ClientException):
"""Indicate exceptions that involve the replacement of :class:`.MoreComments`."""
- def __init__(self) -> None:
+ def __init__(self):
"""Initialize a :class:`.DuplicateReplaceException` instance."""
super().__init__(
"A duplicate comment has been detected. Are you attempting to call"
@@ -101,7 +102,7 @@ def __init__(self, template_id: str):
class InvalidImplicitAuth(ClientException):
"""Indicate exceptions where an implicit auth type is used incorrectly."""
- def __init__(self) -> None:
+ def __init__(self):
"""Initialize an :class:`.InvalidImplicitAuth` instance."""
super().__init__("Implicit authorization can only be used with installed apps.")
@@ -189,7 +190,7 @@ def __init__(self, message: str, exception: Exception | None):
class MediaPostFailed(WebSocketException):
"""Indicate exceptions where media uploads failed.."""
- def __init__(self) -> None:
+ def __init__(self):
"""Initialize a :class:`.MediaPostFailed` instance."""
super().__init__(
"The attempted media upload action has failed. Possible causes include the"
@@ -287,7 +288,7 @@ def __init__(
self.items = self.parse_exception_list(items)
super().__init__(*self.items)
- def _get_old_attr(self, attrname): # noqa: ANN001
+ def _get_old_attr(self, attrname: str) -> Any:
warn(
f"Accessing attribute '{attrname}' through APIException is deprecated."
" This behavior will be removed in PRAW 8.0. Check out"
diff --git a/praw/models/base.py b/praw/models/base.py
index 5df3a4a1b..8e78698d0 100644
--- a/praw/models/base.py
+++ b/praw/models/base.py
@@ -13,7 +13,7 @@ class PRAWBase:
@staticmethod
def _safely_add_arguments(
- *, arguments, key, **new_arguments # noqa: ANN001,ANN003,ANN205
+ *, arguments: dict[str, Any], key: str, **new_arguments: Any
):
"""Replace arguments[key] with a deepcopy and update.
diff --git a/praw/models/comment_forest.py b/praw/models/comment_forest.py
index cf235d096..a34f37325 100644
--- a/praw/models/comment_forest.py
+++ b/praw/models/comment_forest.py
@@ -20,7 +20,11 @@ class CommentForest:
"""
@staticmethod
- def _gather_more_comments(tree, *, parent_tree=None): # noqa: ANN001,ANN205
+ def _gather_more_comments(
+ tree: list[praw.models.MoreComments],
+ *,
+ parent_tree: list[praw.models.MoreComments] | None = None,
+ ) -> list[MoreComments]:
"""Return a list of :class:`.MoreComments` objects obtained from tree."""
more_comments = []
queue = [(None, x) for x in tree]
@@ -57,27 +61,11 @@ def __getitem__(self, index: int) -> praw.models.Comment:
"""
return self._comments[index]
- def __init__(
- self,
- submission: praw.models.Submission,
- comments: list[praw.models.Comment] | None = None,
- ):
- """Initialize a :class:`.CommentForest` instance.
-
- :param submission: An instance of :class:`.Submission` that is the parent of the
- comments.
- :param comments: Initialize the forest with a list of comments (default:
- ``None``).
-
- """
- self._comments = comments
- self._submission = submission
-
def __len__(self) -> int:
"""Return the number of top-level comments in the forest."""
return len(self._comments)
- def _insert_comment(self, comment): # noqa: ANN001
+ def _insert_comment(self, comment: praw.models.Comment):
if comment.name in self._submission._comments_by_id:
raise DuplicateReplaceException
comment.submission = self._submission
@@ -91,7 +79,7 @@ def _insert_comment(self, comment): # noqa: ANN001
parent = self._submission._comments_by_id[comment.parent_id]
parent.replies._comments.append(comment)
- def _update(self, comments): # noqa: ANN001
+ def _update(self, comments: list[praw.models.Comment]):
self._comments = comments
for comment in comments:
comment.submission = self._submission
@@ -114,6 +102,22 @@ def list( # noqa: A003
queue.extend(comment.replies)
return comments
+ def __init__(
+ self,
+ submission: praw.models.Submission,
+ comments: list[praw.models.Comment] | None = None,
+ ):
+ """Initialize a :class:`.CommentForest` instance.
+
+ :param submission: An instance of :class:`.Submission` that is the parent of the
+ comments.
+ :param comments: Initialize the forest with a list of comments (default:
+ ``None``).
+
+ """
+ self._comments = comments
+ self._submission = submission
+
@_deprecate_args("limit", "threshold")
def replace_more(
self, *, limit: int | None = 32, threshold: int = 0
diff --git a/praw/models/helpers.py b/praw/models/helpers.py
index ec3acdc24..c562755ef 100644
--- a/praw/models/helpers.py
+++ b/praw/models/helpers.py
@@ -30,8 +30,8 @@ def __call__(
) -> list[praw.models.Draft] | praw.models.Draft:
"""Return a list of :class:`.Draft` instances.
- :param draft_id: When provided, return :class:`.Draft` instance (default:
- ``None``).
+ :param draft_id: When provided, this returns a :class:`.Draft` instance
+ (default: ``None``).
:returns: A :class:`.Draft` instance if ``draft_id`` is provided. Otherwise, a
list of :class:`.Draft` objects.
@@ -54,7 +54,7 @@ def __call__(
return Draft(self._reddit, id=draft_id)
return self._draft_list()
- def _draft_list(self) -> list[praw.models.Draft]: # noqa: ANN001
+ def _draft_list(self) -> list[praw.models.Draft]:
"""Get a list of :class:`.Draft` instances.
:returns: A list of :class:`.Draft` instances.
@@ -139,9 +139,7 @@ def create(
class LiveHelper(PRAWBase):
r"""Provide a set of functions to interact with :class:`.LiveThread`\ s."""
- def __call__(
- self, id: str
- ) -> praw.models.LiveThread: # pylint: disable=invalid-name,redefined-builtin
+ def __call__(self, id: str) -> praw.models.LiveThread:
"""Return a new lazy instance of :class:`.LiveThread`.
This method is intended to be used as:
@@ -193,8 +191,13 @@ def info(self, ids: list[str]) -> Generator[praw.models.LiveThread, None, None]:
:returns: A generator that yields :class:`.LiveThread` instances.
- Live threads that cannot be matched will not be generated. Requests will be
- issued in batches for each 100 IDs.
+ :raises: ``prawcore.ServerError`` if invalid live threads are requested.
+
+ Requests will be issued in batches for each 100 IDs.
+
+ .. note::
+
+ This method doesn't support IDs for live updates.
.. warning::
diff --git a/praw/models/listing/generator.py b/praw/models/listing/generator.py
index ed01fc898..2e2365bb3 100644
--- a/praw/models/listing/generator.py
+++ b/praw/models/listing/generator.py
@@ -52,7 +52,7 @@ def __init__(
self.url = url
self.yielded = 0
- def __iter__(self): # noqa: ANN204
+ def __iter__(self) -> Any:
"""Permit :class:`.ListingGenerator` to operate as an iterator."""
return self
@@ -68,7 +68,7 @@ def __next__(self) -> Any:
self.yielded += 1
return self._listing[self._list_index - 1]
- def _extract_sublist(self, listing): # noqa: ANN001
+ def _extract_sublist(self, listing: dict[str, Any] | list[Any]):
if isinstance(listing, list):
return listing[1] # for submission duplicates
if isinstance(listing, dict):
@@ -82,7 +82,7 @@ def _extract_sublist(self, listing): # noqa: ANN001
raise ValueError(msg)
return listing
- def _next_batch(self): # noqa: ANN001
+ def _next_batch(self):
if self._exhausted:
raise StopIteration
diff --git a/praw/models/listing/mixins/base.py b/praw/models/listing/mixins/base.py
index 8fe34aaaa..bba3f8fb0 100644
--- a/praw/models/listing/mixins/base.py
+++ b/praw/models/listing/mixins/base.py
@@ -15,7 +15,7 @@ class BaseListingMixin(PRAWBase):
VALID_TIME_FILTERS = {"all", "day", "hour", "month", "week", "year"}
@staticmethod
- def _validate_time_filter(time_filter): # noqa: ANN001
+ def _validate_time_filter(time_filter: str):
"""Validate ``time_filter``.
:raises: :py:class:`ValueError` if ``time_filter`` is not valid.
@@ -28,7 +28,7 @@ def _validate_time_filter(time_filter): # noqa: ANN001
msg = f"'time_filter' must be one of: {valid_time_filters}"
raise ValueError(msg)
- def _prepare(self, *, arguments, sort): # noqa: ANN001
+ def _prepare(self, *, arguments: dict[str, Any], sort: str) -> str:
"""Fix for :class:`.Redditor` methods that use a query param rather than subpath."""
if self.__dict__.get("_listing_use_sort"):
self._safely_add_arguments(arguments=arguments, key="params", sort=sort)
diff --git a/praw/models/listing/mixins/subreddit.py b/praw/models/listing/mixins/subreddit.py
index 5c40b351c..86bcb2059 100644
--- a/praw/models/listing/mixins/subreddit.py
+++ b/praw/models/listing/mixins/subreddit.py
@@ -19,7 +19,7 @@ class CommentHelper(PRAWBase):
"""Provide a set of functions to interact with a :class:`.Subreddit`'s comments."""
@property
- def _path(self) -> str: # noqa: ANN001
+ def _path(self) -> str:
return urljoin(self.subreddit._path, "comments/")
def __call__(
diff --git a/praw/models/mod_action.py b/praw/models/mod_action.py
index eeb692ed9..74d0d616c 100644
--- a/praw/models/mod_action.py
+++ b/praw/models/mod_action.py
@@ -15,8 +15,8 @@ class ModAction(PRAWBase):
@property
def mod(self) -> praw.models.Redditor:
"""Return the :class:`.Redditor` who the action was issued by."""
- return self._reddit.redditor(self._mod) # pylint: disable=no-member
+ return self._reddit.redditor(self._mod)
@mod.setter
def mod(self, value: str | praw.models.Redditor):
- self._mod = value # pylint: disable=attribute-defined-outside-init
+ self._mod = value
diff --git a/praw/models/mod_notes.py b/praw/models/mod_notes.py
index bf413167a..01268208d 100644
--- a/praw/models/mod_notes.py
+++ b/praw/models/mod_notes.py
@@ -63,7 +63,7 @@ def _bulk_generator(
for note_dict in response["mod_notes"]:
yield self._reddit._objector.objectify(note_dict)
- def _ensure_attribute(self, error_message: str, **attributes: Any): # noqa: ANN001
+ def _ensure_attribute(self, error_message: str, **attributes: Any) -> Any:
attribute, _value = attributes.popitem()
value = _value or getattr(self, attribute, None)
if value is None:
diff --git a/praw/models/reddit/base.py b/praw/models/reddit/base.py
index 0e7ae3ca6..316fee279 100644
--- a/praw/models/reddit/base.py
+++ b/praw/models/reddit/base.py
@@ -16,7 +16,7 @@ class RedditBase(PRAWBase):
"""Base class that represents actual Reddit objects."""
@staticmethod
- def _url_parts(url): # noqa: ANN001,ANN205
+ def _url_parts(url: str) -> list[str]:
parsed = urlparse(url)
if not parsed.netloc:
raise InvalidURL(url)
@@ -79,15 +79,15 @@ def __str__(self) -> str:
"""Return a string representation of the instance."""
return getattr(self, self.STR_FIELD)
- def _fetch(self): # pragma: no cover
+ def _fetch(self):
self._fetched = True
- def _fetch_data(self): # noqa: ANN001
+ def _fetch_data(self):
name, fields, params = self._fetch_info()
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)
- def _reset_attributes(self, *attributes): # noqa: ANN001,ANN002
+ def _reset_attributes(self, *attributes: str):
for attribute in attributes:
if attribute in self.__dict__:
del self.__dict__[attribute]
diff --git a/praw/models/reddit/collections.py b/praw/models/reddit/collections.py
index eddda8797..23ef7ed9c 100644
--- a/praw/models/reddit/collections.py
+++ b/praw/models/reddit/collections.py
@@ -36,7 +36,7 @@ def __init__(self, reddit: praw.Reddit, collection_id: str):
super().__init__(reddit, _data=None)
self.collection_id = collection_id
- def _post_fullname(self, post): # noqa: ANN001
+ def _post_fullname(self, post: str | praw.models.Submission) -> str:
"""Get a post's fullname.
:param post: A fullname, a :class:`.Submission`, a permalink, or an ID.
@@ -240,7 +240,7 @@ def __init__(
@_deprecate_args("title", "description", "display_layout")
def create(
self, *, description: str, display_layout: str | None = None, title: str
- ) -> praw.models.Collection:
+ ) -> Collection:
"""Create a new :class:`.Collection`.
The authenticated account must have appropriate moderator permissions in the
@@ -523,7 +523,7 @@ def __setattr__(self, attribute: str, value: Any) -> None:
value = self._reddit._objector.objectify(value)
super().__setattr__(attribute, value)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
try:
self._reddit._objector.check_error(data)
@@ -536,9 +536,9 @@ def _fetch(self): # noqa: ANN001
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return "collection", {}, self._info_params
def follow(self):
diff --git a/praw/models/reddit/comment.py b/praw/models/reddit/comment.py
index 156fb7064..4db967b23 100644
--- a/praw/models/reddit/comment.py
+++ b/praw/models/reddit/comment.py
@@ -89,7 +89,7 @@ def mod(self) -> praw.models.reddit.comment.CommentModeration:
return CommentModeration(self)
@property
- def _kind(self): # noqa: ANN001
+ def _kind(self):
"""Return the class's kind."""
return self._reddit.config.kinds["comment"]
@@ -138,14 +138,13 @@ def submission(self, submission: praw.models.Submission):
"""Update the :class:`.Submission` associated with the :class:`.Comment`."""
submission._comments_by_id[self.fullname] = self
self._submission = submission
- # pylint: disable=not-an-iterable
for reply in getattr(self, "replies", []):
reply.submission = submission
def __init__(
self,
reddit: praw.Reddit,
- id: str | None = None, # pylint: disable=redefined-builtin
+ id: str | None = None,
url: str | None = None,
_data: dict[str, Any] | None = None,
):
@@ -168,7 +167,7 @@ def __setattr__(
self,
attribute: str,
value: str | Redditor | CommentForest | praw.models.Subreddit,
- ) -> None:
+ ):
"""Objectify author, replies, and subreddit."""
if attribute == "author":
value = Redditor.from_data(self._reddit, value)
@@ -182,12 +181,12 @@ def __setattr__(
value = self._reddit.subreddit(value)
super().__setattr__(attribute, value)
- def _extract_submission_id(self): # noqa: ANN001
+ def _extract_submission_id(self):
if "context" in self.__dict__:
return self.context.rsplit("/", 4)[1]
return self.link_id.split("_", 1)[1]
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
data = data["data"]
@@ -198,12 +197,14 @@ def _fetch(self): # noqa: ANN001
comment_data = data["children"][0]["data"]
other = type(self)(self._reddit, _data=comment_data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return "info", {}, {"id": self.fullname}
- def parent(self) -> Comment | praw.models.Submission:
+ def parent(
+ self,
+ ) -> Comment | praw.models.Submission:
"""Return the parent of the comment.
The returned parent will be an instance of either :class:`.Comment`, or
@@ -253,14 +254,12 @@ def parent(self) -> Comment | praw.models.Submission:
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
diff --git a/praw/models/reddit/draft.py b/praw/models/reddit/draft.py
index 2872c8031..4f94d9e54 100644
--- a/praw/models/reddit/draft.py
+++ b/praw/models/reddit/draft.py
@@ -85,10 +85,7 @@ def _prepare_data(
return data
def __init__(
- self,
- reddit: praw.Reddit,
- id: str | None = None, # pylint: disable=redefined-builtin
- _data: dict[str, Any] = None,
+ self, reddit: praw.Reddit, id: str | None = None, _data: dict[str, Any] = None
):
"""Initialize a :class:`.Draft` instance."""
if (id, _data).count(None) != 1:
@@ -119,7 +116,7 @@ def _fetch(self):
for draft in self._reddit.drafts():
if draft.id == self.id:
self.__dict__.update(draft.__dict__)
- self._fetched = True
+ super()._fetch()
return
msg = (
f"The currently authenticated user not have a draft with an ID of {self.id}"
diff --git a/praw/models/reddit/emoji.py b/praw/models/reddit/emoji.py
index 41eca6aa6..791fb1d63 100644
--- a/praw/models/reddit/emoji.py
+++ b/praw/models/reddit/emoji.py
@@ -56,11 +56,11 @@ def __init__(
self.subreddit = subreddit
super().__init__(reddit, _data=_data)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
for emoji in self.subreddit.emoji:
if emoji.name == self.name:
self.__dict__.update(emoji.__dict__)
- self._fetched = True
+ super()._fetch()
return
msg = f"r/{self.subreddit} does not have the emoji {self.name}"
raise ClientException(msg)
diff --git a/praw/models/reddit/live.py b/praw/models/reddit/live.py
index b3e7f68e1..6db192810 100644
--- a/praw/models/reddit/live.py
+++ b/praw/models/reddit/live.py
@@ -1,7 +1,7 @@
"""Provide the LiveThread class."""
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Iterator
+from typing import TYPE_CHECKING, Any, Iterable, Iterator
from ...const import API_PATH
from ...util import _deprecate_args
@@ -21,7 +21,7 @@ class LiveContributorRelationship:
"""Provide methods to interact with live threads' contributors."""
@staticmethod
- def _handle_permissions(permissions): # noqa: ANN001,ANN205
+ def _handle_permissions(permissions: Iterable[str]) -> str:
permissions = {"all"} if permissions is None else set(permissions)
return ",".join(f"+{x}" for x in permissions)
@@ -377,7 +377,7 @@ def __init__(
self,
reddit: praw.Reddit,
id: str | None = None,
- _data: dict[str, Any] | None = None, # pylint: disable=redefined-builtin
+ _data: dict[str, Any] | None = None,
):
"""Initialize a :class:`.LiveThread` instance.
@@ -392,14 +392,14 @@ def __init__(
self.id = id
super().__init__(reddit, _data=_data)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
data = data["data"]
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return "liveabout", {"id": self.id}, None
def discussions(
@@ -428,11 +428,12 @@ def discussions(
url = API_PATH["live_discussions"].format(id=self.id)
return ListingGenerator(self._reddit, url, **generator_kwargs)
- def report(self, type: str): # pylint: disable=redefined-builtin
+ def report(self, type: str):
"""Report the thread violating the Reddit rules.
- :param type: One of ``"spam"``, ``"vote-manipulation"``, ``"personal-
- information"``, ``"sexualizing-minors"``, or ``"site-breaking"``.
+ :param type: One of ``"spam"``, ``"vote-manipulation"``,
+ ``"personal-information"``, ``"sexualizing-minors"``, or
+ ``"site-breaking"``.
Usage:
@@ -789,14 +790,14 @@ def __init__(
msg = "Either 'thread_id' and 'update_id', or '_data' must be provided."
raise TypeError(msg)
- def __setattr__(self, attribute: str, value: Any) -> None:
+ def __setattr__(self, attribute: str, value: Any):
"""Objectify author."""
if attribute == "author":
value = Redditor(self._reddit, name=value)
super().__setattr__(attribute, value)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
url = API_PATH["live_focus"].format(thread_id=self.thread.id, update_id=self.id)
other = self._reddit.get(url)[0]
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
diff --git a/praw/models/reddit/message.py b/praw/models/reddit/message.py
index dfd9604ca..9a0b3571a 100644
--- a/praw/models/reddit/message.py
+++ b/praw/models/reddit/message.py
@@ -70,7 +70,7 @@ def parse(
return cls(reddit, _data=data)
@property
- def _kind(self) -> str: # noqa: ANN001
+ def _kind(self) -> str:
"""Return the class's kind."""
return self._reddit.config.kinds["message"]
diff --git a/praw/models/reddit/mixins/__init__.py b/praw/models/reddit/mixins/__init__.py
index 6b881abdd..211de63aa 100644
--- a/praw/models/reddit/mixins/__init__.py
+++ b/praw/models/reddit/mixins/__init__.py
@@ -194,7 +194,7 @@ def send_removal_message(
*,
message: str,
title: str = "ignored",
- type: str = "public", # pylint: disable=redefined-builtin
+ type: str = "public",
) -> praw.models.Comment | None:
"""Send a removal message for a :class:`.Comment` or :class:`.Submission`.
diff --git a/praw/models/reddit/mixins/votable.py b/praw/models/reddit/mixins/votable.py
index 96f9bc545..1deb3b323 100644
--- a/praw/models/reddit/mixins/votable.py
+++ b/praw/models/reddit/mixins/votable.py
@@ -7,7 +7,7 @@
class VotableMixin:
"""Interface for :class:`.RedditBase` classes that can be voted on."""
- def _vote(self, direction: int): # noqa: ANN001
+ def _vote(self, direction: int):
self._reddit.post(
API_PATH["vote"], data={"dir": str(direction), "id": self.fullname}
)
diff --git a/praw/models/reddit/modmail.py b/praw/models/reddit/modmail.py
index 8c34f6608..932762771 100644
--- a/praw/models/reddit/modmail.py
+++ b/praw/models/reddit/modmail.py
@@ -11,6 +11,19 @@
import praw
+class ModmailObject(RedditBase):
+ """A base class for objects within a modmail conversation."""
+
+ AUTHOR_ATTRIBUTE = "author"
+ STR_FIELD = "id"
+
+ def __setattr__(self, attribute: str, value: Any):
+ """Objectify the AUTHOR_ATTRIBUTE attribute."""
+ if attribute == self.AUTHOR_ATTRIBUTE:
+ value = self._reddit._objector.objectify(value)
+ super().__setattr__(attribute, value)
+
+
class ModmailConversation(RedditBase):
"""A class for modmail conversations.
@@ -52,7 +65,7 @@ class ModmailConversation(RedditBase):
STR_FIELD = "id"
@staticmethod
- def _convert_conversation_objects(data, reddit): # noqa: ANN001
+ def _convert_conversation_objects(data: dict[str, Any], reddit: praw.Reddit):
"""Convert messages and mod actions to PRAW objects."""
result = {"messages": [], "modActions": []}
for thing in data["objIds"]:
@@ -62,7 +75,7 @@ def _convert_conversation_objects(data, reddit): # noqa: ANN001
data.update(result)
@staticmethod
- def _convert_user_summary(data, reddit): # noqa: ANN001
+ def _convert_user_summary(data: dict[str, Any], reddit: praw.Reddit):
"""Convert dictionaries of recent user history to PRAW objects."""
parsers = {
"recentComments": reddit._objector.parsers[reddit.config.kinds["comment"]],
@@ -110,7 +123,7 @@ def parse(
def __init__(
self,
reddit: praw.Reddit,
- id: str | None = None, # pylint: disable=redefined-builtin
+ id: str | None = None,
mark_read: bool = False,
_data: dict[str, Any] | None = None,
):
@@ -131,18 +144,20 @@ def __init__(
self._info_params = {"markRead": True} if mark_read else None
- def _build_conversation_list(self, other_conversations): # noqa: ANN001
+ def _build_conversation_list(
+ self, other_conversations: list[ModmailConversation]
+ ) -> str:
"""Return a comma-separated list of conversation IDs."""
conversations = [self] + (other_conversations or [])
return ",".join(conversation.id for conversation in conversations)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
other = self._reddit._objector.objectify(data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return "modmail_conversation", {"id": self.id}, self._info_params
def archive(self):
@@ -197,9 +212,7 @@ def mute(self, *, num_days: int = 3):
)
@_deprecate_args("other_conversations")
- def read(
- self, *, other_conversations: list[ModmailConversation] | None = None
- ): # noqa: D207, D301
+ def read(self, *, other_conversations: list[ModmailConversation] | None = None):
"""Mark the conversation(s) as read.
:param other_conversations: A list of other conversations to mark (default:
@@ -302,9 +315,7 @@ def unmute(self):
)
@_deprecate_args("other_conversations")
- def unread(
- self, *, other_conversations: list[ModmailConversation] | None = None
- ): # noqa: D207, D301
+ def unread(self, *, other_conversations: list[ModmailConversation] | None = None):
"""Mark the conversation(s) as unread.
:param other_conversations: A list of other conversations to mark (default:
@@ -324,19 +335,6 @@ def unread(
self._reddit.post(API_PATH["modmail_unread"], data=data)
-class ModmailObject(RedditBase):
- """A base class for objects within a modmail conversation."""
-
- AUTHOR_ATTRIBUTE = "author"
- STR_FIELD = "id"
-
- def __setattr__(self, attribute: str, value: Any) -> None:
- """Objectify the AUTHOR_ATTRIBUTE attribute."""
- if attribute == self.AUTHOR_ATTRIBUTE:
- value = self._reddit._objector.objectify(value)
- super().__setattr__(attribute, value)
-
-
class ModmailAction(ModmailObject):
"""A class for moderator actions on modmail conversations."""
diff --git a/praw/models/reddit/more.py b/praw/models/reddit/more.py
index 026421feb..5a00f213f 100644
--- a/praw/models/reddit/more.py
+++ b/praw/models/reddit/more.py
@@ -42,7 +42,7 @@ def __repr__(self) -> str:
children[-1] = "..."
return f"<{self.__class__.__name__} count={self.count}, children={children!r}>"
- def _continue_comments(self, update): # noqa: ANN001
+ def _continue_comments(self, update: bool):
assert not self.children, "Please file a bug report with PRAW."
parent = self._load_comment(self.parent_id.split("_", 1)[1])
self._comments = parent.replies
@@ -51,7 +51,7 @@ def _continue_comments(self, update): # noqa: ANN001
comment.submission = self.submission
return self._comments
- def _load_comment(self, comment_id): # noqa: ANN001
+ def _load_comment(self, comment_id: str):
path = f"{API_PATH['submission'].format(id=self.submission.id)}_/{comment_id}"
_, comments = self._reddit.get(
path,
diff --git a/praw/models/reddit/multi.py b/praw/models/reddit/multi.py
index d44c29b87..e6365f8cc 100644
--- a/praw/models/reddit/multi.py
+++ b/praw/models/reddit/multi.py
@@ -101,14 +101,14 @@ def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
if "subreddits" in self.__dict__:
self.subreddits = [Subreddit(reddit, x["name"]) for x in self.subreddits]
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
data = data["data"]
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return (
"multireddit_api",
{"multi": self.name, "user": self._author.name},
diff --git a/praw/models/reddit/redditor.py b/praw/models/reddit/redditor.py
index 791c318f8..3c7c77a1a 100644
--- a/praw/models/reddit/redditor.py
+++ b/praw/models/reddit/redditor.py
@@ -130,12 +130,12 @@ def stream(self) -> praw.models.reddit.redditor.RedditorStream:
return RedditorStream(self)
@property
- def _kind(self) -> str: # noqa: ANN001
+ def _kind(self) -> str:
"""Return the class's kind."""
return self._reddit.config.kinds["redditor"]
@property
- def _path(self) -> str: # noqa: ANN001
+ def _path(self) -> str:
return API_PATH["user"].format(user=self)
def __init__(
@@ -168,7 +168,7 @@ def __init__(
self._fullname = fullname
super().__init__(reddit, _data=_data, _extra_attribute_to_check="_fullname")
- def __setattr__(self, name: str, value: Any) -> None:
+ def __setattr__(self, name: str, value: Any):
"""Objectify the subreddit attribute."""
if name == "subreddit" and value:
from .user_subreddit import UserSubreddit
@@ -176,24 +176,24 @@ def __setattr__(self, name: str, value: Any) -> None:
value = UserSubreddit(reddit=self._reddit, _data=value)
super().__setattr__(name, value)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
data = data["data"]
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
if hasattr(self, "_fullname"):
self.name = self._fetch_username(self._fullname)
return "user_about", {"user": self.name}, None
- def _fetch_username(self, fullname): # noqa: ANN001
+ def _fetch_username(self, fullname: str):
return self._reddit.get(API_PATH["user_by_fullname"], params={"ids": fullname})[
fullname
]["name"]
- def _friend(self, *, data, method): # noqa: ANN001
+ def _friend(self, *, data: dict[str:Any], method: str):
url = API_PATH["friend_v1"].format(user=self)
self._reddit.request(data=dumps(data), method=method, path=url)
diff --git a/praw/models/reddit/removal_reasons.py b/praw/models/reddit/removal_reasons.py
index d0e8801c3..55c0af8ce 100644
--- a/praw/models/reddit/removal_reasons.py
+++ b/praw/models/reddit/removal_reasons.py
@@ -31,9 +31,9 @@ class RemovalReason(RedditBase):
STR_FIELD = "id"
@staticmethod
- def _warn_reason_id( # noqa: ANN205
+ def _warn_reason_id(
*, id_value: str | None, reason_id_value: str | None
- ):
+ ) -> str | None:
"""Reason ID param is deprecated. Warns if it's used.
:param id_value: Returns the actual value of parameter ``id`` is parameter
@@ -67,7 +67,7 @@ def __init__(
self,
reddit: praw.Reddit,
subreddit: praw.models.Subreddit,
- id: str | None = None, # pylint: disable=redefined-builtin
+ id: str | None = None,
reason_id: str | None = None,
_data: dict[str, Any] | None = None,
):
@@ -90,11 +90,11 @@ def __init__(
self.subreddit = subreddit
super().__init__(reddit, _data=_data)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
for removal_reason in self.subreddit.mod.removal_reasons:
if removal_reason.id == self.id:
self.__dict__.update(removal_reason.__dict__)
- self._fetched = True
+ super()._fetch()
return
msg = f"Subreddit {self.subreddit} does not have the removal reason {self.id}"
raise ClientException(msg)
@@ -144,7 +144,7 @@ class SubredditRemovalReasons:
"""Provide a set of functions to a :class:`.Subreddit`'s removal reasons."""
@cachedproperty
- def _removal_reason_list(self) -> list[RemovalReason]: # noqa: ANN001
+ def _removal_reason_list(self) -> list[RemovalReason]:
"""Get a list of Removal Reason objects.
:returns: A list of instances of :class:`.RemovalReason`.
@@ -250,5 +250,5 @@ def add(self, *, message: str, title: str) -> RemovalReason:
"""
data = {"message": message, "title": title}
url = API_PATH["removal_reasons_list"].format(subreddit=self.subreddit)
- id = self._reddit.post(url, data=data) # noqa: A001
- return RemovalReason(self._reddit, self.subreddit, id)
+ reason_id = self._reddit.post(url, data=data)
+ return RemovalReason(self._reddit, self.subreddit, reason_id)
diff --git a/praw/models/reddit/rules.py b/praw/models/reddit/rules.py
index 3223fe666..281545c87 100644
--- a/praw/models/reddit/rules.py
+++ b/praw/models/reddit/rules.py
@@ -88,11 +88,11 @@ def __init__(
self.subreddit = subreddit
super().__init__(reddit, _data=_data)
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
for rule in self.subreddit.rules:
if rule.short_name == self.short_name:
self.__dict__.update(rule.__dict__)
- self._fetched = True
+ super()._fetch()
return
msg = f"Subreddit {self.subreddit} does not have the rule {self.short_name}"
raise ClientException(msg)
@@ -209,7 +209,7 @@ class SubredditRules:
"""
@cachedproperty
- def _rule_list(self) -> list[Rule]: # noqa: ANN001
+ def _rule_list(self) -> list[Rule]:
"""Get a list of :class:`.Rule` objects.
:returns: A list of instances of :class:`.Rule`.
diff --git a/praw/models/reddit/submission.py b/praw/models/reddit/submission.py
index 9c7bfa8bc..345db0a32 100644
--- a/praw/models/reddit/submission.py
+++ b/praw/models/reddit/submission.py
@@ -3,7 +3,7 @@
import re
from json import dumps
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Generator
from urllib.parse import urljoin
from warnings import warn
@@ -527,7 +527,7 @@ def mod(self) -> SubmissionModeration:
return SubmissionModeration(self)
@property
- def _kind(self) -> str: # noqa: ANN001
+ def _kind(self) -> str:
"""Return the class's kind."""
return self._reddit.config.kinds["submission"]
@@ -577,7 +577,7 @@ def shortlink(self) -> str:
def __init__(
self,
reddit: praw.Reddit,
- id: str | None = None, # pylint: disable=redefined-builtin
+ id: str | None = None,
url: str | None = None,
_data: dict[str, Any] | None = None,
):
@@ -608,7 +608,7 @@ def __init__(
self._additional_fetch_params = {}
self._comments_by_id = {}
- def __setattr__(self, attribute: str, value: Any) -> None:
+ def __setattr__(self, attribute: str, value: Any):
"""Objectify author, subreddit, and poll data attributes."""
if attribute == "author":
value = Redditor.from_data(self._reddit, value)
@@ -630,7 +630,12 @@ def __setattr__(self, attribute: str, value: Any) -> None:
)
super().__setattr__(attribute, value)
- def _chunk(self, *, chunk_size, other_submissions): # noqa: ANN001
+ def _chunk(
+ self,
+ *,
+ chunk_size: int,
+ other_submissions: list[praw.models.Submission] | None,
+ ) -> Generator[str, None, None]:
all_submissions = [self.fullname]
if other_submissions:
all_submissions += [x.fullname for x in other_submissions]
@@ -716,7 +721,7 @@ def _edit_experimental(
self.__dict__.update(updated)
return self
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
submission_listing, comment_listing = data
comment_listing = Listing(self._reddit, _data=comment_listing["data"])
@@ -729,23 +734,22 @@ def _fetch(self): # noqa: ANN001
self.__dict__.update(submission.__dict__)
self.comments._update(comment_listing.children)
+ super()._fetch()
- self._fetched = True
-
- def _fetch_data(self): # noqa: ANN001
+ def _fetch_data(self):
name, fields, params = self._fetch_info()
params.update(self._additional_fetch_params.copy())
path = API_PATH[name].format(**fields)
return self._reddit.request(method="GET", params=params, path=path)
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return (
"submission",
{"id": self.id},
{"limit": self.comment_limit, "sort": self.comment_sort},
)
- def _replace_richtext_links(self, richtext_json: dict): # noqa: ANN001
+ def _replace_richtext_links(self, richtext_json: dict):
parsed_media_types = {
media_id: MEDIA_TYPE_MAPPING[value["e"]]
for media_id, value in self.media_metadata.items()
diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py
index 191c4f981..1613c2f69 100644
--- a/praw/models/reddit/subreddit.py
+++ b/praw/models/reddit/subreddit.py
@@ -59,7 +59,7 @@ class Modmail:
def __call__(
self, id: str | None = None, mark_read: bool = False
- ) -> ModmailConversation: # noqa: D207, D301
+ ) -> ModmailConversation:
"""Return an individual conversation.
:param id: A reddit base36 conversation ID, e.g., ``"2gmz"``.
@@ -100,7 +100,6 @@ def __call__(
print(conversation.user.recent_posts)
"""
- # pylint: disable=invalid-name,redefined-builtin
return ModmailConversation(self.subreddit._reddit, id=id, mark_read=mark_read)
def __init__(self, subreddit: praw.models.Subreddit):
@@ -166,7 +165,7 @@ def conversations(
sort: str | None = None,
state: str | None = None,
**generator_kwargs: Any,
- ) -> Iterator[ModmailConversation]: # noqa: D207, D301
+ ) -> Iterator[ModmailConversation]:
"""Generate :class:`.ModmailConversation` objects for subreddit(s).
:param after: A base36 modmail conversation id. When provided, the listing
@@ -297,3739 +296,3730 @@ def unread_count(self) -> dict[str, int]:
return self.subreddit._reddit.get(API_PATH["modmail_unread_count"])
-class Subreddit(MessageableMixin, SubredditListingMixin, FullnameMixin, RedditBase):
- """A class for Subreddits.
+class SubredditFilters:
+ """Provide functions to interact with the special :class:`.Subreddit`'s filters.
- To obtain an instance of this class for r/test execute:
+ Members of this class should be utilized via :meth:`.Subreddit.filters`. For
+ example, to add a filter, run:
.. code-block:: python
- subreddit = reddit.subreddit("test")
-
- While r/all is not a real subreddit, it can still be treated like one. The following
- outputs the titles of the 25 hottest submissions in r/all:
+ reddit.subreddit("all").filters.add("test")
- .. code-block:: python
+ """
- for submission in reddit.subreddit("all").hot(limit=25):
- print(submission.title)
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditFilters` instance.
- Multiple subreddits can be combined with a ``+`` like so:
+ :param subreddit: The special subreddit whose filters to work with.
- .. code-block:: python
+ As of this writing filters can only be used with the special subreddits ``all``
+ and ``mod``.
- for submission in reddit.subreddit("redditdev+learnpython").top(time_filter="all"):
- print(submission)
+ """
+ self.subreddit = subreddit
- Subreddits can be filtered from combined listings as follows.
+ def __iter__(self) -> Generator[praw.models.Subreddit, None, None]:
+ """Iterate through the special :class:`.Subreddit`'s filters.
- .. note::
+ This method should be invoked as:
- These filters are ignored by certain methods, including :attr:`.comments`,
- :meth:`.gilded`, and :meth:`.SubredditStream.comments`.
+ .. code-block:: python
- .. code-block:: python
+ for subreddit in reddit.subreddit("test").filters:
+ ...
- for submission in reddit.subreddit("all-redditdev").new():
- print(submission)
+ """
+ url = API_PATH["subreddit_filter_list"].format(
+ special=self.subreddit, user=self.subreddit._reddit.user.me()
+ )
+ params = {"unique": self.subreddit._reddit._next_unique}
+ response_data = self.subreddit._reddit.get(url, params=params)
+ yield from response_data.subreddits
- .. include:: ../../typical_attributes.rst
+ def add(self, subreddit: praw.models.Subreddit | str):
+ """Add ``subreddit`` to the list of filtered subreddits.
- ========================= ==========================================================
- Attribute Description
- ========================= ==========================================================
- ``can_assign_link_flair`` Whether users can assign their own link flair.
- ``can_assign_user_flair`` Whether users can assign their own user flair.
- ``created_utc`` Time the subreddit was created, represented in `Unix
- Time`_.
- ``description`` Subreddit description, in Markdown.
- ``description_html`` Subreddit description, in HTML.
- ``display_name`` Name of the subreddit.
- ``id`` ID of the subreddit.
- ``name`` Fullname of the subreddit.
- ``over18`` Whether the subreddit is NSFW.
- ``public_description`` Description of the subreddit, shown in searches and on the
- "You must be invited to visit this community" page (if
- applicable).
- ``spoilers_enabled`` Whether the spoiler tag feature is enabled.
- ``subscribers`` Count of subscribers.
- ``user_is_banned`` Whether the authenticated user is banned.
- ``user_is_moderator`` Whether the authenticated user is a moderator.
- ``user_is_subscriber`` Whether the authenticated user is subscribed.
- ========================= ==========================================================
+ :param subreddit: The subreddit to add to the filter list.
- .. note::
+ Items from subreddits added to the filtered list will no longer be included when
+ obtaining listings for r/all.
- Trying to retrieve attributes of quarantined or private subreddits will result
- in a 403 error. Trying to retrieve attributes of a banned subreddit will result
- in a 404 error.
+ Alternatively, you can filter a subreddit temporarily from a special listing in
+ a manner like so:
- .. _unix time: https://en.wikipedia.org/wiki/Unix_time
+ .. code-block:: python
- """
+ reddit.subreddit("all-redditdev-learnpython")
- # pylint: disable=too-many-public-methods
+ :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.
- STR_FIELD = "display_name"
- MESSAGE_PREFIX = "#"
+ """
+ url = API_PATH["subreddit_filter"].format(
+ special=self.subreddit,
+ user=self.subreddit._reddit.user.me(),
+ subreddit=subreddit,
+ )
+ self.subreddit._reddit.put(url, data={"model": dumps({"name": str(subreddit)})})
- @staticmethod
- def _create_or_update(
- *,
- _reddit, # noqa: ANN001
- allow_images=None, # noqa: ANN001
- allow_post_crossposts=None, # noqa: ANN001
- allow_top=None, # noqa: ANN001
- collapse_deleted_comments=None, # noqa: ANN001
- comment_score_hide_mins=None, # noqa: ANN001
- description=None, # noqa: ANN001
- domain=None, # noqa: ANN001
- exclude_banned_modqueue=None, # noqa: ANN001
- header_hover_text=None, # noqa: ANN001
- hide_ads=None, # noqa: ANN001
- lang=None, # noqa: ANN001
- key_color=None, # noqa: ANN001
- link_type=None, # noqa: ANN001
- name=None, # noqa: ANN001
- over_18=None, # noqa: ANN001
- public_description=None, # noqa: ANN001
- public_traffic=None, # noqa: ANN001
- show_media=None, # noqa: ANN001
- show_media_preview=None, # noqa: ANN001
- spam_comments=None, # noqa: ANN001
- spam_links=None, # noqa: ANN001
- spam_selfposts=None, # noqa: ANN001
- spoilers_enabled=None, # noqa: ANN001
- sr=None, # noqa: ANN001
- submit_link_label=None, # noqa: ANN001
- submit_text=None, # noqa: ANN001
- submit_text_label=None, # noqa: ANN001
- subreddit_type=None, # noqa: ANN001
- suggested_comment_sort=None, # noqa: ANN001
- title=None, # noqa: ANN001
- wiki_edit_age=None, # noqa: ANN001
- wiki_edit_karma=None, # noqa: ANN001
- wikimode=None, # noqa: ANN001
- **other_settings, # noqa: ANN001,ANN003
- ):
- # pylint: disable=invalid-name,too-many-locals,too-many-arguments
- model = {
- "allow_images": allow_images,
- "allow_post_crossposts": allow_post_crossposts,
- "allow_top": allow_top,
- "collapse_deleted_comments": collapse_deleted_comments,
- "comment_score_hide_mins": comment_score_hide_mins,
- "description": description,
- "domain": domain,
- "exclude_banned_modqueue": exclude_banned_modqueue,
- "header-title": header_hover_text, # Remap here - better name
- "hide_ads": hide_ads,
- "key_color": key_color,
- "lang": lang,
- "link_type": link_type,
- "name": name,
- "over_18": over_18,
- "public_description": public_description,
- "public_traffic": public_traffic,
- "show_media": show_media,
- "show_media_preview": show_media_preview,
- "spam_comments": spam_comments,
- "spam_links": spam_links,
- "spam_selfposts": spam_selfposts,
- "spoilers_enabled": spoilers_enabled,
- "sr": sr,
- "submit_link_label": submit_link_label,
- "submit_text": submit_text,
- "submit_text_label": submit_text_label,
- "suggested_comment_sort": suggested_comment_sort,
- "title": title,
- "type": subreddit_type,
- "wiki_edit_age": wiki_edit_age,
- "wiki_edit_karma": wiki_edit_karma,
- "wikimode": wikimode,
- }
+ def remove(self, subreddit: praw.models.Subreddit | str):
+ """Remove ``subreddit`` from the list of filtered subreddits.
- model.update(other_settings)
+ :param subreddit: The subreddit to remove from the filter list.
- _reddit.post(API_PATH["site_admin"], data=model)
+ :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.
- @staticmethod
- def _subreddit_list(*, other_subreddits, subreddit): # noqa: ANN001,ANN205
- if other_subreddits:
- return ",".join([str(subreddit)] + [str(x) for x in other_subreddits])
- return str(subreddit)
+ """
+ url = API_PATH["subreddit_filter"].format(
+ special=self.subreddit,
+ user=self.subreddit._reddit.user.me(),
+ subreddit=str(subreddit),
+ )
+ self.subreddit._reddit.delete(url)
- @staticmethod
- def _validate_gallery(images): # noqa: ANN001,ANN205
- for image in images:
- image_path = image.get("image_path", "")
- if image_path:
- if not Path(image_path).is_file():
- msg = f"{image_path!r} is not a valid image path."
- raise TypeError(msg)
- else:
- msg = "'image_path' is required."
- raise TypeError(msg)
- if not len(image.get("caption", "")) <= 180:
- msg = "Caption must be 180 characters or less."
- raise TypeError(msg)
- @staticmethod
- def _validate_inline_media(inline_media: praw.models.InlineMedia): # noqa: ANN001
- if not Path(inline_media.path).is_file():
- msg = f"{inline_media.path!r} is not a valid file path."
- raise ValueError(msg)
+class SubredditFlair:
+ """Provide a set of functions to interact with a :class:`.Subreddit`'s flair."""
@cachedproperty
- def banned(self) -> praw.models.reddit.subreddit.SubredditRelationship:
- """Provide an instance of :class:`.SubredditRelationship`.
-
- For example, to ban a user try:
-
- .. code-block:: python
-
- reddit.subreddit("test").banned.add("spez", ban_reason="...")
+ def link_templates(
+ self,
+ ) -> praw.models.reddit.subreddit.SubredditLinkFlairTemplates:
+ """Provide an instance of :class:`.SubredditLinkFlairTemplates`.
- To list the banned users along with any notes, try:
+ Use this attribute for interacting with a :class:`.Subreddit`'s link flair
+ templates. For example to list all the link flair templates for a subreddit
+ which you have the ``flair`` moderator permission on try:
.. code-block:: python
- for ban in reddit.subreddit("test").banned():
- print(f"{ban}: {ban.note}")
+ for template in reddit.subreddit("test").flair.link_templates:
+ print(template)
"""
- return SubredditRelationship(self, "banned")
+ return SubredditLinkFlairTemplates(self.subreddit)
@cachedproperty
- def collections(self) -> praw.models.reddit.collections.SubredditCollections:
- r"""Provide an instance of :class:`.SubredditCollections`.
-
- To see the permalinks of all :class:`.Collection`\ s that belong to a subreddit,
- try:
-
- .. code-block:: python
-
- for collection in reddit.subreddit("test").collections:
- print(collection.permalink)
+ def templates(
+ self,
+ ) -> praw.models.reddit.subreddit.SubredditRedditorFlairTemplates:
+ """Provide an instance of :class:`.SubredditRedditorFlairTemplates`.
- To get a specific :class:`.Collection` by its UUID or permalink, use one of the
- following:
+ Use this attribute for interacting with a :class:`.Subreddit`'s flair templates.
+ For example to list all the flair templates for a subreddit which you have the
+ ``flair`` moderator permission on try:
.. code-block:: python
- collection = reddit.subreddit("test").collections("some_uuid")
- collection = reddit.subreddit("test").collections(
- permalink="https://reddit.com/r/SUBREDDIT/collection/some_uuid"
- )
+ for template in reddit.subreddit("test").flair.templates:
+ print(template)
"""
- return self._subreddit_collections_class(self._reddit, self)
+ return SubredditRedditorFlairTemplates(self.subreddit)
- @cachedproperty
- def contributor(self) -> praw.models.reddit.subreddit.ContributorRelationship:
- """Provide an instance of :class:`.ContributorRelationship`.
+ def __call__(
+ self,
+ redditor: praw.models.Redditor | str | None = None,
+ **generator_kwargs: Any,
+ ) -> Iterator[praw.models.Redditor]:
+ """Return a :class:`.ListingGenerator` for Redditors and their flairs.
- Contributors are also known as approved submitters.
+ :param redditor: When provided, yield at most a single :class:`.Redditor`
+ instance (default: ``None``).
- To add a contributor try:
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
+
+ Usage:
.. code-block:: python
- reddit.subreddit("test").contributor.add("spez")
+ for flair in reddit.subreddit("test").flair(limit=None):
+ print(flair)
"""
- return ContributorRelationship(self, "contributor")
-
- @cachedproperty
- def emoji(self) -> SubredditEmoji:
- """Provide an instance of :class:`.SubredditEmoji`.
+ Subreddit._safely_add_arguments(
+ arguments=generator_kwargs, key="params", name=redditor
+ )
+ generator_kwargs.setdefault("limit", None)
+ url = API_PATH["flairlist"].format(subreddit=self.subreddit)
+ return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)
- This attribute can be used to discover all emoji for a subreddit:
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditFlair` instance.
- .. code-block:: python
+ :param subreddit: The subreddit whose flair to work with.
- for emoji in reddit.subreddit("test").emoji:
- print(emoji)
+ """
+ self.subreddit = subreddit
- A single emoji can be lazily retrieved via:
+ @_deprecate_args("position", "self_assign", "link_position", "link_self_assign")
+ def configure(
+ self,
+ *,
+ link_position: str = "left",
+ link_self_assign: bool = False,
+ position: str = "right",
+ self_assign: bool = False,
+ **settings: Any,
+ ):
+ """Update the :class:`.Subreddit`'s flair configuration.
- .. code-block:: python
+ :param link_position: One of ``"left"``, ``"right"``, or ``False`` to disable
+ (default: ``"left"``).
+ :param link_self_assign: Permit self assignment of link flair (default:
+ ``False``).
+ :param position: One of ``"left"``, ``"right"``, or ``False`` to disable
+ (default: ``"right"``).
+ :param self_assign: Permit self assignment of user flair (default: ``False``).
- reddit.subreddit("test").emoji["emoji_name"]
+ .. code-block:: python
- .. note::
+ subreddit = reddit.subreddit("test")
+ subreddit.flair.configure(link_position="right", self_assign=True)
- Attempting to access attributes of a nonexistent emoji will result in a
- :class:`.ClientException`.
+ Additional keyword arguments can be provided to handle new settings as Reddit
+ introduces them.
"""
- return SubredditEmoji(self)
+ data = {
+ "flair_enabled": bool(position),
+ "flair_position": position or "right",
+ "flair_self_assign_enabled": self_assign,
+ "link_flair_position": link_position or "",
+ "link_flair_self_assign_enabled": link_self_assign,
+ }
+ data.update(settings)
+ url = API_PATH["flairconfig"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- @cachedproperty
- def filters(self) -> praw.models.reddit.subreddit.SubredditFilters:
- """Provide an instance of :class:`.SubredditFilters`.
+ def delete(self, redditor: praw.models.Redditor | str):
+ """Delete flair for a :class:`.Redditor`.
- For example, to add a filter, run:
+ :param redditor: A redditor name or :class:`.Redditor` instance.
- .. code-block:: python
+ .. seealso::
- reddit.subreddit("all").filters.add("test")
+ :meth:`~.SubredditFlair.update` to delete the flair of many redditors at
+ once.
"""
- return SubredditFilters(self)
-
- @cachedproperty
- def flair(self) -> praw.models.reddit.subreddit.SubredditFlair:
- """Provide an instance of :class:`.SubredditFlair`.
-
- Use this attribute for interacting with a :class:`.Subreddit`'s flair. For
- example, to list all the flair for a subreddit which you have the ``flair``
- moderator permission on try:
-
- .. code-block:: python
+ url = API_PATH["deleteflair"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data={"name": str(redditor)})
- for flair in reddit.subreddit("test").flair():
- print(flair)
+ def delete_all(self) -> list[dict[str, str | bool | dict[str, str]]]:
+ """Delete all :class:`.Redditor` flair in the :class:`.Subreddit`.
- Flair templates can be interacted with through this attribute via:
+ :returns: List of dictionaries indicating the success or failure of each delete.
- .. code-block:: python
+ """
+ return self.update(x["user"] for x in self())
- for template in reddit.subreddit("test").flair.templates:
- print(template)
+ @_deprecate_args("redditor", "text", "css_class", "flair_template_id")
+ def set( # noqa: A003
+ self,
+ redditor: praw.models.Redditor | str,
+ *,
+ css_class: str = "",
+ flair_template_id: str | None = None,
+ text: str = "",
+ ):
+ """Set flair for a :class:`.Redditor`.
- """
- return SubredditFlair(self)
+ :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param text: The flair text to associate with the :class:`.Redditor` or
+ :class:`.Submission` (default: ``""``).
+ :param css_class: The css class to associate with the flair html (default:
+ ``""``). Use either this or ``flair_template_id``.
+ :param flair_template_id: The ID of the flair template to be used (default:
+ ``None``). Use either this or ``css_class``.
- @cachedproperty
- def mod(self) -> SubredditModeration:
- """Provide an instance of :class:`.SubredditModeration`.
+ This method can only be used by an authenticated user who is a moderator of the
+ associated :class:`.Subreddit`.
- For example, to accept a moderation invite from r/test:
+ For example:
.. code-block:: python
- reddit.subreddit("test").mod.accept_invite()
+ reddit.subreddit("test").flair.set("bboe", text="PRAW author", css_class="mods")
+ template = "6bd28436-1aa7-11e9-9902-0e05ab0fad46"
+ reddit.subreddit("test").flair.set(
+ "spez", text="Reddit CEO", flair_template_id=template
+ )
"""
- return SubredditModeration(self)
+ if css_class and flair_template_id is not None:
+ msg = "Parameter 'css_class' cannot be used in conjunction with 'flair_template_id'."
+ raise TypeError(msg)
+ data = {"name": str(redditor), "text": text}
+ if flair_template_id is not None:
+ data["flair_template_id"] = flair_template_id
+ url = API_PATH["select_flair"].format(subreddit=self.subreddit)
+ else:
+ data["css_class"] = css_class
+ url = API_PATH["flair"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- @cachedproperty
- def moderator(self) -> praw.models.reddit.subreddit.ModeratorRelationship:
- """Provide an instance of :class:`.ModeratorRelationship`.
+ @_deprecate_args("flair_list", "text", "css_class")
+ def update(
+ self,
+ flair_list: Iterator[
+ str | praw.models.Redditor | dict[str, str | praw.models.Redditor]
+ ],
+ *,
+ text: str = "",
+ css_class: str = "",
+ ) -> list[dict[str, str | bool | dict[str, str]]]:
+ """Set or clear the flair for many redditors at once.
- For example, to add a moderator try:
+ :param flair_list: Each item in this list should be either:
- .. code-block:: python
+ - The name of a redditor.
+ - An instance of :class:`.Redditor`.
+ - A dictionary mapping keys ``"user"``, ``"flair_text"``, and
+ ``"flair_css_class"`` to their respective values. The ``"user"`` key
+ should map to a redditor, as described above. When a dictionary isn't
+ provided, or the dictionary is missing either ``"flair_text"`` or
+ ``"flair_css_class"`` keys, the default values will come from the other
+ arguments.
+ :param css_class: The css class to use when not explicitly provided in
+ ``flair_list`` (default: ``""``).
+ :param text: The flair text to use when not explicitly provided in
+ ``flair_list`` (default: ``""``).
- reddit.subreddit("test").moderator.add("spez")
+ :returns: List of dictionaries indicating the success or failure of each update.
- To list the moderators along with their permissions try:
+ For example, to clear the flair text, and set the ``"praw"`` flair css class on
+ a few users try:
.. code-block:: python
- for moderator in reddit.subreddit("test").moderator():
- print(f"{moderator}: {moderator.mod_permissions}")
+ subreddit.flair.update(["bboe", "spez", "spladug"], css_class="praw")
"""
- return ModeratorRelationship(self, "moderator")
-
- @cachedproperty
- def modmail(self) -> praw.models.reddit.subreddit.Modmail:
- """Provide an instance of :class:`.Modmail`.
+ temp_lines = StringIO()
+ for item in flair_list:
+ if isinstance(item, dict):
+ writer(temp_lines).writerow(
+ [
+ str(item["user"]),
+ item.get("flair_text", text),
+ item.get("flair_css_class", css_class),
+ ]
+ )
+ else:
+ writer(temp_lines).writerow([str(item), text, css_class])
- For example, to send a new modmail from r/test to u/spez with the subject
- ``"test"`` along with a message body of ``"hello"``:
+ lines = temp_lines.getvalue().splitlines()
+ temp_lines.close()
+ response = []
+ url = API_PATH["flaircsv"].format(subreddit=self.subreddit)
+ while lines:
+ data = {"flair_csv": "\n".join(lines[:100])}
+ response.extend(self.subreddit._reddit.post(url, data=data))
+ lines = lines[100:]
+ return response
- .. code-block:: python
- reddit.subreddit("test").modmail.create(subject="test", body="hello", recipient="spez")
+class SubredditFlairTemplates:
+ """Provide functions to interact with a :class:`.Subreddit`'s flair templates."""
- """
- return Modmail(self)
+ @staticmethod
+ def flair_type(is_link: bool) -> str:
+ """Return ``"LINK_FLAIR"`` or ``"USER_FLAIR"`` depending on ``is_link`` value."""
+ return "LINK_FLAIR" if is_link else "USER_FLAIR"
- @cachedproperty
- def muted(self) -> praw.models.reddit.subreddit.SubredditRelationship:
- """Provide an instance of :class:`.SubredditRelationship`.
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditFlairTemplates` instance.
- For example, muted users can be iterated through like so:
+ :param subreddit: The subreddit whose flair templates to work with.
- .. code-block:: python
+ .. note::
- for mute in reddit.subreddit("test").muted():
- print(f"{mute}: {mute.date}")
+ This class should not be initialized directly. Instead, obtain an instance
+ via: ``reddit.subreddit("test").flair.templates`` or
+ ``reddit.subreddit("test").flair.link_templates``.
"""
- return SubredditRelationship(self, "muted")
-
- @cachedproperty
- def quaran(self) -> praw.models.reddit.subreddit.SubredditQuarantine:
- """Provide an instance of :class:`.SubredditQuarantine`.
+ self.subreddit = subreddit
- This property is named ``quaran`` because ``quarantine`` is a subreddit
- attribute returned by Reddit to indicate whether or not a subreddit is
- quarantined.
+ def __iter__(self):
+ """Abstract method to return flair templates."""
+ raise NotImplementedError
- To opt-in into a quarantined subreddit:
+ def _add(
+ self,
+ *,
+ allowable_content: str | None = None,
+ background_color: str | None = None,
+ css_class: str = "",
+ is_link: bool | None = None,
+ max_emojis: int | None = None,
+ mod_only: bool | None = None,
+ text: str,
+ text_color: str | None = None,
+ text_editable: bool = False,
+ ):
+ url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
+ data = {
+ "allowable_content": allowable_content,
+ "background_color": background_color,
+ "css_class": css_class,
+ "flair_type": self.flair_type(is_link),
+ "max_emojis": max_emojis,
+ "mod_only": bool(mod_only),
+ "text": text,
+ "text_color": text_color,
+ "text_editable": bool(text_editable),
+ }
+ self.subreddit._reddit.post(url, data=data)
- .. code-block:: python
+ def _clear(self, *, is_link: bool | None = None):
+ url = API_PATH["flairtemplateclear"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data={"flair_type": self.flair_type(is_link)})
- reddit.subreddit("test").quaran.opt_in()
+ def _reorder(self, flair_list: list, *, is_link: bool | None = None):
+ url = API_PATH["flairtemplatereorder"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.patch(
+ url,
+ params={
+ "flair_type": self.flair_type(is_link),
+ "subreddit": self.subreddit.display_name,
+ },
+ json=flair_list,
+ )
- """
- return SubredditQuarantine(self)
+ def delete(self, template_id: str):
+ """Remove a flair template provided by ``template_id``.
- @cachedproperty
- def rules(self) -> SubredditRules:
- """Provide an instance of :class:`.SubredditRules`.
+ For example, to delete the first :class:`.Redditor` flair template listed, try:
- Use this attribute for interacting with a :class:`.Subreddit`'s rules.
+ .. code-block:: python
- For example, to list all the rules for a subreddit:
+ template_info = list(subreddit.flair.templates)[0]
+ subreddit.flair.templates.delete(template_info["id"])
- .. code-block:: python
+ """
+ url = API_PATH["flairtemplatedelete"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data={"flair_template_id": template_id})
- for rule in reddit.subreddit("test").rules:
- print(rule)
+ @_deprecate_args(
+ "template_id",
+ "text",
+ "css_class",
+ "text_editable",
+ "background_color",
+ "text_color",
+ "mod_only",
+ "allowable_content",
+ "max_emojis",
+ "fetch",
+ )
+ def update(
+ self,
+ template_id: str,
+ *,
+ allowable_content: str | None = None,
+ background_color: str | None = None,
+ css_class: str | None = None,
+ fetch: bool = True,
+ max_emojis: int | None = None,
+ mod_only: bool | None = None,
+ text: str | None = None,
+ text_color: str | None = None,
+ text_editable: bool | None = None,
+ ):
+ """Update the flair template provided by ``template_id``.
- Moderators can also add rules to the subreddit. For example, to make a rule
- called ``"No spam"`` in r/test:
+ :param template_id: The flair template to update. If not valid then an exception
+ will be thrown.
+ :param allowable_content: If specified, most be one of ``"all"``, ``"emoji"``,
+ or ``"text"`` to restrict content to that type. If set to ``"emoji"`` then
+ the ``"text"`` param must be a valid emoji string, for example,
+ ``":snoo:"``.
+ :param background_color: The flair template's new background color, as a hex
+ color.
+ :param css_class: The flair template's new css_class (default: ``""``).
+ :param fetch: Whether PRAW will fetch existing information on the existing flair
+ before updating (default: ``True``).
+ :param max_emojis: Maximum emojis in the flair (Reddit defaults this value to
+ ``10``).
+ :param mod_only: Indicate if the flair can only be used by moderators.
+ :param text: The flair template's new text.
+ :param text_color: The flair template's new text color, either ``"light"`` or
+ ``"dark"``.
+ :param text_editable: Indicate if the flair text can be modified for each
+ redditor that sets it (default: ``False``).
+
+ .. warning::
+
+ If parameter ``fetch`` is set to ``False``, all parameters not provided will
+ be reset to their default (``None`` or ``False``) values.
+
+ For example, to make a user flair template text editable, try:
.. code-block:: python
- reddit.subreddit("test").rules.mod.add(
- short_name="No spam", kind="all", description="Do not spam. Spam bad"
+ template_info = list(subreddit.flair.templates)[0]
+ subreddit.flair.templates.update(
+ template_info["id"],
+ text=template_info["flair_text"],
+ text_editable=True,
)
"""
- return SubredditRules(self)
-
- @cachedproperty
- def stream(self) -> praw.models.reddit.subreddit.SubredditStream:
- """Provide an instance of :class:`.SubredditStream`.
+ url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
+ data = {
+ "allowable_content": allowable_content,
+ "background_color": background_color,
+ "css_class": css_class,
+ "flair_template_id": template_id,
+ "max_emojis": max_emojis,
+ "mod_only": mod_only,
+ "text": text,
+ "text_color": text_color,
+ "text_editable": text_editable,
+ }
+ if fetch:
+ _existing_data = [
+ template for template in iter(self) if template["id"] == template_id
+ ]
+ if len(_existing_data) != 1:
+ raise InvalidFlairTemplateID(template_id)
+ existing_data = _existing_data[0]
+ for key, value in existing_data.items():
+ if data.get(key) is None:
+ data[key] = value
+ self.subreddit._reddit.post(url, data=data)
- Streams can be used to indefinitely retrieve new comments made to a subreddit,
- like:
- .. code-block:: python
+class SubredditModeration:
+ """Provides a set of moderation functions to a :class:`.Subreddit`.
- for comment in reddit.subreddit("test").stream.comments():
- print(comment)
+ For example, to accept a moderation invite from r/test:
- Additionally, new submissions can be retrieved via the stream. In the following
- example all submissions are fetched via the special r/all:
+ .. code-block:: python
- .. code-block:: python
+ reddit.subreddit("test").mod.accept_invite()
- for submission in reddit.subreddit("all").stream.submissions():
- print(submission)
+ """
- """
- return SubredditStream(self)
+ @staticmethod
+ def _handle_only(*, generator_kwargs: dict[str, Any], only: str | None):
+ if only is not None:
+ if only == "submissions":
+ only = "links"
+ RedditBase._safely_add_arguments(
+ arguments=generator_kwargs, key="params", only=only
+ )
@cachedproperty
- def stylesheet(self) -> praw.models.reddit.subreddit.SubredditStylesheet:
- """Provide an instance of :class:`.SubredditStylesheet`.
+ def notes(self) -> praw.models.SubredditModNotes:
+ """Provide an instance of :class:`.SubredditModNotes`.
- For example, to add the css data ``.test{color:blue}`` to the existing
- stylesheet:
+ This provides an interface for managing moderator notes for this subreddit.
+
+ For example, all the notes for u/spez in r/test can be iterated through like so:
.. code-block:: python
subreddit = reddit.subreddit("test")
- stylesheet = subreddit.stylesheet()
- stylesheet.stylesheet += ".test{color:blue}"
- subreddit.stylesheet.update(stylesheet.stylesheet)
+
+ for note in subreddit.mod.notes.redditors("spez"):
+ print(f"{note.label}: {note.note}")
"""
- return SubredditStylesheet(self)
+ from praw.models.mod_notes import SubredditModNotes
- @cachedproperty
- def widgets(self) -> praw.models.SubredditWidgets:
- """Provide an instance of :class:`.SubredditWidgets`.
+ return SubredditModNotes(self.subreddit._reddit, subreddit=self.subreddit)
- **Example usage**
+ @cachedproperty
+ def removal_reasons(self) -> SubredditRemovalReasons:
+ """Provide an instance of :class:`.SubredditRemovalReasons`.
- Get all sidebar widgets:
+ Use this attribute for interacting with a :class:`.Subreddit`'s removal reasons.
+ For example to list all the removal reasons for a subreddit which you have the
+ ``posts`` moderator permission on, try:
.. code-block:: python
- for widget in reddit.subreddit("test").widgets.sidebar:
- print(widget)
+ for removal_reason in reddit.subreddit("test").mod.removal_reasons:
+ print(removal_reason)
- Get ID card widget:
+ A single removal reason can be lazily retrieved via:
.. code-block:: python
- print(reddit.subreddit("test").widgets.id_card)
+ reddit.subreddit("test").mod.removal_reasons["reason_id"]
+
+ .. note::
+
+ Attempting to access attributes of an nonexistent removal reason will result
+ in a :class:`.ClientException`.
"""
- return SubredditWidgets(self)
+ return SubredditRemovalReasons(self.subreddit)
@cachedproperty
- def wiki(self) -> praw.models.reddit.subreddit.SubredditWiki:
- """Provide an instance of :class:`.SubredditWiki`.
+ def stream(self) -> praw.models.reddit.subreddit.SubredditModerationStream:
+ """Provide an instance of :class:`.SubredditModerationStream`.
- This attribute can be used to discover all wikipages for a subreddit:
+ Streams can be used to indefinitely retrieve Moderator only items from
+ :class:`.SubredditModeration` made to moderated subreddits, like:
.. code-block:: python
- for wikipage in reddit.subreddit("test").wiki:
- print(wikipage)
+ for log in reddit.subreddit("mod").mod.stream.log():
+ print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
- To fetch the content for a given wikipage try:
+ """
+ return SubredditModerationStream(self.subreddit)
- .. code-block:: python
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditModeration` instance.
- wikipage = reddit.subreddit("test").wiki["proof"]
- print(wikipage.content_md)
+ :param subreddit: The subreddit to moderate.
"""
- return SubredditWiki(self)
-
- @property
- def _kind(self) -> str: # noqa: ANN001
- """Return the class's kind."""
- return self._reddit.config.kinds["subreddit"]
-
- def __init__(
- self,
- reddit: praw.Reddit,
- display_name: str | None = None,
- _data: dict[str, Any] | None = None,
- ):
- """Initialize a :class:`.Subreddit` instance.
+ self.subreddit = subreddit
+ self._stream = None
- :param reddit: An instance of :class:`.Reddit`.
- :param display_name: The name of the subreddit.
+ def accept_invite(self):
+ """Accept an invitation as a moderator of the community."""
+ url = API_PATH["accept_mod_invite"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url)
- .. note::
+ @_deprecate_args("only")
+ def edited(
+ self, *, only: str | None = None, **generator_kwargs: Any
+ ) -> Iterator[praw.models.Comment | praw.models.Submission]:
+ """Return a :class:`.ListingGenerator` for edited comments and submissions.
- This class should not be initialized directly. Instead, obtain an instance
- via: ``reddit.subreddit("test")``
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- """
- if (display_name, _data).count(None) != 1:
- msg = "Either 'display_name' or '_data' must be provided."
- raise TypeError(msg)
- if display_name:
- self.display_name = display_name
- super().__init__(reddit, _data=_data)
- self._path = API_PATH["subreddit"].format(subreddit=self)
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- def _convert_to_fancypants(self, markdown_text: str) -> dict: # noqa: ANN001
- """Convert a Markdown string to a dict for use with the ``richtext_json`` param.
+ To print all items in the edited queue try:
- :param markdown_text: A Markdown string to convert.
+ .. code-block:: python
- :returns: A dict in ``richtext_json`` format.
+ for item in reddit.subreddit("mod").mod.edited(limit=None):
+ print(item)
"""
- text_data = {"output_mode": "rtjson", "markdown_text": markdown_text}
- return self._reddit.post(API_PATH["convert_rte_body"], data=text_data)["output"]
-
- def _fetch(self): # noqa: ANN001
- data = self._fetch_data()
- data = data["data"]
- other = type(self)(self._reddit, _data=data)
- self.__dict__.update(other.__dict__)
- self._fetched = True
-
- def _fetch_data(self) -> dict: # noqa: ANN001
- name, fields, params = self._fetch_info()
- path = API_PATH[name].format(**fields)
- return self._reddit.request(method="GET", params=params, path=path)
-
- def _fetch_info(self): # noqa: ANN001
- return "subreddit_about", {"subreddit": self}, None
+ self._handle_only(generator_kwargs=generator_kwargs, only=only)
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_edited"].format(subreddit=self.subreddit),
+ **generator_kwargs,
+ )
- def _parse_xml_response(self, response: Response): # noqa: ANN001
- """Parse the XML from a response and raise any errors found."""
- xml = response.text
- root = XML(xml)
- tags = [element.tag for element in root]
- if tags[:4] == ["Code", "Message", "ProposedSize", "MaxSizeAllowed"]:
- # Returned if image is too big
- code, message, actual, maximum_size = (element.text for element in root[:4])
- raise TooLargeMediaException(
- actual=int(actual), maximum_size=int(maximum_size)
- )
+ def inbox(self, **generator_kwargs: Any) -> Iterator[praw.models.SubredditMessage]:
+ """Return a :class:`.ListingGenerator` for moderator messages.
- def _read_and_post_media(self, media_path, upload_url, upload_data): # noqa: ANN001
- with media_path.open("rb") as media:
- return self._reddit._core._requestor._http.post(
- upload_url, data=upload_data, files={"file": media}
- )
+ .. warning::
- def _submit_media(
- self, *, data: dict[Any, Any], timeout: int, without_websockets: bool
- ):
- """Submit and return an ``image``, ``video``, or ``videogif``.
+ Legacy modmail is being deprecated in June 2021. Please see
+ https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/
+ for more info.
- This is a helper method for submitting posts that are not link posts or self
- posts.
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- """
- response = self._reddit.post(API_PATH["submit"], data=data)
- websocket_url = response["json"]["data"]["websocket_url"]
- connection = None
- if websocket_url is not None and not without_websockets:
- try:
- connection = websocket.create_connection(websocket_url, timeout=timeout)
- except (
- OSError,
- websocket.WebSocketException,
- BlockingIOError,
- ) as ws_exception:
- msg = "Error establishing websocket connection."
- raise WebSocketException(msg, ws_exception) from None
+ .. seealso::
- if connection is None:
- return None
+ :meth:`.unread` for unread moderator messages.
- try:
- ws_update = loads(connection.recv())
- connection.close()
- except (OSError, websocket.WebSocketException, BlockingIOError) as ws_exception:
- msg = "Websocket error. Check your media file. Your post may still have been created."
- raise WebSocketException(
- msg,
- ws_exception,
- ) from None
- if ws_update.get("type") == "failed":
- raise MediaPostFailed
- url = ws_update["payload"]["redirect"]
- return self._reddit.submission(url=url)
+ To print the last 5 moderator mail messages and their replies, try:
- def _upload_inline_media(
- self, inline_media: praw.models.InlineMedia
- ): # noqa: ANN001
- """Upload media for use in self posts and return ``inline_media``.
+ .. code-block:: python
- :param inline_media: An :class:`.InlineMedia` object to validate and upload.
+ for message in reddit.subreddit("mod").mod.inbox(limit=5):
+ print(f"From: {message.author}, Body: {message.body}")
+ for reply in message.replies:
+ print(f"From: {reply.author}, Body: {reply.body}")
"""
- self._validate_inline_media(inline_media)
- inline_media.media_id = self._upload_media(
- media_path=inline_media.path, upload_type="selfpost"
+ warn(
+ "Legacy modmail is being deprecated in June 2021. Please see"
+ " https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/"
+ " for more info.",
+ category=DeprecationWarning,
+ stacklevel=3,
+ )
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["moderator_messages"].format(subreddit=self.subreddit),
+ **generator_kwargs,
)
- return inline_media
- def _upload_media(
+ @_deprecate_args("action", "mod")
+ def log(
self,
*,
- expected_mime_prefix: str | None = None,
- media_path: str,
- upload_type: str = "link",
- ):
- """Upload media and return its URL and a websocket (Undocumented endpoint).
-
- :param expected_mime_prefix: If provided, enforce that the media has a mime type
- that starts with the provided prefix.
- :param upload_type: One of ``"link"``, ``"gallery"'', or ``"selfpost"``
- (default: ``"link"``).
-
- :returns: A tuple containing ``(media_url, websocket_url)`` for the piece of
- media. The websocket URL can be used to determine when media processing is
- finished, or it can be ignored.
-
- """
- if media_path is None:
- file = Path(__file__).absolute()
- media_path = file.parent.parent.parent / "images" / "PRAW logo.png"
- else:
- file = Path(media_path)
+ action: str | None = None,
+ mod: praw.models.Redditor | str | None = None,
+ **generator_kwargs: Any,
+ ) -> Iterator[praw.models.ModAction]:
+ """Return a :class:`.ListingGenerator` for moderator log entries.
- file_name = file.name.lower()
- file_extension = file_name.rpartition(".")[2]
- mime_type = {
- "png": "image/png",
- "mov": "video/quicktime",
- "mp4": "video/mp4",
- "jpg": "image/jpeg",
- "jpeg": "image/jpeg",
- "gif": "image/gif",
- }.get(
- file_extension, "image/jpeg"
- ) # default to JPEG
- if (
- expected_mime_prefix is not None
- and mime_type.partition("/")[0] != expected_mime_prefix
- ):
- msg = f"Expected a mimetype starting with {expected_mime_prefix!r} but got mimetype {mime_type!r} (from file extension {file_extension!r})."
- raise ClientException(msg)
- img_data = {"filepath": file_name, "mimetype": mime_type}
+ :param action: If given, only return log entries for the specified action.
+ :param mod: If given, only return log entries for actions made by the passed in
+ redditor.
- url = API_PATH["media_asset"]
- # until we learn otherwise, assume this request always succeeds
- upload_response = self._reddit.post(url, data=img_data)
- upload_lease = upload_response["args"]
- upload_url = f"https:{upload_lease['action']}"
- upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- response = self._read_and_post_media(file, upload_url, upload_data)
- if not response.ok:
- self._parse_xml_response(response)
- try:
- response.raise_for_status()
- except HTTPError as err:
- raise ServerError(response=err.response) from None
+ To print the moderator and subreddit of the last 5 modlog entries try:
- upload_response["asset"]["websocket_url"]
+ .. code-block:: python
- if upload_type == "link":
- return f"{upload_url}/{upload_data['key']}"
- return upload_response["asset"]["asset_id"]
+ for log in reddit.subreddit("mod").mod.log(limit=5):
+ print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
- def post_requirements(self) -> dict[str, str | int | bool]:
- """Get the post requirements for a subreddit.
+ """
+ params = {"mod": str(mod) if mod else mod, "type": action}
+ Subreddit._safely_add_arguments(
+ arguments=generator_kwargs, key="params", **params
+ )
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_log"].format(subreddit=self.subreddit),
+ **generator_kwargs,
+ )
- :returns: A dict with the various requirements.
+ @_deprecate_args("only")
+ def modqueue(
+ self, *, only: str | None = None, **generator_kwargs: Any
+ ) -> Iterator[praw.models.Submission | praw.models.Comment]:
+ """Return a :class:`.ListingGenerator` for modqueue items.
- The returned dict contains the following keys:
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- - ``domain_blacklist``
- - ``body_restriction_policy``
- - ``domain_whitelist``
- - ``title_regexes``
- - ``body_blacklisted_strings``
- - ``body_required_strings``
- - ``title_text_min_length``
- - ``is_flair_required``
- - ``title_text_max_length``
- - ``body_regexes``
- - ``link_repost_age``
- - ``body_text_min_length``
- - ``link_restriction_policy``
- - ``body_text_max_length``
- - ``title_required_strings``
- - ``title_blacklisted_strings``
- - ``guidelines_text``
- - ``guidelines_display_policy``
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- For example, to fetch the post requirements for r/test:
+ To print all modqueue items try:
.. code-block:: python
- print(reddit.subreddit("test").post_requirements)
+ for item in reddit.subreddit("mod").mod.modqueue(limit=None):
+ print(item)
"""
- return self._reddit.get(
- API_PATH["post_requirements"].format(subreddit=str(self))
+ self._handle_only(generator_kwargs=generator_kwargs, only=only)
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_modqueue"].format(subreddit=self.subreddit),
+ **generator_kwargs,
)
- def random(self) -> praw.models.Submission | None:
- """Return a random :class:`.Submission`.
+ @_deprecate_args("only")
+ def reports(
+ self, *, only: str | None = None, **generator_kwargs: Any
+ ) -> Iterator[praw.models.Submission | praw.models.Comment]:
+ """Return a :class:`.ListingGenerator` for reported comments and submissions.
- Returns ``None`` on subreddits that do not support the random feature. One
- example, at the time of writing, is r/wallpapers.
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- For example, to get a random submission off of r/AskReddit:
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
+
+ To print the user and mod report reasons in the report queue try:
.. code-block:: python
- submission = reddit.subreddit("AskReddit").random()
- print(submission.title)
+ for reported_item in reddit.subreddit("mod").mod.reports():
+ print(f"User Reports: {reported_item.user_reports}")
+ print(f"Mod Reports: {reported_item.mod_reports}")
"""
- url = API_PATH["subreddit_random"].format(subreddit=self)
- try:
- self._reddit.get(url, params={"unique": self._reddit._next_unique})
- except Redirect as redirect:
- path = redirect.path
- try:
- return self._submission_class(
- self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
- )
- except ClientException:
- return None
-
- @_deprecate_args("query", "sort", "syntax", "time_filter")
- def search(
- self,
- query: str,
- *,
- sort: str = "relevance",
- syntax: str = "lucene",
- time_filter: str = "all",
- **generator_kwargs: Any,
- ) -> Iterator[praw.models.Submission]:
- """Return a :class:`.ListingGenerator` for items that match ``query``.
+ self._handle_only(generator_kwargs=generator_kwargs, only=only)
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_reports"].format(subreddit=self.subreddit),
+ **generator_kwargs,
+ )
- :param query: The query string to search for.
- :param sort: Can be one of: ``"relevance"``, ``"hot"``, ``"top"``, ``"new"``, or
- ``"comments"``. (default: ``"relevance"``).
- :param syntax: Can be one of: ``"cloudsearch"``, ``"lucene"``, or ``"plain"``
- (default: ``"lucene"``).
- :param time_filter: Can be one of: ``"all"``, ``"day"``, ``"hour"``,
- ``"month"``, ``"week"``, or ``"year"`` (default: ``"all"``).
+ def settings(self) -> dict[str, str | int | bool]:
+ """Return a dictionary of the :class:`.Subreddit`'s current settings."""
+ url = API_PATH["subreddit_settings"].format(subreddit=self.subreddit)
+ return self.subreddit._reddit.get(url)["data"]
- For more information on building a search query see:
- https://www.reddit.com/wiki/search
+ @_deprecate_args("only")
+ def spam(
+ self, *, only: str | None = None, **generator_kwargs: Any
+ ) -> Iterator[praw.models.Submission | praw.models.Comment]:
+ """Return a :class:`.ListingGenerator` for spam comments and submissions.
- For example, to search all subreddits for ``"praw"`` try:
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
+
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
+
+ To print the items in the spam queue try:
.. code-block:: python
- for submission in reddit.subreddit("all").search("praw"):
- print(submission.title)
+ for item in reddit.subreddit("mod").mod.spam():
+ print(item)
"""
- self._validate_time_filter(time_filter)
- not_all = self.display_name.lower() != "all"
- self._safely_add_arguments(
- arguments=generator_kwargs,
- key="params",
- q=query,
- restrict_sr=not_all,
- sort=sort,
- syntax=syntax,
- t=time_filter,
+ self._handle_only(generator_kwargs=generator_kwargs, only=only)
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_spam"].format(subreddit=self.subreddit),
+ **generator_kwargs,
)
- url = API_PATH["search"].format(subreddit=self)
- return ListingGenerator(self._reddit, url, **generator_kwargs)
- @_deprecate_args("number")
- def sticky(self, *, number: int = 1) -> praw.models.Submission:
- """Return a :class:`.Submission` object for a sticky of the subreddit.
-
- :param number: Specify which sticky to return. 1 appears at the top (default:
- ``1``).
+ def unmoderated(self, **generator_kwargs: Any) -> Iterator[praw.models.Submission]:
+ """Return a :class:`.ListingGenerator` for unmoderated submissions.
- :raises: ``prawcore.NotFound`` if the sticky does not exist.
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- For example, to get the stickied post on r/test:
+ To print the items in the unmoderated queue try:
.. code-block:: python
- reddit.subreddit("test").sticky()
+ for item in reddit.subreddit("mod").mod.unmoderated():
+ print(item)
"""
- url = API_PATH["about_sticky"].format(subreddit=self)
- try:
- self._reddit.get(url, params={"num": number})
- except Redirect as redirect:
- path = redirect.path
- return self._submission_class(
- self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["about_unmoderated"].format(subreddit=self.subreddit),
+ **generator_kwargs,
)
- @_deprecate_args(
- "title",
- "selftext",
- "url",
- "flair_id",
- "flair_text",
- "resubmit",
- "send_replies",
- "nsfw",
- "spoiler",
- "collection_id",
- "discussion_type",
- "inline_media",
- "draft_id",
- )
- def submit(
- self,
- title: str,
- *,
- collection_id: str | None = None,
- discussion_type: str | None = None,
- draft_id: str | None = None,
- flair_id: str | None = None,
- flair_text: str | None = None,
- inline_media: dict[str, praw.models.InlineMedia] | None = None,
- nsfw: bool = False,
- resubmit: bool = True,
- selftext: str | None = None,
- send_replies: bool = True,
- spoiler: bool = False,
- url: str | None = None,
- ) -> praw.models.Submission: # noqa: D301
- r"""Add a submission to the :class:`.Subreddit`.
-
- :param title: The title of the submission.
- :param collection_id: The UUID of a :class:`.Collection` to add the
- newly-submitted post to.
- :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
- traditional comments (default: ``None``).
- :param draft_id: The ID of a draft to submit.
- :param flair_id: The flair template to select (default: ``None``).
- :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
- this value will set a custom text (default: ``None``). ``flair_id`` is
- required when ``flair_text`` is provided.
- :param inline_media: A dict of :class:`.InlineMedia` objects where the key is
- the placeholder name in ``selftext``.
- :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
- :param resubmit: When ``False``, an error will occur if the URL has already been
- submitted (default: ``True``).
- :param selftext: The Markdown formatted content for a ``text`` submission. Use
- an empty string, ``""``, to make a title-only submission.
- :param send_replies: When ``True``, messages will be sent to the submission
- author when comments are made to the submission (default: ``True``).
- :param spoiler: Whether the submission should be marked as a spoiler (default:
- ``False``).
- :param url: The URL for a ``link`` submission.
+ def unread(self, **generator_kwargs: Any) -> Iterator[praw.models.SubredditMessage]:
+ """Return a :class:`.ListingGenerator` for unread moderator messages.
- :returns: A :class:`.Submission` object for the newly created submission.
+ .. warning::
- Either ``selftext`` or ``url`` can be provided, but not both.
+ Legacy modmail is being deprecated in June 2021. Please see
+ https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/
+ for more info.
- For example, to submit a URL to r/test do:
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- .. code-block:: python
+ .. seealso::
- title = "PRAW documentation"
- url = "https://praw.readthedocs.io"
- reddit.subreddit("test").submit(title, url=url)
+ :meth:`.inbox` for all messages.
- For example, to submit a self post with inline media do:
+ To print the mail in the unread modmail queue try:
.. code-block:: python
- from praw.models import InlineGif, InlineImage, InlineVideo
-
- gif = InlineGif(path="path/to/image.gif", caption="optional caption")
- image = InlineImage(path="path/to/image.jpg", caption="optional caption")
- video = InlineVideo(path="path/to/video.mp4", caption="optional caption")
- selftext = "Text with a gif {gif1} an image {image1} and a video {video1} inline"
- media = {"gif1": gif, "image1": image, "video1": video}
- reddit.subreddit("test").submit("title", inline_media=media, selftext=selftext)
-
- .. note::
-
- Inserted media will have a padding of ``\\n\\n`` automatically added. This
- is due to the weirdness with Reddit's API. Using the example above, the
- result selftext body will look like so:
-
- .. code-block::
-
- Text with a gif
-
- ![gif](u1rchuphryq51 "optional caption")
-
- an image
-
- ![img](srnr8tshryq51 "optional caption")
-
- and video
-
- ![video](gmc7rvthryq51 "optional caption")
-
- inline
-
- .. note::
-
- To submit a post to a subreddit with the ``"news"`` flair, you can get the
- flair id like this:
-
- .. code-block::
-
- choices = list(subreddit.flair.link_templates.user_selectable())
- template_id = next(x for x in choices if x["flair_text"] == "news")["flair_template_id"]
- subreddit.submit("title", flair_id=template_id, url="https://www.news.com/")
+ for message in reddit.subreddit("mod").mod.unread():
+ print(f"From: {message.author}, To: {message.dest}")
- .. seealso::
+ """
+ warn(
+ "Legacy modmail is being deprecated in June 2021. Please see"
+ " https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/"
+ " for more info.",
+ category=DeprecationWarning,
+ stacklevel=3,
+ )
+ return ListingGenerator(
+ self.subreddit._reddit,
+ API_PATH["moderator_unread"].format(subreddit=self.subreddit),
+ **generator_kwargs,
+ )
- - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
- same post
- - :meth:`~.Subreddit.submit_image` to submit images
- - :meth:`~.Subreddit.submit_poll` to submit polls
- - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
+ def update(self, **settings: str | int | bool) -> dict[str, str | int | bool]:
+ """Update the :class:`.Subreddit`'s settings.
- """
- if (bool(selftext) or selftext == "") == bool(url):
- msg = "Either 'selftext' or 'url' must be provided."
- raise TypeError(msg)
+ See https://www.reddit.com/dev/api#POST_api_site_admin for the full list.
- data = {
- "sr": str(self),
- "resubmit": bool(resubmit),
- "sendreplies": bool(send_replies),
- "title": title,
- "nsfw": bool(nsfw),
- "spoiler": bool(spoiler),
- "validate_on_submit": self._reddit.validate_on_submit,
- }
- for key, value in (
- ("flair_id", flair_id),
- ("flair_text", flair_text),
- ("collection_id", collection_id),
- ("discussion_type", discussion_type),
- ("draft_id", draft_id),
- ):
- if value is not None:
- data[key] = value
- if selftext is not None:
- data.update(kind="self")
- if inline_media:
- body = selftext.format(
- **{
- placeholder: self._upload_inline_media(media)
- for placeholder, media in inline_media.items()
- }
- )
- converted = self._convert_to_fancypants(body)
- data.update(richtext_json=dumps(converted))
- else:
- data.update(text=selftext)
- else:
- data.update(kind="link", url=url)
-
- return self._reddit.post(API_PATH["submit"], data=data)
-
- @_deprecate_args(
- "title",
- "images",
- "collection_id",
- "discussion_type",
- "flair_id",
- "flair_text",
- "nsfw",
- "send_replies",
- "spoiler",
- )
- def submit_gallery(
- self,
- title: str,
- images: list[dict[str, str]],
- *,
- collection_id: str | None = None,
- discussion_type: str | None = None,
- flair_id: str | None = None,
- flair_text: str | None = None,
- nsfw: bool = False,
- send_replies: bool = True,
- spoiler: bool = False,
- ) -> praw.models.Submission:
- """Add an image gallery submission to the subreddit.
-
- :param title: The title of the submission.
- :param images: The images to post in dict with the following structure:
- ``{"image_path": "path", "caption": "caption", "outbound_url": "url"}``,
- only ``image_path`` is required.
- :param collection_id: The UUID of a :class:`.Collection` to add the
- newly-submitted post to.
- :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
- traditional comments (default: ``None``).
- :param flair_id: The flair template to select (default: ``None``).
- :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
- this value will set a custom text (default: ``None``). ``flair_id`` is
- required when ``flair_text`` is provided.
- :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
- :param send_replies: When ``True``, messages will be sent to the submission
- author when comments are made to the submission (default: ``True``).
- :param spoiler: Whether the submission should be marked asa spoiler (default:
- ``False``).
-
- :returns: A :class:`.Submission` object for the newly created submission.
-
- :raises: :class:`.ClientException` if ``image_path`` in ``images`` refers to a
- file that is not an image.
-
- For example, to submit an image gallery to r/test do:
-
- .. code-block:: python
-
- title = "My favorite pictures"
- image = "/path/to/image.png"
- image2 = "/path/to/image2.png"
- image3 = "/path/to/image3.png"
- images = [
- {"image_path": image},
- {
- "image_path": image2,
- "caption": "Image caption 2",
- },
- {
- "image_path": image3,
- "caption": "Image caption 3",
- "outbound_url": "https://example.com/link3",
- },
- ]
- reddit.subreddit("test").submit_gallery(title, images)
-
- .. seealso::
-
- - :meth:`~.Subreddit.submit` to submit url posts and selftexts
- - :meth:`~.Subreddit.submit_image` to submit single images
- - :meth:`~.Subreddit.submit_poll` to submit polls
- - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
-
- """
- self._validate_gallery(images)
- data = {
- "api_type": "json",
- "items": [],
- "nsfw": bool(nsfw),
- "sendreplies": bool(send_replies),
- "show_error_list": True,
- "spoiler": bool(spoiler),
- "sr": str(self),
- "title": title,
- "validate_on_submit": self._reddit.validate_on_submit,
- }
- for key, value in (
- ("flair_id", flair_id),
- ("flair_text", flair_text),
- ("collection_id", collection_id),
- ("discussion_type", discussion_type),
- ):
- if value is not None:
- data[key] = value
- for image in images:
- data["items"].append(
- {
- "caption": image.get("caption", ""),
- "outbound_url": image.get("outbound_url", ""),
- "media_id": self._upload_media(
- expected_mime_prefix="image",
- media_path=image["image_path"],
- upload_type="gallery",
- ),
- }
- )
- response = self._reddit.request(
- json=data, method="POST", path=API_PATH["submit_gallery_post"]
- )["json"]
- if response["errors"]:
- raise RedditAPIException(response["errors"])
- return self._reddit.submission(url=response["data"]["url"])
-
- @_deprecate_args(
- "title",
- "image_path",
- "flair_id",
- "flair_text",
- "resubmit",
- "send_replies",
- "nsfw",
- "spoiler",
- "timeout",
- "collection_id",
- "without_websockets",
- "discussion_type",
- )
- def submit_image(
- self,
- title: str,
- image_path: str,
- *,
- collection_id: str | None = None,
- discussion_type: str | None = None,
- flair_id: str | None = None,
- flair_text: str | None = None,
- nsfw: bool = False,
- resubmit: bool = True,
- send_replies: bool = True,
- spoiler: bool = False,
- timeout: int = 10,
- without_websockets: bool = False,
- ) -> praw.models.Submission:
- """Add an image submission to the subreddit.
-
- :param collection_id: The UUID of a :class:`.Collection` to add the
- newly-submitted post to.
- :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
- traditional comments (default: ``None``).
- :param flair_id: The flair template to select (default: ``None``).
- :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
- this value will set a custom text (default: ``None``). ``flair_id`` is
- required when ``flair_text`` is provided.
- :param image_path: The path to an image, to upload and post.
- :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
- :param resubmit: When ``False``, an error will occur if the URL has already been
- submitted (default: ``True``).
- :param send_replies: When ``True``, messages will be sent to the submission
- author when comments are made to the submission (default: ``True``).
- :param spoiler: Whether the submission should be marked as a spoiler (default:
- ``False``).
- :param timeout: Specifies a particular timeout, in seconds. Use to avoid
- "Websocket error" exceptions (default: ``10``).
- :param title: The title of the submission.
- :param without_websockets: Set to ``True`` to disable use of WebSockets (see
- note below for an explanation). If ``True``, this method doesn't return
- anything (default: ``False``).
-
- :returns: A :class:`.Submission` object for the newly created submission, unless
- ``without_websockets`` is ``True``.
-
- :raises: :class:`.ClientException` if ``image_path`` refers to a file that is
- not an image.
+ :param all_original_content: Mandate all submissions to be original content
+ only.
+ :param allow_chat_post_creation: Allow users to create chat submissions.
+ :param allow_images: Allow users to upload images using the native image
+ hosting.
+ :param allow_polls: Allow users to post polls to the subreddit.
+ :param allow_post_crossposts: Allow users to crosspost submissions from other
+ subreddits.
+ :param allow_videos: Allow users to upload videos using the native image
+ hosting.
+ :param collapse_deleted_comments: Collapse deleted and removed comments on
+ comments pages by default.
+ :param comment_score_hide_mins: The number of minutes to hide comment scores.
+ :param content_options: The types of submissions users can make. One of
+ ``"any"``, ``"link"``, or ``"self"``.
+ :param crowd_control_chat_level: Controls the crowd control level for chat
+ rooms. Goes from 0-3.
+ :param crowd_control_level: Controls the crowd control level for submissions.
+ Goes from 0-3.
+ :param crowd_control_mode: Enables/disables crowd control.
+ :param default_set: Allow the subreddit to appear on r/all as well as the
+ default and trending lists.
+ :param disable_contributor_requests: Specifies whether redditors may send
+ automated modmail messages requesting approval as a submitter.
+ :param exclude_banned_modqueue: Exclude posts by site-wide banned users from
+ modqueue/unmoderated.
+ :param free_form_reports: Allow users to specify custom reasons in the report
+ menu.
+ :param header_hover_text: The text seen when hovering over the snoo.
+ :param hide_ads: Don't show ads within this subreddit. Only applies to
+ Premium-user only subreddits.
+ :param key_color: A 6-digit rgb hex color (e.g., ``"#AABBCC"``), used as a
+ thematic color for your subreddit on mobile.
+ :param language: A valid IETF language tag (underscore separated).
+ :param original_content_tag_enabled: Enables the use of the ``original content``
+ label for submissions.
+ :param over_18: Viewers must be over 18 years old (i.e., NSFW).
+ :param public_description: Public description blurb. Appears in search results
+ and on the landing page for private subreddits.
+ :param restrict_commenting: Specifies whether approved users have the ability to
+ comment.
+ :param restrict_posting: Specifies whether approved users have the ability to
+ submit posts.
+ :param show_media: Show thumbnails on submissions.
+ :param show_media_preview: Expand media previews on comments pages.
+ :param spam_comments: Spam filter strength for comments. One of ``"all"``,
+ ``"low"``, or ``"high"``.
+ :param spam_links: Spam filter strength for links. One of ``"all"``, ``"low"``,
+ or ``"high"``.
+ :param spam_selfposts: Spam filter strength for selfposts. One of ``"all"``,
+ ``"low"``, or ``"high"``.
+ :param spoilers_enabled: Enable marking posts as containing spoilers.
+ :param submit_link_label: Custom label for submit link button (``None`` for
+ default).
+ :param submit_text: Text to show on submission page.
+ :param submit_text_label: Custom label for submit text post button (``None`` for
+ default).
+ :param subreddit_type: One of ``"archived"``, ``"employees_only"``,
+ ``"gold_only"``, ``gold_restricted``, ``"private"``, ``"public"``, or
+ ``"restricted"``.
+ :param suggested_comment_sort: All comment threads will use this sorting method
+ by default. Leave ``None``, or choose one of ``"confidence"``,
+ ``"controversial"``, ``"live"``, ``"new"``, ``"old"``, ``"qa"``,
+ ``"random"``, or ``"top"``.
+ :param title: The title of the subreddit.
+ :param welcome_message_enabled: Enables the subreddit welcome message.
+ :param welcome_message_text: The text to be used as a welcome message. A welcome
+ message is sent to all new subscribers by a Reddit bot.
+ :param wiki_edit_age: Account age, in days, required to edit and create wiki
+ pages.
+ :param wiki_edit_karma: Subreddit karma required to edit and create wiki pages.
+ :param wikimode: One of ``"anyone"``, ``"disabled"``, or ``"modonly"``.
.. note::
- Reddit's API uses WebSockets to respond with the link of the newly created
- post. If this fails, the method will raise :class:`.WebSocketException`.
- Occasionally, the Reddit post will still be created. More often, there is an
- error with the image file. If you frequently get exceptions but successfully
- created posts, try setting the ``timeout`` parameter to a value above 10.
-
- To disable the use of WebSockets, set ``without_websockets=True``. This will
- make the method return ``None``, though the post will still be created. You
- may wish to do this if you are running your program in a restricted network
- environment, or using a proxy that doesn't support WebSockets connections.
-
- For example, to submit an image to r/test do:
+ Updating the subreddit sidebar on old reddit (``description``) is no longer
+ supported using this method. You can update the sidebar by editing the
+ ``"config/sidebar"`` wiki page. For example:
- .. code-block:: python
+ .. code-block:: python
- title = "My favorite picture"
- image = "/path/to/image.png"
- reddit.subreddit("test").submit_image(title, image)
+ sidebar = reddit.subreddit("test").wiki["config/sidebar"]
+ sidebar.edit(content="new sidebar content")
- .. seealso::
+ Additional keyword arguments can be provided to handle new settings as Reddit
+ introduces them.
- - :meth:`~.Subreddit.submit` to submit url posts and selftexts
- - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
- same post
- - :meth:`~.Subreddit.submit_poll` to submit polls
- - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
+ Settings that are documented here and aren't explicitly set by you in a call to
+ :meth:`.SubredditModeration.update` should retain their current value. If they
+ do not, please file a bug.
"""
- data = {
- "sr": str(self),
- "resubmit": bool(resubmit),
- "sendreplies": bool(send_replies),
- "title": title,
- "nsfw": bool(nsfw),
- "spoiler": bool(spoiler),
- "validate_on_submit": self._reddit.validate_on_submit,
+ # These attributes come out using different names than they go in.
+ remap = {
+ "content_options": "link_type",
+ "default_set": "allow_top",
+ "header_hover_text": "header_title",
+ "language": "lang",
+ "subreddit_type": "type",
}
- for key, value in (
- ("flair_id", flair_id),
- ("flair_text", flair_text),
- ("collection_id", collection_id),
- ("discussion_type", discussion_type),
- ):
- if value is not None:
- data[key] = value
-
- image_url = self._upload_media(
- expected_mime_prefix="image", media_path=image_path
- )
- data.update(kind="image", url=image_url)
- return self._submit_media(
- data=data, timeout=timeout, without_websockets=without_websockets
- )
-
- @_deprecate_args(
- "title",
- "selftext",
- "options",
- "duration",
- "flair_id",
- "flair_text",
- "resubmit",
- "send_replies",
- "nsfw",
- "spoiler",
- "collection_id",
- "discussion_type",
- )
- def submit_poll(
- self,
- title: str,
- *,
- collection_id: str | None = None,
- discussion_type: str | None = None,
- duration: int,
- flair_id: str | None = None,
- flair_text: str | None = None,
- nsfw: bool = False,
- options: list[str],
- resubmit: bool = True,
- selftext: str,
- send_replies: bool = True,
- spoiler: bool = False,
- ) -> praw.models.Submission:
- """Add a poll submission to the subreddit.
-
- :param title: The title of the submission.
- :param collection_id: The UUID of a :class:`.Collection` to add the
- newly-submitted post to.
- :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
- traditional comments (default: ``None``).
- :param duration: The number of days the poll should accept votes, as an ``int``.
- Valid values are between ``1`` and ``7``, inclusive.
- :param flair_id: The flair template to select (default: ``None``).
- :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
- this value will set a custom text (default: ``None``). ``flair_id`` is
- required when ``flair_text`` is provided.
- :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
- :param options: A list of two to six poll options as ``str``.
- :param resubmit: When ``False``, an error will occur if the URL has already been
- submitted (default: ``True``).
- :param selftext: The Markdown formatted content for the submission. Use an empty
- string, ``""``, to make a submission with no text contents.
- :param send_replies: When ``True``, messages will be sent to the submission
- author when comments are made to the submission (default: ``True``).
- :param spoiler: Whether the submission should be marked as a spoiler (default:
- ``False``).
-
- :returns: A :class:`.Submission` object for the newly created submission.
-
- For example, to submit a poll to r/test do:
+ settings = {remap.get(key, key): value for key, value in settings.items()}
+ settings["sr"] = self.subreddit.fullname
+ return self.subreddit._reddit.patch(API_PATH["update_settings"], json=settings)
- .. code-block:: python
- title = "Do you like PRAW?"
- reddit.subreddit("test").submit_poll(
- title, selftext="", options=["Yes", "No"], duration=3
- )
+class SubredditModerationStream:
+ """Provides moderator streams."""
- .. seealso::
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditModerationStream` instance.
- - :meth:`~.Subreddit.submit` to submit url posts and selftexts
- - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
- same post
- - :meth:`~.Subreddit.submit_image` to submit single images
- - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
+ :param subreddit: The moderated subreddit associated with the streams.
"""
- data = {
- "sr": str(self),
- "text": selftext,
- "options": options,
- "duration": duration,
- "resubmit": bool(resubmit),
- "sendreplies": bool(send_replies),
- "title": title,
- "nsfw": bool(nsfw),
- "spoiler": bool(spoiler),
- "validate_on_submit": self._reddit.validate_on_submit,
- }
- for key, value in (
- ("flair_id", flair_id),
- ("flair_text", flair_text),
- ("collection_id", collection_id),
- ("discussion_type", discussion_type),
- ):
- if value is not None:
- data[key] = value
-
- return self._reddit.post(API_PATH["submit_poll_post"], json=data)
+ self.subreddit = subreddit
- @_deprecate_args(
- "title",
- "video_path",
- "videogif",
- "thumbnail_path",
- "flair_id",
- "flair_text",
- "resubmit",
- "send_replies",
- "nsfw",
- "spoiler",
- "timeout",
- "collection_id",
- "without_websockets",
- "discussion_type",
- )
- def submit_video(
- self,
- title: str,
- video_path: str,
- *,
- collection_id: str | None = None,
- discussion_type: str | None = None,
- flair_id: str | None = None,
- flair_text: str | None = None,
- nsfw: bool = False,
- resubmit: bool = True,
- send_replies: bool = True,
- spoiler: bool = False,
- thumbnail_path: str | None = None,
- timeout: int = 10,
- videogif: bool = False,
- without_websockets: bool = False,
- ) -> praw.models.Submission:
- """Add a video or videogif submission to the subreddit.
+ @_deprecate_args("only")
+ def edited(
+ self, *, only: str | None = None, **stream_options: Any
+ ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
+ """Yield edited comments and submissions as they become available.
- :param title: The title of the submission.
- :param video_path: The path to a video, to upload and post.
- :param collection_id: The UUID of a :class:`.Collection` to add the
- newly-submitted post to.
- :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
- traditional comments (default: ``None``).
- :param flair_id: The flair template to select (default: ``None``).
- :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
- this value will set a custom text (default: ``None``). ``flair_id`` is
- required when ``flair_text`` is provided.
- :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
- :param resubmit: When ``False``, an error will occur if the URL has already been
- submitted (default: ``True``).
- :param send_replies: When ``True``, messages will be sent to the submission
- author when comments are made to the submission (default: ``True``).
- :param spoiler: Whether the submission should be marked as a spoiler (default:
- ``False``).
- :param thumbnail_path: The path to an image, to be uploaded and used as the
- thumbnail for this video. If not provided, the PRAW logo will be used as the
- thumbnail.
- :param timeout: Specifies a particular timeout, in seconds. Use to avoid
- "Websocket error" exceptions (default: ``10``).
- :param videogif: If ``True``, the video is uploaded as a videogif, which is
- essentially a silent video (default: ``False``).
- :param without_websockets: Set to ``True`` to disable use of WebSockets (see
- note below for an explanation). If ``True``, this method doesn't return
- anything (default: ``False``).
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- :returns: A :class:`.Submission` object for the newly created submission, unless
- ``without_websockets`` is ``True``.
+ Keyword arguments are passed to :func:`.stream_generator`.
- :raises: :class:`.ClientException` if ``video_path`` refers to a file that is
- not a video.
+ For example, to retrieve all new edited submissions/comments made to all
+ moderated subreddits, try:
- .. note::
+ .. code-block:: python
- Reddit's API uses WebSockets to respond with the link of the newly created
- post. If this fails, the method will raise :class:`.WebSocketException`.
- Occasionally, the Reddit post will still be created. More often, there is an
- error with the image file. If you frequently get exceptions but successfully
- created posts, try setting the ``timeout`` parameter to a value above 10.
+ for item in reddit.subreddit("mod").mod.stream.edited():
+ print(item)
- To disable the use of WebSockets, set ``without_websockets=True``. This will
- make the method return ``None``, though the post will still be created. You
- may wish to do this if you are running your program in a restricted network
- environment, or using a proxy that doesn't support WebSockets connections.
+ """
+ return stream_generator(self.subreddit.mod.edited, only=only, **stream_options)
- For example, to submit a video to r/test do:
+ @_deprecate_args("action", "mod")
+ def log(
+ self,
+ *,
+ action: str | None = None,
+ mod: str | praw.models.Redditor | None = None,
+ **stream_options: Any,
+ ) -> Generator[praw.models.ModAction, None, None]:
+ """Yield moderator log entries as they become available.
- .. code-block:: python
+ :param action: If given, only return log entries for the specified action.
+ :param mod: If given, only return log entries for actions made by the passed in
+ redditor.
- title = "My favorite movie"
- video = "/path/to/video.mp4"
- reddit.subreddit("test").submit_video(title, video)
+ For example, to retrieve all new mod actions made to all moderated subreddits,
+ try:
- .. seealso::
+ .. code-block:: python
- - :meth:`~.Subreddit.submit` to submit url posts and selftexts
- - :meth:`~.Subreddit.submit_image` to submit images
- - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
- same post
- - :meth:`~.Subreddit.submit_poll` to submit polls
+ for log in reddit.subreddit("mod").mod.stream.log():
+ print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
"""
- data = {
- "sr": str(self),
- "resubmit": bool(resubmit),
- "sendreplies": bool(send_replies),
- "title": title,
- "nsfw": bool(nsfw),
- "spoiler": bool(spoiler),
- "validate_on_submit": self._reddit.validate_on_submit,
- }
- for key, value in (
- ("flair_id", flair_id),
- ("flair_text", flair_text),
- ("collection_id", collection_id),
- ("discussion_type", discussion_type),
- ):
- if value is not None:
- data[key] = value
-
- video_url = self._upload_media(
- expected_mime_prefix="video", media_path=video_path
- )
- data.update(
- kind="videogif" if videogif else "video",
- url=video_url,
- # if thumbnail_path is None, it uploads the PRAW logo
- video_poster_url=self._upload_media(media_path=thumbnail_path),
- )
- return self._submit_media(
- data=data, timeout=timeout, without_websockets=without_websockets
+ return stream_generator(
+ self.subreddit.mod.log,
+ attribute_name="id",
+ action=action,
+ mod=mod,
+ **stream_options,
)
- @_deprecate_args("other_subreddits")
- def subscribe(self, *, other_subreddits: list[praw.models.Subreddit] | None = None):
- """Subscribe to the subreddit.
+ @_deprecate_args("other_subreddits", "sort", "state")
+ def modmail_conversations(
+ self,
+ *,
+ other_subreddits: list[praw.models.Subreddit] | None = None,
+ sort: str | None = None,
+ state: str | None = None,
+ **stream_options: Any,
+ ) -> Generator[ModmailConversation, None, None]:
+ """Yield new-modmail conversations as they become available.
- :param other_subreddits: When provided, also subscribe to the provided list of
- subreddits.
+ :param other_subreddits: A list of :class:`.Subreddit` instances for which to
+ fetch conversations (default: ``None``).
+ :param sort: Can be one of: ``"mod"``, ``"recent"``, ``"unread"``, or ``"user"``
+ (default: ``"recent"``).
+ :param state: Can be one of: ``"all"``, ``"appeals"``, ``"archived"``,
+ ``"default"``, ``"highlighted"``, ``"inbox"``, ``"inprogress"``,
+ ``"join_requests"``, ``"mod"``, ``"new"``, or ``"notifications"`` (default:
+ ``"all"``). ``"all"`` does not include mod or archived conversations.
+ ``"inbox"`` does not include appeals conversations.
- For example, to subscribe to r/test:
+ Keyword arguments are passed to :func:`.stream_generator`.
+
+ To print new mail in the unread modmail queue try:
.. code-block:: python
- reddit.subreddit("test").subscribe()
+ subreddit = reddit.subreddit("all")
+ for message in subreddit.mod.stream.modmail_conversations():
+ print(f"From: {message.owner}, To: {message.participant}")
"""
- data = {
- "action": "sub",
- "skip_inital_defaults": True,
- "sr_name": self._subreddit_list(
- other_subreddits=other_subreddits, subreddit=self
- ),
- }
- self._reddit.post(API_PATH["subscribe"], data=data)
-
- def traffic(self) -> dict[str, list[list[int]]]:
- """Return a dictionary of the :class:`.Subreddit`'s traffic statistics.
-
- :raises: ``prawcore.NotFound`` when the traffic stats aren't available to the
- authenticated user, that is, they are not public and the authenticated user
- is not a moderator of the subreddit.
+ if self.subreddit == "mod":
+ self.subreddit = self.subreddit._reddit.subreddit("all")
+ return stream_generator(
+ self.subreddit.modmail.conversations,
+ attribute_name="id",
+ exclude_before=True,
+ other_subreddits=other_subreddits,
+ sort=sort,
+ state=state,
+ **stream_options,
+ )
- The traffic method returns a dict with three keys. The keys are ``day``,
- ``hour`` and ``month``. Each key contains a list of lists with 3 or 4 values.
- The first value is a timestamp indicating the start of the category (start of
- the day for the ``day`` key, start of the hour for the ``hour`` key, etc.). The
- second, third, and fourth values indicate the unique pageviews, total pageviews,
- and subscribers, respectively.
+ @_deprecate_args("only")
+ def modqueue(
+ self, *, only: str | None = None, **stream_options: Any
+ ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
+ r"""Yield :class:`.Comment`\ s and :class:`.Submission`\ s in the modqueue as they become available.
- .. note::
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- The ``hour`` key does not contain subscribers, and therefore each sub-list
- contains three values.
+ Keyword arguments are passed to :func:`.stream_generator`.
- For example, to get the traffic stats for r/test:
+ To print all new modqueue items try:
.. code-block:: python
- stats = reddit.subreddit("test").traffic()
+ for item in reddit.subreddit("mod").mod.stream.modqueue():
+ print(item)
"""
- return self._reddit.get(API_PATH["about_traffic"].format(subreddit=self))
+ return stream_generator(
+ self.subreddit.mod.modqueue, only=only, **stream_options
+ )
- @_deprecate_args("other_subreddits")
- def unsubscribe(
- self, *, other_subreddits: list[praw.models.Subreddit] | None = None
- ):
- """Unsubscribe from the subreddit.
+ @_deprecate_args("only")
+ def reports(
+ self, *, only: str | None = None, **stream_options: Any
+ ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
+ r"""Yield reported :class:`.Comment`\ s and :class:`.Submission`\ s as they become available.
- :param other_subreddits: When provided, also unsubscribe from the provided list
- of subreddits.
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
- To unsubscribe from r/test:
+ Keyword arguments are passed to :func:`.stream_generator`.
+
+ To print new user and mod report reasons in the report queue try:
.. code-block:: python
- reddit.subreddit("test").unsubscribe()
+ for item in reddit.subreddit("mod").mod.stream.reports():
+ print(item)
"""
- data = {
- "action": "unsub",
- "sr_name": self._subreddit_list(
- other_subreddits=other_subreddits, subreddit=self
- ),
- }
- self._reddit.post(API_PATH["subscribe"], data=data)
+ return stream_generator(self.subreddit.mod.reports, only=only, **stream_options)
+
+ @_deprecate_args("only")
+ def spam(
+ self, *, only: str | None = None, **stream_options: Any
+ ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
+ r"""Yield spam :class:`.Comment`\ s and :class:`.Submission`\ s as they become available.
+ :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
+ only results of that type.
-WidgetEncoder._subreddit_class = Subreddit
+ Keyword arguments are passed to :func:`.stream_generator`.
+ To print new items in the spam queue try:
-class SubredditFilters:
- """Provide functions to interact with the special :class:`.Subreddit`'s filters.
+ .. code-block:: python
- Members of this class should be utilized via :meth:`.Subreddit.filters`. For
- example, to add a filter, run:
+ for item in reddit.subreddit("mod").mod.stream.spam():
+ print(item)
- .. code-block:: python
+ """
+ return stream_generator(self.subreddit.mod.spam, only=only, **stream_options)
- reddit.subreddit("all").filters.add("test")
+ def unmoderated(
+ self, **stream_options: Any
+ ) -> Generator[praw.models.Submission, None, None]:
+ r"""Yield unmoderated :class:`.Submission`\ s as they become available.
- """
+ Keyword arguments are passed to :func:`.stream_generator`.
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditFilters` instance.
+ To print new items in the unmoderated queue try:
- :param subreddit: The special subreddit whose filters to work with.
+ .. code-block:: python
- As of this writing filters can only be used with the special subreddits ``all``
- and ``mod``.
+ for item in reddit.subreddit("mod").mod.stream.unmoderated():
+ print(item)
"""
- self.subreddit = subreddit
+ return stream_generator(self.subreddit.mod.unmoderated, **stream_options)
- def __iter__(self) -> Generator[praw.models.Subreddit, None, None]:
- """Iterate through the special :class:`.Subreddit`'s filters.
+ def unread(
+ self, **stream_options: Any
+ ) -> Generator[praw.models.SubredditMessage, None, None]:
+ """Yield unread old modmail messages as they become available.
- This method should be invoked as:
+ Keyword arguments are passed to :func:`.stream_generator`.
+
+ .. seealso::
+
+ :meth:`.SubredditModeration.inbox` for all messages.
+
+ To print new mail in the unread modmail queue try:
.. code-block:: python
- for subreddit in reddit.subreddit("test").filters:
- ...
+ for message in reddit.subreddit("mod").mod.stream.unread():
+ print(f"From: {message.author}, To: {message.dest}")
"""
- url = API_PATH["subreddit_filter_list"].format(
- special=self.subreddit, user=self.subreddit._reddit.user.me()
- )
- params = {"unique": self.subreddit._reddit._next_unique}
- response_data = self.subreddit._reddit.get(url, params=params)
- yield from response_data.subreddits
+ return stream_generator(self.subreddit.mod.unread, **stream_options)
+
- def add(self, subreddit: praw.models.Subreddit | str):
- """Add ``subreddit`` to the list of filtered subreddits.
+class SubredditQuarantine:
+ """Provides subreddit quarantine related methods.
- :param subreddit: The subreddit to add to the filter list.
+ To opt-in into a quarantined subreddit:
- Items from subreddits added to the filtered list will no longer be included when
- obtaining listings for r/all.
+ .. code-block:: python
- Alternatively, you can filter a subreddit temporarily from a special listing in
- a manner like so:
+ reddit.subreddit("test").quaran.opt_in()
- .. code-block:: python
+ """
- reddit.subreddit("all-redditdev-learnpython")
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditQuarantine` instance.
- :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.
+ :param subreddit: The :class:`.Subreddit` associated with the quarantine.
"""
- url = API_PATH["subreddit_filter"].format(
- special=self.subreddit,
- user=self.subreddit._reddit.user.me(),
- subreddit=subreddit,
- )
- self.subreddit._reddit.put(url, data={"model": dumps({"name": str(subreddit)})})
+ self.subreddit = subreddit
- def remove(self, subreddit: praw.models.Subreddit | str):
- """Remove ``subreddit`` from the list of filtered subreddits.
+ def opt_in(self):
+ """Permit your user access to the quarantined subreddit.
- :param subreddit: The subreddit to remove from the filter list.
+ Usage:
- :raises: ``prawcore.NotFound`` when calling on a non-special subreddit.
+ .. code-block:: python
- """
- url = API_PATH["subreddit_filter"].format(
- special=self.subreddit,
- user=self.subreddit._reddit.user.me(),
- subreddit=str(subreddit),
- )
- self.subreddit._reddit.delete(url)
+ subreddit = reddit.subreddit("QUESTIONABLE")
+ next(subreddit.hot()) # Raises prawcore.Forbidden
+ subreddit.quaran.opt_in()
+ next(subreddit.hot()) # Returns Submission
-class SubredditFlair:
- """Provide a set of functions to interact with a :class:`.Subreddit`'s flair."""
+ """
+ data = {"sr_name": self.subreddit}
+ with contextlib.suppress(Redirect):
+ self.subreddit._reddit.post(API_PATH["quarantine_opt_in"], data=data)
- @cachedproperty
- def link_templates(
- self,
- ) -> praw.models.reddit.subreddit.SubredditLinkFlairTemplates:
- """Provide an instance of :class:`.SubredditLinkFlairTemplates`.
+ def opt_out(self):
+ """Remove access to the quarantined subreddit.
- Use this attribute for interacting with a :class:`.Subreddit`'s link flair
- templates. For example to list all the link flair templates for a subreddit
- which you have the ``flair`` moderator permission on try:
+ Usage:
.. code-block:: python
- for template in reddit.subreddit("test").flair.link_templates:
- print(template)
+ subreddit = reddit.subreddit("QUESTIONABLE")
+ next(subreddit.hot()) # Returns Submission
+
+ subreddit.quaran.opt_out()
+ next(subreddit.hot()) # Raises prawcore.Forbidden
"""
- return SubredditLinkFlairTemplates(self.subreddit)
+ data = {"sr_name": self.subreddit}
+ with contextlib.suppress(Redirect):
+ self.subreddit._reddit.post(API_PATH["quarantine_opt_out"], data=data)
- @cachedproperty
- def templates(
- self,
- ) -> praw.models.reddit.subreddit.SubredditRedditorFlairTemplates:
- """Provide an instance of :class:`.SubredditRedditorFlairTemplates`.
- Use this attribute for interacting with a :class:`.Subreddit`'s flair templates.
- For example to list all the flair templates for a subreddit which you have the
- ``flair`` moderator permission on try:
+class SubredditRelationship:
+ """Represents a relationship between a :class:`.Redditor` and :class:`.Subreddit`.
- .. code-block:: python
+ Instances of this class can be iterated through in order to discover the Redditors
+ that make up the relationship.
- for template in reddit.subreddit("test").flair.templates:
- print(template)
+ For example, banned users of a subreddit can be iterated through like so:
- """
- return SubredditRedditorFlairTemplates(self.subreddit)
+ .. code-block:: python
+
+ for ban in reddit.subreddit("test").banned():
+ print(f"{ban}: {ban.note}")
+
+ """
def __call__(
self,
- redditor: praw.models.Redditor | str | None = None,
+ redditor: str | praw.models.Redditor | None = None,
**generator_kwargs: Any,
) -> Iterator[praw.models.Redditor]:
- """Return a :class:`.ListingGenerator` for Redditors and their flairs.
+ r"""Return a :class:`.ListingGenerator` for :class:`.Redditor`\ s in the relationship.
:param redditor: When provided, yield at most a single :class:`.Redditor`
- instance (default: ``None``).
+ instance. This is useful to confirm if a relationship exists, or to fetch
+ the metadata associated with a particular relationship (default: ``None``).
Additional keyword arguments are passed in the initialization of
:class:`.ListingGenerator`.
- Usage:
-
- .. code-block:: python
-
- for flair in reddit.subreddit("test").flair(limit=None):
- print(flair)
-
"""
Subreddit._safely_add_arguments(
- arguments=generator_kwargs, key="params", name=redditor
+ arguments=generator_kwargs, key="params", user=redditor
)
- generator_kwargs.setdefault("limit", None)
- url = API_PATH["flairlist"].format(subreddit=self.subreddit)
+ url = API_PATH[f"list_{self.relationship}"].format(subreddit=self.subreddit)
return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditFlair` instance.
+ def __init__(self, subreddit: praw.models.Subreddit, relationship: str):
+ """Initialize a :class:`.SubredditRelationship` instance.
- :param subreddit: The subreddit whose flair to work with.
+ :param subreddit: The :class:`.Subreddit` for the relationship.
+ :param relationship: The name of the relationship.
"""
+ self.relationship = relationship
self.subreddit = subreddit
- @_deprecate_args("position", "self_assign", "link_position", "link_self_assign")
- def configure(
- self,
- *,
- link_position: str = "left",
- link_self_assign: bool = False,
- position: str = "right",
- self_assign: bool = False,
- **settings: Any,
- ):
- """Update the :class:`.Subreddit`'s flair configuration.
-
- :param link_position: One of ``"left"``, ``"right"``, or ``False`` to disable
- (default: ``"left"``).
- :param link_self_assign: Permit self assignment of link flair (default:
- ``False``).
- :param position: One of ``"left"``, ``"right"``, or ``False`` to disable
- (default: ``"right"``).
- :param self_assign: Permit self assignment of user flair (default: ``False``).
-
- .. code-block:: python
-
- subreddit = reddit.subreddit("test")
- subreddit.flair.configure(link_position="right", self_assign=True)
-
- Additional keyword arguments can be provided to handle new settings as Reddit
- introduces them.
-
- """
- data = {
- "flair_enabled": bool(position),
- "flair_position": position or "right",
- "flair_self_assign_enabled": self_assign,
- "link_flair_position": link_position or "",
- "link_flair_self_assign_enabled": link_self_assign,
- }
- data.update(settings)
- url = API_PATH["flairconfig"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
-
- def delete(self, redditor: praw.models.Redditor | str):
- """Delete flair for a :class:`.Redditor`.
+ def add(self, redditor: str | praw.models.Redditor, **other_settings: Any):
+ """Add ``redditor`` to this relationship.
:param redditor: A redditor name or :class:`.Redditor` instance.
- .. seealso::
-
- :meth:`~.SubredditFlair.update` to delete the flair of many redditors at
- once.
-
- """
- url = API_PATH["deleteflair"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data={"name": str(redditor)})
-
- def delete_all(self) -> list[dict[str, str | bool | dict[str, str]]]:
- """Delete all :class:`.Redditor` flair in the :class:`.Subreddit`.
-
- :returns: List of dictionaries indicating the success or failure of each delete.
-
"""
- return self.update(x["user"] for x in self())
+ data = {"name": str(redditor), "type": self.relationship}
+ data.update(other_settings)
+ url = API_PATH["friend"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- @_deprecate_args("redditor", "text", "css_class", "flair_template_id")
- def set( # noqa: A003
- self,
- redditor: praw.models.Redditor | str,
- *,
- css_class: str = "",
- flair_template_id: str | None = None,
- text: str = "",
- ):
- """Set flair for a :class:`.Redditor`.
+ def remove(self, redditor: str | praw.models.Redditor):
+ """Remove ``redditor`` from this relationship.
:param redditor: A redditor name or :class:`.Redditor` instance.
- :param text: The flair text to associate with the :class:`.Redditor` or
- :class:`.Submission` (default: ``""``).
- :param css_class: The css class to associate with the flair html (default:
- ``""``). Use either this or ``flair_template_id``.
- :param flair_template_id: The ID of the flair template to be used (default:
- ``None``). Use either this or ``css_class``.
-
- This method can only be used by an authenticated user who is a moderator of the
- associated :class:`.Subreddit`.
-
- For example:
-
- .. code-block:: python
-
- reddit.subreddit("test").flair.set("bboe", text="PRAW author", css_class="mods")
- template = "6bd28436-1aa7-11e9-9902-0e05ab0fad46"
- reddit.subreddit("test").flair.set(
- "spez", text="Reddit CEO", flair_template_id=template
- )
"""
- if css_class and flair_template_id is not None:
- msg = "Parameter 'css_class' cannot be used in conjunction with 'flair_template_id'."
- raise TypeError(msg)
- data = {"name": str(redditor), "text": text}
- if flair_template_id is not None:
- data["flair_template_id"] = flair_template_id
- url = API_PATH["select_flair"].format(subreddit=self.subreddit)
- else:
- data["css_class"] = css_class
- url = API_PATH["flair"].format(subreddit=self.subreddit)
+ data = {"name": str(redditor), "type": self.relationship}
+ url = API_PATH["unfriend"].format(subreddit=self.subreddit)
self.subreddit._reddit.post(url, data=data)
- @_deprecate_args("flair_list", "text", "css_class")
- def update(
- self,
- flair_list: Iterator[
- str | praw.models.Redditor | dict[str, str | praw.models.Redditor]
- ],
- *,
- text: str = "",
- css_class: str = "",
- ) -> list[dict[str, str | bool | dict[str, str]]]:
- """Set or clear the flair for many redditors at once.
- :param flair_list: Each item in this list should be either:
+class SubredditStream:
+ """Provides submission and comment streams."""
- - The name of a redditor.
- - An instance of :class:`.Redditor`.
- - A dictionary mapping keys ``"user"``, ``"flair_text"``, and
- ``"flair_css_class"`` to their respective values. The ``"user"`` key
- should map to a redditor, as described above. When a dictionary isn't
- provided, or the dictionary is missing either ``"flair_text"`` or
- ``"flair_css_class"`` keys, the default values will come from the other
- arguments.
- :param css_class: The css class to use when not explicitly provided in
- ``flair_list`` (default: ``""``).
- :param text: The flair text to use when not explicitly provided in
- ``flair_list`` (default: ``""``).
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditStream` instance.
- :returns: List of dictionaries indicating the success or failure of each update.
+ :param subreddit: The subreddit associated with the streams.
- For example, to clear the flair text, and set the ``"praw"`` flair css class on
- a few users try:
+ """
+ self.subreddit = subreddit
- .. code-block:: python
+ def comments(
+ self, **stream_options: Any
+ ) -> Generator[praw.models.Comment, None, None]:
+ """Yield new comments as they become available.
- subreddit.flair.update(["bboe", "spez", "spladug"], css_class="praw")
+ Comments are yielded oldest first. Up to 100 historical comments will initially
+ be returned.
- """
- temp_lines = StringIO()
- for item in flair_list:
- if isinstance(item, dict):
- writer(temp_lines).writerow(
- [
- str(item["user"]),
- item.get("flair_text", text),
- item.get("flair_css_class", css_class),
- ]
- )
- else:
- writer(temp_lines).writerow([str(item), text, css_class])
+ Keyword arguments are passed to :func:`.stream_generator`.
- lines = temp_lines.getvalue().splitlines()
- temp_lines.close()
- response = []
- url = API_PATH["flaircsv"].format(subreddit=self.subreddit)
- while lines:
- data = {"flair_csv": "\n".join(lines[:100])}
- response.extend(self.subreddit._reddit.post(url, data=data))
- lines = lines[100:]
- return response
+ .. note::
+ While PRAW tries to catch all new comments, some high-volume streams,
+ especially the r/all stream, may drop some comments.
-class SubredditFlairTemplates:
- """Provide functions to interact with a :class:`.Subreddit`'s flair templates."""
+ For example, to retrieve all new comments made to r/test, try:
- @staticmethod
- def flair_type(is_link: bool) -> str:
- """Return ``"LINK_FLAIR"`` or ``"USER_FLAIR"`` depending on ``is_link`` value."""
- return "LINK_FLAIR" if is_link else "USER_FLAIR"
+ .. code-block:: python
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditFlairTemplates` instance.
+ for comment in reddit.subreddit("test").stream.comments():
+ print(comment)
- :param subreddit: The subreddit whose flair templates to work with.
+ To only retrieve new submissions starting when the stream is created, pass
+ ``skip_existing=True``:
- .. note::
+ .. code-block:: python
- This class should not be initialized directly. Instead, obtain an instance
- via: ``reddit.subreddit("test").flair.templates`` or
- ``reddit.subreddit("test").flair.link_templates``.
+ subreddit = reddit.subreddit("test")
+ for comment in subreddit.stream.comments(skip_existing=True):
+ print(comment)
"""
- self.subreddit = subreddit
+ return stream_generator(self.subreddit.comments, **stream_options)
- def __iter__(self):
- """Abstract method to return flair templates."""
- raise NotImplementedError
+ def submissions(
+ self, **stream_options: Any
+ ) -> Generator[praw.models.Submission, None, None]:
+ r"""Yield new :class:`.Submission`\ s as they become available.
- def _add(
- self,
- *,
- allowable_content: str | None = None,
- background_color: str | None = None,
- css_class: str = "",
- is_link: bool | None = None,
- max_emojis: int | None = None,
- mod_only: bool | None = None,
- text: str,
- text_color: str | None = None,
- text_editable: bool = False,
- ):
- url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
- data = {
- "allowable_content": allowable_content,
- "background_color": background_color,
- "css_class": css_class,
- "flair_type": self.flair_type(is_link),
- "max_emojis": max_emojis,
- "mod_only": bool(mod_only),
- "text": text,
- "text_color": text_color,
- "text_editable": bool(text_editable),
- }
- self.subreddit._reddit.post(url, data=data)
+ Submissions are yielded oldest first. Up to 100 historical submissions will
+ initially be returned.
- def _clear(self, *, is_link: bool | None = None): # noqa: ANN001
- url = API_PATH["flairtemplateclear"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data={"flair_type": self.flair_type(is_link)})
+ Keyword arguments are passed to :func:`.stream_generator`.
- def delete(self, template_id: str):
- """Remove a flair template provided by ``template_id``.
+ .. note::
- For example, to delete the first :class:`.Redditor` flair template listed, try:
+ While PRAW tries to catch all new submissions, some high-volume streams,
+ especially the r/all stream, may drop some submissions.
+
+ For example, to retrieve all new submissions made to all of Reddit, try:
.. code-block:: python
- template_info = list(subreddit.flair.templates)[0]
- subreddit.flair.templates.delete(template_info["id"])
+ for submission in reddit.subreddit("all").stream.submissions():
+ print(submission)
"""
- url = API_PATH["flairtemplatedelete"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data={"flair_template_id": template_id})
+ return stream_generator(self.subreddit.new, **stream_options)
- def _reorder(
- self, flair_list: list, *, is_link: bool | None = None
- ): # noqa: ANN001
- url = API_PATH["flairtemplatereorder"].format(subreddit=self.subreddit)
- self.subreddit._reddit.patch(
- url,
- params={
- "flair_type": self.flair_type(is_link),
- "subreddit": self.subreddit.display_name,
- },
- json=flair_list,
- )
- @_deprecate_args(
- "template_id",
- "text",
- "css_class",
- "text_editable",
- "background_color",
- "text_color",
- "mod_only",
- "allowable_content",
- "max_emojis",
- "fetch",
- )
- def update(
- self,
- template_id: str,
- *,
- allowable_content: str | None = None,
- background_color: str | None = None,
- css_class: str | None = None,
- fetch: bool = True,
- max_emojis: int | None = None,
- mod_only: bool | None = None,
- text: str | None = None,
- text_color: str | None = None,
- text_editable: bool | None = None,
- ):
- """Update the flair template provided by ``template_id``.
+class SubredditStylesheet:
+ """Provides a set of stylesheet functions to a :class:`.Subreddit`.
- :param template_id: The flair template to update. If not valid then an exception
- will be thrown.
- :param allowable_content: If specified, most be one of ``"all"``, ``"emoji"``,
- or ``"text"`` to restrict content to that type. If set to ``"emoji"`` then
- the ``"text"`` param must be a valid emoji string, for example,
- ``":snoo:"``.
- :param background_color: The flair template's new background color, as a hex
- color.
- :param css_class: The flair template's new css_class (default: ``""``).
- :param fetch: Whether PRAW will fetch existing information on the existing flair
- before updating (default: ``True``).
- :param max_emojis: Maximum emojis in the flair (Reddit defaults this value to
- ``10``).
- :param mod_only: Indicate if the flair can only be used by moderators.
- :param text: The flair template's new text.
- :param text_color: The flair template's new text color, either ``"light"`` or
- ``"dark"``.
- :param text_editable: Indicate if the flair text can be modified for each
- redditor that sets it (default: ``False``).
+ For example, to add the css data ``.test{color:blue}`` to the existing stylesheet:
- .. warning::
+ .. code-block:: python
- If parameter ``fetch`` is set to ``False``, all parameters not provided will
- be reset to their default (``None`` or ``False``) values.
+ subreddit = reddit.subreddit("test")
+ stylesheet = subreddit.stylesheet()
+ stylesheet.stylesheet += ".test{color:blue}"
+ subreddit.stylesheet.update(stylesheet.stylesheet)
- For example, to make a user flair template text editable, try:
+ """
+
+ def __call__(self) -> praw.models.Stylesheet:
+ """Return the :class:`.Subreddit`'s stylesheet.
+
+ To be used as:
.. code-block:: python
- template_info = list(subreddit.flair.templates)[0]
- subreddit.flair.templates.update(
- template_info["id"],
- text=template_info["flair_text"],
- text_editable=True,
- )
+ stylesheet = reddit.subreddit("test").stylesheet()
"""
- url = API_PATH["flairtemplate_v2"].format(subreddit=self.subreddit)
- data = {
- "allowable_content": allowable_content,
- "background_color": background_color,
- "css_class": css_class,
- "flair_template_id": template_id,
- "max_emojis": max_emojis,
- "mod_only": mod_only,
- "text": text,
- "text_color": text_color,
- "text_editable": text_editable,
- }
- if fetch:
- _existing_data = [
- template for template in iter(self) if template["id"] == template_id
- ]
- if len(_existing_data) != 1:
- raise InvalidFlairTemplateID(template_id)
- existing_data = _existing_data[0]
- for key, value in existing_data.items():
- if data.get(key) is None:
- data[key] = value
- self.subreddit._reddit.post(url, data=data)
+ url = API_PATH["about_stylesheet"].format(subreddit=self.subreddit)
+ return self.subreddit._reddit.get(url)
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditStylesheet` instance.
-class SubredditModeration:
- """Provides a set of moderation functions to a :class:`.Subreddit`.
+ :param subreddit: The :class:`.Subreddit` associated with the stylesheet.
- For example, to accept a moderation invite from r/test:
+ An instance of this class is provided as:
- .. code-block:: python
+ .. code-block:: python
+
+ reddit.subreddit("test").stylesheet
+
+ """
+ self.subreddit = subreddit
+
+ def _update_structured_styles(self, style_data: dict[str, str | Any]):
+ url = API_PATH["structured_styles"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.patch(url, data=style_data)
+
+ def _upload_image(
+ self, *, data: dict[str, str | Any], image_path: str
+ ) -> dict[str, Any]:
+ with Path(image_path).open("rb") as image:
+ header = image.read(len(JPEG_HEADER))
+ image.seek(0)
+ data["img_type"] = "jpg" if header == JPEG_HEADER else "png"
+ url = API_PATH["upload_image"].format(subreddit=self.subreddit)
+ response = self.subreddit._reddit.post(
+ url, data=data, files={"file": image}
+ )
+ if response["errors"]:
+ error_type = response["errors"][0]
+ error_value = response.get("errors_values", [""])[0]
+ assert error_type in [
+ "BAD_CSS_NAME",
+ "IMAGE_ERROR",
+ ], "Please file a bug with PRAW."
+ raise RedditAPIException([[error_type, error_value, None]])
+ return response
- reddit.subreddit("test").mod.accept_invite()
+ def _upload_style_asset(self, *, image_path: str, image_type: str) -> str:
+ file = Path(image_path)
+ data = {"imagetype": image_type, "filepath": file.name}
+ data["mimetype"] = "image/jpeg"
+ if image_path.lower().endswith(".png"):
+ data["mimetype"] = "image/png"
+ url = API_PATH["style_asset_lease"].format(subreddit=self.subreddit)
- """
+ upload_lease = self.subreddit._reddit.post(url, data=data)["s3UploadLease"]
+ upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
+ upload_url = f"https:{upload_lease['action']}"
- @staticmethod
- def _handle_only(
- *, generator_kwargs: dict[str, Any], only: str | None
- ): # noqa: ANN001
- if only is not None:
- if only == "submissions":
- only = "links"
- RedditBase._safely_add_arguments(
- arguments=generator_kwargs, key="params", only=only
+ with file.open("rb") as image:
+ response = self.subreddit._reddit._core._requestor._http.post(
+ upload_url, data=upload_data, files={"file": image}
)
+ response.raise_for_status()
- @cachedproperty
- def notes(self) -> praw.models.SubredditModNotes:
- """Provide an instance of :class:`.SubredditModNotes`.
+ return f"{upload_url}/{upload_data['key']}"
- This provides an interface for managing moderator notes for this subreddit.
+ def delete_banner(self):
+ """Remove the current :class:`.Subreddit` (redesign) banner image.
- For example, all the notes for u/spez in r/test can be iterated through like so:
+ Succeeds even if there is no banner image.
- .. code-block:: python
+ For example:
- subreddit = reddit.subreddit("test")
+ .. code-block:: python
- for note in subreddit.mod.notes.redditors("spez"):
- print(f"{note.label}: {note.note}")
+ reddit.subreddit("test").stylesheet.delete_banner()
"""
- from praw.models.mod_notes import SubredditModNotes
+ data = {"bannerBackgroundImage": ""}
+ self._update_structured_styles(data)
- return SubredditModNotes(self.subreddit._reddit, subreddit=self.subreddit)
+ def delete_banner_additional_image(self):
+ """Remove the current :class:`.Subreddit` (redesign) banner additional image.
- @cachedproperty
- def removal_reasons(self) -> SubredditRemovalReasons:
- """Provide an instance of :class:`.SubredditRemovalReasons`.
+ Succeeds even if there is no additional image. Will also delete any configured
+ hover image.
- Use this attribute for interacting with a :class:`.Subreddit`'s removal reasons.
- For example to list all the removal reasons for a subreddit which you have the
- ``posts`` moderator permission on, try:
+ For example:
.. code-block:: python
- for removal_reason in reddit.subreddit("test").mod.removal_reasons:
- print(removal_reason)
+ reddit.subreddit("test").stylesheet.delete_banner_additional_image()
- A single removal reason can be lazily retrieved via:
+ """
+ data = {"bannerPositionedImage": "", "secondaryBannerPositionedImage": ""}
+ self._update_structured_styles(data)
- .. code-block:: python
+ def delete_banner_hover_image(self):
+ """Remove the current :class:`.Subreddit` (redesign) banner hover image.
- reddit.subreddit("test").mod.removal_reasons["reason_id"]
+ Succeeds even if there is no hover image.
- .. note::
+ For example:
- Attempting to access attributes of an nonexistent removal reason will result
- in a :class:`.ClientException`.
+ .. code-block:: python
+
+ reddit.subreddit("test").stylesheet.delete_banner_hover_image()
"""
- return SubredditRemovalReasons(self.subreddit)
+ data = {"secondaryBannerPositionedImage": ""}
+ self._update_structured_styles(data)
- @cachedproperty
- def stream(self) -> praw.models.reddit.subreddit.SubredditModerationStream:
- """Provide an instance of :class:`.SubredditModerationStream`.
+ def delete_header(self):
+ """Remove the current :class:`.Subreddit` header image.
- Streams can be used to indefinitely retrieve Moderator only items from
- :class:`.SubredditModeration` made to moderated subreddits, like:
+ Succeeds even if there is no header image.
+
+ For example:
.. code-block:: python
- for log in reddit.subreddit("mod").mod.stream.log():
- print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
+ reddit.subreddit("test").stylesheet.delete_header()
"""
- return SubredditModerationStream(self.subreddit)
+ url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url)
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditModeration` instance.
+ def delete_image(self, name: str):
+ """Remove the named image from the :class:`.Subreddit`.
- :param subreddit: The subreddit to moderate.
+ Succeeds even if the named image does not exist.
- """
- self.subreddit = subreddit
- self._stream = None
+ For example:
- def accept_invite(self):
- """Accept an invitation as a moderator of the community."""
- url = API_PATH["accept_mod_invite"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url)
+ .. code-block:: python
- @_deprecate_args("only")
- def edited(
- self, *, only: str | None = None, **generator_kwargs: Any
- ) -> Iterator[praw.models.Comment | praw.models.Submission]:
- """Return a :class:`.ListingGenerator` for edited comments and submissions.
+ reddit.subreddit("test").stylesheet.delete_image("smile")
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ """
+ url = API_PATH["delete_sr_image"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data={"img_name": name})
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ def delete_mobile_banner(self):
+ """Remove the current :class:`.Subreddit` (redesign) mobile banner.
- To print all items in the edited queue try:
+ Succeeds even if there is no mobile banner.
+
+ For example:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.edited(limit=None):
- print(item)
+ subreddit = reddit.subreddit("test")
+ subreddit.stylesheet.delete_banner_hover_image()
"""
- self._handle_only(generator_kwargs=generator_kwargs, only=only)
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_edited"].format(subreddit=self.subreddit),
- **generator_kwargs,
- )
+ data = {"mobileBannerImage": ""}
+ self._update_structured_styles(data)
- def inbox(self, **generator_kwargs: Any) -> Iterator[praw.models.SubredditMessage]:
- """Return a :class:`.ListingGenerator` for moderator messages.
+ def delete_mobile_header(self):
+ """Remove the current :class:`.Subreddit` mobile header.
- .. warning::
+ Succeeds even if there is no mobile header.
- Legacy modmail is being deprecated in June 2021. Please see
- https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/
- for more info.
+ For example:
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ .. code-block:: python
- .. seealso::
+ reddit.subreddit("test").stylesheet.delete_mobile_header()
- :meth:`.unread` for unread moderator messages.
+ """
+ url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url)
- To print the last 5 moderator mail messages and their replies, try:
+ def delete_mobile_icon(self):
+ """Remove the current :class:`.Subreddit` mobile icon.
+
+ Succeeds even if there is no mobile icon.
+
+ For example:
.. code-block:: python
- for message in reddit.subreddit("mod").mod.inbox(limit=5):
- print(f"From: {message.author}, Body: {message.body}")
- for reply in message.replies:
- print(f"From: {reply.author}, Body: {reply.body}")
+ reddit.subreddit("test").stylesheet.delete_mobile_icon()
"""
- warn(
- "Legacy modmail is being deprecated in June 2021. Please see"
- " https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/"
- " for more info.",
- category=DeprecationWarning,
- stacklevel=3,
- )
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["moderator_messages"].format(subreddit=self.subreddit),
- **generator_kwargs,
- )
-
- @_deprecate_args("action", "mod")
- def log(
- self,
- *,
- action: str | None = None,
- mod: praw.models.Redditor | str | None = None,
- **generator_kwargs: Any,
- ) -> Iterator[praw.models.ModAction]:
- """Return a :class:`.ListingGenerator` for moderator log entries.
+ url = API_PATH["delete_sr_icon"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url)
- :param action: If given, only return log entries for the specified action.
- :param mod: If given, only return log entries for actions made by the passed in
- redditor.
+ @_deprecate_args("stylesheet", "reason")
+ def update(self, stylesheet: str, *, reason: str | None = None):
+ """Update the :class:`.Subreddit`'s stylesheet.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :param stylesheet: The CSS for the new stylesheet.
+ :param reason: The reason for updating the stylesheet.
- To print the moderator and subreddit of the last 5 modlog entries try:
+ For example:
.. code-block:: python
- for log in reddit.subreddit("mod").mod.log(limit=5):
- print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
+ reddit.subreddit("test").stylesheet.update(
+ "p { color: green; }", reason="color text green"
+ )
"""
- params = {"mod": str(mod) if mod else mod, "type": action}
- Subreddit._safely_add_arguments(
- arguments=generator_kwargs, key="params", **params
- )
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_log"].format(subreddit=self.subreddit),
- **generator_kwargs,
- )
+ data = {"op": "save", "reason": reason, "stylesheet_contents": stylesheet}
+ url = API_PATH["subreddit_stylesheet"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- @_deprecate_args("only")
- def modqueue(
- self, *, only: str | None = None, **generator_kwargs: Any
- ) -> Iterator[praw.models.Submission | praw.models.Comment]:
- """Return a :class:`.ListingGenerator` for modqueue items.
+ @_deprecate_args("name", "image_path")
+ def upload(self, *, image_path: str, name: str) -> dict[str, str]:
+ """Upload an image to the :class:`.Subreddit`.
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ :param image_path: A path to a jpeg or png image.
+ :param name: The name to use for the image. If an image already exists with the
+ same name, it will be replaced.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :returns: A dictionary containing a link to the uploaded image under the key
+ ``img_src``.
- To print all modqueue items try:
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
+
+ For example:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.modqueue(limit=None):
- print(item)
+ reddit.subreddit("test").stylesheet.upload(name="smile", image_path="img.png")
"""
- self._handle_only(generator_kwargs=generator_kwargs, only=only)
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_modqueue"].format(subreddit=self.subreddit),
- **generator_kwargs,
+ return self._upload_image(
+ data={"name": name, "upload_type": "img"}, image_path=image_path
)
- @_deprecate_args("only")
- def reports(
- self, *, only: str | None = None, **generator_kwargs: Any
- ) -> Iterator[praw.models.Submission | praw.models.Comment]:
- """Return a :class:`.ListingGenerator` for reported comments and submissions.
+ def upload_banner(self, image_path: str):
+ """Upload an image for the :class:`.Subreddit`'s (redesign) banner image.
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ :param image_path: A path to a jpeg or png image.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
- To print the user and mod report reasons in the report queue try:
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
+
+ For example:
.. code-block:: python
- for reported_item in reddit.subreddit("mod").mod.reports():
- print(f"User Reports: {reported_item.user_reports}")
- print(f"Mod Reports: {reported_item.mod_reports}")
+ reddit.subreddit("test").stylesheet.upload_banner("banner.png")
"""
- self._handle_only(generator_kwargs=generator_kwargs, only=only)
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_reports"].format(subreddit=self.subreddit),
- **generator_kwargs,
+ image_type = "bannerBackgroundImage"
+ image_url = self._upload_style_asset(
+ image_path=image_path, image_type=image_type
)
+ self._update_structured_styles({image_type: image_url})
- def settings(self) -> dict[str, str | int | bool]:
- """Return a dictionary of the :class:`.Subreddit`'s current settings."""
- url = API_PATH["subreddit_settings"].format(subreddit=self.subreddit)
- return self.subreddit._reddit.get(url)["data"]
+ @_deprecate_args("image_path", "align")
+ def upload_banner_additional_image(
+ self,
+ image_path: str,
+ *,
+ align: str | None = None,
+ ):
+ """Upload an image for the :class:`.Subreddit`'s (redesign) additional image.
- @_deprecate_args("only")
- def spam(
- self, *, only: str | None = None, **generator_kwargs: Any
- ) -> Iterator[praw.models.Submission | praw.models.Comment]:
- """Return a :class:`.ListingGenerator` for spam comments and submissions.
+ :param image_path: A path to a jpeg or png image.
+ :param align: Either ``"left"``, ``"centered"``, or ``"right"``. (default:
+ ``"left"``).
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
- To print the items in the spam queue try:
+ For example:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.spam():
- print(item)
+ subreddit = reddit.subreddit("test")
+ subreddit.stylesheet.upload_banner_additional_image("banner.png")
"""
- self._handle_only(generator_kwargs=generator_kwargs, only=only)
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_spam"].format(subreddit=self.subreddit),
- **generator_kwargs,
+ alignment = {}
+ if align is not None:
+ if align not in {"left", "centered", "right"}:
+ msg = "'align' argument must be either 'left', 'centered', or 'right'"
+ raise ValueError(msg)
+ alignment["bannerPositionedImagePosition"] = align
+
+ image_type = "bannerPositionedImage"
+ image_url = self._upload_style_asset(
+ image_path=image_path, image_type=image_type
)
+ style_data = {image_type: image_url}
+ if alignment:
+ style_data.update(alignment)
+ self._update_structured_styles(style_data)
- def unmoderated(self, **generator_kwargs: Any) -> Iterator[praw.models.Submission]:
- """Return a :class:`.ListingGenerator` for unmoderated submissions.
+ def upload_banner_hover_image(self, image_path: str):
+ """Upload an image for the :class:`.Subreddit`'s (redesign) additional image.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :param image_path: A path to a jpeg or png image.
- To print the items in the unmoderated queue try:
+ Fails if the :class:`.Subreddit` does not have an additional image defined.
+
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
+
+ For example:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.unmoderated():
- print(item)
+ subreddit = reddit.subreddit("test")
+ subreddit.stylesheet.upload_banner_hover_image("banner.png")
"""
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["about_unmoderated"].format(subreddit=self.subreddit),
- **generator_kwargs,
+ image_type = "secondaryBannerPositionedImage"
+ image_url = self._upload_style_asset(
+ image_path=image_path, image_type=image_type
)
+ self._update_structured_styles({image_type: image_url})
- def unread(self, **generator_kwargs: Any) -> Iterator[praw.models.SubredditMessage]:
- """Return a :class:`.ListingGenerator` for unread moderator messages.
-
- .. warning::
+ def upload_header(self, image_path: str) -> dict[str, str]:
+ """Upload an image to be used as the :class:`.Subreddit`'s header image.
- Legacy modmail is being deprecated in June 2021. Please see
- https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/
- for more info.
+ :param image_path: A path to a jpeg or png image.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :returns: A dictionary containing a link to the uploaded image under the key
+ ``img_src``.
- .. seealso::
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
- :meth:`.inbox` for all messages.
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
- To print the mail in the unread modmail queue try:
+ For example:
.. code-block:: python
- for message in reddit.subreddit("mod").mod.unread():
- print(f"From: {message.author}, To: {message.dest}")
+ reddit.subreddit("test").stylesheet.upload_header("header.png")
"""
- warn(
- "Legacy modmail is being deprecated in June 2021. Please see"
- " https://www.reddit.com/r/modnews/comments/mar9ha/even_more_modmail_improvements/"
- " for more info.",
- category=DeprecationWarning,
- stacklevel=3,
- )
- return ListingGenerator(
- self.subreddit._reddit,
- API_PATH["moderator_unread"].format(subreddit=self.subreddit),
- **generator_kwargs,
- )
+ return self._upload_image(data={"upload_type": "header"}, image_path=image_path)
- def update(self, **settings: str | int | bool) -> dict[str, str | int | bool]:
- """Update the :class:`.Subreddit`'s settings.
+ def upload_mobile_banner(self, image_path: str):
+ """Upload an image for the :class:`.Subreddit`'s (redesign) mobile banner.
- See https://www.reddit.com/dev/api#POST_api_site_admin for the full list.
+ :param image_path: A path to a JPEG or PNG image.
- :param all_original_content: Mandate all submissions to be original content
- only.
- :param allow_chat_post_creation: Allow users to create chat submissions.
- :param allow_images: Allow users to upload images using the native image
- hosting.
- :param allow_polls: Allow users to post polls to the subreddit.
- :param allow_post_crossposts: Allow users to crosspost submissions from other
- subreddits.
- :param allow_videos: Allow users to upload videos using the native image
- hosting.
- :param collapse_deleted_comments: Collapse deleted and removed comments on
- comments pages by default.
- :param comment_score_hide_mins: The number of minutes to hide comment scores.
- :param content_options: The types of submissions users can make. One of
- ``"any"``, ``"link"``, or ``"self"``.
- :param crowd_control_chat_level: Controls the crowd control level for chat
- rooms. Goes from 0-3.
- :param crowd_control_level: Controls the crowd control level for submissions.
- Goes from 0-3.
- :param crowd_control_mode: Enables/disables crowd control.
- :param default_set: Allow the subreddit to appear on r/all as well as the
- default and trending lists.
- :param disable_contributor_requests: Specifies whether redditors may send
- automated modmail messages requesting approval as a submitter.
- :param exclude_banned_modqueue: Exclude posts by site-wide banned users from
- modqueue/unmoderated.
- :param free_form_reports: Allow users to specify custom reasons in the report
- menu.
- :param header_hover_text: The text seen when hovering over the snoo.
- :param hide_ads: Don't show ads within this subreddit. Only applies to
- Premium-user only subreddits.
- :param key_color: A 6-digit rgb hex color (e.g., ``"#AABBCC"``), used as a
- thematic color for your subreddit on mobile.
- :param language: A valid IETF language tag (underscore separated).
- :param original_content_tag_enabled: Enables the use of the ``original content``
- label for submissions.
- :param over_18: Viewers must be over 18 years old (i.e., NSFW).
- :param public_description: Public description blurb. Appears in search results
- and on the landing page for private subreddits.
- :param restrict_commenting: Specifies whether approved users have the ability to
- comment.
- :param restrict_posting: Specifies whether approved users have the ability to
- submit posts.
- :param show_media: Show thumbnails on submissions.
- :param show_media_preview: Expand media previews on comments pages.
- :param spam_comments: Spam filter strength for comments. One of ``"all"``,
- ``"low"``, or ``"high"``.
- :param spam_links: Spam filter strength for links. One of ``"all"``, ``"low"``,
- or ``"high"``.
- :param spam_selfposts: Spam filter strength for selfposts. One of ``"all"``,
- ``"low"``, or ``"high"``.
- :param spoilers_enabled: Enable marking posts as containing spoilers.
- :param submit_link_label: Custom label for submit link button (``None`` for
- default).
- :param submit_text: Text to show on submission page.
- :param submit_text_label: Custom label for submit text post button (``None`` for
- default).
- :param subreddit_type: One of ``"archived"``, ``"employees_only"``,
- ``"gold_only"``, ``gold_restricted``, ``"private"``, ``"public"``, or
- ``"restricted"``.
- :param suggested_comment_sort: All comment threads will use this sorting method
- by default. Leave ``None``, or choose one of ``"confidence"``,
- ``"controversial"``, ``"live"``, ``"new"``, ``"old"``, ``"qa"``,
- ``"random"``, or ``"top"``.
- :param title: The title of the subreddit.
- :param welcome_message_enabled: Enables the subreddit welcome message.
- :param welcome_message_text: The text to be used as a welcome message. A welcome
- message is sent to all new subscribers by a Reddit bot.
- :param wiki_edit_age: Account age, in days, required to edit and create wiki
- pages.
- :param wiki_edit_karma: Subreddit karma required to edit and create wiki pages.
- :param wikimode: One of ``"anyone"``, ``"disabled"``, or ``"modonly"``.
+ For example:
+
+ .. code-block:: python
+
+ subreddit = reddit.subreddit("test")
+ subreddit.stylesheet.upload_mobile_banner("banner.png")
+
+ Fails if the :class:`.Subreddit` does not have an additional image defined.
+
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
- .. note::
+ """
+ image_type = "mobileBannerImage"
+ image_url = self._upload_style_asset(
+ image_path=image_path, image_type=image_type
+ )
+ self._update_structured_styles({image_type: image_url})
- Updating the subreddit sidebar on old reddit (``description``) is no longer
- supported using this method. You can update the sidebar by editing the
- ``"config/sidebar"`` wiki page. For example:
+ def upload_mobile_header(self, image_path: str) -> dict[str, str]:
+ """Upload an image to be used as the :class:`.Subreddit`'s mobile header.
- .. code-block:: python
+ :param image_path: A path to a jpeg or png image.
- sidebar = reddit.subreddit("test").wiki["config/sidebar"]
- sidebar.edit(content="new sidebar content")
+ :returns: A dictionary containing a link to the uploaded image under the key
+ ``img_src``.
- Additional keyword arguments can be provided to handle new settings as Reddit
- introduces them.
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
- Settings that are documented here and aren't explicitly set by you in a call to
- :meth:`.SubredditModeration.update` should retain their current value. If they
- do not, please file a bug.
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
+
+ For example:
+
+ .. code-block:: python
+
+ reddit.subreddit("test").stylesheet.upload_mobile_header("header.png")
"""
- # These attributes come out using different names than they go in.
- remap = {
- "content_options": "link_type",
- "default_set": "allow_top",
- "header_hover_text": "header_title",
- "language": "lang",
- "subreddit_type": "type",
- }
- settings = {remap.get(key, key): value for key, value in settings.items()}
- settings["sr"] = self.subreddit.fullname
- return self.subreddit._reddit.patch(API_PATH["update_settings"], json=settings)
+ return self._upload_image(data={"upload_type": "banner"}, image_path=image_path)
+
+ def upload_mobile_icon(self, image_path: str) -> dict[str, str]:
+ """Upload an image to be used as the :class:`.Subreddit`'s mobile icon.
+ :param image_path: A path to a jpeg or png image.
-class SubredditModerationStream:
- """Provides moderator streams."""
+ :returns: A dictionary containing a link to the uploaded image under the key
+ ``img_src``.
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditModerationStream` instance.
+ :raises: ``prawcore.TooLarge`` if the overall request body is too large.
- :param subreddit: The moderated subreddit associated with the streams.
+ :raises: :class:`.RedditAPIException` if there are other issues with the
+ uploaded image. Unfortunately the exception info might not be very specific,
+ so try through the website with the same image to see what the problem
+ actually might be.
+
+ For example:
+
+ .. code-block:: python
+
+ reddit.subreddit("test").stylesheet.upload_mobile_icon("icon.png")
"""
- self.subreddit = subreddit
+ return self._upload_image(data={"upload_type": "icon"}, image_path=image_path)
- @_deprecate_args("only")
- def edited(
- self, *, only: str | None = None, **stream_options: Any
- ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
- """Yield edited comments and submissions as they become available.
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+class SubredditWiki:
+ """Provides a set of wiki functions to a :class:`.Subreddit`."""
- Keyword arguments are passed to :func:`.stream_generator`.
+ def __getitem__(self, page_name: str) -> WikiPage:
+ """Lazily return the :class:`.WikiPage` for the :class:`.Subreddit` named ``page_name``.
- For example, to retrieve all new edited submissions/comments made to all
- moderated subreddits, try:
+ This method is to be used to fetch a specific wikipage, like so:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.stream.edited():
- print(item)
+ wikipage = reddit.subreddit("test").wiki["proof"]
+ print(wikipage.content_md)
"""
- return stream_generator(self.subreddit.mod.edited, only=only, **stream_options)
+ return WikiPage(self.subreddit._reddit, self.subreddit, page_name.lower())
- @_deprecate_args("action", "mod")
- def log(
+ def __init__(self, subreddit: praw.models.Subreddit):
+ """Initialize a :class:`.SubredditWiki` instance.
+
+ :param subreddit: The subreddit whose wiki to work with.
+
+ """
+ self.banned = SubredditRelationship(subreddit, "wikibanned")
+ self.contributor = SubredditRelationship(subreddit, "wikicontributor")
+ self.subreddit = subreddit
+
+ def __iter__(self) -> Generator[WikiPage, None, None]:
+ """Iterate through the pages of the wiki.
+
+ This method is to be used to discover all wikipages for a subreddit:
+
+ .. code-block:: python
+
+ for wikipage in reddit.subreddit("test").wiki:
+ print(wikipage)
+
+ """
+ response = self.subreddit._reddit.get(
+ API_PATH["wiki_pages"].format(subreddit=self.subreddit),
+ params={"unique": self.subreddit._reddit._next_unique},
+ )
+ for page_name in response["data"]:
+ yield WikiPage(self.subreddit._reddit, self.subreddit, page_name)
+
+ @_deprecate_args("name", "content", "reason")
+ def create(
self,
*,
- action: str | None = None,
- mod: str | praw.models.Redditor | None = None,
- **stream_options: Any,
- ) -> Generator[praw.models.ModAction, None, None]:
- """Yield moderator log entries as they become available.
+ content: str,
+ name: str,
+ reason: str | None = None,
+ **other_settings: Any,
+ ) -> WikiPage:
+ """Create a new :class:`.WikiPage`.
- :param action: If given, only return log entries for the specified action.
- :param mod: If given, only return log entries for actions made by the passed in
- redditor.
+ :param name: The name of the new :class:`.WikiPage`. This name will be
+ normalized.
+ :param content: The content of the new :class:`.WikiPage`.
+ :param reason: The reason for the creation.
+ :param other_settings: Additional keyword arguments to pass.
- For example, to retrieve all new mod actions made to all moderated subreddits,
- try:
+ To create the wiki page ``"praw_test"`` in r/test try:
.. code-block:: python
- for log in reddit.subreddit("mod").mod.stream.log():
- print(f"Mod: {log.mod}, Subreddit: {log.subreddit}")
+ reddit.subreddit("test").wiki.create(
+ name="praw_test", content="wiki body text", reason="PRAW Test Creation"
+ )
"""
- return stream_generator(
- self.subreddit.mod.log,
- attribute_name="id",
- action=action,
- mod=mod,
- **stream_options,
+ name = name.replace(" ", "_").lower()
+ new = WikiPage(self.subreddit._reddit, self.subreddit, name)
+ new.edit(content=content, reason=reason, **other_settings)
+ return new
+
+ def revisions(
+ self, **generator_kwargs: Any
+ ) -> Generator[
+ dict[str, praw.models.Redditor | WikiPage | str | int | bool | None],
+ None,
+ None,
+ ]:
+ """Return a :class:`.ListingGenerator` for recent wiki revisions.
+
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
+
+ To view the wiki revisions for ``"praw_test"`` in r/test try:
+
+ .. code-block:: python
+
+ for item in reddit.subreddit("test").wiki["praw_test"].revisions():
+ print(item)
+
+ """
+ url = API_PATH["wiki_revisions"].format(subreddit=self.subreddit)
+ return WikiPage._revision_generator(
+ generator_kwargs=generator_kwargs, subreddit=self.subreddit, url=url
)
- @_deprecate_args("other_subreddits", "sort", "state")
- def modmail_conversations(
- self,
+
+class ContributorRelationship(SubredditRelationship):
+ r"""Provides methods to interact with a :class:`.Subreddit`'s contributors.
+
+ Contributors are also known as approved submitters.
+
+ Contributors of a subreddit can be iterated through like so:
+
+ .. code-block:: python
+
+ for contributor in reddit.subreddit("test").contributor():
+ print(contributor)
+
+ """
+
+ def leave(self):
+ """Abdicate the contributor position."""
+ self.subreddit._reddit.post(
+ API_PATH["leavecontributor"], data={"id": self.subreddit.fullname}
+ )
+
+
+class ModeratorRelationship(SubredditRelationship):
+ r"""Provides methods to interact with a :class:`.Subreddit`'s moderators.
+
+ Moderators of a subreddit can be iterated through like so:
+
+ .. code-block:: python
+
+ for moderator in reddit.subreddit("test").moderator():
+ print(moderator)
+
+ """
+
+ PERMISSIONS = {
+ "access",
+ "chat_config",
+ "chat_operator",
+ "config",
+ "flair",
+ "mail",
+ "posts",
+ "wiki",
+ }
+
+ @staticmethod
+ def _handle_permissions(
*,
- other_subreddits: list[praw.models.Subreddit] | None = None,
- sort: str | None = None,
- state: str | None = None,
- **stream_options: Any,
- ) -> Generator[ModmailConversation, None, None]:
- """Yield new-modmail conversations as they become available.
+ other_settings: dict | None = None,
+ permissions: list[str] | None = None,
+ ) -> dict[str, Any]:
+ other_settings = deepcopy(other_settings) if other_settings else {}
+ other_settings["permissions"] = permissions_string(
+ known_permissions=ModeratorRelationship.PERMISSIONS, permissions=permissions
+ )
+ return other_settings
- :param other_subreddits: A list of :class:`.Subreddit` instances for which to
- fetch conversations (default: ``None``).
- :param sort: Can be one of: ``"mod"``, ``"recent"``, ``"unread"``, or ``"user"``
- (default: ``"recent"``).
- :param state: Can be one of: ``"all"``, ``"appeals"``, ``"archived"``,
- ``"default"``, ``"highlighted"``, ``"inbox"``, ``"inprogress"``,
- ``"join_requests"``, ``"mod"``, ``"new"``, or ``"notifications"`` (default:
- ``"all"``). ``"all"`` does not include mod or archived conversations.
- ``"inbox"`` does not include appeals conversations.
+ def __call__(
+ self, redditor: str | praw.models.Redditor | None = None
+ ) -> list[praw.models.Redditor]:
+ r"""Return a list of :class:`.Redditor`\ s who are moderators.
- Keyword arguments are passed to :func:`.stream_generator`.
+ :param redditor: When provided, return a list containing at most one
+ :class:`.Redditor` instance. This is useful to confirm if a relationship
+ exists, or to fetch the metadata associated with a particular relationship
+ (default: ``None``).
- To print new mail in the unread modmail queue try:
+ .. note::
- .. code-block:: python
+ To help mitigate targeted moderator harassment, this call requires the
+ :class:`.Reddit` instance to be authenticated i.e., :attr:`.read_only` must
+ return ``False``. This call, however, only makes use of the ``read`` scope.
+ For more information on why the moderator list is hidden can be found here:
+ https://reddit.zendesk.com/hc/en-us/articles/360049499032-Why-is-the-moderator-list-hidden-
- subreddit = reddit.subreddit("all")
- for message in subreddit.mod.stream.modmail_conversations():
- print(f"From: {message.owner}, To: {message.participant}")
+ .. note::
- """ # noqa: E501
- if self.subreddit == "mod":
- self.subreddit = self.subreddit._reddit.subreddit("all")
- return stream_generator(
- self.subreddit.modmail.conversations,
- attribute_name="id",
- exclude_before=True,
- other_subreddits=other_subreddits,
- sort=sort,
- state=state,
- **stream_options,
- )
+ Unlike other relationship callables, this relationship is not paginated.
+ Thus, it simply returns the full list, rather than an iterator for the
+ results.
- @_deprecate_args("only")
- def modqueue(
- self, *, only: str | None = None, **stream_options: Any
- ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
- r"""Yield :class:`.Comment`\ s and :class:`.Submission`\ s in the modqueue as they become available.
+ To be used like:
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ .. code-block:: python
- Keyword arguments are passed to :func:`.stream_generator`.
+ moderators = reddit.subreddit("test").moderator()
- To print all new modqueue items try:
+ For example, to list the moderators along with their permissions try:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.stream.modqueue():
- print(item)
+ for moderator in reddit.subreddit("test").moderator():
+ print(f"{moderator}: {moderator.mod_permissions}")
"""
- return stream_generator(
- self.subreddit.mod.modqueue, only=only, **stream_options
- )
+ params = {} if redditor is None else {"user": redditor}
+ url = API_PATH[f"list_{self.relationship}"].format(subreddit=self.subreddit)
+ return self.subreddit._reddit.get(url, params=params)
- @_deprecate_args("only")
- def reports(
- self, *, only: str | None = None, **stream_options: Any
- ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
- r"""Yield reported :class:`.Comment`\ s and :class:`.Submission`\ s as they become available.
+ @_deprecate_args("redditor", "permissions")
+ def add(
+ self,
+ redditor: str | praw.models.Redditor,
+ *,
+ permissions: list[str] | None = None,
+ **other_settings: Any,
+ ):
+ """Add or invite ``redditor`` to be a moderator of the :class:`.Subreddit`.
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param permissions: When provided (not ``None``), permissions should be a list
+ of strings specifying which subset of permissions to grant. An empty list
+ ``[]`` indicates no permissions, and when not provided ``None``, indicates
+ full permissions (default: ``None``).
- Keyword arguments are passed to :func:`.stream_generator`.
+ An invite will be sent unless the user making this call is an admin user.
- To print new user and mod report reasons in the report queue try:
+ For example, to invite u/spez with ``"posts"`` and ``"mail"`` permissions to
+ r/test, try:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.stream.reports():
- print(item)
+ reddit.subreddit("test").moderator.add("spez", permissions=["posts", "mail"])
"""
- return stream_generator(self.subreddit.mod.reports, only=only, **stream_options)
-
- @_deprecate_args("only")
- def spam(
- self, *, only: str | None = None, **stream_options: Any
- ) -> Generator[praw.models.Comment | praw.models.Submission, None, None]:
- r"""Yield spam :class:`.Comment`\ s and :class:`.Submission`\ s as they become available.
+ other_settings = self._handle_permissions(
+ other_settings=other_settings, permissions=permissions
+ )
+ super().add(redditor, **other_settings)
- :param only: If specified, one of ``"comments"`` or ``"submissions"`` to yield
- only results of that type.
+ @_deprecate_args("redditor", "permissions")
+ def invite(
+ self,
+ redditor: str | praw.models.Redditor,
+ *,
+ permissions: list[str] | None = None,
+ **other_settings: Any,
+ ):
+ """Invite ``redditor`` to be a moderator of the :class:`.Subreddit`.
- Keyword arguments are passed to :func:`.stream_generator`.
+ :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param permissions: When provided (not ``None``), permissions should be a list
+ of strings specifying which subset of permissions to grant. An empty list
+ ``[]`` indicates no permissions, and when not provided ``None``, indicates
+ full permissions (default: ``None``).
- To print new items in the spam queue try:
+ For example, to invite u/spez with ``"posts"`` and ``"mail"`` permissions to
+ r/test, try:
.. code-block:: python
- for item in reddit.subreddit("mod").mod.stream.spam():
- print(item)
+ reddit.subreddit("test").moderator.invite("spez", permissions=["posts", "mail"])
"""
- return stream_generator(self.subreddit.mod.spam, only=only, **stream_options)
+ data = self._handle_permissions(
+ other_settings=other_settings, permissions=permissions
+ )
+ data.update({"name": str(redditor), "type": "moderator_invite"})
+ url = API_PATH["friend"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- def unmoderated(
- self, **stream_options: Any
- ) -> Generator[praw.models.Submission, None, None]:
- r"""Yield unmoderated :class:`.Submission`\ s as they become available.
+ @_deprecate_args("redditor")
+ def invited(
+ self,
+ *,
+ redditor: str | praw.models.Redditor | None = None,
+ **generator_kwargs: Any,
+ ) -> Iterator[praw.models.Redditor]:
+ r"""Return a :class:`.ListingGenerator` for :class:`.Redditor`\ s invited to be moderators.
- Keyword arguments are passed to :func:`.stream_generator`.
+ :param redditor: When provided, return a list containing at most one
+ :class:`.Redditor` instance. This is useful to confirm if a relationship
+ exists, or to fetch the metadata associated with a particular relationship
+ (default: ``None``).
- To print new items in the unmoderated queue try:
+ Additional keyword arguments are passed in the initialization of
+ :class:`.ListingGenerator`.
- .. code-block:: python
+ .. note::
- for item in reddit.subreddit("mod").mod.stream.unmoderated():
- print(item)
+ Unlike other usages of :class:`.ListingGenerator`, ``limit`` has no effect
+ in the quantity returned. This endpoint always returns moderators in batches
+ of 25 at a time regardless of what ``limit`` is set to.
- """
- return stream_generator(self.subreddit.mod.unmoderated, **stream_options)
+ Usage:
- def unread(
- self, **stream_options: Any
- ) -> Generator[praw.models.SubredditMessage, None, None]:
- """Yield unread old modmail messages as they become available.
+ .. code-block:: python
- Keyword arguments are passed to :func:`.stream_generator`.
+ for invited_mod in reddit.subreddit("test").moderator.invited():
+ print(invited_mod)
- .. seealso::
+ """
+ generator_kwargs["params"] = {"username": redditor} if redditor else None
+ url = API_PATH["list_invited_moderator"].format(subreddit=self.subreddit)
+ return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)
- :meth:`.SubredditModeration.inbox` for all messages.
+ def leave(self):
+ """Abdicate the moderator position (use with care).
- To print new mail in the unread modmail queue try:
+ For example:
.. code-block:: python
- for message in reddit.subreddit("mod").mod.stream.unread():
- print(f"From: {message.author}, To: {message.dest}")
+ reddit.subreddit("test").moderator.leave()
"""
- return stream_generator(self.subreddit.mod.unread, **stream_options)
+ self.remove(
+ self.subreddit._reddit.config.username or self.subreddit._reddit.user.me()
+ )
+ def remove_invite(self, redditor: str | praw.models.Redditor):
+ """Remove the moderator invite for ``redditor``.
-class SubredditQuarantine:
- """Provides subreddit quarantine related methods.
+ :param redditor: A redditor name or :class:`.Redditor` instance.
- To opt-in into a quarantined subreddit:
+ For example:
- .. code-block:: python
+ .. code-block:: python
- reddit.subreddit("test").quaran.opt_in()
+ reddit.subreddit("test").moderator.remove_invite("spez")
- """
+ """
+ data = {"name": str(redditor), "type": "moderator_invite"}
+ url = API_PATH["unfriend"].format(subreddit=self.subreddit)
+ self.subreddit._reddit.post(url, data=data)
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditQuarantine` instance.
+ @_deprecate_args("redditor", "permissions")
+ def update(
+ self,
+ redditor: str | praw.models.Redditor,
+ *,
+ permissions: list[str] | None = None,
+ ):
+ """Update the moderator permissions for ``redditor``.
- :param subreddit: The :class:`.Subreddit` associated with the quarantine.
+ :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param permissions: When provided (not ``None``), permissions should be a list
+ of strings specifying which subset of permissions to grant. An empty list
+ ``[]`` indicates no permissions, and when not provided, ``None``, indicates
+ full permissions (default: ``None``).
- """
- self.subreddit = subreddit
+ For example, to add all permissions to the moderator, try:
- def opt_in(self):
- """Permit your user access to the quarantined subreddit.
+ .. code-block:: python
- Usage:
+ subreddit.moderator.update("spez")
- .. code-block:: python
+ To remove all permissions from the moderator, try:
- subreddit = reddit.subreddit("QUESTIONABLE")
- next(subreddit.hot()) # Raises prawcore.Forbidden
+ .. code-block:: python
- subreddit.quaran.opt_in()
- next(subreddit.hot()) # Returns Submission
+ subreddit.moderator.update("spez", permissions=[])
"""
- data = {"sr_name": self.subreddit}
- with contextlib.suppress(Redirect):
- self.subreddit._reddit.post(API_PATH["quarantine_opt_in"], data=data)
+ url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
+ data = self._handle_permissions(
+ other_settings={"name": str(redditor), "type": "moderator"},
+ permissions=permissions,
+ )
+ self.subreddit._reddit.post(url, data=data)
- def opt_out(self):
- """Remove access to the quarantined subreddit.
+ @_deprecate_args("redditor", "permissions")
+ def update_invite(
+ self,
+ redditor: str | praw.models.Redditor,
+ *,
+ permissions: list[str] | None = None,
+ ):
+ """Update the moderator invite permissions for ``redditor``.
- Usage:
+ :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param permissions: When provided (not ``None``), permissions should be a list
+ of strings specifying which subset of permissions to grant. An empty list
+ ``[]`` indicates no permissions, and when not provided, ``None``, indicates
+ full permissions (default: ``None``).
+
+ For example, to grant the ``"flair"`` and ``"mail"`` permissions to the
+ moderator invite, try:
.. code-block:: python
- subreddit = reddit.subreddit("QUESTIONABLE")
- next(subreddit.hot()) # Returns Submission
-
- subreddit.quaran.opt_out()
- next(subreddit.hot()) # Raises prawcore.Forbidden
+ subreddit.moderator.update_invite("spez", permissions=["flair", "mail"])
"""
- data = {"sr_name": self.subreddit}
- with contextlib.suppress(Redirect):
- self.subreddit._reddit.post(API_PATH["quarantine_opt_out"], data=data)
-
+ url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
+ data = self._handle_permissions(
+ other_settings={"name": str(redditor), "type": "moderator_invite"},
+ permissions=permissions,
+ )
+ self.subreddit._reddit.post(url, data=data)
-class SubredditRelationship:
- """Represents a relationship between a :class:`.Redditor` and :class:`.Subreddit`.
- Instances of this class can be iterated through in order to discover the Redditors
- that make up the relationship.
+class Subreddit(MessageableMixin, SubredditListingMixin, FullnameMixin, RedditBase):
+ """A class for Subreddits.
- For example, banned users of a subreddit can be iterated through like so:
+ To obtain an instance of this class for r/test execute:
.. code-block:: python
- for ban in reddit.subreddit("test").banned():
- print(f"{ban}: {ban.note}")
+ subreddit = reddit.subreddit("test")
- """
+ While r/all is not a real subreddit, it can still be treated like one. The following
+ outputs the titles of the 25 hottest submissions in r/all:
- def __call__(
- self,
- redditor: str | praw.models.Redditor | None = None,
- **generator_kwargs: Any,
- ) -> Iterator[praw.models.Redditor]:
- r"""Return a :class:`.ListingGenerator` for :class:`.Redditor`\ s in the relationship.
+ .. code-block:: python
- :param redditor: When provided, yield at most a single :class:`.Redditor`
- instance. This is useful to confirm if a relationship exists, or to fetch
- the metadata associated with a particular relationship (default: ``None``).
+ for submission in reddit.subreddit("all").hot(limit=25):
+ print(submission.title)
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ Multiple subreddits can be combined with a ``+`` like so:
- """
- Subreddit._safely_add_arguments(
- arguments=generator_kwargs, key="params", user=redditor
- )
- url = API_PATH[f"list_{self.relationship}"].format(subreddit=self.subreddit)
- return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)
+ .. code-block:: python
- def __init__(self, subreddit: praw.models.Subreddit, relationship: str):
- """Initialize a :class:`.SubredditRelationship` instance.
+ for submission in reddit.subreddit("redditdev+learnpython").top(time_filter="all"):
+ print(submission)
- :param subreddit: The :class:`.Subreddit` for the relationship.
- :param relationship: The name of the relationship.
+ Subreddits can be filtered from combined listings as follows.
- """
- self.relationship = relationship
- self.subreddit = subreddit
+ .. note::
- def add(self, redditor: str | praw.models.Redditor, **other_settings: Any):
- """Add ``redditor`` to this relationship.
+ These filters are ignored by certain methods, including :attr:`.comments`,
+ :meth:`.gilded`, and :meth:`.SubredditStream.comments`.
- :param redditor: A redditor name or :class:`.Redditor` instance.
+ .. code-block:: python
- """
- data = {"name": str(redditor), "type": self.relationship}
- data.update(other_settings)
- url = API_PATH["friend"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
+ for submission in reddit.subreddit("all-redditdev").new():
+ print(submission)
- def remove(self, redditor: str | praw.models.Redditor):
- """Remove ``redditor`` from this relationship.
+ .. include:: ../../typical_attributes.rst
- :param redditor: A redditor name or :class:`.Redditor` instance.
+ ========================= ==========================================================
+ Attribute Description
+ ========================= ==========================================================
+ ``can_assign_link_flair`` Whether users can assign their own link flair.
+ ``can_assign_user_flair`` Whether users can assign their own user flair.
+ ``created_utc`` Time the subreddit was created, represented in `Unix
+ Time`_.
+ ``description`` Subreddit description, in Markdown.
+ ``description_html`` Subreddit description, in HTML.
+ ``display_name`` Name of the subreddit.
+ ``icon_img`` The URL of the subreddit icon image.
+ ``id`` ID of the subreddit.
+ ``name`` Fullname of the subreddit.
+ ``over18`` Whether the subreddit is NSFW.
+ ``public_description`` Description of the subreddit, shown in searches and on the
+ "You must be invited to visit this community" page (if
+ applicable).
+ ``spoilers_enabled`` Whether the spoiler tag feature is enabled.
+ ``subscribers`` Count of subscribers.
+ ``user_is_banned`` Whether the authenticated user is banned.
+ ``user_is_moderator`` Whether the authenticated user is a moderator.
+ ``user_is_subscriber`` Whether the authenticated user is subscribed.
+ ========================= ==========================================================
- """
- data = {"name": str(redditor), "type": self.relationship}
- url = API_PATH["unfriend"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
+ .. note::
+ Trying to retrieve attributes of quarantined or private subreddits will result
+ in a 403 error. Trying to retrieve attributes of a banned subreddit will result
+ in a 404 error.
-class SubredditStream:
- """Provides submission and comment streams."""
+ .. _unix time: https://en.wikipedia.org/wiki/Unix_time
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditStream` instance.
+ """
- :param subreddit: The subreddit associated with the streams.
+ STR_FIELD = "display_name"
+ MESSAGE_PREFIX = "#"
- """
- self.subreddit = subreddit
+ @staticmethod
+ def _create_or_update(
+ *,
+ _reddit: praw.Reddit,
+ allow_images: bool | None = None,
+ allow_post_crossposts: bool | None = None,
+ allow_top: bool | None = None,
+ collapse_deleted_comments: bool | None = None,
+ comment_score_hide_mins: int | None = None,
+ description: str | None = None,
+ domain: str | None = None,
+ exclude_banned_modqueue: bool | None = None,
+ header_hover_text: str | None = None,
+ hide_ads: bool | None = None,
+ lang: str | None = None,
+ key_color: str | None = None,
+ link_type: str | None = None,
+ name: str | None = None,
+ over_18: bool | None = None,
+ public_description: str | None = None,
+ public_traffic: bool | None = None,
+ show_media: bool | None = None,
+ show_media_preview: bool | None = None,
+ spam_comments: bool | None = None,
+ spam_links: bool | None = None,
+ spam_selfposts: bool | None = None,
+ spoilers_enabled: bool | None = None,
+ sr: str | None = None,
+ submit_link_label: str | None = None,
+ submit_text: str | None = None,
+ submit_text_label: str | None = None,
+ subreddit_type: str | None = None,
+ suggested_comment_sort: str | None = None,
+ title: str | None = None,
+ wiki_edit_age: int | None = None,
+ wiki_edit_karma: int | None = None,
+ wikimode: str | None = None,
+ **other_settings: Any,
+ ):
+ model = {
+ "allow_images": allow_images,
+ "allow_post_crossposts": allow_post_crossposts,
+ "allow_top": allow_top,
+ "collapse_deleted_comments": collapse_deleted_comments,
+ "comment_score_hide_mins": comment_score_hide_mins,
+ "description": description,
+ "domain": domain,
+ "exclude_banned_modqueue": exclude_banned_modqueue,
+ "header-title": header_hover_text, # Remap here - better name
+ "hide_ads": hide_ads,
+ "key_color": key_color,
+ "lang": lang,
+ "link_type": link_type,
+ "name": name,
+ "over_18": over_18,
+ "public_description": public_description,
+ "public_traffic": public_traffic,
+ "show_media": show_media,
+ "show_media_preview": show_media_preview,
+ "spam_comments": spam_comments,
+ "spam_links": spam_links,
+ "spam_selfposts": spam_selfposts,
+ "spoilers_enabled": spoilers_enabled,
+ "sr": sr,
+ "submit_link_label": submit_link_label,
+ "submit_text": submit_text,
+ "submit_text_label": submit_text_label,
+ "suggested_comment_sort": suggested_comment_sort,
+ "title": title,
+ "type": subreddit_type,
+ "wiki_edit_age": wiki_edit_age,
+ "wiki_edit_karma": wiki_edit_karma,
+ "wikimode": wikimode,
+ }
- def comments(
- self, **stream_options: Any
- ) -> Generator[praw.models.Comment, None, None]:
- """Yield new comments as they become available.
+ model.update(other_settings)
- Comments are yielded oldest first. Up to 100 historical comments will initially
- be returned.
+ _reddit.post(API_PATH["site_admin"], data=model)
- Keyword arguments are passed to :func:`.stream_generator`.
+ @staticmethod
+ def _subreddit_list(
+ *,
+ other_subreddits: list[str | praw.models.Subreddit],
+ subreddit: praw.models.Subreddit,
+ ) -> str:
+ if other_subreddits:
+ return ",".join([str(subreddit)] + [str(x) for x in other_subreddits])
+ return str(subreddit)
- .. note::
+ @staticmethod
+ def _validate_gallery(images: list[dict[str, str]]):
+ for image in images:
+ image_path = image.get("image_path", "")
+ if image_path:
+ if not Path(image_path).is_file():
+ msg = f"{image_path!r} is not a valid image path."
+ raise TypeError(msg)
+ else:
+ msg = "'image_path' is required."
+ raise TypeError(msg)
+ if not len(image.get("caption", "")) <= 180:
+ msg = "Caption must be 180 characters or less."
+ raise TypeError(msg)
- While PRAW tries to catch all new comments, some high-volume streams,
- especially the r/all stream, may drop some comments.
+ @staticmethod
+ def _validate_inline_media(inline_media: praw.models.InlineMedia):
+ if not Path(inline_media.path).is_file():
+ msg = f"{inline_media.path!r} is not a valid file path."
+ raise ValueError(msg)
- For example, to retrieve all new comments made to r/test, try:
+ @cachedproperty
+ def banned(self) -> praw.models.reddit.subreddit.SubredditRelationship:
+ """Provide an instance of :class:`.SubredditRelationship`.
+
+ For example, to ban a user try:
.. code-block:: python
- for comment in reddit.subreddit("test").stream.comments():
- print(comment)
+ reddit.subreddit("test").banned.add("spez", ban_reason="...")
- To only retrieve new submissions starting when the stream is created, pass
- ``skip_existing=True``:
+ To list the banned users along with any notes, try:
.. code-block:: python
- subreddit = reddit.subreddit("test")
- for comment in subreddit.stream.comments(skip_existing=True):
- print(comment)
+ for ban in reddit.subreddit("test").banned():
+ print(f"{ban}: {ban.note}")
"""
- return stream_generator(self.subreddit.comments, **stream_options)
+ return SubredditRelationship(self, "banned")
- def submissions(
- self, **stream_options: Any
- ) -> Generator[praw.models.Submission, None, None]:
- r"""Yield new :class:`.Submission`\ s as they become available.
+ @cachedproperty
+ def collections(self) -> praw.models.reddit.collections.SubredditCollections:
+ r"""Provide an instance of :class:`.SubredditCollections`.
- Submissions are yielded oldest first. Up to 100 historical submissions will
- initially be returned.
+ To see the permalinks of all :class:`.Collection`\ s that belong to a subreddit,
+ try:
- Keyword arguments are passed to :func:`.stream_generator`.
+ .. code-block:: python
- .. note::
+ for collection in reddit.subreddit("test").collections:
+ print(collection.permalink)
- While PRAW tries to catch all new submissions, some high-volume streams,
- especially the r/all stream, may drop some submissions.
+ To get a specific :class:`.Collection` by its UUID or permalink, use one of the
+ following:
- For example, to retrieve all new submissions made to all of Reddit, try:
+ .. code-block:: python
+
+ collection = reddit.subreddit("test").collections("some_uuid")
+ collection = reddit.subreddit("test").collections(
+ permalink="https://reddit.com/r/SUBREDDIT/collection/some_uuid"
+ )
+
+ """
+ return self._subreddit_collections_class(self._reddit, self)
+
+ @cachedproperty
+ def contributor(self) -> praw.models.reddit.subreddit.ContributorRelationship:
+ """Provide an instance of :class:`.ContributorRelationship`.
+
+ Contributors are also known as approved submitters.
+
+ To add a contributor try:
.. code-block:: python
- for submission in reddit.subreddit("all").stream.submissions():
- print(submission)
+ reddit.subreddit("test").contributor.add("spez")
"""
- return stream_generator(self.subreddit.new, **stream_options)
-
+ return ContributorRelationship(self, "contributor")
-class SubredditStylesheet:
- """Provides a set of stylesheet functions to a :class:`.Subreddit`.
+ @cachedproperty
+ def emoji(self) -> SubredditEmoji:
+ """Provide an instance of :class:`.SubredditEmoji`.
- For example, to add the css data ``.test{color:blue}`` to the existing stylesheet:
+ This attribute can be used to discover all emoji for a subreddit:
- .. code-block:: python
+ .. code-block:: python
- subreddit = reddit.subreddit("test")
- stylesheet = subreddit.stylesheet()
- stylesheet.stylesheet += ".test{color:blue}"
- subreddit.stylesheet.update(stylesheet.stylesheet)
+ for emoji in reddit.subreddit("test").emoji:
+ print(emoji)
- """
+ A single emoji can be lazily retrieved via:
- def __call__(self) -> praw.models.Stylesheet:
- """Return the :class:`.Subreddit`'s stylesheet.
+ .. code-block:: python
- To be used as:
+ reddit.subreddit("test").emoji["emoji_name"]
- .. code-block:: python
+ .. note::
- stylesheet = reddit.subreddit("test").stylesheet()
+ Attempting to access attributes of a nonexistent emoji will result in a
+ :class:`.ClientException`.
"""
- url = API_PATH["about_stylesheet"].format(subreddit=self.subreddit)
- return self.subreddit._reddit.get(url)
-
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditStylesheet` instance.
+ return SubredditEmoji(self)
- :param subreddit: The :class:`.Subreddit` associated with the stylesheet.
+ @cachedproperty
+ def filters(self) -> praw.models.reddit.subreddit.SubredditFilters:
+ """Provide an instance of :class:`.SubredditFilters`.
- An instance of this class is provided as:
+ For example, to add a filter, run:
.. code-block:: python
- reddit.subreddit("test").stylesheet
+ reddit.subreddit("all").filters.add("test")
"""
- self.subreddit = subreddit
+ return SubredditFilters(self)
- def _update_structured_styles(
- self, style_data: dict[str, str | Any]
- ): # noqa: ANN001
- url = API_PATH["structured_styles"].format(subreddit=self.subreddit)
- self.subreddit._reddit.patch(url, data=style_data)
+ @cachedproperty
+ def flair(self) -> praw.models.reddit.subreddit.SubredditFlair:
+ """Provide an instance of :class:`.SubredditFlair`.
- def _upload_image(
- self, *, data: dict[str, str | Any], image_path: str
- ) -> dict[str, Any]:
- with Path(image_path).open("rb") as image:
- header = image.read(len(JPEG_HEADER))
- image.seek(0)
- data["img_type"] = "jpg" if header == JPEG_HEADER else "png"
- url = API_PATH["upload_image"].format(subreddit=self.subreddit)
- response = self.subreddit._reddit.post(
- url, data=data, files={"file": image}
- )
- if response["errors"]:
- error_type = response["errors"][0]
- error_value = response.get("errors_values", [""])[0]
- assert error_type in [
- "BAD_CSS_NAME",
- "IMAGE_ERROR",
- ], "Please file a bug with PRAW."
- raise RedditAPIException([[error_type, error_value, None]])
- return response
+ Use this attribute for interacting with a :class:`.Subreddit`'s flair. For
+ example, to list all the flair for a subreddit which you have the ``flair``
+ moderator permission on try:
- def _upload_style_asset(
- self, *, image_path: str, image_type: str
- ) -> str: # noqa: ANN001
- file = Path(image_path)
- data = {"imagetype": image_type, "filepath": file.name}
- data["mimetype"] = "image/jpeg"
- if image_path.lower().endswith(".png"):
- data["mimetype"] = "image/png"
- url = API_PATH["style_asset_lease"].format(subreddit=self.subreddit)
+ .. code-block:: python
- upload_lease = self.subreddit._reddit.post(url, data=data)["s3UploadLease"]
- upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
- upload_url = f"https:{upload_lease['action']}"
+ for flair in reddit.subreddit("test").flair():
+ print(flair)
- with file.open("rb") as image:
- response = self.subreddit._reddit._core._requestor._http.post(
- upload_url, data=upload_data, files={"file": image}
- )
- response.raise_for_status()
+ Flair templates can be interacted with through this attribute via:
- return f"{upload_url}/{upload_data['key']}"
+ .. code-block:: python
- def delete_banner(self):
- """Remove the current :class:`.Subreddit` (redesign) banner image.
+ for template in reddit.subreddit("test").flair.templates:
+ print(template)
- Succeeds even if there is no banner image.
+ """
+ return SubredditFlair(self)
- For example:
+ @cachedproperty
+ def mod(self) -> SubredditModeration:
+ """Provide an instance of :class:`.SubredditModeration`.
+
+ For example, to accept a moderation invite from r/test:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_banner()
+ reddit.subreddit("test").mod.accept_invite()
"""
- data = {"bannerBackgroundImage": ""}
- self._update_structured_styles(data)
+ return SubredditModeration(self)
- def delete_banner_additional_image(self):
- """Remove the current :class:`.Subreddit` (redesign) banner additional image.
+ @cachedproperty
+ def moderator(self) -> praw.models.reddit.subreddit.ModeratorRelationship:
+ """Provide an instance of :class:`.ModeratorRelationship`.
- Succeeds even if there is no additional image. Will also delete any configured
- hover image.
+ For example, to add a moderator try:
- For example:
+ .. code-block:: python
+
+ reddit.subreddit("test").moderator.add("spez")
+
+ To list the moderators along with their permissions try:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_banner_additional_image()
+ for moderator in reddit.subreddit("test").moderator():
+ print(f"{moderator}: {moderator.mod_permissions}")
"""
- data = {"bannerPositionedImage": "", "secondaryBannerPositionedImage": ""}
- self._update_structured_styles(data)
-
- def delete_banner_hover_image(self):
- """Remove the current :class:`.Subreddit` (redesign) banner hover image.
+ return ModeratorRelationship(self, "moderator")
- Succeeds even if there is no hover image.
+ @cachedproperty
+ def modmail(self) -> praw.models.reddit.subreddit.Modmail:
+ """Provide an instance of :class:`.Modmail`.
- For example:
+ For example, to send a new modmail from r/test to u/spez with the subject
+ ``"test"`` along with a message body of ``"hello"``:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_banner_hover_image()
+ reddit.subreddit("test").modmail.create(subject="test", body="hello", recipient="spez")
"""
- data = {"secondaryBannerPositionedImage": ""}
- self._update_structured_styles(data)
-
- def delete_header(self):
- """Remove the current :class:`.Subreddit` header image.
+ return Modmail(self)
- Succeeds even if there is no header image.
+ @cachedproperty
+ def muted(self) -> praw.models.reddit.subreddit.SubredditRelationship:
+ """Provide an instance of :class:`.SubredditRelationship`.
- For example:
+ For example, muted users can be iterated through like so:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_header()
+ for mute in reddit.subreddit("test").muted():
+ print(f"{mute}: {mute.date}")
"""
- url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url)
+ return SubredditRelationship(self, "muted")
- def delete_image(self, name: str):
- """Remove the named image from the :class:`.Subreddit`.
+ @cachedproperty
+ def quaran(self) -> praw.models.reddit.subreddit.SubredditQuarantine:
+ """Provide an instance of :class:`.SubredditQuarantine`.
- Succeeds even if the named image does not exist.
+ This property is named ``quaran`` because ``quarantine`` is a subreddit
+ attribute returned by Reddit to indicate whether or not a subreddit is
+ quarantined.
- For example:
+ To opt-in into a quarantined subreddit:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_image("smile")
+ reddit.subreddit("test").quaran.opt_in()
"""
- url = API_PATH["delete_sr_image"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data={"img_name": name})
+ return SubredditQuarantine(self)
- def delete_mobile_banner(self):
- """Remove the current :class:`.Subreddit` (redesign) mobile banner.
+ @cachedproperty
+ def rules(self) -> SubredditRules:
+ """Provide an instance of :class:`.SubredditRules`.
- Succeeds even if there is no mobile banner.
+ Use this attribute for interacting with a :class:`.Subreddit`'s rules.
- For example:
+ For example, to list all the rules for a subreddit:
.. code-block:: python
- subreddit = reddit.subreddit("test")
- subreddit.stylesheet.delete_banner_hover_image()
-
- """
- data = {"mobileBannerImage": ""}
- self._update_structured_styles(data)
-
- def delete_mobile_header(self):
- """Remove the current :class:`.Subreddit` mobile header.
-
- Succeeds even if there is no mobile header.
+ for rule in reddit.subreddit("test").rules:
+ print(rule)
- For example:
+ Moderators can also add rules to the subreddit. For example, to make a rule
+ called ``"No spam"`` in r/test:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_mobile_header()
+ reddit.subreddit("test").rules.mod.add(
+ short_name="No spam", kind="all", description="Do not spam. Spam bad"
+ )
"""
- url = API_PATH["delete_sr_header"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url)
+ return SubredditRules(self)
- def delete_mobile_icon(self):
- """Remove the current :class:`.Subreddit` mobile icon.
+ @cachedproperty
+ def stream(self) -> praw.models.reddit.subreddit.SubredditStream:
+ """Provide an instance of :class:`.SubredditStream`.
- Succeeds even if there is no mobile icon.
+ Streams can be used to indefinitely retrieve new comments made to a subreddit,
+ like:
- For example:
+ .. code-block:: python
+
+ for comment in reddit.subreddit("test").stream.comments():
+ print(comment)
+
+ Additionally, new submissions can be retrieved via the stream. In the following
+ example all submissions are fetched via the special r/all:
.. code-block:: python
- reddit.subreddit("test").stylesheet.delete_mobile_icon()
+ for submission in reddit.subreddit("all").stream.submissions():
+ print(submission)
"""
- url = API_PATH["delete_sr_icon"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url)
-
- @_deprecate_args("stylesheet", "reason")
- def update(self, stylesheet: str, *, reason: str | None = None):
- """Update the :class:`.Subreddit`'s stylesheet.
+ return SubredditStream(self)
- :param stylesheet: The CSS for the new stylesheet.
- :param reason: The reason for updating the stylesheet.
+ @cachedproperty
+ def stylesheet(self) -> praw.models.reddit.subreddit.SubredditStylesheet:
+ """Provide an instance of :class:`.SubredditStylesheet`.
- For example:
+ For example, to add the css data ``.test{color:blue}`` to the existing
+ stylesheet:
.. code-block:: python
- reddit.subreddit("test").stylesheet.update(
- "p { color: green; }", reason="color text green"
- )
-
- """
- data = {"op": "save", "reason": reason, "stylesheet_contents": stylesheet}
- url = API_PATH["subreddit_stylesheet"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
+ subreddit = reddit.subreddit("test")
+ stylesheet = subreddit.stylesheet()
+ stylesheet.stylesheet += ".test{color:blue}"
+ subreddit.stylesheet.update(stylesheet.stylesheet)
- @_deprecate_args("name", "image_path")
- def upload(self, *, image_path: str, name: str) -> dict[str, str]:
- """Upload an image to the :class:`.Subreddit`.
+ """
+ return SubredditStylesheet(self)
- :param image_path: A path to a jpeg or png image.
- :param name: The name to use for the image. If an image already exists with the
- same name, it will be replaced.
+ @cachedproperty
+ def widgets(self) -> praw.models.SubredditWidgets:
+ """Provide an instance of :class:`.SubredditWidgets`.
- :returns: A dictionary containing a link to the uploaded image under the key
- ``img_src``.
+ **Example usage**
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ Get all sidebar widgets:
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ .. code-block:: python
- For example:
+ for widget in reddit.subreddit("test").widgets.sidebar:
+ print(widget)
+
+ Get ID card widget:
.. code-block:: python
- reddit.subreddit("test").stylesheet.upload(name="smile", image_path="img.png")
+ print(reddit.subreddit("test").widgets.id_card)
"""
- return self._upload_image(
- data={"name": name, "upload_type": "img"}, image_path=image_path
- )
+ return SubredditWidgets(self)
- def upload_banner(self, image_path: str):
- """Upload an image for the :class:`.Subreddit`'s (redesign) banner image.
+ @cachedproperty
+ def wiki(self) -> praw.models.reddit.subreddit.SubredditWiki:
+ """Provide an instance of :class:`.SubredditWiki`.
- :param image_path: A path to a jpeg or png image.
+ This attribute can be used to discover all wikipages for a subreddit:
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ .. code-block:: python
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ for wikipage in reddit.subreddit("test").wiki:
+ print(wikipage)
- For example:
+ To fetch the content for a given wikipage try:
.. code-block:: python
- reddit.subreddit("test").stylesheet.upload_banner("banner.png")
+ wikipage = reddit.subreddit("test").wiki["proof"]
+ print(wikipage.content_md)
"""
- image_type = "bannerBackgroundImage"
- image_url = self._upload_style_asset(
- image_path=image_path, image_type=image_type
- )
- self._update_structured_styles({image_type: image_url})
+ return SubredditWiki(self)
- @_deprecate_args("image_path", "align")
- def upload_banner_additional_image(
+ @property
+ def _kind(self) -> str:
+ """Return the class's kind."""
+ return self._reddit.config.kinds["subreddit"]
+
+ def __init__(
self,
- image_path: str,
- *,
- align: str | None = None,
+ reddit: praw.Reddit,
+ display_name: str | None = None,
+ _data: dict[str, Any] | None = None,
):
- """Upload an image for the :class:`.Subreddit`'s (redesign) additional image.
-
- :param image_path: A path to a jpeg or png image.
- :param align: Either ``"left"``, ``"centered"``, or ``"right"``. (default:
- ``"left"``).
+ """Initialize a :class:`.Subreddit` instance.
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ :param reddit: An instance of :class:`.Reddit`.
+ :param display_name: The name of the subreddit.
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ .. note::
- For example:
+ This class should not be initialized directly. Instead, obtain an instance
+ via:
- .. code-block:: python
+ .. code-block:: python
- subreddit = reddit.subreddit("test")
- subreddit.stylesheet.upload_banner_additional_image("banner.png")
+ subreddit = reddit.subreddit("test")
"""
- alignment = {}
- if align is not None:
- if align not in {"left", "centered", "right"}:
- msg = "'align' argument must be either 'left', 'centered', or 'right'"
- raise ValueError(msg)
- alignment["bannerPositionedImagePosition"] = align
-
- image_type = "bannerPositionedImage"
- image_url = self._upload_style_asset(
- image_path=image_path, image_type=image_type
- )
- style_data = {image_type: image_url}
- if alignment:
- style_data.update(alignment)
- self._update_structured_styles(style_data)
-
- def upload_banner_hover_image(self, image_path: str):
- """Upload an image for the :class:`.Subreddit`'s (redesign) additional image.
-
- :param image_path: A path to a jpeg or png image.
+ if (display_name, _data).count(None) != 1:
+ msg = "Either 'display_name' or '_data' must be provided."
+ raise TypeError(msg)
+ if display_name:
+ self.display_name = display_name
+ super().__init__(reddit, _data=_data)
+ self._path = API_PATH["subreddit"].format(subreddit=self)
- Fails if the :class:`.Subreddit` does not have an additional image defined.
+ def _convert_to_fancypants(self, markdown_text: str) -> dict:
+ """Convert a Markdown string to a dict for use with the ``richtext_json`` param.
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ :param markdown_text: A Markdown string to convert.
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ :returns: A dict in ``richtext_json`` format.
- For example:
+ """
+ text_data = {"output_mode": "rtjson", "markdown_text": markdown_text}
+ return self._reddit.post(API_PATH["convert_rte_body"], data=text_data)["output"]
- .. code-block:: python
+ def _fetch(self):
+ data = self._fetch_data()
+ data = data["data"]
+ other = type(self)(self._reddit, _data=data)
+ self.__dict__.update(other.__dict__)
+ super()._fetch()
- subreddit = reddit.subreddit("test")
- subreddit.stylesheet.upload_banner_hover_image("banner.png")
+ def _fetch_info(self):
+ return "subreddit_about", {"subreddit": self}, None
- """
- image_type = "secondaryBannerPositionedImage"
- image_url = self._upload_style_asset(
- image_path=image_path, image_type=image_type
- )
- self._update_structured_styles({image_type: image_url})
+ def _parse_xml_response(self, response: Response):
+ """Parse the XML from a response and raise any errors found."""
+ xml = response.text
+ root = XML(xml)
+ tags = [element.tag for element in root]
+ if tags[:4] == ["Code", "Message", "ProposedSize", "MaxSizeAllowed"]:
+ # Returned if image is too big
+ code, message, actual, maximum_size = (element.text for element in root[:4])
+ raise TooLargeMediaException(
+ actual=int(actual), maximum_size=int(maximum_size)
+ )
- def upload_header(self, image_path: str) -> dict[str, str]:
- """Upload an image to be used as the :class:`.Subreddit`'s header image.
+ def _read_and_post_media(
+ self, media_path: str, upload_url: str, upload_data: dict[str, Any]
+ ) -> Response:
+ with media_path.open("rb") as media:
+ return self._reddit._core._requestor._http.post(
+ upload_url, data=upload_data, files={"file": media}
+ )
- :param image_path: A path to a jpeg or png image.
+ def _submit_media(
+ self, *, data: dict[Any, Any], timeout: int, without_websockets: bool
+ ):
+ """Submit and return an ``image``, ``video``, or ``videogif``.
- :returns: A dictionary containing a link to the uploaded image under the key
- ``img_src``.
+ This is a helper method for submitting posts that are not link posts or self
+ posts.
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ """
+ response = self._reddit.post(API_PATH["submit"], data=data)
+ websocket_url = response["json"]["data"]["websocket_url"]
+ connection = None
+ if websocket_url is not None and not without_websockets:
+ try:
+ connection = websocket.create_connection(websocket_url, timeout=timeout)
+ except (
+ OSError,
+ websocket.WebSocketException,
+ BlockingIOError,
+ ) as ws_exception:
+ msg = "Error establishing websocket connection."
+ raise WebSocketException(msg, ws_exception) from None
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ if connection is None:
+ return None
- For example:
+ try:
+ ws_update = loads(connection.recv())
+ connection.close()
+ except (OSError, websocket.WebSocketException, BlockingIOError) as ws_exception:
+ msg = "Websocket error. Check your media file. Your post may still have been created."
+ raise WebSocketException(
+ msg,
+ ws_exception,
+ ) from None
+ if ws_update.get("type") == "failed":
+ raise MediaPostFailed
+ url = ws_update["payload"]["redirect"]
+ return self._reddit.submission(url=url)
- .. code-block:: python
+ def _upload_inline_media(self, inline_media: praw.models.InlineMedia):
+ """Upload media for use in self posts and return ``inline_media``.
- reddit.subreddit("test").stylesheet.upload_header("header.png")
+ :param inline_media: An :class:`.InlineMedia` object to validate and upload.
"""
- return self._upload_image(data={"upload_type": "header"}, image_path=image_path)
-
- def upload_mobile_banner(self, image_path: str):
- """Upload an image for the :class:`.Subreddit`'s (redesign) mobile banner.
+ self._validate_inline_media(inline_media)
+ inline_media.media_id = self._upload_media(
+ media_path=inline_media.path, upload_type="selfpost"
+ )
+ return inline_media
- :param image_path: A path to a JPEG or PNG image.
+ def _upload_media(
+ self,
+ *,
+ expected_mime_prefix: str | None = None,
+ media_path: str,
+ upload_type: str = "link",
+ ):
+ """Upload media and return its URL and a websocket (Undocumented endpoint).
- For example:
+ :param expected_mime_prefix: If provided, enforce that the media has a mime type
+ that starts with the provided prefix.
+ :param upload_type: One of ``"link"``, ``"gallery"'', or ``"selfpost"``
+ (default: ``"link"``).
- .. code-block:: python
+ :returns: A tuple containing ``(media_url, websocket_url)`` for the piece of
+ media. The websocket URL can be used to determine when media processing is
+ finished, or it can be ignored.
- subreddit = reddit.subreddit("test")
- subreddit.stylesheet.upload_mobile_banner("banner.png")
+ """
+ if media_path is None:
+ file = Path(__file__).absolute()
+ media_path = file.parent.parent.parent / "images" / "PRAW logo.png"
+ else:
+ file = Path(media_path)
- Fails if the :class:`.Subreddit` does not have an additional image defined.
+ file_name = file.name.lower()
+ file_extension = file_name.rpartition(".")[2]
+ mime_type = {
+ "png": "image/png",
+ "mov": "video/quicktime",
+ "mp4": "video/mp4",
+ "jpg": "image/jpeg",
+ "jpeg": "image/jpeg",
+ "gif": "image/gif",
+ }.get(
+ file_extension, "image/jpeg"
+ ) # default to JPEG
+ if (
+ expected_mime_prefix is not None
+ and mime_type.partition("/")[0] != expected_mime_prefix
+ ):
+ msg = f"Expected a mimetype starting with {expected_mime_prefix!r} but got mimetype {mime_type!r} (from file extension {file_extension!r})."
+ raise ClientException(msg)
+ img_data = {"filepath": file_name, "mimetype": mime_type}
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ url = API_PATH["media_asset"]
+ # until we learn otherwise, assume this request always succeeds
+ upload_response = self._reddit.post(url, data=img_data)
+ upload_lease = upload_response["args"]
+ upload_url = f"https:{upload_lease['action']}"
+ upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ response = self._read_and_post_media(file, upload_url, upload_data)
+ if not response.ok:
+ self._parse_xml_response(response)
+ try:
+ response.raise_for_status()
+ except HTTPError as err:
+ raise ServerError(response=err.response) from None
- """
- image_type = "mobileBannerImage"
- image_url = self._upload_style_asset(
- image_path=image_path, image_type=image_type
- )
- self._update_structured_styles({image_type: image_url})
+ upload_response["asset"]["websocket_url"]
- def upload_mobile_header(self, image_path: str) -> dict[str, str]:
- """Upload an image to be used as the :class:`.Subreddit`'s mobile header.
+ if upload_type == "link":
+ return f"{upload_url}/{upload_data['key']}"
+ return upload_response["asset"]["asset_id"]
- :param image_path: A path to a jpeg or png image.
+ def post_requirements(self) -> dict[str, str | int | bool]:
+ """Get the post requirements for a subreddit.
- :returns: A dictionary containing a link to the uploaded image under the key
- ``img_src``.
+ :returns: A dict with the various requirements.
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ The returned dict contains the following keys:
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ - ``domain_blacklist``
+ - ``body_restriction_policy``
+ - ``domain_whitelist``
+ - ``title_regexes``
+ - ``body_blacklisted_strings``
+ - ``body_required_strings``
+ - ``title_text_min_length``
+ - ``is_flair_required``
+ - ``title_text_max_length``
+ - ``body_regexes``
+ - ``link_repost_age``
+ - ``body_text_min_length``
+ - ``link_restriction_policy``
+ - ``body_text_max_length``
+ - ``title_required_strings``
+ - ``title_blacklisted_strings``
+ - ``guidelines_text``
+ - ``guidelines_display_policy``
- For example:
+ For example, to fetch the post requirements for r/test:
.. code-block:: python
- reddit.subreddit("test").stylesheet.upload_mobile_header("header.png")
+ print(reddit.subreddit("test").post_requirements)
"""
- return self._upload_image(data={"upload_type": "banner"}, image_path=image_path)
-
- def upload_mobile_icon(self, image_path: str) -> dict[str, str]:
- """Upload an image to be used as the :class:`.Subreddit`'s mobile icon.
-
- :param image_path: A path to a jpeg or png image.
-
- :returns: A dictionary containing a link to the uploaded image under the key
- ``img_src``.
+ return self._reddit.get(
+ API_PATH["post_requirements"].format(subreddit=str(self))
+ )
- :raises: ``prawcore.TooLarge`` if the overall request body is too large.
+ def random(self) -> praw.models.Submission | None:
+ """Return a random :class:`.Submission`.
- :raises: :class:`.RedditAPIException` if there are other issues with the
- uploaded image. Unfortunately the exception info might not be very specific,
- so try through the website with the same image to see what the problem
- actually might be.
+ Returns ``None`` on subreddits that do not support the random feature. One
+ example, at the time of writing, is r/wallpapers.
- For example:
+ For example, to get a random submission off of r/AskReddit:
.. code-block:: python
- reddit.subreddit("test").stylesheet.upload_mobile_icon("icon.png")
+ submission = reddit.subreddit("AskReddit").random()
+ print(submission.title)
"""
- return self._upload_image(data={"upload_type": "icon"}, image_path=image_path)
+ url = API_PATH["subreddit_random"].format(subreddit=self)
+ try:
+ self._reddit.get(url, params={"unique": self._reddit._next_unique})
+ except Redirect as redirect:
+ path = redirect.path
+ try:
+ return self._submission_class(
+ self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
+ )
+ except ClientException:
+ return None
+ @_deprecate_args("query", "sort", "syntax", "time_filter")
+ def search(
+ self,
+ query: str,
+ *,
+ sort: str = "relevance",
+ syntax: str = "lucene",
+ time_filter: str = "all",
+ **generator_kwargs: Any,
+ ) -> Iterator[praw.models.Submission]:
+ """Return a :class:`.ListingGenerator` for items that match ``query``.
-class SubredditWiki:
- """Provides a set of wiki functions to a :class:`.Subreddit`."""
+ :param query: The query string to search for.
+ :param sort: Can be one of: ``"relevance"``, ``"hot"``, ``"top"``, ``"new"``, or
+ ``"comments"``. (default: ``"relevance"``).
+ :param syntax: Can be one of: ``"cloudsearch"``, ``"lucene"``, or ``"plain"``
+ (default: ``"lucene"``).
+ :param time_filter: Can be one of: ``"all"``, ``"day"``, ``"hour"``,
+ ``"month"``, ``"week"``, or ``"year"`` (default: ``"all"``).
- def __getitem__(self, page_name: str) -> WikiPage:
- """Lazily return the :class:`.WikiPage` for the :class:`.Subreddit` named ``page_name``.
+ For more information on building a search query see:
+ https://www.reddit.com/wiki/search
- This method is to be used to fetch a specific wikipage, like so:
+ For example, to search all subreddits for ``"praw"`` try:
.. code-block:: python
- wikipage = reddit.subreddit("test").wiki["proof"]
- print(wikipage.content_md)
+ for submission in reddit.subreddit("all").search("praw"):
+ print(submission.title)
"""
- return WikiPage(self.subreddit._reddit, self.subreddit, page_name.lower())
-
- def __init__(self, subreddit: praw.models.Subreddit):
- """Initialize a :class:`.SubredditWiki` instance.
+ self._validate_time_filter(time_filter)
+ not_all = self.display_name.lower() != "all"
+ self._safely_add_arguments(
+ arguments=generator_kwargs,
+ key="params",
+ q=query,
+ restrict_sr=not_all,
+ sort=sort,
+ syntax=syntax,
+ t=time_filter,
+ )
+ url = API_PATH["search"].format(subreddit=self)
+ return ListingGenerator(self._reddit, url, **generator_kwargs)
- :param subreddit: The subreddit whose wiki to work with.
+ @_deprecate_args("number")
+ def sticky(self, *, number: int = 1) -> praw.models.Submission:
+ """Return a :class:`.Submission` object for a sticky of the subreddit.
- """
- self.banned = SubredditRelationship(subreddit, "wikibanned")
- self.contributor = SubredditRelationship(subreddit, "wikicontributor")
- self.subreddit = subreddit
+ :param number: Specify which sticky to return. 1 appears at the top (default:
+ ``1``).
- def __iter__(self) -> Generator[WikiPage, None, None]:
- """Iterate through the pages of the wiki.
+ :raises: ``prawcore.NotFound`` if the sticky does not exist.
- This method is to be used to discover all wikipages for a subreddit:
+ For example, to get the stickied post on r/test:
.. code-block:: python
- for wikipage in reddit.subreddit("test").wiki:
- print(wikipage)
+ reddit.subreddit("test").sticky()
"""
- response = self.subreddit._reddit.get(
- API_PATH["wiki_pages"].format(subreddit=self.subreddit),
- params={"unique": self.subreddit._reddit._next_unique},
+ url = API_PATH["about_sticky"].format(subreddit=self)
+ try:
+ self._reddit.get(url, params={"num": number})
+ except Redirect as redirect:
+ path = redirect.path
+ return self._submission_class(
+ self._reddit, url=urljoin(self._reddit.config.reddit_url, path)
)
- for page_name in response["data"]:
- yield WikiPage(self.subreddit._reddit, self.subreddit, page_name)
- @_deprecate_args("name", "content", "reason")
- def create(
+ @_deprecate_args(
+ "title",
+ "selftext",
+ "url",
+ "flair_id",
+ "flair_text",
+ "resubmit",
+ "send_replies",
+ "nsfw",
+ "spoiler",
+ "collection_id",
+ "discussion_type",
+ "inline_media",
+ "draft_id",
+ )
+ def submit(
self,
+ title: str,
*,
- content: str,
- name: str,
- reason: str | None = None,
- **other_settings: Any,
- ) -> WikiPage:
- """Create a new :class:`.WikiPage`.
+ collection_id: str | None = None,
+ discussion_type: str | None = None,
+ draft_id: str | None = None,
+ flair_id: str | None = None,
+ flair_text: str | None = None,
+ inline_media: dict[str, praw.models.InlineMedia] | None = None,
+ nsfw: bool = False,
+ resubmit: bool = True,
+ selftext: str | None = None,
+ send_replies: bool = True,
+ spoiler: bool = False,
+ url: str | None = None,
+ ) -> praw.models.Submission:
+ r"""Add a submission to the :class:`.Subreddit`.
- :param name: The name of the new :class:`.WikiPage`. This name will be
- normalized.
- :param content: The content of the new :class:`.WikiPage`.
- :param reason: The reason for the creation.
- :param other_settings: Additional keyword arguments to pass.
+ :param title: The title of the submission.
+ :param collection_id: The UUID of a :class:`.Collection` to add the
+ newly-submitted post to.
+ :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
+ traditional comments (default: ``None``).
+ :param draft_id: The ID of a draft to submit.
+ :param flair_id: The flair template to select (default: ``None``).
+ :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
+ this value will set a custom text (default: ``None``). ``flair_id`` is
+ required when ``flair_text`` is provided.
+ :param inline_media: A dict of :class:`.InlineMedia` objects where the key is
+ the placeholder name in ``selftext``.
+ :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
+ :param resubmit: When ``False``, an error will occur if the URL has already been
+ submitted (default: ``True``).
+ :param selftext: The Markdown formatted content for a ``text`` submission. Use
+ an empty string, ``""``, to make a title-only submission.
+ :param send_replies: When ``True``, messages will be sent to the submission
+ author when comments are made to the submission (default: ``True``).
+ :param spoiler: Whether the submission should be marked as a spoiler (default:
+ ``False``).
+ :param url: The URL for a ``link`` submission.
+
+ :returns: A :class:`.Submission` object for the newly created submission.
+
+ Either ``selftext`` or ``url`` can be provided, but not both.
- To create the wiki page ``"praw_test"`` in r/test try:
+ For example, to submit a URL to r/test do:
.. code-block:: python
- reddit.subreddit("test").wiki.create(
- name="praw_test", content="wiki body text", reason="PRAW Test Creation"
- )
+ title = "PRAW documentation"
+ url = "https://praw.readthedocs.io"
+ reddit.subreddit("test").submit(title, url=url)
- """
- name = name.replace(" ", "_").lower()
- new = WikiPage(self.subreddit._reddit, self.subreddit, name)
- new.edit(content=content, reason=reason, **other_settings)
- return new
+ For example, to submit a self post with inline media do:
- def revisions(
- self, **generator_kwargs: Any
- ) -> Generator[
- dict[str, praw.models.Redditor | WikiPage | str | int | bool | None],
- None,
- None,
- ]:
- """Return a :class:`.ListingGenerator` for recent wiki revisions.
+ .. code-block:: python
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ from praw.models import InlineGif, InlineImage, InlineVideo
- To view the wiki revisions for ``"praw_test"`` in r/test try:
+ gif = InlineGif(path="path/to/image.gif", caption="optional caption")
+ image = InlineImage(path="path/to/image.jpg", caption="optional caption")
+ video = InlineVideo(path="path/to/video.mp4", caption="optional caption")
+ selftext = "Text with a gif {gif1} an image {image1} and a video {video1} inline"
+ media = {"gif1": gif, "image1": image, "video1": video}
+ reddit.subreddit("test").submit("title", inline_media=media, selftext=selftext)
- .. code-block:: python
+ .. note::
- for item in reddit.subreddit("test").wiki["praw_test"].revisions():
- print(item)
+ Inserted media will have a padding of ``\\n\\n`` automatically added. This
+ is due to the weirdness with Reddit's API. Using the example above, the
+ result selftext body will look like so:
- """
- url = API_PATH["wiki_revisions"].format(subreddit=self.subreddit)
- return WikiPage._revision_generator(
- generator_kwargs=generator_kwargs, subreddit=self.subreddit, url=url
- )
+ .. code-block::
+ Text with a gif
-class ContributorRelationship(SubredditRelationship):
- r"""Provides methods to interact with a :class:`.Subreddit`'s contributors.
+ ![gif](u1rchuphryq51 "optional caption")
- Contributors are also known as approved submitters.
+ an image
- Contributors of a subreddit can be iterated through like so:
+ ![img](srnr8tshryq51 "optional caption")
- .. code-block:: python
+ and video
- for contributor in reddit.subreddit("test").contributor():
- print(contributor)
+ ![video](gmc7rvthryq51 "optional caption")
- """
+ inline
- def leave(self):
- """Abdicate the contributor position."""
- self.subreddit._reddit.post(
- API_PATH["leavecontributor"], data={"id": self.subreddit.fullname}
- )
+ .. note::
+ To submit a post to a subreddit with the ``"news"`` flair, you can get the
+ flair id like this:
-class ModeratorRelationship(SubredditRelationship):
- r"""Provides methods to interact with a :class:`.Subreddit`'s moderators.
+ .. code-block::
- Moderators of a subreddit can be iterated through like so:
+ choices = list(subreddit.flair.link_templates.user_selectable())
+ template_id = next(x for x in choices if x["flair_text"] == "news")["flair_template_id"]
+ subreddit.submit("title", flair_id=template_id, url="https://www.news.com/")
- .. code-block:: python
+ .. seealso::
- for moderator in reddit.subreddit("test").moderator():
- print(moderator)
+ - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
+ same post
+ - :meth:`~.Subreddit.submit_image` to submit images
+ - :meth:`~.Subreddit.submit_poll` to submit polls
+ - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
- """
+ """
+ if (bool(selftext) or selftext == "") == bool(url):
+ msg = "Either 'selftext' or 'url' must be provided."
+ raise TypeError(msg)
- PERMISSIONS = {
- "access",
- "chat_config",
- "chat_operator",
- "config",
- "flair",
- "mail",
- "posts",
- "wiki",
- }
+ data = {
+ "sr": str(self),
+ "resubmit": bool(resubmit),
+ "sendreplies": bool(send_replies),
+ "title": title,
+ "nsfw": bool(nsfw),
+ "spoiler": bool(spoiler),
+ "validate_on_submit": self._reddit.validate_on_submit,
+ }
+ for key, value in (
+ ("flair_id", flair_id),
+ ("flair_text", flair_text),
+ ("collection_id", collection_id),
+ ("discussion_type", discussion_type),
+ ("draft_id", draft_id),
+ ):
+ if value is not None:
+ data[key] = value
+ if selftext is not None:
+ data.update(kind="self")
+ if inline_media:
+ body = selftext.format(
+ **{
+ placeholder: self._upload_inline_media(media)
+ for placeholder, media in inline_media.items()
+ }
+ )
+ converted = self._convert_to_fancypants(body)
+ data.update(richtext_json=dumps(converted))
+ else:
+ data.update(text=selftext)
+ else:
+ data.update(kind="link", url=url)
- @staticmethod
- def _handle_permissions( # noqa: ANN205
+ return self._reddit.post(API_PATH["submit"], data=data)
+
+ @_deprecate_args(
+ "title",
+ "images",
+ "collection_id",
+ "discussion_type",
+ "flair_id",
+ "flair_text",
+ "nsfw",
+ "send_replies",
+ "spoiler",
+ )
+ def submit_gallery(
+ self,
+ title: str,
+ images: list[dict[str, str]],
*,
- other_settings: dict | None = None,
- permissions: list[str] | None = None,
- ):
- other_settings = deepcopy(other_settings) if other_settings else {}
- other_settings["permissions"] = permissions_string(
- known_permissions=ModeratorRelationship.PERMISSIONS, permissions=permissions
- )
- return other_settings
+ collection_id: str | None = None,
+ discussion_type: str | None = None,
+ flair_id: str | None = None,
+ flair_text: str | None = None,
+ nsfw: bool = False,
+ send_replies: bool = True,
+ spoiler: bool = False,
+ ) -> praw.models.Submission:
+ """Add an image gallery submission to the subreddit.
+
+ :param title: The title of the submission.
+ :param images: The images to post in dict with the following structure:
+ ``{"image_path": "path", "caption": "caption", "outbound_url": "url"}``,
+ only ``image_path`` is required.
+ :param collection_id: The UUID of a :class:`.Collection` to add the
+ newly-submitted post to.
+ :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
+ traditional comments (default: ``None``).
+ :param flair_id: The flair template to select (default: ``None``).
+ :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
+ this value will set a custom text (default: ``None``). ``flair_id`` is
+ required when ``flair_text`` is provided.
+ :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
+ :param send_replies: When ``True``, messages will be sent to the submission
+ author when comments are made to the submission (default: ``True``).
+ :param spoiler: Whether the submission should be marked asa spoiler (default:
+ ``False``).
+
+ :returns: A :class:`.Submission` object for the newly created submission.
+
+ :raises: :class:`.ClientException` if ``image_path`` in ``images`` refers to a
+ file that is not an image.
+
+ For example, to submit an image gallery to r/test do:
+
+ .. code-block:: python
+
+ title = "My favorite pictures"
+ image = "/path/to/image.png"
+ image2 = "/path/to/image2.png"
+ image3 = "/path/to/image3.png"
+ images = [
+ {"image_path": image},
+ {
+ "image_path": image2,
+ "caption": "Image caption 2",
+ },
+ {
+ "image_path": image3,
+ "caption": "Image caption 3",
+ "outbound_url": "https://example.com/link3",
+ },
+ ]
+ reddit.subreddit("test").submit_gallery(title, images)
+
+ .. seealso::
+
+ - :meth:`~.Subreddit.submit` to submit url posts and selftexts
+ - :meth:`~.Subreddit.submit_image` to submit single images
+ - :meth:`~.Subreddit.submit_poll` to submit polls
+ - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
+
+ """
+ self._validate_gallery(images)
+ data = {
+ "api_type": "json",
+ "items": [],
+ "nsfw": bool(nsfw),
+ "sendreplies": bool(send_replies),
+ "show_error_list": True,
+ "spoiler": bool(spoiler),
+ "sr": str(self),
+ "title": title,
+ "validate_on_submit": self._reddit.validate_on_submit,
+ }
+ for key, value in (
+ ("flair_id", flair_id),
+ ("flair_text", flair_text),
+ ("collection_id", collection_id),
+ ("discussion_type", discussion_type),
+ ):
+ if value is not None:
+ data[key] = value
+ for image in images:
+ data["items"].append(
+ {
+ "caption": image.get("caption", ""),
+ "outbound_url": image.get("outbound_url", ""),
+ "media_id": self._upload_media(
+ expected_mime_prefix="image",
+ media_path=image["image_path"],
+ upload_type="gallery",
+ ),
+ }
+ )
+ response = self._reddit.request(
+ json=data, method="POST", path=API_PATH["submit_gallery_post"]
+ )["json"]
+ if response["errors"]:
+ raise RedditAPIException(response["errors"])
+ return self._reddit.submission(url=response["data"]["url"])
- def __call__(
- self, redditor: str | praw.models.Redditor | None = None
- ) -> list[praw.models.Redditor]: # pylint: disable=arguments-differ
- r"""Return a list of :class:`.Redditor`\ s who are moderators.
+ @_deprecate_args(
+ "title",
+ "image_path",
+ "flair_id",
+ "flair_text",
+ "resubmit",
+ "send_replies",
+ "nsfw",
+ "spoiler",
+ "timeout",
+ "collection_id",
+ "without_websockets",
+ "discussion_type",
+ )
+ def submit_image(
+ self,
+ title: str,
+ image_path: str,
+ *,
+ collection_id: str | None = None,
+ discussion_type: str | None = None,
+ flair_id: str | None = None,
+ flair_text: str | None = None,
+ nsfw: bool = False,
+ resubmit: bool = True,
+ send_replies: bool = True,
+ spoiler: bool = False,
+ timeout: int = 10,
+ without_websockets: bool = False,
+ ) -> praw.models.Submission | None:
+ """Add an image submission to the subreddit.
- :param redditor: When provided, return a list containing at most one
- :class:`.Redditor` instance. This is useful to confirm if a relationship
- exists, or to fetch the metadata associated with a particular relationship
- (default: ``None``).
+ :param collection_id: The UUID of a :class:`.Collection` to add the
+ newly-submitted post to.
+ :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
+ traditional comments (default: ``None``).
+ :param flair_id: The flair template to select (default: ``None``).
+ :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
+ this value will set a custom text (default: ``None``). ``flair_id`` is
+ required when ``flair_text`` is provided.
+ :param image_path: The path to an image, to upload and post.
+ :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
+ :param resubmit: When ``False``, an error will occur if the URL has already been
+ submitted (default: ``True``).
+ :param send_replies: When ``True``, messages will be sent to the submission
+ author when comments are made to the submission (default: ``True``).
+ :param spoiler: Whether the submission should be marked as a spoiler (default:
+ ``False``).
+ :param timeout: Specifies a particular timeout, in seconds. Use to avoid
+ "Websocket error" exceptions (default: ``10``).
+ :param title: The title of the submission.
+ :param without_websockets: Set to ``True`` to disable use of WebSockets (see
+ note below for an explanation). If ``True``, this method doesn't return
+ anything (default: ``False``).
- .. note::
+ :returns: A :class:`.Submission` object for the newly created submission, unless
+ ``without_websockets`` is ``True``.
- To help mitigate targeted moderator harassment, this call requires the
- :class:`.Reddit` instance to be authenticated i.e., :attr:`.read_only` must
- return ``False``. This call, however, only makes use of the ``read`` scope.
- For more information on why the moderator list is hidden can be found here:
- https://reddit.zendesk.com/hc/en-us/articles/360049499032-Why-is-the-moderator-list-hidden-
+ :raises: :class:`.ClientException` if ``image_path`` refers to a file that is
+ not an image.
.. note::
- Unlike other relationship callables, this relationship is not paginated.
- Thus it simply returns the full list, rather than an iterator for the
- results.
+ Reddit's API uses WebSockets to respond with the link of the newly created
+ post. If this fails, the method will raise :class:`.WebSocketException`.
+ Occasionally, the Reddit post will still be created. More often, there is an
+ error with the image file. If you frequently get exceptions but successfully
+ created posts, try setting the ``timeout`` parameter to a value above 10.
- To be used like:
+ To disable the use of WebSockets, set ``without_websockets=True``. This will
+ make the method return ``None``, though the post will still be created. You
+ may wish to do this if you are running your program in a restricted network
+ environment, or using a proxy that doesn't support WebSockets connections.
- .. code-block:: python
+ For example, to submit an image to r/test do:
- moderators = reddit.subreddit("test").moderator()
+ .. code-block:: python
- For example, to list the moderators along with their permissions try:
+ title = "My favorite picture"
+ image = "/path/to/image.png"
+ reddit.subreddit("test").submit_image(title, image)
- .. code-block:: python
+ .. seealso::
- for moderator in reddit.subreddit("test").moderator():
- print(f"{moderator}: {moderator.mod_permissions}")
+ - :meth:`~.Subreddit.submit` to submit url posts and selftexts
+ - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
+ same post
+ - :meth:`~.Subreddit.submit_poll` to submit polls
+ - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
"""
- params = {} if redditor is None else {"user": redditor}
- url = API_PATH[f"list_{self.relationship}"].format(subreddit=self.subreddit)
- return self.subreddit._reddit.get(url, params=params)
+ data = {
+ "sr": str(self),
+ "resubmit": bool(resubmit),
+ "sendreplies": bool(send_replies),
+ "title": title,
+ "nsfw": bool(nsfw),
+ "spoiler": bool(spoiler),
+ "validate_on_submit": self._reddit.validate_on_submit,
+ }
+ for key, value in (
+ ("flair_id", flair_id),
+ ("flair_text", flair_text),
+ ("collection_id", collection_id),
+ ("discussion_type", discussion_type),
+ ):
+ if value is not None:
+ data[key] = value
- # pylint: disable=arguments-differ
- @_deprecate_args("redditor", "permissions")
- def add(
+ image_url = self._upload_media(
+ expected_mime_prefix="image", media_path=image_path
+ )
+ data.update(kind="image", url=image_url)
+ return self._submit_media(
+ data=data, timeout=timeout, without_websockets=without_websockets
+ )
+
+ @_deprecate_args(
+ "title",
+ "selftext",
+ "options",
+ "duration",
+ "flair_id",
+ "flair_text",
+ "resubmit",
+ "send_replies",
+ "nsfw",
+ "spoiler",
+ "collection_id",
+ "discussion_type",
+ )
+ def submit_poll(
self,
- redditor: str | praw.models.Redditor,
+ title: str,
*,
- permissions: list[str] | None = None,
- **other_settings: Any,
- ):
- """Add or invite ``redditor`` to be a moderator of the :class:`.Subreddit`.
+ collection_id: str | None = None,
+ discussion_type: str | None = None,
+ duration: int,
+ flair_id: str | None = None,
+ flair_text: str | None = None,
+ nsfw: bool = False,
+ options: list[str],
+ resubmit: bool = True,
+ selftext: str,
+ send_replies: bool = True,
+ spoiler: bool = False,
+ ) -> praw.models.Submission:
+ """Add a poll submission to the subreddit.
- :param redditor: A redditor name or :class:`.Redditor` instance.
- :param permissions: When provided (not ``None``), permissions should be a list
- of strings specifying which subset of permissions to grant. An empty list
- ``[]`` indicates no permissions, and when not provided ``None``, indicates
- full permissions (default: ``None``).
+ :param title: The title of the submission.
+ :param collection_id: The UUID of a :class:`.Collection` to add the
+ newly-submitted post to.
+ :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
+ traditional comments (default: ``None``).
+ :param duration: The number of days the poll should accept votes, as an ``int``.
+ Valid values are between ``1`` and ``7``, inclusive.
+ :param flair_id: The flair template to select (default: ``None``).
+ :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
+ this value will set a custom text (default: ``None``). ``flair_id`` is
+ required when ``flair_text`` is provided.
+ :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
+ :param options: A list of two to six poll options as ``str``.
+ :param resubmit: When ``False``, an error will occur if the URL has already been
+ submitted (default: ``True``).
+ :param selftext: The Markdown formatted content for the submission. Use an empty
+ string, ``""``, to make a submission with no text contents.
+ :param send_replies: When ``True``, messages will be sent to the submission
+ author when comments are made to the submission (default: ``True``).
+ :param spoiler: Whether the submission should be marked as a spoiler (default:
+ ``False``).
- An invite will be sent unless the user making this call is an admin user.
+ :returns: A :class:`.Submission` object for the newly created submission.
- For example, to invite u/spez with ``"posts"`` and ``"mail"`` permissions to
- r/test, try:
+ For example, to submit a poll to r/test do:
.. code-block:: python
- reddit.subreddit("test").moderator.add("spez", permissions=["posts", "mail"])
-
- """
- other_settings = self._handle_permissions(
- other_settings=other_settings, permissions=permissions
- )
- super().add(redditor, **other_settings)
-
- # pylint: enable=arguments-differ
- @_deprecate_args("redditor", "permissions")
- def invite(
- self,
- redditor: str | praw.models.Redditor,
- *,
- permissions: list[str] | None = None,
- **other_settings: Any,
- ):
- """Invite ``redditor`` to be a moderator of the :class:`.Subreddit`.
-
- :param redditor: A redditor name or :class:`.Redditor` instance.
- :param permissions: When provided (not ``None``), permissions should be a list
- of strings specifying which subset of permissions to grant. An empty list
- ``[]`` indicates no permissions, and when not provided ``None``, indicates
- full permissions (default: ``None``).
+ title = "Do you like PRAW?"
+ reddit.subreddit("test").submit_poll(
+ title, selftext="", options=["Yes", "No"], duration=3
+ )
- For example, to invite u/spez with ``"posts"`` and ``"mail"`` permissions to
- r/test, try:
+ .. seealso::
- .. code-block:: python
+ - :meth:`~.Subreddit.submit` to submit url posts and selftexts
+ - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
+ same post
+ - :meth:`~.Subreddit.submit_image` to submit single images
+ - :meth:`~.Subreddit.submit_video` to submit videos and videogifs
- reddit.subreddit("test").moderator.invite("spez", permissions=["posts", "mail"])
+ """
+ data = {
+ "sr": str(self),
+ "text": selftext,
+ "options": options,
+ "duration": duration,
+ "resubmit": bool(resubmit),
+ "sendreplies": bool(send_replies),
+ "title": title,
+ "nsfw": bool(nsfw),
+ "spoiler": bool(spoiler),
+ "validate_on_submit": self._reddit.validate_on_submit,
+ }
+ for key, value in (
+ ("flair_id", flair_id),
+ ("flair_text", flair_text),
+ ("collection_id", collection_id),
+ ("discussion_type", discussion_type),
+ ):
+ if value is not None:
+ data[key] = value
- """
- data = self._handle_permissions(
- other_settings=other_settings, permissions=permissions
- )
- data.update({"name": str(redditor), "type": "moderator_invite"})
- url = API_PATH["friend"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
+ return self._reddit.post(API_PATH["submit_poll_post"], json=data)
- @_deprecate_args("redditor")
- def invited(
+ @_deprecate_args(
+ "title",
+ "video_path",
+ "videogif",
+ "thumbnail_path",
+ "flair_id",
+ "flair_text",
+ "resubmit",
+ "send_replies",
+ "nsfw",
+ "spoiler",
+ "timeout",
+ "collection_id",
+ "without_websockets",
+ "discussion_type",
+ )
+ def submit_video(
self,
+ title: str,
+ video_path: str,
*,
- redditor: str | praw.models.Redditor | None = None,
- **generator_kwargs: Any,
- ) -> Iterator[praw.models.Redditor]:
- r"""Return a :class:`.ListingGenerator` for :class:`.Redditor`\ s invited to be moderators.
-
- :param redditor: When provided, return a list containing at most one
- :class:`.Redditor` instance. This is useful to confirm if a relationship
- exists, or to fetch the metadata associated with a particular relationship
- (default: ``None``).
+ collection_id: str | None = None,
+ discussion_type: str | None = None,
+ flair_id: str | None = None,
+ flair_text: str | None = None,
+ nsfw: bool = False,
+ resubmit: bool = True,
+ send_replies: bool = True,
+ spoiler: bool = False,
+ thumbnail_path: str | None = None,
+ timeout: int = 10,
+ videogif: bool = False,
+ without_websockets: bool = False,
+ ) -> praw.models.Submission | None:
+ """Add a video or videogif submission to the subreddit.
- Additional keyword arguments are passed in the initialization of
- :class:`.ListingGenerator`.
+ :param title: The title of the submission.
+ :param video_path: The path to a video, to upload and post.
+ :param collection_id: The UUID of a :class:`.Collection` to add the
+ newly-submitted post to.
+ :param discussion_type: Set to ``"CHAT"`` to enable live discussion instead of
+ traditional comments (default: ``None``).
+ :param flair_id: The flair template to select (default: ``None``).
+ :param flair_text: If the template's ``flair_text_editable`` value is ``True``,
+ this value will set a custom text (default: ``None``). ``flair_id`` is
+ required when ``flair_text`` is provided.
+ :param nsfw: Whether the submission should be marked NSFW (default: ``False``).
+ :param resubmit: When ``False``, an error will occur if the URL has already been
+ submitted (default: ``True``).
+ :param send_replies: When ``True``, messages will be sent to the submission
+ author when comments are made to the submission (default: ``True``).
+ :param spoiler: Whether the submission should be marked as a spoiler (default:
+ ``False``).
+ :param thumbnail_path: The path to an image, to be uploaded and used as the
+ thumbnail for this video. If not provided, the PRAW logo will be used as the
+ thumbnail.
+ :param timeout: Specifies a particular timeout, in seconds. Use to avoid
+ "Websocket error" exceptions (default: ``10``).
+ :param videogif: If ``True``, the video is uploaded as a videogif, which is
+ essentially a silent video (default: ``False``).
+ :param without_websockets: Set to ``True`` to disable use of WebSockets (see
+ note below for an explanation). If ``True``, this method doesn't return
+ anything (default: ``False``).
- .. note::
+ :returns: A :class:`.Submission` object for the newly created submission, unless
+ ``without_websockets`` is ``True``.
- Unlike other usages of :class:`.ListingGenerator`, ``limit`` has no effect
- in the quantity returned. This endpoint always returns moderators in batches
- of 25 at a time regardless of what ``limit`` is set to.
+ :raises: :class:`.ClientException` if ``video_path`` refers to a file that is
+ not a video.
- Usage:
+ .. note::
- .. code-block:: python
+ Reddit's API uses WebSockets to respond with the link of the newly created
+ post. If this fails, the method will raise :class:`.WebSocketException`.
+ Occasionally, the Reddit post will still be created. More often, there is an
+ error with the image file. If you frequently get exceptions but successfully
+ created posts, try setting the ``timeout`` parameter to a value above 10.
- for invited_mod in reddit.subreddit("test").moderator.invited():
- print(invited_mod)
+ To disable the use of WebSockets, set ``without_websockets=True``. This will
+ make the method return ``None``, though the post will still be created. You
+ may wish to do this if you are running your program in a restricted network
+ environment, or using a proxy that doesn't support WebSockets connections.
- """
- generator_kwargs["params"] = {"username": redditor} if redditor else None
- url = API_PATH["list_invited_moderator"].format(subreddit=self.subreddit)
- return ListingGenerator(self.subreddit._reddit, url, **generator_kwargs)
+ For example, to submit a video to r/test do:
- def leave(self):
- """Abdicate the moderator position (use with care).
+ .. code-block:: python
- For example:
+ title = "My favorite movie"
+ video = "/path/to/video.mp4"
+ reddit.subreddit("test").submit_video(title, video)
- .. code-block:: python
+ .. seealso::
- reddit.subreddit("test").moderator.leave()
+ - :meth:`~.Subreddit.submit` to submit url posts and selftexts
+ - :meth:`~.Subreddit.submit_image` to submit images
+ - :meth:`~.Subreddit.submit_gallery` to submit more than one image in the
+ same post
+ - :meth:`~.Subreddit.submit_poll` to submit polls
"""
- self.remove(
- self.subreddit._reddit.config.username or self.subreddit._reddit.user.me()
+ data = {
+ "sr": str(self),
+ "resubmit": bool(resubmit),
+ "sendreplies": bool(send_replies),
+ "title": title,
+ "nsfw": bool(nsfw),
+ "spoiler": bool(spoiler),
+ "validate_on_submit": self._reddit.validate_on_submit,
+ }
+ for key, value in (
+ ("flair_id", flair_id),
+ ("flair_text", flair_text),
+ ("collection_id", collection_id),
+ ("discussion_type", discussion_type),
+ ):
+ if value is not None:
+ data[key] = value
+
+ video_url = self._upload_media(
+ expected_mime_prefix="video", media_path=video_path
+ )
+ data.update(
+ kind="videogif" if videogif else "video",
+ url=video_url,
+ # if thumbnail_path is None, it uploads the PRAW logo
+ video_poster_url=self._upload_media(media_path=thumbnail_path),
+ )
+ return self._submit_media(
+ data=data, timeout=timeout, without_websockets=without_websockets
)
- def remove_invite(self, redditor: str | praw.models.Redditor):
- """Remove the moderator invite for ``redditor``.
+ @_deprecate_args("other_subreddits")
+ def subscribe(self, *, other_subreddits: list[praw.models.Subreddit] | None = None):
+ """Subscribe to the subreddit.
- :param redditor: A redditor name or :class:`.Redditor` instance.
+ :param other_subreddits: When provided, also subscribe to the provided list of
+ subreddits.
- For example:
+ For example, to subscribe to r/test:
.. code-block:: python
- reddit.subreddit("test").moderator.remove_invite("spez")
+ reddit.subreddit("test").subscribe()
"""
- data = {"name": str(redditor), "type": "moderator_invite"}
- url = API_PATH["unfriend"].format(subreddit=self.subreddit)
- self.subreddit._reddit.post(url, data=data)
+ data = {
+ "action": "sub",
+ "skip_inital_defaults": True,
+ "sr_name": self._subreddit_list(
+ other_subreddits=other_subreddits, subreddit=self
+ ),
+ }
+ self._reddit.post(API_PATH["subscribe"], data=data)
- @_deprecate_args("redditor", "permissions")
- def update(
- self,
- redditor: str | praw.models.Redditor,
- *,
- permissions: list[str] | None = None,
- ):
- """Update the moderator permissions for ``redditor``.
+ def traffic(self) -> dict[str, list[list[int]]]:
+ """Return a dictionary of the :class:`.Subreddit`'s traffic statistics.
- :param redditor: A redditor name or :class:`.Redditor` instance.
- :param permissions: When provided (not ``None``), permissions should be a list
- of strings specifying which subset of permissions to grant. An empty list
- ``[]`` indicates no permissions, and when not provided, ``None``, indicates
- full permissions (default: ``None``).
+ :raises: ``prawcore.NotFound`` when the traffic stats aren't available to the
+ authenticated user, that is, they are not public and the authenticated user
+ is not a moderator of the subreddit.
- For example, to add all permissions to the moderator, try:
+ The traffic method returns a dict with three keys. The keys are ``day``,
+ ``hour`` and ``month``. Each key contains a list of lists with 3 or 4 values.
+ The first value is a timestamp indicating the start of the category (start of
+ the day for the ``day`` key, start of the hour for the ``hour`` key, etc.). The
+ second, third, and fourth values indicate the unique pageviews, total pageviews,
+ and subscribers, respectively.
- .. code-block:: python
+ .. note::
- subreddit.moderator.update("spez")
+ The ``hour`` key does not contain subscribers, and therefore each sub-list
+ contains three values.
- To remove all permissions from the moderator, try:
+ For example, to get the traffic stats for r/test:
.. code-block:: python
- subreddit.moderator.update("spez", permissions=[])
+ stats = reddit.subreddit("test").traffic()
"""
- url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
- data = self._handle_permissions(
- other_settings={"name": str(redditor), "type": "moderator"},
- permissions=permissions,
- )
- self.subreddit._reddit.post(url, data=data)
+ return self._reddit.get(API_PATH["about_traffic"].format(subreddit=self))
- @_deprecate_args("redditor", "permissions")
- def update_invite(
- self,
- redditor: str | praw.models.Redditor,
- *,
- permissions: list[str] | None = None,
+ @_deprecate_args("other_subreddits")
+ def unsubscribe(
+ self, *, other_subreddits: list[praw.models.Subreddit] | None = None
):
- """Update the moderator invite permissions for ``redditor``.
+ """Unsubscribe from the subreddit.
- :param redditor: A redditor name or :class:`.Redditor` instance.
- :param permissions: When provided (not ``None``), permissions should be a list
- of strings specifying which subset of permissions to grant. An empty list
- ``[]`` indicates no permissions, and when not provided, ``None``, indicates
- full permissions (default: ``None``).
+ :param other_subreddits: When provided, also unsubscribe from the provided list
+ of subreddits.
- For example, to grant the ``"flair"`` and ``"mail"`` permissions to the
- moderator invite, try:
+ To unsubscribe from r/test:
.. code-block:: python
- subreddit.moderator.update_invite("spez", permissions=["flair", "mail"])
+ reddit.subreddit("test").unsubscribe()
"""
- url = API_PATH["setpermissions"].format(subreddit=self.subreddit)
- data = self._handle_permissions(
- other_settings={"name": str(redditor), "type": "moderator_invite"},
- permissions=permissions,
- )
- self.subreddit._reddit.post(url, data=data)
+ data = {
+ "action": "unsub",
+ "sr_name": self._subreddit_list(
+ other_subreddits=other_subreddits, subreddit=self
+ ),
+ }
+ self._reddit.post(API_PATH["subscribe"], data=data)
+
+
+WidgetEncoder._subreddit_class = Subreddit
class SubredditLinkFlairTemplates(SubredditFlairTemplates):
diff --git a/praw/models/reddit/user_subreddit.py b/praw/models/reddit/user_subreddit.py
index 47763d389..d99e3b056 100644
--- a/praw/models/reddit/user_subreddit.py
+++ b/praw/models/reddit/user_subreddit.py
@@ -2,7 +2,7 @@
from __future__ import annotations
import inspect
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Callable
from warnings import warn
from ...util.cache import cachedproperty
@@ -52,7 +52,7 @@ class UserSubreddit(Subreddit):
"""
@staticmethod
- def _dict_depreciated_wrapper(func): # noqa: ANN001,ANN205
+ def _dict_deprecated_wrapper(func: Callable) -> Callable:
"""Show deprecation notice for dict only methods."""
def wrapper(*args: Any, **kwargs: Any):
@@ -112,7 +112,7 @@ def predicate(item: str):
setattr(
self,
name,
- self._dict_depreciated_wrapper(getattr(self.__dict__, name)),
+ self._dict_deprecated_wrapper(getattr(self.__dict__, name)),
)
super().__init__(reddit, *args, **kwargs)
diff --git a/praw/models/reddit/widgets.py b/praw/models/reddit/widgets.py
index 79a8ba8ad..48230580a 100644
--- a/praw/models/reddit/widgets.py
+++ b/praw/models/reddit/widgets.py
@@ -301,12 +301,12 @@ def topbar(self) -> list[praw.models.Menu]:
self.items[widget_name] for widget_name in self.layout["topbar"]["order"]
]
- def __getattr__(self, attribute: str) -> Any:
+ def __getattr__(self, attr: str) -> Any:
"""Return the value of ``attr``."""
- if not attribute.startswith("_") and not self._fetched:
+ if not attr.startswith("_") and not self._fetched:
self._fetch()
- return getattr(self, attribute)
- msg = f"{self.__class__.__name__!r} object has no attribute {attribute!r}"
+ return getattr(self, attr)
+ msg = f"{self.__class__.__name__!r} object has no attribute {attr!r}"
raise AttributeError(msg)
def __init__(self, subreddit: praw.models.Subreddit):
@@ -326,7 +326,7 @@ def __repr__(self) -> str:
"""Return an object initialization representation of the instance."""
return f"SubredditWidgets(subreddit={self.subreddit!r})"
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._reddit.get(
API_PATH["widgets"].format(subreddit=self.subreddit),
params={"progressive_images": self.progressive_images},
@@ -387,7 +387,6 @@ def __eq__(self, other: object) -> bool:
return self.id.lower() == other.id.lower()
return str(other).lower() == self.id.lower()
- # pylint: disable=invalid-name
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
"""Initialize a :class:`.Widget` instance."""
self.subreddit = "" # in case it isn't in _data
@@ -399,7 +398,7 @@ def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
class WidgetEncoder(JSONEncoder):
"""Class to encode widget-related objects."""
- def default(self, o: Any) -> Any: # pylint: disable=E0202
+ def default(self, o: Any) -> Any:
"""Serialize ``PRAWBase`` objects."""
if isinstance(o, self._subreddit_class):
return str(o)
@@ -1109,6 +1108,85 @@ class TextArea(Widget):
"""
+class WidgetModeration:
+ """Class for moderating a particular widget.
+
+ Example usage:
+
+ .. code-block:: python
+
+ widget = reddit.subreddit("test").widgets.sidebar[0]
+ widget.mod.update(shortName="My new title")
+ widget.mod.delete()
+
+ """
+
+ def __init__(
+ self,
+ widget: Widget,
+ subreddit: praw.models.Subreddit | str,
+ reddit: praw.Reddit,
+ ):
+ """Initialize a :class:`.WidgetModeration` instance."""
+ self.widget = widget
+ self._reddit = reddit
+ self._subreddit = subreddit
+
+ def delete(self):
+ """Delete the widget.
+
+ Example usage:
+
+ .. code-block:: python
+
+ widget.mod.delete()
+
+ """
+ path = API_PATH["widget_modify"].format(
+ widget_id=self.widget.id, subreddit=self._subreddit
+ )
+ self._reddit.delete(path)
+
+ def update(self, **kwargs: Any) -> Widget:
+ """Update the widget. Returns the updated widget.
+
+ Parameters differ based on the type of widget. See `Reddit documentation
+ `_ or the document of
+ the particular type of widget.
+
+ :returns: The updated :class:`.Widget`.
+
+ For example, update a text widget like so:
+
+ .. code-block:: python
+
+ text_widget.mod.update(shortName="New text area", text="Hello!")
+
+ .. note::
+
+ Most parameters follow the ``lowerCamelCase`` convention. When in doubt,
+ check the Reddit documentation linked above.
+
+ """
+ path = API_PATH["widget_modify"].format(
+ widget_id=self.widget.id, subreddit=self._subreddit
+ )
+ payload = {
+ key: value
+ for key, value in vars(self.widget).items()
+ if not key.startswith("_")
+ }
+ del payload["subreddit"] # not JSON serializable
+ if "mod" in payload:
+ del payload["mod"]
+ payload.update(kwargs)
+ widget = self._reddit.put(
+ path, data={"json": dumps(payload, cls=WidgetEncoder)}
+ )
+ widget.subreddit = self._subreddit
+ return widget
+
+
class SubredditWidgetsModeration:
"""Class for moderating a :class:`.Subreddit`'s widgets.
@@ -1135,7 +1213,7 @@ def __init__(self, subreddit: praw.models.Subreddit, reddit: praw.Reddit):
self._subreddit = subreddit
self._reddit = reddit
- def _create_widget(self, payload: dict[str, Any]) -> WidgetType: # noqa: ANN001
+ def _create_widget(self, payload: dict[str, Any]) -> WidgetType:
path = API_PATH["widget_create"].format(subreddit=self._subreddit)
widget = self._reddit.post(
path, data={"json": dumps(payload, cls=WidgetEncoder)}
@@ -1147,7 +1225,7 @@ def _create_widget(self, payload: dict[str, Any]) -> WidgetType: # noqa: ANN001
def add_button_widget(
self,
*,
- buttons: list[dict[str, dict[str, str] | str | int | dict[str, str | int]]],
+ buttons: list[dict[str, dict[str, str | int] | str | int]],
description: str,
short_name: str,
styles: dict[str, str],
@@ -1790,82 +1868,3 @@ def upload_image(self, file_path: str) -> str:
response.raise_for_status()
return f"{upload_url}/{upload_data['key']}"
-
-
-class WidgetModeration:
- """Class for moderating a particular widget.
-
- Example usage:
-
- .. code-block:: python
-
- widget = reddit.subreddit("test").widgets.sidebar[0]
- widget.mod.update(shortName="My new title")
- widget.mod.delete()
-
- """
-
- def __init__(
- self,
- widget: Widget,
- subreddit: praw.models.Subreddit | str,
- reddit: praw.Reddit,
- ):
- """Initialize a :class:`.WidgetModeration` instance."""
- self.widget = widget
- self._reddit = reddit
- self._subreddit = subreddit
-
- def delete(self):
- """Delete the widget.
-
- Example usage:
-
- .. code-block:: python
-
- widget.mod.delete()
-
- """
- path = API_PATH["widget_modify"].format(
- widget_id=self.widget.id, subreddit=self._subreddit
- )
- self._reddit.delete(path)
-
- def update(self, **kwargs: Any) -> Widget:
- """Update the widget. Returns the updated widget.
-
- Parameters differ based on the type of widget. See `Reddit documentation
- `_ or the document of
- the particular type of widget.
-
- :returns: The updated :class:`.Widget`.
-
- For example, update a text widget like so:
-
- .. code-block:: python
-
- text_widget.mod.update(shortName="New text area", text="Hello!")
-
- .. note::
-
- Most parameters follow the ``lowerCamelCase`` convention. When in doubt,
- check the Reddit documentation linked above.
-
- """
- path = API_PATH["widget_modify"].format(
- widget_id=self.widget.id, subreddit=self._subreddit
- )
- payload = {
- key: value
- for key, value in vars(self.widget).items()
- if not key.startswith("_")
- }
- del payload["subreddit"] # not JSON serializable
- if "mod" in payload:
- del payload["mod"]
- payload.update(kwargs)
- widget = self._reddit.put(
- path, data={"json": dumps(payload, cls=WidgetEncoder)}
- )
- widget.subreddit = self._subreddit
- return widget
diff --git a/praw/models/reddit/wikipage.py b/praw/models/reddit/wikipage.py
index 18ac295a7..1bb46e897 100644
--- a/praw/models/reddit/wikipage.py
+++ b/praw/models/reddit/wikipage.py
@@ -239,7 +239,7 @@ def __str__(self) -> str:
"""Return a string representation of the instance."""
return f"{self.subreddit}/{self.name}"
- def _fetch(self): # noqa: ANN001
+ def _fetch(self):
data = self._fetch_data()
data = data["data"]
if data["revision_by"] is not None:
@@ -247,9 +247,9 @@ def _fetch(self): # noqa: ANN001
self._reddit, _data=data["revision_by"]["data"]
)
self.__dict__.update(data)
- self._fetched = True
+ super()._fetch()
- def _fetch_info(self): # noqa: ANN001
+ def _fetch_info(self):
return (
"wiki_page",
{"subreddit": self.subreddit, "page": self.name},
diff --git a/praw/models/subreddits.py b/praw/models/subreddits.py
index 715e603bf..e6306fd3b 100644
--- a/praw/models/subreddits.py
+++ b/praw/models/subreddits.py
@@ -19,7 +19,7 @@ class Subreddits(PRAWBase):
"""Subreddits is a Listing class that provides various subreddit lists."""
@staticmethod
- def _to_list(subreddit_list): # noqa: ANN001,ANN003,ANN205
+ def _to_list(subreddit_list: list[str | praw.models.Subreddit]) -> str:
return ",".join([str(x) for x in subreddit_list])
def default(
diff --git a/praw/models/trophy.py b/praw/models/trophy.py
index 8e8662de4..fc86671b0 100644
--- a/praw/models/trophy.py
+++ b/praw/models/trophy.py
@@ -54,4 +54,4 @@ def __repr__(self) -> str:
def __str__(self) -> str:
"""Return a name of the trophy."""
- return self.name # pylint: disable=no-member
+ return self.name
diff --git a/praw/models/user.py b/praw/models/user.py
index 5e3a15e62..4ae5d5b9c 100644
--- a/praw/models/user.py
+++ b/praw/models/user.py
@@ -133,9 +133,7 @@ def karma(self) -> dict[praw.models.Subreddit, dict[str, int]]:
return karma_map
@_deprecate_args("use_cache")
- def me(
- self, *, use_cache: bool = True
- ) -> praw.models.Redditor | None: # pylint: disable=invalid-name
+ def me(self, *, use_cache: bool = True) -> praw.models.Redditor | None:
"""Return a :class:`.Redditor` instance for the authenticated user.
:param use_cache: When ``True``, and if this function has been previously
diff --git a/praw/models/util.py b/praw/models/util.py
index 498898d08..d0907974e 100644
--- a/praw/models/util.py
+++ b/praw/models/util.py
@@ -189,7 +189,7 @@ def __init__(self, max_items: int):
self.max_items = max_items
self._set = OrderedDict()
- def _access(self, item: Any): # noqa: ANN001
+ def _access(self, item: Any):
if item in self._set:
self._set.move_to_end(item)
diff --git a/praw/objector.py b/praw/objector.py
index 3352d1777..68b3b29c4 100644
--- a/praw/objector.py
+++ b/praw/objector.py
@@ -59,7 +59,9 @@ def __init__(self, reddit: praw.Reddit, parsers: dict[str, Any] | None = None):
self.parsers = {} if parsers is None else parsers
self._reddit = reddit
- def _objectify_dict(self, data): # noqa: ANN001,PLR0912,PLR0915
+ def _objectify_dict( # noqa: PLR0912,PLR0915
+ self, data: dict[str:Any]
+ ) -> RedditBase:
"""Create :class:`.RedditBase` objects from dicts.
:param data: The structured data, assumed to be a dict.
@@ -223,7 +225,6 @@ def objectify( # noqa: PLR0911,PLR0912,PLR0915
``None``.
"""
- # pylint: disable=too-many-return-statements
if data is None: # 204 no content
return None
if isinstance(data, list):
diff --git a/praw/reddit.py b/praw/reddit.py
index ab3e0f200..9528d2a2d 100644
--- a/praw/reddit.py
+++ b/praw/reddit.py
@@ -82,7 +82,7 @@ class Reddit:
_ratelimit_regex = re.compile(r"([0-9]{1,3}) (milliseconds?|seconds?|minutes?)")
@property
- def _next_unique(self) -> int: # noqa: ANN001
+ def _next_unique(self) -> int:
value = self._unique_counter
self._unique_counter += 1
return value
@@ -139,7 +139,7 @@ def __enter__(self): # noqa: ANN204
"""Handle the context manager open."""
return self
- def __exit__(self, *_args):
+ def __exit__(self, *_: object):
"""Handle the context manager close."""
@_deprecate_args(
@@ -158,7 +158,7 @@ def __init__(
requestor_kwargs: dict[str, Any] | None = None,
token_manager: BaseTokenManager | None = None,
**config_settings: str | bool | int | None,
- ): # noqa: D207, D301
+ ):
"""Initialize a :class:`.Reddit` instance.
:param site_name: The name of a section in your ``praw.ini`` file from which to
@@ -436,7 +436,7 @@ def request(self, *args, **kwargs):
"""
- def _check_for_async(self): # noqa: ANN001
+ def _check_for_async(self):
if self.config.check_for_async: # pragma: no cover
try:
shell = get_ipython().__class__.__name__
@@ -448,10 +448,8 @@ def _check_for_async(self): # noqa: ANN001
try:
asyncio.get_running_loop()
in_async = True
- except ( # noqa: S110: Quietly fail if any exception occurs during the check
- Exception # noqa: BLE001
- ):
- pass
+ except Exception: # noqa: BLE001,S110
+ pass # Quietly fail if any exception occurs during the check
if in_async:
logger.warning(
"It appears that you are using PRAW in an asynchronous"
@@ -462,7 +460,7 @@ def _check_for_async(self): # noqa: ANN001
" for more info.\n",
)
- def _check_for_update(self): # noqa: ANN001
+ def _check_for_update(self):
if UPDATE_CHECKER_MISSING:
return
if not Reddit.update_checked and self.config.check_for_updates:
@@ -520,7 +518,9 @@ def _objectify_request(
)
)
- def _prepare_common_authorizer(self, authenticator): # noqa: ANN001
+ def _prepare_common_authorizer(
+ self, authenticator: prawcore.auth.BaseAuthenticator
+ ):
if self._token_manager is not None:
warn(
"Token managers have been deprecated and will be removed in the near"
@@ -548,7 +548,7 @@ def _prepare_common_authorizer(self, authenticator): # noqa: ANN001
return
self._core = self._authorized_core = session(authorizer)
- def _prepare_objector(self): # noqa: ANN001
+ def _prepare_objector(self):
mappings = {
self.config.kinds["comment"]: models.Comment,
self.config.kinds["message"]: models.Message,
@@ -597,7 +597,10 @@ def _prepare_objector(self): # noqa: ANN001
self._objector = Objector(self, mappings)
def _prepare_prawcore(
- self, *, requestor_class=None, requestor_kwargs=None # noqa: ANN001
+ self,
+ *,
+ requestor_class: type[prawcore.requestor.Requestor] = None,
+ requestor_kwargs: Any | None = None,
):
requestor_class = requestor_class or Requestor
requestor_kwargs = requestor_kwargs or {}
@@ -614,7 +617,7 @@ def _prepare_prawcore(
else:
self._prepare_untrusted_prawcore(requestor)
- def _prepare_trusted_prawcore(self, requestor): # noqa: ANN001
+ def _prepare_trusted_prawcore(self, requestor: prawcore.requestor.Requestor):
authenticator = TrustedAuthenticator(
requestor,
self.config.client_id,
@@ -632,7 +635,7 @@ def _prepare_trusted_prawcore(self, requestor): # noqa: ANN001
else:
self._prepare_common_authorizer(authenticator)
- def _prepare_untrusted_prawcore(self, requestor): # noqa: ANN001
+ def _prepare_untrusted_prawcore(self, requestor: prawcore.requestor.Requestor):
authenticator = UntrustedAuthenticator(
requestor, self.config.client_id, self.config.redirect_uri
)
@@ -642,10 +645,7 @@ def _prepare_untrusted_prawcore(self, requestor): # noqa: ANN001
@_deprecate_args("id", "url")
def comment(
- self, # pylint: disable=invalid-name
- id: str | None = None, # pylint: disable=redefined-builtin noqa: A002
- *,
- url: str | None = None,
+ self, id: str | None = None, *, url: str | None = None
) -> models.Comment:
"""Return a lazy instance of :class:`.Comment`.
@@ -849,8 +849,9 @@ def post(
if seconds is None:
break
second_string = "second" if seconds == 1 else "seconds"
- stmt = f"Rate limit hit, sleeping for {seconds} {second_string}"
- logger.debug(stmt)
+ logger.debug(
+ "Rate limit hit, sleeping for %d %s", seconds, second_string
+ )
time.sleep(seconds)
raise last_exception
@@ -965,7 +966,7 @@ def request(
) from exception
@_deprecate_args("id", "url")
- def submission( # pylint: disable=invalid-name,redefined-builtin
+ def submission(
self, id: str | None = None, *, url: str | None = None
) -> praw.models.Submission:
"""Return a lazy instance of :class:`.Submission`.
diff --git a/praw/util/__init__.py b/praw/util/__init__.py
index 5b7891699..064b8ff73 100644
--- a/praw/util/__init__.py
+++ b/praw/util/__init__.py
@@ -1,5 +1,5 @@
"""Package imports for utilities."""
-from .cache import cachedproperty # noqa: F401
-from .deprecate_args import _deprecate_args # noqa: F401
-from .snake import camel_to_snake, snake_case_keys # noqa: F401
+from .cache import cachedproperty
+from .deprecate_args import _deprecate_args
+from .snake import camel_to_snake, snake_case_keys
diff --git a/praw/util/cache.py b/praw/util/cache.py
index bf771b5b7..acd777617 100644
--- a/praw/util/cache.py
+++ b/praw/util/cache.py
@@ -22,13 +22,10 @@ class cachedproperty: # noqa: N801
"""
# This to make sphinx run properly
- # noqa: D102
def __call__(self, *args: Any, **kwargs: Any): # pragma: no cover
"""Empty method to make sphinx run properly."""
- def __get__(
- self, obj: Any | None, objtype: Any | None = None
- ) -> Any: # noqa: ANN401
+ def __get__(self, obj: Any | None, objtype: Any | None = None) -> Any:
"""Implement descriptor getter.
Calculate the property's value and then store it in the associated object's
diff --git a/praw/util/deprecate_args.py b/praw/util/deprecate_args.py
index e9be7c50c..283991d09 100644
--- a/praw/util/deprecate_args.py
+++ b/praw/util/deprecate_args.py
@@ -7,8 +7,8 @@
from warnings import warn
-def _deprecate_args(*old_args: str): # noqa: ANN001
- def _generate_arg_string(used_args: tuple[str, ...]) -> str: # noqa: ANN001
+def _deprecate_args(*old_args: str) -> Callable:
+ def _generate_arg_string(used_args: tuple[str, ...]) -> str:
used_args = list(map(repr, used_args))
arg_count = len(used_args)
arg_string = (
diff --git a/praw/util/token_manager.py b/praw/util/token_manager.py
index 8b49a2571..843b06892 100644
--- a/praw/util/token_manager.py
+++ b/praw/util/token_manager.py
@@ -64,7 +64,7 @@ def reddit(self, value: praw.Reddit):
raise RuntimeError(msg)
self._reddit = value
- def __init__(self) -> None:
+ def __init__(self):
"""Initialize a :class:`.BaseTokenManager` instance."""
self._reddit = None
@@ -120,7 +120,7 @@ class SQLiteTokenManager(BaseTokenManager):
"""
@_deprecate_args("database", "key")
- def __init__(self, *, database: str, key: str) -> None:
+ def __init__(self, *, database: str, key: str):
"""Initialize a :class:`.SQLiteTokenManager` instance.
:param database: The path to the SQLite database.
@@ -143,7 +143,7 @@ def __init__(self, *, database: str, key: str) -> None:
self._connection.commit()
self.key = key
- def _get(self): # noqa: ANN001
+ def _get(self):
cursor = self._connection.execute(
"SELECT refresh_token FROM tokens WHERE id=?", (self.key,)
)
@@ -152,7 +152,7 @@ def _get(self): # noqa: ANN001
raise KeyError
return result[0]
- def _set(self, refresh_token): # noqa: ANN001
+ def _set(self, refresh_token: str):
"""Set the refresh token in the database.
This function will overwrite an existing value if the corresponding ``key``
@@ -166,7 +166,7 @@ def _set(self, refresh_token): # noqa: ANN001
self._connection.commit()
def is_registered(self) -> bool:
- """Return whether or not ``key`` already has a ``refresh_token``."""
+ """Return whether ``key`` already has a ``refresh_token``."""
cursor = self._connection.execute(
"SELECT refresh_token FROM tokens WHERE id=?", (self.key,)
)
diff --git a/pyproject.toml b/pyproject.toml
index a38b70149..38a7ae3b6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -91,6 +91,7 @@ ignore = [
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"E501", # line-length
+ "N818", # exception name should be named with an Error suffix
"PLR0913", # too many arguments
"PLR2004", # Magic value used in comparison,
"S101" # use of assert
diff --git a/tests/integration/models/reddit/test_submission.py b/tests/integration/models/reddit/test_submission.py
index e9ee13a41..81abd61fb 100644
--- a/tests/integration/models/reddit/test_submission.py
+++ b/tests/integration/models/reddit/test_submission.py
@@ -314,14 +314,14 @@ def test_choices(self, reddit):
expected = [
{
"flair_text": "SATISFIED",
- "flair_template_id": "680f43b8-1fec-11e3-80d1-12313b0b80bc", # noqa:E501
+ "flair_template_id": "680f43b8-1fec-11e3-80d1-12313b0b80bc",
"flair_text_editable": False,
"flair_position": "left",
"flair_css_class": "",
},
{
"flair_text": "STATS",
- "flair_template_id": "16cabd0a-a68d-11e5-8349-0e8ff96e6679", # noqa:E501
+ "flair_template_id": "16cabd0a-a68d-11e5-8349-0e8ff96e6679",
"flair_text_editable": False,
"flair_position": "left",
"flair_css_class": "",
diff --git a/tools/set_version.py b/tools/set_version.py
index d870b0d4e..4de0431fb 100755
--- a/tools/set_version.py
+++ b/tools/set_version.py
@@ -7,7 +7,7 @@
CHANGELOG_HEADER = (
"Change Log\n==========\n\n"
- "PRAW follows `semantic versioning `_.\n\n"
+ "PRAW follows `semantic versioning `_.\n\n"
)
UNRELEASED_HEADER = "Unreleased\n----------\n\n"
diff --git a/tox.ini b/tox.ini
index 5f14cf811..f5d620c45 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,11 +7,3 @@ deps =
.[test]
commands =
pytest
-passenv =
- prawtest_client_id
- prawtest_client_secret
- prawtest_password
- prawtest_refresh_token
- prawtest_test_subreddit
- prawtest_username
- prawtest_user_agent