Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 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
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement