Hi all, Can anyone confirm that the following is a bug in Django (and that I'm not simply missing the correct way to do this)?
I'm using Django 1.7 on Python 3.4. I'm new to Django but a longtime Python programmer. I've been digging into a ValueError I get when I supply a BytesIO instance as the `file` parameter to an ImageFile instance, then supply that ImageFile instance as the `content` parameter to `FieldFile.save`. In summary: The method `FieldFile.save` saves `content` to `self.storage` (which causes `content` to be closed) but then attempts to access the size of `content`. This causes a ValueError("I/O operation on closed file.") to be raised if the internal file inside the ImageFile instance is a BytesIO instance. Here is `FieldFile.save`: https://github.com/django/django/blob/master/django/db/models/fields/files.py#L86 ---- This appears to be an interaction between the following two changesets: - 8 months ago: https://github.com/django/django/commit/30fc49a7ca0d030c7855f31ed44395903fa6abdd - 3 years ago: https://github.com/django/django/commit/88ff6567310d809abddb97adebf04d5e9403ca8a This changeset also touches relevant code, but I don't think it altered any logic relevant to this problem: - 2 months ago: https://github.com/django/django/commit/96fc3908ad25f40b4c6e2f76be2639f665223227 ---- Here's the detailed line-by-line trace: In "django/db/models/fields/files.py", in method `FieldFile.save(self, name, content, save=True)`: https://github.com/django/django/blob/master/django/db/models/fields/files.py#L86 this function invokes `self.storage.save(name, content)` [line 88], then assigns `content.size` to `self._size` [line 92]. In "django/core/files/storage.py", in method `Storage.save(self, name, content)`: https://github.com/django/django/blob/master/django/core/files/storage.py#L37 this function invokes `self._save(name, content)` [line 51]. In "django/core/files/storage.py", in method `FileSystemStorage._save(self, name, content)`: https://github.com/django/django/blob/master/django/core/files/storage.py#L175 this function invokes `content.close()` [line 231]: https://github.com/django/django/blob/master/django/core/files/storage.py#L231 At this point, `content` is now closed. But then back to method `FieldFile.save(self, name, content, save=True)`, line 92: https://github.com/django/django/blob/master/django/db/models/fields/files.py#L92 we next access the `size` attribute of `content`. The type of `content` is ImageFile, which inherits File: - class ImageFile: https://github.com/django/django/blob/master/django/core/files/images.py#L11 - class File: https://github.com/django/django/blob/master/django/core/files/base.py#L13 Accessing the `size` attribute of `content` accesses the `size` property: https://github.com/django/django/blob/master/django/core/files/base.py#L64 which invokes the `File._get_size(self)` method: https://github.com/django/django/blob/master/django/core/files/base.py#L55 At this point, `content` (a recently-created ImageFile instance) doesn't have a `_size` attribute, so `self._get_size_from_underlying_file()` is invoked [line 58]: In method `File._get_size_from_underlying_file()`, line 39: https://github.com/django/django/blob/master/django/core/files/base.py#L39 the function checks for various attributes of the underlying Python file in a chained if-statement. In my case, the underlying Python file is a BytesIO instance: https://docs.python.org/3/library/io.html#io.BytesIO which inherits io.BufferedIOBase: https://docs.python.org/3/library/io.html#io.BufferedIOBase which in turn inherits io.IOBase: https://docs.python.org/3/library/io.html#io.IOBase The attributes checked, and the results when the file is a BytesIO instance: - `size`: no - `name`: no - `tell` and `seek`: yes, in io.IOBase Because the `tell` and `seek` attributes were found, the function attempts to determine the file size using `self.file.tell()` [line 48]: https://github.com/django/django/blob/master/django/core/files/base.py#L48 However, at this point, because the BytesIO instance is closed, this will result in a ValueError("I/O operation on closed file.") being raised. This ValueError exception is raised in the Python 3.4 source, in the file "Python-3.4.0/Modules/_io/bytesio.c", when the macro `CHECK_CLOSED(self)` [line 22] is invoked by function `bytesio_tell(bytesio *self)` [line 254]. ---- At this point, my work-around is to set the `_size` attribute of the ImageFile manually (to the BytesIO size calculated using the same method as in `File._get_size_from_underlying_file()`) before invoking `FieldFile.save`. def get_bytesio_size(buf): """Calculate the size of BytesIO instance `buf`. Note: Ensure `buf` is not already closed! """ # This approach was copied from "django/core/files/base.py" pos = buf.tell() buf.seek(0, os.SEEK_END) size = buf.tell() buf.seek(pos) return size img_file._size = get_bytesio_size(bytesio) # Then img_field.save(img_name, img_file), etc... It works, but obviously it's hacky to do this. Can anyone confirm that this problem is indeed a bug in Django (and that I'm not simply missing the correct way to do this)? Thanks, jb -- You received this message because you are subscribed to the Google Groups "Django users" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-users+unsubscr...@googlegroups.com. To post to this group, send email to django-users@googlegroups.com. Visit this group at http://groups.google.com/group/django-users. To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/CABtX16GWKggatron%3Dm1R%2B2La4-m8Gu%2B%2BP7ZjZtWAyN205Z1ihA%40mail.gmail.com. For more options, visit https://groups.google.com/d/optout.