Skip to content

Commit

Permalink
add graph instance transformation prototype as recipe
Browse files Browse the repository at this point in the history
  • Loading branch information
xflr6 committed Nov 25, 2021
1 parent 9a2e350 commit 1cd6894
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 0 deletions.
104 changes: 104 additions & 0 deletions examples/graphviz_transform_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3

"""graphviz.(Di)graph instance transformation: LazyGraph and LazyDigraph."""

import functools
from unittest import mock

import graphviz

RENDERING_METHODS = ('pipe', 'save', 'render', 'view', 'unflatten')

INPUT_PROPERTIES = ('source',)

__all__ = ['LazyGraph', 'LazyDigraph']


def create_lazy_graph(cls_name: str,
*init_args, **init_kwargs) -> mock.NonCallableMagicMock:
cls = getattr(graphviz, cls_name)
if cls not in (graphviz.Graph, graphviz.Digraph):
raise ValueError(f'cls_name: {cls_name!r}')

fake = mock.create_autospec(cls, instance=True, spec_set=True)

fake.copy.side_effect = NotImplementedError # TODO

def make_real_inst(calls):
dot = cls(*init_args, **init_kwargs)
for method_name, args, kwargs in calls:
method = getattr(dot, method_name)
method(*args, **kwargs) # ignore return value
return dot

def make_methodcaller(method_name):
def call_method(*args, **kwargs):
last_call = fake.mock_calls.pop()
assert last_call == getattr(mock.call, method_name)(*args, **kwargs)

dot = make_real_inst(fake.mock_calls)
method = getattr(dot, method_name)
return method(*args, **kwargs)

return call_method

for method_name in RENDERING_METHODS:
getattr(fake, method_name).side_effect = make_methodcaller(method_name)

def make_property(property_name):
def property_func(*args):
last_call = property_mock.mock_calls.pop()
assert last_call == mock.call(*args)
assert not property_mock.mock_calls

dot = make_real_inst(fake.mock_calls)
property_obj = getattr(dot.__class__, property_name)
property_func = property_obj.fset if args else property_obj.fget
return property_func(dot, *args)

property_mock = mock.PropertyMock(side_effect=property_func)
return property_mock

for property_name in INPUT_PROPERTIES:
setattr(type(fake), property_name, make_property(property_name))

return fake


LazyGraph, LazyDigraph = (functools.partial(create_lazy_graph, cls_name)
for cls_name in ('Graph', 'Digraph'))


if __name__ == '__main__':
dot = LazyDigraph(filename='round-table.gv', comment='The Round Table')

dot.node('A', 'King Arthur')
dot.node('B', 'Sir Bedevere the Wise')
dot.node('L', 'Sir Lancelot the Brave')

dot.edges(['AB', 'AL'])
dot.edge('B', 'L', constraint='false')

print(repr(dot), dot.mock_calls, dot.source, sep='\n')
#dot.view()

def transform(mock_calls):
"""Replace full name labels with first names."""
for call in mock_calls:
method_name, args, kwargs = call
if method_name == 'node':
name, label = args
label = label.split()[1]
yield mock.call.node(name, label, **kwargs)
else:
yield call

dot.mock_calls = list(transform(dot.mock_calls))

# reverse the Bedvedere -> Lancelot edge
method_name, tail_head, edge_attrs = dot.mock_calls.pop()
assert method_name == 'edge'
dot.edge(*reversed(tail_head), **edge_attrs)

print(repr(dot), dot.mock_calls, dot.source, sep='\n')
#dot.view('round-table-transformed.gv')
3 changes: 3 additions & 0 deletions try-examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@
for path in pathlib.Path().glob('*.py'):
print(path)
code = path.read_text(**IO_KWARGS)

try:
exec(code)
except Exception as e:
raised.append(e)
warnings.warn(e)
else:
if path.name == 'graphviz_transform_recipe.py':
continue
rendered = f'{path.stem}.gv.{DEFAULT_FORMAT}'
assert pathlib.Path(rendered).stat().st_size, f'non-empty {rendered}'
mock_view.assert_called_once_with(rendered, DEFAULT_FORMAT, False)
Expand Down

0 comments on commit 1cd6894

Please sign in to comment.