Repository: pydicom/pydicom

Issue:
Second Deferred Read Fails For File-Like Objects
**Describe the bug**
Deferred reads fail when using a file-like object if two or more data elements are deferred.

This is somewhat related to https://github.com/pydicom/pydicom/issues/932.

**Steps To Reproduce**
```python
import io
import pydicom
import pathlib

filelike = io.BytesIO(pathlib.Path('file.dcm').read_bytes())
# Set defer_size to something stupidly small to force all data elements to be deferred
dataset = pydicom.dcmread(filelike, defer_size=1)
print(dataset)
```
**Expected behavior**
The above code should display the contents of the Dataset. It should display the same result as when read directly from an actual file. e.g. like this

```python
dataset = pydicom.dcmread('file.dcm', defer_size=1)
print(dataset)
``` 

**Actual behavior**
When run, the above shows an unhandled exception.
```
Traceback (most recent call last):
  File "/home/brian/pydicom/bug.py", line 8, in <module>
    print(dataset)
  File "/home/brian/.local/lib/python3.9/site-packages/pydicom/dataset.py", line 2298, in __str__
    return self._pretty_str()
  File "/home/brian/.local/lib/python3.9/site-packages/pydicom/dataset.py", line 2012, in _pretty_str
    for elem in self:
  File "/home/brian/.local/lib/python3.9/site-packages/pydicom/dataset.py", line 1242, in __iter__
    yield self[tag]
  File "/home/brian/.local/lib/python3.9/site-packages/pydicom/dataset.py", line 929, in __getitem__
    elem = read_deferred_data_element(
  File "/home/brian/.local/lib/python3.9/site-packages/pydicom/filereader.py", line 1187, in read_deferred_data_element
    fp.seek(raw_data_elem.value_tell - offset)
ValueError: I/O operation on closed file.
```
**Comments**
A look at `read_deferred_data_element` in `filereader.py` suggests that the file pointer is closed after each deferred read, whether or not the code actually opened the file in the first place.

```
    # Open the file, position to the right place
    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)
    # Seek back to the start of the deferred element
    fp.seek(raw_data_elem.value_tell - offset)
    elem_gen = data_element_generator(
        fp, is_implicit_VR, is_little_endian, defer_size=None
    )

    # Read the data element and check matches what was stored before
    # The first element out of the iterator should be the same type as the
    #   the deferred element == RawDataElement
    elem = cast(RawDataElement, next(elem_gen))
    fp.close()
```

This works in cases where the file is reopened but not when using a file-like object. My gut feeling is changing the last line to:
```
    if is_filename:
        fp.close()
```
would resolve the issue.

**Your environment**

module       | version
------       | -------
platform     | Linux-5.11.0-37-generic-x86_64-with-glibc2.31
Python       | 3.9.10 (main, Jan 15 2022, 18:17:56)  [GCC 9.3.0]
pydicom      | 2.2.2
gdcm         | 3.0.10
jpeg_ls      | _module not found_
numpy        | 1.21.4
PIL          | 7.0.0
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
> Merging [#1610](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (01ef1c8) into [master](https://codecov.io/gh/pydicom/pydicom/commit/0fa18d2a2179c92efc22200ed6b3689e66cecf92?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) (0fa18d2) will **decrease** coverage by `9.54%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/pydicom/pydicom/pull/1610/graphs/tree.svg?width=650&height=150&src=pr&token=YciLV15Lwr&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)

```diff
@@            Coverage Diff             @@
##           master    #1610      +/-   ##
==========================================
- Coverage   97.56%   88.02%   -9.55%     
==========================================
  Files          66       66              
  Lines       10645    10405     -240     
==========================================
- Hits        10386     9159    -1227     
- Misses        259     1246     +987     
```


| [Impacted Files](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | Coverage Δ | |
|---|---|---|
| [pydicom/filereader.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9maWxlcmVhZGVyLnB5) | `94.32% <100.00%> (ø)` | |
| [pydicom/pixel\_data\_handlers/util.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL3V0aWwucHk=) | `17.93% <0.00%> (-82.07%)` | :arrow_down: |
| [pydicom/waveforms/numpy\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS93YXZlZm9ybXMvbnVtcHlfaGFuZGxlci5weQ==) | `20.31% <0.00%> (-79.69%)` | :arrow_down: |
| [pydicom/encoders/gdcm.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9lbmNvZGVycy9nZGNtLnB5) | `20.96% <0.00%> (-75.81%)` | :arrow_down: |
| [pydicom/pixel\_data\_handlers/numpy\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL251bXB5X2hhbmRsZXIucHk=) | `27.14% <0.00%> (-72.86%)` | :arrow_down: |
| [pydicom/pixel\_data\_handlers/gdcm\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL2dkY21faGFuZGxlci5weQ==) | `26.22% <0.00%> (-66.46%)` | :arrow_down: |
| [pydicom/overlays/numpy\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9vdmVybGF5cy9udW1weV9oYW5kbGVyLnB5) | `35.18% <0.00%> (-64.82%)` | :arrow_down: |
| [pydicom/pixel\_data\_handlers/pillow\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL3BpbGxvd19oYW5kbGVyLnB5) | `31.73% <0.00%> (-64.43%)` | :arrow_down: |
| [pydicom/pixel\_data\_handlers/rle\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL3JsZV9oYW5kbGVyLnB5) | `36.44% <0.00%> (-63.56%)` | :arrow_down: |
| [pydicom/pixel\_data\_handlers/pylibjpeg\_handler.py](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom#diff-cHlkaWNvbS9waXhlbF9kYXRhX2hhbmRsZXJzL3B5bGlianBlZ19oYW5kbGVyLnB5) | `40.74% <0.00%> (-57.41%)` | :arrow_down: |
| ... and [18 more](https://codecov.io/gh/pydicom/pydicom/pull/1610/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Last update [0fa18d2...01ef1c8](https://codecov.io/gh/pydicom/pydicom/pull/1610?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).


Patch:
diff --git a/doc/release_notes/v2.3.0.rst b/doc/release_notes/v2.3.0.rst
index 0b3f7fee5..573255324 100644
--- a/doc/release_notes/v2.3.0.rst
+++ b/doc/release_notes/v2.3.0.rst
@@ -49,4 +49,6 @@ Fixes
   undefined length items (:issue:`1596`)
 * Assigning a list of length one as tag value is now correctly handled as
   assigning the single value (:issue:`1606`)
+* Fixed an exception with multiple deferred reads with file-like objects
+  (:issue:`1609`)
 
diff --git a/pydicom/filereader.py b/pydicom/filereader.py
index 458983b04..0396a0f1e 100644
--- a/pydicom/filereader.py
+++ b/pydicom/filereader.py
@@ -1190,7 +1190,8 @@ def read_deferred_data_element(
     # The first element out of the iterator should be the same type as the
     #   the deferred element == RawDataElement
     elem = cast(RawDataElement, next(elem_gen))
-    fp.close()
+    if is_filename:
+        fp.close()
     if elem.VR != raw_data_elem.VR:
         raise ValueError(
             f"Deferred read VR {elem.VR} does not match original "


Test patch:
diff --git a/pydicom/tests/test_filereader.py b/pydicom/tests/test_filereader.py
index fd212ce51..7375c2741 100644
--- a/pydicom/tests/test_filereader.py
+++ b/pydicom/tests/test_filereader.py
@@ -1522,6 +1522,10 @@ class TestDeferredRead:
         filelike = io.BytesIO(data)
         dataset = pydicom.dcmread(filelike, defer_size=1024)
         assert 32768 == len(dataset.PixelData)
+        # The 'Histogram tables' private data element is also > 1024 bytes so
+        # pluck this out to confirm multiple deferred reads work (#1609).
+        private_block = dataset.private_block(0x43, 'GEMS_PARM_01')
+        assert 2068 == len(private_block[0x29].value)
 
 
 class TestReadTruncatedFile: