Repository: tefra/xsdata
Pull request #993 (2024-03-23 18:09:19)

feat: Use unicodedata.name on non-alphanumeric attr names
## 📒 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=993) **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=993&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=993&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=993&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=993&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=993)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/993?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.78%. Comparing base [(`6820aa1`)](https://app.codecov.io/gh/tefra/xsdata/commit/6820aa1dc8ffd15a486dd2d845ff76cde4511db9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) to head [(`c4cc538`)](https://app.codecov.io/gh/tefra/xsdata/pull/993?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #993      +/-   ##
==========================================
- Coverage   99.94%   99.78%   -0.17%     
==========================================
  Files         117      117              
  Lines        9139     9143       +4     
  Branches     2168     2171       +3     
==========================================
- Hits         9134     9123      -11     
- Misses          4       16      +12     
- Partials        1        4       +3     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/993?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 091ab0a2..0b00cb2c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
       - id: end-of-file-fixer
       - id: debug-statements
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.3.3
+    rev: v0.3.4
     hooks:
       - id: ruff
         args: [ --fix, --show-fixes]
diff --git a/xsdata/codegen/handlers/create_compound_fields.py b/xsdata/codegen/handlers/create_compound_fields.py
index 2f1eb2ec..8f536038 100644
--- a/xsdata/codegen/handlers/create_compound_fields.py
+++ b/xsdata/codegen/handlers/create_compound_fields.py
@@ -252,7 +252,8 @@ class CreateCompoundFields(RelativeHandlerInterface):
         restrictions.sequence = None
 
         return Attr(
-            name=attr.local_name,
+            name=attr.name,
+            local_name=attr.local_name,
             namespace=attr.namespace,
             types=[x.clone() for x in attr.types],
             tag=attr.tag,
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index bd1cf95c..3acbd32b 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -1,6 +1,7 @@
 import copy
 import operator
 import sys
+import unicodedata
 from dataclasses import asdict, dataclass, field, fields, replace
 from enum import IntEnum
 from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, TypeVar
@@ -271,6 +272,7 @@ class Attr(CodegenModel):
         tag: The xml tag that produced this attr
         name: The final attr name
         local_name: The original attr name
+        wrapper: The wrapper element name
         index: The index position of this attr in the class
         default: The default value
         fixed: Specifies if the default value is fixed
@@ -286,7 +288,7 @@ class Attr(CodegenModel):
 
     tag: str
     name: str = field(compare=False)
-    local_name: str = field(init=False)
+    local_name: str = field(default="")
     wrapper: Optional[str] = field(default=None)
     index: int = field(compare=False, default_factory=int)
     default: Optional[str] = field(default=None, compare=False)
@@ -301,8 +303,12 @@ class Attr(CodegenModel):
     substitution: Optional[str] = field(default=None, compare=False)
 
     def __post_init__(self):
-        """Set the original attr name on init."""
-        self.local_name = self.name
+        """Post init processing."""
+        if not self.local_name:
+            self.local_name = self.name
+
+        if text.alnum(self.name) == "":
+            self.name = "_".join(unicodedata.name(char) for char in self.name)
 
     @property
     def key(self) -> str:
diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py
index 7b84db6c..3c1ae4b1 100644
--- a/xsdata/formats/dataclass/filters.py
+++ b/xsdata/formats/dataclass/filters.py
@@ -456,7 +456,10 @@ class Filters:
         return self.filter_metadata(metadata)
 
     def field_choices(
-        self, attr: Attr, parent_namespace: Optional[str], parents: List[str]
+        self,
+        attr: Attr,
+        parent_namespace: Optional[str],
+        parents: List[str],
     ) -> Optional[Tuple]:
         """Return a tuple of field metadata if the attr has choices."""
         if not attr.choices:
@@ -471,7 +474,7 @@ class Filters:
             )
 
             metadata = {
-                "name": choice.name,
+                "name": choice.local_name,
                 "wildcard": choice.is_wildcard,
                 "type": self.choice_type(choice, parents),
                 "namespace": namespace,


Test patch:
diff --git a/tests/codegen/handlers/test_create_compound_fields.py b/tests/codegen/handlers/test_create_compound_fields.py
index ff56e338..a17fad16 100644
--- a/tests/codegen/handlers/test_create_compound_fields.py
+++ b/tests/codegen/handlers/test_create_compound_fields.py
@@ -237,7 +237,8 @@ class CreateCompoundFieldsTests(FactoryTestCase):
 
         actual = self.processor.build_attr_choice(attr)
 
-        self.assertEqual(attr.local_name, actual.name)
+        self.assertEqual(attr.name, actual.name)
+        self.assertEqual(attr.local_name, actual.local_name)
         self.assertEqual(attr.namespace, actual.namespace)
         self.assertIsNone(actual.default)
         self.assertEqual(attr.tag, actual.tag)
diff --git a/tests/codegen/models/test_attr.py b/tests/codegen/models/test_attr.py
index 7578e12e..f754e1da 100644
--- a/tests/codegen/models/test_attr.py
+++ b/tests/codegen/models/test_attr.py
@@ -6,6 +6,15 @@ from xsdata.utils.testing import AttrFactory, AttrTypeFactory, FactoryTestCase
 
 
 class AttrTests(FactoryTestCase):
+    def test__post__init__(self):
+        attr = AttrFactory.create(name="$")
+        self.assertEqual("$", attr.local_name)
+        self.assertEqual("DOLLAR SIGN", attr.name)
+
+        attr = AttrFactory.create(local_name="$", name="dollar")
+        self.assertEqual("$", attr.local_name)
+        self.assertEqual("dollar", attr.name)
+
     def test__eq__(self):
         attr = AttrFactory.element()
         clone = attr.clone()
diff --git a/tests/codegen/test_utils.py b/tests/codegen/test_utils.py
index 28840227..d34540a2 100644
--- a/tests/codegen/test_utils.py
+++ b/tests/codegen/test_utils.py
@@ -324,8 +324,6 @@ class ClassUtilsTests(FactoryTestCase):
             AttrFactory.create(name="g[A]", tag=Tag.ENUMERATION),
             AttrFactory.create(name="g_a", tag=Tag.ENUMERATION),
             AttrFactory.create(name="g_a_1", tag=Tag.ENUMERATION),
-            AttrFactory.create(name="%", tag=Tag.ENUMERATION),
-            AttrFactory.create(name="$", tag=Tag.ENUMERATION),
         ]
         target = ClassFactory.create(attrs=attrs)
 
@@ -346,8 +344,6 @@ class ClassUtilsTests(FactoryTestCase):
             "g[A]_2",
             "g_a_3",
             "g_a_1",
-            "%",
-            "$_1",
         ]
         self.assertEqual(expected, [x.name for x in attrs])
 
diff --git a/tests/fixtures/annotations/units.py b/tests/fixtures/annotations/units.py
index bd3878da..ef8409bc 100644
--- a/tests/fixtures/annotations/units.py
+++ b/tests/fixtures/annotations/units.py
@@ -8,5 +8,5 @@ __NAMESPACE__ = "http://domain.org/schema/model/units"
 class unit(Enum):
     M = "m"
     KG = "kg"
-    VALUE = "%"
+    PERCENT_SIGN = "%"
     NA = "NA"
diff --git a/tests/formats/dataclass/test_filters.py b/tests/formats/dataclass/test_filters.py
index 3d7e8edb..d079924f 100644
--- a/tests/formats/dataclass/test_filters.py
+++ b/tests/formats/dataclass/test_filters.py
@@ -517,6 +517,7 @@ class FiltersTests(FactoryTestCase):
         attr = AttrFactory.create(
             choices=[
                 AttrFactory.element(
+                    name="$",
                     namespace="foo",
                     types=[type_float],
                     restrictions=Restrictions(max_exclusive="10"),
@@ -530,8 +531,8 @@ class FiltersTests(FactoryTestCase):
 
         actual = self.filters.field_choices(attr, "foo", ["a", "b"])
         expected = (
-            {"name": "attr_B", "type": "Type[float]", "max_exclusive": 10.0},
-            {"name": "attr_C", "namespace": "bar", "type": "Type[str]"},
+            {"name": "$", "type": "Type[float]", "max_exclusive": 10.0},
+            {"name": "attr_B", "namespace": "bar", "type": "Type[str]"},
             {
                 "namespace": "##other",
                 "wildcard": True,