From 351c4dda5e4b98250a0c7903cb012eda26a2c1e0 Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Thu, 26 Jan 2017 00:15:32 +0100 Subject: [PATCH 1/7] Draft for formatting stuff, needs streamlining --- automaton/__init__.py | 4 +++ automaton/automaton.py | 74 +++++++++++++++++++++++++++++++++++++++- requirements/install.txt | 1 + setup.py | 2 +- tests/test_automaton.py | 7 ++++ 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/automaton/__init__.py b/automaton/__init__.py index e8e22a7..c11bc0c 100644 --- a/automaton/__init__.py +++ b/automaton/__init__.py @@ -27,6 +27,8 @@ AutomatonError, DefinitionError, InvalidTransitionError, + tabulate, + plantuml, ) @@ -46,6 +48,8 @@ "AutomatonError", "DefinitionError", "InvalidTransitionError", + "tabulate", + "plantuml", ) ################################################### diff --git a/automaton/automaton.py b/automaton/automaton.py index 973e88e..a4f33ae 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -21,6 +21,7 @@ from itertools import chain, product, filterfalse from collections import namedtuple, Iterable +import tabulate as tabulator import networkx as nx @@ -226,7 +227,7 @@ def __new__(mcs, class_name, class_bases, class_dict): len(components), ", ".join("{}".format(c.nodes()) for c in components)) ) # 4. Save - cls.__graph__ = graph + cls.__graph__ = nx.freeze(graph) return cls @@ -355,6 +356,11 @@ def _get_cut(cls, *states, inbound=True): If set, the inbound events will be returned, outbound otherwise. Defaults to `True`. + Raises + ------ + KeyError + When an unknown state is found while iterating. + Yields ------ any @@ -401,6 +407,11 @@ def in_events(cls, *states): states : tuple(any) The states subset. + Raises + ------ + KeyError + When an unknown state is found while iterating. + Yields ------ any @@ -413,6 +424,11 @@ def out_events(cls, *states): """ Retrieves all the outbound events leaving the specified states with no duplicates. + Raises + ------ + KeyError + When an unknown state is found while iterating. + Parameters ---------- states : tuple(any) @@ -435,3 +451,59 @@ def get_default_initial_state(cls): The automaton default initial state. """ return cls.__default_initial_state__ + + def __str__(self): + return '<{}@{}>'.format(self.__class__.__name__, self.state) + + +############################################################################### +# Rendering stuff, everything beyond this point is mere IO and formatting +# for humans. + +def plantuml(graph): + """ + @startuml + [*] --> NEW : creation + NEW --> PENDING : frontend_ack + PENDING --> IDLE : allocation + DONE --> [*] + FAILED --> [*] + CANCELLED --> [*] + @enduml + """ + source_nodes = {node for node in graph.nodes() if len(graph.in_edges(node)) == 0} + sink_nodes = {node for node in graph.nodes() if len(graph.out_edges(node)) == 0} + sources = [('[*]', node) for node in source_nodes] + sinks = [(node, '[*]') for node in sink_nodes] + table = to_table(graph=graph) + return """ +@startuml +{} +{} +{} +@enduml +""".format('\n'.join([' {} --> {}'.format(*row) for row in sources]), + '\n'.join([' {} --> {} : {}'.format(*row) for row in table]), + '\n'.join([' {} --> {}'.format(*row) for row in sinks])) + + +def to_table(graph, source=None, traversal=None): + source = source or min(graph.nodes(), key=lambda n: len(graph.in_edges(n))) + if traversal: + traversal = lambda: traversal(graph, source=source) + else: + traversal = lambda: nx.bfs_edges(graph, source=source) + # Retrieve event names since networkx traversal + # functions lack data retrieval + events = nx.get_edge_attributes(graph, 'event') + # Build raw data table to be rendered + return [(source, dest, events[source, dest]) for source, dest in traversal()] + + +def tabulate(graph, header=None, tablefmt=None, source=None, traversal=None): + header = header or ['Source', 'Dest', 'Event'] + table = to_table(graph=graph, source=None, traversal=traversal) + if tablefmt == 'plantuml': + return format_plantuml(table) + else: + return tabulator.tabulate(table, header, tablefmt=tablefmt) diff --git a/requirements/install.txt b/requirements/install.txt index d126c4b..abe7fc2 100644 --- a/requirements/install.txt +++ b/requirements/install.txt @@ -1,2 +1,3 @@ decorator==4.0.11 networkx==1.11 +tabulate==0.7.7 diff --git a/setup.py b/setup.py index d445ba8..a773ff2 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ author_email="federico.ficarelli@gmail.com", url="https://github.com/nazavode/automaton", packages=['automaton'], - install_requires=['networkx'], + install_requires=['networkx', 'tabulate'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', diff --git a/tests/test_automaton.py b/tests/test_automaton.py index d5ee222..ceeec75 100644 --- a/tests/test_automaton.py +++ b/tests/test_automaton.py @@ -33,6 +33,13 @@ class TrafficLight(Automaton): return TrafficLight +def test_str(traffic_light): + auto = traffic_light() + assert str(auto) == '' + auto.go() + assert str(auto) == '' + + def test_definition(): class Simple(Automaton): __default_initial_state__ = 'state_a' From d98ed4d48a6a5de9ef738842fa412b111a3f38ff Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Thu, 26 Jan 2017 00:19:13 +0100 Subject: [PATCH 2/7] Tabulation function cleanup --- automaton/automaton.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/automaton/automaton.py b/automaton/automaton.py index a4f33ae..6d00aeb 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -502,8 +502,5 @@ def to_table(graph, source=None, traversal=None): def tabulate(graph, header=None, tablefmt=None, source=None, traversal=None): header = header or ['Source', 'Dest', 'Event'] - table = to_table(graph=graph, source=None, traversal=traversal) - if tablefmt == 'plantuml': - return format_plantuml(table) - else: - return tabulator.tabulate(table, header, tablefmt=tablefmt) + table = to_table(graph=graph, source=source, traversal=traversal) + return tabulator.tabulate(table, header, tablefmt=tablefmt) From 37815cf3fadb3c8beffb3c3d79d461981406ad35 Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Thu, 26 Jan 2017 23:07:50 +0100 Subject: [PATCH 3/7] Fancy tests --- requirements/develop.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/develop.txt b/requirements/develop.txt index 036bffc..aa51219 100644 --- a/requirements/develop.txt +++ b/requirements/develop.txt @@ -16,3 +16,5 @@ tox==2.5.0 virtualenv==15.1.0 wrapt==1.10.8 bumpversion==0.5.3 +pytest-sugar==0.8.0 +termcolor==1.1.0 From d579874abdb407a78047767fc5ae5deb875036c7 Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Thu, 26 Jan 2017 23:29:35 +0100 Subject: [PATCH 4/7] Added tests for transition table formatting Still missing a lot of coverage. --- automaton/automaton.py | 7 +++---- tests/test_automaton.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/automaton/automaton.py b/automaton/automaton.py index 6d00aeb..5ca2b98 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -456,9 +456,8 @@ def __str__(self): return '<{}@{}>'.format(self.__class__.__name__, self.state) -############################################################################### -# Rendering stuff, everything beyond this point is mere IO and formatting -# for humans. +######################################################################### +# Rendering stuff, everything beyond this point is formatting for humans. def plantuml(graph): """ @@ -497,7 +496,7 @@ def to_table(graph, source=None, traversal=None): # functions lack data retrieval events = nx.get_edge_attributes(graph, 'event') # Build raw data table to be rendered - return [(source, dest, events[source, dest]) for source, dest in traversal()] + return [(source, dest, events[source, dest, 0]) for source, dest in traversal()] def tabulate(graph, header=None, tablefmt=None, source=None, traversal=None): diff --git a/tests/test_automaton.py b/tests/test_automaton.py index ceeec75..702da3d 100644 --- a/tests/test_automaton.py +++ b/tests/test_automaton.py @@ -403,3 +403,39 @@ class Star(Automaton): with pytest.raises(KeyError): set(Star.out_events('unknown1', 'unknown2', 'center')) # Unknown state + + +def test_event_edges(): + event = Event('a', 'b') + event.bind('testevent') + assert list(event.edges()) == [('a', 'b')] + assert list(event.edges(data=True)) == [('a', 'b', {'event': 'testevent'})] + # + event = Event(('a', 'b', 'c', 'd'), 'x') + event.bind('testevent') + assert list(event.edges()) == [('a', 'x'), ('b', 'x'), ('c', 'x'), ('d', 'x')] + assert list(event.edges(data=True)) == [('a', 'x', {'event': 'testevent'}), + ('b', 'x', {'event': 'testevent'}), ('c', 'x', {'event': 'testevent'}), + ('d', 'x', {'event': 'testevent'})] + # + class Sink(Automaton): + event1 = Event('state_a', 'state_b') + event2 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink1') + event3 = Event(('state_a', 'state_b', 'state_c', 'state_d', 'sink1'), 'sink2') + event4 = Event('sink2', 'state_a') + assert list(Sink.event2.edges()) == \ + [('state_a', 'sink1'), ('state_b', 'sink1'), ('state_c', 'sink1'), ('state_d', 'sink1')] + + + +@pytest.mark.parametrize('header', [[1, 2, 3], ['a', 'b', 'c']]) +@pytest.mark.parametrize('tablefmt', ['rst', 'pipe']) +def test_format(header, tablefmt): + + class Sink(Automaton): + event1 = Event('state_a', 'state_b') + event2 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink1') + event3 = Event(('state_a', 'state_b', 'state_c', 'state_d', 'sink1'), 'sink2') + event4 = Event('sink2', 'state_a') + + assert tabulate(Sink.__graph__, header=header, tablefmt=tablefmt) From 85fa976f9bcc115ade694e286026769e1f51c34b Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Fri, 27 Jan 2017 23:23:24 +0100 Subject: [PATCH 5/7] Generator streamlining and added missing docstrings Squashed commit of the following: commit e95b94c35a396c86800b47360c9220747ffb6012 Author: Federico Ficarelli Date: Fri Jan 27 23:20:57 2017 +0100 WIP commit bf39b4995c80e426fec600984ecaeba9fa0caeff Author: Federico Ficarelli Date: Fri Jan 27 22:58:23 2017 +0100 WIP commit 162ee37db66c33e40448697ffed5e94c26605c97 Author: Federico Ficarelli Date: Fri Jan 27 20:30:35 2017 +0100 WIP commit caf59df89ed82a22ce1d4464f0cdb4547677d742 Author: Federico Ficarelli Date: Fri Jan 27 20:28:37 2017 +0100 WIP commit 73c5d4a5f7e62150897bbec7ec18a9ac48388d77 Author: Federico Ficarelli Date: Fri Jan 27 20:21:37 2017 +0100 WIP --- automaton/automaton.py | 140 ++++++++++++++++++++++++++++++++-------- flake8.ini | 4 +- tests/test_automaton.py | 5 +- 3 files changed, 119 insertions(+), 30 deletions(-) diff --git a/automaton/automaton.py b/automaton/automaton.py index 2bf40bd..a31d3ea 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -459,47 +459,135 @@ def __str__(self): ######################################################################### # Rendering stuff, everything beyond this point is formatting for humans. -def plantuml(graph): - """ - @startuml - [*] --> NEW : creation - NEW --> PENDING : frontend_ack - PENDING --> IDLE : allocation - DONE --> [*] - FAILED --> [*] - CANCELLED --> [*] - @enduml +def plantuml(automaton): + """ Render an automaton's state-transition graph as a + `PlantUML state diagram `_. + + A simple example will be formatted as follows: + + >>> class TrafficLight(Automaton): + ... go = Event('red', 'green') + ... slowdown = Event('green', 'yellow') + ... stop = Event('yellow', 'red') + >>> print( plantuml(TrafficLight) ) # doctest: +SKIP + + @startuml + green --> yellow : slowdown + yellow --> red : stop + red --> green : go + @enduml + + Parameters + ---------- + automaton : `~automaton.Automaton` + The automaton to be rendered. It can be both + a class and an instance. + + Returns + ------- + str + Returns the formatted state graph. """ - source_nodes = {node for node in graph.nodes() if len(graph.in_edges(node)) == 0} - sink_nodes = {node for node in graph.nodes() if len(graph.out_edges(node)) == 0} + graph = automaton.__graph__ + source_nodes = filter(lambda n: not graph.in_edges(n), graph.nodes()) + sink_nodes = filter(lambda n: not graph.out_edges(n), graph.nodes()) sources = [('[*]', node) for node in source_nodes] sinks = [(node, '[*]') for node in sink_nodes] table = to_table(graph=graph) - return """ -@startuml + return """@startuml {} {} {} -@enduml -""".format('\n'.join([' {} --> {}'.format(*row) for row in sources]), - '\n'.join([' {} --> {} : {}'.format(*row) for row in table]), - '\n'.join([' {} --> {}'.format(*row) for row in sinks])) +@enduml""".format('\n'.join([' {} --> {}'.format(*row) for row in sources]), + '\n'.join([' {} --> {} : {}'.format(*row) for row in table]), + '\n'.join([' {} --> {}'.format(*row) for row in sinks])) + +def to_table(graph, traversal=None): + """ Build the adjacency table of the given graph. -def to_table(graph, source=None, traversal=None): - source = source or min(graph.nodes(), key=lambda n: len(graph.in_edges(n))) + Parameters + ---------- + graph : `~networkx.MultiDiGraph` + The directed graph. + traversal : callable(graph), optional + An optional callable used to yield the + edges of the graph. It must accept the graph + itself as the first positional argument + and yield one edge at a time as a tuple in the + form ``(source_node, destination_node)``. The default + traversal sorts the nodes in ascending order by + inbound grade. + + Yields + ------ + (source, dest, event) + Yields one row at a time as a tuple containing + the source and destination node of the edge and + the name of the event associated with the edge. + """ if traversal: - traversal = lambda: traversal(graph, source=source) + traversal = lambda: traversal(graph) else: - traversal = lambda: nx.bfs_edges(graph, source=source) + traversal = lambda: sorted(graph.edges(), key=lambda e: len(graph.in_edges(e[0]))) # Retrieve event names since networkx traversal # functions lack data retrieval - events = nx.get_edge_attributes(graph, 'event') + events = nx.get_edge_attributes(graph, 'event') # -> (source, dest, key==0): event # Build raw data table to be rendered - return [(source, dest, events[source, dest, 0]) for source, dest in traversal()] + for source, dest in traversal(): + yield (source, dest, events[source, dest, 0]) + + +def tabulate(automaton, header=None, tablefmt=None, traversal=None): + """ Render an automaton's transition table as in + text format. + + The transition table has three columns: source node, + destination node and the name of the event. + A simple example will be formatted as follows: -def tabulate(graph, header=None, tablefmt=None, source=None, traversal=None): + >>> class TrafficLight(Automaton): + ... go = Event('red', 'green') + ... slowdown = Event('green', 'yellow') + ... stop = Event('yellow', 'red') + >>> tabulate(TrafficLight) # doctest: +SKIP + + ======== ====== ======== + Source Dest Event + ======== ====== ======== + green yellow slowdown + yellow red stop + red green go + ======== ====== ======== + + Parameters + ---------- + automaton : `~automaton.Automaton` + The automaton to be rendered. It can be both + a class and an instance. + header : list[str, str, str] + An optional list of fields to be used as table + headers. Defaults to a predefined header. + tablefmt str, optional + Specifies the output format for the table. + All formats supported by + `tabulate `_ + package are supported (e.g.: ``rst`` for + reStructuredText, ``pipe`` for Markdown). + Defaults to ``rst``. + traversal : callable(graph), optional + An optional callable used to sort the events. + It has the same meaning as the ``traversal`` + parameter of `automaton.to_table`. + + Returns + ------- + str + Returns the formatted transition table. + """ + graph = automaton.__graph__ header = header or ['Source', 'Dest', 'Event'] - table = to_table(graph=graph, source=source, traversal=traversal) + tablefmt = tablefmt or 'rst' + table = to_table(graph=graph, traversal=traversal) return tabulator.tabulate(table, header, tablefmt=tablefmt) diff --git a/flake8.ini b/flake8.ini index 58d4265..1846078 100644 --- a/flake8.ini +++ b/flake8.ini @@ -1,5 +1,5 @@ [flake8] -ignore = W391,F841 +ignore = W391,F841,E731 max-line-length = 120 # exclude = tests/* -# max-complexity = 10 \ No newline at end of file +# max-complexity = 10 diff --git a/tests/test_automaton.py b/tests/test_automaton.py index 702da3d..22be889 100644 --- a/tests/test_automaton.py +++ b/tests/test_automaton.py @@ -435,7 +435,8 @@ def test_format(header, tablefmt): class Sink(Automaton): event1 = Event('state_a', 'state_b') event2 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink1') - event3 = Event(('state_a', 'state_b', 'state_c', 'state_d', 'sink1'), 'sink2') + event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') event4 = Event('sink2', 'state_a') - assert tabulate(Sink.__graph__, header=header, tablefmt=tablefmt) + assert tabulate(Sink, header=header, tablefmt=tablefmt) + print(plantuml(Sink)) From 6798fab8eda65bae0cdc16f93793d3756b608b34 Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Fri, 27 Jan 2017 23:56:03 +0100 Subject: [PATCH 6/7] Fixed plantuml function parameters, tests and misc docstrings --- automaton/__init__.py | 17 ++--------------- automaton/automaton.py | 35 ++++++++++++++++++++++------------- tests/test_automaton.py | 34 +++++++++++++++++++++++++++++----- 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/automaton/__init__.py b/automaton/__init__.py index a23d26c..74589cf 100644 --- a/automaton/__init__.py +++ b/automaton/__init__.py @@ -21,7 +21,7 @@ import re import collections -from .automaton import ( +from .automaton import ( # noqa: F401 Event, Automaton, AutomatonError, @@ -29,6 +29,7 @@ InvalidTransitionError, tabulate, plantuml, + transition_table, ) @@ -37,20 +38,6 @@ __license__ = 'Apache License Version 2.0' __version__ = '1.0.0' -__all__ = ( - # Version - "VERSION_INFO", - "VERSION", - # Automaton API - "Event", - "Automaton", - # Exceptions - "AutomatonError", - "DefinitionError", - "InvalidTransitionError", - "tabulate", - "plantuml", -) ################################################### VERSION = __version__ diff --git a/automaton/automaton.py b/automaton/automaton.py index a31d3ea..ac3ec4b 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -31,6 +31,9 @@ "AutomatonError", "DefinitionError", "InvalidTransitionError", + "transition_table", + "plantuml", + "tabulate", ) @@ -459,7 +462,7 @@ def __str__(self): ######################################################################### # Rendering stuff, everything beyond this point is formatting for humans. -def plantuml(automaton): +def plantuml(automaton, traversal=None): """ Render an automaton's state-transition graph as a `PlantUML state diagram `_. @@ -482,6 +485,10 @@ def plantuml(automaton): automaton : `~automaton.Automaton` The automaton to be rendered. It can be both a class and an instance. + traversal : callable(graph), optional + An optional callable used to sort the events. + It has the same meaning as the ``traversal`` + parameter of `automaton.transition_table`. Returns ------- @@ -493,7 +500,7 @@ def plantuml(automaton): sink_nodes = filter(lambda n: not graph.out_edges(n), graph.nodes()) sources = [('[*]', node) for node in source_nodes] sinks = [(node, '[*]') for node in sink_nodes] - table = to_table(graph=graph) + table = transition_table(automaton, traversal=traversal) return """@startuml {} {} @@ -503,21 +510,23 @@ def plantuml(automaton): '\n'.join([' {} --> {}'.format(*row) for row in sinks])) -def to_table(graph, traversal=None): +def transition_table(automaton, traversal=None): """ Build the adjacency table of the given graph. Parameters ---------- - graph : `~networkx.MultiDiGraph` - The directed graph. + automaton : `~automaton.Automaton` + The automaton to be rendered. It can be both + a class and an instance. traversal : callable(graph), optional An optional callable used to yield the - edges of the graph. It must accept the graph - itself as the first positional argument + edges of the graph representation of the + automaton. It must accept a `networkx.MultiDiGraph` + as the first positional argument and yield one edge at a time as a tuple in the - form ``(source_node, destination_node)``. The default - traversal sorts the nodes in ascending order by - inbound grade. + form ``(source_state, destination_state)``. The default + traversal sorts the states in ascending order by + inbound grade (number of incoming events). Yields ------ @@ -526,6 +535,7 @@ def to_table(graph, traversal=None): the source and destination node of the edge and the name of the event associated with the edge. """ + graph = automaton.__graph__ if traversal: traversal = lambda: traversal(graph) else: @@ -579,15 +589,14 @@ def tabulate(automaton, header=None, tablefmt=None, traversal=None): traversal : callable(graph), optional An optional callable used to sort the events. It has the same meaning as the ``traversal`` - parameter of `automaton.to_table`. + parameter of `automaton.transition_table`. Returns ------- str Returns the formatted transition table. """ - graph = automaton.__graph__ header = header or ['Source', 'Dest', 'Event'] tablefmt = tablefmt or 'rst' - table = to_table(graph=graph, traversal=traversal) + table = transition_table(automaton=automaton, traversal=traversal) return tabulator.tabulate(table, header, tablefmt=tablefmt) diff --git a/tests/test_automaton.py b/tests/test_automaton.py index 22be889..cc933ab 100644 --- a/tests/test_automaton.py +++ b/tests/test_automaton.py @@ -427,10 +427,9 @@ class Sink(Automaton): [('state_a', 'sink1'), ('state_b', 'sink1'), ('state_c', 'sink1'), ('state_d', 'sink1')] - -@pytest.mark.parametrize('header', [[1, 2, 3], ['a', 'b', 'c']]) -@pytest.mark.parametrize('tablefmt', ['rst', 'pipe']) -def test_format(header, tablefmt): +@pytest.mark.parametrize('header', [None, [], [1, 2, 3], ['a', 'b', 'c']]) +@pytest.mark.parametrize('tablefmt', [None, '', 'rst', 'pipe']) +def test_tabulate(header, tablefmt): class Sink(Automaton): event1 = Event('state_a', 'state_b') @@ -439,4 +438,29 @@ class Sink(Automaton): event4 = Event('sink2', 'state_a') assert tabulate(Sink, header=header, tablefmt=tablefmt) - print(plantuml(Sink)) + + +def test_plantuml(): + + class Sink(Automaton): + event1 = Event('state_a', 'state_b') + event2 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink1') + event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') + event4 = Event('sink2', 'state_a') + + assert plantuml(Sink) + + +def test_transition_table(): + + class Sink(Automaton): + event1 = Event('state_a', 'state_b') + event2 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink1') + event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') + event4 = Event('sink2', 'state_a') + + table = list(transition_table(Sink, traversal=None)) + assert table + assert len(table) == 10 + for row in table: + assert len(row) == 3 From 6efcbddde8cf543e42cfa3a4f684c7429a64f2e8 Mon Sep 17 00:00:00 2001 From: Federico Ficarelli Date: Sat, 28 Jan 2017 00:28:11 +0100 Subject: [PATCH 7/7] Added traversal tests for formatting fuctions --- automaton/automaton.py | 8 +++----- tests/test_automaton.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/automaton/automaton.py b/automaton/automaton.py index ac3ec4b..836a3fe 100644 --- a/automaton/automaton.py +++ b/automaton/automaton.py @@ -536,15 +536,13 @@ def transition_table(automaton, traversal=None): the name of the event associated with the edge. """ graph = automaton.__graph__ - if traversal: - traversal = lambda: traversal(graph) - else: - traversal = lambda: sorted(graph.edges(), key=lambda e: len(graph.in_edges(e[0]))) + if not traversal: + traversal = lambda G: sorted(G.edges(), key=lambda e: len(G.in_edges(e[0]))) # Retrieve event names since networkx traversal # functions lack data retrieval events = nx.get_edge_attributes(graph, 'event') # -> (source, dest, key==0): event # Build raw data table to be rendered - for source, dest in traversal(): + for source, dest in traversal(graph): yield (source, dest, events[source, dest, 0]) diff --git a/tests/test_automaton.py b/tests/test_automaton.py index cc933ab..07010bd 100644 --- a/tests/test_automaton.py +++ b/tests/test_automaton.py @@ -427,9 +427,14 @@ class Sink(Automaton): [('state_a', 'sink1'), ('state_b', 'sink1'), ('state_c', 'sink1'), ('state_d', 'sink1')] +@pytest.fixture(params=[None, lambda G: G.edges(data=False)]) +def traversal(request): + return request.param + + @pytest.mark.parametrize('header', [None, [], [1, 2, 3], ['a', 'b', 'c']]) @pytest.mark.parametrize('tablefmt', [None, '', 'rst', 'pipe']) -def test_tabulate(header, tablefmt): +def test_tabulate(header, tablefmt, traversal): class Sink(Automaton): event1 = Event('state_a', 'state_b') @@ -437,10 +442,10 @@ class Sink(Automaton): event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') event4 = Event('sink2', 'state_a') - assert tabulate(Sink, header=header, tablefmt=tablefmt) + assert tabulate(Sink, header=header, tablefmt=tablefmt, traversal=traversal) -def test_plantuml(): +def test_plantuml(traversal): class Sink(Automaton): event1 = Event('state_a', 'state_b') @@ -448,10 +453,10 @@ class Sink(Automaton): event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') event4 = Event('sink2', 'state_a') - assert plantuml(Sink) + assert plantuml(Sink, traversal=traversal) -def test_transition_table(): +def test_transition_table(traversal): class Sink(Automaton): event1 = Event('state_a', 'state_b') @@ -459,7 +464,7 @@ class Sink(Automaton): event3 = Event(('state_a', 'state_b', 'state_c', 'state_d'), 'sink2') event4 = Event('sink2', 'state_a') - table = list(transition_table(Sink, traversal=None)) + table = list(transition_table(Sink, traversal=traversal)) assert table assert len(table) == 10 for row in table: