Repository: jcrist/msgspec (language: python)

Issue:
Converting an object / dict to a struct where the struct contains a Raw field fails with "Expected `any`, got ..."
### Description

# Description
Converting using `msgspec.convert` from an object with `from_attributes=True` to a struct that contains a `Raw` field doesn't appear to work.

# Example

Given a struct like:
```python
class Wrapper(msgspec.Struct):
    result: msgspec.Raw | msgspec.UnsetType = msgspec.UNSET
```

That is used to deserialise json successfully, e.g `b'{"result": {"status": 0}}'` => `Wrapper(result=<msgspec.Raw object at 0x1028b3bb0>)`

However going from an object or dict and trying to `msgspec.convert` it into a `Wrapper` doesn't appear to work or am I doing something wrong?

```python
from types import SimpleNamespace
import msgspec

class Wrapper(msgspec.Struct):
    result: msgspec.Raw | msgspec.UnsetType = msgspec.UNSET

wrapped = {"status": 0}
raw_bytes = msgspec.json.encode(wrapped)
raw = msgspec.Raw(raw_bytes)

try:
    print("wrapped: raw")
    ns_a = SimpleNamespace(result=raw)
    print(ns_a)
    # expected that this would work?
    print(msgspec.convert(ns_a, type=Wrapper, from_attributes=True))
except Exception as e:
    print(e) # Expected `any`, got `msgspec.Raw` - at `$.result`

try:
    print("wrapped: nested namespace")
    ns_a = SimpleNamespace(result=SimpleNamespace(status=0))
    print(ns_a)
    # didn't really expect that this would work
    print(msgspec.convert(ns_a, type=Wrapper, from_attributes=True))
except Exception as e:
    print(e) # Expected `any`, got `types.SimpleNamespace` - at `$.result`

try:
    print("wrapped: bytes")
    ns_a = SimpleNamespace(result=raw_bytes)
    print(ns_a)
    # didn't really expect that this would work but thought it might
    print(msgspec.convert(ns_a, type=Wrapper, from_attributes=True))
except Exception as e:
    print(e) # Expected `any`, got `bytes` - at `$.result`

```

# Expected

```python
    ns_a = SimpleNamespace(result=msgspec.Raw(b'{"status":0}'))
    print(msgspec.convert(ns_a, type=Wrapper, from_attributes=True))
    # Wrapper(result=<msgspec.Raw object at 0x1028b3bb0>)`
```


Discussion / hints:

Fix patch:
diff --git a/msgspec/_core.c b/msgspec/_core.c
index 55f79aba..c994bce0 100644
--- a/msgspec/_core.c
+++ b/msgspec/_core.c
@@ -20556,6 +20556,17 @@ convert_immutable(
     return ms_validation_error(expected, type, path);
 }
 
+static PyObject *
+convert_raw(
+    ConvertState *self, PyObject *obj, TypeNode *type, PathNode *path
+) {
+    if (type->types == 0) {
+        Py_INCREF(obj);
+        return obj;
+    }
+    return ms_validation_error("raw", type, path);
+}
+
 static PyObject *
 convert_seq_to_list(
     ConvertState *self, PyObject **items, Py_ssize_t size,
@@ -21666,6 +21677,9 @@ convert(
     else if (pytype == &Ext_Type) {
         return convert_immutable(self, MS_TYPE_EXT, "ext", obj, type, path);
     }
+    else if (pytype == &Raw_Type) {
+        return convert_raw(self, obj, type, path);
+    }
     else if (PyAnySet_Check(obj)) {
         return convert_any_set(self, obj, type, path);
     }


Test patch:
diff --git a/tests/test_convert.py b/tests/test_convert.py
index 0bb8cdc0..ea697838 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -2600,3 +2600,14 @@ def dec_hook(typ, x):
 
         assert rec.value.__cause__ is rec.value.__context__
         assert type(rec.value.__cause__) is TypeError
+
+
+class TestRaw:
+    def test_raw(self):
+        raw = msgspec.Raw(b"123")
+
+        class Ex(Struct):
+            x: msgspec.Raw
+
+        sol = Ex(x=raw)
+        assert convert({"x": raw}, type=Ex) == sol