Skip to content

Commit 4396f70

Browse files
committed
The main goal is to be able to run tests without using an intermediate tree structure.
This follows issue exercism#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.
1 parent 8133d0f commit 4396f70

File tree

3 files changed

+188
-103
lines changed

3 files changed

+188
-103
lines changed

exercises/zipper/example.py

+95-29
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,107 @@
1-
class Zipper(object):
2-
@staticmethod
3-
def from_tree(tree):
4-
return Zipper(dict(tree), [])
1+
from textwrap import indent
52

6-
def __init__(self, tree, ancestors):
7-
self.tree = tree
8-
self.ancestors = ancestors
93

4+
class Zipper:
5+
def __init__(self, focus=None, context=None):
6+
self.focus = focus
7+
self.context = context or []
8+
9+
@property
1010
def value(self):
11-
return self.tree['value']
11+
try:
12+
return self.focus.value
13+
except AttributeError as attribute:
14+
raise ValueError("There is no value here, but you can insert a node here.") from attribute
1215

1316
def set_value(self, value):
14-
self.tree['value'] = value
15-
return self
17+
new_left = self.focus.left or None
18+
new_right = self.focus.right or None
19+
new_location = Zipper(Focus(value, new_left, new_right), self.context)
20+
new_location.focus.value = value
21+
return new_location
1622

23+
@property
1724
def left(self):
18-
if self.tree['left'] is None:
19-
return None
20-
return Zipper(self.tree['left'], self.ancestors + [self.tree])
21-
22-
def set_left(self, tree):
23-
self.tree['left'] = tree
24-
return self
25+
new_focus, new_context = self.focus.focus_left()
26+
return Zipper(new_focus, self.context+[new_context])
2527

28+
@property
2629
def right(self):
27-
if self.tree['right'] is None:
28-
return None
29-
return Zipper(self.tree['right'], self.ancestors + [self.tree])
30-
31-
def set_right(self, tree):
32-
self.tree['right'] = tree
33-
return self
30+
new_focus, new_context = self.focus.focus_right()
31+
return Zipper(new_focus, self.context+[new_context])
3432

33+
@property
3534
def up(self):
36-
return Zipper(self.ancestors[-1], self.ancestors[:-1])
35+
last_context = self.context[-1]
36+
previous_focus = last_context.reattach(self.focus)
37+
return Zipper(previous_focus, self.context[:-1])
38+
39+
@property
40+
def tree(self):
41+
return self.root.focus
42+
43+
@property
44+
def root(self):
45+
zipper = Zipper(self.focus, self.context)
46+
while zipper.context:
47+
zipper = zipper.up
48+
return zipper
49+
50+
def insert(self, obj):
51+
focus = None
52+
# Both `Zipper` and `Focus` obj instances are not tested in zipper_test.py
53+
# We could suggest this as optional things to do:
54+
if isinstance(obj, Zipper):
55+
focus = obj.tree
56+
elif isinstance(obj, Focus):
57+
focus = obj
58+
else:
59+
# This is the tested behavior:
60+
focus = Focus(obj, None, None)
61+
return Zipper(focus, self.context)
62+
63+
# These two are not tested either, they could also be suggested as optional
64+
# things to do
65+
def insert_left(self, obj):
66+
return self.left.insert(obj).up
67+
68+
def insert_right(self, obj):
69+
return self.right.insert(obj).up
70+
71+
def __repr__(self):
72+
context_str = '('+'), ('.join(map(str, self.context))+')'
73+
return f"focus:\n{self.focus}\ncontext:[\n{context_str}\n]"
74+
75+
76+
class BinaryTree:
77+
def __init__(self, value, left, right):
78+
self.value = value
79+
self.left = left
80+
self.right = right
81+
82+
def __repr__(self):
83+
text = str(self.value)
84+
if self.left:
85+
text += '\n L:' + indent(str(self.left), ' ')
86+
if self.right:
87+
text += '\n R:' + indent(str(self.right), ' ')
88+
return text
89+
90+
91+
class Context(BinaryTree):
92+
def __init__(self, value, left, right, is_left=False):
93+
self.is_left = is_left
94+
super().__init__(value, left, right)
95+
96+
def reattach(self, tree):
97+
if self.is_left:
98+
return Focus(self.value, tree, self.right)
99+
return Focus(self.value, self.left, tree)
100+
101+
102+
class Focus(BinaryTree):
103+
def focus_left(self):
104+
return self.left, Context(self.value, None, self.right, is_left=True)
37105

38-
def to_tree(self):
39-
if any(self.ancestors):
40-
return self.ancestors[0]
41-
return self.tree
106+
def focus_right(self):
107+
return self.right, Context(self.value, self.left, None)

exercises/zipper/zipper.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
1-
class Zipper(object):
2-
@staticmethod
3-
def from_tree(tree):
1+
class Zipper:
2+
def __init__(self, focus=None, context=None):
43
pass
54

5+
@property
66
def value(self):
77
pass
88

9-
def set_value(self):
9+
def set_value(self, value):
1010
pass
1111

12+
@property
1213
def left(self):
1314
pass
1415

15-
def set_left(self):
16-
pass
17-
16+
@property
1817
def right(self):
1918
pass
2019

21-
def set_right(self):
20+
@property
21+
def up(self):
2222
pass
2323

24-
def up(self):
24+
@property
25+
def root(self):
2526
pass
2627

27-
def to_tree(self):
28+
def insert(self, obj):
2829
pass

exercises/zipper/zipper_test.py

+82-64
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,94 @@
11
import unittest
22

3+
34
from zipper import Zipper
45

56

67
# Tests adapted from `problem-specifications//canonical-data.json` @ v1.1.0
7-
88
class ZipperTest(unittest.TestCase):
9-
def bt(self, value, left, right):
10-
return {
11-
'value': value,
12-
'left': left,
13-
'right': right
14-
}
15-
16-
def leaf(self, value):
17-
return self.bt(value, None, None)
18-
19-
def create_trees(self):
20-
t1 = self.bt(1, self.bt(2, None, self.leaf(3)), self.leaf(4))
21-
t2 = self.bt(1, self.bt(5, None, self.leaf(3)), self.leaf(4))
22-
t3 = self.bt(1, self.bt(2, self.leaf(5), self.leaf(3)), self.leaf(4))
23-
t4 = self.bt(1, self.leaf(2), self.leaf(4))
24-
return (t1, t2, t3, t4)
25-
26-
def test_data_is_retained(self):
27-
t1, _, _, _ = self.create_trees()
28-
zipper = Zipper.from_tree(t1)
29-
tree = zipper.to_tree()
30-
self.assertEqual(tree, t1)
31-
32-
def test_left_and_right_value(self):
33-
t1, _, _, _ = self.create_trees()
34-
zipper = Zipper.from_tree(t1)
35-
self.assertEqual(zipper.left().right().value(), 3)
36-
37-
def test_dead_end(self):
38-
t1, _, _, _ = self.create_trees()
39-
zipper = Zipper.from_tree(t1)
40-
self.assertIsNone(zipper.left().left())
41-
42-
def test_tree_from_deep_focus(self):
43-
t1, _, _, _ = self.create_trees()
44-
zipper = Zipper.from_tree(t1)
45-
self.assertEqual(zipper.left().right().to_tree(), t1)
9+
def test_empty(self):
10+
zipper = Zipper()
11+
with self.assertRaises(ValueError):
12+
zipper.value
13+
14+
def test_insert_non_mutating(self):
15+
zipper = Zipper()
16+
zipper.insert(1)
17+
with self.assertRaises(ValueError):
18+
zipper.value
19+
20+
def test_insert_and_get_value(self):
21+
expected = 1
22+
actual = Zipper().insert(expected).value
23+
self.assertEqual(actual, expected)
24+
25+
def test_left(self):
26+
expected = 2
27+
zipper = Zipper().insert(1).left
28+
with self.assertRaises(ValueError):
29+
zipper.value
30+
actual = zipper.insert(expected).value
31+
self.assertEqual(actual, expected)
32+
33+
def test_right(self):
34+
expected = 3
35+
zipper = Zipper().insert(1).right
36+
with self.assertRaises(ValueError):
37+
zipper.value
38+
zipper = zipper.insert(expected)
39+
self.assertEqual(zipper.value, expected)
40+
41+
def test_left_and_right_inserts_non_mutating(self):
42+
expected = 1
43+
zipper = Zipper().insert(1)
44+
zipper.left.insert(2)
45+
zipper.right.insert(3)
46+
self.assertEqual(zipper.value, expected)
47+
48+
def test_left_up_cancels(self):
49+
expected = 1
50+
zipper = Zipper().insert(expected)
51+
actual = zipper.left.up.value
52+
self.assertEqual(actual, expected)
53+
54+
def test_right_up_cancels(self):
55+
expected = 2
56+
zipper = Zipper().insert(expected)
57+
actual = zipper.right.up.value
58+
self.assertEqual(actual, expected)
59+
60+
def test_up_non_mutating(self):
61+
expected = 3
62+
zipper = Zipper().insert(1).left.insert(expected)
63+
zipper.up
64+
actual = zipper.value
65+
self.assertEqual(actual, expected)
4666

4767
def test_set_value(self):
48-
t1, t2, _, _ = self.create_trees()
49-
zipper = Zipper.from_tree(t1)
50-
updatedZipper = zipper.left().set_value(5)
51-
tree = updatedZipper.to_tree()
52-
self.assertEqual(tree, t2)
53-
54-
def test_set_left_with_value(self):
55-
t1, _, t3, _ = self.create_trees()
56-
zipper = Zipper.from_tree(t1)
57-
updatedZipper = zipper.left().set_left(self.leaf(5))
58-
tree = updatedZipper.to_tree()
59-
self.assertEqual(tree, t3)
60-
61-
def test_set_right_to_none(self):
62-
t1, _, _, t4 = self.create_trees()
63-
zipper = Zipper.from_tree(t1)
64-
updatedZipper = zipper.left().set_right(None)
65-
tree = updatedZipper.to_tree()
66-
self.assertEqual(tree, t4)
67-
68-
def test_different_paths_to_same_zipper(self):
69-
t1, _, _, _ = self.create_trees()
70-
zipper = Zipper.from_tree(t1)
71-
self.assertEqual(zipper.left().up().right().to_tree(),
72-
zipper.right().to_tree())
73-
68+
expected = 2
69+
zipper = Zipper().insert(1).right.insert(3).up.left.insert(2)
70+
actual = zipper.set_value(zipper.value*expected).value//zipper.value
71+
self.assertEqual(actual, expected)
72+
73+
def test_set_value_non_mutating(self):
74+
expected = 4
75+
zipper = Zipper().insert(expected)
76+
zipper.set_value(8)
77+
actual = zipper.value
78+
self.assertEqual(actual, expected)
79+
80+
def test_root(self):
81+
expected = 1
82+
zipper = Zipper().insert(expected).right.insert(3).left.insert(5)
83+
actual = zipper.root.value
84+
self.assertEqual(actual, expected)
85+
86+
def test_root_non_mutating(self):
87+
expected = 5
88+
zipper = Zipper().insert(expected).right.insert(3).left.insert(expected)
89+
zipper.root
90+
actual = zipper.value
91+
self.assertEqual(actual, expected)
7492

7593
if __name__ == '__main__':
7694
unittest.main()

0 commit comments

Comments
 (0)