From 196a48b4fd5cd388d5412d7aa0115e99b6e8c792 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 28 Feb 2014 15:57:53 -0800 Subject: [PATCH 1/5] added context manager support --- PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/PIL/Image.py b/PIL/Image.py index 75e7efc75..d7435d1ef 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -497,6 +497,25 @@ class Image: _makeself = _new # compatibility + # with compatibility + def __enter__(self): + return self + def __exit__(self, *args): + self.close() + + def close(self): + """ Close the file pointer, if possible. Destroy the image core. + This releases memory, and the image will be unusable afterward + """ + try: + self.fp.close() + except Exception as msg: + if Image.DEBUG: + print ("Error closing: %s" %msg) + + self.im = None + + def _copy(self): self.load() self.im = self.im.copy() From b27ef7646855d4e27d8e3dca528fd2e847756ac8 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 23:42:34 -0700 Subject: [PATCH 2/5] Rename import_err to something more general --- PIL/ImageCms.py | 4 ++-- PIL/_util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PIL/ImageCms.py b/PIL/ImageCms.py index c875712c1..363650250 100644 --- a/PIL/ImageCms.py +++ b/PIL/ImageCms.py @@ -89,8 +89,8 @@ try: except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. - from _util import import_err - _imagingcms = import_err(ex) + from _util import deferred_error + _imagingcms = deferred_error(ex) from PIL._util import isStringType core = _imagingcms diff --git a/PIL/_util.py b/PIL/_util.py index 761c258f1..eb5c2c242 100644 --- a/PIL/_util.py +++ b/PIL/_util.py @@ -15,7 +15,7 @@ else: def isDirectory(f): return isPath(f) and os.path.isdir(f) -class import_err(object): +class deferred_error(object): def __init__(self, ex): self.ex = ex def __getattr__(self, elt): From 3d352329f45139458bdf3b11a2352cc24b4c6c47 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 8 Apr 2014 23:43:13 -0700 Subject: [PATCH 3/5] Use the deferred error to provide a logical exception on access to a closed image --- PIL/Image.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 36060c759..99acb78fa 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -92,7 +92,7 @@ except ImportError: from PIL import ImageMode from PIL._binary import i8, o8 -from PIL._util import isPath, isStringType +from PIL._util import isPath, isStringType, deferred_error import os, sys @@ -513,7 +513,10 @@ class Image: if Image.DEBUG: print ("Error closing: %s" %msg) - self.im = None + # Instead of simply setting to None, we're setting up a + # deferred error that will better explain that the core image + # object is gone. + self.im = deferred_error(ValueError("Operation on closed image")) def _copy(self): From 8c6a4c0299f33aa05ddba19041d15b4c10684095 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 21:53:49 -0700 Subject: [PATCH 4/5] Docs changes for close/context manager --- PIL/Image.py | 27 ++++++++++++++++++--------- docs/handbook/tutorial.rst | 4 ++-- docs/reference/Image.rst | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/PIL/Image.py b/PIL/Image.py index 99acb78fa..333397701 100644 --- a/PIL/Image.py +++ b/PIL/Image.py @@ -497,16 +497,23 @@ class Image: _makeself = _new # compatibility - # with compatibility + # Context Manager Support def __enter__(self): return self def __exit__(self, *args): self.close() def close(self): - """ Close the file pointer, if possible. Destroy the image core. - This releases memory, and the image will be unusable afterward - """ + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release it's memory. + The image data will be unusable afterward. + + This function is only required to close images that have not + had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. + """ try: self.fp.close() except Exception as msg: @@ -664,7 +671,8 @@ class Image: Allocates storage for the image and loads the pixel data. In normal cases, you don't need to call this method, since the Image class automatically loads an opened image when it is - accessed for the first time. + accessed for the first time. This method will close the file + associated with the image. :returns: An image access object. """ @@ -2096,10 +2104,11 @@ def open(fp, mode="r"): """ Opens and identifies the given image file. - This is a lazy operation; this function identifies the file, but the - actual image data is not read from the file until you try to process - the data (or call the :py:meth:`~PIL.Image.Image.load` method). - See :py:func:`~PIL.Image.new`. + This is a lazy operation; this function identifies the file, but + the file remains open and the actual image data is not read from + the file until you try to process the data (or call the + :py:meth:`~PIL.Image.Image.load` method). See + :py:func:`~PIL.Image.new`. :param file: A filename (string) or a file object. The file object must implement :py:meth:`~file.read`, :py:meth:`~file.seek`, and diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index 9ce50da7d..05d619f40 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -126,8 +126,8 @@ Identify Image Files for infile in sys.argv[1:]: try: - im = Image.open(infile) - print(infile, im.format, "%dx%d" % im.size, im.mode) + with Image.open(infile) as im: + print(infile, im.format, "%dx%d" % im.size, im.mode) except IOError: pass diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index fe13c882b..7125fcad4 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -136,9 +136,9 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.verify .. automethod:: PIL.Image.Image.fromstring -.. deprecated:: 2.0 .. automethod:: PIL.Image.Image.load +.. automethod:: PIL.Image.Image.close Attributes ---------- From 471cecb5237b8481d0ce9469bd4974e6ab299836 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 17 Apr 2014 22:01:55 -0700 Subject: [PATCH 5/5] tests for close and context manager --- Tests/test_image_load.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index d28452c41..b385b9686 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -2,6 +2,8 @@ from tester import * from PIL import Image +import os + def test_sanity(): im = lena() @@ -9,3 +11,17 @@ def test_sanity(): pix = im.load() assert_equal(pix[0, 0], (223, 162, 133)) + +def test_close(): + im = Image.open("Images/lena.gif") + assert_no_exception(lambda: im.close()) + assert_exception(ValueError, lambda: im.load()) + assert_exception(ValueError, lambda: im.getpixel((0,0))) + +def test_contextmanager(): + fn = None + with Image.open("Images/lena.gif") as im: + fn = im.fp.fileno() + assert_no_exception(lambda: os.fstat(fn)) + + assert_exception(OSError, lambda: os.fstat(fn))