Repository: tefra/xsdata
Pull request #980 (2024-03-18 17:34:15)

feat: Use deepcopy to clone codegen models
## 📒 Description

> Write a brief description of your PR.

Resolves #xxxx

## 🔗 What I've Done

> Write a description of the steps taken to resolve the issue

## 💬 Comments

> A place to write any comments to the reviewer.

## 🛫 Checklist

- [ ] Updated docs
- [ ] Added unit-tests
- [ ] [Sample tests](https://github.com/tefra/xsdata-samples) pass
- [ ] [W3C tests](https://github.com/tefra/xsdata-w3c-tests) pass



Patch:
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index 5b73cd50..c58a60f3 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -1,6 +1,7 @@
+import copy
 import operator
 import sys
-from dataclasses import asdict, dataclass, field, replace
+from dataclasses import asdict, dataclass, field, fields, replace
 from enum import IntEnum
 from typing import Any, Dict, Iterator, List, Optional, Tuple, Type
 
@@ -32,6 +33,17 @@ GLOBAL_TYPES = (
 class CodegenModel:
     """Base codegen model."""
 
+    def clone(self, **kwargs: Any) -> "CodegenModel":
+        """Return a deep cloned instance."""
+        clone = copy.deepcopy(self)
+        return replace(clone, **kwargs) if kwargs else clone
+
+    def swap(self, source: "CodegenModel"):
+        """Swap the instance attributes from the source instance."""
+        for f in fields(self):
+            value = copy.deepcopy(getattr(source, f.name))
+            setattr(self, f.name, value)
+
 
 @dataclass
 class Restrictions(CodegenModel):
@@ -187,10 +199,6 @@ class Restrictions(CodegenModel):
 
         return result
 
-    def clone(self, **kwargs: Any) -> "Restrictions":
-        """Return a deep cloned instance and replace any args."""
-        return replace(self, **kwargs)
-
     @classmethod
     def from_element(cls, element: ElementBase) -> "Restrictions":
         """Static constructor from a xsd model.
@@ -252,10 +260,6 @@ class AttrType(CodegenModel):
             self.forward or self.native or (not allow_circular and self.circular)
         )
 
-    def clone(self, **kwargs: Any) -> "AttrType":
-        """Return a deep cloned instance."""
-        return replace(self, **kwargs)
-
 
 @dataclass
 class Attr(CodegenModel):
@@ -321,6 +325,11 @@ class Attr(CodegenModel):
         """Return whether this attr represents a xml attribute node."""
         return self.tag in (Tag.ATTRIBUTE, Tag.ANY_ATTRIBUTE)
 
+    @property
+    def is_element(self) -> bool:
+        """Return whether this attr represents a xml element."""
+        return self.tag == Tag.ELEMENT
+
     @property
     def is_enumeration(self) -> bool:
         """Return whether this attr an enumeration member."""
@@ -423,14 +432,6 @@ class Attr(CodegenModel):
         """Return the xml type this attribute is mapped to."""
         return xml_type_map.get(self.tag)
 
-    def clone(self) -> "Attr":
-        """Return a deep cloned instance."""
-        return replace(
-            self,
-            types=[x.clone() for x in self.types],
-            restrictions=self.restrictions.clone(),
-        )
-
     def get_native_types(self) -> Iterator[Type]:
         """Yield an iterator of all the native attr types."""
         for tp in self.types:
@@ -457,14 +458,6 @@ class Extension(CodegenModel):
     type: AttrType
     restrictions: Restrictions = field(hash=False)
 
-    def clone(self) -> "Extension":
-        """Return a deep cloned instance."""
-        return replace(
-            self,
-            type=self.type.clone(),
-            restrictions=self.restrictions.clone(),
-        )
-
 
 class Status(IntEnum):
     """Class process status enumeration."""
@@ -634,13 +627,6 @@ class Class(CodegenModel):
             "Type has not been assigned to a module yet!", type=self.qname
         )
 
-    def clone(self) -> "Class":
-        """Return a deep cloned instance."""
-        inners = [inner.clone() for inner in self.inner]
-        extensions = [extension.clone() for extension in self.extensions]
-        attrs = [attr.clone() for attr in self.attrs]
-        return replace(self, inner=inners, extensions=extensions, attrs=attrs)
-
     def dependencies(self, allow_circular: bool = False) -> Iterator[str]:
         """Yields all class dependencies.
 


Test patch:
diff --git a/tests/codegen/models/test_codegen.py b/tests/codegen/models/test_codegen.py
new file mode 100644
index 00000000..265a9ced
--- /dev/null
+++ b/tests/codegen/models/test_codegen.py
@@ -0,0 +1,35 @@
+import unittest
+from dataclasses import dataclass
+from typing import List
+
+from xsdata.codegen.models import CodegenModel
+
+
+@dataclass
+class Foo(CodegenModel):
+    bar: List["Bar"]
+
+    @dataclass
+    class Bar(CodegenModel):
+        foo: str
+
+
+class CodegenModelTests(unittest.TestCase):
+    def test_clone(self):
+        obj = Foo(bar=[Foo.Bar("one"), Foo.Bar("two")])
+        actual = obj.clone()
+
+        self.assertIsNot(obj, actual)
+        self.assertIsNot(obj.bar, actual.bar)
+        self.assertIsNot(obj.bar[0], actual.bar[0])
+        self.assertIsNot(obj.bar[1], actual.bar[1])
+
+    def test_swap(self):
+        obj = Foo(bar=[Foo.Bar("one")])
+        src = Foo(bar=[Foo.Bar("two")])
+
+        obj.swap(src)
+
+        self.assertEqual(src.bar, obj.bar)
+        self.assertIsNot(src.bar, obj.bar)
+        self.assertIsNot(src.bar[0], obj.bar[0])
diff --git a/tests/codegen/models/test_restrictions.py b/tests/codegen/models/test_restrictions.py
index 0b336cfe..b45ba341 100644
--- a/tests/codegen/models/test_restrictions.py
+++ b/tests/codegen/models/test_restrictions.py
@@ -137,10 +137,3 @@ class RestrictionsTests(TestCase):
 
         restrictions.process_contents = "strict"
         self.assertEqual({}, restrictions.asdict())
-
-    def test_clone(self):
-        restrictions = Restrictions(max_occurs=2)
-        clone = restrictions.clone()
-
-        self.assertEqual(clone, restrictions)
-        self.assertIsNot(clone, restrictions)