From 4be78de253f1f158d310297472ecb36c6180d493 Mon Sep 17 00:00:00 2001 From: Dan Poirier Date: Wed, 1 Apr 2020 09:56:13 -0400 Subject: [PATCH] Fix size on File object after optimizing Since the size attribute/property on a File object is a cached_property, we need to make sure we update it after changing the data since it might already have cached the old size. This becomes important because Google Cloud Storage's client library refuses to upload a File object if there's not as much data as the size says there is. --- README.rst | 2 -- optimized_image/tests/test_utils.py | 27 ++++++++++++++++--- optimized_image/utils.py | 18 +++++++++++++ runtests.py | 39 ++++++++++++++++++++++++++++ small_kitten.jpeg | Bin 0 -> 1195 bytes tox.ini | 2 +- 6 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 runtests.py create mode 100644 small_kitten.jpeg diff --git a/README.rst b/README.rst index 3e44338..e8efabd 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,6 @@ django_optimized_image django_optimized_image is a simple Django library that allows optimization of images by using `TinyPNG `_ or `Pillow `_. -Detailed documentation is in the "docs" directory. - Quick start ----------- diff --git a/optimized_image/tests/test_utils.py b/optimized_image/tests/test_utils.py index 1810cc5..681eeed 100644 --- a/optimized_image/tests/test_utils.py +++ b/optimized_image/tests/test_utils.py @@ -1,9 +1,8 @@ -from factory.fuzzy import FuzzyText import io from unittest.mock import patch, Mock -from django.core.files.uploadedfile import InMemoryUploadedFile -from django.test import TestCase +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase, override_settings from . import factories from ..utils import optimize_from_buffer, optimize_legacy_images_in_model_fields @@ -11,6 +10,26 @@ class TestOptimizeFromBuffer(TestCase): """Test case for the mock_optimize_from_buffer() function.""" + + @override_settings(OPTIMIZED_IMAGE_METHOD="justtesting") + @patch('optimized_image.utils.is_testing_mode') + def test_changing_size(self, mock_is_testing_mode): + mock_is_testing_mode.return_value = False + TESTFILE = "small_kitten.jpeg" + + data = SimpleUploadedFile(name=TESTFILE, content=open(TESTFILE, "rb").read()) + + initial_size = data.size + optimize_from_buffer(data) + resized_data_size = data.size + data.open() + actual_final_size = len(data.read()) + self.assertEqual(actual_final_size, resized_data_size) + self.assertLess( + actual_final_size, initial_size, + msg="Test not valid - image was not reduced" + ) + @patch('optimized_image.utils.is_testing_mode') @patch('optimized_image.utils.Image') @patch('optimized_image.utils.tinify') @@ -86,6 +105,8 @@ def test_settings(self, mock_tinify, mock_pil_image): """The OPTIMIZED_IMAGE_METHOD is used to determine whether Pillow or TinyPNG is used.""" generic_model = factories.GenericModelFactory() + mock_tinify.from_buffer.return_value.to_buffer.return_value = b"" + with self.subTest(OPTIMIZED_IMAGE_METHOD='pillow'): with self.settings( OPTIMIZED_IMAGE_METHOD='pillow', diff --git a/optimized_image/utils.py b/optimized_image/utils.py index 3401619..81ffae0 100644 --- a/optimized_image/utils.py +++ b/optimized_image/utils.py @@ -42,6 +42,20 @@ def optimize_from_buffer(data): data.seek(0) data.file.write(optimized_buffer) data.file.truncate() + elif settings.OPTIMIZED_IMAGE_METHOD == 'justtesting': + # Make a tiny image and use that instead of the input image. + # (justtesting is NOT a publicly allowed value, it's just for internal testing.) + bytes_io = BytesIO() + Image.new('RGB', (10, 10), "blue").save(bytes_io, format="JPEG") + data.seek(0) + data.file.write(bytes_io.getvalue()) + data.file.truncate() + # Else - just don't change it + else: + return data + + # We optimized it - fix the computed size + data.size = data.file.tell() return data @@ -122,6 +136,10 @@ def optimize_legacy_images_in_model_fields(list_of_models, verbosity=0): image_name = os.path.relpath(image_file.name, image_file.field.upload_to) image_file.save(image_name, content_file) except: + if is_testing_mode(): + # This shouldn't actually happen, so if testing, let the exception continue + # up the call chain so it makes the test fail. + raise # If the optimization failed for any reason, write this # to stdout. sys.stdout.write('\nOptimization failed for {}.'.format(image_file.name)) diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..3473a36 --- /dev/null +++ b/runtests.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +import sys +import django +from django.conf import settings + +if not settings.configured: + settings.configure( + DATABASES={ + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test.db",} + }, + MIDDLEWARE_CLASSES=(), + INSTALLED_APPS=("optimized_image", "not_optimized"), + SITE_ID=1, + ADMINS=(("Admin", "admin@example.com"),), + OPTIMIZED_IMAGE_METHOD="pillow", + TINYPNG_KEY="versecrettinypngkey", + BASE_DIR="", # tells compatibility checker not to emit warning + TEMPLATES=[ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["optimized_image/templates"], + } + ], + ) + + +def runtests(): + django.setup() + from django.test.utils import get_runner + + TestRunner = get_runner(settings) + test_runner = TestRunner(verbosity=1, interactive=True, failfast=True) + failures = test_runner.run_tests(["optimized_image"]) + if failures: + sys.exit(1) + + +if __name__ == "__main__": + runtests() diff --git a/small_kitten.jpeg b/small_kitten.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3d4cc68642f7e0bc83cb15bc3760603eed457396 GIT binary patch literal 1195 zcmex=_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(yimrOjF=gf7?=bZnFSgDA7PMZU|?hgdKKhbC}3n_ zW?^Mx=iubx1}fMpz`(@F%*@2X%*x8b0#scKlxJWOWEE00bYv3_Ok`Io6ftU?xR68H zY2!iBpoX!XqN1l2cOC z(lau%ic3n%$}1|Xnp;}i+B-VCCQY6)b=ve9GiNPYykzOJeA&aSFc^aar4&0M~|O8efIpt%U2&ieg5+G+xH(oe}VkP$iNKo7TjlO z{t^WGi;0DWnS~wXFGi+vAZ8Y1VO2C_6LJh>Pb?HxGHT=yahkYr<3UbkKb$@|Xn~>=}+Zbxjb^mqm zuCm(}w%-@SPjx(-Ud;FW*e%^X){l2&x7n8}_dNNRz!+D(=EwBFYpV5T z>3w(RPZKC$`2O`z_NB!i-t~&iUc9xx;Ooit=GSv)zx{5`ooZutRx_l`iE(8)YqD^0 zO3KGr{kA`@m5UEst^Ay)XF9X;PhN zW_R&Kj<9#&3-5>LMg7;st`WMW!rK(h z>JYQ4tLu#-5`G1G@9RoZ=3+R>Y#JPJY4Mc@8R?R4uWW6PW-v~^*6hKPEz9xp+G&<@ zA-j7mA45!i