Login

Model dependency graph using graphviz (script)

Author:
andrew
Posted:
September 5, 2008
Language:
Python
Version:
1.0
Tags:
models graph graphviz
Score:
3 (after 3 ratings)

Instructions: Set your environment variables, install graphviz, and run.

Finds all models in your installed apps and makes a handsome graph of your their dependencies based on foreignkey relationships. Django models have green outlines, yours have purple. The edge styling could be changed based on edge type.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
from django.conf import settings
from django.db.models import Model, ForeignKey, ManyToManyField, OneToOneField

def get_all_models():
    all_models = set()
    
    for app in settings.INSTALLED_APPS:
        try:
            models = __import__(app + '.models', {}, {}, 'models')
        except ImportError:
            continue
        for attr in dir(models):
            obj = getattr(models, attr)
            if isinstance(obj, object) and \
               hasattr(obj, '__bases__') and \
               Model in obj.__bases__:
                    all_models.add(obj)
    return all_models

def get_relns(all_models):
    foreignkeys = []
    one_to_one = []
    many_to_many = []
    
    for model in all_models:
        for field in model._meta.fields:
            if isinstance(field, OneToOneField): # must come before FK
                to = field.rel.to
                if to not in all_models:
                    raise ValueError
                print "%s o2o to %s" % (repr(model), repr(to))
                one_to_one.append((model, to))
            elif isinstance(field, ForeignKey):
                to = field.rel.to
                if to not in all_models:
                    raise ValueError
                print "%s fk to %s" % (repr(model), repr(to))
                foreignkeys.append((model, to))
            elif isinstance(field, ManyToManyField):
                to = field.rel.to
                if to not in all_models:
                    raise ValueError
                print "%s m2m to %s" % (repr(model), repr(to))
                many_to_many.append((model, to))
    return foreignkeys, one_to_one, many_to_many
                   
def class_name(cls):
    name = repr(cls)
    name = name[name.index("'")+1:]
    name = name[:name.index("'")]
    return quoted(name)

# A lot of code below this line is copied from some stuff I wrote for my paper.
# Should probably be cleaned up.

import os, collections

BG_COLOR = 'black'
FG_COLOR = 'white'

RED    = '"#B30000"'
GREEN  = '"#008F00"'
PURPLE = '"#24006B"'
YELLOW = '"#B38F00"'
GREY   = '"#8F8F8F"'
COLORS = [YELLOW, GREEN, PURPLE, RED]

def quoted(s):
    return '"%s"' % str(s)
    
def nodename(state):
    return quoted(state[1]) if (state[1] != '') else quoted('start')

def attrs_string(attrs):
    return '[%s]' %  ', '.join(["%s=%s" % (k, v) for k, v in attrs.iteritems()])
    
def model_attrs_string(model, extra_node_attrs, labels):
    attrs = {}
    attrs['color'] = GREEN if 'django' in repr(model) else PURPLE
    if not labels:
        attrs['label'] = '""'
    
    attrs.update(extra_node_attrs.get(model, {}))
    return attrs_string(attrs)

def edge_attrs_string(c):
    return attrs_string({
        'label': quoted(c),
     })

def fg_attrs_string(extra={}):
    attrs = {
        'color': FG_COLOR,
        'fontcolor': FG_COLOR
      }
    attrs.update(extra)
    return attrs_string(attrs)

def relns_to_dot(models, foreignkeys, one_to_one, many_to_many, extra_node_attrs={}, **kwargs):
    edge_len = kwargs.pop('edgelen', 3)
    labels = kwargs.pop('labels', True)
    result = []
    result.append('digraph django_model_relationships {')
    result.append('    graph [bgcolor=%s];' % BG_COLOR)
    result.append('    node %s;' % fg_attrs_string())
    result.append('    edge %s;' % fg_attrs_string({'len': 3}))
    result.append('    rankdir=LR;')
    result.append('    size="30,20!";')   
    for model in sorted(models):
        result.append('    %s %s;' % (class_name(model), model_attrs_string(model, extra_node_attrs, labels)))
    for source, dest in foreignkeys:
        edge_label = 'fk'
        result.append('    %s -> %s %s;' % (class_name(source), class_name(dest), edge_attrs_string(edge_label)))
    for source, dest in one_to_one:
        edge_label = 'o2o'
        result.append('    %s -> %s %s;' % (class_name(source), class_name(dest), edge_attrs_string(edge_label)))
    for source, dest in many_to_many:
        edge_label = 'm2m'
        result.append('    %s -> %s %s;' % (class_name(source), class_name(dest), edge_attrs_string(edge_label)))
    result.append('}')
    return '\n'.join(result)

def write_dot(dot, filename, neato_options={}, *args, **kwargs):
    program = kwargs.pop('program', 'dot')
    f = open(filename, 'w')
    f.write(dot.encode('utf-8'))
    f.close()
    neato_options_str = ' '.join("-G%s=%s" % pair for pair in neato_options.iteritems())
    cmd = "%s %s %s -T png -o %s.png" % (program, filename, neato_options_str, filename)
    os.system(cmd)

def main():
    filename = '/tmp/model_graph.dot'
    models = get_all_models()
    relns = get_relns(models)
    dot = relns_to_dot(models, *relns)
    write_dot(dot, filename)

if __name__ == '__main__':
    main()
    

More like this

  1. Automatic graph of your database structure by adamlofts 6 years, 5 months ago
  2. dynamic model graph by findlay 6 years, 11 months ago
  3. Diagram of your database structure by vnbang2003 3 years, 8 months ago
  4. Simple Entity Relationship Diagram using GraphViz by bendavis78 5 years, 11 months ago
  5. See what depends on a given model by jdunck 4 years, 2 months ago

Comments

aarond10ster (on September 5, 2008):

I love the idea but it doesn't work for me. I get the Django models but not my own. Am I doing something wrong here:

DJANGO_SETTINGS_MODULE=myproject.settings python ../graphdep.py

I will try to track down the problem later - just thought I'd give a heads up now.

#

peterbe (on September 5, 2008):

@aarond10ster: I put graphdep.py into the project (not the parent of the project). Then I did this:

export DJANGO_SETTINGS_MODULE="settings"
python graphdep.py
gwenview /tmp/model_graph.dot.png

#

andrew (on September 7, 2008):

To #1, it depends on your setup. It should work like any other python script.

To #3: A few differences. This one: does not display database columns; automatically detects your installed apps; is a stand-alone script; visually distinguishes built-in models from your own; and, in my humble opinion, produces sexier output.

#

leosh (on July 24, 2009):

It looks like edge_len is unused in relns_to_dot()

Is that intentional?

#

andrew (on July 29, 2010):

Nope!

#

andrew (on November 1, 2010):

This thing also has path issues and is not updated to work with model inheritence. get_all_models should use importlib and should check whether obj issubclass of django.db.models.model

#

Please login first before commenting.