Skip to content

Commit 3ea4c05

Browse files
author
aleksandr.tseluyko
committed
Cache structs.fields() result for non-generic structs
Store the computed tuple of FieldInfo on the class as __struct_field_info__ to avoid expensive get_class_annotations() and FieldInfo recreation on every call. Generic aliases are not cached since type parameters affect annotations. Closes #963
1 parent dbdee05 commit 3ea4c05

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

‎src/msgspec/structs.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ def fields(type_or_instance: Struct | type[Struct]) -> tuple[FieldInfo]:
8888
if not isinstance(cls, StructMeta):
8989
raise TypeError("Must be called with a struct type or instance")
9090

91+
# Return cached result for non-generic structs
92+
is_generic = annotated_cls is not cls
93+
if not is_generic:
94+
cached = cls.__dict__.get("__struct_field_info__")
95+
if cached is not None:
96+
return cached
97+
9198
hints = _get_class_annotations(annotated_cls)
9299
npos = len(cls.__struct_fields__) - len(cls.__struct_defaults__)
93100
fields = []
@@ -111,4 +118,10 @@ def fields(type_or_instance: Struct | type[Struct]) -> tuple[FieldInfo]:
111118
)
112119
fields.append(field)
113120

114-
return tuple(fields)
121+
result = tuple(fields)
122+
123+
# Cache for non-generic structs
124+
if not is_generic:
125+
cls.__struct_field_info__ = result
126+
127+
return result

‎tests/unit/test_struct.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2526,6 +2526,33 @@ class Example(msgspec.Struct, Generic[T]):
25262526
)
25272527
assert msgspec.structs.fields(Example[str])
25282528

2529+
def test_fields_cached(self):
2530+
class Example(msgspec.Struct):
2531+
x: int
2532+
y: str = "hello"
2533+
2534+
result1 = msgspec.structs.fields(Example)
2535+
result2 = msgspec.structs.fields(Example)
2536+
assert result1 is result2
2537+
2538+
def test_fields_cached_instance(self):
2539+
class Example(msgspec.Struct):
2540+
x: int
2541+
2542+
result1 = msgspec.structs.fields(Example)
2543+
result2 = msgspec.structs.fields(Example(1))
2544+
assert result1 is result2
2545+
2546+
def test_fields_generic_not_cached_across_params(self):
2547+
T = TypeVar("T")
2548+
2549+
class Example(msgspec.Struct, Generic[T]):
2550+
x: T
2551+
2552+
result1 = msgspec.structs.fields(Example[str])
2553+
result2 = msgspec.structs.fields(Example[int])
2554+
assert result1 is not result2
2555+
25292556

25302557
class TestClassVar:
25312558
def case1(self):

0 commit comments

Comments
 (0)