tortoise-orm icon indicating copy to clipboard operation
tortoise-orm copied to clipboard

Signals are not inherited

Open antixar opened this issue 3 years ago • 4 comments

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

antixar avatar Feb 21 '22 10:02 antixar

I feel like adding listeners in __init_subclass__ would be a simpler solution, wouldn't it?

zmievsa avatar Jul 05 '23 07:07 zmievsa

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 []

edimedia avatar Nov 25 '23 21:11 edimedia

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

zmievsa avatar Nov 25 '23 21:11 zmievsa

Very clear

edimedia avatar Nov 25 '23 22:11 edimedia