【问题标题】:Disconnect signals for models and reconnect in django断开模型的信号并在 django 中重新连接
【发布时间】:2011-01-13 15:05:19
【问题描述】:

我需要保存模型,但在保存之前我需要断开一些信号接收器。

我的意思是,

我有一个模型:

class MyModel(models.Model):
    ...

def pre_save_model(sender, instance, **kwargs):
    ...

pre_save.connect(pre_save_model, sender=MyModel)

在代码的另一个地方我需要类似的东西:

a = MyModel()
...
disconnect_signals_for_model(a)
a.save()
...
reconnect_signals_for_model(a)

因为我需要在这种情况下保存模型而不执行函数pre_save_model。

【问题讨论】:

    标签: django django-signals


    【解决方案1】:

    我没有测试过下面的代码,但它应该可以工作:

    from django.db.models.signals import pre_save
    
    
    def save_without_the_signals(instance, *args, **kwargs):
        receivers = pre_save.receivers
        pre_save.receivers = []
        new_instance = instance.save(*args, **kwargs)
        pre_save.receivers = receivers
        return new_instance
    

    它将使来自所有发件人的信号静音,但不仅仅是instance.__class__


    此版本仅禁用给定模型的信号:

    from django.db.models.signals import pre_save
    from django.dispatch.dispatcher import _make_id
    
    
    def save_without_the_signals(instance, *args, **kwargs):
        receivers = []
        sender_id = _make_id(instance.__class__)
        for index in xrange(len(self.receivers)):
            if pre_save.receivers[index][0][1] == sender_id:
                receivers.append(pre_save.receivers.pop(index))
        new_instance = instance.save(*args, **kwargs)
        pre_save.receivers.extend(receivers)
        return new_instance
    

    【讨论】:

    • 您可能应该将保存包装在 try 块中,并将接收器的重新附加在 finally 块中。否则你可能会永远断开信号。
    • 是的,使用try..finally
    • 小心这段代码,因为它不是线程安全的。我们修改应用的全局状态@muhuk stackoverflow.com/questions/20907631/…
    【解决方案2】:

    对于干净且可重用的解决方案,您可以使用上下文管理器:

    class temp_disconnect_signal():
        """ Temporarily disconnect a model from a signal """
        def __init__(self, signal, receiver, sender, dispatch_uid=None):
            self.signal = signal
            self.receiver = receiver
            self.sender = sender
            self.dispatch_uid = dispatch_uid
    
        def __enter__(self):
            self.signal.disconnect(
                receiver=self.receiver,
                sender=self.sender,
                dispatch_uid=self.dispatch_uid,
                weak=False
            )
    
        def __exit__(self, type, value, traceback):
            self.signal.connect(
                receiver=self.receiver,
                sender=self.sender,
                dispatch_uid=self.dispatch_uid,
                weak=False
            )
    

    现在,您可以执行以下操作:

    from django.db.models import signals
    
    from your_app.signals import some_receiver_func
    from your_app.models import SomeModel
    
    ...
    kwargs = {
        'signal': signals.post_save,
        'receiver': some_receiver_func,
        'sender': SomeModel, 
        'dispatch_uid': "optional_uid"
    }
    with temp_disconnect_signal(**kwargs):
        SomeModel.objects.create(
            name='Woohoo',
            slug='look_mom_no_signals',
        )
    

    注意:如果您的信号处理程序使用 dispatch_uid,您必须使用 dispatch_uid 参数。

    【讨论】:

    • 太棒了。这是最优雅的解决方案。您可以在代码的多个部分重用上下文管理器。
    • 一个小警告:weak=False 不是将接收器连接到信号时的默认设置。
    • weak is deprecated 另外,人们应该知道禁用信号将阻止 所有 实例触发信号,而不仅仅是当前上下文(即其他线程,因为信号似乎是线程安全的),建议here
    • @DanielDubovski 他们似乎是线程安全的?那么它应该像这样工作,不是吗?
    • 如果信号上有多个接收器装饰器,由于某种原因它不起作用。但还是很不错的解决方案!
    【解决方案3】:

    我需要防止在单元测试期间触发某些信号,因此我根据 qris 的响应制作了一个装饰器:

    from django.db.models import signals
    
    def prevent_signal(signal_name, signal_fn, sender):
        def wrap(fn):
            def wrapped_fn(*args, **kwargs):
                signal = getattr(signals, signal_name)
                signal.disconnect(signal_fn, sender)
                fn(*args, **kwargs)
                signal.connect(signal_fn, sender)
            return wrapped_fn
        return wrap
    

    使用很简单:

    @prevent_signal('post_save', my_signal, SenderClass)
    def test_something_without_signal(self):
        # the signal will not fire inside this test
    

    【讨论】:

    • 在测试期间禁用信号有点错过了测试的重点。关于场景,代码流应该保持不变。如果有你不需要作为测试的一部分执行的代码,那么模拟它的结果,不要跳过它。
    • 如果包装函数是为了返回一些值,你的代码将无法工作。您必须在装饰器中返回函数结果值。
    • @DanielDubovski 在某些情况下,您可能有一段测试代码会生成大量测试数据。通常,如果用户创建了这些模型,它会产生副作用,但您现在想跳过它。是的,你可以模拟所有的接收器函数,但是如果你只是禁用信号,它会更加明确。然后,您将创建一个重新启用信号的正常集成测试。
    • @JordanReiter 我明白你的意思,但是,我仍然不同意。恕我直言,出于测试目的更改代码流是一种不好的做法,因为很容易忘记代码可以采用的不同路径。具体来说,嘲笑本质上是非常明确的,在我看来更像是pythonic。也就是说,就像任何规则一样,我想经验规则总会有例外,危险在于例外将成为以后维护者的规范..
    【解决方案4】:

    您可以像Haystack 在 RealTimeSearchIndex 中那样连接和断开信号,这似乎更标准:

    from django.db.models import signals
    signals.pre_save.disconnect(pre_save_model, sender=MyModel)
    a.save()
    signals.pre_save.connect(pre_save_model, sender=MyModel)
    

    【讨论】:

    • pre_savel_modelpre_save 一样吗?
    • @Latrova - 我假设pre_save_model 只是信号接收器名称的一个示例。 connectdisconnect 的第一个参数是信号接收器。 (Docs)
    • 小心这段代码,因为它不是线程安全的。我们修改应用的全局状态@qris stackoverflow.com/questions/20907631/…
    【解决方案5】:

    如果您只想断开和重新连接一个自定义信号,您可以使用以下代码:

    def disconnect_signal(signal, receiver, sender):
        disconnect = getattr(signal, 'disconnect')
        disconnect(receiver, sender)
    
    def reconnect_signal(signal, receiver, sender):
        connect = getattr(signal, 'connect')
        connect(receiver, sender=sender)
    

    这样你就可以做到:

    disconnect_signal(pre_save, pre_save_model, MyModel)
    a.save()
    reconnect_signal(pre_save, pre_save_model, MyModel)
    

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-27
    • 1970-01-01
    • 2020-11-29
    • 2013-07-14
    • 2020-09-20
    • 2012-06-20
    • 2012-05-28
    相关资源
    最近更新 更多