BEP1: Serialization Framework - Vipyr/BazaarCI GitHub Wiki

21 May 2019 - Selected Proposal 2 Option 3

Abstract

There is a need for an intuitive, hierarchical serialization framework for steps and graphs. It must not require injection of specific target format code into any class in the bazaarci.runner package, but should still allow the MRO to be used to determine what function to call to serialize a Node.

Proposal 1 (Tom Manner)

This just felt like too much code, especially when it's basically trying to mimic Python's class attribute lookups.

Look up target format by name, with both class and instance level dictionary variables mapping the name to a callable.

def step_to_dot(step):
    # Do stuff

class Step(Node):
    serializers = { "dot" : step_to_dot }

    def __init__(self, ...):
        super().__init__(...)
        self.serializers = {}

    def serialize(self, format):
        if format in self.serializers:
            return self.serializers[format](self)
        elif format in self.__class__.serializers:
            return self.__class__.serializers[format](self)
        return super().serialize(format)

Proposal 2 (Tom Manner)

Inspect the class hierarchy and graft a user-implemented serializer onto this it, adding a new function to each of the classes that it needs to. It could be something as simple as a dict of type to function which is iterated over to add a particular named function to each of the associated classes.

Option 1 - Classes by Full Name

import sys

def apply_serializer(function_name, class_func_dict):
    for key, function in class_func_dict.items():
        module_name, class_name = key.rsplit('.', 1)
        setattr(getattr(sys.modules[module_name], class_name), function_name, function)

dot_serializers = {
    "bazaarci.runner.Node": lambda node: node.name,
    "bazaarci.runner.Graph": lambda graph: '\n'.join([node.to_dot() for node in graph]),
}
apply_serializer("to_dot", dot_serializers)

class MyStep(bazaarci.runner.Step):
    ...
    def to_dot(self):
        return '{name} [label="{name}: {something}"]'.format(name=self.name, something=self.some_attribute)

Option 2 - Classes directly

Just use the types so there's no janky-ish lookup and to guarantee that the module and type are both available.

from bazaarci.runner import Graph, Node

def apply_serializer(function_name, class_func_dict):
    for class_to_modify, function in class_func_dict.items():
        setattr(class_to_modify, function_name, function)

dot_serializers = {
    Node: lambda node: node.name,
    Graph: lambda graph: '\n'.join([node.to_dot() for node in graph]),
}
apply_serializer("to_dot", dot_serializers)

Option 3 - Skip the Middleman Dictionary

This option is the simplest solution that still allows us to intercept the incoming functions if we need to do any sanitization.

from bazaarci.runner import Graph, Node

def apply_serializer(class_or_instance, function_name, function):
    setattr(class_or_instance, function_name, function)

apply_serializer(Node, "to_dot", lambda node: node.name)
apply_serializer(Graph, "to_dot", lambda graph: '\n'.join([node.to_dot() for node in graph]))

Option 4 - Apply Directly (serialization is a cookbook item)

from bazaarci.runner import Graph, Node

Node.to_dot = lambda node: node.name
Graph.to_dot = lambda graph: '\n'.join([node.to_dot() for node in graph])