Source code for visitor_design_pattern.visitor_design_pattern

"""
Visitor Design Pattern main module

This module defines the main component exposed by the visitor-design-pattern package:
 - visitor class decorator
 - traverse method decorator
 - prefix method decorator
 - infix method decorator
 - suffix method decorator
 - Visitable interface
"""

import inspect

ALLOWED_VISIT_MODES = [
    'prefix',
    'infix',
    'suffix',
]


def _visit(self, node, mode, *args, **kwargs):

    for visitable_type in self.VISITABLE_TYPES[mode]:
        if isinstance(node, visitable_type):
            method = type(self).VISITABLE_TYPES[mode][visitable_type]
            return method(
                self,
                node,
                *args,
                **kwargs
            )
    raise ValueError(
        f"No suitable {mode} method found for node type"
        f" {type(node)} in visitor {type(self)}"
    )


def _visit_prefix(self, node, *args, **kwargs):
    return self._visit(node, 'prefix', *args, **kwargs)


def _visit_infix(self, node, *args, **kwargs):
    return self._visit(node, 'infix', *args, **kwargs)


def _visit_suffix(self, node, *args, **kwargs):
    return self._visit(node, 'suffix', *args, **kwargs)


def _do_nothing(self, *_, **__):
    pass


class _Wrapper():

    def __init__(self, method) -> None:
        self.method = method
        self.arg_spec = inspect.getfullargspec(method)

    def __call__(self, *args, **kwargs):
        definitive_args = args
        if self.arg_spec.varargs is None:
            definitive_args = args[:len(self.arg_spec.args)]
        definitive_kwargs = {}
        if not self.arg_spec.defaults is None:
            definitive_kwargs = {
                key: kwargs[key] for key in self.arg_spec.args[-len(self.arg_spec.defaults):]
            }
        return self.method(*definitive_args, **definitive_kwargs)


[docs]def visitor(traversal_mode=None): """ visitor class decorator Mark a class cls as visitor. This initializes the supported VISITABLE_TYPES dict in cls, and gather the different visit method marked with the `traverse`, `prefix`, `infix` or `suffix` decorators Usage: .. code:: python @visitor() def MyVisitor(): ... Parameters: - traversal_mode: None, "prefix", "infix" or "suffix" Specifying a non None traversal_mode prevents other modes to be used in the decorated visitor """ def actual_class_wrapper(cls): cls.VISITABLE_TYPES = { 'prefix': {}, 'infix': {}, 'suffix': {} } for name, method in cls.__dict__.items(): modes = getattr(method, "__traversal_mode", None) if modes is None: continue arg_spec = inspect.getfullargspec(method) if len(arg_spec.args) < 2: raise ValueError( f"visitmethod {name} of class {cls}" " does not provide a node argument to visit" ) visitable_argname = arg_spec.args[1] if visitable_argname not in arg_spec.annotations: raise ValueError( f"Argument {visitable_argname} is not type" f" annotated for visitmethod {name} of class {cls}" ) type_annotation = arg_spec.annotations[visitable_argname] for mode in modes: cls.VISITABLE_TYPES[mode][type_annotation] = _Wrapper(method) setattr(cls, '_visit', _visit) setattr(cls, 'visit_prefix', _visit_prefix) setattr(cls, 'visit_infix', _visit_infix) setattr(cls, 'visit_suffix', _visit_suffix) if traversal_mode == 'prefix': setattr(cls, 'visit_infix', _do_nothing) setattr(cls, 'visit_suffix', _do_nothing) if traversal_mode == 'infix': setattr(cls, 'visit_prefix', _do_nothing) setattr(cls, 'visit_suffix', _do_nothing) if traversal_mode == 'suffix': setattr(cls, 'visit_infix', _do_nothing) setattr(cls, 'visit_prefix', _do_nothing) return cls return actual_class_wrapper
[docs]def traverse(traversal_mode): """ visit method decorator Mark a method as visit method Usage: .. code:: python @visitor() def MyVisitor(): @traverse("prefix") def my_visit_method(self, node: Type): ... ... Parameters: - traversal_mode: "prefix", "infix" or "suffix" or list The specified mode defines when the visit method should be called when traversing the structure. A list of multiple modes can be specified. """ if isinstance(traversal_mode, str): traversal_mode = [traversal_mode] for m in traversal_mode: if m not in ALLOWED_VISIT_MODES: raise ValueError( f"Visit mode {m} does not exist. " "Only 'prefix', 'infix' or 'suffix' are available" ) def actual_decorator(method): if hasattr(method, '__traversal_mode'): method.__traversal_mode += traversal_mode else: method.__traversal_mode = traversal_mode return method return actual_decorator
[docs]def prefix(): """ prefix visit method decorator Equivalent to traverse('prefix') """ return traverse('prefix')
[docs]def infix(): """ infix visit method decorator Equivalent to traverse('infix') """ return traverse("infix")
[docs]def suffix(): """ suffix visit method decorator Equivalent to traverse('suffix') """ return traverse("suffix")
[docs]class VisitableInterface(): """ Base class for visited nodes of the data structure """
[docs] def accept(self, visitor, parent_res=None): """ Default accept method This method implement the base logic of the traversal. A complex data structure may need a custom implementation. In that case users may subclass the VisitableInterface and implement their own traversal logic. Parameters: - visitor: class decorated by the visitor class decorator - parent_res: return value of the prefix call of visitor on the current node's parent """ prefix_res = visitor.visit_prefix(self, parent_res=parent_res) visited_attrs = {} for i, (key, value) in enumerate(self.__dict__.items()): if isinstance(value, list): visited = [] for j, x in enumerate(value): if not isinstance(x, VisitableInterface): continue res = x.accept(visitor, parent_res=prefix_res) if res is not None: visited.append(res) if j < len(value) - 1: visitor.visit_infix(self, parent_res=parent_res) if len(visited) != 0: visited_attrs[key] = visited elif isinstance(value, VisitableInterface): visited = value.accept(visitor, parent_res=prefix_res) if i < len(self.__dict__.keys()) - 1: visitor.visit_infix(self, parent_res=parent_res) if visited is not None: visited_attrs[key] = visited postfix_res = visitor.visit_suffix( self, parent_res=parent_res, prefix_res=prefix_res, visited_attrs=visited_attrs ) return prefix_res, visited_attrs, postfix_res