diff --git a/code2flow/engine.py b/code2flow/engine.py index ad13a43..0d43b06 100644 --- a/code2flow/engine.py +++ b/code2flow/engine.py @@ -11,7 +11,7 @@ from .javascript import Javascript from .ruby import Ruby from .php import PHP -from .model import (TRUNK_COLOR, LEAF_COLOR, NODE_COLOR, GROUP_TYPE, OWNER_CONST, +from .model import (COLOR_SCHEMES, GROUP_TYPE, OWNER_CONST, Edge, Group, Node, Variable, is_installed, flatten) VERSION = '2.5.1' @@ -24,19 +24,22 @@ "See the README at https://github.com/scottrogowski/code2flow." -LEGEND = """subgraph legend{ - rank = min; - label = "legend"; - Legend [shape=none, margin=0, label = < -
Code2flow Legend
- - - - - -
Regular function
Trunk function (nothing calls this)
Leaf function (this calls nothing else)
Function call
- >]; -}""" % (NODE_COLOR, TRUNK_COLOR, LEAF_COLOR) +def _generate_legend(color_scheme='default'): + colors = COLOR_SCHEMES[color_scheme] + legend = """subgraph legend{ + rank = min; + label = "legend"; + Legend [shape=none, margin=0, label = < +
Code2flow Legend
+ + + + + +
Regular function
Trunk function (nothing calls this)
Leaf function (this calls nothing else)
Function call
+ >]; + }""" % colors + return legend LANGUAGES = { @@ -229,7 +232,7 @@ def generate_json(nodes, edges): def write_file(outfile, nodes, edges, groups, hide_legend=False, - no_grouping=False, as_json=False): + no_grouping=False, as_json=False, color_scheme='default'): ''' Write a dot file that can be read by graphviz @@ -253,9 +256,9 @@ def write_file(outfile, nodes, edges, groups, hide_legend=False, content += f'splines="{splines}";\n' content += 'rankdir="LR";\n' if not hide_legend: - content += LEGEND + content += _generate_legend(color_scheme=color_scheme) for node in nodes: - content += node.to_dot() + ';\n' + content += node.to_dot(color_scheme=color_scheme) + ';\n' for edge in edges: content += edge.to_dot() + ';\n' if not no_grouping: @@ -672,7 +675,7 @@ def code2flow(raw_source_paths, output_file, language=None, hide_legend=True, exclude_namespaces=None, exclude_functions=None, include_only_namespaces=None, include_only_functions=None, no_grouping=False, no_trimming=False, skip_parse_errors=False, - lang_params=None, subset_params=None, level=logging.INFO): + lang_params=None, subset_params=None, color_scheme='default', level=logging.INFO): """ Top-level function. Generate a diagram based on source code. Can generate either a dotfile or an image. @@ -691,6 +694,7 @@ def code2flow(raw_source_paths, output_file, language=None, hide_legend=True, :param lang_params LanguageParams: Object to store lang-specific params :param subset_params SubsetParams: Object to store subset-specific params :param int level: logging level + :param str color_scheme: legend color scheme :rtype: None """ start_time = time.time() @@ -751,11 +755,11 @@ def code2flow(raw_source_paths, output_file, language=None, hide_legend=True, as_json = output_ext == 'json' write_file(fh, nodes=all_nodes, edges=edges, groups=file_groups, hide_legend=hide_legend, - no_grouping=no_grouping, as_json=as_json) + no_grouping=no_grouping, as_json=as_json, color_scheme=color_scheme) else: write_file(output_file, nodes=all_nodes, edges=edges, groups=file_groups, hide_legend=hide_legend, - no_grouping=no_grouping) + no_grouping=no_grouping, color_scheme=color_scheme) logging.info("Wrote output file %r with %d nodes and %d edges.", output_file, len(all_nodes), len(edges)) @@ -837,6 +841,8 @@ def main(sys_argv=None): help='add more logging') parser.add_argument( '--version', action='version', version='%(prog)s ' + VERSION) + parser.add_argument('--color-scheme', action='store', choices=list(COLOR_SCHEMES.keys()), default='default', + help="select a color for the graph legend") sys_argv = sys_argv or sys.argv[1:] args = parser.parse_args(sys_argv) @@ -872,4 +878,5 @@ def main(sys_argv=None): lang_params=lang_params, subset_params=subset_params, level=level, + color_scheme=args.color_scheme, ) diff --git a/code2flow/model.py b/code2flow/model.py index adb4a36..7623c00 100644 --- a/code2flow/model.py +++ b/code2flow/model.py @@ -2,11 +2,18 @@ import os -TRUNK_COLOR = '#966F33' -LEAF_COLOR = '#6db33f' EDGE_COLORS = ["#000000", "#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"] -NODE_COLOR = "#cccccc" + +# (NODE_COLOR, TRUNK_COLOR, LEAF_COLOR) +COLOR_SCHEMES = { + 'default': ('#cccccc', '#966F33', '#6db33f'), + 'blue': ('#138bfa', '#989a97', '#044a9a'), + 'desert': ('#fda653', '#ffecd5', '#d57720'), + 'pink': ('#b99cd6', '#ddd5d7', '#cb3870'), + 'light_blue': ('#ace2ff', '#feffff', '#5bcce0'), + 'aqua': ('#1a9e85', '#304960', '#1fdec3'), +} class Namespace(dict): @@ -401,7 +408,7 @@ def resolve_variables(self, file_groups): else: assert isinstance(variable.points_to, (Node, Group)) - def to_dot(self): + def to_dot(self, color_scheme='default'): """ Output for graphviz (.dot) files :rtype: str @@ -411,12 +418,12 @@ def to_dot(self): 'name': self.name(), 'shape': "rect", 'style': 'rounded,filled', - 'fillcolor': NODE_COLOR, + 'fillcolor': COLOR_SCHEMES[color_scheme][0], } if self.is_trunk: - attributes['fillcolor'] = TRUNK_COLOR + attributes['fillcolor'] = COLOR_SCHEMES[color_scheme][1] elif self.is_leaf: - attributes['fillcolor'] = LEAF_COLOR + attributes['fillcolor'] = COLOR_SCHEMES[color_scheme][2] ret = self.uid + ' [' for k, v in attributes.items(): diff --git a/tests/test_interface.py b/tests/test_interface.py index ad9541f..e13c9e3 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -13,6 +13,8 @@ from code2flow import model IMG_PATH = '/tmp/code2flow/output.png' +GRAPHVIZ_PATH = '/tmp/code2flow/out.gv' + if os.path.exists("/tmp/code2flow"): try: shutil.rmtree('/tmp/code2flow') @@ -95,7 +97,8 @@ def test_json(): assert jobj['graph']['directed'] is True assert isinstance(jobj['graph']['nodes'], dict) assert len(jobj['graph']['nodes']) == 4 - assert set(n['name'] for n in jobj['graph']['nodes'].values()) == {'simple_b::a', 'simple_b::(global)', 'simple_b::c.d', 'simple_b::b'} + assert set(n['name'] for n in jobj['graph']['nodes'].values()) == {'simple_b::a', 'simple_b::(global)', + 'simple_b::c.d', 'simple_b::b'} assert isinstance(jobj['graph']['edges'], list) assert len(jobj['graph']['edges']) == 4 @@ -202,6 +205,7 @@ def test_cli_log_quiet(mocker): logging.basicConfig.assert_called_once_with(format="Code2Flow: %(message)s", level=logging.WARNING) + def test_subset_cli(mocker): with pytest.raises(AssertionError): SubsetParams.generate(target_function='', upstream_depth=1, downstream_depth=0) @@ -221,3 +225,24 @@ def test_subset_cli(mocker): main(['test_code/py/subset_find_exception/two.py', '--target-function', 'func', '--upstream-depth', '1']) +def test_color_scheme_invalid_input(capsys): + with pytest.raises(SystemExit): + main(['test_code/py/simple_a', '--color-scheme', 'invalid']) + assert 'error: argument --color-scheme: invalid choice:' in capsys.readouterr().err + + +def test_color_scheme_none_given(capsys): + code2flow(['test_code/py/simple_a'], output_file=IMG_PATH) + assert os.path.exists(IMG_PATH) + + +def test_color_scheme_test_not_default(capsys): + code2flow(['test_code/py/two_file_simple', '--color-scheme', 'blue'], output_file=GRAPHVIZ_PATH, color_scheme='blue') + assert os.path.exists(GRAPHVIZ_PATH) + contents = '' + with open(GRAPHVIZ_PATH, 'r') as file: + contents = "".join(file.readlines()) + blue = ('#138bfa', '#989a97', '#044a9a') + assert blue[0] in contents + assert blue[1] in contents + assert blue[2] in contents