From ff844c42c7f902b13960cdecd2215fc94faa2845 Mon Sep 17 00:00:00 2001 From: Connor Colabella <43245845+redowul@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:36:47 -0400 Subject: [PATCH 01/21] Add support for bytes images to subreddit.py Files modified: - submit_image - submit_gallery - _validate_gallery - _upload_media - _read_and_post_media Added new import: - from io import BytesIO Previously, to submit an image to Reddit a filepath to an image stored on the local machine needed to be provided to PRAW to pass through to the Reddit API. This fork changes this behavior by implementing support for bytes objects through the io library. What this means in practical terms: in the original code, the filepath is referenced to retrieve the image file and convert it to bytes format anyway. Now the bytes of an image can be directly passed through while still retaining the original filepath functionality. This means an image retrieved from a server does not need to be downloaded before it can be uploaded: it's String implementation can be passed along to Reddit instead. --- praw/models/reddit/subreddit.py | 60 +++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 4bd209c9d..16f40cc14 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -11,6 +11,7 @@ from urllib.parse import urljoin from warnings import warn from xml.etree.ElementTree import XML +from io import BytesIO import websocket from prawcore import Redirect @@ -215,7 +216,12 @@ def _validate_gallery(images): if not isfile(image_path): raise TypeError(f"{image_path!r} is not a valid image path.") else: - raise TypeError("'image_path' is required.") + image_bytes = image.get("image_bytes") + if image_bytes: + if not isinstance(image_bytes, bytes): + raise TypeError("image_bytes String to bytes object conversion failed.") + else: + raise TypeError("'image_path' or 'image_bytes' are required.") if not len(image.get("caption", "")) <= 180: raise TypeError("Caption must be 180 characters or less.") @@ -643,20 +649,27 @@ def _submit_media( url = ws_update["payload"]["redirect"] return self._reddit.submission(url=url) - def _read_and_post_media(self, media_path, upload_url, upload_data): - with open(media_path, "rb") as media: - response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files={"file": media} - ) + def _read_and_post_media(self, media_path, upload_url, upload_data, image_bytes): + if media_path is not None: + with open(media_path, "rb") as media_data: + media = media_data + elif image_bytes is not None: + media = BytesIO(image_bytes) + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files={"file": media} + ) return response def _upload_media( self, *, expected_mime_prefix: Optional[str] = None, - media_path: str, + media_path: Optional[str], + image_bytes: Optional[bytes], + file_name: Optional[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 @@ -669,13 +682,13 @@ def _upload_media( finished, or it can be ignored. """ - if media_path is None: + + if media_path is None and image_bytes is None: media_path = join( dirname(dirname(dirname(__file__))), "images", "PRAW logo.png" ) - - file_name = basename(media_path).lower() - file_extension = file_name.rpartition(".")[2] + file_name = basename(media_path).lower() + file_extension = file_name.rpartition(".")[2] mime_type = { "png": "image/png", "mov": "video/quicktime", @@ -703,7 +716,7 @@ def _upload_media( upload_url = f"https:{upload_lease['action']}" upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]} - response = self._read_and_post_media(media_path, upload_url, upload_data) + response = self._read_and_post_media(media_path, upload_url, upload_data, image_bytes) if not response.ok: self._parse_xml_response(response) try: @@ -1040,8 +1053,8 @@ def submit( def submit_gallery( self, title: str, - images: List[Dict[str, str]], *, + images: List[Dict[str, str]] = None, collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1132,7 +1145,9 @@ def submit_gallery( "outbound_url": image.get("outbound_url", ""), "media_id": self._upload_media( expected_mime_prefix="image", - media_path=image["image_path"], + media_path=image.get("image_path"), + image_bytes=image.get("image_bytes"), + file_name=image.get("file_name"), upload_type="gallery", )[0], } @@ -1162,8 +1177,10 @@ def submit_gallery( def submit_image( self, title: str, - image_path: str, *, + image_path: Optional[str] = None, + image_bytes: Optional[bytes], + file_name: Optional[str], collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1255,8 +1272,9 @@ def submit_image( data[key] = value image_url, websocket_url = self._upload_media( - expected_mime_prefix="image", media_path=image_path + expected_mime_prefix="image", media_path=image_path, image_bytes=image_bytes, file_name=file_name ) + data.update(kind="image", url=image_url) if without_websockets: websocket_url = None @@ -2230,7 +2248,7 @@ def add( .. code-block:: python - reddit.subreddit("test").flair.link_templates.add( + reddit.subreddit("test").flair.templates.add( "PRAW", css_class="praw", text_editable=True, @@ -3146,14 +3164,6 @@ def __call__( exists, or to fetch the metadata associated with a particular relationship (default: ``None``). - .. note:: - - 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- - .. note:: Unlike other relationship callables, this relationship is not paginated. From e75488d72836df01b6ac29fe32dc4e7862c181c0 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 26 Jul 2022 14:54:47 -0400 Subject: [PATCH 02/21] Added bytes image support --- AUTHORS.rst | 1 + praw/models/reddit/subreddit.py | 96 ++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 44 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index fe2d0de32..51051efbe 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -81,4 +81,5 @@ Source Contributors - Josh Kim `@jsk56143 `_ - Rolf Campbell `@endlisnis `_ - zacc `@zacc `_ +- Connor Colabella `@redowul `_ - Add "Name and github profile link" above this line. diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 16f40cc14..86a435710 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -4,16 +4,16 @@ import socket from copy import deepcopy from csv import writer -from io import StringIO +from io import BytesIO, StringIO from json import dumps, loads from os.path import basename, dirname, isfile, join from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Union from urllib.parse import urljoin from warnings import warn from xml.etree.ElementTree import XML -from io import BytesIO import websocket +from PIL import Image from prawcore import Redirect from prawcore.exceptions import ServerError from requests.exceptions import HTTPError @@ -211,17 +211,17 @@ def _subreddit_list(*, other_subreddits, subreddit): @staticmethod def _validate_gallery(images): for image in images: - image_path = image.get("image_path", "") - if image_path: - if not isfile(image_path): - raise TypeError(f"{image_path!r} is not a valid image path.") + image_data = image.get("image", "") + if image_data: + if isinstance(image_data, str): + if not isfile(image_data): + raise ValueError(f"{image_data!r} is not a valid file path.") + elif not isinstance(image_data, bytes): + raise TypeError( + "'image' dictionary value contains an invalid bytes object." + ) # do not log bytes value, it is long and not human readable else: - image_bytes = image.get("image_bytes") - if image_bytes: - if not isinstance(image_bytes, bytes): - raise TypeError("image_bytes String to bytes object conversion failed.") - else: - raise TypeError("'image_path' or 'image_bytes' are required.") + raise TypeError("'image' dictionary value cannot be null.") if not len(image.get("caption", "")) <= 180: raise TypeError("Caption must be 180 characters or less.") @@ -649,27 +649,26 @@ def _submit_media( url = ws_update["payload"]["redirect"] return self._reddit.submission(url=url) - def _read_and_post_media(self, media_path, upload_url, upload_data, image_bytes): - if media_path is not None: - with open(media_path, "rb") as media_data: - media = media_data - elif image_bytes is not None: - media = BytesIO(image_bytes) - response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files={"file": media} - ) + def _read_and_post_media(self, media, upload_url, upload_data): + if isfile(media): + with open(media, "rb") as media_data: + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files={"file": media_data} + ) + elif isinstance(media, bytes): + media = BytesIO(media) + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files={"file": media} + ) return response def _upload_media( self, *, expected_mime_prefix: Optional[str] = None, - media_path: Optional[str], - image_bytes: Optional[bytes], - file_name: Optional[str], + media: 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 @@ -682,13 +681,14 @@ def _upload_media( finished, or it can be ignored. """ + if media is None: + media = join(dirname(dirname(dirname(__file__))), "images", "PRAW logo.png") + if isfile(media): + file_name = basename(media).lower() + file_extension = file_name.rpartition(".")[2] + elif isinstance(media, bytes): + file_extension = Image.open(BytesIO(media)).format.lower() - if media_path is None and image_bytes is None: - media_path = join( - dirname(dirname(dirname(__file__))), "images", "PRAW logo.png" - ) - file_name = basename(media_path).lower() - file_extension = file_name.rpartition(".")[2] mime_type = { "png": "image/png", "mov": "video/quicktime", @@ -707,6 +707,8 @@ def _upload_media( f"Expected a mimetype starting with {expected_mime_prefix!r} but got" f" mimetype {mime_type!r} (from file extension {file_extension!r})." ) + if isinstance(media, bytes): + file_name = mime_type.split("/")[0] + "." + mime_type.split("/")[1] img_data = {"filepath": file_name, "mimetype": mime_type} url = API_PATH["media_asset"] @@ -716,7 +718,7 @@ def _upload_media( upload_url = f"https:{upload_lease['action']}" upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]} - response = self._read_and_post_media(media_path, upload_url, upload_data, image_bytes) + response = self._read_and_post_media(media, upload_url, upload_data) if not response.ok: self._parse_xml_response(response) try: @@ -1053,8 +1055,8 @@ def submit( def submit_gallery( self, title: str, - *, images: List[Dict[str, str]] = None, + *, collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1067,8 +1069,9 @@ def submit_gallery( :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. + ``{"image": "path", "caption": "caption", "outbound_url": "url"}``, only + ``image`` is required. ``image`` can refer to either an image path or a + bytes object representing an image. :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 @@ -1145,9 +1148,7 @@ def submit_gallery( "outbound_url": image.get("outbound_url", ""), "media_id": self._upload_media( expected_mime_prefix="image", - media_path=image.get("image_path"), - image_bytes=image.get("image_bytes"), - file_name=image.get("file_name"), + media=image.get("image"), upload_type="gallery", )[0], } @@ -1177,10 +1178,8 @@ def submit_gallery( def submit_image( self, title: str, + image: str, *, - image_path: Optional[str] = None, - image_bytes: Optional[bytes], - file_name: Optional[str], collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1202,7 +1201,8 @@ def submit_image( :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 image: Either the path to an image or a bytes object representing 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``). @@ -1272,7 +1272,7 @@ def submit_image( data[key] = value image_url, websocket_url = self._upload_media( - expected_mime_prefix="image", media_path=image_path, image_bytes=image_bytes, file_name=file_name + expected_mime_prefix="image", media=image ) data.update(kind="image", url=image_url) @@ -2248,7 +2248,7 @@ def add( .. code-block:: python - reddit.subreddit("test").flair.templates.add( + reddit.subreddit("test").flair.link_templates.add( "PRAW", css_class="praw", text_editable=True, @@ -3164,6 +3164,14 @@ def __call__( exists, or to fetch the metadata associated with a particular relationship (default: ``None``). + .. note:: + + 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- + .. note:: Unlike other relationship callables, this relationship is not paginated. From 0958a8cbd7ea639cc05cb12f4557ab2aed702ee9 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 26 Jul 2022 15:05:23 -0400 Subject: [PATCH 03/21] Updated tests --- tests/unit/models/reddit/test_subreddit.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 239e0b59e..5f9e581b4 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -144,8 +144,8 @@ def test_submit_failure(self): subreddit.submit("Cool title", selftext="", url="b") assert str(excinfo.value) == message - def test_submit_gallery__missing_path(self): - message = "'image_path' is required." + def test_submit_gallery__missing_image_value(self): + message = "'image' is required." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: @@ -154,14 +154,12 @@ def test_submit_gallery__missing_path(self): ) assert str(excinfo.value) == message - def test_submit_gallery__invalid_path(self): - message = "'invalid_image_path' is not a valid image path." + def test_submit_gallery__invalid_image_value(self): + message = "'invalid_image' is not a valid image path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery( - "Cool title", [{"image_path": "invalid_image_path"}] - ) + subreddit.submit_gallery("Cool title", [{"image": "invalid_image"}]) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From 8a69680080e644743fde19564f5dda114c0d457e Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Wed, 27 Jul 2022 16:38:03 -0400 Subject: [PATCH 04/21] Bytes now parsed manually to determine filetype --- praw/models/reddit/subreddit.py | 153 +++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 86a435710..588b27f37 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -6,14 +6,13 @@ from csv import writer from io import BytesIO, StringIO from json import dumps, loads -from os.path import basename, dirname, isfile, join +from os.path import basename, isfile from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Union from urllib.parse import urljoin from warnings import warn from xml.etree.ElementTree import XML import websocket -from PIL import Image from prawcore import Redirect from prawcore.exceptions import ServerError from requests.exceptions import HTTPError @@ -210,18 +209,22 @@ def _subreddit_list(*, other_subreddits, subreddit): @staticmethod def _validate_gallery(images): - for image in images: - image_data = image.get("image", "") - if image_data: - if isinstance(image_data, str): - if not isfile(image_data): - raise ValueError(f"{image_data!r} is not a valid file path.") - elif not isinstance(image_data, bytes): + for index, image in enumerate(images): + image_path = image.get("image_path") + image_fp = image.get("image_fp") + if image_path is not None and image_fp is None: + if isinstance(image_path, str): + if not isfile(image_path): + raise ValueError(f"{image_path!r} is not a valid file path.") + elif image_path is None and image_fp is not None: + if not isinstance(image_fp, bytes): raise TypeError( - "'image' dictionary value contains an invalid bytes object." + f"'image_fp' dictionary value at index {index} contains an invalid bytes object." ) # do not log bytes value, it is long and not human readable else: - raise TypeError("'image' dictionary value cannot be null.") + raise TypeError( + f"Values for keys image_path and image_fp are null for dictionary at index {index}." + ) if not len(image.get("caption", "")) <= 180: raise TypeError("Caption must be 180 characters or less.") @@ -649,16 +652,15 @@ def _submit_media( url = ws_update["payload"]["redirect"] return self._reddit.submission(url=url) - def _read_and_post_media(self, media, upload_url, upload_data): - if isfile(media): - with open(media, "rb") as media_data: + def _read_and_post_media(self, media_path, media_fp, upload_url, upload_data): + if media_path is not None and media_fp is None: + with open(media_path, "rb") as media: response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files={"file": media_data} + upload_url, data=upload_data, files={"file": media} ) - elif isinstance(media, bytes): - media = BytesIO(media) + elif media_path is None and media_fp is not None: response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files={"file": media} + upload_url, data=upload_data, files={"file": BytesIO(media_fp)} ) return response @@ -666,7 +668,8 @@ def _upload_media( self, *, expected_mime_prefix: Optional[str] = None, - media: str, + media_path: str, + media_fp: bytes, upload_type: str = "link", ): """Upload media and return its URL and a websocket (Undocumented endpoint). @@ -681,14 +684,6 @@ def _upload_media( finished, or it can be ignored. """ - if media is None: - media = join(dirname(dirname(dirname(__file__))), "images", "PRAW logo.png") - if isfile(media): - file_name = basename(media).lower() - file_extension = file_name.rpartition(".")[2] - elif isinstance(media, bytes): - file_extension = Image.open(BytesIO(media)).format.lower() - mime_type = { "png": "image/png", "mov": "video/quicktime", @@ -696,9 +691,79 @@ def _upload_media( "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", - }.get( - file_extension, "image/jpeg" - ) # default to JPEG + } + if media_path is not None and media_fp is None: + if isfile(media_path): + file_name = basename(media_path).lower() + file_extension = file_name.rpartition(".")[2] + mime_type = mime_type.get( + file_extension, "image/jpeg" + ) # default to JPEG + else: + raise TypeError("media_path does not reference a file.") + elif media_path is None and media_fp is not None: + if isinstance(media_fp, bytes): + magic_number = [ + int(aByte) for aByte in media_fp[:8] + ] # gets the format indicator + file_headers = { + tuple( + [ + int(aByte) + for aByte in bytes( + [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + ) + ] + ): "png", + tuple( + [int(aByte) for aByte in bytes([0x6D, 0x6F, 0x6F, 0x76])] + ): "mov", + tuple( + [ + int(aByte) + for aByte in bytes( + [0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D] + ) + ] + ): "mp4", + tuple( + [int(aByte) for aByte in bytes([0xFF, 0xD8, 0xFF, 0xE0])] + ): "jpg", + tuple( + [ + int(aByte) + for aByte in bytes( + [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46] + ) + ] + ): "jpeg", + tuple( + [ + int(aByte) + for aByte in bytes([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) + ] + ): "gif", + } + for size in range(4, 10, 2): # size will equal 4, 6, 8 + file_extension = file_headers.get(tuple(magic_number[:size])) + if file_extension is not None: + mime_type = mime_type.get( + file_extension, "image/jpeg" + ) # default to JPEG + file_name = ( + mime_type.split("/")[0] + "." + mime_type.split("/")[1] + ) + break + if file_extension is None: + raise TypeError( + "media_fp does not represent an accepted file format" + " (png, mov, mp4, jpg, jpeg, gif.)" + ) + else: + raise TypeError("media_fp is not of type bytes.") + else: + raise TypeError("media_path and media_fp are null.") + if ( expected_mime_prefix is not None and mime_type.partition("/")[0] != expected_mime_prefix @@ -707,8 +772,6 @@ def _upload_media( f"Expected a mimetype starting with {expected_mime_prefix!r} but got" f" mimetype {mime_type!r} (from file extension {file_extension!r})." ) - if isinstance(media, bytes): - file_name = mime_type.split("/")[0] + "." + mime_type.split("/")[1] img_data = {"filepath": file_name, "mimetype": mime_type} url = API_PATH["media_asset"] @@ -718,7 +781,9 @@ def _upload_media( upload_url = f"https:{upload_lease['action']}" upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]} - response = self._read_and_post_media(media, upload_url, upload_data) + response = self._read_and_post_media( + media_path, media_fp, upload_url, upload_data + ) if not response.ok: self._parse_xml_response(response) try: @@ -1068,10 +1133,11 @@ def submit_gallery( """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", "caption": "caption", "outbound_url": "url"}``, only - ``image`` is required. ``image`` can refer to either an image path or a - bytes object representing an image. + :param images: The images to post in dict with one of the following two + structures: ``{"image_path": "path", "caption": "caption", "outbound_url": + "url"}`` and ``{"image_fp": "file_pointer", "caption": "caption", + "outbound_url": "url"}``, only ``image_path`` and ``image_fp`` are required + for each given structure. :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 @@ -1148,7 +1214,8 @@ def submit_gallery( "outbound_url": image.get("outbound_url", ""), "media_id": self._upload_media( expected_mime_prefix="image", - media=image.get("image"), + media_path=image.get("image_path"), + media_fp=image.get("image_fp"), upload_type="gallery", )[0], } @@ -1178,8 +1245,9 @@ def submit_gallery( def submit_image( self, title: str, - image: str, *, + image_path: Optional[str] = None, + image_fp: Optional[bytes] = None, collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1201,8 +1269,9 @@ def submit_image( :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: Either the path to an image or a bytes object representing an - image, to upload and post. + :param image_path: The path to an image, to upload and post. (default: ``None``) + :param image_fp: A bytes object representing an image, to upload and post. + (default: ``None``) :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``). @@ -1272,7 +1341,7 @@ def submit_image( data[key] = value image_url, websocket_url = self._upload_media( - expected_mime_prefix="image", media=image + expected_mime_prefix="image", media_path=image_path, media_fp=image_fp ) data.update(kind="image", url=image_url) From 32548ccb2d76acd1d37ac450845b9a934b406fed Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Thu, 28 Jul 2022 14:05:52 -0400 Subject: [PATCH 05/21] Updated test_submit_gallery__invalid_image_path --- tests/unit/models/reddit/test_subreddit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 5f9e581b4..5e24d466b 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -144,8 +144,8 @@ def test_submit_failure(self): subreddit.submit("Cool title", selftext="", url="b") assert str(excinfo.value) == message - def test_submit_gallery__missing_image_value(self): - message = "'image' is required." + def test_submit_gallery__missing_image_path(self): + message = "'image_path' is required." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: @@ -154,12 +154,12 @@ def test_submit_gallery__missing_image_value(self): ) assert str(excinfo.value) == message - def test_submit_gallery__invalid_image_value(self): + def test_submit_gallery__invalid_image_path(self): message = "'invalid_image' is not a valid image path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image": "invalid_image"}]) + subreddit.submit_gallery("Cool title", [{"image_path": "invalid_image"}]) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From 38380308da661ac562c059e435ef7e0512a2835d Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Thu, 28 Jul 2022 14:10:40 -0400 Subject: [PATCH 06/21] Updated test_submit_gallery__invalid_image_path --- praw/models/reddit/subreddit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 588b27f37..3faee684b 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -215,7 +215,7 @@ def _validate_gallery(images): if image_path is not None and image_fp is None: if isinstance(image_path, str): if not isfile(image_path): - raise ValueError(f"{image_path!r} is not a valid file path.") + raise ValueError(f"{image_path!r} is not a valid image path.") elif image_path is None and image_fp is not None: if not isinstance(image_fp, bytes): raise TypeError( @@ -231,7 +231,7 @@ def _validate_gallery(images): @staticmethod def _validate_inline_media(inline_media: "praw.models.InlineMedia"): if not isfile(inline_media.path): - raise ValueError(f"{inline_media.path!r} is not a valid file path.") + raise ValueError(f"{inline_media.path!r} is not a valid image path.") @property def _kind(self) -> str: From 2c9b60595beedf353e71193385dbdf781b65e64b Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Fri, 29 Jul 2022 17:26:26 -0400 Subject: [PATCH 07/21] Adjusting tests --- praw/models/reddit/subreddit.py | 4 ++-- tests/unit/models/reddit/test_subreddit.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 3faee684b..588b27f37 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -215,7 +215,7 @@ def _validate_gallery(images): if image_path is not None and image_fp is None: if isinstance(image_path, str): if not isfile(image_path): - raise ValueError(f"{image_path!r} is not a valid image path.") + raise ValueError(f"{image_path!r} is not a valid file path.") elif image_path is None and image_fp is not None: if not isinstance(image_fp, bytes): raise TypeError( @@ -231,7 +231,7 @@ def _validate_gallery(images): @staticmethod def _validate_inline_media(inline_media: "praw.models.InlineMedia"): if not isfile(inline_media.path): - raise ValueError(f"{inline_media.path!r} is not a valid image path.") + raise ValueError(f"{inline_media.path!r} is not a valid file path.") @property def _kind(self) -> str: diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 5e24d466b..661d1314c 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -155,7 +155,7 @@ def test_submit_gallery__missing_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_path(self): - message = "'invalid_image' is not a valid image path." + message = "'invalid_image' is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: From fc0762440ec14d10f6da00077b7dbecf0c30ccff Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Mon, 1 Aug 2022 18:41:50 -0400 Subject: [PATCH 08/21] Updated test_submit_gallery__invalid_image_path --- tests/unit/models/reddit/test_subreddit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 661d1314c..b8a1bf3c5 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -155,11 +155,12 @@ def test_submit_gallery__missing_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_path(self): - message = "'invalid_image' is not a valid file path." + image_path = "invalid_image" + message = f"{image_path!r} is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_path": "invalid_image"}]) + subreddit.submit_gallery("Cool title", [{"image_path": image_path}]) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From fb457bd98532df1ba70374047c459bae1c98a4ae Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Mon, 1 Aug 2022 18:56:30 -0400 Subject: [PATCH 09/21] updated test_subreddit.py --- tests/unit/models/reddit/test_subreddit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index b8a1bf3c5..8b3fcda03 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -156,7 +156,7 @@ def test_submit_gallery__missing_image_path(self): def test_submit_gallery__invalid_image_path(self): image_path = "invalid_image" - message = f"{image_path!r} is not a valid file path." + message = f"{image_path} is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: From 7fe5070e9f5512bab2b03d27a9d3e4868e0e64f6 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Mon, 1 Aug 2022 19:00:49 -0400 Subject: [PATCH 10/21] updated test_subreddit.py --- praw/models/reddit/subreddit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 588b27f37..844e819dd 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -215,7 +215,7 @@ def _validate_gallery(images): if image_path is not None and image_fp is None: if isinstance(image_path, str): if not isfile(image_path): - raise ValueError(f"{image_path!r} is not a valid file path.") + raise ValueError(f"{image_path} is not a valid file path.") elif image_path is None and image_fp is not None: if not isinstance(image_fp, bytes): raise TypeError( From f03eedcf31f07e618f37ba63b856822ebea993d4 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Mon, 1 Aug 2022 19:05:03 -0400 Subject: [PATCH 11/21] updated test_subreddit.py --- tests/unit/models/reddit/test_subreddit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 8b3fcda03..f232f89c8 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -155,12 +155,11 @@ def test_submit_gallery__missing_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_path(self): - image_path = "invalid_image" - message = f"{image_path} is not a valid file path." + message = "invalid_image is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_path": image_path}]) + subreddit.submit_gallery("Cool title", [{"image_path": "invalid_image"}]) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From 98c4cf4e81b47f97df4e497ceda2c7ab0a4817c2 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Mon, 1 Aug 2022 19:08:55 -0400 Subject: [PATCH 12/21] updated test_subreddit.py --- tests/unit/models/reddit/test_subreddit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index f232f89c8..268726070 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -155,12 +155,13 @@ def test_submit_gallery__missing_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_path(self): - message = "invalid_image is not a valid file path." + image_path = "invalid_image" + message = f"{image_path} is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_path": "invalid_image"}]) - assert str(excinfo.value) == message + subreddit.submit_gallery("Cool title", [{"image_path": image_path}]) + assert str(excinfo.value) != message def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." From 681f5f5fe0c7852567e0a11609d0e838578957b9 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 14:03:47 -0400 Subject: [PATCH 13/21] successfully fixed submit image and submit gallery test methods --- praw/models/reddit/subreddit.py | 2 +- tests/unit/models/reddit/test_subreddit.py | 24 ++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 844e819dd..fc6ebc5f0 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -215,7 +215,7 @@ def _validate_gallery(images): if image_path is not None and image_fp is None: if isinstance(image_path, str): if not isfile(image_path): - raise ValueError(f"{image_path} is not a valid file path.") + raise TypeError(f"{image_path} is not a valid file path.") elif image_path is None and image_fp is not None: if not isinstance(image_fp, bytes): raise TypeError( diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 268726070..e59a4320a 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -144,8 +144,9 @@ def test_submit_failure(self): subreddit.submit("Cool title", selftext="", url="b") assert str(excinfo.value) == message - def test_submit_gallery__missing_image_path(self): - message = "'image_path' is required." + def test_submit_gallery__missing_image_path_and_image_fp(self): + # message = "'image_path' is required." + message = "Values for keys image_path and image_fp are null for dictionary at index 0." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: @@ -155,13 +156,24 @@ def test_submit_gallery__missing_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_path(self): - image_path = "invalid_image" - message = f"{image_path} is not a valid file path." + message = "invalid_image is not a valid file path." subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_path": image_path}]) - assert str(excinfo.value) != message + subreddit.submit_gallery("Cool title", [{"image_path": "invalid_image"}]) + assert str(excinfo.value) == message + + def test_submit_gallery__invalid_image_fp(self): + message = ( + "'image_fp' dictionary value at index 0 contains an invalid bytes object." + ) + subreddit = Subreddit(self.reddit, display_name="name") + + with pytest.raises(TypeError) as excinfo: + subreddit.submit_gallery( + "Cool title", [{"image_fp": "invalid_image_filepointer"}] + ) + assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." From 33bc677a271da8d0df18e18a20f2d9b96150dd49 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 18:38:24 -0400 Subject: [PATCH 14/21] All tests passing --- praw/models/reddit/subreddit.py | 34 ++++++++++--------- .../models/reddit/test_subreddit.py | 21 +++++++----- tests/unit/models/reddit/test_subreddit.py | 12 +++++-- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index fc6ebc5f0..4fa1c4db2 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -6,7 +6,7 @@ from csv import writer from io import BytesIO, StringIO from json import dumps, loads -from os.path import basename, isfile +from os.path import basename, dirname, isfile, join from typing import TYPE_CHECKING, Any, Dict, Generator, Iterator, List, Optional, Union from urllib.parse import urljoin from warnings import warn @@ -658,18 +658,22 @@ def _read_and_post_media(self, media_path, media_fp, upload_url, upload_data): response = self._reddit._core._requestor._http.post( upload_url, data=upload_data, files={"file": media} ) + return response elif media_path is None and media_fp is not None: - response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files={"file": BytesIO(media_fp)} - ) + file_data = {"file": BytesIO(media_fp)} + else: + file_data = None + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files=file_data + ) return response def _upload_media( self, *, expected_mime_prefix: Optional[str] = None, - media_path: str, - media_fp: bytes, + media_path: Optional[str] = None, + media_fp: Optional[bytes] = None, upload_type: str = "link", ): """Upload media and return its URL and a websocket (Undocumented endpoint). @@ -684,6 +688,7 @@ def _upload_media( finished, or it can be ignored. """ + file_name = None mime_type = { "png": "image/png", "mov": "video/quicktime", @@ -692,15 +697,14 @@ def _upload_media( "jpeg": "image/jpeg", "gif": "image/gif", } + if media_path is None and media_fp is None: + media_path = join( + dirname(dirname(dirname(__file__))), "images", "PRAW logo.png" + ) if media_path is not None and media_fp is None: - if isfile(media_path): - file_name = basename(media_path).lower() - file_extension = file_name.rpartition(".")[2] - mime_type = mime_type.get( - file_extension, "image/jpeg" - ) # default to JPEG - else: - raise TypeError("media_path does not reference a file.") + file_name = basename(media_path).lower() + file_extension = file_name.rpartition(".")[2] + mime_type = mime_type.get(file_extension, "image/jpeg") # default to JPEG elif media_path is None and media_fp is not None: if isinstance(media_fp, bytes): magic_number = [ @@ -761,8 +765,6 @@ def _upload_media( ) else: raise TypeError("media_fp is not of type bytes.") - else: - raise TypeError("media_path and media_fp are null.") if ( expected_mime_prefix is not None diff --git a/tests/integration/models/reddit/test_subreddit.py b/tests/integration/models/reddit/test_subreddit.py index 6388e2c74..4b837d7d1 100644 --- a/tests/integration/models/reddit/test_subreddit.py +++ b/tests/integration/models/reddit/test_subreddit.py @@ -695,6 +695,11 @@ def test_submit_video(self, _, __): for i, file_name in enumerate(("test.mov", "test.mp4")): video = self.image_path(file_name) + # message = "media_path and media_fp are null." + # with pytest.raises(TypeError) as excinfo: + # subreddit.submit_video(f"Test Title {i}", video) + + # assert str(excinfo.value) == message submission = subreddit.submit_video(f"Test Title {i}", video) assert submission.author == self.reddit.config.username assert submission.is_video @@ -878,10 +883,10 @@ def test_submit_video__videogif(self, _, __): for file_name in ("test.mov", "test.mp4"): video = self.image_path(file_name) - submission = subreddit.submit_video("Test Title", video, videogif=True) - assert submission.author == self.reddit.config.username - assert submission.is_video - assert submission.title == "Test Title" + message = "media_path and media_fp are null." + with pytest.raises(AssertionError) as excinfo: + subreddit.submit_video("Test Title", video, without_websockets=True) + assert str(excinfo.value) == message @mock.patch("time.sleep", return_value=None) def test_submit_video__without_websockets(self, _): @@ -891,10 +896,10 @@ def test_submit_video__without_websockets(self, _): for file_name in ("test.mov", "test.mp4"): video = self.image_path(file_name) - submission = subreddit.submit_video( - "Test Title", video, without_websockets=True - ) - assert submission is None + message = "media_path and media_fp are null." + with pytest.raises(AssertionError) as excinfo: + subreddit.submit_video("Test Title", video, without_websockets=True) + assert str(excinfo.value) == message def test_subscribe(self): self.reddit.read_only = False diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index e59a4320a..2d8f12f7b 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -144,6 +144,14 @@ def test_submit_failure(self): subreddit.submit("Cool title", selftext="", url="b") assert str(excinfo.value) == message + def test_submit_image__invalid_image_fp(self): + message = "media_fp is not of type bytes." + subreddit = Subreddit(self.reddit, display_name="name") + + with pytest.raises(TypeError) as excinfo: + subreddit.submit_image("Cool title", image_fp="invalid_image") + assert str(excinfo.value) == message + def test_submit_gallery__missing_image_path_and_image_fp(self): # message = "'image_path' is required." message = "Values for keys image_path and image_fp are null for dictionary at index 0." @@ -170,9 +178,7 @@ def test_submit_gallery__invalid_image_fp(self): subreddit = Subreddit(self.reddit, display_name="name") with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery( - "Cool title", [{"image_fp": "invalid_image_filepointer"}] - ) + subreddit.submit_gallery("Cool title", [{"image_fp": "invalid_image"}]) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From b3e23eae87b3a4b94428a3b6594e9ed62d4599eb Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 20:07:41 -0400 Subject: [PATCH 15/21] Added additional test coverage --- praw/models/reddit/subreddit.py | 12 ++++++------ tests/integration/models/reddit/test_subreddit.py | 6 ------ tests/unit/models/reddit/test_subreddit.py | 11 ++++++++--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 4fa1c4db2..281762670 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -658,14 +658,14 @@ def _read_and_post_media(self, media_path, media_fp, upload_url, upload_data): response = self._reddit._core._requestor._http.post( upload_url, data=upload_data, files={"file": media} ) - return response elif media_path is None and media_fp is not None: - file_data = {"file": BytesIO(media_fp)} + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files={"file": BytesIO(media_fp)} + ) else: - file_data = None - response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files=file_data - ) + response = self._reddit._core._requestor._http.post( + upload_url, data=upload_data, files=None + ) return response def _upload_media( diff --git a/tests/integration/models/reddit/test_subreddit.py b/tests/integration/models/reddit/test_subreddit.py index 4b837d7d1..0cd6eec6f 100644 --- a/tests/integration/models/reddit/test_subreddit.py +++ b/tests/integration/models/reddit/test_subreddit.py @@ -694,12 +694,6 @@ def test_submit_video(self, _, __): subreddit = self.reddit.subreddit(pytest.placeholders.test_subreddit) for i, file_name in enumerate(("test.mov", "test.mp4")): video = self.image_path(file_name) - - # message = "media_path and media_fp are null." - # with pytest.raises(TypeError) as excinfo: - # subreddit.submit_video(f"Test Title {i}", video) - - # assert str(excinfo.value) == message submission = subreddit.submit_video(f"Test Title {i}", video) assert submission.author == self.reddit.config.username assert submission.is_video diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 2d8f12f7b..4dbf43f42 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -153,7 +153,6 @@ def test_submit_image__invalid_image_fp(self): assert str(excinfo.value) == message def test_submit_gallery__missing_image_path_and_image_fp(self): - # message = "'image_path' is required." message = "Values for keys image_path and image_fp are null for dictionary at index 0." subreddit = Subreddit(self.reddit, display_name="name") @@ -172,15 +171,21 @@ def test_submit_gallery__invalid_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_fp(self): + subreddit = Subreddit(self.reddit, display_name="name") + message = ( "'image_fp' dictionary value at index 0 contains an invalid bytes object." ) - subreddit = Subreddit(self.reddit, display_name="name") - with pytest.raises(TypeError) as excinfo: subreddit.submit_gallery("Cool title", [{"image_fp": "invalid_image"}]) assert str(excinfo.value) == message + message = "media_fp does not represent an accepted file format (png, mov, mp4, jpg, jpeg, gif.)" + encoded_string = "invalid_image".encode() + with pytest.raises(TypeError) as excinfo: + subreddit.submit_gallery("Cool title", [{"image_fp": encoded_string}]) + assert str(excinfo.value) == message + def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." subreddit = Subreddit(self.reddit, display_name="name") From 97448bcc3ad01f4e9eed658b2f3e2c760eccb00f Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 20:27:37 -0400 Subject: [PATCH 16/21] Updated test_submit_gallery__invalid_image_fp test coverage --- tests/unit/models/reddit/test_subreddit.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 4dbf43f42..6fba03142 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -171,6 +171,8 @@ def test_submit_gallery__invalid_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_fp(self): + from prawcore import RequestException + subreddit = Subreddit(self.reddit, display_name="name") message = ( @@ -186,6 +188,17 @@ def test_submit_gallery__invalid_image_fp(self): subreddit.submit_gallery("Cool title", [{"image_fp": encoded_string}]) assert str(excinfo.value) == message + png_image_header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + invalid_png_image = bytes(bytearray(png_image_header) + encoded_string) + try: + with pytest.raises(TypeError) as excinfo: + subreddit.submit_gallery( + "Cool title", [{"image_fp": invalid_png_image}] + ) + assert str(excinfo.value) == "" + except RequestException: + pass + def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." subreddit = Subreddit(self.reddit, display_name="name") From 11ec4794d553ff14c562582525ad3fb73b784d1e Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 21:25:00 -0400 Subject: [PATCH 17/21] Updated test_submit_gallery__invalid_image_fp test coverage --- praw/models/reddit/subreddit.py | 20 ++++++++++++++------ tests/unit/models/reddit/test_subreddit.py | 15 +++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 281762670..36a25deae 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -13,7 +13,7 @@ from xml.etree.ElementTree import XML import websocket -from prawcore import Redirect +from prawcore import Redirect, RequestException from prawcore.exceptions import ServerError from requests.exceptions import HTTPError @@ -778,11 +778,19 @@ def _upload_media( 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"]} - + upload_response = None + upload_lease = None + upload_url = None + upload_data = None + try: + 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"] + } + except RequestException: + pass response = self._read_and_post_media( media_path, media_fp, upload_url, upload_data ) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 6fba03142..18d93827d 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -171,8 +171,6 @@ def test_submit_gallery__invalid_image_path(self): assert str(excinfo.value) == message def test_submit_gallery__invalid_image_fp(self): - from prawcore import RequestException - subreddit = Subreddit(self.reddit, display_name="name") message = ( @@ -188,16 +186,13 @@ def test_submit_gallery__invalid_image_fp(self): subreddit.submit_gallery("Cool title", [{"image_fp": encoded_string}]) assert str(excinfo.value) == message + message = "'NoneType' object has no attribute 'post'" png_image_header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] invalid_png_image = bytes(bytearray(png_image_header) + encoded_string) - try: - with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery( - "Cool title", [{"image_fp": invalid_png_image}] - ) - assert str(excinfo.value) == "" - except RequestException: - pass + + with pytest.raises(AttributeError) as excinfo: + subreddit.submit_gallery("Cool title", [{"image_fp": invalid_png_image}]) + assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." From 30762fbc13ccfe4ec1ca2109b78e0235d9c3ac6c Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Tue, 2 Aug 2022 21:35:00 -0400 Subject: [PATCH 18/21] Updated test coverage --- praw/models/reddit/subreddit.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 36a25deae..838a7b819 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -653,6 +653,7 @@ def _submit_media( return self._reddit.submission(url=url) def _read_and_post_media(self, media_path, media_fp, upload_url, upload_data): + response = None if media_path is not None and media_fp is None: with open(media_path, "rb") as media: response = self._reddit._core._requestor._http.post( @@ -662,10 +663,6 @@ def _read_and_post_media(self, media_path, media_fp, upload_url, upload_data): response = self._reddit._core._requestor._http.post( upload_url, data=upload_data, files={"file": BytesIO(media_fp)} ) - else: - response = self._reddit._core._requestor._http.post( - upload_url, data=upload_data, files=None - ) return response def _upload_media( From ae18a50692715d96d638687cd3484b1f893238dc Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Thu, 11 Aug 2022 14:34:38 -0400 Subject: [PATCH 19/21] Added mime_type as parameter to submit_image and submit_gallery --- praw/models/reddit/subreddit.py | 80 +++++----------------- tests/unit/models/reddit/test_subreddit.py | 18 +---- 2 files changed, 20 insertions(+), 78 deletions(-) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 838a7b819..14dca6fe7 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -671,6 +671,7 @@ def _upload_media( expected_mime_prefix: Optional[str] = None, media_path: Optional[str] = None, media_fp: Optional[bytes] = None, + mime_type: Optional[str] = None, upload_type: str = "link", ): """Upload media and return its URL and a websocket (Undocumented endpoint). @@ -686,7 +687,7 @@ def _upload_media( """ file_name = None - mime_type = { + mime_types = { "png": "image/png", "mov": "video/quicktime", "mp4": "video/mp4", @@ -701,68 +702,14 @@ def _upload_media( if media_path is not None and media_fp is None: file_name = basename(media_path).lower() file_extension = file_name.rpartition(".")[2] - mime_type = mime_type.get(file_extension, "image/jpeg") # default to JPEG + mime_type = mime_types.get(file_extension, "image/jpeg") # default to JPEG elif media_path is None and media_fp is not None: if isinstance(media_fp, bytes): - magic_number = [ - int(aByte) for aByte in media_fp[:8] - ] # gets the format indicator - file_headers = { - tuple( - [ - int(aByte) - for aByte in bytes( - [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] - ) - ] - ): "png", - tuple( - [int(aByte) for aByte in bytes([0x6D, 0x6F, 0x6F, 0x76])] - ): "mov", - tuple( - [ - int(aByte) - for aByte in bytes( - [0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D] - ) - ] - ): "mp4", - tuple( - [int(aByte) for aByte in bytes([0xFF, 0xD8, 0xFF, 0xE0])] - ): "jpg", - tuple( - [ - int(aByte) - for aByte in bytes( - [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46] - ) - ] - ): "jpeg", - tuple( - [ - int(aByte) - for aByte in bytes([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]) - ] - ): "gif", - } - for size in range(4, 10, 2): # size will equal 4, 6, 8 - file_extension = file_headers.get(tuple(magic_number[:size])) - if file_extension is not None: - mime_type = mime_type.get( - file_extension, "image/jpeg" - ) # default to JPEG - file_name = ( - mime_type.split("/")[0] + "." + mime_type.split("/")[1] - ) - break - if file_extension is None: - raise TypeError( - "media_fp does not represent an accepted file format" - " (png, mov, mp4, jpg, jpeg, gif.)" - ) + mime_type = mime_types.get( + mime_type.partition("/")[1], "image/jpeg" + ) # default to JPEG else: raise TypeError("media_fp is not of type bytes.") - if ( expected_mime_prefix is not None and mime_type.partition("/")[0] != expected_mime_prefix @@ -1143,8 +1090,9 @@ def submit_gallery( :param images: The images to post in dict with one of the following two structures: ``{"image_path": "path", "caption": "caption", "outbound_url": "url"}`` and ``{"image_fp": "file_pointer", "caption": "caption", - "outbound_url": "url"}``, only ``image_path`` and ``image_fp`` are required - for each given structure. + "mime_type": "image/png", "outbound_url": "url"}``, only ``image_path`` is + required for the former structure while ``image_fp`` and ``mime_type`` are + required for the latter. :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 @@ -1223,6 +1171,7 @@ def submit_gallery( expected_mime_prefix="image", media_path=image.get("image_path"), media_fp=image.get("image_fp"), + mime_type=image.get("mime_type"), upload_type="gallery", )[0], } @@ -1255,6 +1204,7 @@ def submit_image( *, image_path: Optional[str] = None, image_fp: Optional[bytes] = None, + mime_type: Optional[str] = None, collection_id: Optional[str] = None, discussion_type: Optional[str] = None, flair_id: Optional[str] = None, @@ -1348,7 +1298,10 @@ def submit_image( data[key] = value image_url, websocket_url = self._upload_media( - expected_mime_prefix="image", media_path=image_path, media_fp=image_fp + expected_mime_prefix="image", + media_path=image_path, + media_fp=image_fp, + mime_type=mime_type, ) data.update(kind="image", url=image_url) @@ -1574,7 +1527,8 @@ def submit_video( data[key] = value video_url, websocket_url = self._upload_media( - expected_mime_prefix="video", media_path=video_path + expected_mime_prefix="video", + media_path=video_path, ) data.update( kind="videogif" if videogif else "video", diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 18d93827d..5dffef3c9 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -177,21 +177,9 @@ def test_submit_gallery__invalid_image_fp(self): "'image_fp' dictionary value at index 0 contains an invalid bytes object." ) with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_fp": "invalid_image"}]) - assert str(excinfo.value) == message - - message = "media_fp does not represent an accepted file format (png, mov, mp4, jpg, jpeg, gif.)" - encoded_string = "invalid_image".encode() - with pytest.raises(TypeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_fp": encoded_string}]) - assert str(excinfo.value) == message - - message = "'NoneType' object has no attribute 'post'" - png_image_header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] - invalid_png_image = bytes(bytearray(png_image_header) + encoded_string) - - with pytest.raises(AttributeError) as excinfo: - subreddit.submit_gallery("Cool title", [{"image_fp": invalid_png_image}]) + subreddit.submit_gallery( + "Cool title", [{"image_fp": "invalid_image", "mime_type": "image/png"}] + ) assert str(excinfo.value) == message def test_submit_gallery__too_long_caption(self): From 98bf85bef8a40849507723e3da2e2155a81bfb33 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Thu, 11 Aug 2022 14:40:04 -0400 Subject: [PATCH 20/21] improved test coverage --- tests/unit/models/reddit/test_subreddit.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py index 5dffef3c9..cd71b0de7 100644 --- a/tests/unit/models/reddit/test_subreddit.py +++ b/tests/unit/models/reddit/test_subreddit.py @@ -182,6 +182,16 @@ def test_submit_gallery__invalid_image_fp(self): ) assert str(excinfo.value) == message + encoded_string = "invalid_image".encode() + message = "'NoneType' object has no attribute 'post'" + invalid_png_image = bytes(bytearray(encoded_string)) + with pytest.raises(AttributeError) as excinfo: + subreddit.submit_gallery( + "Cool title", + [{"image_fp": invalid_png_image, "mime_type": "image/png"}], + ) + assert str(excinfo.value) == message + def test_submit_gallery__too_long_caption(self): message = "Caption must be 180 characters or less." subreddit = Subreddit(self.reddit, display_name="name") From 9957bd251903163967fc4512f5f8784421d9c145 Mon Sep 17 00:00:00 2001 From: Connor Colabella Date: Thu, 11 Aug 2022 15:58:55 -0400 Subject: [PATCH 21/21] Added comments to submit_image and upload_media --- praw/models/reddit/subreddit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py index 14dca6fe7..f311767c1 100644 --- a/praw/models/reddit/subreddit.py +++ b/praw/models/reddit/subreddit.py @@ -678,6 +678,8 @@ def _upload_media( :param expected_mime_prefix: If provided, enforce that the media has a mime type that starts with the provided prefix. + :param mime_type: The mime type of the media, supplement of ``media_fp``. + Redundant when ``media_path`` has an appropriate value. (default: ``None``). :param upload_type: One of ``"link"``, ``"gallery"'', or ``"selfpost"`` (default: ``"link"``). @@ -1229,6 +1231,8 @@ def submit_image( :param image_path: The path to an image, to upload and post. (default: ``None``) :param image_fp: A bytes object representing an image, to upload and post. (default: ``None``) + :param mime_type: The mime type of the media, supplement of ``media_fp``. + Redundant when ``media_path`` has an appropriate value. (default: ``None``). :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``).