From ffba2e2f55b80169b4fa1b5b90ff5572747bc565 Mon Sep 17 00:00:00 2001 From: Alkis Plaskovitis Date: Thu, 2 Jun 2016 15:15:24 +0300 Subject: [PATCH 1/2] Added script for the generation of state diagrams --- state_maker.py | 249 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 state_maker.py diff --git a/state_maker.py b/state_maker.py new file mode 100644 index 00000000..503900d5 --- /dev/null +++ b/state_maker.py @@ -0,0 +1,249 @@ +""" + +description: + + This script allows the declarative specification of uml state diagrams. + + 1) use the commands stated below to form 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) provide the arguments as stated below + 4) at the test-output directory you will find the diagram both in dot format and in pdf. + +cmd args: + + argv[1]: input file name. (Mandatory) + argv[2]: output file name. (Optional). Defaults to the input file's name + argv[3]: view. (Optional). If true the generated pdf opens immediately. Defaults to False + +commands: + + state: creates a state + arguments: id (mandatory), name (mandatory), events[](optional), style[](optional + + transition: creates a transition + arguments: source (mandatory), target (mandatory), label (optional), styling[] (optional) + + initial: the initial node. To be referred as initial + + final: the final node. To be referred as final + + compound_state: creates a subgraph which includes states and transitions + arguments: name (optional) + + compound_end: ends the subgraph + + styles: applies styling to the element specified + arguments: element (mandatory), styling[] (mandatory) + + note: applies a note to the state specified + arguments: node_id (mandatory), note (mandatory) + +""" + + +from graphviz import Digraph +from pyparsing import * +import sys + + +# -----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 + + Args of 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="" + args_list[1] + "|" + + '\\n'.join([str(lst) for lst in args_list[2]])) + + +def add_transition(graph, args_list): + """ + Creates the transition based on the arguments given + + Args of args_list: + args_list[0]: Start state identifier(MANDATORY). + args_list[1]: End state identifier(MANDATORY). + args_list[2]: Caption to be displayed near the edge(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 + + +# Parses every line of the script and calls an add method depending on that line's action command + +def parse_and_draw(graph, script): + 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 + + # checks if user wants to view the graph right away + if len(sys.argv) == 4: + accepted_values = ['true', 'TRUE', 'True'] + if sys.argv[3] in accepted_values: + view = True + else: + raise Exception("no suitable value for view") + else: + view = False + + if sys.argv[2]: # if the user has specified an output file + graph.render('test-output/'+str(sys.argv[2])+'.gv', view=view) + 'test-output/'+str(sys.argv[2])+'.pdf' + else: # else use the input file's name + graph.render('test-output/'+str(sys.argv[1])+'.gv', view=view) + 'test-output/'+str(sys.argv[1])+'.pdf' + + except Exception as e: + + print "could not draw diagram because: " + str(e) + + +# Reads data from the input file + +with open(str(sys.argv[1])) 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 + +parse_and_draw(g1, script) From c9a956a0ac7527c4bec25ce27c1875ceb60ef360 Mon Sep 17 00:00:00 2001 From: Alkis Plaskovitis Date: Tue, 7 Jun 2016 17:03:43 +0300 Subject: [PATCH 2/2] Provided documentation, an example and made some changes in the interface --- doc/recruitment.pdf | Bin 0 -> 43386 bytes doc/recruitment.txt | 23 ++++++ doc/stategraph-READ-ME.txt | 90 +++++++++++++++++++++ state_maker.py => stategraph.py | 135 ++++++++++++++------------------ 4 files changed, 172 insertions(+), 76 deletions(-) create mode 100644 doc/recruitment.pdf create mode 100644 doc/recruitment.txt create mode 100644 doc/stategraph-READ-ME.txt rename state_maker.py => stategraph.py (65%) diff --git a/doc/recruitment.pdf b/doc/recruitment.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f7757be98071d2212841541a66eb68c4f6638309 GIT binary patch literal 43386 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

$5)T7?^%ciEHs{Auk!P4 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

-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 G^Zx*8GV^2r literal 0 HcmV?d00001 diff --git a/doc/recruitment.txt b/doc/recruitment.txt new file mode 100644 index 00000000..b47611bb --- /dev/null +++ b/doc/recruitment.txt @@ -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) + + diff --git a/doc/stategraph-READ-ME.txt b/doc/stategraph-READ-ME.txt new file mode 100644 index 00000000..fef3c169 --- /dev/null +++ b/doc/stategraph-READ-ME.txt @@ -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: alkisplas@gmail.com diff --git a/state_maker.py b/stategraph.py similarity index 65% rename from state_maker.py rename to stategraph.py index 503900d5..75a0bd28 100644 --- a/state_maker.py +++ b/stategraph.py @@ -1,53 +1,13 @@ -""" - -description: - - This script allows the declarative specification of uml state diagrams. - - 1) use the commands stated below to form 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) provide the arguments as stated below - 4) at the test-output directory you will find the diagram both in dot format and in pdf. - -cmd args: - - argv[1]: input file name. (Mandatory) - argv[2]: output file name. (Optional). Defaults to the input file's name - argv[3]: view. (Optional). If true the generated pdf opens immediately. Defaults to False - -commands: - - state: creates a state - arguments: id (mandatory), name (mandatory), events[](optional), style[](optional - - transition: creates a transition - arguments: source (mandatory), target (mandatory), label (optional), styling[] (optional) - - initial: the initial node. To be referred as initial - - final: the final node. To be referred as final - - compound_state: creates a subgraph which includes states and transitions - arguments: name (optional) - - compound_end: ends the subgraph - - styles: applies styling to the element specified - arguments: element (mandatory), styling[] (mandatory) - - note: applies a note to the state specified - arguments: node_id (mandatory), note (mandatory) - -""" - +#!/usr/bin/env python from graphviz import Digraph from pyparsing import * -import sys - +import argparse +import os # -----Parser----- + LP, RP, LB, RB = map(Suppress, "()[]") # those will be skipped when iterating tokens = "+" + "'" + "=" + "/" + "_" @@ -65,10 +25,11 @@ def add_state(graph, args_list): - """ - Creates the state based on the arguments given - Args of 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). @@ -88,13 +49,14 @@ def add_state(graph, args_list): def add_transition(graph, args_list): - """ - Creates the transition based on the arguments given - Args of args_list: - args_list[0]: Start state identifier(MANDATORY). - args_list[1]: End state identifier(MANDATORY). - args_list[2]: Caption to be displayed near the edge(OPTIONAL). + """ 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). """ @@ -159,8 +121,8 @@ def compound_state(args_list): def list_to_dict(alist): - """ - takes the list that contains the styling arguments to be applied + + """ Takes the list that contains the styling arguments to be applied and turns it to a dictionary which graphviz understands """ @@ -172,9 +134,16 @@ def list_to_dict(alist): return changed_dict -# Parses every line of the script and calls an add method depending on that line's action command - 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: @@ -214,31 +183,45 @@ def parse_and_draw(graph, script): entry.subgraph(graph) graph = entry - # checks if user wants to view the graph right away - if len(sys.argv) == 4: - accepted_values = ['true', 'TRUE', 'True'] - if sys.argv[3] in accepted_values: - view = True - else: - raise Exception("no suitable value for view") - else: - view = False - - if sys.argv[2]: # if the user has specified an output file - graph.render('test-output/'+str(sys.argv[2])+'.gv', view=view) - 'test-output/'+str(sys.argv[2])+'.pdf' - else: # else use the input file's name - graph.render('test-output/'+str(sys.argv[1])+'.gv', view=view) - 'test-output/'+str(sys.argv[1])+'.pdf' + return graph except Exception as e: print "could not draw diagram because: " + str(e) -# Reads data from the input file +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(str(sys.argv[1])) as f: +with open(args.filename) as f: content = f.read().splitlines() script = "\n".join(content) @@ -246,4 +229,4 @@ def parse_and_draw(graph, script): g1 = Digraph('g1', node_attr={'shape': 'box', 'style': 'rounded'}) g1.body.extend(['rankdir=LR']) # graph's direction left-->right -parse_and_draw(g1, script) +render_graph(args, parse_and_draw(g1, script))