Signals are not inherited
All necessary models should be added to signals directly. Sometimes it is not comfortably because we must remember about this point and can't use the standard habitual OOP logic. Example for reproducing:
from tortoise import fields, Model
from typing import Any
class Base(Model):
id = fields.IntField(pk=True)
class Meta:
abstract = True
@pre_save(Base)
async def signal_pre_save(*args: Any, **kwargs: Any) -> None:
print("Base pre_save. And it will be executed nowhere")
class Foo(Base):
pass
I've explored and detected a reason of this behavior. Who can sort out with this logic? I guess it was made specially.
I fixed this "issue" by the following workaround (maybe it will be useful for someone):
from tortoise import fields, Model
from tortoise.signals import Signals
from tortoise.signals import pre_save
class CustomListener(dict):
def get(self, cls: Type, default=None):
"""Tries to look for signals with MRO logic
"""
listeners = super().get(cls, default)
if not listeners:
for parent_cls in cls.__bases__:
listeners = self.get(parent_cls, default=default)
if listeners:
return listeners
return []
class Base(Model):
_listeners: Dict[Signals, CustomListener] = { # type: ignore
Signals.pre_save: CustomListener(),
Signals.post_save: CustomListener(),
Signals.pre_delete: CustomListener(),
Signals.post_delete: CustomListener(),
}
class Meta:
abstract = True
@pre_save(Base)
async def signal_pre_save(*args: Any, **kwargs: Any) -> None:
print("Base pre_save. And it will be executed for all children")
class Foo(Base):
pass
I feel like adding listeners in __init_subclass__ would be a simpler solution, wouldn't it?
thanks, and i think that you forgot the "else" case
def get(self, cls: Type, default=None):
"""Tries to look for signals with MRO logic
"""
if not listeners: # <-------- when the condition fails
...
...
else: # handle it to avoid returning [] when the condition fails
return listeners
return []
I prefer to use this one. It is much easier to understand but it's less generic.
async def pre_save_model(sender: Any, instance: "Base", using_db: str, update_fields: list[str]) -> None:
print("Pre save")
class Base(Model):
def __init_subclass__(cls) -> None:
super().__init_subclass__()
pre_save(cls)(pre_save_model)
class Meta:
abstract = True
Very clear