Repository: tefra/xsdata
Pull request #978 (2024-03-11 15:27:34)

feat: Capture namespace prefixes in user dicts
Resolves: #847

## 📒 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


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=978) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=978&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=978&metric=new_accepted_issues&view=list)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=978&resolved=false&inNewCodePeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=978&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=978)



Patch:
diff --git a/docs/data_binding/xml_parsing.md b/docs/data_binding/xml_parsing.md
index 7dada893..875cd9d9 100644
--- a/docs/data_binding/xml_parsing.md
+++ b/docs/data_binding/xml_parsing.md
@@ -120,6 +120,17 @@ all the imported modules to find a matching dataclass.
 
 ```
 
+## Capture namespace prefixes
+
+```python
+>>> from tests.fixtures.books.books import Books
+>>> ns_map = {}
+>>> order = parser.parse("tests/fixtures/books/books.xml", Books, ns_map=ns_map)
+>>> ns_map
+{'brk': 'urn:books'}
+
+```
+
 ## Alternative handlers
 
 XmlHandlers read the xml source and push build events to create the target class. xsData
diff --git a/xsdata/codegen/parsers/definitions.py b/xsdata/codegen/parsers/definitions.py
index 78394552..1cfc7f87 100644
--- a/xsdata/codegen/parsers/definitions.py
+++ b/xsdata/codegen/parsers/definitions.py
@@ -2,9 +2,9 @@ from dataclasses import dataclass
 from typing import Any, List, Optional
 
 from xsdata.codegen.parsers.schema import SchemaParser
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.parsers.bases import Parsed
 from xsdata.formats.dataclass.parsers.mixins import XmlNode
+from xsdata.formats.types import T
 from xsdata.models import wsdl
 
 
diff --git a/xsdata/codegen/parsers/schema.py b/xsdata/codegen/parsers/schema.py
index 2d724d39..9bf84608 100644
--- a/xsdata/codegen/parsers/schema.py
+++ b/xsdata/codegen/parsers/schema.py
@@ -3,10 +3,10 @@ from dataclasses import dataclass, field
 from typing import Any, Dict, List, Optional, Type, Union
 from urllib.parse import urljoin
 
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.parsers.bases import Parsed
 from xsdata.formats.dataclass.parsers.mixins import XmlNode
 from xsdata.formats.dataclass.parsers.xml import UserXmlParser
+from xsdata.formats.types import T
 from xsdata.models import xsd
 from xsdata.models.enums import FormType, Mode, Namespace
 from xsdata.models.mixins import ElementBase
diff --git a/xsdata/formats/bindings.py b/xsdata/formats/bindings.py
deleted file mode 100644
index 69e56679..00000000
--- a/xsdata/formats/bindings.py
+++ /dev/null
@@ -1,85 +0,0 @@
-import abc
-import io
-import pathlib
-from typing import Any, Optional, Type, TypeVar
-
-T = TypeVar("T")
-
-
-class AbstractSerializer(abc.ABC):
-    """Abstract serializer class."""
-
-    @abc.abstractmethod
-    def render(self, obj: Any) -> str:
-        """Serialize the input model instance to the output string format.
-
-        Args:
-            obj: The input model instance to serialize
-
-        Returns:
-            The serialized string output format.
-        """
-
-
-class AbstractParser(abc.ABC):
-    """Abstract parser class."""
-
-    def from_path(self, path: pathlib.Path, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input file into the target class type.
-
-        If no clazz is provided, the binding context will try
-        to locate it from imported dataclasses.
-
-        Args:
-            path: The path to the input file
-            clazz: The target class type to parse the file into
-
-        Returns:
-            An instance of the specified class representing the parsed content.
-        """
-        return self.parse(str(path.resolve()), clazz)
-
-    def from_string(self, source: str, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input source string into the target class type.
-
-        If no clazz is provided, the binding context will try
-        to locate it from imported dataclasses.
-
-        Args:
-            source: The source string to parse
-            clazz: The target class type to parse the source string into
-
-        Returns:
-            An instance of the specified class representing the parsed content.
-        """
-        return self.from_bytes(source.encode(), clazz)
-
-    def from_bytes(self, source: bytes, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input source bytes object into the target class type.
-
-        If no clazz is provided, the binding context will try
-        to locate it from imported dataclasses.
-
-        Args:
-            source: The source bytes object to parse
-            clazz: The target class type to parse the source bytes object
-
-        Returns:
-            An instance of the specified class representing the parsed content.
-        """
-        return self.parse(io.BytesIO(source), clazz)
-
-    @abc.abstractmethod
-    def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
-        """Parse the input file or stream into the target class type.
-
-        If no clazz is provided, the binding context will try
-        to locate it from imported dataclasses.
-
-        Args:
-            source: The source stream object to parse
-            clazz: The target class type to parse the source bytes object
-
-        Returns:
-            An instance of the specified class representing the parsed content.
-        """
diff --git a/xsdata/formats/dataclass/context.py b/xsdata/formats/dataclass/context.py
index 0095cc8e..719fccae 100644
--- a/xsdata/formats/dataclass/context.py
+++ b/xsdata/formats/dataclass/context.py
@@ -3,10 +3,10 @@ from collections import defaultdict
 from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Type
 
 from xsdata.exceptions import XmlContextError
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.compat import class_types
 from xsdata.formats.dataclass.models.builders import XmlMetaBuilder
 from xsdata.formats.dataclass.models.elements import XmlMeta
+from xsdata.formats.types import T
 from xsdata.models.enums import DataType
 from xsdata.utils.constants import return_input
 
diff --git a/xsdata/formats/dataclass/parsers/bases.py b/xsdata/formats/dataclass/parsers/bases.py
index a6a822a6..97205dc0 100644
--- a/xsdata/formats/dataclass/parsers/bases.py
+++ b/xsdata/formats/dataclass/parsers/bases.py
@@ -4,7 +4,6 @@ from dataclasses import dataclass, field
 from typing import Any, Dict, List, Optional, Tuple, Type, cast
 
 from xsdata.exceptions import ConverterWarning, ParserError
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.parsers.mixins import (
     EventsHandler,
@@ -13,6 +12,7 @@ from xsdata.formats.dataclass.parsers.mixins import (
     XmlNode,
 )
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.formats.types import T
 from xsdata.models.enums import EventType
 
 Parsed = Tuple[Optional[str], Any]
@@ -33,7 +33,12 @@ class NodeParser(PushParser):
     context: XmlContext = field(default_factory=XmlContext)
     handler: Type[XmlHandler] = field(default=EventsHandler)
 
-    def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
+    def parse(
+        self,
+        source: Any,
+        clazz: Optional[Type[T]] = None,
+        ns_map: Optional[Dict[Optional[str], str]] = None,
+    ) -> T:
         """Parse the input file or stream into the target class type.
 
         If no clazz is provided, the binding context will try
@@ -42,6 +47,7 @@ class NodeParser(PushParser):
         Args:
             source: The source file or stream object to parse
             clazz: The target class type to parse the source bytes object
+            ns_map: A namespace prefix-URI map to record prefixes during parsing
 
         Returns:
             An instance of the specified class representing the parsed content.
@@ -53,7 +59,8 @@ class NodeParser(PushParser):
                 warnings.filterwarnings("error", category=ConverterWarning)
 
             try:
-                result = handler.parse(source)
+                ns_map = self.ns_map if ns_map is None else ns_map
+                result = handler.parse(source, ns_map)
             except (ConverterWarning, SyntaxError) as e:
                 raise ParserError(e)
 
@@ -210,14 +217,17 @@ class RecordParser(NodeParser):
         self.events.append((EventType.END, qname, text, tail))
         return super().end(queue, objects, qname, text, tail)
 
-    def register_namespace(self, prefix: Optional[str], uri: str):
-        """Register the uri prefix in the namespace registry.
+    def register_namespace(
+        self, ns_map: Dict[Optional[str], str], prefix: Optional[str], uri: str
+    ):
+        """Register the uri prefix in the namespace prefix-URI map.
 
-        Record the start-ns event for later processing
+        Record the start-ns event for later processing.
 
         Args:
-            prefix: Namespace prefix
-            uri: Namespace uri
+            ns_map: The namespace prefix-URI map
+            prefix: The namespace prefix
+            uri: The namespace uri
         """
         self.events.append((EventType.START_NS, prefix, uri))
-        super().register_namespace(prefix, uri)
+        super().register_namespace(ns_map, prefix, uri)
diff --git a/xsdata/formats/dataclass/parsers/config.py b/xsdata/formats/dataclass/parsers/config.py
index 3a3d9490..bed20517 100644
--- a/xsdata/formats/dataclass/parsers/config.py
+++ b/xsdata/formats/dataclass/parsers/config.py
@@ -1,7 +1,7 @@
 from dataclasses import dataclass, field
 from typing import Any, Callable, Dict, Optional, Type
 
-from xsdata.formats.bindings import T
+from xsdata.formats.types import T
 
 
 def default_class_factory(cls: Type[T], params: Dict[str, Any]) -> T:
diff --git a/xsdata/formats/dataclass/parsers/dict.py b/xsdata/formats/dataclass/parsers/dict.py
index eb4566ab..45edcc8b 100644
--- a/xsdata/formats/dataclass/parsers/dict.py
+++ b/xsdata/formats/dataclass/parsers/dict.py
@@ -3,13 +3,13 @@ from dataclasses import dataclass, field
 from typing import Any, Dict, Iterable, List, Optional, Type, Union
 
 from xsdata.exceptions import ConverterWarning, ParserError
-from xsdata.formats.bindings import T
 from xsdata.formats.converter import converter
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
 from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
 from xsdata.formats.dataclass.typing import get_args, get_origin
+from xsdata.formats.types import T
 from xsdata.utils import collections
 from xsdata.utils.constants import EMPTY_MAP
 
diff --git a/xsdata/formats/dataclass/parsers/handlers/lxml.py b/xsdata/formats/dataclass/parsers/handlers/lxml.py
index cc7c986d..b5fd8fc3 100644
--- a/xsdata/formats/dataclass/parsers/handlers/lxml.py
+++ b/xsdata/formats/dataclass/parsers/handlers/lxml.py
@@ -1,4 +1,4 @@
-from typing import Any, Iterable, Tuple
+from typing import Any, Dict, Iterable, Optional, Tuple
 
 from lxml import etree
 
@@ -12,12 +12,13 @@ EVENTS = (EventType.START, EventType.END, EventType.START_NS)
 class LxmlEventHandler(XmlHandler):
     """An lxml event handler."""
 
-    def parse(self, source: Any) -> Any:
+    def parse(self, source: Any, ns_map: Dict[Optional[str], str]) -> Any:
         """Parse the source XML document.
 
         Args:
             source: The xml source, can be a file resource or an input stream,
                 or a lxml tree/element.
+            ns_map: A namespace prefix-URI recorder map
 
         Returns:
             An instance of the class type representing the parsed content.
@@ -37,13 +38,18 @@ class LxmlEventHandler(XmlHandler):
                 load_dtd=self.parser.config.load_dtd,
             )
 
-        return self.process_context(ctx)
+        return self.process_context(ctx, ns_map)
 
-    def process_context(self, context: Iterable[Tuple[str, Any]]) -> Any:
+    def process_context(
+        self,
+        context: Iterable[Tuple[str, Any]],
+        ns_map: Dict[Optional[str], str],
+    ) -> Any:
         """Iterate context and push events to main parser.
 
         Args:
             context: The iterable lxml context
+            ns_map: A namespace prefix-URI recorder map
 
         Returns:
             An instance of the class type representing the parsed content.
@@ -69,7 +75,7 @@ class LxmlEventHandler(XmlHandler):
                 element.clear()
             elif event == EventType.START_NS:
                 prefix, uri = element
-                self.parser.register_namespace(prefix or None, uri)
+                self.parser.register_namespace(ns_map, prefix or None, uri)
             else:
                 raise XmlHandlerError(f"Unhandled event: `{event}`.")
 
diff --git a/xsdata/formats/dataclass/parsers/handlers/native.py b/xsdata/formats/dataclass/parsers/handlers/native.py
index 0b6d6e24..f149d795 100644
--- a/xsdata/formats/dataclass/parsers/handlers/native.py
+++ b/xsdata/formats/dataclass/parsers/handlers/native.py
@@ -15,12 +15,13 @@ EVENTS = (EventType.START, EventType.END, EventType.START_NS)
 class XmlEventHandler(XmlHandler):
     """A native xml event handler."""
 
-    def parse(self, source: Any) -> Any:
+    def parse(self, source: Any, ns_map: Dict[Optional[str], str]) -> Any:
         """Parse the source XML document.
 
         Args:
             source: The xml source, can be a file resource or an input stream,
                 or a xml tree/element.
+            ns_map: A namespace prefix-URI recorder map
 
         Returns:
             An instance of the class type representing the parsed content.
@@ -40,18 +41,21 @@ class XmlEventHandler(XmlHandler):
         else:
             ctx = etree.iterparse(source, EVENTS)  # nosec
 
-        return self.process_context(ctx)
+        return self.process_context(ctx, ns_map)
 
-    def process_context(self, context: Iterable[Tuple[str, Any]]) -> Any:
+    def process_context(
+        self, context: Iterable[Tuple[str, Any]], ns_map: Dict[Optional[str], str]
+    ) -> Any:
         """Iterate context and push events to main parser.
 
         Args:
             context: The iterable xml context
+            ns_map: A namespace prefix-URI recorder map
 
         Returns:
             An instance of the class type representing the parsed content.
         """
-        ns_map: Dict = {}
+        element_ns_map: Dict = {}
         for event, element in context:
             if event == EventType.START:
                 self.parser.start(
@@ -60,9 +64,9 @@ class XmlEventHandler(XmlHandler):
                     self.objects,
                     element.tag,
                     element.attrib,
-                    self.merge_parent_namespaces(ns_map),
+                    self.merge_parent_namespaces(element_ns_map),
                 )
-                ns_map = {}
+                element_ns_map = {}
             elif event == EventType.END:
                 self.parser.end(
                     self.queue,
@@ -74,12 +78,41 @@ class XmlEventHandler(XmlHandler):
                 element.clear()
             elif event == EventType.START_NS:
                 prefix, uri = element
-                ns_map[prefix or None] = uri
+                prefix = prefix or None
+                element_ns_map[prefix] = uri
+                self.parser.register_namespace(ns_map, prefix, uri)
+
             else:
                 raise XmlHandlerError(f"Unhandled event: `{event}`.")
 
         return self.objects[-1][1] if self.objects else None
 
+    def merge_parent_namespaces(self, ns_map: Dict[Optional[str], str]) -> Dict:
+        """Merge the given prefix-URI map with the parent node map.
+
+        This method also registers new prefixes with the parser.
+
+        Args:
+            ns_map: The current element namespace prefix-URI map
+
+        Returns:
+            The new merged namespace prefix-URI map.
+        """
+        if self.queue:
+            parent_ns_map = self.queue[-1].ns_map
+
+            if not ns_map:
+                return parent_ns_map
+
+            result = parent_ns_map.copy() if parent_ns_map else {}
+        else:
+            result = {}
+
+        for prefix, uri in ns_map.items():
+            result[prefix] = uri
+
+        return result
+
 
 def iterwalk(element: etree.Element, ns_map: Dict) -> Iterator[Tuple[str, Any]]:
     """Walk over the element tree and emit events.
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index a280d9d9..0b95b0b1 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -1,13 +1,15 @@
+import io
 import json
+import pathlib
 from dataclasses import dataclass, field
 from typing import Any, Callable, Dict, List, Optional, Type, Union
 
-from xsdata.formats.bindings import AbstractParser, T
 from xsdata.formats.dataclass.parsers import DictDecoder
+from xsdata.formats.types import T
 
 
 @dataclass
-class JsonParser(DictDecoder, AbstractParser):
+class JsonParser(DictDecoder):
     """Json parser for data classes.
 
     Args:
@@ -18,6 +20,51 @@ class JsonParser(DictDecoder, AbstractParser):
 
     load_factory: Callable = field(default=json.load)
 
+    def from_path(self, path: pathlib.Path, clazz: Optional[Type[T]] = None) -> T:
+        """Parse the input file into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            path: The path to the input file
+            clazz: The target class type to parse the file into
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.parse(str(path.resolve()), clazz)
+
+    def from_string(self, source: str, clazz: Optional[Type[T]] = None) -> T:
+        """Parse the input source string into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            source: The source string to parse
+            clazz: The target class type to parse the source string into
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.from_bytes(source.encode(), clazz)
+
+    def from_bytes(self, source: bytes, clazz: Optional[Type[T]] = None) -> T:
+        """Parse the input source bytes object into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            source: The source bytes object to parse
+            clazz: The target class type to parse the source bytes object
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.parse(io.BytesIO(source), clazz)
+
     def parse(self, source: Any, clazz: Optional[Type[T]] = None) -> T:
         """Parse the input stream into the target class type.
 
diff --git a/xsdata/formats/dataclass/parsers/mixins.py b/xsdata/formats/dataclass/parsers/mixins.py
index ddc5cea2..9599aa73 100644
--- a/xsdata/formats/dataclass/parsers/mixins.py
+++ b/xsdata/formats/dataclass/parsers/mixins.py
@@ -1,17 +1,17 @@
 import abc
+import io
+import pathlib
 from dataclasses import dataclass, field
 from typing import Any, Dict, List, Optional, Tuple, Type
 
 from xsdata.exceptions import XmlHandlerError
-from xsdata.formats.bindings import AbstractParser
 from xsdata.formats.dataclass.parsers.config import ParserConfig
+from xsdata.formats.types import T
 from xsdata.models.enums import EventType
 
-NoneStr = Optional[str]
-
 
 @dataclass
-class PushParser(AbstractParser):
+class PushParser:
     """A generic interface for event based content handlers like sax.
 
     Args:
@@ -24,6 +24,90 @@ class PushParser(AbstractParser):
     config: ParserConfig = field(default_factory=ParserConfig)
     ns_map: Dict[Optional[str], str] = field(init=False, default_factory=dict)
 
+    def from_path(
+        self,
+        path: pathlib.Path,
+        clazz: Optional[Type[T]] = None,
+        ns_map: Optional[Dict[Optional[str], str]] = None,
+    ) -> T:
+        """Parse the input file into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            path: The path to the input file
+            clazz: The target class type to parse the file into
+            ns_map: A namespace prefix-URI map to record prefixes during parsing
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.parse(str(path.resolve()), clazz, ns_map)
+
+    def from_string(
+        self,
+        source: str,
+        clazz: Optional[Type[T]] = None,
+        ns_map: Optional[Dict[Optional[str], str]] = None,
+    ) -> T:
+        """Parse the input source string into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            source: The source string to parse
+            clazz: The target class type to parse the source string into
+            ns_map: A namespace prefix-URI map to record prefixes during parsing
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.from_bytes(source.encode(), clazz, ns_map)
+
+    def from_bytes(
+        self,
+        source: bytes,
+        clazz: Optional[Type[T]] = None,
+        ns_map: Optional[Dict[Optional[str], str]] = None,
+    ) -> T:
+        """Parse the input source bytes object into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            source: The source bytes object to parse
+            clazz: The target class type to parse the source bytes object
+            ns_map: A namespace prefix-URI map to record prefixes during parsing
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+        return self.parse(io.BytesIO(source), clazz, ns_map)
+
+    @abc.abstractmethod
+    def parse(
+        self,
+        source: Any,
+        clazz: Optional[Type[T]] = None,
+        ns_map: Optional[Dict[Optional[str], str]] = None,
+    ) -> T:
+        """Parse the input file or stream into the target class type.
+
+        If no clazz is provided, the binding context will try
+        to locate it from imported dataclasses.
+
+        Args:
+            source: The source stream object to parse
+            clazz: The target class type to parse the source bytes object
+            ns_map: A namespace prefix-URI map to record prefixes during parsing
+
+        Returns:
+            An instance of the specified class representing the parsed content.
+        """
+
     @abc.abstractmethod
     def start(
         self,
@@ -51,8 +135,8 @@ class PushParser(AbstractParser):
         queue: List,
         objects: List,
         qname: str,
-        text: NoneStr,
-        tail: NoneStr,
+        text: Optional[str],
+        tail: Optional[str],
     ) -> bool:
         """Parse the last xml node and bind any intermediate objects.
 
@@ -67,15 +151,18 @@ class PushParser(AbstractParser):
             Whether the binding process was successful.
         """
 
-    def register_namespace(self, prefix: NoneStr, uri: str):
-        """Register the uri prefix in the namespace registry.
+    def register_namespace(
+        self, ns_map: Dict[Optional[str], str], prefix: Optional[str], uri: str
+    ):
+        """Register the uri prefix in the namespace prefix-URI map.
 
         Args:
-            prefix: Namespace prefix
-            uri: Namespace uri
+            ns_map: The namespace prefix-URI map
+            prefix: The namespace prefix
+            uri: The namespace uri
         """
-        if prefix not in self.ns_map:
-            self.ns_map[prefix] = uri
+        if prefix not in ns_map:
+            ns_map[prefix] = uri
 
 
 class XmlNode(abc.ABC):
@@ -109,8 +196,8 @@ class XmlNode(abc.ABC):
     def bind(
         self,
         qname: str,
-        text: NoneStr,
-        tail: NoneStr,
+        text: Optional[str],
+        tail: Optional[str],
         objects: List[Any],
     ) -> bool:
         """Bind the parsed data into an object for the ending element.
@@ -150,49 +237,23 @@ class XmlHandler:
         self.queue: List = []
         self.objects: List = []
 
-    def parse(self, source: Any) -> Any:
+    def parse(self, source: Any, ns_map: Dict[Optional[str], str]) -> Any:
         """Parse the source XML document.
 
         Args:
             source: The xml source, can be a file resource or an input stream.
+            ns_map: A dictionary to capture namespace prefixes.
 
         Returns:
             An instance of the class type representing the parsed content.
         """
         raise NotImplementedError("This method must be implemented!")
 
-    def merge_parent_namespaces(self, ns_map: Dict[Optional[str], str]) -> Dict:
-        """Merge the given prefix-URI map with the parent node map.
-
-        This method also registers new prefixes with the parser.
-
-        Args:
-            ns_map: The current element namespace prefix-URI map
-
-        Returns:
-            The new merged namespace prefix-URI map.
-        """
-        if self.queue:
-            parent_ns_map = self.queue[-1].ns_map
-
-            if not ns_map:
-                return parent_ns_map
-
-            result = parent_ns_map.copy() if parent_ns_map else {}
-        else:
-            result = {}
-
-        for prefix, uri in ns_map.items():
-            self.parser.register_namespace(prefix, uri)
-            result[prefix] = uri
-
-        return result
-
 
 class EventsHandler(XmlHandler):
     """Sax content handler for pre-recorded events."""
 
-    def parse(self, source: List[Tuple]) -> Any:
+    def parse(self, source: List[Tuple], ns_map: Dict[Optional[str], str]) -> Any:
         """Forward the pre-recorded events to the main parser.
 
         Args:
@@ -203,21 +264,21 @@ class EventsHandler(XmlHandler):
         """
         for event, *args in source:
             if event == EventType.START:
-                qname, attrs, ns_map = args
+                qname, attrs, element_ns_map = args
                 self.parser.start(
                     self.clazz,
                     self.queue,
                     self.objects,
                     qname,
                     attrs,
-                    ns_map,
+                    element_ns_map,
                 )
             elif event == EventType.END:
                 qname, text, tail = args
                 self.parser.end(self.queue, self.objects, qname, text, tail)
             elif event == EventType.START_NS:
                 prefix, uri = args
-                self.parser.register_namespace(prefix or None, uri)
+                self.parser.register_namespace(ns_map, prefix or None, uri)
             else:
                 raise XmlHandlerError(f"Unhandled event: `{event}`.")
 
diff --git a/xsdata/formats/dataclass/parsers/nodes/union.py b/xsdata/formats/dataclass/parsers/nodes/union.py
index d22604de..5d42b453 100644
--- a/xsdata/formats/dataclass/parsers/nodes/union.py
+++ b/xsdata/formats/dataclass/parsers/nodes/union.py
@@ -3,13 +3,13 @@ import warnings
 from typing import Any, Dict, List, Optional, Tuple, Type
 
 from xsdata.exceptions import ConverterWarning, ParserError
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.formats.dataclass.models.elements import XmlVar
 from xsdata.formats.dataclass.parsers.bases import NodeParser
 from xsdata.formats.dataclass.parsers.config import ParserConfig
 from xsdata.formats.dataclass.parsers.mixins import EventsHandler, XmlNode
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.formats.types import T
 from xsdata.utils.namespaces import target_uri
 
 
diff --git a/xsdata/formats/dataclass/serializers/code.py b/xsdata/formats/dataclass/serializers/code.py
index e555436a..a1c39b53 100644
--- a/xsdata/formats/dataclass/serializers/code.py
+++ b/xsdata/formats/dataclass/serializers/code.py
@@ -3,7 +3,6 @@ from enum import Enum
 from io import StringIO
 from typing import Any, Iterator, List, Mapping, Set, TextIO, Tuple, Type, Union
 
-from xsdata.formats.bindings import AbstractSerializer
 from xsdata.formats.dataclass.context import XmlContext
 from xsdata.utils import collections
 from xsdata.utils.objects import literal_value
@@ -15,7 +14,7 @@ unset = object()
 
 
 @dataclass
-class PycodeSerializer(AbstractSerializer):
+class PycodeSerializer:
     """Pycode serializer for data class instances.
 
     Generate python pretty representation code from a model instance.
diff --git a/xsdata/formats/dataclass/serializers/json.py b/xsdata/formats/dataclass/serializers/json.py
index 77b7e5ef..506028ed 100644
--- a/xsdata/formats/dataclass/serializers/json.py
+++ b/xsdata/formats/dataclass/serializers/json.py
@@ -3,12 +3,11 @@ from dataclasses import dataclass, field
 from io import StringIO
 from typing import Any, Callable, TextIO
 
-from xsdata.formats.bindings import AbstractSerializer
 from xsdata.formats.dataclass.serializers import DictEncoder
 
 
 @dataclass
-class JsonSerializer(DictEncoder, AbstractSerializer):
+class JsonSerializer(DictEncoder):
     """Json serializer for data classes.
 
     Args:
diff --git a/xsdata/formats/dataclass/serializers/tree/lxml.py b/xsdata/formats/dataclass/serializers/tree/lxml.py
index ea796caf..72552856 100644
--- a/xsdata/formats/dataclass/serializers/tree/lxml.py
+++ b/xsdata/formats/dataclass/serializers/tree/lxml.py
@@ -2,8 +2,8 @@ from dataclasses import dataclass
 
 from lxml.etree import Element, TreeBuilder
 
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.serializers.tree.mixins import TreeSerializer
+from xsdata.formats.types import T
 
 
 @dataclass
diff --git a/xsdata/formats/dataclass/serializers/tree/mixins.py b/xsdata/formats/dataclass/serializers/tree/mixins.py
index 10c7bc6f..d1fa703c 100644
--- a/xsdata/formats/dataclass/serializers/tree/mixins.py
+++ b/xsdata/formats/dataclass/serializers/tree/mixins.py
@@ -3,8 +3,8 @@ from dataclasses import dataclass
 from typing import Any, Dict, Protocol
 
 from xsdata.exceptions import XmlHandlerError
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.serializers.mixins import EventGenerator, XmlWriterEvent
+from xsdata.formats.types import T
 from xsdata.models.enums import EventType
 
 
diff --git a/xsdata/formats/dataclass/serializers/tree/native.py b/xsdata/formats/dataclass/serializers/tree/native.py
index c64eed52..5a21a2f5 100644
--- a/xsdata/formats/dataclass/serializers/tree/native.py
+++ b/xsdata/formats/dataclass/serializers/tree/native.py
@@ -1,8 +1,8 @@
 from dataclasses import dataclass
 from xml.etree.ElementTree import Element, TreeBuilder
 
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.serializers.tree.mixins import TreeSerializer
+from xsdata.formats.types import T
 
 
 @dataclass
diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py
index df0a23c2..f45d8025 100644
--- a/xsdata/formats/dataclass/serializers/xml.py
+++ b/xsdata/formats/dataclass/serializers/xml.py
@@ -1,39 +1,23 @@
 from dataclasses import dataclass, field
-from enum import Enum
 from io import StringIO
 from typing import (
     Any,
     Dict,
-    Iterable,
-    Iterator,
-    List,
     Optional,
     TextIO,
-    Tuple,
     Type,
 )
-from xml.etree.ElementTree import QName
 
-from xsdata.exceptions import SerializerError
-from xsdata.formats.bindings import AbstractSerializer
-from xsdata.formats.converter import converter
-from xsdata.formats.dataclass.context import XmlContext
-from xsdata.formats.dataclass.models.elements import XmlMeta, XmlVar
-from xsdata.formats.dataclass.serializers.config import SerializerConfig
 from xsdata.formats.dataclass.serializers.mixins import (
     EventGenerator,
-    EventIterator,
     XmlWriter,
-    XmlWriterEvent,
 )
 from xsdata.formats.dataclass.serializers.writers import default_writer
-from xsdata.models.enums import DataType, QNames
-from xsdata.utils import collections, namespaces
-from xsdata.utils.constants import EMPTY_MAP
+from xsdata.utils import namespaces
 
 
 @dataclass
-class XmlSerializer(EventGenerator, AbstractSerializer):
+class XmlSerializer(EventGenerator):
     """Xml serializer for data classes.
 
     Args:
diff --git a/xsdata/formats/types.py b/xsdata/formats/types.py
new file mode 100644
index 00000000..b186d5ad
--- /dev/null
+++ b/xsdata/formats/types.py
@@ -0,0 +1,3 @@
+from typing import TypeVar
+
+T = TypeVar("T")


Test patch:
diff --git a/tests/formats/dataclass/parsers/handlers/test_lxml.py b/tests/formats/dataclass/parsers/handlers/test_lxml.py
index ee883404..fb91abbd 100644
--- a/tests/formats/dataclass/parsers/handlers/test_lxml.py
+++ b/tests/formats/dataclass/parsers/handlers/test_lxml.py
@@ -58,7 +58,7 @@ class LxmlEventHandlerTests(TestCase):
         handler = LxmlEventHandler(clazz=Books, parser=self.parser)
 
         with self.assertRaises(XmlHandlerError) as cm:
-            handler.process_context([("reverse", "")])
+            handler.process_context([("reverse", "")], {})
 
         self.assertEqual("Unhandled event: `reverse`.", str(cm.exception))
 
diff --git a/tests/formats/dataclass/parsers/handlers/test_native.py b/tests/formats/dataclass/parsers/handlers/test_native.py
index ba415893..ac927abd 100644
--- a/tests/formats/dataclass/parsers/handlers/test_native.py
+++ b/tests/formats/dataclass/parsers/handlers/test_native.py
@@ -34,7 +34,7 @@ class XmlEventHandlerTests(TestCase):
         handler = XmlEventHandler(parser=self.parser, clazz=Books)
 
         with self.assertRaises(XmlHandlerError) as cm:
-            handler.process_context(context)
+            handler.process_context(context, {})
 
         self.assertEqual("Unhandled event: `reverse`.", str(cm.exception))
 
diff --git a/tests/formats/dataclass/parsers/test_mixins.py b/tests/formats/dataclass/parsers/test_mixins.py
index be0e5222..a05dec2e 100644
--- a/tests/formats/dataclass/parsers/test_mixins.py
+++ b/tests/formats/dataclass/parsers/test_mixins.py
@@ -16,7 +16,7 @@ class XmlHandlerTests(TestCase):
         self.assertEqual([], handler.objects)
 
         with self.assertRaises(NotImplementedError):
-            handler.parse(None)
+            handler.parse(None, {})
 
 
 class EventsHandlerTests(TestCase):
diff --git a/tests/formats/dataclass/parsers/test_node.py b/tests/formats/dataclass/parsers/test_node.py
index caf86cd0..f9e3dc52 100644
--- a/tests/formats/dataclass/parsers/test_node.py
+++ b/tests/formats/dataclass/parsers/test_node.py
@@ -1,5 +1,5 @@
 from dataclasses import make_dataclass
-from typing import Any
+from typing import Any, Dict
 from unittest import mock
 from unittest.case import TestCase
 
@@ -25,11 +25,11 @@ class NodeParserTests(TestCase):
 
     def test_parse(self):
         class TestHandler(XmlHandler):
-            def parse(self, source: Any) -> Any:
+            def parse(self, source: Any, ns_map: Dict) -> Any:
                 return Books()
 
         self.parser.handler = TestHandler
-        self.assertEqual(Books(), self.parser.parse([], Books))
+        self.assertEqual(Books(), self.parser.parse([], Books, {}))
 
     def test_parse_when_result_type_is_wrong(self):
         parser = self.parser
@@ -199,12 +199,13 @@ class NodeParserTests(TestCase):
 
     def test_register_namespace(self):
         parser = NodeParser()
-        parser.register_namespace("bar", "foo")
-        parser.register_namespace("bar", "exists")
-        self.assertEqual({"bar": "foo"}, parser.ns_map)
+        ns_map = {}
+        parser.register_namespace(ns_map, "bar", "foo")
+        parser.register_namespace(ns_map, "bar", "exists")
+        self.assertEqual({"bar": "foo"}, ns_map)
 
-        parser.register_namespace(None, "a")
-        self.assertEqual({"bar": "foo", None: "a"}, parser.ns_map)
+        parser.register_namespace(ns_map, None, "a")
+        self.assertEqual({"bar": "foo", None: "a"}, ns_map)
 
-        parser.register_namespace(None, "b")
-        self.assertEqual({"bar": "foo", None: "a"}, parser.ns_map)
+        parser.register_namespace(ns_map, None, "b")
+        self.assertEqual({"bar": "foo", None: "a"}, ns_map)
diff --git a/tests/formats/dataclass/test_typing.py b/tests/formats/dataclass/test_typing.py
index 1c9a7725..9b1b419d 100644
--- a/tests/formats/dataclass/test_typing.py
+++ b/tests/formats/dataclass/test_typing.py
@@ -6,8 +6,8 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
 from unittest import TestCase
 from xml.etree.ElementTree import QName
 
-from xsdata.formats.bindings import T
 from xsdata.formats.dataclass.typing import evaluate
+from xsdata.formats.types import T
 from xsdata.models.datatype import XmlDate, XmlDateTime, XmlDuration, XmlPeriod, XmlTime
 from xsdata.models.enums import Namespace