From 80a7c955834a6208defa9108be5105e349a54923 Mon Sep 17 00:00:00 2001 From: satya-waylit <65822230+satya-waylit@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:03:47 -0600 Subject: [PATCH] Close files in create thumbnail (#252) * Close the orig file in `create_thumbnail` method * Skip python-magic tests on windows * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Johannes Wilm --- CHANGELOG.rst | 1 + avatar/models.py | 59 ++++++++++++++++++++++++------------------------ tests/tests.py | 6 +++++ 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ea2784..2c04bac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,7 @@ Changelog ========= * 9.0.0 (in progress) + * Fix files not closed in `create_thumbnail` * Add Django 5.2 and 6.0 support * Add Python 3.13, 3.14 support * Drop Python 3.8, 3.9 support diff --git a/avatar/models.py b/avatar/models.py index dec9d31..8c5d38f 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,6 +1,7 @@ import binascii import hashlib import os +from contextlib import closing from io import BytesIO from django.core.files import File @@ -142,38 +143,38 @@ class Avatar(models.Model): orig = self.avatar.storage.open(self.avatar.name, "rb") except IOError: return # What should we do here? Render a "sorry, didn't work" img? - try: - image = Image.open(orig) - image = self.transpose_image(image) - quality = quality or settings.AVATAR_THUMB_QUALITY - w, h = image.size - if w != width or h != height: - ratioReal = 1.0 * w / h - ratioWant = 1.0 * width / height - if ratioReal > ratioWant: - diff = int((w - (h * ratioWant)) / 2) - image = image.crop((diff, 0, w - diff, h)) - elif ratioReal < ratioWant: - diff = int((h - (w / ratioWant)) / 2) - image = image.crop((0, diff, w, h - diff)) - if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA": - image = image.convert("RGB") - elif image.mode not in (settings.AVATAR_THUMB_MODES): - image = image.convert(settings.AVATAR_THUMB_MODES[0]) - image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD) - thumb = BytesIO() - image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality) - thumb_file = ContentFile(thumb.getvalue()) - else: + + with closing(orig): + try: + image = Image.open(orig) + except IOError: thumb_file = File(orig) + else: + image = self.transpose_image(image) + quality = quality or settings.AVATAR_THUMB_QUALITY + w, h = image.size + if w != width or h != height: + ratioReal = 1.0 * w / h + ratioWant = 1.0 * width / height + if ratioReal > ratioWant: + diff = int((w - (h * ratioWant)) / 2) + image = image.crop((diff, 0, w - diff, h)) + elif ratioReal < ratioWant: + diff = int((h - (w / ratioWant)) / 2) + image = image.crop((0, diff, w, h - diff)) + if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA": + image = image.convert("RGB") + elif image.mode not in (settings.AVATAR_THUMB_MODES): + image = image.convert(settings.AVATAR_THUMB_MODES[0]) + image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD) + thumb = BytesIO() + image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality) + thumb_file = ContentFile(thumb.getvalue()) + else: + thumb_file = File(orig) thumb_name = self.avatar_name(width, height) thumb = self.avatar.storage.save(thumb_name, thumb_file) - except IOError: - thumb_file = File(orig) - thumb = self.avatar.storage.save( - self.avatar_name(width, height), thumb_file - ) - invalidate_cache(self.user, width, height) + invalidate_cache(self.user, width, height) def avatar_url(self, width, height=None): return self.avatar.storage.url(self.avatar_name(width, height)) diff --git a/tests/tests.py b/tests/tests.py index 742e816..36579a3 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,7 +1,9 @@ import math import os.path +import sys from pathlib import Path from shutil import rmtree +from unittest import skipIf from django.contrib.admin.sites import AdminSite from django.core import management @@ -118,6 +120,7 @@ class AvatarTests(TestCase): self.assertTrue(avatar.primary) # We allow the .tiff file extension but not the mime type + @skipIf(sys.platform == "win32", "Skipping test on Windows platform") @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff")) @override_settings( AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg") @@ -130,6 +133,7 @@ class AvatarTests(TestCase): self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) # We allow the .tiff file extension and the mime type + @skipIf(sys.platform == "win32", "Skipping test on Windows platform") @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff")) @override_settings( AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg", "image/tiff") @@ -141,6 +145,7 @@ class AvatarTests(TestCase): self.assertEqual(len(response.redirect_chain), 1) # Redirect only if it worked self.assertEqual(response.context["upload_avatar_form"].errors, {}) + @skipIf(sys.platform == "win32", "Skipping test on Windows platform") @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) def test_image_without_wrong_extension(self): response = upload_helper(self, "imagefilewithoutext") @@ -148,6 +153,7 @@ class AvatarTests(TestCase): self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) + @skipIf(sys.platform == "win32", "Skipping test on Windows platform") @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) def test_image_with_wrong_extension(self): response = upload_helper(self, "imagefilewithwrongext.ogg")