Джанго - как визуализировать сигналы и сохранять переопределения?

По мере роста проекта растут зависимости и цепочки событий, особенно в переопределенных методах save() и post_save и pre_save.

Пример:

A.save создает два связанных объекта с A - B и C Когда C сохраняется, post_save сигнал post_save который делает что-то еще и т.д...

Как сделать эти подбородки событий более понятными? Есть ли способ визуализировать (генерировать автоматически) такие цепочки/потоки? Я не ищу ERD, ни Class диаграммы. Я должен быть уверен, что выполнение одной вещи в одном месте не повлияет на что-то на другой стороне проекта, поэтому лучше всего будет использовать простую визуализацию.

РЕДАКТИРОВАТЬ

Чтобы было ясно, я знаю, что было бы почти невозможно проверить динамически генерируемые сигналы. Я просто хочу проверить все (не генерируемые динамически) post_save, pre_save и переопределенные save и визуализировать их, чтобы я мог сразу видеть, что происходит и где, когда я что-то save.

Ответ 1

Это не полное решение, но я надеюсь, что это может быть хорошей отправной точкой. Рассмотрим этот код:

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

Обновлено: изменение метода на найденное переопределенное save классом dict экземпляра.

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

Ответ 2

(Слишком долго, чтобы вписаться в комментарий, не хватает кода, чтобы быть полным ответом)

Я не могу макетировать тонну кода прямо сейчас, но другое интересное решение, вдохновленное комментарием Марио Орланди выше, было бы своего рода сценарием, который сканирует весь проект и ищет любые переопределенные методы сохранения и сигналы до и после сохранения, отслеживание класса/объекта, который их создает. Это может быть так же просто, как серия выражений регулярных выражений, которые ищут определения class за которыми следуют любые переопределенные методы save внутри.

После того как вы отсканировали все, вы можете использовать эту коллекцию ссылок для создания дерева зависимостей (или набора деревьев) на основе имени класса, а затем топологически отсортировать каждое из них. Любые связанные компоненты будут иллюстрировать зависимости, и вы можете визуализировать или искать эти деревья, чтобы увидеть зависимости очень простым и естественным способом. Я относительно наивен в django, но кажется, что вы могли бы статически отслеживать зависимости таким образом, если только эти методы не являются переопределенными в нескольких местах в разное время.

Ответ 3

Python, являющийся динамическим языком, плохо подходит для статического анализа.

Рекомендуемый подход с динамическими языками состоит в том, чтобы использовать модульное тестирование, чтобы гарантировать, что все требования выполнены. Ваш проект должен иметь проектный документ, из которого получены требуемые функциональные возможности, тогда тесты обеспечат надлежащее соответствие, проверив, что ваши динамически подключенные функции вызываются в правильном порядке или порядке, как определено в вашем документе.

Ответ 4

Если вы хотите отслеживать только сохранение моделей и не интересоваться другими вещами, происходящими внутри переопределенных методов сохранения и сигналов, вы можете использовать механизм, такой как angio. Вы можете зарегистрировать глобальный получатель post_save без аргумента отправителя, который будет вызываться для всех сохранений модели, и распечатать имя сохраненной модели в этой функции. Затем напишите скрипт, который будет вызывать 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

Это просто базовый набросок того, что можно сделать, и его можно улучшить в соответствии со структурой вашего приложения. Это предполагает, что у вас уже есть запись в БД для каждой модели. Хотя этот подход ничего не меняет, он также сохраняет данные в базе данных, поэтому его лучше запустить на тестовой базе данных.

Ответ 5

Предполагая, что вашей конечной целью является отслеживание изменений в базе данных при сохранении экземпляра некоторой модели, одним из возможных решений может быть сканирование базы данных на предмет изменений вместо исходного кода. Преимущество этого подхода в том, что он также может охватывать динамический код. И недостаток, очевидно, это будет охватывать только изменения базы данных.

Это может быть достигнуто с помощью простых методов тестирования. Предполагая следующие модели..

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())

Я могу написать тестовый пример, который подсчитывает все экземпляры базы данных, создает экземпляр модели, снова считает и вычисляет разницу. Экземпляры базы данных могут быть созданы с использованием таких фабрик, как мама. Вот простой, но рабочий пример этой техники.

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 для тестирования, и она работает довольно быстро.

Ответ 6

Я работаю в приложении Django, которое делает нечто похожее, но пока я это делаю, я прокомментирую пример использования, который вы представили здесь:

Я должен быть уверен, что выполнение одной вещи в одном месте не повлияет на другую сторону проекта...

Вы наверняка могли бы написать тесты с некоторыми фиктивными обработчиками сигналов, чтобы узнать, вызывает ли выполнение определенного кода нежелательное поведение, например:

# I use pytest, put this example is suitable also for 
# django 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.

В конечном итоге вы решаете головоломку каждый раз, когда хотите добавить код, который что-то сохраняет/изменяет.

Тем не мение...

Было бы лучше написать свой код, а затем некоторые тесты не пройдут (решая для вас загадку) и покажут вам , где именно ваш код будет работать неправильно.

Заключение:

Выполните эти тесты, и ваша жизнь станет проще.

Лучший сценарий с использованием тестов: напишите свой код, и если тест не пройден, вы готовы приступить к следующей задаче программирования.

Худший сценарий с использованием тестов: напишите свой код, некоторые тесты не пройдены, поскольку вы знаете, где именно ваш код сломался, просто исправьте его.

Лучший сценарий с использованием инструмента: проанализируйте выходные данные инструмента, напишите свой код, все в порядке.

Наихудший сценарий с использованием инструмента: проанализируйте выходные данные инструмента, напишите свой код, что-то не получается, повторяйте, пока все не будет в порядке.

Итак, такой инструмент будет полезен? Конечно, но это не тот инструмент, который гарантирует, что дела идут хорошо, используйте для этого тесты.