From 4396f709b8cda65b74b9844ca68309cb1bc4ceaf Mon Sep 17 00:00:00 2001 From: Christian Glacet Date: Sun, 2 Sep 2018 15:17:25 +0200 Subject: [PATCH] The main goal is to be able to run tests without using an intermediate tree structure. This follows issue exercism/python#1497. Now the test consists in creating a tree starting from an empty zipper using the the atomic operations left, right, up and insert/modify/read node values. (Each operation is tested independently.) In the old version there were no immutability check. Immutability will now be an important part of students goals. --- exercises/zipper/example.py | 124 ++++++++++++++++++++------- exercises/zipper/zipper.py | 21 ++--- exercises/zipper/zipper_test.py | 146 ++++++++++++++++++-------------- 3 files changed, 188 insertions(+), 103 deletions(-) diff --git a/exercises/zipper/example.py b/exercises/zipper/example.py index a9003e9e21..eae0e00a5c 100644 --- a/exercises/zipper/example.py +++ b/exercises/zipper/example.py @@ -1,41 +1,107 @@ -class Zipper(object): - @staticmethod - def from_tree(tree): - return Zipper(dict(tree), []) +from textwrap import indent - def __init__(self, tree, ancestors): - self.tree = tree - self.ancestors = ancestors +class Zipper: + def __init__(self, focus=None, context=None): + self.focus = focus + self.context = context or [] + + @property def value(self): - return self.tree['value'] + try: + return self.focus.value + except AttributeError as attribute: + raise ValueError("There is no value here, but you can insert a node here.") from attribute def set_value(self, value): - self.tree['value'] = value - return self + new_left = self.focus.left or None + new_right = self.focus.right or None + new_location = Zipper(Focus(value, new_left, new_right), self.context) + new_location.focus.value = value + return new_location + @property def left(self): - if self.tree['left'] is None: - return None - return Zipper(self.tree['left'], self.ancestors + [self.tree]) - - def set_left(self, tree): - self.tree['left'] = tree - return self + new_focus, new_context = self.focus.focus_left() + return Zipper(new_focus, self.context+[new_context]) + @property def right(self): - if self.tree['right'] is None: - return None - return Zipper(self.tree['right'], self.ancestors + [self.tree]) - - def set_right(self, tree): - self.tree['right'] = tree - return self + new_focus, new_context = self.focus.focus_right() + return Zipper(new_focus, self.context+[new_context]) + @property def up(self): - return Zipper(self.ancestors[-1], self.ancestors[:-1]) + last_context = self.context[-1] + previous_focus = last_context.reattach(self.focus) + return Zipper(previous_focus, self.context[:-1]) + + @property + def tree(self): + return self.root.focus + + @property + def root(self): + zipper = Zipper(self.focus, self.context) + while zipper.context: + zipper = zipper.up + return zipper + + def insert(self, obj): + focus = None + # Both `Zipper` and `Focus` obj instances are not tested in zipper_test.py + # We could suggest this as optional things to do: + if isinstance(obj, Zipper): + focus = obj.tree + elif isinstance(obj, Focus): + focus = obj + else: + # This is the tested behavior: + focus = Focus(obj, None, None) + return Zipper(focus, self.context) + + # These two are not tested either, they could also be suggested as optional + # things to do + def insert_left(self, obj): + return self.left.insert(obj).up + + def insert_right(self, obj): + return self.right.insert(obj).up + + def __repr__(self): + context_str = '('+'), ('.join(map(str, self.context))+')' + return f"focus:\n{self.focus}\ncontext:[\n{context_str}\n]" + + +class BinaryTree: + def __init__(self, value, left, right): + self.value = value + self.left = left + self.right = right + + def __repr__(self): + text = str(self.value) + if self.left: + text += '\n L:' + indent(str(self.left), ' ') + if self.right: + text += '\n R:' + indent(str(self.right), ' ') + return text + + +class Context(BinaryTree): + def __init__(self, value, left, right, is_left=False): + self.is_left = is_left + super().__init__(value, left, right) + + def reattach(self, tree): + if self.is_left: + return Focus(self.value, tree, self.right) + return Focus(self.value, self.left, tree) + + +class Focus(BinaryTree): + def focus_left(self): + return self.left, Context(self.value, None, self.right, is_left=True) - def to_tree(self): - if any(self.ancestors): - return self.ancestors[0] - return self.tree + def focus_right(self): + return self.right, Context(self.value, self.left, None) diff --git a/exercises/zipper/zipper.py b/exercises/zipper/zipper.py index 14903a8a09..eb26ef4e4d 100644 --- a/exercises/zipper/zipper.py +++ b/exercises/zipper/zipper.py @@ -1,28 +1,29 @@ -class Zipper(object): - @staticmethod - def from_tree(tree): +class Zipper: + def __init__(self, focus=None, context=None): pass + @property def value(self): pass - def set_value(self): + def set_value(self, value): pass + @property def left(self): pass - def set_left(self): - pass - + @property def right(self): pass - def set_right(self): + @property + def up(self): pass - def up(self): + @property + def root(self): pass - def to_tree(self): + def insert(self, obj): pass diff --git a/exercises/zipper/zipper_test.py b/exercises/zipper/zipper_test.py index f24e03c2c9..aa45f1a58a 100644 --- a/exercises/zipper/zipper_test.py +++ b/exercises/zipper/zipper_test.py @@ -1,76 +1,94 @@ import unittest + from zipper import Zipper # Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0 - class ZipperTest(unittest.TestCase): - def bt(self, value, left, right): - return { - 'value': value, - 'left': left, - 'right': right - } - - def leaf(self, value): - return self.bt(value, None, None) - - def create_trees(self): - t1 = self.bt(1, self.bt(2, None, self.leaf(3)), self.leaf(4)) - t2 = self.bt(1, self.bt(5, None, self.leaf(3)), self.leaf(4)) - t3 = self.bt(1, self.bt(2, self.leaf(5), self.leaf(3)), self.leaf(4)) - t4 = self.bt(1, self.leaf(2), self.leaf(4)) - return (t1, t2, t3, t4) - - def test_data_is_retained(self): - t1, _, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - tree = zipper.to_tree() - self.assertEqual(tree, t1) - - def test_left_and_right_value(self): - t1, _, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - self.assertEqual(zipper.left().right().value(), 3) - - def test_dead_end(self): - t1, _, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - self.assertIsNone(zipper.left().left()) - - def test_tree_from_deep_focus(self): - t1, _, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - self.assertEqual(zipper.left().right().to_tree(), t1) + def test_empty(self): + zipper = Zipper() + with self.assertRaises(ValueError): + zipper.value + + def test_insert_non_mutating(self): + zipper = Zipper() + zipper.insert(1) + with self.assertRaises(ValueError): + zipper.value + + def test_insert_and_get_value(self): + expected = 1 + actual = Zipper().insert(expected).value + self.assertEqual(actual, expected) + + def test_left(self): + expected = 2 + zipper = Zipper().insert(1).left + with self.assertRaises(ValueError): + zipper.value + actual = zipper.insert(expected).value + self.assertEqual(actual, expected) + + def test_right(self): + expected = 3 + zipper = Zipper().insert(1).right + with self.assertRaises(ValueError): + zipper.value + zipper = zipper.insert(expected) + self.assertEqual(zipper.value, expected) + + def test_left_and_right_inserts_non_mutating(self): + expected = 1 + zipper = Zipper().insert(1) + zipper.left.insert(2) + zipper.right.insert(3) + self.assertEqual(zipper.value, expected) + + def test_left_up_cancels(self): + expected = 1 + zipper = Zipper().insert(expected) + actual = zipper.left.up.value + self.assertEqual(actual, expected) + + def test_right_up_cancels(self): + expected = 2 + zipper = Zipper().insert(expected) + actual = zipper.right.up.value + self.assertEqual(actual, expected) + + def test_up_non_mutating(self): + expected = 3 + zipper = Zipper().insert(1).left.insert(expected) + zipper.up + actual = zipper.value + self.assertEqual(actual, expected) def test_set_value(self): - t1, t2, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - updatedZipper = zipper.left().set_value(5) - tree = updatedZipper.to_tree() - self.assertEqual(tree, t2) - - def test_set_left_with_value(self): - t1, _, t3, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - updatedZipper = zipper.left().set_left(self.leaf(5)) - tree = updatedZipper.to_tree() - self.assertEqual(tree, t3) - - def test_set_right_to_none(self): - t1, _, _, t4 = self.create_trees() - zipper = Zipper.from_tree(t1) - updatedZipper = zipper.left().set_right(None) - tree = updatedZipper.to_tree() - self.assertEqual(tree, t4) - - def test_different_paths_to_same_zipper(self): - t1, _, _, _ = self.create_trees() - zipper = Zipper.from_tree(t1) - self.assertEqual(zipper.left().up().right().to_tree(), - zipper.right().to_tree()) - + expected = 2 + zipper = Zipper().insert(1).right.insert(3).up.left.insert(2) + actual = zipper.set_value(zipper.value*expected).value//zipper.value + self.assertEqual(actual, expected) + + def test_set_value_non_mutating(self): + expected = 4 + zipper = Zipper().insert(expected) + zipper.set_value(8) + actual = zipper.value + self.assertEqual(actual, expected) + + def test_root(self): + expected = 1 + zipper = Zipper().insert(expected).right.insert(3).left.insert(5) + actual = zipper.root.value + self.assertEqual(actual, expected) + + def test_root_non_mutating(self): + expected = 5 + zipper = Zipper().insert(expected).right.insert(3).left.insert(expected) + zipper.root + actual = zipper.value + self.assertEqual(actual, expected) if __name__ == '__main__': unittest.main()