Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- 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__ = ()
- -
- -
- -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__ = ()
- +
- +
- +class ChoiceListed(QH, nt('ChoiceListed', 'hash choice')):
- +
- + __slots__ = ()
- +
- +
- +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]
- -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
- +
- +
- +def mapping():
- + import nobilis.mapping
- + return nobilis.mapping
- +
- +
- +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'
Advertisement
Add Comment
Please, Sign In to add comment