Sunday, May 17, 2020

Python Generated Sequence Diagrams


Understanding a system or software architecture simply, truly, absolutely, undoubtedly requires an occasional sequence diagram.

I left university before object-oriented design became the norm, but it was trending up.  As part of my first job we were officially trained in OO and the use of Rational Rose.  The tool was integrated into our development process and environment, I embraced that tool, particularly in the ability to depict class interactions via 'message trace diagrams', otherwise known as 'sequence diagrams'.  Unfortunately, Rational Rose is pricey and uncommon in today's development practices but sequence diagrams are alive and well.  Whiteboarding diagrams is tedious and time-consuming, but the inter-webs has on-line tools that really ease the creation of diagrams; like this fella right here: https://www.websequencediagrams.com/

With a bit of effort, Python paired with ImageMagick can generate a rough approximation.

A quick tool/library can make this:
  diagram=Diagram();
  obj1=Object('object1');
  obj2=Object('object2');
  obj3=Object('object3');

  m1=Message(obj1,obj2,'method1(arg1)');
  diagram.add(m1);

  m2=Message(obj2,obj1,'return val');
  diagram.add(m2);

  m3=Message(obj1,obj1,'method2(abc)');
  diagram.add(m3);

  m4=Message(obj1,obj3,'method3()');
  diagram.add(m4);

  diagram.draw();

into this:


The code/library follows:
user@kaylee:~/PyMtd$ cat -n drawMtd 
     1 #!/usr/bin/python
     2 import os;
     3
     4 def log(S):
     5   print "__LOG '%s'"%(S);
     6   pass;
     7
     8 class Diagram: 
     9   def __init__(self):
    10     self.outFile_="./out.jpg";
    11     self.width_=0;
    12     self.height_=0;
    13     self.msgList_=[];
    14
    15   def add(self, msg):
    16     self.msgList_.append(msg);
    17
    18   def textDim(self, text):
    19     cmd="convert label:'%s' %s"%(text,'temp.jpg');
    20     os.system(cmd);
    21     cmd="identify temp.jpg | cut -f 3 -d ' '";
    22     retVal=os.popen(cmd).read();
    23     os.system("rm temp.jpg");
    24     return retVal.rstrip('\n');
    25
    26   def draw(self):
    27     betweenObj=100;
    28     objY=20;
    29
    30     #--position object, heads of lifelines
    31     iW=betweenObj;
    32     for msg in self.msgList_:
    33       msg.src_.y_=objY;
    34       msg.sink_.y_=objY;
    35       if msg.src_.x_==0:
    36         msg.src_.x_=iW;
    37         iW+=betweenObj;
    38       if msg.sink_.x_==0:
    39         msg.sink_.x_=iW;
    40         iW+=betweenObj;
    41
    42     #--draw message lines
    43     cmd="convert ";
    44     y=50;
    45     rightArrow="l -15,-5  +5,+5  -5,+5  +15,-5 z"
    46     leftArrow="l +15,+5  -5,-5  +5,-5  -15,+5 z"
    47     for msg in self.msgList_:
    48       src=msg.src_;
    49       sink=msg.sink_;
    50       if src.x_ == sink.x_:
    51         W=30;
    52         H=20;
    53         cmd+="-draw 'line %d, %d %d,%d' "%(src.x_,y, src.x_+W,y);
    54         cmd+="-draw 'line %d, %d %d,%d' "%(src.x_+W,y,src.x_+W,y+H);
    55         cmd+="-draw 'line %d, %d %d,%d' "%(src.x_+W,y+H,src.x_,y+H);
    56         cmd+="-draw \"path \'M %d,%d %s'\" "%(src.x_,y+H,leftArrow);
    57         D=self.textDim(msg.label_);
    58         tH=int(D.split('x')[0])/2;
    59         tW=int(D.split('x')[1])/2;
    60         cmd+="-draw 'text %d,%d \"%s\"' "%(src.x_+W+5,y+((tH)/2), msg.label_);
    61         y=y+H;
    62       else:
    63         cmd+= "-draw 'line %d,%d %d,%d' "%(src.x_,y,sink.x_,y);
    64         cmd+="-draw \"path \'M %d,%d %s'\" "%(sink.x_,y,(leftArrow if src.x_ > sink.x_ else rightArrow));
    65         D=self.textDim(msg.label_);
    66         textWidth=int(D.split('x')[0])/2;
    67         textHeight=int(D.split('x')[1])/2;
    68         cmd+="-draw 'text %d,%d \"%s\"' "%((src.x_+sink.x_)/2-textWidth, y-(textHeight/2), msg.label_);
    69       y+=20;
    70     self.height_=y+50;
    71     self.width_=iW;
    72     cmd+="-size %dx%d xc:white -fill none -stroke black "%(self.width_,self.height_);
    73
    74     #--draw lifeline
    75     L=[];
    76     for msg in self.msgList_:
    77       src=msg.src_;
    78       sink=msg.sink_;
    79       D=self.textDim(src.name_);
    80       w=int(D.split('x')[0])/2;
    81       for obj in [src, sink]:
    82         draw=not (obj in L);
    83         if (draw):
    84           cmd+="-draw 'text %d,%d \"%s\"' "%(obj.x_-w,obj.y_-5,obj.name_);
    85           cmd+="-draw 'stroke-dasharray 5 5 line %d, %d %d,%d' "%(obj.x_,obj.y_, obj.x_,self.height_-20);
    86           L.append(obj);
    87     
    88     cmd+="%s"%(self.outFile_);
    89     log(cmd);
    90     os.system(cmd);
    91
    92 class Object:
    93   def __init__(self,name):
    94     self.name_=name;
    95     self.x_=0;
    96     self.y_=0;
    97
    98 class Message:
    99   def __init__(self, srcObj, sinkObj,label):
   100     self.src_=srcObj;
   101     self.sink_=sinkObj;
   102     self.label_=label;
   103
   104
   105 def test00():
   106   diagram=Diagram();
   107   obj1=Object('object1');
   108   obj2=Object('object2');
   109   obj3=Object('object3');
   110
   111   m1=Message(obj1,obj2,'method1(arg1)');
   112   diagram.add(m1);
   113
   114   m2=Message(obj2,obj1,'return val');
   115   diagram.add(m2);
   116
   117   m3=Message(obj1,obj1,'method2(abc)');
   118   diagram.add(m3);
   119
   120   m4=Message(obj1,obj3,'method3()');
   121   diagram.add(m4);
   122
   123   diagram.draw();
   124
   125 def test01():
   126   diagram=Diagram();
   127   diagram.draw();
   128
   129 def test02():
   130   L=[];
   131   for i in range(0,10):
   132     L.append(Object('object%d'%i));
   133
   134   diagram=Diagram();
   135   for obj in L:
   136     m=Message(L[0],obj,'message x');
   137     diagram.add(m);
   138     m=Message(L[0],obj,'methodX()');
   139     diagram.add(m);
   140     diagram.add(Message(obj,obj,'ping'));
   141
   142   diagram.draw();
   143
   144 #---main---
   145 test00();
   146 #test01();
   147 #test02();

Pair this with some logging analysis, or debugger traces and you could auto-generate entire system interactions with ease.

Take it and do great things.

3 comments:

  1. This is great in a python-only environment. But plantuml is a great room.

    ReplyDelete
  2. In line 19 of the code, a command string is created referring to a temp.jpg. How is this image created? the convert command doesn't seem happy with the arguments passed to it

    ReplyDelete
    Replies
    1. The 'temp.jpg' is the created image file. The command results in the form: "convert label:'SomeLabel' temp.jpg", which means 'create a text label SomeLabel and store it in a JPG image file called temp.jpg. Home that helps.

      Delete