Django-如何可视化信号并保存替代?

时间:2019-04-08 16:54:25

标签: python django architecture software-design

随着项目的发展,依存关系和事件链也会不断增长,尤其是在覆盖的save()方法以及post_savepre_save信号中。

示例:

被覆盖的A.saveA-BC创建两个相关的对象。保存C后,将调用post_save信号执行其他操作,等等。

如何弄清楚这些事件下巴?有没有办法可视化(自动生成)这样的链/流?我不是在寻找ERD还是Class图。我需要确保在一个地方做一件事情不会影响项目另一端的事情,所以简单的可视化将是最好的。

编辑

要清楚,我知道检查动态生成的信号几乎是不可能的。我只想检查所有(不是动态生成的)post_savepre_save和覆盖的save方法并对其进行可视化,这样我就可以立即查看正在发生什么以及何时{{1} }。

6 个答案:

答案 0 :(得分:10)

这不是完整的解决方案,但我希望它可以成为一个好的起点。考虑以下代码:

from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver

class A(models.Model):
    def save(self, *args, **kwargs):
        if not self.pk:
            C.objects.create()

class B(models.Model):
    pass

class C(models.Model):
    b = models.ForeignKey(B, on_delete=models.CASCADE, blank=True)

@receiver(pre_save, sender=C)
def pre_save_c(sender, instance, **kwargs):
    if not instance.pk:
        b = B.objects.create()
        instance.b = b

我们可以使用inspect,django get_models()signals通过以下方式获取应用名称列表的依赖关系:

import inspect
import re
from collections import defaultdict

from django.apps import apps
from django.db.models import signals

RECEIVER_MODELS = re.compile('sender=(\w+)\W')
SAVE_MODELS = re.compile('(\w+).objects.')

project_signals = defaultdict(list)
for signal in vars(signals).values():
    if not isinstance(signal, signals.ModelSignal):
        continue
    for _, receiver in signal.receivers:
        rcode = inspect.getsource(receiver())
        rmodel = RECEIVER_MODELS.findall(rcode)
        if not rmodel:
            continue
        auto_by_signals = [
            '{} auto create -> {}'.format(rmodel[0], cmodel)
            for cmodel in SAVE_MODELS.findall(rcode)
        ]
        project_signals[rmodel[0]].extend(auto_by_signals)

for model in apps.get_models():
    is_self_save = 'save' in model().__class__.__dict__.keys()
    if is_self_save:
        scode = inspect.getsource(model.save)
        model_name = model.__name__
        for cmodel in SAVE_MODELS.findall(scode):
            print('{} auto create -> {}'.format(model_name, cmodel))
            for smodels in project_signals.get(cmodel, []):
                print(smodels)

这给出了:

A auto create -> C
C auto create -> B

已更新:更改方法以查找实例类dict覆盖的save

is_self_save = 'save' in model().__class__.__dict__.keys()

答案 1 :(得分:5)

(注释太长,缺少完整的答案代码)

我现在无法模拟大量代码,但是另一个有趣的解决方案,是由马里奥·奥兰迪(Mario Orlandi)的上述评论启发而来的,它将是一种脚本,它可以扫描整个项目并搜索任何覆盖的保存方法,并在发布之前和发布之后保存信号,跟踪创建信号的类/对象。它可能像一系列正则表达式表达式一样简单,这些表达式表达式寻找class定义,后跟内部的任何覆盖的save方法。

一旦您扫描了所有内容,就可以使用该引用集合根据类名称创建一个依赖树(或树集),然后对每个树进行拓扑排序。任何连接的组件都将说明依赖关系,您可以可视化或搜索这些树以非常简单自然的方式查看依赖关系。我在django中比较天真,但是看来您可以通过这种方式静态跟踪依赖项,除非这些方法在不同时间在多个地方被覆盖是很普遍的。

答案 2 :(得分:0)

Python是一种动态语言,因此无法很好地进行静态分析。

使用动态语言的推荐方法是使用单元测试,以确保满足所有要求。您的项目应该有一个设计文档,可以从该文档中获得所需的功能,然后这些测试将通过验证按您的文档中定义的正确方式或顺序调用动态挂钩函数来确保正确的一致性。

答案 3 :(得分:0)

如果您只想跟踪模型保存,而对覆盖的保存方法和信号中发生的其他事情不感兴趣,则可以使用诸如angio之类的机制。您可以注册一个没有sender参数的全局post_save接收者,该模型将被所有模型保存调用,并在该函数中打印保存的模型名称。然后,编写脚本以仅调用所有现有模型的保存。类似以下内容可能会起作用:

@receiver(models.signals.post_save)
def global_post_save(sender, instance, created, *args, **kwargs):
    print(' --> ' + str(sender.__name__))

from django.apps import apps
for model in apps.get_models():
    instance = model.objects.first()
    if instance:
        print('Saving ' + str(model.__name__))
        instance.save()
        print('\n\n')

具有以下模型结构;

class A(models.Model):
    ...
    def save(self, *args, **kwargs):
        B.objects.create()

@receiver(post_save, sender=B)
def post_save_b(sender, instance, **kwargs):
    C.objects.create()

脚本将打印:

Saving A
 --> A
 --> B
 --> C

Saving B
 --> B
 --> C

Saving C
 --> C

这只是可以完成的基本工作,可以根据您的应用程序的结构进行改进。假定您已经在每个模型的数据库中都有一个条目。尽管不做任何更改,但这种方法还可以将内容保存在数据库中,因此最好在测试数据库上运行。

答案 4 :(得分:0)

假设您的最终目标是在保存某些模型的实例时跟踪数据库中的更改,那么一种潜在的解决方案是扫描数据库中的更改而不是源代码。这种方法的好处是它还可以涵盖动态代码。不利的一面是,显然,它将仅涵盖数据库更改。

这可以使用简单的测试技术来实现。假设有以下模型。

from django.db import models
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver


class B(models.Model):
    def save(self, *args, **kwargs):
        X.objects.create()
        super().save(*args, **kwargs)


class C(models.Model):
    y = models.OneToOneField('Y', on_delete=models.CASCADE)


class D(models.Model):
    pass


class X(models.Model):
    pass


class Y(models.Model):
    related = models.ForeignKey('Z', on_delete=models.CASCADE)


class Z(models.Model):
    pass


@receiver(pre_save, sender=D)
def pre_save_d(*args, instance, **kwargs):
    Z.objects.create()


@receiver(post_save, sender=C)
def pre_save_c(*args, instance, **kwargs):
    Y.objects.create(related=Z.objects.create())

我可以编写一个测试用例,对所有数据库实例进行计数,创建一个模型实例,再次进行计数并计算差值。可以使用mommy之类的工厂创建数据库实例。这是此技术的一个简单但可行的示例。

class TestModelDependency(TestCase):
    def test_dependency(self):
        models = apps.get_models()
        models = [model for model in models if model._meta.app_label == 'model_effects']

        for model in models:
            kwargs = self.get_related_attributes(model)

            initial_count = self.take_count(models)
            mommy.make(model, **kwargs)
            final_count = self.take_count(models)

            diff = self.diff(initial_count, final_count)

            print(f'Creating {model._meta.model_name}')
            print(f'Created {" | ".join(f"{v} instance of {k}" for k, v in diff.items())}')

            call_command('flush', interactive=False)

    @staticmethod
    def take_count(models):
        return {model._meta.model_name: model.objects.count() for model in models}

    @staticmethod
    def diff(initial, final):
        result = dict()
        for k, v in final.items():
            i = initial[k]
            d = v - i
            if d != 0:
                result[k] = d
        return result

    @staticmethod
    def get_related_attributes(model):
        kwargs = dict()
        for field in model._meta.fields:
            if any(isinstance(field, r) for r in [ForeignKey, OneToOneField]):
                kwargs[field.name] = mommy.make(field.related_model)
        return kwargs

我的输出是

Creating b
Created 1 instance of b | 1 instance of x
Creating c
Created 1 instance of c | 1 instance of y | 1 instance of z
Creating d
Created 1 instance of d | 1 instance of z
Creating x
Created 1 instance of x
Creating y
Created 1 instance of y
Creating z
Created 1 instance of z

对于大型应用程序,它可能很慢,但是我在内存sqlite数据库中使用它进行测试,并且运行速度非常快。

答案 5 :(得分:0)

我正在使用类似Django应用程序工作,但是当我完成该工作时,我将对您在此处介绍的用例进行评论:

  

我需要确保在一个地方做一件事情不会影响项目另一端的事情...

您肯定可以使用一些虚拟信号处理程序编写测试,以了解某些代码的执行是否会触发不良行为,例如:

# I use pytest, put this example is suitable also for 
# django's TestCase and others
class TestSome:

    # For Django TestCase this would be setUp
    def setup_method(self, test_method):

        self.singals_info = []

        def dummy_handler(*args, **kwargs):
            # collect_info is a function you must implement, it would
            # gather info about signal, sender, instance, etc ... and
            # save that info in (for example) self.signals_info.
            # You can then use that info for test assertions.
            self.collect_info(*args, **kwargs)

        # connect your handler to every signal you want to control
        post_save.connect(dummy_handler)


    def test_foo():
         # Your normal test here ...
         some_value = some_tested_function()

         # Check your signals behave 
         assert self.signals_behave(self.signals_info)

为什么这比拥有显示事件链的脚本更好?

正如您所说,当出现诸如此类的需求时,是因为项目规模很大,并且如果您使用所要求的工具,则可以得到如下结果:

Save A -> Creates B -> Creates C
Save B -> Creates D
Save B -> Creates C
.
.
.
# Imagine here 3 or 4 more lines.

每次添加一些代码时,您都会最终解决一个难题,该代码可以保存/修改某些内容。

但是...

最好编写代码,然后进行一些测试失败(为您解决难题),并准确地向您展示代码的错误行为。

结论:

实施这些测试,您的生活将会更加轻松。

使用测试的最佳方案:编写代码,如果没有测试失败,则准备解决下一个编程任务。

使用测试的最糟糕的情况:编写代码,某些测试失败,因为您知道代码的确切位置在哪里,只需对其进行修复。

使用该工具的最佳方案:分析工具输出,编写代码,一切正常。

使用该工具的最糟糕的情况:分析该工具的输出,编写您的代码,某些操作失败,重复直到一切正常。

那么,类似的工具会有所帮助吗?当然,但不是确保一切正常的正确工具,请为此进行测试。