diff --git a/exercises/react/example.py b/exercises/react/example.py index b424c1e422..7ea464d7ed 100644 --- a/exercises/react/example.py +++ b/exercises/react/example.py @@ -1,65 +1,56 @@ class Cell(object): - def __init__(self, reactor, value=0, dependencies=set(), - updater=None): - self.reactor = reactor - self.index = len(reactor.cells) - reactor.cells.append(self) - self.value = value - self.dirty = False - self.dependencies = dependencies - self.dependents = set() - self.updater = updater - self.watchers = set() - if updater is not None: - self.update() - self.notify() - - def add_watcher(self, watcher): - self.watchers.add(watcher) - - def remove_watcher(self, watcher): - self.watchers.remove(watcher) - - def set_value(self, value, top=True): - self.value = value - for d in self.dependents: - d.update() - if top: - self.reactor.notify() - - def update(self): - if self.updater is not None: - values = [d.value for d in self.dependencies] - value = self.updater(values) - if self.value != value: - self.set_value(value, False) - self.dirty = True - - def notify(self): - if self.dirty: - for watcher in self.watchers: - watcher(self, self.value) - self.dirty = False - - def __hash__(self): - return self.index - - -class Reactor(object): def __init__(self): - self.cells = [] - - def create_input_cell(self, value): - return Cell(self, value=value) - - def create_compute_cell(self, dependencies, updater): - cell = Cell(self, - dependencies=dependencies, - updater=updater) - for d in dependencies: - d.dependents.add(cell) - return cell - - def notify(self): - for cell in self.cells: - cell.notify() + self._watchers = [] + self._value = None + self.counter = 0 + + def add_watcher(self, cell): + self._watchers.append(cell) + + @property + def value(self): + return self._value + + @value.setter + def value(self, new_value): + self._value = new_value + self.counter += 1 + for cell in self._watchers: + cell.compute() + + +class InputCell(Cell): + def __init__(self, initial_value): + super(InputCell, self).__init__() + self._value = initial_value + + +class ComputeCell(Cell): + def __init__(self, inputs, compute_function): + super(ComputeCell, self).__init__() + self.inputs = inputs + self.func = compute_function + self.callbacks = set() + self.compute() + self._register_inputs() + + def _register_inputs(self): + for inp in self.inputs: + inp.add_watcher(self) + + def compute(self): + # Only compute this cell when all inputs have same counters + if len(set([inp.counter for inp in self.inputs])) > 1: + return + new_val = self.func([inp.value for inp in self.inputs]) + if new_val != self._value: + self.value = new_val + for cb in self.callbacks: + cb(new_val) + + def add_callback(self, callback): + self.callbacks.add(callback) + + def remove_callback(self, callback): + if callback in self.callbacks: + self.callbacks.remove(callback) diff --git a/exercises/react/react.py b/exercises/react/react.py index c67fa13619..6fc26206fb 100644 --- a/exercises/react/react.py +++ b/exercises/react/react.py @@ -1,17 +1,17 @@ -class Cell(object): - def set_value(value): - pass +class InputCell(object): + def __init__(self, initial_value): + self.value = None - def add_watcher(self, watcher_callback): - pass - def remove_watcher(self, watcher_callback): - pass +class ComputeCell(object): + def __init__(self, inputs, compute_function): + self.value = None + def add_callback(self, callback): + pass -class Reactor(object): - def create_input_cell(self, value): + def remove_callback(self, callback): pass - def create_compute_cell(self, dependencies, updater_callback): + def expect_callback_values(self, callback): pass diff --git a/exercises/react/react_test.py b/exercises/react/react_test.py index 68e6e6f135..3e488ea16f 100644 --- a/exercises/react/react_test.py +++ b/exercises/react/react_test.py @@ -1,135 +1,201 @@ import unittest - -from react import Reactor - - -class CallbackManager(object): - counter = 0 - observed1 = [] - observed2 = [] - - @staticmethod - def reset(): - CallbackManager.counter = 0 - CallbackManager.observed1 = [] - CallbackManager.observed2 = [] - - @staticmethod - def count(sender, value): - CallbackManager.counter += 1 - - @staticmethod - def observe1(sender, value): - CallbackManager.observed1.append(value) - - @staticmethod - def observe2(sender, value): - CallbackManager.observed2.append(value) - - -def increment(values): - return values[0] + 1 - - -def decrement(values): - return values[0] - 1 - - -def product(values): - return values[0] * values[1] - - -def minimum_of_2(values): - return values[0] + 1 if values[0] > 2 else 2 - - -class ReactTest(unittest.TestCase): - def test_setting_input_changes_value(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - self.assertEqual(inputCell1.value, 1) - inputCell1.set_value(2) - self.assertEqual(inputCell1.value, 2) - - def test_computed_cell_determined_by_dependencies(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, increment) - - self.assertEqual(computeCell1.value, 2) - inputCell1.set_value(2) - self.assertEqual(computeCell1.value, 3) - - def test_compute_cells_determined_by_other_compute_cells(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, increment) - computeCell2 = reactor.create_compute_cell({inputCell1}, decrement) - computeCell3 = reactor.create_compute_cell({computeCell1, - computeCell2}, - product) - self.assertEqual(computeCell3.value, 0) - inputCell1.set_value(3) - self.assertEqual(computeCell3.value, 8) - - def test_compute_cells_can_have_callbacks(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, increment) - observed = [] - computeCell1.add_watcher(lambda sender, value: observed.append(value)) - self.assertEqual(observed, []) - inputCell1.set_value(2) - self.assertEqual(observed, [3]) - - def test_callbacks__only_trigger_on_change(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, minimum_of_2) - - CallbackManager.reset() - computeCell1.add_watcher(CallbackManager.count) - - inputCell1.set_value(1) - self.assertEqual(CallbackManager.counter, 0) - inputCell1.set_value(2) - self.assertEqual(CallbackManager.counter, 0) - inputCell1.set_value(3) - self.assertEqual(CallbackManager.counter, 1) - - def test_callbacks_can_be_removed(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, increment) - - CallbackManager.reset() - computeCell1.add_watcher(CallbackManager.observe1) - computeCell1.add_watcher(CallbackManager.observe2) - - inputCell1.set_value(2) - self.assertEqual(CallbackManager.observed1, [3]) - self.assertEqual(CallbackManager.observed2, [3]) - - computeCell1.remove_watcher(CallbackManager.observe1) - inputCell1.set_value(3) - self.assertEqual(CallbackManager.observed1, [3]) - self.assertEqual(CallbackManager.observed2, [3, 4]) - - def test_callbacks_only_called_once(self): - reactor = Reactor() - inputCell1 = reactor.create_input_cell(1) - computeCell1 = reactor.create_compute_cell({inputCell1}, increment) - computeCell2 = reactor.create_compute_cell({inputCell1}, decrement) - computeCell3 = reactor.create_compute_cell({computeCell2}, decrement) - computeCell4 = reactor.create_compute_cell({computeCell1, - computeCell3}, - product) - - CallbackManager.reset() - computeCell4.add_watcher(CallbackManager.count) - - inputCell1.set_value(3) - self.assertEqual(CallbackManager.counter, 1) +from functools import partial + +from react import InputCell, ComputeCell + + +# Tests adapted from `problem-specifications//canonical-data.json` @ v2.0.0 + +class ReactTests(unittest.TestCase): + + def test_input_cells_have_a_value(self): + input_ = InputCell(10) + self.assertEqual(input_.value, 10) + + def test_can_set_input_cell_value(self): + input_ = InputCell(4) + input_.value = 20 + self.assertEqual(input_.value, 20) + + def test_compute_cells_calculate_initial_value(self): + input_ = InputCell(1) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + self.assertEqual(output.value, 2) + + def test_compute_cells_take_inputs_in_right_order(self): + one = InputCell(1) + two = InputCell(2) + output = ComputeCell( + [one, two], + lambda inputs: inputs[0] + inputs[1]*10 + ) + self.assertEqual(output.value, 21) + + def test_compute_cells_update_value_when_dependencies_are_changed(self): + input_ = InputCell(1) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + + input_.value = 3 + self.assertEqual(output.value, 4) + + def test_compute_cells_can_depend_on_other_compute_cells(self): + input_ = InputCell(1) + times_two = ComputeCell([input_], lambda inputs: inputs[0] * 2) + times_thirty = ComputeCell([input_], lambda inputs: inputs[0] * 30) + output = ComputeCell( + [times_two, times_thirty], + lambda inputs: inputs[0] + inputs[1] + ) + + self.assertEqual(output.value, 32) + input_.value = 3 + self.assertEqual(output.value, 96) + + def test_compute_cells_fire_callbacks(self): + input_ = InputCell(1) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + + observer = [] + callback1 = self.callback_factory(observer) + + output.add_callback(callback1) + input_.value = 3 + self.assertEqual(observer[-1], 4) + + def test_callbacks_only_fire_on_change(self): + input_ = InputCell(1) + output = ComputeCell( + [input_], + lambda inputs: 111 if inputs[0] < 3 else 222 + ) + + observer = [] + callback1 = self.callback_factory(observer) + + output.add_callback(callback1) + input_.value = 2 + self.assertEqual(observer, []) + input_.value = 4 + self.assertEqual(observer[-1], 222) + + def test_callbacks_do_not_report_already_reported_values(self): + input_ = InputCell(1) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + + observer = [] + callback1 = self.callback_factory(observer) + + output.add_callback(callback1) + input_.value = 2 + self.assertEqual(observer[-1], 3) + input_.value = 3 + self.assertEqual(observer[-1], 4) + + def test_callbacks_can_fire_from_multiple_cells(self): + input_ = InputCell(1) + plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1) + minus_one = ComputeCell([input_], lambda inputs: inputs[0] - 1) + + cb1_observer, cb2_observer = [], [] + callback1 = self.callback_factory(cb1_observer) + callback2 = self.callback_factory(cb2_observer) + + plus_one.add_callback(callback1) + minus_one.add_callback(callback2) + input_.value = 10 + + self.assertEqual(cb1_observer[-1], 11) + self.assertEqual(cb2_observer[-1], 9) + + def test_callbacks_can_be_added_and_removed(self): + input_ = InputCell(11) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + + cb1_observer, cb2_observer, cb3_observer = [], [], [] + callback1 = self.callback_factory(cb1_observer) + callback2 = self.callback_factory(cb2_observer) + callback3 = self.callback_factory(cb3_observer) + + output.add_callback(callback1) + output.add_callback(callback2) + input_.value = 31 + self.assertEqual(cb1_observer[-1], 32) + self.assertEqual(cb2_observer[-1], 32) + + output.remove_callback(callback1) + output.add_callback(callback3) + input_.value = 41 + self.assertEqual(cb2_observer[-1], 42) + self.assertEqual(cb3_observer[-1], 42) + + # Expect callback1 not to be called. + self.assertEqual(len(cb1_observer), 1) + + def test_removing_a_callback_multiple_times(self): + """Guard against incorrect implementations which store their + callbacks in an array.""" + input_ = InputCell(1) + output = ComputeCell([input_], lambda inputs: inputs[0] + 1) + + cb1_observer, cb2_observer = [], [] + callback1 = self.callback_factory(cb1_observer) + callback2 = self.callback_factory(cb2_observer) + + output.add_callback(callback1) + output.add_callback(callback2) + output.remove_callback(callback1) + output.remove_callback(callback1) + output.remove_callback(callback1) + input_.value = 2 + + self.assertEqual(cb1_observer, []) + self.assertEqual(cb2_observer[-1], 3) + + def test_callbacks_should_only_be_called_once(self): + """Guard against incorrect implementations which call a callback + function multiple times when multiple dependencies change.""" + input_ = InputCell(1) + plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1) + minus_one1 = ComputeCell([input_], lambda inputs: inputs[0] - 1) + minus_one2 = ComputeCell([minus_one1], lambda inputs: inputs[0] - 1) + output = ComputeCell( + [plus_one, minus_one2], + lambda inputs: inputs[0] * inputs[1] + ) + + observer = [] + callback1 = self.callback_factory(observer) + + output.add_callback(callback1) + input_.value = 4 + self.assertEqual(observer[-1], 10) + + def test_callbacks_not_called_so_long_as_output_not_changed(self): + """Guard against incorrect implementations which call callbacks + if dependencies change but output value doesn't change.""" + input_ = InputCell(1) + plus_one = ComputeCell([input_], lambda inputs: inputs[0] + 1) + minus_one = ComputeCell([input_], lambda inputs: inputs[0] - 1) + always_two = ComputeCell( + [plus_one, minus_one], + lambda inputs: inputs[0] - inputs[1] + ) + + observer = [] + callback1 = self.callback_factory(observer) + + always_two.add_callback(callback1) + input_.value = 2 + input_.value = 3 + input_.value = 4 + input_.value = 5 + self.assertEqual(observer, []) + + # Utility functions. + def callback_factory(self, observer): + def callback(observer, value): + observer.append(value) + return partial(callback, observer) if __name__ == '__main__':