msgspec icon indicating copy to clipboard operation
msgspec copied to clipboard

msgspec.inspect error

Open lblanquet opened this issue 9 months ago • 2 comments
trafficstars

Description

I want to define JSON fields. I thus use the following code.

import msgspec
import uuid
from typing import TypeAlias

t_JSON: TypeAlias = dict[str, "t_JSON"] | list["t_JSON"] | str | int | float | bool | None

class t_1(msgspec.Struct):
        n: int
        uid: uuid.UUID
        jdata: t_JSON

print('Version:', msgspec.__version__)
msgspec.inspect.type_info(t_1)

and it issues an exception (I don't know if it's actually a bug):

Version: 0.19.0
Traceback (most recent call last):
  File "c:\program files\wing pro 10\bin\dbg\src\debug\tserver\dbgutils.py", line 2334, in to_trace
  Python Shell, prompt 6, line 1
    msgspec.inspect.type_info(t_1)
  File "d:\python\python312\lib\site-packages\msgspec\inspect.py", line 629, in type_info
    return multi_type_info([type])[0]
  File "d:\python\python312\lib\site-packages\msgspec\inspect.py", line 598, in multi_type_info
    return _Translator(types).run()
  File "d:\python\python312\lib\site-packages\msgspec\inspect.py", line 744, in run
    MsgpackDecoder(Tuple[self.types])
builtins.TypeError: Type 'ForwardRef('t_JSON')' is not supported```

lblanquet avatar Jan 24 '25 07:01 lblanquet

Did you try adapting JsonValue from pydantic? Your approach fails there too, but they've found a work-around.

rafalkrupinski avatar Jan 24 '25 13:01 rafalkrupinski

Thank you for the hint. I've cloned pydantic. It's not easy to understand if their approach is reproductible with msgspec . I've found the following workaround that works (It's a little bit treaky and maybe fragile 😅) :

import msgspec
import orjson
from typing import TypeAlias, Any
from icecream import ic

t_JSON: TypeAlias = dict[str, "t_JSON"] | list["t_JSON"] | str | int | float | bool | None

class JSON:
    def __init__(self, v : t_JSON):
        self.v = v
    
    def __repr__(self):
        return self.v.__repr__()

class T2(msgspec.Struct):
    n: int    
    jdata: JSON
    
    @property    
    def jdata(self):
        return self.jdata.v
    
    @jdata.setter    
    def jdata(self, v):
        self.jdata = JSON(v)
    
    def __post_init__(self):
        if not isinstance(self.jdata, JSON):
            self.jdata = JSON(self.jdata)

jdata=[1,2,3,{"n":1, "l":[1,2,3]}]

t2 = T2(n=1, jdata=jdata)

def m_enc_hook(obj: Any) -> Any:
    if isinstance(obj, JSON):
        result = orjson.dumps(obj.v)
        return result     
    else:
        # Raise a NotImplementedError for other types
        raise NotImplementedError(f"Objects of type {type(obj)} are not supported")

def j_enc_hook(obj: Any) -> Any:
    if isinstance(obj, JSON):
        result = orjson.dumps(obj.v).decode('utf8')
        return result     
    else:
        # Raise a NotImplementedError for other types
        raise NotImplementedError(f"Objects of type {type(obj)} are not supported")

m_enc = msgspec.msgpack.Encoder(enc_hook=m_enc_hook)
j_enc = msgspec.json.Encoder(enc_hook=j_enc_hook)


print('t2.jdata', t2.jdata, 'expected:', jdata)
ic(m_enc.encode(t2))
ic(j_enc.encode(t2))
ic(msgspec.inspect.type_info(T2))

result is as expected:

t2.jdata [1, 2, 3, {'n': 1, 'l': [1, 2, 3]}] expected: [1, 2, 3, {'n': 1, 'l': [1, 2, 3]}]
ic| m_enc.encode(t2): b'\x82\xa1n\x01\xa5jdata\xc4\x1b[1,2,3,{"n":1,"l":[1,2,3]}]'
ic| j_enc.encode(t2): b'{"n":1,"jdata":"[1,2,3,{\\"n\\":1,\\"l\\":[1,2,3]}]"}'
ic| msgspec.inspect.type_info(T2): StructType(cls=<class '__main__.T2'>, fields=(Field(name='n', encode_name='n', type=IntType(gt=None, ge=None, lt=None, le=None, multiple_of=None), required=True, default=NODEFAULT, default_factory=NODEFAULT), Field(name='jdata', encode_name='jdata', type=CustomType(cls=<class '__main__.JSON'>), required=False, default=<property object at 0x000001F6043B2BB0>, default_factory=NODEFAULT)), tag_field=None, tag=None, array_like=False, forbid_unknown_fields=False)

lblanquet avatar Jan 24 '25 15:01 lblanquet