Repository: tefra/xsdata
Pull request #848 (2023-08-30 09:27:28)

Enhance compat layer 
## 📒 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/formats/dataclass/compat.py b/xsdata/formats/dataclass/compat.py
index c0db5bf9..7e2fe105 100644
--- a/xsdata/formats/dataclass/compat.py
+++ b/xsdata/formats/dataclass/compat.py
@@ -1,13 +1,15 @@
 import abc
-from dataclasses import Field
 from dataclasses import fields
 from dataclasses import is_dataclass
 from dataclasses import MISSING
 from typing import Any
+from typing import cast
 from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import NamedTuple
 from typing import Optional
 from typing import Set
-from typing import Tuple
 from typing import Type
 
 from xsdata.exceptions import XmlContextError
@@ -16,27 +18,35 @@ from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.utils.hooks import load_entry_points
 
 
+class FieldInfo(NamedTuple):
+    name: str
+    init: bool
+    metadata: Dict[str, Any]
+    default: Any
+    default_factory: Any
+
+
 class ClassType(abc.ABC):
     __slots__ = ()
 
     @property
     @abc.abstractmethod
     def any_element(self) -> Type:
-        """Return the any type used to bind wildcard element nodes."""
+        """Return the AnyElement used to bind wildcard element nodes."""
 
     @property
     @abc.abstractmethod
     def derived_element(self) -> Type:
-        """Return the derived type used to bind ambiguous element nodes."""
+        """Return the DerivedElement used to bind ambiguous element nodes."""
 
     @property
     def any_keys(self) -> Set[str]:
-        """Return the field names of the any type."""
+        """Return the field names of the AnyElement class."""
         return {field.name for field in self.get_fields(self.any_element)}
 
     @property
     def derived_keys(self) -> Set[str]:
-        """Return the field names of the derived type."""
+        """Return the field names of the DerivedElement class."""
         return {field.name for field in self.get_fields(self.derived_element)}
 
     @abc.abstractmethod
@@ -52,11 +62,11 @@ class ClassType(abc.ABC):
         """
 
     @abc.abstractmethod
-    def get_fields(self, obj: Any) -> Tuple[Any, ...]:
+    def get_fields(self, obj: Any) -> Iterator[FieldInfo]:
         """Return the models fields in the correct mro ordering."""
 
     @abc.abstractmethod
-    def default_value(self, field: Any, default: Optional[Any] = None) -> Any:
+    def default_value(self, field: FieldInfo, default: Optional[Any] = None) -> Any:
         """Return the default value or factory of the given model field."""
 
     @abc.abstractmethod
@@ -122,10 +132,10 @@ class Dataclasses(ClassType):
         if not self.is_model(obj):
             raise XmlContextError(f"Type '{obj}' is not a dataclass.")
 
-    def get_fields(self, obj: Any) -> Tuple[Any, ...]:
-        return fields(obj)
+    def get_fields(self, obj: Any) -> Iterator[FieldInfo]:
+        yield from cast(List[FieldInfo], fields(obj))
 
-    def default_value(self, field: Field, default: Optional[Any] = None) -> Any:
+    def default_value(self, field: FieldInfo, default: Optional[Any] = None) -> Any:
         if field.default_factory is not MISSING:
             return field.default_factory
 
diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py
index 57c318cd..e7e114b3 100644
--- a/xsdata/formats/dataclass/filters.py
+++ b/xsdata/formats/dataclass/filters.py
@@ -142,6 +142,7 @@ class Filters:
                 "clean_docstring": self.clean_docstring,
                 "import_module": self.import_module,
                 "import_class": self.import_class,
+                "post_meta_hook": self.post_meta_hook,
             }
         )
 
@@ -201,7 +202,9 @@ class Filters:
 
     def class_annotations(self, obj: Class, class_name: str) -> List[str]:
         """Return a list of decorator names."""
-        annotations = [self.default_class_annotation]
+        annotations = []
+        if self.default_class_annotation:
+            annotations.append(self.default_class_annotation)
 
         derived = len(obj.extensions) > 0
         for ext in self.extensions[ExtensionType.DECORATOR]:
@@ -346,6 +349,11 @@ class Filters:
 
         return self.class_name(name)
 
+    def post_meta_hook(self, obj: Class) -> Optional[str]:
+        """Plugin hook to render additional information after the xsdata meta
+        class."""
+        return None
+
     def field_metadata(
         self, attr: Attr, parent_namespace: Optional[str], parents: List[str]
     ) -> Dict:
diff --git a/xsdata/formats/dataclass/templates/class.jinja2 b/xsdata/formats/dataclass/templates/class.jinja2
index 63b79945..4075911a 100644
--- a/xsdata/formats/dataclass/templates/class.jinja2
+++ b/xsdata/formats/dataclass/templates/class.jinja2
@@ -10,6 +10,7 @@
 {% set local_name =  obj.meta_name or obj.name -%}
 {% set local_name = None if class_name == local_name or not global_type else local_name -%}
 {% set base_classes = obj | class_bases(class_name) | join(', ')-%}
+{% set post_meta_output = obj | post_meta_hook -%}
 {% set target_namespace = obj.target_namespace if global_type and module_namespace != obj.target_namespace else None %}
 
 {{ class_annotations | join('\n') }}
@@ -34,9 +35,12 @@ class {{ class_name }}{{"({})".format(base_classes) if base_classes }}:
         {%- if target_namespace and target_namespace != obj.namespace %}
         target_namespace = "{{ target_namespace }}"
         {%- endif %}
-{% elif obj.attrs|length == 0 and not help %}
+{% elif obj.attrs|length == 0 and not help and not extra_output %}
     pass
 {%- endif -%}
+{%- if post_meta_output %}
+{{ post_meta_output|indent(4, first=True) }}
+{%- endif -%}
 {%- for attr in obj.attrs %}
     {%- set field_typing = attr|field_type(parents) %}
     {%- set field_definition = attr|field_definition(obj.ns_map, parent_namespace, parents) %}
diff --git a/xsdata/utils/text.py b/xsdata/utils/text.py
index 5f9ac680..2dee853f 100644
--- a/xsdata/utils/text.py
+++ b/xsdata/utils/text.py
@@ -36,6 +36,7 @@ stop_words = {
     "else",
     "except",
     "field",
+    "Field",
     "finally",
     "float",
     "for",


Test patch:
diff --git a/tests/formats/dataclass/test_filters.py b/tests/formats/dataclass/test_filters.py
index 7c26011f..6b984741 100644
--- a/tests/formats/dataclass/test_filters.py
+++ b/tests/formats/dataclass/test_filters.py
@@ -142,6 +142,10 @@ class FiltersTests(FactoryTestCase):
         expected = self.filters.class_annotations(target, "FooBar")
         self.assertEqual(["@c", "@b", "@dataclass", "@d"], expected)
 
+        self.filters.default_class_annotation = None
+        expected = self.filters.class_annotations(target, "FooBar")
+        self.assertEqual(["@c", "@b", "@d"], expected)
+
     def test_field_name(self):
         self.filters.substitutions[ObjectType.FIELD]["abc"] = "cba"