Repository: tefra/xsdata

Issue:
extending the SOAP Config NamedTuple...
Hello,

Recently you changed the xsdata.formats.dataclass.client.Config from a dataclass to a NamedTuple. However I'm having hard times trying to get my subclass Client, called FiscalClient(Client), use my own config class: FiscalConfig(Config):

```python
class FiscalConfig(Config):
    pkcs12_data: bytes = None
    pkcs12_password: str = None
    server: str = "undef"

class FiscalClient(Client):
    def __init__(
        self,
        config: Config,
    ):
    self.config = FiscalConfig(**config)
   # ... call super etc...
```


If I inherit the xsdata SOAP Config, then either my custom attributes are not taken into account (if I use the origin Config#from_service method for instance), either Config#new complains that it doens't know attributes defined in FiscalConfig like in the constructor above:

```Config.__new__() got an unexpected keyword argument 'ambiente```

It seems it boils down to how Python NamedTuple works: https://stackoverflow.com/questions/44287623/a-way-to-subclass-namedtuple-for-purposes-of-typechecking

Some posts like this one also suggest to use dataclass instead. I could do what I wanted when config was a dataclass. Is there a good reason it changed to NamedTupĺe? Do you have any hint how one can use its own SOAP config class? (I could work around it putting the attributes in FiscalClient directly but it looks strange). Thanks.

Discussion:

Patch:
diff --git a/xsdata/formats/dataclass/client.py b/xsdata/formats/dataclass/client.py
index 88241c9f..3c97d4d2 100644
--- a/xsdata/formats/dataclass/client.py
+++ b/xsdata/formats/dataclass/client.py
@@ -1,3 +1,4 @@
+from dataclasses import dataclass, fields
 from typing import Any, Dict, NamedTuple, Optional, Type, Union
 
 from xsdata.exceptions import ClientValueError
@@ -7,7 +8,8 @@ from xsdata.formats.dataclass.serializers import XmlSerializer
 from xsdata.formats.dataclass.transports import DefaultTransport, Transport
 
 
-class Config(NamedTuple):
+@dataclass(frozen=True)
+class Config:
     """Service configuration class.
 
     Attributes:
@@ -45,8 +47,8 @@ class Config(NamedTuple):
             A new config instance.
         """
         params = {
-            key: kwargs[key] if key in kwargs else getattr(obj, key, None)
-            for key in cls._fields
+            f.name: kwargs[f.name] if f.name in kwargs else getattr(obj, f.name, None)
+            for f in fields(cls)
         }
 
         return cls(**params)


Test patch:
diff --git a/tests/formats/dataclass/test_client.py b/tests/formats/dataclass/test_client.py
index a68a1198..f387a495 100644
--- a/tests/formats/dataclass/test_client.py
+++ b/tests/formats/dataclass/test_client.py
@@ -1,3 +1,4 @@
+from dataclasses import asdict, replace
 from unittest import TestCase, mock
 
 from tests.fixtures.calculator import (
@@ -43,7 +44,7 @@ class ClientTests(TestCase):
     def test_from_service(self):
         client = Client.from_service(CalculatorSoapAdd, location="http://testurl.com")
 
-        actual = client.config._asdict()
+        actual = asdict(client.config)
         expected = {
             "style": "document",
             "input": CalculatorSoapAddInput,
@@ -138,7 +139,7 @@ class ClientTests(TestCase):
         self.assertEqual({"content-type": "text/xml", "foo": "bar"}, result)
         self.assertEqual(1, len(headers))
 
-        config = config._replace(soap_action="add")
+        config = replace(config, soap_action="add")
         client = Client(config=config)
         result = client.prepare_headers({})
         self.assertEqual({"SOAPAction": "add", "content-type": "text/xml"}, result)