Repository: elastic/elasticsearch-dsl-py (language: python)

Issue:
v8.15.0 breaks things related to type hints?
Hello,

some of my existing code breaks when upgrading from v8.14.0 to v8.15.0 as follows:

```
self = elasticsearch_dsl.utils.AttrDict[typing.Any], attr = 'doc_count'

    def __getattr__(self, attr):
        if attr in {'__name__', '__qualname__'}:
            return self._name or self.__origin__.__name__

        # We are careful for copy and pickle.
        # Also for simplicity we don't relay any dunder names
        if '__origin__' in self.__dict__ and not _is_dunder(attr):
>           return getattr(self.__origin__, attr)
E           AttributeError: type object 'AttrDict' has no attribute 'doc_count'

/usr/lib/python3.12/typing.py:1185: AttributeError
```

Greetings,

jgb


Discussion / hints:
I'm not sure how this could have happened. Can you also show me your code?
```
search.aggs.bucket("filters", "filters", filters=filters)

results = search.execute().aggregations.filters.buckets.to_dict()
watcher_counts_via_filters_agg = [v.doc_count for k, v in sorted(results.items())]
```

It's the doc_count from the last line that triggers the error from my original post.
The code works fine on 8.14.0 (and before) but breaks in 8.15.0.
Downgrading fixes it.

Different side effect for the same issue happens in one other part of our code (these are all pytests by the way):

```
    result = search.execute()
    assert result.aggs == {
        "gender_1": {
            "avg_attention": {"value": 40.0},
            "doc_count": 2,
        },
    }
```

The assert fails here because doc_count is gone with elasticsearch_dsl >= 8.15.0.

Greetings,

jgb
Okay, I need to investigate this. A lot of our testing is based on bucket aggregations that are returned as a list, and those do have the `doc_count`. I'm going to build a new test that uses the object format for the buckets, it seems we have a testing hole there and for that reason I guess it is possible that something broke.

Fix patch:
diff --git a/elasticsearch_dsl/utils.py b/elasticsearch_dsl/utils.py
index 830cc4f7..021afc99 100644
--- a/elasticsearch_dsl/utils.py
+++ b/elasticsearch_dsl/utils.py
@@ -215,7 +215,11 @@ def __delitem__(self, key: str) -> None:
         del self._d_[key]
 
     def __setattr__(self, name: str, value: _ValT) -> None:
-        if name in self._d_ or not hasattr(self.__class__, name):
+        # the __orig__class__ attribute has to be treated as an exception, as
+        # is it added to an object when it is instantiated with type arguments
+        if (
+            name in self._d_ or not hasattr(self.__class__, name)
+        ) and name != "__orig_class__":
             self._d_[name] = value
         else:
             # there is an attribute on the class (could be property, ..) - don't add it as field


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 7c8bf232..fe417d2f 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -45,12 +45,23 @@ class MyAttrDict(utils.AttrDict[str]):
     assert isinstance(l[:][0], MyAttrDict)
 
 
+def test_attrlist_with_type_argument() -> None:
+    a = utils.AttrList[str](["a", "b"])
+    assert list(a) == ["a", "b"]
+
+
 def test_attrdict_keys_items() -> None:
     a = utils.AttrDict({"a": {"b": 42, "c": 47}, "d": "e"})
     assert list(a.keys()) == ["a", "d"]
     assert list(a.items()) == [("a", {"b": 42, "c": 47}), ("d", "e")]
 
 
+def test_attrdict_with_type_argument() -> None:
+    a = utils.AttrDict[str]({"a": "b"})
+    assert list(a.keys()) == ["a"]
+    assert list(a.items()) == [("a", "b")]
+
+
 def test_merge() -> None:
     a: utils.AttrDict[Any] = utils.AttrDict({"a": {"b": 42, "c": 47}})
     b = {"a": {"b": 123, "d": -12}, "e": [1, 2, 3]}