Skip to content

Commit f78a1e7

Browse files
authored
Add utilities for Struct-like checks (#950)
1 parent cda7e24 commit f78a1e7

File tree

7 files changed

+147
-5
lines changed

7 files changed

+147
-5
lines changed

docs/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Inspect
140140

141141
.. currentmodule:: msgspec.inspect
142142

143+
.. autofunction:: is_struct
144+
.. autofunction:: is_struct_type
143145
.. autofunction:: type_info
144146
.. autofunction:: multi_type_info
145147
.. autoclass:: Type

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Support Python 3.14, including freethreaded mode ({pr}`852`, {pr}`877`).
66
- Expose the `StructMeta` metaclass ({pr}`890`, {pr}`927`, {pr}`928`, {pr}`940`, {pr}`945`).
7+
- Add `msgspec.inspect.is_struct` and `msgspec.inspect.is_struct_type` functions for checking whether an object is a `msgspec.Struct`-like instance or class ({pr}`950`).
78
- Support Windows `arm64` builds ({pr}`943`).
89
- Enable ThinLTO on macOS `aarch64` builds ({pr}`937`).
910
- Fix leaks of `re.Pattern` objects when using `pattern` constraints of `msgspec.Meta` ({pr}`899`).

src/msgspec/_typing_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
from . import StructMeta
4+
5+
6+
def is_struct(obj: object) -> bool:
7+
"""Check whether ``obj`` is a `msgspec.Struct`-like instance.
8+
9+
Parameters
10+
----------
11+
obj:
12+
Object to check.
13+
14+
Returns
15+
-------
16+
bool
17+
`True` if ``obj`` is an instance of a class whose metaclass is
18+
`msgspec.StructMeta` (or a subclass of it), and `False` otherwise.
19+
Static type checkers treat a successful ``is_struct(obj)`` check as
20+
narrowing ``obj`` to `msgspec.Struct` within the true branch, even if
21+
the runtime class does not literally inherit `msgspec.Struct`.
22+
"""
23+
return isinstance(type(obj), StructMeta)
24+
25+
26+
def is_struct_type(tp: object) -> bool:
27+
"""Check whether ``tp`` is a `msgspec.Struct`-like class.
28+
29+
Parameters
30+
----------
31+
tp:
32+
Object to check, typically a class object.
33+
34+
Returns
35+
-------
36+
bool
37+
`True` if ``tp`` is a class whose metaclass is `msgspec.StructMeta`
38+
(or a subclass of it), and `False` otherwise. Static type checkers
39+
treat a successful ``is_struct_type(tp)`` check as narrowing
40+
``tp`` to `type[msgspec.Struct]` within the true branch.
41+
"""
42+
return isinstance(tp, StructMeta)

src/msgspec/_typing_utils.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from typing_extensions import TypeGuard
2+
3+
from . import Struct
4+
5+
def is_struct(obj: object) -> TypeGuard[Struct]: ...
6+
def is_struct_type(tp: object) -> TypeGuard[type[Struct]]: ...

src/msgspec/inspect.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
Factory as _Factory,
3333
to_builtins as _to_builtins,
3434
)
35+
from ._typing_utils import is_struct, is_struct_type
3536
from ._utils import ( # type: ignore
3637
_CONCRETE_TYPES,
3738
_AnnotatedAlias,
@@ -78,6 +79,8 @@
7879
"NamedTupleType",
7980
"DataclassType",
8081
"StructType",
82+
"is_struct",
83+
"is_struct_type",
8184
)
8285

8386

@@ -674,10 +677,6 @@ def _origin_args_metadata(t):
674677
return origin, args, tuple(metadata)
675678

676679

677-
def _is_struct(t):
678-
return isinstance(t, msgspec.StructMeta)
679-
680-
681680
def _is_enum(t):
682681
return type(t) is enum.EnumMeta
683682

@@ -889,7 +888,7 @@ def _translate_inner(
889888
return LiteralType(tuple(sorted(args)))
890889
elif _is_enum(t):
891890
return EnumType(t)
892-
elif _is_struct(t):
891+
elif is_struct_type(t):
893892
cls = t[args] if args else t
894893
if cls in self.cache:
895894
return self.cache[cls]

tests/typing/basic_typing_examples.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,40 @@ def check_consume_inspect_types() -> None:
10701070
reveal_type(t.includes_none) # assert "bool" in typ.lower()
10711071

10721072

1073+
def check_inspect_is_struct() -> None:
1074+
class Point(msgspec.Struct):
1075+
x: int
1076+
1077+
obj: object = Point(1)
1078+
if msgspec.inspect.is_struct(obj):
1079+
reveal_type(obj) # assert "Struct" in typ
1080+
else:
1081+
reveal_type(obj) # assert "Struct" not in typ
1082+
1083+
ns: object = object()
1084+
if msgspec.inspect.is_struct(ns):
1085+
reveal_type(ns) # assert "Struct" in typ
1086+
else:
1087+
reveal_type(ns) # assert "Struct" not in typ
1088+
1089+
1090+
def check_inspect_is_struct_type() -> None:
1091+
class Point(msgspec.Struct):
1092+
x: int
1093+
1094+
tp: type[Any] = Point
1095+
if msgspec.inspect.is_struct_type(tp):
1096+
reveal_type(tp) # assert "type" in typ and "Struct" in typ
1097+
else:
1098+
reveal_type(tp) # assert "Struct" not in typ
1099+
1100+
other: type[Any] = type("NotStruct", (), {})
1101+
if msgspec.inspect.is_struct_type(other):
1102+
reveal_type(other) # assert "Struct" in typ
1103+
else:
1104+
reveal_type(other) # assert "Struct" not in typ
1105+
1106+
10731107
##########################################################
10741108
# JSON Schema #
10751109
##########################################################

tests/unit/test_inspect.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,61 @@ class Model(Base):
815815
array_like=False,
816816
forbid_unknown_fields=False,
817817
)
818+
819+
820+
def test_is_struct_runtime():
821+
class Base(msgspec.Struct):
822+
x: int
823+
824+
class Derived(Base):
825+
pass
826+
827+
Generated = msgspec.defstruct("InspectStructRuntime", ["x"])
828+
829+
class CustomMeta(msgspec.StructMeta):
830+
pass
831+
832+
class CustomBase(metaclass=CustomMeta):
833+
x: int
834+
835+
class Custom(CustomBase):
836+
pass
837+
838+
class NotStruct:
839+
pass
840+
841+
assert mi.is_struct(Base(1))
842+
assert mi.is_struct(Derived(1))
843+
assert mi.is_struct(Generated(1))
844+
assert mi.is_struct(Custom(1))
845+
assert not mi.is_struct(NotStruct())
846+
assert not mi.is_struct(object())
847+
848+
849+
def test_is_struct_type_runtime():
850+
class Base(msgspec.Struct):
851+
x: int
852+
853+
class Derived(Base):
854+
pass
855+
856+
Generated = msgspec.defstruct("InspectStructTypeRuntime", ["x"])
857+
858+
class CustomMeta(msgspec.StructMeta):
859+
pass
860+
861+
class CustomBase(metaclass=CustomMeta):
862+
pass
863+
864+
class Custom(CustomBase):
865+
x: int
866+
867+
class NotStruct:
868+
pass
869+
870+
assert mi.is_struct_type(Base)
871+
assert mi.is_struct_type(Derived)
872+
assert mi.is_struct_type(Generated)
873+
assert mi.is_struct_type(Custom)
874+
assert not mi.is_struct_type(NotStruct)
875+
assert not mi.is_struct_type(object)

0 commit comments

Comments
 (0)