Repository: jcrist/msgspec (python)

Issue:
Fix for when obj is `UnionType` in `_utils._apply_params`
### Description

Hi. I'm using Python 3.13 and installing the package directly from this git repo.

I had an issue with `_utils._apply_params` - I don't have a minimal example, but here's the traceback:

```
...
  File ".../mycode.py", line 264, in decode
    return msgspec.msgpack.decode(item, **kwargs)
    ^^^^^^^^^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 111, in get_class_annotations
    mro, typevar_mappings = _get_class_mro_and_typevar_mappings(obj)
      ^^^^^^^^^^^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 88, in _get_class_mro_and_typevar_mappings
    inner(obj, {})
      ~~~^^^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 86, in inner
    inner(b, new_scope)
    ^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 79, in inner
    args = tuple(_apply_params(a, scope) for a in c.__args__)
    ^^^^^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 79, in <genexpr>
    args = tuple(_apply_params(a, scope) for a in c.__args__)
    ^^^^^^^^^^^
  File ".../.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 55, in _apply_params
    args = tuple(mapping.get(p, p) for p in params)
    ^^^^^^^^^^^^^^^
TypeError: 'getset_descriptor' object is not iterable
```

I found that this was due to `_utils._apply_params` being called with `obj == types.UnionType`. UnionType returns a descriptor for `__parameters__`.

```pycon
>>> type(int | str).__parameters__
<attribute '__parameters__' of 'types.UnionType' objects>
```

I've pasted my workaround below:

```py
def _apply_params(obj, mapping):
    import types
    if (params := getattr(obj, "__parameters__", None)) and (obj != types.UnionType):
        args = tuple(mapping.get(p, p) for p in params)
        return obj[args]
    elif isinstance(obj, typing.TypeVar):
        return mapping.get(obj, obj)
    return obj
```

Pull request description:
Don't fail when parsing types with invalid `__parameters__`
Fixes #769.

Patch:
diff --git a/msgspec/_utils.py b/msgspec/_utils.py
index 2a8a571..7761b07 100644
--- a/msgspec/_utils.py
+++ b/msgspec/_utils.py
@@ -50,12 +50,22 @@ else:
 
 
 def _apply_params(obj, mapping):
-    if params := getattr(obj, "__parameters__", None):
-        args = tuple(mapping.get(p, p) for p in params)
-        return obj[args]
-    elif isinstance(obj, typing.TypeVar):
+    if isinstance(obj, typing.TypeVar):
         return mapping.get(obj, obj)
-    return obj
+
+    try:
+        parameters = tuple(obj.__parameters__)
+    except Exception:
+        # Not parameterized or __parameters__ is invalid, ignore
+        return obj
+
+    if not parameters:
+        # Not parametrized
+        return obj
+
+    # Parametrized
+    args = tuple(mapping.get(p, p) for p in parameters)
+    return obj[args]
 
 
 def _get_class_mro_and_typevar_mappings(obj):


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 83630c9..a991b42 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -190,3 +190,14 @@ class TestGetClassAnnotations:
             z: str
 
         assert get_class_annotations(Sub2) == {"x": int, "y": float, "z": str}
+
+    def test_generic_invalid_parameters(self):
+        class Invalid:
+            @property
+            def __parameters__(self):
+                pass
+
+        class Sub(Base[Invalid]):
+            pass
+
+        assert get_class_annotations(Sub) == {"x": Invalid}