Source code for curry.utility.visitation

'''Implementation of the visitor pattern.'''
# See https://chris-lamb.co.uk/posts/visitor-pattern-in-python
import functools, inspect

# From http://code.activestate.com/recipes/577413-topological-sort/
[docs] def toposort(data): for k, v in data.items(): v.discard(k) # Ignore self dependencies extra_items_in_deps = functools.reduce(set.union, data.values(), set()) - set(data.keys()) data.update({item:set() for item in extra_items_in_deps}) while True: ordered = set(item for item,dep in data.items() if not dep) if not ordered: break yield ordered data = { item: (dep - ordered) for item,dep in data.items() if item not in ordered } assert not data, "cyclic dependency in %r" % data
[docs] class Visitable(object): '''Implements the "accept" half of a visitor pattern.''' def __new__(cls, selector, default): self = object.__new__(cls) self.handlers = {} self._ordering = None self.default = default self.selector = selector # Locate the selector parameter once. Calling inspect.getcallargs on # every visit is slow, and inspect converts a RecursionError raised during # deep evaluation into TypeError, which would hide it. parameters = inspect.signature(default).parameters param = parameters[selector] positional = ( inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD ) self._index = ( list(parameters).index(selector) if param.kind in positional else None ) self._default = param.default return self def _selected(self, args, kwds): '''Find the selector argument. Raises LookupError if it was not supplied.''' if self.selector in kwds: return kwds[self.selector] if self._index is not None and len(args) > self._index: return args[self._index] if self._default is not inspect.Parameter.empty: return self._default raise LookupError(self.selector) @property def ordering(self): ''' The ordering sorts classes by their (possibly abstract) subclass relationships. This is used when no handler provides an exact match. Computing the ordering is O(n^2) w.r.t. the number of handlers. ''' if self._ordering is None: classes = self.handlers.keys() data = { cls: set(filter(lambda subcls: issubclass(subcls, cls), classes)) for cls in classes } self._ordering = [item for group in toposort(data) for item in group] return self._ordering def __call__(self, *args, **kwds): ''' Visitor application. Finds the selector argument, gets its type, identifies the appropriate handler, and then dispatches the call to there. ''' try: arg = self._selected(args, kwds) except LookupError: # The selector was not supplied. Let the default handler report it. handler = self.default else: ty = type(arg) if ty in self.handlers: handler = self.handlers[ty] else: for cls in self.ordering: if issubclass(ty, cls): handler = self.handlers[cls] break else: handler = self.default return handler(*args, **kwds)
[docs] def when(self, ty, replacement, no): if no is not None or isinstance(ty, tuple): ty = instance_checker(ty, no) def decorator(handler): self.handlers[ty] = handler self._ordering = None return replacement return decorator
[docs] class dispatch(object): ''' Creates a new visitable function. ``selector`` names the function parameter used for dispatch. '''
[docs] @staticmethod def on(selector): def decorator(f): sig = inspect.signature(f) bad = selector not in sig.parameters if bad: raise TypeError( "'%s' has no parameter '%s'" % (f.__name__, selector) ) visitor = Visitable(selector, default=f) @functools.wraps(f) def replacement(*args, **kwds): return visitor(*args, **kwds) replacement.when = lambda ty,no=None: visitor.when(ty, replacement, no) return replacement return decorator
[docs] def instance_checker(yes=None, no=None): ''' Builds a this-but-not-that instance checker class. The class returned is intended to be used with ``isinstance`` and ``issubclass``. An object will be an instance of the returned class if it is an instance of ``yes`` but not an instance of ``no``. Likewise for subclasses. Either argument may be a tuple of classes. Identical calls to this function are guaranteed to return the same object, so the result may be used as a key. ''' yes, no = (() if arg is None else arg for arg in [yes, no]) memoized = instance_checker.__dict__.setdefault('_memoized', {}) key = tuple(tuple(set(x if type(x) == tuple else [x])) for x in [yes,no]) if key not in memoized: class _Meta(type): def __instancecheck__(self, obj): return isinstance(obj, yes) and not isinstance(obj, no) def __subclasscheck__(self, cls): return issubclass(cls, yes) and not issubclass(cls, no) class InstanceChecker(metaclass=_Meta): pass memoized[key] = InstanceChecker return memoized[key]