Repository: python-attrs/attrs
Pull request #1406 (2025-02-08 08:38:27)

Accept valid unicode identifiers as class parameter to make_class
Related to #1404



hynek: Thanks! How would you summarize the change for the changelog and `.. versionchanged`?

> `attrs.make_class()` now allows for Unicode class and field names.

?
serge-sans-paille: It's actually just

> `attrs.make_class()` now allows for Unicode class name

Attribute name was correctly supported before that patch, it's just my testing code that was incorrect (I was testing a `kwargs` that contained the unnormalized attribute name, and that doesn't work for real class, there's no reason it should work with `attrs`)
hynek: thanks; it was faster to do it myself, than explaining it: https://github.com/python-attrs/attrs/pull/1406/commits/fc8b9c6af03e812b1609a3ec0c4e873573e18ad2

Patch:
diff --git a/changelog.d/1406.change.md b/changelog.d/1406.change.md
new file mode 100644
index 0000000..4eae78a
--- /dev/null
+++ b/changelog.d/1406.change.md
@@ -0,0 +1,1 @@
+`attrs.make_class()` now allows for Unicode class names.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index e3e31ff..a7a4075 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -13,6 +13,7 @@ import linecache
 import sys
 import types
 import typing
+import unicodedata
 
 from operator import itemgetter
 
@@ -2907,7 +2908,11 @@ def make_class(
     .. versionadded:: 17.1.0 *bases*
     .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained.
     .. versionchanged:: 23.2.0 *class_body*
+    .. versionchanged:: 25.2.0 Class names can now be unicode.
     """
+    # Class identifiers are converted into the normal form NFKC while parsing
+    name = unicodedata.normalize("NFKC", name)
+
     if isinstance(attrs, dict):
         cls_dict = attrs
     elif isinstance(attrs, (list, tuple)):


Test patch:
diff --git a/tests/test_make.py b/tests/test_make.py
index cc78cb3..f5e3b31 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -10,6 +10,7 @@ import gc
 import inspect
 import itertools
 import sys
+import unicodedata
 
 from operator import attrgetter
 from typing import Generic, TypeVar
@@ -1100,6 +1101,48 @@ class TestMakeClass:
 
         assert repr(C(1)).startswith("<tests.test_make.C object at 0x")
 
+    def test_normalized_unicode_attr_args(self):
+        """
+        Unicode identifiers are valid in Python.
+        """
+        clsname = "ü"
+
+        assert clsname == unicodedata.normalize("NFKC", clsname)
+
+        attrname = "ß"
+
+        assert attrname == unicodedata.normalize("NFKC", attrname)
+
+        C = make_class(clsname, [attrname], repr=False)
+
+        assert repr(C(1)).startswith("<tests.test_make.ü object at 0x")
+
+        kwargs = {"ß": 1}
+        c = C(**kwargs)
+
+        assert 1 == c.ß
+
+    def test_unnormalized_unicode_attr_args(self):
+        """
+        Unicode identifiers are normalized to NFKC form in Python.
+        """
+
+        clsname = "Ŀ"
+
+        assert clsname != unicodedata.normalize("NFKC", clsname)
+
+        attrname = "ㅁ"
+
+        assert attrname != unicodedata.normalize("NFKC", attrname)
+
+        C = make_class(clsname, [attrname], repr=False)
+        assert repr(C(1)).startswith("<tests.test_make.L· object at 0x")
+
+        kwargs = {unicodedata.normalize("NFKC", attrname): 1}
+        c = C(**kwargs)
+
+        assert 1 == c.ㅁ
+
     def test_catches_wrong_attrs_type(self):
         """
         Raise `TypeError` if an invalid type for attrs is passed.