Repository: pydicom/pydicom

Issue:
Deferred Read Fails For File-Like Objects
#### Description
Deferred reads are failing when dcmread is passed a file-like object (instead of a filepath).  There are two old issues from 2014 which describe the same issue which were apparently fixed, but I'm still seeing it on v1.3:
https://github.com/pydicom/pydicom/issues/104
https://github.com/pydicom/pydicom/issues/74

#### Steps/Code to Reproduce
```
import io
import pydicom

with open("./0.dcm", "rb") as fp:
   data = fp.read()
filelike = io.BytesIO(data)

dataset = pydicom.dcmread(filelike, defer_size=1024)
print(len(dataset.PixelData))
```

#### Expected Results
Pydicom should hold onto the supplied file-like and use that for the deferred read, rather than trying to grab the file-like's .name/.filename attr and use that to re-open.  It could also hold onto it's own open'd file-like (if supplied a file_path) and use that for deferred reads to simplify things.

#### Actual Results
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.6/dist-packages/pydicom/dataset.py", line 747, in __getattr__
    data_elem = self[tag]
  File "/usr/local/lib/python3.6/dist-packages/pydicom/dataset.py", line 826, in __getitem__
    data_elem)
  File "/usr/local/lib/python3.6/dist-packages/pydicom/filereader.py", line 911, in read_deferred_data_element
    raise IOError("Deferred read -- original filename not stored. "
OSError: Deferred read -- original filename not stored. Cannot re-open

#### Versions
Linux-4.18.0-25-generic-x86_64-with-Ubuntu-18.10-cosmic
Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
pydicom 1.3.0


Discussion:
This certainly makes sense, though deferred reads have not been implemented for file-like, so I see this as a feature request. 
@darcymason - I suggest to flag this for the 1.4 release.
Note that the two mentioned issues have not addressed this - the first one was not about deferred read, the second one was about deferred read from a gzip file (the fix just made sure the correct filename was used).

Patch:
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -2114,11 +2114,12 @@ def __init__(self,
         self.file_meta = file_meta
         self.is_implicit_VR = is_implicit_VR
         self.is_little_endian = is_little_endian
+        filename = None
         if isinstance(filename_or_obj, compat.string_types):
-            self.filename = filename_or_obj
+            filename = filename_or_obj
             self.fileobj_type = open
         elif isinstance(filename_or_obj, io.BufferedReader):
-            self.filename = filename_or_obj.name
+            filename = filename_or_obj.name
             # This is the appropriate constructor for io.BufferedReader
             self.fileobj_type = open
         else:
@@ -2126,17 +2127,20 @@ def __init__(self,
             # http://docs.python.org/reference/datamodel.html
             self.fileobj_type = filename_or_obj.__class__
             if getattr(filename_or_obj, "name", False):
-                self.filename = filename_or_obj.name
+                filename = filename_or_obj.name
             elif getattr(filename_or_obj, "filename",
                          False):  # gzip python <2.7?
-                self.filename = filename_or_obj.filename
+                filename = filename_or_obj.filename
             else:
                 # e.g. came from BytesIO or something file-like
-                self.filename = None
+                self.filename = filename_or_obj
+
         self.timestamp = None
-        if self.filename and os.path.exists(self.filename):
-            statinfo = os.stat(self.filename)
-            self.timestamp = statinfo.st_mtime
+        if filename:
+            self.filename = filename
+            if os.path.exists(filename):
+                statinfo = os.stat(filename)
+                self.timestamp = statinfo.st_mtime
 
     def __eq__(self, other):
         """Compare `self` and `other` for equality.
diff --git a/pydicom/filereader.py b/pydicom/filereader.py
--- a/pydicom/filereader.py
+++ b/pydicom/filereader.py
@@ -44,7 +44,7 @@ def data_element_generator(fp,
     Parameters
     ----------
     fp : file-like
-        The file like to read from.
+        The file-like to read from.
     is_implicit_VR : bool
         ``True`` if the data is encoded as implicit VR, ``False`` otherwise.
     is_little_endian : bool
@@ -912,28 +912,63 @@ def data_element_offset_to_value(is_implicit_VR, VR):
     return offset
 
 
-def read_deferred_data_element(fileobj_type, filename, timestamp,
+def read_deferred_data_element(fileobj_type, filename_or_obj, timestamp,
                                raw_data_elem):
     """Read the previously deferred value from the file into memory
-    and return a raw data element"""
+    and return a raw data element.
+
+    .. note:
+
+        This is called internally by pydicom and will normally not be
+        needed in user code.
+
+    Parameters
+    ----------
+    fileobj_type : type
+        The type of the original file object.
+    filename_or_obj : str or file-like
+        The filename of the original file if one exists, or the file-like
+        object where the data element persists.
+    timestamp : time or None
+        The time the original file has been read, if not a file-like.
+    raw_data_elem : dataelem.RawDataElement
+        The raw data element with no value set.
+
+    Returns
+    -------
+    dataelem.RawDataElement
+        The data element with the value set.
+
+    Raises
+    ------
+    IOError
+        If `filename_or_obj` is ``None``.
+    IOError
+        If `filename_or_obj` is a filename and the corresponding file does
+        not exist.
+    ValueError
+        If the VR or tag of `raw_data_elem` does not match the read value.
+    """
     logger.debug("Reading deferred element %r" % str(raw_data_elem.tag))
     # If it wasn't read from a file, then return an error
-    if filename is None:
+    if filename_or_obj is None:
         raise IOError("Deferred read -- original filename not stored. "
                       "Cannot re-open")
+    is_filename = isinstance(filename_or_obj, compat.string_types)
+
     # Check that the file is the same as when originally read
-    if not os.path.exists(filename):
+    if is_filename and not os.path.exists(filename_or_obj):
         raise IOError(u"Deferred read -- original file "
-                      "{0:s} is missing".format(filename))
+                      "{0:s} is missing".format(filename_or_obj))
     if timestamp is not None:
-        statinfo = os.stat(filename)
+        statinfo = os.stat(filename_or_obj)
         if statinfo.st_mtime != timestamp:
             warnings.warn("Deferred read warning -- file modification time "
                           "has changed.")
 
     # Open the file, position to the right place
-    # fp = self.typefileobj(self.filename, "rb")
-    fp = fileobj_type(filename, 'rb')
+    fp = (fileobj_type(filename_or_obj, 'rb')
+          if is_filename else filename_or_obj)
     is_implicit_VR = raw_data_elem.is_implicit_VR
     is_little_endian = raw_data_elem.is_little_endian
     offset = data_element_offset_to_value(is_implicit_VR, raw_data_elem.VR)


Test patch:
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -3,6 +3,7 @@
 """Unit tests for the pydicom.filereader module."""
 
 import gzip
+import io
 from io import BytesIO
 import os
 import shutil
@@ -918,6 +919,14 @@ def test_zipped_deferred(self):
         # the right place, it was re-opened as a normal file, not a zip file
         ds.InstanceNumber
 
+    def test_filelike_deferred(self):
+        """Deferred values work with file-like objects."""
+        with open(ct_name, 'rb') as fp:
+            data = fp.read()
+        filelike = io.BytesIO(data)
+        dataset = pydicom.dcmread(filelike, defer_size=1024)
+        assert 32768 == len(dataset.PixelData)
+
 
 class TestReadTruncatedFile(object):
     def testReadFileWithMissingPixelData(self):