Repository: tefra/xsdata
Pull request #598 (2021-10-27 16:01:04)

Adding config option fail_on_unknown_attributes
* Allows to raise a ParseError when an XML element has attributes,
  which were not defined as part of the known schema

Allows #597 

codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#598](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (b0b0136) into [master](https://codecov.io/gh/tefra/xsdata/commit/e47c78f1457aa21dca9ec26069264051ee30d11f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (e47c78f) will **decrease** coverage by `0.03%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/598/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

```diff
@@             Coverage Diff             @@
##            master     #598      +/-   ##
===========================================
- Coverage   100.00%   99.96%   -0.04%     
===========================================
  Files           94       94              
  Lines         8119     8122       +3     
  Branches      1409     1401       -8     
===========================================
  Hits          8119     8119              
- Misses           0        2       +2     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/dataclass/parsers/config.py](https://codecov.io/gh/tefra/xsdata/pull/598/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvY29uZmlnLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/parsers/nodes/element.py](https://codecov.io/gh/tefra/xsdata/pull/598/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvbm9kZXMvZWxlbWVudC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/formats/dataclass/typing.py](https://codecov.io/gh/tefra/xsdata/pull/598/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3R5cGluZy5weQ==) | `97.19% <0.00%> (-2.81%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [e47c78f...b0b0136](https://codecov.io/gh/tefra/xsdata/pull/598?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/docs/json.rst b/docs/json.rst
index 0ec63ea5..922131ab 100644
--- a/docs/json.rst
+++ b/docs/json.rst
@@ -115,6 +115,7 @@ errors through configuration.
     ...
     >>> config = ParserConfig(
     ...     fail_on_unknown_properties=False,
+    ...     fail_on_unknown_attributes=False,
     ... )
     >>> json_string = """{
     ...   "author": "Hightower, Kim",
diff --git a/docs/xml.rst b/docs/xml.rst
index 07da1558..2f448aa2 100644
--- a/docs/xml.rst
+++ b/docs/xml.rst
@@ -169,6 +169,7 @@ The configuration allows to enable/disable various features and failures.
     ...     base_url=None,
     ...     process_xinclude=False,
     ...     fail_on_unknown_properties=False,
+    ...     fail_on_unknown_attributes=False,
     ... )
     >>> parser = XmlParser(config=config)
     >>> order = parser.from_bytes(xml_path.read_bytes())
diff --git a/xsdata/formats/dataclass/parsers/config.py b/xsdata/formats/dataclass/parsers/config.py
index cca47fb9..a34f3f33 100644
--- a/xsdata/formats/dataclass/parsers/config.py
+++ b/xsdata/formats/dataclass/parsers/config.py
@@ -20,6 +20,8 @@ class ParserConfig:
     :param class_factory: Override default object instantiation
     :param fail_on_unknown_properties: Skip unknown properties or
         fail with exception
+    :param fail_on_unknown_attributes: Skip unknown XML attributes
+        or fail with exception
     :param fail_on_converter_warnings: Turn converter warnings to
         exceptions
     """
@@ -29,6 +31,7 @@ class ParserConfig:
         "process_xinclude",
         "class_factory",
         "fail_on_unknown_properties",
+        "fail_on_unknown_attributes",
         "fail_on_converter_warnings",
     )
 
@@ -38,10 +41,12 @@ class ParserConfig:
         process_xinclude: bool = False,
         class_factory: Callable[[Type[T], Dict], T] = default_class_factory,
         fail_on_unknown_properties: bool = True,
+        fail_on_unknown_attributes: bool = False,
         fail_on_converter_warnings: bool = False,
     ):
         self.base_url = base_url
         self.process_xinclude = process_xinclude
         self.class_factory = class_factory
         self.fail_on_unknown_properties = fail_on_unknown_properties
+        self.fail_on_unknown_attributes = fail_on_unknown_attributes
         self.fail_on_converter_warnings = fail_on_converter_warnings
diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py
index 001520a5..6f891588 100644
--- a/xsdata/formats/dataclass/parsers/nodes/element.py
+++ b/xsdata/formats/dataclass/parsers/nodes/element.py
@@ -135,6 +135,11 @@ class ElementNode(XmlNode):
                 var = self.meta.find_any_attributes(qname)
                 if var:
                     self.bind_any_attr(params, var, qname, value)
+                else:
+                    if self.config.fail_on_unknown_attributes:
+                        raise ParserError(
+                            f"Unknown attribute {self.meta.qname}:{qname}"
+                        )
 
     def bind_attr(self, params: Dict, var: XmlVar, value: Any):
         if var.init:


Test patch:
diff --git a/tests/formats/dataclass/parsers/nodes/test_element.py b/tests/formats/dataclass/parsers/nodes/test_element.py
index d7f4da9b..f71e0d8e 100644
--- a/tests/formats/dataclass/parsers/nodes/test_element.py
+++ b/tests/formats/dataclass/parsers/nodes/test_element.py
@@ -194,6 +194,33 @@ class ElementNodeTests(FactoryTestCase):
         expected = {"attrs": {"extended": "attr", "{what}ever": "qname"}, "index": 0}
         self.assertEqual(expected, params)
 
+    def test_bind_attrs_with_fail_on_unknown_attributes(self):
+        self.node.meta = self.context.build(AttrsType)
+        self.node.config.fail_on_unknown_attributes = True
+        self.node.attrs = {
+            "index": "0",
+            "fixed": "will be ignored",
+            "{what}ever": "qname",
+            "extended": "attr",
+        }
+
+        params = {}
+        self.node.bind_attrs(params)
+
+        expected = {"attrs": {"extended": "attr", "{what}ever": "qname"}, "index": 0}
+        self.assertEqual(expected, params)
+
+    def test_bind_with_fail_on_unknown_attributes(self):
+        self.node.meta = self.context.build(ExtendedType)
+        self.node.config.fail_on_unknown_attributes = True
+        self.node.attrs = {"a": "b"}
+
+        objects = [("a", "1")]
+        with self.assertRaises(ParserError) as cm:
+            self.node.bind("foo", "text", "tail", objects)
+
+        self.assertEqual("Unknown attribute ExtendedType:a", str(cm.exception))
+
     @mock.patch("xsdata.formats.dataclass.parsers.nodes.element.logger.warning")
     def test_bind_objects(self, mock_warning):
         self.node.meta = self.context.build(TypeC)