Question: accessing `__struct_config__` inside `__init_subclass__()`?
Description
I notice when accessing cls.__struct_config__.tag inside __init_subclass__() it is always None, so just curious when __init_subclass__() is called in the scheme of things.
The pattern that led me to this is populating some mappings to structs on subclass, where the struct's tag is a reasonable default, but not always appropriate for the mapping keys:
TABLE_NAME_MAP: "dict[str, type[Obj]]" = {}
class Obj(msgspec.Struct, tag_field="type"):
id: str
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
TABLE_NAME_MAP[cls.table_name()] = cls
@classmethod
def table_name(cls) -> str:
"""Name of database table.
Usually the same as the object's type tag, but may differ.
"""
return str(cls.__struct_config__.tag)
class Country(Object, tag="country"):
name: str
class Sport(Object, tag="sport"):
name: str
@classmethod
def table_name(cls) -> str:
return "sports"
There's plenty of ways I can go about this, so its no sort of blocker, but just curious if it is possible to have any info like this available by the time __init_subclass__() is called.
Apologies for the delayed response here, I'm currently out on parental leave.
Currently there's not a way to have the struct class be fully finalized when __init_subclass__ is called. This is due to how __init_subclass__ is implemented in the CPython VM - the method is called when defining a new type as part of the base type definition, not some later hook. The definition of a new struct type currently executes like:
- Metaclass processes type annotations and attributes to determine fields on struct
- Metaclass calls base
typeimplementation to create new type object - The base
type.__new__implementation calls__init_subclass__ - Metaclass sets attributes on new type
We could make this work, but it would rely on moving 4 to be part of a default __init_subclass__ definition on the Struct type, and some metadata being passed through and ignored by any subclasses. In short, you'd have to write your __init_subclass__ method like:
class Object(Struct):
def __init_subclass__(cls, **kwargs):
super().__init_subclass(**kwargs) # super() call must be first, and must forward all kwargs
# remainder must be resilient to kwargs unrelated to your subclass beng in `kwargs`
# to make this work, we'd pass some boxed metadata as a private key in `kwargs`, perhaps
# named something like "_msgspec_metadata"
...
We should be able to detect if the user does something wrong in their __init_subclass__ implementation, so failure to call the base __init_subclass__ definition would raise a nice error rather than segfaulting.
Either way, this is a decent enough refactor for what I'd consider an esoteric-ish feature, so I don't anticipate hacking on this anytime soon.
Apologies for the delayed response here, I'm currently out on parental leave.
No probs whatsoever!
Thanks for the explanation of the interplay between Struct and type, that's really helpful.
We could make this work, but it would rely on moving 4 to be part of a default init_subclass definition on the Struct type
I suppose an alternative approach might be to have another hook that is called on subclass, after msgspec has finished its handling of the class (take this as a comment, as my example certainly isn't worthy of the feature request). Attrs has a similar feature request in their issues: https://github.com/python-attrs/attrs/issues/595.