msgspec icon indicating copy to clipboard operation
msgspec copied to clipboard

Allow passing a namespace to get_class_annotations for resolving forward references

Open jayqi opened this issue 6 months ago • 0 comments
trafficstars

Description

I am the author of erdantic, which uses runtime type annotation analysis to draw entity relationship diagrams for dataclass-likes. I recently added support for msgspec's structs to the project.

I'm using msgspec._utils.get_class_annotations to resolve type annotations, but it has the limitation that one cannot pass a custom namespace to get_class_annotations to search when resolving forward references. This means the following kind of situation results in a runtime error when trying to use get_class_annotations:

from msgspec import Struct
from msgspec._utils import get_class_annotations

def struct_factory():
    class OuterStruct(Struct):
        inner: "InnerStruct"

    class InnerStruct(Struct):
        x: int
        y: int

    return OuterStruct

CreatedStruct = struct_factory()
get_class_annotations(CreatedStruct)
#> Traceback (most recent call last):
#>   File "<string>", line 1, in <module>
#>   File "/Users/jay/Desktop/temp_erdantic/temp_msgspec/.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 160, in get_class_annotations
#>     value = _eval_type(value, cls_locals, cls_globals)
#>   File "/Users/jay/Desktop/temp_erdantic/temp_msgspec/.venv/lib/python3.13/site-packages/msgspec/_utils.py", line 47, in _eval_type
#>     return typing._eval_type(t, globalns, localns, ())
#>            ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
#>   File "/Users/jay/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/lib/python3.13/typing.py", line 474, in _eval_type
#>     return t._evaluate(globalns, localns, type_params, recursive_guard=recursive_guard)
#>            ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#>   File "/Users/jay/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/lib/python3.13/typing.py", line 1081, in _evaluate
#>     eval(self.__forward_code__, globalns, localns),
#>     ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#>   File "<string>", line 1, in <module>
#> NameError: name 'InnerStruct' is not defined

Created at 2025-04-26 13:11 EDT by reprexlite v1.0.0

It would be great if get_class_annotations supported passing in a namespace so that such forward references would be resolved correctly.

As a reference, attrs has resolve_types which has globalns and localns parameters and passes them through to typing.get_type_hints. I can see in the msgspec source that msgspec is explicitly defining globalns and localns already. Perhaps a user-specified namespace could be merged in with one of them?

I recognize the above example is fairly contrived and probably should be considered a weird edge case. However, other frameworks like attrs support resolving them, so it would be nice to be able to do so for msgspec for completeness.

This issue may be related to #704.

jayqi avatar Apr 26 '25 17:04 jayqi