msgspec
msgspec copied to clipboard
Expose and Enable Inheritance of StructMeta Metaclass
Expose and Enable Inheritance of StructMeta Metaclass
Changes
- Exposed
StructMetametaclass to Python for direct use - Added
Py_TPFLAGS_BASETYPEflag to enable Python inheritance ofStructMeta - Created tests to verify functionality
Why
This allows users to:
- Use
StructMetadirectly without subclassingStruct - Create custom metaclasses that extend
StructMetabehavior - Implement reusable patterns across multiple struct classes
This PR resolves:
- Issue #841
- Issue #837
Example
from msgspec import StructMeta
# Custom metaclass with kw_only_default parameter
class KwOnlyMeta(StructMeta):
_kw_only_default_settings = {}
def __new__(mcls, name, bases, namespace, **kwargs):
# Store and inherit kw_only_default
kw_only_default = kwargs.pop('kw_only_default', None)
if kw_only_default is None:
for base in bases:
if base.__name__ in mcls._kw_only_default_settings:
kw_only_default = mcls._kw_only_default_settings[base.__name__]
break
else:
mcls._kw_only_default_settings[name] = kw_only_default
# Apply kw_only_default if kw_only not specified
if 'kw_only' not in kwargs and kw_only_default is not None:
kwargs['kw_only'] = kw_only_default
return super().__new__(mcls, name, bases, namespace, **kwargs)
# Base model requiring keyword arguments
class BaseModel(metaclass=KwOnlyMeta, kw_only_default=True):
pass
# Child class inherits kw_only=True
class User(BaseModel):
name: str
age: int
# Must use keyword arguments
user = User(name="John", age=30)
Technical Details
Added StructMeta to module exports in _core.c Added Py_TPFLAGS_BASETYPE flag to StructMetaType Updated init.py to import and expose StructMeta Added type hints in init.pyi
Compatibility
These changes are backward compatible and only add new capabilities without modifying existing behavior.
Related Issues
@nightsailer I sure hope this passes you don't know how annoying it has been to try and turn Msgspec into an Object Relational Mapping Library. If this passes it would likely remedy my recent problems https://github.com/jcrist/msgspec/issues/820 and possibly allow StructMeta and any SQLAlchemy Type Metaclass object to be theoretically mergeable similar to how SQLModel works.
Also I ran your test on my computer and it is working correctly.
Ah, hey @nightsailer, seems like we were working in parallel! See my PR #844. [You were first, I did not re-check for open PRs :)]
Our changes are quite similar, except that
- you also updated the equalities
(Py_TYPE(type) == &StructMetaType)to(PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) - You added
metaclass=StructMetato theStructclass in__init__.pyi - I added a stub for
StructMetain__init__.pyi
May I make one commit to your PR to update the stub for StructMeta to this one?
Ah, hey @nightsailer, seems like we were working in parallel! See my PR #844. [You were first, I did not re-check for open PRs :)]
Our changes are quite similar, except that
- you also updated the equalities
(Py_TYPE(type) == &StructMetaType)to(PyType_IsSubtype(Py_TYPE(type), &StructMetaType))
May I make one commit to your PR to update the stub for StructMeta to this one?
Sure, absolutely! Please feel free to add that commit to update the stub. Thanks for offering!
By the way, the reason for updating the check from (Py_TYPE(type) == &StructMetaType) to (PyType_IsSubtype(Py_TYPE(type), &StructMetaType)) is that without the subtype check, functions like asdict and other encoding logic would fail to identify objects that are instances of classes derived from StructMetaType.
Ok that makes sense, thanks for clarifying!
Please merge this PR for the change we discussed: https://github.com/nightsailer/msgspec/pull/1
@nightsailer I sure hope this passes you don't know how annoying it has been to try and turn Msgspec into an Object Relational Mapping Library. If this passes it would likely remedy my recent problems #820 and possibly allow StructMeta and any SQLAlchemy Type Metaclass object to be theoretically mergeable similar to how SQLModel works.
Seconded. I am working on an ORM and want to set msgspec.UNSET to everything by default in the metaclass, so that I can have quick and dirty DTOs on the cheap. But currently it is verbose and annoying having to manually annotate every model with ... | UnsetType = UNSET. Class decorators don't work either, since __struct_defaults__ is readonly.
@jcrist I think there's a large amount of interest in this PR! It would be really wonderful if you could take a look and consider merging. Thanks so much.
@jcrist I think there's a large amount of interest in this PR! It would be really wonderful if you could take a look and consider merging. Thanks so much.
I'm interested because it means I would be able to add msgspec to a few libraries such as a few that I currently maintain such as aiocallback and a few others.
I have fork a new package: msgspec-x, this pr was conflict, close now.
Hey @nightsailer, while you've made a new package with this feature, I feel like the existing package has many pre-existing users and also deserves to have this feature. Would you consider re-opening this PR? Thanks.
@nightsailer given that there seems to be a transition to new mantainers, could you open a new PR with the original changes so that these can be merged into msgspec?
I'd be eager to merge this if you or someone else wouldn't mind opening another PR 😄
That would be great, thanks a lot for taking over maintenance for this project @ofek!
Since I was involved in this effort (per #837, #843), I'm happy to re-open the PR.
Please see #890 for that! This corresponds to the current PR, prior to the msgspec-x rebranding.