Skip to main content

fetch: remove the now-unused fetch_image function

ID
1b2da07
date
2026-04-10 21:40:59+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
9881a6a
message
fetch: remove the now-unused `fetch_image` function
changed files
9 files, 117 additions, 403 deletions

Changed files

CHANGELOG.md (3972) → CHANGELOG.md (4093)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d03c8fe..5c9922e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # CHANGELOG
 
+## v33 - 2026-04-10
+
+Remove the `fetch_image` function from `chives.fetch`; either use `fetch_url` or `download_image`.
+
 ## v32 - 2026-04-01
 
 Add a `download_image` function to `chives.fetch`.

src/chives/__init__.py (391) → src/chives/__init__.py (391)

diff --git a/src/chives/__init__.py b/src/chives/__init__.py
index a3ab715..5378754 100644
--- a/src/chives/__init__.py
+++ b/src/chives/__init__.py
@@ -11,4 +11,4 @@ I share across multiple sites.
 
 """
 
-__version__ = "32"
+__version__ = "33"

src/chives/fetch.py (3246) → src/chives/fetch.py (2853)

diff --git a/src/chives/fetch.py b/src/chives/fetch.py
index 3d60e78..71f896c 100644
--- a/src/chives/fetch.py
+++ b/src/chives/fetch.py
@@ -11,7 +11,7 @@ import urllib.request
 import certifi
 
 
-__all__ = ["download_image", "fetch_url", "fetch_image", "ImageFormat"]
+__all__ = ["download_image", "fetch_url"]
 
 
 ssl_context = ssl.create_default_context(cafile=certifi.where())
@@ -81,15 +81,20 @@ def _guess_image_format(content_type: str | None) -> ImageFormat:
         raise ValueError(f"unrecognised image format: {content_type}")
 
 
-def fetch_image(
+def download_image(
     url: str,
+    out_prefix: Path,
     *,
     params: dict[str, str] | None = None,
     headers: dict[str, str] | None = None,
-) -> tuple[bytes, ImageFormat]:
+) -> Path:
     """
-    Fetch an image from the given URL and return the image data and
-    image format.
+    Download an image from the given URL to the target path, and return
+    the path of the downloaded file.
+
+    Add the appropriate file extension, based on the image's Content-Type.
+
+    Throws a FileExistsError if you try to overwrite an existing file.
     """
     ssl_context = ssl.create_default_context(cafile=certifi.where())
 
@@ -101,30 +106,11 @@ def fetch_image(
 
     img_format = _guess_image_format(content_type=resp.headers["content-type"])
 
-    return img_data, img_format
-
-
-def download_image(
-    url: str,
-    out_prefix: Path,
-    *,
-    params: dict[str, str] | None = None,
-    headers: dict[str, str] | None = None,
-) -> Path:
-    """
-    Download an image from the given URL to the target path, and return
-    the path of the downloaded file.
-
-    Add the appropriate file extension, based on the image's Content-Type.
-
-    Throws a FileExistsError if you try to overwrite an existing file.
-    """
-    im_data, im_format = fetch_image(url, params=params, headers=headers)
-    out_path = out_prefix.with_suffix("." + im_format)
+    out_path = out_prefix.with_suffix("." + img_format)
 
     out_path.parent.mkdir(exist_ok=True, parents=True)
 
     with open(out_path, "xb") as out_file:
-        out_file.write(im_data)
+        out_file.write(img_data)
 
     return out_path

tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml (0) → tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml (605)

diff --git a/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml b/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml
new file mode 100644
index 0000000..f6d7647
--- /dev/null
+++ b/tests/fixtures/cassettes/TestDownloadImage.test_no_content_type_header.yml
@@ -0,0 +1,33 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Connection:
+      - close
+      Host:
+      - httpbin.org
+      User-Agent:
+      - Python-urllib/3.13
+    method: GET
+    uri: http://httpbin.org/status/200
+  response:
+    body:
+      string: ''
+    headers:
+      Access-Control-Allow-Credentials:
+      - 'true'
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Content-Type: []
+      Date:
+      - Fri, 10 Apr 2026 21:39:22 GMT
+      Server:
+      - gunicorn/19.9.0
+    status:
+      code: 200
+      message: OK
+version: 1

tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml (0) → tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml (635)

diff --git a/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml b/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml
new file mode 100644
index 0000000..3ad0077
--- /dev/null
+++ b/tests/fixtures/cassettes/TestDownloadImage.test_non_image.yml
@@ -0,0 +1,34 @@
+interactions:
+- request:
+    body: null
+    headers:
+      Connection:
+      - close
+      Host:
+      - httpbin.org
+      User-Agent:
+      - Python-urllib/3.13
+    method: GET
+    uri: http://httpbin.org/status/200
+  response:
+    body:
+      string: ''
+    headers:
+      Access-Control-Allow-Credentials:
+      - 'true'
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - close
+      Content-Length:
+      - '0'
+      Content-Type:
+      - text/html; charset=utf-8
+      Date:
+      - Fri, 10 Apr 2026 21:39:22 GMT
+      Server:
+      - gunicorn/19.9.0
+    status:
+      code: 200
+      message: OK
+version: 1

tests/fixtures/cassettes/TestFetchImage.test_http_200.yml (16041) → tests/fixtures/cassettes/TestFetchImage.test_http_200.yml (0)

diff --git a/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml b/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml
deleted file mode 100644
index 31a0d50..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_http_200.yml
+++ /dev/null
@@ -1,257 +0,0 @@
-interactions:
-- request:
-    body: null
-    headers:
-      Connection:
-      - close
-      Host:
-      - api.tumblr.com
-      User-Agent:
-      - Python-urllib/3.14
-    method: GET
-    uri: https://api.tumblr.com/v2/blog/thecroissantgirl.tumblr.com/avatar
-  response:
-    body:
-      string: '{"meta":{"status":302,"msg":"Found"},"response":{"avatar_url":"https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png"}}'
-    headers:
-      Alt-Svc:
-      - h3=":443"; ma=86400
-      Connection:
-      - close
-      Content-Length:
-      - '202'
-      Content-Type:
-      - application/json
-      Date:
-      - Mon, 30 Mar 2026 05:04:45 GMT
-      Location:
-      - https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png
-      Server:
-      - nginx
-      Server-Timing:
-      - a8c-cdn, dc;desc=lhr, cache;desc=BYPASS;dur=243.0
-      Strict-Transport-Security:
-      - max-age=31536000; preload
-      X-Cache-Avatar:
-      - 'true'
-      X-Rid:
-      - 35e6c63c1df237d105ecbc7e70c6e78f
-      X-UA-Compatible:
-      - IE=Edge,chrome=1
-    status:
-      code: 302
-      message: Found
-- request:
-    body: null
-    headers:
-      Connection:
-      - close
-      Host:
-      - 64.media.tumblr.com
-      User-Agent:
-      - Python-urllib/3.14
-    method: GET
-    uri: https://64.media.tumblr.com/b9db8c282532e62b4009e9b41e9b0cb0/1773c30d064a7637-b4/s64x64u_c1/f88f4f722bf06397895453a98ac976eaf767de08.png
-  response:
-    body:
-      string: !!binary |
-        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
-        b5UTb+T6FQAAAABJRU5ErkJggg==
-    headers:
-      Access-Control-Allow-Methods:
-      - GET
-      Access-Control-Allow-Origin:
-      - '*'
-      Access-Control-Max-Age:
-      - '86400'
-      Alt-Svc:
-      - h3=":443"; ma=86400
-      Cache-Control:
-      - max-age=315360000
-      Connection:
-      - close
-      Content-Disposition:
-      - inline; filename="tumblr_b9db8c282532e62b4009e9b41e9b0cb0_f88f4f72_64.png"
-      Content-Length:
-      - '9025'
-      Content-Type:
-      - image/png
-      Date:
-      - Mon, 30 Mar 2026 05:04:45 GMT
-      Etag:
-      - '"7c7a5affc81421707c94439f0333734c-1498089600-0de4da0"'
-      Last-Modified:
-      - Fri, 02 Jun 2023 20:33:11 GMT
-      Server:
-      - nginx
-      Server-Timing:
-      - a8c-cdn, dc;desc=lhr, cache;desc=HIT;dur=0.0
-      Strict-Transport-Security:
-      - max-age=31536000; preload
-      Timing-Allow-Origin:
-      - '*'
-      X-nc:
-      - HIT lhr 24
-      x-frames:
-      - '1'
-    status:
-      code: 200
-      message: OK
-version: 1

tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml (582) → tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml (0)

diff --git a/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml b/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml
deleted file mode 100644
index 2cbcf91..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_no_content_type_header.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-interactions:
-- request:
-    body: null
-    headers:
-      Connection:
-      - close
-      Host:
-      - httpbin.org
-      User-Agent:
-      - Python-urllib/3.14
-    method: GET
-    uri: http://httpbin.org/status/200
-  response:
-    body:
-      string: ''
-    headers:
-      Access-Control-Allow-Credentials:
-      - 'true'
-      Access-Control-Allow-Origin:
-      - '*'
-      Connection:
-      - close
-      Content-Length:
-      - '0'
-      Date:
-      - Mon, 30 Mar 2026 05:20:46 GMT
-      Server:
-      - gunicorn/19.9.0
-    status:
-      code: 200
-      message: OK
-version: 1

tests/fixtures/cassettes/TestFetchImage.test_non_image.yml (635) → tests/fixtures/cassettes/TestFetchImage.test_non_image.yml (0)

diff --git a/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml b/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml
deleted file mode 100644
index 706342a..0000000
--- a/tests/fixtures/cassettes/TestFetchImage.test_non_image.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-interactions:
-- request:
-    body: null
-    headers:
-      Connection:
-      - close
-      Host:
-      - httpbin.org
-      User-Agent:
-      - Python-urllib/3.14
-    method: GET
-    uri: http://httpbin.org/status/200
-  response:
-    body:
-      string: ''
-    headers:
-      Access-Control-Allow-Credentials:
-      - 'true'
-      Access-Control-Allow-Origin:
-      - '*'
-      Connection:
-      - close
-      Content-Length:
-      - '0'
-      Content-Type:
-      - text/html; charset=utf-8
-      Date:
-      - Mon, 30 Mar 2026 05:21:30 GMT
-      Server:
-      - gunicorn/19.9.0
-    status:
-      code: 200
-      message: OK
-version: 1

tests/test_fetch.py (5164) → tests/test_fetch.py (4709)

diff --git a/tests/test_fetch.py b/tests/test_fetch.py
index 442de02..18786be 100644
--- a/tests/test_fetch.py
+++ b/tests/test_fetch.py
@@ -3,18 +3,16 @@ Tests for `chives.fetch`.
 """
 
 import filecmp
-from io import BytesIO
 import json
 from pathlib import Path
 from typing import Any
 from urllib.error import HTTPError
 
-from PIL import Image
 import pytest
 import vcr
 from vcr.cassette import Cassette
 
-from chives.fetch import download_image, fetch_image, fetch_url
+from chives.fetch import download_image, fetch_url
 
 
 class TestFetchUrl:
@@ -68,56 +66,6 @@ class TestFetchUrl:
         assert headers["X-Author"] == "alexwlchan"
 
 
-class TestFetchImage:
-    """
-    Tests for `fetch_image`.
-    """
-
-    def test_http_200(self, vcr_cassette: Cassette) -> None:
-        """
-        Fetch an image and check we get the correct format.
-        """
-        url = "https://api.tumblr.com/v2/blog/thecroissantgirl.tumblr.com/avatar"
-
-        img_data, img_format = fetch_image(url)
-        assert img_format == "png"
-
-        im = Image.open(BytesIO(img_data))
-        assert im.format == "PNG"
-
-    def test_non_image(self, vcr_cassette: Cassette) -> None:
-        """
-        Fetching an "image" which has a non-image Content-Type header
-        throws an error.
-        """
-        url = "http://httpbin.org/status/200"
-
-        with pytest.raises(ValueError, match="unrecognised image format"):
-            fetch_image(url)
-
-    def test_no_content_type_header(self, cassette_name: str) -> None:
-        """
-        Fetching a URL which doesn't return a Content-Type header
-        throws an error.
-        """
-        url = "http://httpbin.org/status/200"
-
-        def delete_content_type_header(response: Any) -> Any:
-            response["headers"]["Content-Type"] = []
-            return response
-
-        with vcr.use_cassette(
-            cassette_name,
-            cassette_library_dir="tests/fixtures/cassettes",
-            decode_compressed_response=True,
-            before_record_response=delete_content_type_header,
-        ):
-            with pytest.raises(
-                RuntimeError, match="no Content-Type header in response"
-            ):
-                fetch_image(url)
-
-
 class TestDownloadImage:
     """
     Tests for `download_image`.
@@ -167,3 +115,35 @@ class TestDownloadImage:
 
         # The file contents are the same as the first download.
         assert filecmp.cmp(out_path, "tests/fixtures/media/470906.png", shallow=False)
+
+    def test_non_image(self, vcr_cassette: Cassette) -> None:
+        """
+        Fetching an "image" which has a non-image Content-Type header
+        throws an error.
+        """
+        url = "http://httpbin.org/status/200"
+
+        with pytest.raises(ValueError, match="unrecognised image format"):
+            download_image(url, out_prefix=Path("example"))
+
+    def test_no_content_type_header(self, cassette_name: str) -> None:
+        """
+        Fetching a URL which doesn't return a Content-Type header
+        throws an error.
+        """
+        url = "http://httpbin.org/status/200"
+
+        def delete_content_type_header(response: Any) -> Any:
+            response["headers"]["Content-Type"] = []
+            return response
+
+        with vcr.use_cassette(
+            cassette_name,
+            cassette_library_dir="tests/fixtures/cassettes",
+            decode_compressed_response=True,
+            before_record_response=delete_content_type_header,
+        ):
+            with pytest.raises(
+                RuntimeError, match="no Content-Type header in response"
+            ):
+                download_image(url, out_prefix=Path("example"))