Skip to content

Commit b1266b1

Browse files
provinzkrautofek
andauthored
fix inspection for structs that use the StructMeta metaclass (#941) (#945)
Co-authored-by: Ofek Lev <ofekmeister@gmail.com>
1 parent 7fcc790 commit b1266b1

File tree

6 files changed

+83
-16
lines changed

6 files changed

+83
-16
lines changed

docs/structs.rst

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -931,8 +931,11 @@ has a signature similar to `dataclasses.make_dataclass`. See
931931
Point(x=1.0, y=2.0)
932932
933933
934+
Advanced
935+
--------
936+
934937
Metaclasses
935-
-----------
938+
~~~~~~~~~~~
936939

937940
You can define project-wide :class:`msgspec.Struct` policies at class-creation
938941
time by extending the :class:`msgspec.StructMeta` metaclass.
@@ -1002,19 +1005,22 @@ abstract base Structs (like ``Event``) cannot be instantiated, and
10021005
:func:`isinstance` and :func:`issubclass` checks behave the same as for normal
10031006
ABCs.
10041007

1005-
.. warning::
1008+
.. important::
10061009

1007-
Mixing :class:`msgspec.StructMeta` with arbitrary metaclasses
1008-
is not supported. Only combinations involving :class:`abc.ABCMeta`
1009-
(or its subclasses) are guaranteed to work. Prefer using
1010-
:meth:`object.__init_subclass__` on a :class:`msgspec.Struct` base class
1011-
instead of additional custom metaclasses.
1010+
- Classes with a :class:`msgspec.StructMeta`-derived metaclass do not
1011+
*technically* need to inherit from :class:`msgspec.Struct`, but it is
1012+
recommended to do so for static typing support in IDEs and other tools.
1013+
- Mixing :class:`msgspec.StructMeta` with arbitrary metaclasses
1014+
is not supported. Only combinations involving :class:`abc.ABCMeta`
1015+
(or its subclasses) are guaranteed to work. Prefer using
1016+
:meth:`object.__init_subclass__` on a :class:`msgspec.Struct` base class
1017+
instead of additional custom metaclasses.
10121018

10131019

10141020
.. _struct-gc:
10151021

1016-
Disabling Garbage Collection (Advanced)
1017-
---------------------------------------
1022+
Disabling Garbage Collection
1023+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10181024

10191025
.. warning::
10201026

src/msgspec/inspect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ def _origin_args_metadata(t):
675675

676676

677677
def _is_struct(t):
678-
return type(t) is type(msgspec.Struct)
678+
return isinstance(t, msgspec.StructMeta)
679679

680680

681681
def _is_enum(t):

src/msgspec/structs.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from ._core import ( # noqa
77
Factory as _Factory,
88
StructConfig,
9+
StructMeta,
910
asdict,
1011
astuple,
1112
force_setattr,
@@ -72,12 +73,19 @@ def fields(type_or_instance: Struct | type[Struct]) -> tuple[FieldInfo]:
7273
-------
7374
tuple[FieldInfo]
7475
"""
75-
if isinstance(type_or_instance, Struct):
76-
annotated_cls = cls = type(type_or_instance)
76+
obj = type_or_instance
77+
78+
# Struct class
79+
if isinstance(obj, StructMeta):
80+
annotated_cls = cls = obj
81+
# Struct instance
82+
elif isinstance(type(obj), StructMeta):
83+
annotated_cls = cls = type(obj)
84+
# Generic alias
7785
else:
78-
annotated_cls = type_or_instance
79-
cls = getattr(type_or_instance, "__origin__", type_or_instance)
80-
if not (isinstance(cls, type) and issubclass(cls, Struct)):
86+
annotated_cls = obj
87+
cls = getattr(obj, "__origin__", obj)
88+
if not isinstance(cls, StructMeta):
8189
raise TypeError("Must be called with a struct type or instance")
8290

8391
hints = _get_class_annotations(annotated_cls)

src/msgspec/structs.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ class FieldInfo(Struct):
3434
@property
3535
def required(self) -> bool: ...
3636

37-
def fields(type_or_instance: Struct | type[Struct]) -> tuple[FieldInfo]: ...
37+
def fields(type_or_instance: S | type[S]) -> tuple[FieldInfo]: ...

tests/unit/test_inspect.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,3 +786,32 @@ class Example(msgspec.Struct):
786786
res = mi.multi_type_info([Example, List[Example]])
787787
assert res == (ex_type, mi.ListType(ex_type))
788788
assert res[0] is res[1].item_type
789+
790+
791+
def test_type_info_custom_base_class():
792+
class CustomMeta(msgspec.StructMeta):
793+
pass
794+
795+
class Base(metaclass=CustomMeta):
796+
pass
797+
798+
class Model(Base):
799+
foo: str
800+
801+
assert mi.type_info(Model) == mi.StructType(
802+
cls=Model,
803+
fields=(
804+
mi.Field(
805+
name="foo",
806+
encode_name="foo",
807+
type=mi.StrType(min_length=None, max_length=None, pattern=None),
808+
required=True,
809+
default=msgspec.NODEFAULT,
810+
default_factory=msgspec.NODEFAULT,
811+
),
812+
),
813+
tag_field=None,
814+
tag=None,
815+
array_like=False,
816+
forbid_unknown_fields=False,
817+
)

tests/unit/test_struct.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,6 +2412,30 @@ class Bad(Generic[T]):
24122412
with pytest.raises(TypeError, match="struct type or instance"):
24132413
msgspec.structs.fields(val)
24142414

2415+
def test_fields_struct_meta(self):
2416+
class CustomMeta(msgspec.StructMeta):
2417+
pass
2418+
2419+
class Base(metaclass=CustomMeta):
2420+
pass
2421+
2422+
class Model(Base):
2423+
pass
2424+
2425+
assert msgspec.structs.fields(Model) == ()
2426+
2427+
def test_fields_struct_meta_instance(self):
2428+
class CustomMeta(msgspec.StructMeta):
2429+
pass
2430+
2431+
class Base(metaclass=CustomMeta):
2432+
pass
2433+
2434+
class Model(Base):
2435+
pass
2436+
2437+
assert msgspec.structs.fields(Model()) == ()
2438+
24152439
def test_fields_no_fields(self):
24162440
assert msgspec.structs.fields(msgspec.Struct) == ()
24172441

0 commit comments

Comments
 (0)