Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added script for the generation of state diagrams #29

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added doc/recruitment.pdf
Binary file not shown.
23 changes: 23 additions & 0 deletions doc/recruitment.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
initial()

compound_state(Application)
state(A, Non Hire)
styles(node, [fontname=Helvetica, fontcolor=red])
state(B, New Hire,[entry/Candidate Hire, do/Add Employee])
compound_end()

state(C, Employee, [entry/Intake New Employee])
state(D, Retiree, [do/Employee Retirement])
state(E, Former Employee, [do/Terminate, exit/Archive])

final()

transition(initial, B, Accepts offer)
transition(initial, A, Rejects offer)
transition(B, C, Begins work)
transition(C, C, Boring Reality)
transition(C, D, Employee Retires, [color=blue])
transition(C, E, Employee Terminates, [color=red])
transition(D, final)


90 changes: 90 additions & 0 deletions doc/stategraph-READ-ME.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
stategraph
========

Stategraph is a python script which allows the declarative specification and drawing of UML state diagrams.


How to use it:
--------

1) Use the commands stated below to declare a state diagram and save it in a text file.
2) Change your directory to the place where both the script and the text file are located
3) Run the script as stated below
4) At the test-output directory you will find the diagram in the format you provided.


State diagram commands
--------

state(id, name, events[])
-description:Creates a state
-arguments: id: Unique identifier for the state inside the source (mandatory).
name: Caption to be displayed (defaults to the state id).
events[]: Events of the state (optional).


transition(source, target, label, styling[])
-description:Creates a transition
-arguments: source: Source state identifier (mandatory).
target: Target state identifier (mandatory).
label: Caption to be displayed near the transition (optional).
styling[]: Array of styling attributes to be applied (optional).


initial()
-description:The initial state. To be referred as initial when referenced in a transition.


final()
-description:The final state. To be referred as final when referenced in a transition.


compound_state(name)
-description: Creates a subgraph which includes states and transitions
-arguments: name (optional)


compound_end()
-description: Ends the compound state


styles(element, styling[])
-description: Applies styling to the element specified
-arguments: element (mandatory), styling[] (mandatory)


note(id, note)
-description: Applies a note to the state specified
-arguments: node_id (mandatory), note (mandatory)


Running the script
------------

Run the script as stategraph.py [-o output_file] [-v] [input file]

1) If an output file is not specified a pdf is generated using
the name of the input file.

2) The -v parameter will open the diagram for you to view

example: Running the script as stategraph.py -recruitment.txt -v -o recruitment.png
will create a png file called recruitment in the test-output directory as well as
open the file right away.


Example
-------

A full example containing both the declaration and the resulting diagram can be found
under the stategraph example directory.

The example is called recruitment.txt and the generated pdf is called
recruitment.pdf


Support
-------

If you are having issues, please let us know.
Contact at: [email protected]
232 changes: 232 additions & 0 deletions stategraph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
#!/usr/bin/env python

from graphviz import Digraph
from pyparsing import *
import argparse
import os

# -----Parser-----


LP, RP, LB, RB = map(Suppress, "()[]") # those will be skipped when iterating
tokens = "+" + "'" + "=" + "/" + "_"

item = originalTextFor(OneOrMore(Word(alphanums + tokens))) # originalText keeps the whitespaces between words

table = LB + item + ZeroOrMore(Suppress(Literal(",")) + item) + RB # table: [item, item, ...]

argument = item | Group(table) # argument = item or [item, item, ...]

listOfItems = Optional(argument + ZeroOrMore(Suppress(Literal(",")) + argument)) # LoI: [argument, argument, ...]

sentence = item.setResultsName("action") + LP + listOfItems.setResultsName("args") + RP # sentence: item(LoI)

rule = OneOrMore(Group(sentence)) # rule: sentence, sentence, ...


def add_state(graph, args_list):

""" Creates the state based on the arguments given

:param args_list:

args_list[0]: Unique identifier for the state inside the source(MANDATORY).
args_list[1]: Caption to be displayed (defaults to the state id).
args_list[2]: Events of the state(OPTIONAL).
"""

if len(args_list) > 3 or len(args_list) == 0:
raise Exception("states must follow the pattern: (id (mandatory), name (mandatory), "
"events[](optional), style[](optional))")
else:

if len(args_list) == 2:
graph.node(args_list[0], args_list[1])

elif len(args_list) == 3:
graph.node(args_list[0], shape="record", label="<f0>" + args_list[1] + "|<f1>" +
'\\n'.join([str(lst) for lst in args_list[2]]))


def add_transition(graph, args_list):

""" Creates the transition based on the arguments given

:param args_list :

args_list[0]: Source state identifier(MANDATORY).
args_list[1]: Target state identifier(MANDATORY).
args_list[2]: Caption to be displayed near the transition(OPTIONAL).
args_list[3]: Any styling to be applied(OPTIONAL).
"""

if len(args_list) > 4 or len(args_list) < 2:
raise Exception("transitions must follow the pattern: "
"(source (mandatory), target (mandatory), label (optional), styling[] (optional))")
else:

if len(args_list) == 2:
graph.edge(args_list[0], args_list[1])

elif len(args_list) == 3:
graph.edge(args_list[0], args_list[1], args_list[2])

else:
# turns the args_list to a styling dictionary
styles = list_to_dict(args_list[3])

graph.edge(args_list[0], args_list[1], args_list[2], styles)


def apply_styles(graph, args_list):
element = args_list[0] # where the styling will be applied (graph, node, edge)
styles = list_to_dict(args_list[1])

if element == 'graph':
graph.graph_attr.update(styles)
elif element == 'node':
graph.node_attr.update(styles)
elif element == 'edge':
graph.edge_attr.update(styles)
else:
raise Exception("styling must be applied to either graphs, nodes or edges")


def add_note(graph, args_list):
note_name = "note" + args_list[0] # unique note name
graph.node(note_name, args_list[1], shape="note")
graph.edge(note_name, args_list[0], arrowhead="none", style="dashed")


def initial(graph):
graph.node("initial", shape="circle", style="filled", fillcolor="black", label="", width='0.3')


def final(graph):
graph.node("final", shape="doublecircle", style="filled", fillcolor="black", label="", width='0.3')


def compound_state(args_list):
g2 = Digraph('cluster_g2') # subgraph's name MUST start with cluster_
g2.body.extend(['rankdir=LR'])
g2.body.append('color=black')
g2.body.append('style=rounded')

if args_list:
label = args_list[0]
g2.body.append('labeljust=center')
g2.body.append('label="' + label + '"')

return g2


def list_to_dict(alist):

""" Takes the list that contains the styling arguments to be applied
and turns it to a dictionary which graphviz understands
"""

changed_dict = {}
for each_item in alist:
a = each_item.split("=")
changed_dict[a[0]] = a[1]

return changed_dict


def parse_and_draw(graph, script):

""" Parses every line of the script and calls
an add method depending on that line's action command

:param graph: current graph being used

:param script: txt file being parsed
"""

entry = graph
try:

for line in rule.parseString(script):

if line.action == "initial":
initial(graph)

if line.action == "final":
final(graph)

if line.action == "state":

try:
add_state(graph, line.args)

except Exception as e:
raise Exception(str(e))

if line.action == "transition":

try:
add_transition(graph, line.args)
except Exception as e:
raise Exception(str(e))

if line.action == "note":
add_note(graph, line.args)

if line.action == "styles":
apply_styles(graph, line.args)

if line.action == "compound_state":
graph = compound_state(line.args)

if line.action == "compound_end":
entry.subgraph(graph)
graph = entry

return graph

except Exception as e:

print "could not draw diagram because: " + str(e)


def render_graph(cmd_args, graph):

""" Renders the graph and generates the output files as specified by the user
If an output file is not specified. A pdf is generated using
the name of the input file.
The extension dot is erased because the format function can't handle it.

:param cmd_args: contains the input file, the viewing preference and the output file
:param graph: graph to render
"""

if cmd_args.output is not None:
filename, file_extension = os.path.splitext(cmd_args.output)

else:
filename = os.path.splitext(cmd_args.filename)[0]
file_extension = 'pdf'

graph.format = file_extension.replace('.', '')

graph.render('test-output/' + filename, view=cmd_args.view)


# parse cmd arguments
parser = argparse.ArgumentParser()
parser.add_argument("filename")
parser.add_argument("-v", "--view", action="store_true", default=False)
parser.add_argument("-o", "--output", type=str)

args = parser.parse_args()

with open(args.filename) as f:
content = f.read().splitlines()

script = "\n".join(content)

g1 = Digraph('g1', node_attr={'shape': 'box', 'style': 'rounded'})
g1.body.extend(['rankdir=LR']) # graph's direction left-->right

render_graph(args, parse_and_draw(g1, script))