mwchase icon

LC:Nobilis Update 5: diff

mwchase | PRO | 05/28/17 06:23:42 PM UTC | 0 ⭐ | 1345 👁️ | Never ⏰ | []
Diff |

12.64 KB

|

None

|

0 👍

/

0 👎

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