import collections
import functools
import string
ALLOWED_CHARS = set(string.letters + string.digits + '_')
SENTINEL = object()
PARENT = {}
def visitor_name(cls):
node_type = getattr(cls, 'node_type', None)
if node_type is None:
raise ValueError(f'No node_type defined for {cls}')
return f'visit_{node_type}'
class NodeMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs):
# Not sure this doesn't blow up.
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
node_type = getattr(cls, 'node_type', SENTINEL)
if node_type is not SENTINEL:
if node_type is None:
return cls
if node_type in cls._types:
raise ValueError(f'Duplicate node type: {node_type!r}')
if not ALLOWED_CHARS.issuperset(node_type):
raise ValueError(f'Impolite node type: {node_type!r}')
cls._types.add(node_type)
_visitor_name = visitor_name(cls)
def dispatch(self, visitor):
return getattr(visitor, _visitor_name)(self)
setattr(cls.Visitor, _visitor_name, cls.Visitor.default_visit)
else:
cls.node_type = None
cls._types = set()
def dispatch(self, methods):
raise NotImplementedError(
'Subclasses should auto-generate this.')
class Visitor:
def default_visit(self, node):
raise NotImplementedError(
f'No implementation for {node} on {self}.')
@classmethod
def bind(cls_, node_class, function=SENTINEL):
if cls_ is cls.Visitor:
raise ValueError('Cannot bind base Visitor class.')
if function is SENTINEL:
return functools.partial(cls_.bind, node_class)
setattr(cls_, visitor_name(node_class), function)
return cls_
@classmethod
def undefined(cls_):
if cls_ is cls.Visitor:
raise ValueError(
'No point checking undefined on base Visitor '
'class.')
default = cls.Visitor.default_visit
return {attr for attr in vars(cls.Visitor) if
attr.startswith('visit_') and
default == getattr(cls_, attr)}
cls.Visitor = Visitor
cls.dispatch = dispatch
return cls
class BusinessNode(metaclass=NodeMeta):
__slots__ = ()
@property
def parent(self):
return PARENT.get((type(self), self))
class CachingMeta(type):
def __get__(self, instance, owner):
if instance is None:
return functools.partial(self)
else:
return functools.partial(self, instance)
class Caching(metaclass=CachingMeta):
set_up = False
def __new__(cls, instance, node):
return instance.setdefault((type(node), node), super().__new__(cls))
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
init = vars(cls).get('__init__')
if init is not None:
def __init__(self, *args, **kwargs):
if self.set_up:
return
init(self, *args, **kwargs)
self.set_up = True
cls.__init__ = __init__
class StateVisitor(BusinessNode.Visitor, dict):
__slots__ = ()
class UIVisitor(BusinessNode.Visitor, dict):
__slots__ = 'state',
def __init__(self, state):
super().__init__()
self.state = state
class Choice(BusinessNode, collections.namedtuple('Choice', 'template')):
node_type = 'choice'
class ChoiceDisplay(BusinessNode,
collections.namedtuple('ChoiceDisplay', 'choice')):
node_type = 'choice_display'
class ChoiceTaken(BusinessNode,
collections.namedtuple('ChoiceTaken', 'choice')):
node_type = 'choice_taken'
class ChoiceListed(BusinessNode,
collections.namedtuple('ChoiceListed', 'choice')):
node_type = 'choice_listed'
class ChoiceList(BusinessNode,
collections.namedtuple('ChoiceList',
'choices initial_min initial_max')):
node_type = 'choice_list'
def __init__(self, choices, initial_min, initial_max):
for choice in choices:
key = type(choice), choice
if PARENT.setdefault(key, self) != self:
for choice in choices:
if PARENT.get(key) is self:
del PARENT[key]
raise ValueError(f'Cannot double-parent a choice: {choice}')
class DisplayList(BusinessNode,
collections.namedtuple('DisplayList',
'factory _choices _sections')):
node_type = 'display_list'
def __init__(self, factory, _choices, sections):
for choice in self.choices:
key = type(choice), choice
if PARENT.setdefault(key, self) != self:
for choice in self.choices:
if PARENT.get(key) is self:
del PARENT[key]
raise ValueError(f'Cannot double-parent a choice: {choice}')
@property
def choices(self):
for choice in self._choices:
yield self.factory(choice)
@property
def sections(self):
# This is O(n^2), but n is small
for section in self._sections:
yield Section(self, section[0])
class Section(BusinessNode,
collections.namedtuple('Section', 'display_list', 'name')):
def __init__(self, display_list, name):
self.section
@property
def predicate(self):
return self.section[1]
@property
def section(self):
for section in self.display_list._sections:
if section[0] == self.name:
return section
raise ValueError(
f'No section in {self.display_list} matching {self.name}')
class StateBase(Caching):
visible = False
def __init__(self, instance, node):
self.visitor = instance
self.node = node
@property
def parent(self):
if self.node.parent:
return self.node.parent.dispatch(self.visitor)
class BusinessButton(Caching):
@property
def is_valid(self):
return self.enabled or not self.selected
@property
def visible(self):
parent = self.parent
return parent is not None and parent.visible
class ChoiceButton(BusinessButton):
@property
def choice(self):
return self.node.choice.dispatch(self.visitor)
@StateVisitor.bind(Choice)
class StateVisitor(Caching):
is_valid = True
selected = False
@property
def choice(self):
return self.node
@StateVisitor.bind(ChoiceDisplay)
class StateVisitor(ChoiceButton):
enabled = False
selected = False
@property
def choice_display(self):
return self.node
@StateVisitor.bind(ChoiceTaken)
class StateVisitor(ChoiceButton):
@property
def choice_taken(self):
return self.node
# Should this go on UI instead?
@property
def visible(self):
return (super().visible and
self.choice_taken.choice.dispatch(self.visitor).selected)
@StateVisitor.bind(ChoiceListed)
class StateVisitor(ChoiceButton):
@property
def choice_listed(self):
return self.node
@property
def selected(self):
return self.choice_listed.choice.dispatch(self.visitor).selected
@property
def enabled(self):
parent = self.parent
if parent:
return self.selected or not parent.at_maximum
return True
@StateVisitor.bind(ChoiceList)
class StateVisitor(Caching):
def __init__(self, instance, node):
super().__init__(instance, node)
self.minimum = node.initial_min
self.maximum = node.initial_max
@property
def is_valid(self):
if self.maximum is not None and self.selected > self.maximum:
return False
return all(choice.is_valid for choice in self.choices)
@property
def choices(self):
for choice in self.choice_list.choices:
yield choice.dispatch(self.visitor)
@property
def selected(self):
return sum(choice.selected for choice in self.choices)
@property
def choice_list(self):
return self.node
@property
def at_minimum(self):
return self.selected >= self.minimum
@property
def at_maximum(self):
return self.maximum is not None and self.selected >= self.maximum
@StateVisitor.bind(DisplayList)
class StateVisitor(Caching):
@property
def display_list(self):
return self.node
@property
def is_valid(self):
return all(choice.is_valid for choice in self.choices)
@property
def choices(self):
for choice in self.display_list.choices:
yield choice.dispatch(self.visitor)
@property
def sections(self):
for section in self.display_list.sections:
yield section.dispatch(self.visitor)
@StateVisitor.bind(Section)
class StateVisitor(Caching):
@property
def section(self):
return self.node
@property
def name(self):
return self.section.name
@property
def choices(self):
predicate = self.section.predicate
for choice in self.section.display_list.choices:
visited = choice.dispatch(self.visitor)
if predicate(visited):
yield visited
Comments