diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -40,8 +40,4 @@
packages=find_packages('src', exclude=['contrib', 'docs', 'tests']),
package_dir={'': 'src'},
-
- extras_require={
- 'test': ['coverage', 'pytest'],
- },
)
diff --git a/src/nobilis/lifepath.py b/src/nobilis/lifepath.py
--- a/src/nobilis/lifepath.py
+++ b/src/nobilis/lifepath.py
@@ -1,5 +1,8 @@
import collections
+import functools
+from . import map_tuple
+from . import mapping
from . import visitor
@@ -12,51 +15,133 @@
return type(item), item
-class Parentable:
+# This is horrendously brittle, but I don't feel like writing test cases right
+# now. Basically, it needs to be used as a mixin AHEAD OF a namedtuple class
+# with a first field of "hash".
+class QuickHash:
+
+ __slots__ = ()
+
+ def __new__(cls, *args, **kwargs):
+ my_hash = cls._pre_hash(*args, **kwargs)
+ return super().__new__(cls, my_hash, *args, **kwargs)
+
+ @classmethod
+ def _pre_hash(cls, *args, **kwargs):
+ args = tuple(args)
+ fields = getattr(cls, '_fields', ('hash',))[1:]
+ keywords = fields[len(args):]
+ args += tuple((kwargs[keyword] for keyword in keywords))
+ return hash(args)
+
+ def __hash__(self):
+ return self.hash
+
+
+QH = QuickHash
+
+
+class Child(Business):
__slots__ = ()
+ def default_visit(self, node):
+ yield from ()
-P = Parentable
+
+CHILD = Child()
+
+
+class Tree(QH, nt('Tree', 'hash root reverse_mapping')):
+
+ __slots__ = ()
+
+ def __new__(cls, root):
+ reverse_mapping = {}
+ nodes_to_add = [root]
+ while nodes_to_add:
+ node = nodes_to_add.pop()
+ for child in CHILD.visit(node):
+ child_key = key(child)
+ if child_key in reverse_mapping:
+ raise ValueError(f'{root} is not a valid tree!')
+ else:
+ reverse_mapping[child_key] = node
+ nodes_to_add.append(child)
+ return super().__new__(cls, root, mapping.Map(reverse_mapping))
+
+ def parent(self, node):
+ return self.reverse_mapping.get(key(node))
@Business.mark
-class Choice(P, nt('Choice', 'template')):
-
- __slots__ = ()
-
-
[email protected]
-class ChoiceDisplay(P, nt('ChoiceDisplay', 'choice')):
+class Choice(QH, nt('Choice', 'hash template')):
__slots__ = ()
@Business.mark
-class ChoiceTaken(P, nt('ChoiceTaken', 'choice')):
+class ChoiceDisplay(QH, nt('ChoiceDisplay', 'hash choice')):
__slots__ = ()
@Business.mark
-class ChoiceListed(P, nt('ChoiceListed', 'choice')):
+class ChoiceTaken(QH, nt('ChoiceTaken', 'hash choice')):
+
+ __slots__ = ()
+
+
[email protected]
+class ChoiceListed(QH, nt('ChoiceListed', 'hash choice')):
+
+ __slots__ = ()
+
+
[email protected]
+class ChoiceList(QH, nt('ChoiceList', 'hash choices initial_min initial_max')):
__slots__ = ()
@Business.mark
-class ChoiceList(P, nt('ChoiceList', 'choices initial_min initial_max')):
+class DisplayList(QH, nt('DisplayList', 'hash factory choices_ sections_')):
__slots__ = ()
+ @property
+ def choices(self):
+ return map_tuple.MapTuple(self.factory, self.choices_)
+
+ @property
+ def sections(self):
+ return map_tuple.MapTuple(
+ functools.partial(Section, self), range(len(self.sections_)))
+
@Business.mark
-class DisplayList(P, nt('DisplayList', 'factory choices_ sections_')):
+class Section(QH, nt('Section', 'hash display_list index')):
__slots__ = ()
+ @property
+ def _data(self):
+ return self.display_list.sections_[self.index]
[email protected]
-class Section(P, nt('Section', 'display_list name')):
+ @property
+ def name(self):
+ return self._data[0]
+
+ @property
+ def predicate(self):
+ return self._data[1]
+
- __slots__ = ()
[email protected](ChoiceList)
+def Child(self, node, next_visitor):
+ yield from node.choices
+
+
[email protected](DisplayList)
+def Child(self, node, next_visitor):
+ yield from node.choices
diff --git a/src/nobilis/map_tuple.py b/src/nobilis/map_tuple.py
new file mode 100644
--- /dev/null
+++ b/src/nobilis/map_tuple.py
@@ -0,0 +1,16 @@
+import collections
+
+
+class MapTuple(collections.Sequence):
+
+ __slots__ = '_function', '_sequence'
+
+ def __init__(self, function, sequence):
+ self._function = function
+ self._sequence = sequence
+
+ def __len__(self):
+ return len(self._sequence)
+
+ def __getitem__(self, index):
+ return self._function(self._sequence[index])
diff --git a/src/nobilis/mapping.py b/src/nobilis/mapping.py
new file mode 100644
--- /dev/null
+++ b/src/nobilis/mapping.py
@@ -0,0 +1,28 @@
+import collections
+
+
+class Map(collections.abc.Mapping):
+
+ __slots__ = '_dict', '_hash'
+
+ def __init__(*args, **kwargs):
+ try:
+ self, *args = args
+ except ValueError:
+ raise TypeError
+ self._hash = None
+ self._dict = dict(*args, **kwargs)
+
+ def __hash__(self):
+ if self._hash is None:
+ self._hash = hash(frozenset(self._dict.items()))
+ return self._hash
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ def __iter__(self):
+ return iter(self._dict)
+
+ def __len__(self):
+ return len(self._dict)
diff --git a/src/nobilis/visitor.py b/src/nobilis/visitor.py
--- a/src/nobilis/visitor.py
+++ b/src/nobilis/visitor.py
@@ -19,6 +19,8 @@
"""Return a base Visitor class."""
class Visitor:
+ __slots__ = ()
+
__classes = set()
def __new__(cls, *args, **kwargs):
diff --git a/tests/nobilis/conftest.py b/tests/nobilis/conftest.py
--- a/tests/nobilis/conftest.py
+++ b/tests/nobilis/conftest.py
@@ -17,3 +17,15 @@
def lifepath():
import nobilis.lifepath
return nobilis.lifepath
+
+
[email protected]
+def mapping():
+ import nobilis.mapping
+ return nobilis.mapping
+
+
[email protected]
+def map_tuple():
+ import nobilis.map_tuple
+ return nobilis.map_tuple
diff --git a/tests/nobilis/test_lifepath.py b/tests/nobilis/test_lifepath.py
--- a/tests/nobilis/test_lifepath.py
+++ b/tests/nobilis/test_lifepath.py
@@ -1,3 +1,6 @@
+import pytest
+
+
def test_import(lifepath):
assert lifepath
@@ -5,3 +8,108 @@
def test_key(lifepath):
assert lifepath.key(lifepath.Choice(None)) != lifepath.key(
lifepath.ChoiceDisplay(None))
+
+
+def test_hash_amortization(lifepath):
+ # This is a little tricky. Basically, I don't want hashing a recursive type
+ # to hit the leaves repeatedly. Of course, a leaf has to be hashed at least
+ # once. But let's give this a shot.
+ class TrackingChoice(lifepath.Choice):
+
+ hash_count = 0
+
+ def __hash__(self):
+ self.hash_count += 1
+ return super().__hash__()
+
+ leaf = TrackingChoice(None)
+ assert leaf.hash_count == 0
+ root = lifepath.Choice(leaf)
+ assert leaf.hash_count <= 1
+ for _ in range(3):
+ hash(root)
+ assert leaf.hash_count <= 1
+
+
+# So, my current plan for setting up the "parent" relationship is to lean on
+# the visitors, in order to avoid putting more logic onto the classes
+# themselves. Basically, each class defines its own concept of "children", and
+# there's a visitor or a protocol that defines and enforces the reverse
+# relationship. I think a non-visitor would be fine, since it simply has to
+# consume the child visitor, which defaults to yield from (), and defines
+# actual behavior for specific classes.
+
+# To speed up construction of the parent graph, each component could cache a
+# mapping of its data, which could be pulled in directly. What's a good dict
+# idiom for "update, but fail on collision"?
+
+# Here's something I think would make sense: rather than a global cache, the
+# "parent" instances have an OrderedDict attribute that they share when
+# creating other instances from themselves. The OrderedDict is accessed through
+# synchronized functions, that pop and push the requested value. When it's
+# larger than some threshold, the first few entries are removed. This is like
+# an LRU, but with shared state rather than global state, so it's more
+# testable. Have a wrapper class in its own module, just for managing this
+# stuff.
+
+# Actually, given all that, I could just make it an instance attribute that's
+# an LRU. That seems pretty simple.
+
+# But for now, I'll just construct the dictionary per-instance, no funny stuff.
+
+
+def test_children(lifepath):
+ child = lifepath.Child()
+
+ leaf = lifepath.Choice(None)
+
+ assert tuple(child.visit(leaf)) == ()
+
+ for wrapper in (lifepath.ChoiceDisplay, lifepath.ChoiceTaken,
+ lifepath.ChoiceListed):
+ assert tuple(child.visit(wrapper(leaf))) == ()
+
+ assert tuple(child.visit(lifepath.ChoiceList((leaf,), 0, None))) == (leaf,)
+
+ display_list = lifepath.DisplayList(
+ lifepath.ChoiceDisplay, (leaf,), (('name', None),))
+
+ assert tuple(child.visit(display_list)) == (lifepath.ChoiceDisplay(leaf),)
+
+
+def test_tree(lifepath):
+ leaf = lifepath.Choice(None)
+ root = lifepath.ChoiceList((leaf,), 0, None)
+ tree = lifepath.Tree(root)
+ assert tree.parent(leaf) == root
+
+ rooter = lifepath.ChoiceList((root,), 0, None)
+
+ treeer = lifepath.Tree(rooter)
+ assert treeer.parent(root) == rooter
+ assert treeer.parent(leaf) == root
+
+ dag = lifepath.ChoiceList((leaf, leaf), 0, None)
+
+ with pytest.raises(ValueError):
+ print(lifepath.Tree(dag))
+
+
+def test_section(lifepath):
+ leaf = lifepath.Choice(None)
+ sentinel = object()
+
+ display_list = lifepath.DisplayList(
+ lifepath.ChoiceDisplay, (leaf,), (('name', sentinel),))
+
+ section, = display_list.sections
+
+ assert section.name == 'name'
+ assert section.predicate is sentinel
+
+
+def test_state(lifepath):
+ # Okay, so, let's think how this has to go.
+ # Ideally, I wouldn't be relying on mutable state, so...
+ # state = lifepath.State()
+ pass
diff --git a/tests/nobilis/test_map_tuple.py b/tests/nobilis/test_map_tuple.py
new file mode 100644
--- /dev/null
+++ b/tests/nobilis/test_map_tuple.py
@@ -0,0 +1,12 @@
+def test_import(map_tuple):
+ assert map_tuple
+
+
+def test_class(map_tuple):
+ assert map_tuple.MapTuple
+
+
+def test_basic(map_tuple):
+ my_tuple = map_tuple.MapTuple(str.upper, ('abc',))
+ assert len(my_tuple) == 1
+ assert my_tuple[0] == 'ABC'
diff --git a/tests/nobilis/test_mapping.py b/tests/nobilis/test_mapping.py
new file mode 100644
--- /dev/null
+++ b/tests/nobilis/test_mapping.py
@@ -0,0 +1,25 @@
+import pytest
+
+
+def test_import(mapping):
+ assert mapping
+
+
+def test_map(mapping):
+ assert not mapping.Map()
+ assert not mapping.Map({})
+
+ assert mapping.Map() == mapping.Map({})
+
+ assert mapping.Map(self=1) == mapping.Map(self=1)
+ assert {mapping.Map()}
+ a_mapping = mapping.Map()
+ dct = {a_mapping: a_mapping}
+ assert dct[a_mapping] == a_mapping
+
+
+def test_a_weird_edge(mapping):
+ with pytest.raises(TypeError):
+ print(dict.__init__())
+ with pytest.raises(TypeError):
+ print(mapping.Map.__init__())
diff --git a/tests/nobilis/test_visitor.py b/tests/nobilis/test_visitor.py
--- a/tests/nobilis/test_visitor.py
+++ b/tests/nobilis/test_visitor.py
@@ -147,3 +147,43 @@
return 'd' + next_visitor('e')
assert Visitor2().visit(Test2()) == 'abcde'
+
+
+def test_next_visitor_as_generator(visitor):
+ Visitor = visitor.make_visitor()
+
+ @Visitor.mark
+ class Test1:
+ pass
+
+ @Visitor.mark
+ class Test2(Test1):
+ pass
+
+ class Visitor1(Visitor):
+ pass
+
+ class Visitor2(Visitor1):
+ pass
+
+ @Visitor2.bind(Test2)
+ def Visitor2(self, node, next_visitor):
+ yield 'a'
+ yield from next_visitor('f')
+
+ @Visitor1.bind(Test2)
+ def Visitor1(self, node, next_visitor):
+ yield 'b'
+ yield from next_visitor('f')
+
+ @Visitor2.bind(Test1)
+ def Visitor2(self, node, next_visitor):
+ yield 'c'
+ yield from next_visitor('f')
+
+ @Visitor1.bind(Test1)
+ def Visitor1(self, node, next_visitor):
+ yield 'd'
+ yield from next_visitor('e')
+
+ assert ''.join(Visitor2().visit(Test2())) == 'abcde'
Comments