Skip to content

Commit 097999f

Browse files
committed
Merge branch 'release/4.0.2'
* release/4.0.2: Bump version to 4.0.2 Do not leak open files after generation Fix `ImageCacheFile.__repr__` to not send signals Make generateimages support pre Django 1.8 versions generateimages: fix taking arguments README - use Python 3 print function In Python 3 files should be opened as binary Fixed #368 use specs directly in ProcessedImageField
2 parents ef45747 + ea66e3d commit 097999f

File tree

11 files changed

+115
-24
lines changed

11 files changed

+115
-24
lines changed

README.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ your model class:
7070
options={'quality': 60})
7171
7272
profile = Profile.objects.all()[0]
73-
print profile.avatar_thumbnail.url # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
74-
print profile.avatar_thumbnail.width # > 100
73+
print(profile.avatar_thumbnail.url) # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
74+
print(profile.avatar_thumbnail.width) # > 100
7575
7676
As you can probably tell, ImageSpecFields work a lot like Django's
7777
ImageFields. The difference is that they're automatically generated by
@@ -97,8 +97,8 @@ class:
9797
options={'quality': 60})
9898
9999
profile = Profile.objects.all()[0]
100-
print profile.avatar_thumbnail.url # > /media/avatars/MY-avatar.jpg
101-
print profile.avatar_thumbnail.width # > 100
100+
print(profile.avatar_thumbnail.url) # > /media/avatars/MY-avatar.jpg
101+
print(profile.avatar_thumbnail.width) # > 100
102102
103103
This is pretty similar to our previous example. We don't need to specify a
104104
"source" any more since we're not processing another image field, but we do need
@@ -144,7 +144,7 @@ on, or what should be done with the result; that's up to you:
144144

145145
.. code-block:: python
146146
147-
source_file = open('/path/to/myimage.jpg')
147+
source_file = open('/path/to/myimage.jpg', 'rb')
148148
image_generator = Thumbnail(source=source_file)
149149
result = image_generator.generate()
150150
@@ -159,7 +159,7 @@ example, if you wanted to save it to disk:
159159

160160
.. code-block:: python
161161
162-
dest = open('/path/to/dest.jpg', 'w')
162+
dest = open('/path/to/dest.jpg', 'wb')
163163
dest.write(result.read())
164164
dest.close()
165165

docs/advanced_usage.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ A simple example of a custom source group class is as follows:
163163
def files(self):
164164
os.chdir(self.dir)
165165
for name in glob.glob('*.jpg'):
166-
yield open(name)
166+
yield open(name, 'rb')
167167
168168
Instances of this class could then be registered with one or more spec id:
169169

imagekit/cachefiles/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.core.files import File
44
from django.core.files.images import ImageFile
55
from django.utils.functional import SimpleLazyObject
6+
from django.utils.encoding import smart_str
67
from ..files import BaseIKFile
78
from ..registry import generator_registry
89
from ..signals import content_required, existence_required
@@ -149,6 +150,11 @@ def __nonzero__(self):
149150
# Python 2 compatibility
150151
return self.__bool__()
151152

153+
def __repr__(self):
154+
return smart_str("<%s: %s>" % (
155+
self.__class__.__name__, self if self.name else "None")
156+
)
157+
152158

153159
class LazyImageCacheFile(SimpleLazyObject):
154160
def __init__(self, generator_id, *args, **kwargs):

imagekit/management/commands/generateimages.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ class Command(BaseCommand):
1414
well as "a:b" and "a:b:c".""")
1515
args = '[generator_ids]'
1616

17+
def add_arguments(self, parser):
18+
parser.add_argument('generator_id', nargs='*', help='<app_name>:<model>:<field> for model specs')
19+
1720
def handle(self, *args, **options):
1821
generators = generator_registry.get_ids()
1922

20-
if args:
21-
patterns = self.compile_patterns(args)
23+
generator_ids = options['generator_id'] if 'generator_id' in options else args
24+
if generator_ids:
25+
patterns = self.compile_patterns(generator_ids)
2226
generators = (id for id in generators if any(p.match(id) for p in patterns))
2327

2428
for generator_id in generators:

imagekit/models/fields/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,18 @@ class ProcessedImageField(models.ImageField, SpecHostField):
9393

9494
def __init__(self, processors=None, format=None, options=None,
9595
verbose_name=None, name=None, width_field=None, height_field=None,
96-
autoconvert=True, spec=None, spec_id=None, **kwargs):
96+
autoconvert=None, spec=None, spec_id=None, **kwargs):
9797
"""
9898
The ProcessedImageField constructor accepts all of the arguments that
9999
the :class:`django.db.models.ImageField` constructor accepts, as well
100100
as the ``processors``, ``format``, and ``options`` arguments of
101101
:class:`imagekit.models.ImageSpecField`.
102102
103103
"""
104+
# if spec is not provided then autoconvert will be True by default
105+
if spec is None and autoconvert is None:
106+
autoconvert = True
107+
104108
SpecHost.__init__(self, processors=processors, format=format,
105109
options=options, autoconvert=autoconvert, spec=spec,
106110
spec_id=spec_id)

imagekit/pkgmeta.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = 'django-imagekit'
22
__author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll'
3-
__version__ = '4.0.1'
3+
__version__ = '4.0.2'
44
__license__ = 'BSD'
55
__all__ = ['__title__', '__author__', '__version__', '__license__']

imagekit/specs/__init__.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,26 @@ def generate(self):
143143
raise MissingSource("The spec '%s' has no source file associated"
144144
" with it." % self)
145145

146-
file_opened_locally = False
147146
# TODO: Move into a generator base class
148147
# TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.)
149-
try:
150-
img = open_image(self.source)
151-
except ValueError:
152148

153-
# Re-open the file -- https://code.djangoproject.com/ticket/13750
149+
closed = self.source.closed
150+
if closed:
151+
# Django file object should know how to reopen itself if it was closed
152+
# https://code.djangoproject.com/ticket/13750
154153
self.source.open()
155-
file_opened_locally = True
156-
img = open_image(self.source)
157154

158-
new_image = process_image(img, processors=self.processors,
159-
format=self.format, autoconvert=self.autoconvert,
160-
options=self.options)
161-
if file_opened_locally:
162-
self.source.close()
155+
try:
156+
img = open_image(self.source)
157+
new_image = process_image(img,
158+
processors=self.processors,
159+
format=self.format,
160+
autoconvert=self.autoconvert,
161+
options=self.options)
162+
finally:
163+
if closed:
164+
# We need to close the file if it was opened by us
165+
self.source.close()
163166
return new_image
164167

165168

tests/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
from django.db import models
22

3+
from imagekit import ImageSpec
34
from imagekit.models import ProcessedImageField
45
from imagekit.models import ImageSpecField
56
from imagekit.processors import Adjust, ResizeToFill, SmartCrop
67

78

9+
class Thumbnail(ImageSpec):
10+
processors = [ResizeToFill(100, 60)]
11+
format = 'JPEG'
12+
options = {'quality': 60}
13+
14+
815
class ImageModel(models.Model):
916
image = models.ImageField(upload_to='b')
1017

@@ -27,6 +34,10 @@ class ProcessedImageFieldModel(models.Model):
2734
options={'quality': 90}, upload_to='p')
2835

2936

37+
class ProcessedImageFieldWithSpecModel(models.Model):
38+
processed = ProcessedImageField(spec=Thumbnail, upload_to='p')
39+
40+
3041
class CountingCacheFileStrategy(object):
3142
def __init__(self):
3243
self.on_existence_required_count = 0

tests/test_cachefiles.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import mock
12
from django.conf import settings
23
from hashlib import md5
34
from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile
@@ -48,6 +49,31 @@ def test_no_source_error():
4849
file.generate()
4950

5051

52+
def test_repr_does_not_send_existence_required():
53+
"""
54+
Ensure that `__repr__` method does not send `existance_required` signal
55+
56+
Cachefile strategy may be configured to generate file on
57+
`existance_required`.
58+
To generate images, backend passes `ImageCacheFile` instance to worker.
59+
Both celery and RQ calls `__repr__` method for each argument to enque call.
60+
And if `__repr__` of object will send this signal, we will get endless
61+
recursion
62+
63+
"""
64+
with mock.patch('imagekit.cachefiles.existence_required') as signal:
65+
# import here to apply mock
66+
from imagekit.cachefiles import ImageCacheFile
67+
68+
spec = TestSpec(source=get_unique_image_file())
69+
file = ImageCacheFile(
70+
spec,
71+
cachefile_backend=DummyAsyncCacheFileBackend()
72+
)
73+
file.__repr__()
74+
eq_(signal.send.called, False)
75+
76+
5177
def test_memcached_cache_key():
5278
"""
5379
Ensure the default cachefile backend is sanitizing its cache key for

tests/test_closing_fieldfiles.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from nose.tools import assert_false, assert_true
2+
3+
from .models import Thumbnail
4+
from .utils import create_photo
5+
6+
7+
def test_do_not_leak_open_files():
8+
instance = create_photo('leak-test.jpg')
9+
source_file = instance.original_image
10+
# Ensure the FieldFile is closed before generation
11+
source_file.close()
12+
image_generator = Thumbnail(source=source_file)
13+
image_generator.generate()
14+
assert_true(source_file.closed)
15+
16+
17+
def test_do_not_close_open_files_after_generate():
18+
instance = create_photo('do-not-close-test.jpg')
19+
source_file = instance.original_image
20+
# Ensure the FieldFile is opened before generation
21+
source_file.open()
22+
image_generator = Thumbnail(source=source_file)
23+
image_generator.generate()
24+
assert_false(source_file.closed)
25+
source_file.close()

0 commit comments

Comments
 (0)