Repository: pydicom/pydicom

Issue:
Deepcopy on throws on certain Dataset
**Describe the bug**
Doing a deepcopy() on a Dataset after an update() with another Dataset containing a Sequence throws `AttributeError: 'DataElement' object has no attribute 'VR'`. 
I'm using 2.4.1 (not sure why version info says otherwise).

**Expected behavior**
Using 2.3.1 I get a copy of the Dataset including the elements from the update-with Dataset.
With the steps to reproduce:
```python
(0048, 0105)  Optical Path Sequence  1 item(s) ---- 
   (0048, 0106) Optical Path Identifier             SH: '0'
   ---------
```

**Steps To Reproduce**
```python
from copy import deepcopy
from pydicom import Dataset, Sequence
optical_path_dataset = Dataset()
optical_path_item = Dataset()
optical_path_item.OpticalPathIdentifier = "0"
optical_path_dataset.OpticalPathSequence = Sequence([optical_path_item])
# Does not throw
deepcopy(optical_path_dataset)

# Also does not throw
dataset = Dataset()
dataset.update(deepcopy(optical_path_dataset))
deepcopy(dataset)

# Throws
dataset = Dataset()
dataset.update(optical_path_dataset)
deepcopy(dataset)
```
```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 18
     16 dataset = Dataset()
     17 dataset.update(optical_path_dataset)
---> 18 deepcopy(dataset)

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:153](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:153), in deepcopy(x, memo, _nil)
    151 copier = getattr(x, "__deepcopy__", None)
    152 if copier is not None:
--> 153     y = copier(memo)
    154 else:
    155     reductor = dispatch_table.get(cls)

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\site-packages\pydicom\dataset.py:477](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/site-packages/pydicom/dataset.py:477), in Dataset.__deepcopy__(self, memo)
    475     memo[id(self)] = copied   # add the new class to the memo
    476 # Insert a deepcopy of all instance attributes
--> 477 copied.__dict__.update(copy.deepcopy(self.__dict__, memo))
    478 # Manually update the weakref to be correct
    479 for elem in copied:  # DICOM elements

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:146](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:146), in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:231](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:231), in _deepcopy_dict(x, memo, deepcopy)
    229 memo[id(x)] = y
    230 for key, value in x.items():
--> 231     y[deepcopy(key, memo)] = deepcopy(value, memo)
    232 return y

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:146](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:146), in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:231](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:231), in _deepcopy_dict(x, memo, deepcopy)
    229 memo[id(x)] = y
    230 for key, value in x.items():
--> 231     y[deepcopy(key, memo)] = deepcopy(value, memo)
    232 return y

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:172](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:172), in deepcopy(x, memo, _nil)
    170                 y = x
    171             else:
--> 172                 y = _reconstruct(x, memo, *rv)
    174 # If is its own copy, don't memoize.
    175 if y is not x:

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:271](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:271), in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    269 if state is not None:
    270     if deep:
--> 271         state = deepcopy(state, memo)
    272     if hasattr(y, '__setstate__'):
    273         y.__setstate__(state)

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:146](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:146), in deepcopy(x, memo, _nil)
    144 copier = _deepcopy_dispatch.get(cls)
    145 if copier is not None:
--> 146     y = copier(x, memo)
    147 else:
    148     if issubclass(cls, type):

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:231](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:231), in _deepcopy_dict(x, memo, deepcopy)
    229 memo[id(x)] = y
    230 for key, value in x.items():
--> 231     y[deepcopy(key, memo)] = deepcopy(value, memo)
    232 return y

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\copy.py:153](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/copy.py:153), in deepcopy(x, memo, _nil)
    151 copier = getattr(x, "__deepcopy__", None)
    152 if copier is not None:
--> 153     y = copier(memo)
    154 else:
    155     reductor = dispatch_table.get(cls)

File [c:\Users\er-gac\.pyenv\pyenv-win\versions\3.11.2\Lib\site-packages\pydicom\dataset.py:480](file:///C:/Users/er-gac/.pyenv/pyenv-win/versions/3.11.2/Lib/site-packages/pydicom/dataset.py:480), in Dataset.__deepcopy__(self, memo)
    478 # Manually update the weakref to be correct
    479 for elem in copied:  # DICOM elements
--> 480     if elem.VR == VR_.SQ:
    481         elem.value.parent_dataset = copied  # setter does weakref
    482 return copied

AttributeError: 'DataElement' object has no attribute 'VR'
```

**Your environment**
module       | version
------       | -------
platform     | Windows-10-10.0.19045-SP0
Python       | 3.11.2 (tags/v3.11.2:878ead1, Feb  7 2023, 16:38:35) [MSC v.1934 64 bit (AMD64)]
pydicom      | 2.4.0
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | 1.24.0
PIL          | 9.4.0
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_


Discussion:
scaramallion: Is the `DataElement.parent` attribute linked to the removed `Dataset.parent` and `Sequence.parent` attributes? Should it also be removed?

Patch:
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index 403a65f7b..c33095f54 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -91,6 +91,8 @@ Fixes
 * Fixed a possible security issue with :class:`~pydicom.fileset.FileInstance` instances
   being able to escape the temporary directory when being added to a
   :class:`~pydicom.fileset.FileSet` (:issue:`1922`)
+* Fixed an ``AttributeError`` when running :py:func:`~copy.deepcopy` after
+  :meth:`Dataset.update<pydicom.dataset.Dataset.update>` (:issue:`1816`)
 
 Pydicom Internals
 -----------------
diff --git a/src/pydicom/dataelem.py b/src/pydicom/dataelem.py
index 28e00e6fe..105348b6a 100644
--- a/src/pydicom/dataelem.py
+++ b/src/pydicom/dataelem.py
@@ -8,6 +8,7 @@ A DataElement has a tag,
 """
 
 import base64
+import copy
 import json
 from typing import Optional, Any, TYPE_CHECKING, NamedTuple
 from collections.abc import Callable, MutableSequence
@@ -551,6 +552,18 @@ class DataElement:
         self.validate(val)
         return val
 
+    def __deepcopy__(self, memo: dict[int, Any] | None) -> "DataElement":
+        cls = self.__class__
+        copied = cls.__new__(cls)
+        if memo:
+            memo[id(self)] = copied
+
+        # Fix for #1816: don't deepcopy the parent!
+        for key in (k for k in self.__dict__ if k != "parent"):
+            copied.__dict__[key] = copy.deepcopy(self.__dict__[key], memo)
+
+        return copied
+
     def __eq__(self, other: Any) -> Any:
         """Compare `self` and `other` for equality.
 
diff --git a/src/pydicom/dataset.py b/src/pydicom/dataset.py
index 359a65e59..9f5f6dea7 100644
--- a/src/pydicom/dataset.py
+++ b/src/pydicom/dataset.py
@@ -467,6 +467,8 @@ class Dataset:
         for elem in copied:  # DICOM elements
             if elem.VR == VR_.SQ:
                 elem.value.parent_dataset = copied  # setter does weakref
+                elem.parent = copied
+
         return copied
 
     def __enter__(self) -> "Dataset":
@@ -2412,6 +2414,8 @@ class Dataset:
                 setattr(self, key, value)
             else:
                 self[Tag(cast(int, key))] = value
+                if getattr(value, "parent", None):
+                    value.parent = self  # type: ignore[union-attr]
 
     def iterall(self) -> Iterator[DataElement]:
         """Iterate through the :class:`Dataset`, yielding all the elements.


Test patch:
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index feca2d904..c774c42e5 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -2008,6 +2008,22 @@ class TestFileDataset:
         ds2 = copy.deepcopy(my_dataset_subclass)
         assert ds2.__class__ is MyDatasetSubclass
 
+    def test_deepcopy_after_update(self):
+        """Regression test for #1816"""
+        ds = Dataset()
+        ds.BeamSequence = []
+        elem = ds["BeamSequence"]
+        assert elem.parent is ds
+
+        ds2 = Dataset()
+        ds2.update(ds)
+        elem = ds2["BeamSequence"]
+        assert elem.parent is ds2
+
+        ds3 = copy.deepcopy(ds2)
+        elem = ds3["BeamSequence"]
+        assert elem.parent is ds3
+
 
 class TestDatasetOverlayArray:
     """Tests for Dataset.overlay_array()."""