From 4f474607de9b470f977a734bdd47590ab202e778 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Sun, 22 May 2016 12:43:56 -0400 Subject: [PATCH] Fixed #26646 -- Added IOBase methods required by TextIOWrapper to File. Thanks Tim for the review. --- django/core/files/base.py | 4 ---- django/core/files/temp.py | 9 -------- django/core/files/utils.py | 30 ++++++++++++++++++++----- docs/ref/files/file.txt | 9 ++++++-- docs/releases/1.11.txt | 4 +++- docs/spelling_wordlist | 1 + tests/files/tests.py | 45 ++++++++++++++++++++++++++++---------- 7 files changed, 69 insertions(+), 33 deletions(-) diff --git a/django/core/files/base.py b/django/core/files/base.py index a27d363658..11049a45de 100644 --- a/django/core/files/base.py +++ b/django/core/files/base.py @@ -64,10 +64,6 @@ class File(FileProxyMixin): size = property(_get_size, _set_size) - def _get_closed(self): - return not self.file or self.file.closed - closed = property(_get_closed) - def chunks(self, chunk_size=None): """ Read the file and yield chunks of ``chunk_size`` bytes (defaults to diff --git a/django/core/files/temp.py b/django/core/files/temp.py index f70d975dbe..39b61fd52e 100644 --- a/django/core/files/temp.py +++ b/django/core/files/temp.py @@ -58,15 +58,6 @@ if os.name == 'nt': except (OSError): pass - @property - def closed(self): - """ - This attribute needs to be accessible in certain situations, - because this class is supposed to mock the API of the class - tempfile.NamedTemporaryFile in the Python standard library. - """ - return self.file.closed - def __del__(self): self.close() diff --git a/django/core/files/utils.py b/django/core/files/utils.py index 2656fa7188..8e891bf23f 100644 --- a/django/core/files/utils.py +++ b/django/core/files/utils.py @@ -1,6 +1,3 @@ -from django.utils import six - - class FileProxyMixin(object): """ A mixin class used to forward file methods to an underlaying file @@ -27,8 +24,31 @@ class FileProxyMixin(object): write = property(lambda self: self.file.write) writelines = property(lambda self: self.file.writelines) xreadlines = property(lambda self: self.file.xreadlines) - if six.PY3: - seekable = property(lambda self: self.file.seekable) + + @property + def closed(self): + return not self.file or self.file.closed + + def readable(self): + if self.closed: + return False + if hasattr(self.file, 'readable'): + return self.file.readable() + return True + + def writable(self): + if self.closed: + return False + if hasattr(self.file, 'writable'): + return self.file.writable() + return 'w' in getattr(self.file, 'mode', '') + + def seekable(self): + if self.closed: + return False + if hasattr(self.file, 'seekable'): + return self.file.seekable() + return True def __iter__(self): return iter(self.file) diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index e88032ab66..6c218e38b3 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -92,8 +92,13 @@ The ``File`` class the following attributes and methods of its ``file`` object: ``encoding``, ``fileno``, ``flush``, ``isatty``, ``newlines``, ``read``, ``readinto``, ``readlines``, ``seek``, ``softspace``, ``tell``, - ``truncate``, ``writelines``, ``xreadlines``. If you are using - Python 3, the ``seekable`` method is also available. + ``truncate``, ``writelines``, ``xreadlines``, ``readable()``, + ``writable()``, and ``seekable()``. + + .. versionchanged:: 1.11 + + The ``readable()`` and ``writable()`` methods were added and the + ``seekable()`` method was made available on Python 2. .. currentmodule:: django.core.files.base diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index e2583a4dc4..6a1f1e0241 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -122,7 +122,9 @@ Email File Storage ~~~~~~~~~~~~ -* ... +* To make it wrappable by :class:`io.TextIOWrapper`, + :class:`~django.core.files.File` now has the ``readable()``, ``writable()``, + and ``seekable()`` methods. File Uploads ~~~~~~~~~~~~ diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index c9fb05f5ea..4355bbd065 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -972,6 +972,7 @@ wordcount wordwrap workflow worksforme +wrappable wsgi www xe diff --git a/tests/files/tests.py b/tests/files/tests.py index 6eb7f30d61..d22020cb8c 100644 --- a/tests/files/tests.py +++ b/tests/files/tests.py @@ -6,7 +6,7 @@ import os import struct import tempfile import unittest -from io import BytesIO, StringIO +from io import BytesIO, StringIO, TextIOWrapper from django.core.files import File from django.core.files.base import ContentFile @@ -120,18 +120,39 @@ class FileTests(unittest.TestCase): f = File(StringIO('one\ntwo\nthree')) self.assertEqual(list(f), ['one\n', 'two\n', 'three']) + def test_readable(self): + with tempfile.TemporaryFile() as temp, File(temp, name='something.txt') as test_file: + self.assertTrue(test_file.readable()) + self.assertFalse(test_file.readable()) + + def test_writable(self): + with tempfile.TemporaryFile() as temp, File(temp, name='something.txt') as test_file: + self.assertTrue(test_file.writable()) + self.assertFalse(test_file.writable()) + with tempfile.TemporaryFile('rb') as temp, File(temp, name='something.txt') as test_file: + self.assertFalse(test_file.writable()) + def test_seekable(self): - """ - File.seekable() should be available on Python 3. - """ - with tempfile.TemporaryFile() as temp: - temp.write(b"contents\n") - test_file = File(temp, name="something.txt") - if six.PY2: - self.assertFalse(hasattr(test_file, 'seekable')) - if six.PY3: - self.assertTrue(hasattr(test_file, 'seekable')) - self.assertTrue(test_file.seekable()) + with tempfile.TemporaryFile() as temp, File(temp, name='something.txt') as test_file: + self.assertTrue(test_file.seekable()) + self.assertFalse(test_file.seekable()) + + def test_io_wrapper(self): + content = "vive l'été\n" + with tempfile.TemporaryFile() as temp, File(temp, name='something.txt') as test_file: + test_file.write(content.encode('utf-8')) + test_file.seek(0) + wrapper = TextIOWrapper(test_file, 'utf-8', newline='\n') + self.assertEqual(wrapper.read(), content) + # The following seek() call is required on Windows Python 2 when + # switching from reading to writing. + wrapper.seek(0, 2) + wrapper.write(content) + wrapper.seek(0) + self.assertEqual(wrapper.read(), content * 2) + test_file = wrapper.detach() + test_file.seek(0) + self.assertEqual(test_file.read(), (content * 2).encode('utf-8')) class NoNameFileTestCase(unittest.TestCase):