10👍
This is not the full solution, but I hope it can be a good starting point. Consider this code:
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
We can get the dependencies for the app name list using inspect
, django get_models()
, and signals
in this manner:
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)
This gives:
A auto create -> C
C auto create -> B
Updated: change method to found overridden save
by the instance class dict.
is_self_save = 'save' in model().__class__.__dict__.keys()
6👍
(Too long to fit into a comment, lacking code to be a complete answer)
I can’t mock up a ton of code right now, but another interesting solution, inspired by Mario Orlandi’s comment above, would be some sort of script that scans the whole project and searches for any overridden save methods and pre and post save signals, tracking the class/object that creates them. It could be as simple as a series of regex expressions that look for class
definitions followed by any overridden save
methods inside.
Once you have scanned everything, you could use this collection of references to create a dependency tree (or set of trees) based on the class name and then topologically sort each one. Any connected components would illustrate the dependencies, and you could visualize or search these trees to see the dependencies in a very easy, natural way. I am relatively naive in django, but it seems you could statically track dependencies this way, unless it is common for these methods to be overridden in multiple places at different times.
- [Django]-Cannot set Django to work with smtp.gmail.com
- [Django]-Django rest framework lookup_field through OneToOneField
- [Django]-With DEBUG=False, how can I log django exceptions to a log file
0👍
If you only want to track models saves, and not interested in other things happening inside overridden save methods and signals, you can use a mechanism like angio. You can register a global post_save receiver, without a sender argument, one that will be called for all model saves, and print the saved model name in that function. Then, write a script to just call save for all existing models. Something like the following could work:
@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')
With the following model structure;
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()
The script would print:
Saving A
--> A
--> B
--> C
Saving B
--> B
--> C
Saving C
--> C
This is just a basic sketch of what could be done, and can be improved according to the structure of your application. This assumes you already have an entry in the db for each model. Though not changing anything, this approach also saves things in the database, so would be better run on a test db.
- [Django]-How to force application version on AWS Elastic Beanstalk
- [Django]-How to resize an ImageField image before saving it in python Django model
- [Django]-Iterating over related objects in Django: loop over query set or use one-liner select_related (or prefetch_related)
0👍
Assuming your ultimate goal is to track changes in database when an instance of some model is saved, one potential solution can be scanning database for changes instead of source code. The upside of this approach is that it can also cover dynamic code. And downside is, obviously, it will ONLY cover database changes.
This can be achieved using simple testing techniques. Assuming following models..
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())
I can write up a test case which takes count all database instances, creates an instance of a model, takes count again and calculates the difference. Database instances can be created using factories like mommy. Here is a simple but working example of this technique.
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
And my output is
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
For large applications it can be slow, but I use in memory sqlite database for testing and it runs pretty fast.
- [Django]-Django: how save bytes object to models.FileField?
- [Django]-Do I need Nginx with Gunicorn if I am not serving any static content?
- [Django]-Django project models.py versus app models.py
0👍
I’working in a Django app that does something similar, but while I get it done, I will comment about the use case you’ve presented here:
I need to be sure that doing one thing one place won’t affect something on the other side of the project …
You surely could write tests with some dummy signal handlers in order to know if the execution of certain code, triggers unwanted behavior, for instance:
# 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)
Why this is better than having a script that show’s the events chain?
Well, as you say, when the need for things like this emerges, is because the size of the project is very big, and if you use a tool like you are asking for, you can end with a result like this:
Save A -> Creates B -> Creates C
Save B -> Creates D
Save B -> Creates C
.
.
.
# Imagine here 3 or 4 more lines.
You will end up solving a puzzle every time you want to add some code, that saves/modify something.
However …
It would be better, to write your code, and then, some test fails (resolving the puzzle for you) and showing to you exactly where will your code miss-behave.
Conclusion:
Implements those tests and your life will be easier.
Best scenario using tests: Write your code, and if no test fail, you’re ready to tackle your next programming task.
Worst scenario using tests: Write your code, some test fail, as you know where exactly your code broke, just fix it.
Best scenario using the tool: Analyze the tool output, write your code, everything is ok.
Worst scenario using the tool: Analyze the tool output, write your code, something fails, repeat until all ok.
So, a tool like that would be helpful? Of course, but is not the right tool to ensure things are well, use tests for that.
- [Django]-Add a custom button to a Django application's admin page
- [Django]-Folder Structure for Python Django-REST-framework and Angularjs
- [Django]-Django template includes slow?