Repository: tefra/xsdata

Issue:
v24 and #881 break private package names
I found another breakage in my code generation as of v24, specifically, added by [this line](https://github.com/tefra/xsdata/pull/881/files#diff-f1c09b10ffdb9dea2adce211aad87e08882a703be2165c8c7e2c163deb69b34cR322) in #881

When `CodeWriter.write`, calls `Generator.normalize_packages`, it ultimately calls `DataclassGenerator.package_name` -> `Filters.package_name` -> `Filters.safe_name` -> `text.variable`

The problem is that `DataclassGenerator.package_name('my.valid._but._private.package')` now returns `'my.valid.but.private.package'` ... so all of my outputs have been changed to a new module name. 

Discussion:
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=950) **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=950&resolved=false&inNewCodePeriod=true)

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=950&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=950&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=950)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/tefra/xsdata/pull/950?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:
> Comparison is base [(`b4b6a58`)](https://app.codecov.io/gh/tefra/xsdata/commit/b4b6a58bd33ee8a22b06188689ff0b7178d4900c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) 99.97% compared to head [(`ebfc0a7`)](https://app.codecov.io/gh/tefra/xsdata/pull/950?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) 99.97%.


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


```diff
@@            Coverage Diff             @@
##             main     #950      +/-   ##
==========================================
- Coverage   99.97%   99.97%   -0.01%     
==========================================
  Files         109      109              
  Lines        8763     8761       -2     
  Branches     2075     2075              
==========================================
- Hits         8761     8759       -2     
  Misses          1        1              
  Partials        1        1              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tefra/xsdata/pull/950?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).

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=950) **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=950&resolved=false&inNewCodePeriod=true)

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=950&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=950&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=950)



Patch:
diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py
index e582023f..b5e1de7a 100644
--- a/xsdata/formats/dataclass/filters.py
+++ b/xsdata/formats/dataclass/filters.py
@@ -384,7 +384,7 @@ class Filters:
         if not slug or not slug[0].isalpha():
             return self.safe_name(f"{prefix}_{name}", prefix, name_case, **kwargs)
 
-        result = text.variable(name_case(name, **kwargs))
+        result = name_case(name, **kwargs)
         if text.is_reserved(result):
             return self.safe_name(f"{name}_{prefix}", prefix, name_case, **kwargs)
 
diff --git a/xsdata/utils/text.py b/xsdata/utils/text.py
index 560d7d1f..bb8e9a0b 100644
--- a/xsdata/utils/text.py
+++ b/xsdata/utils/text.py
@@ -87,8 +87,11 @@ def capitalize(value: str, **kwargs: Any) -> str:
 
 
 def original_case(value: str, **kwargs: Any) -> str:
-    """Return the input string without any modifications."""
-    return value
+    """Return the input string but ensure it's a valid Python variable."""
+    # Strip out all characters that are not alphanumeric or underscores
+    value = re.sub(r"\W", "", value)
+    # Then strip out leading digit and underscore characters
+    return re.sub(r"^[^a-zA-Z_]+", "", value)
 
 
 def pascal_case(value: str, **kwargs: Any) -> str:
@@ -214,11 +217,3 @@ __alnum_ascii__ = set(string.digits + string.ascii_letters)
 def alnum(value: str) -> str:
     """Return the ascii alphanumerical characters in lower case."""
     return "".join(filter(__alnum_ascii__.__contains__, value)).lower()
-
-
-def variable(value: str) -> str:
-    """Returns a version of the string that will be a valid Python variable."""
-    # Strip out all characters that are not alphanumeric or underscores
-    value = re.sub(r"\W", "", value)
-    # Then strip out leading digit and underscore characters
-    return re.sub(r"^[^a-zA-Z]+", "", value)


Test patch:
diff --git a/tests/utils/test_text.py b/tests/utils/test_text.py
index 42086958..9e86b8d4 100644
--- a/tests/utils/test_text.py
+++ b/tests/utils/test_text.py
@@ -16,7 +16,6 @@ from xsdata.utils.text import (
     screaming_snake_case,
     snake_case,
     split_words,
-    variable,
 )
 
 
@@ -25,9 +24,11 @@ class TextTests(TestCase):
         self.assertEqual("p00p", original_case("p00p", foobar=True))
         self.assertEqual("p00p", original_case("p00p"))
         self.assertEqual("USERName", original_case("USERName"))
+        self.assertEqual("_USERName", original_case("_USERName"))
         self.assertEqual("UserNAME", original_case("UserNAME"))
         self.assertEqual("USER_name", original_case("USER_name"))
-        self.assertEqual("USER-NAME", original_case("USER-NAME"))
+        self.assertEqual("USER_name", original_case("3USER_name"))
+        self.assertEqual("USERNAME", original_case("USER-NAME"))
         self.assertEqual("User_Name", original_case("User_Name"))
         self.assertEqual("user_name", original_case("user_name"))
         self.assertEqual("SUserNAME", original_case("SUserNAME"))
@@ -146,13 +147,6 @@ class TextTests(TestCase):
         self.assertEqual("foo1", alnum(" foo*1"))
         self.assertEqual("1", alnum(" βιβλίο*1"))
 
-    def test_variable(self):
-        self.assertEqual("foO1", variable("foO1"))
-        self.assertEqual("foo_1", variable("foo_1"))
-        self.assertEqual("foo1", variable("foo-1"))
-        self.assertEqual("foo1", variable("@foo1"))
-        self.assertEqual("foo1", variable("1foo1"))
-
     def test_classify(self):
         for ltr in string.ascii_uppercase:
             self.assertEqual(CharType.UPPER, classify(ltr))