| 1 | 1 | class InputCell: |
| 2 | 2 | def __init__(self, initial_value): |
| 3 | | self.value = None |
| 3 | self._value = initial_value |
| 4 | self.dependents = set() |
| 5 | self.callbacks = set() |
| 6 | |
| 7 | @property |
| 8 | def value(self): |
| 9 | return self._value |
| 10 | |
| 11 | @value.setter |
| 12 | def value(self, new_value): |
| 13 | self.set_value(new_value) |
| 14 | |
| 15 | def set_value(self, new_value): |
| 16 | # Edge Case: Setting same value should not trigger updates |
| 17 | if self._value == new_value: |
| 18 | return |
| 19 | |
| 20 | self._value = new_value |
| 21 | # Start a two-phase update process |
| 22 | coordinator = UpdateCoordinator() |
| 23 | coordinator.start_update() |
| 24 | # Phase 1: propagate all changes |
| 25 | self._propagate_updates() |
| 26 | # Phase 2: call callbacks after system is stable |
| 27 | coordinator.finish_update() |
| 28 | |
| 29 | def _propagate_updates(self): |
| 30 | # Create a copy of dependents to avoid modification during iteration |
| 31 | dependents_copy = self.dependents.copy() |
| 32 | for dependent in dependents_copy: |
| 33 | dependent._compute_and_propagate() |
| 34 | |
| 35 | def _add_dependent(self, dependent): |
| 36 | self.dependents.add(dependent) |
| 37 | |
| 38 | def _remove_dependent(self, dependent): |
| 39 | self.dependents.discard(dependent) |
| 40 | |
| 41 | |
| 42 | class UpdateCoordinator: |
| 43 | """Coordinates two-phase updates to ensure callbacks are only called after system stability.""" |
| 44 | _instance = None |
| 45 | |
| 46 | def __new__(cls): |
| 47 | if cls._instance is None: |
| 48 | cls._instance = super().__new__(cls) |
| 49 | cls._instance._updating = False |
| 50 | cls._instance._cells_needing_callbacks = set() |
| 51 | return cls._instance |
| 52 | |
| 53 | def start_update(self): |
| 54 | """Start a new update cycle.""" |
| 55 | self._updating = True |
| 56 | self._cells_needing_callbacks.clear() |
| 57 | |
| 58 | def finish_update(self): |
| 59 | """Finish the update cycle and call all necessary callbacks.""" |
| 60 | if self._updating: |
| 61 | # Call callbacks for all cells that need them |
| 62 | for cell in self._cells_needing_callbacks: |
| 63 | cell._call_callbacks() |
| 64 | self._updating = False |
| 65 | self._cells_needing_callbacks.clear() |
| 66 | |
| 67 | def register_cell_for_callback(self, cell): |
| 68 | """Register a cell that needs callbacks after the update cycle.""" |
| 69 | if self._updating: |
| 70 | self._cells_needing_callbacks.add(cell) |
| 4 | 71 | |
| 5 | 72 | |
| 6 | 73 | class ComputeCell: |
| 7 | 74 | def __init__(self, inputs, compute_function): |
| 8 | | self.value = None |
| 9 | | |
| 75 | self.inputs = inputs |
| 76 | self.compute_function = compute_function |
| 77 | self.callbacks = set() |
| 78 | self.dependents = set() |
| 79 | self._value = None |
| 80 | self._stable_value = None # Track the last stable value for callback decisions |
| 81 | |
| 82 | # Register this compute cell as dependent of its inputs |
| 83 | for input_cell in inputs: |
| 84 | input_cell._add_dependent(self) |
| 85 | |
| 86 | # Compute initial value |
| 87 | self._compute_value() |
| 88 | self._stable_value = self._value # Initialize stable value |
| 89 | |
| 90 | @property |
| 91 | def value(self): |
| 92 | return self._value |
| 93 | |
| 94 | def _compute_value(self): |
| 95 | # Edge Case: Compute function might raise exception, but we need to handle it gracefully |
| 96 | try: |
| 97 | new_value = self.compute_function([input_cell.value for input_cell in self.inputs]) |
| 98 | self._value = new_value |
| 99 | except Exception: |
| 100 | # In case of error in computation, we keep the previous value |
| 101 | pass |
| 102 | |
| 103 | def _compute_and_propagate(self): |
| 104 | """Compute new value and propagate to dependents (phase 1 of update).""" |
| 105 | old_value = self._value |
| 106 | |
| 107 | # Edge Case: Handle possible exceptions during computation |
| 108 | try: |
| 109 | self._compute_value() |
| 110 | except Exception: |
| 111 | return |
| 112 | |
| 113 | # If value changed from stable state, register for callbacks |
| 114 | if self._stable_value != self._value: |
| 115 | UpdateCoordinator().register_cell_for_callback(self) |
| 116 | |
| 117 | # Propagate to dependents |
| 118 | dependents_copy = self.dependents.copy() |
| 119 | for dependent in dependents_copy: |
| 120 | dependent._compute_and_propagate() |
| 121 | |
| 122 | def _call_callbacks(self): |
| 123 | """Call callbacks if value actually changed from stable state (phase 2 of update).""" |
| 124 | if self._stable_value != self._value: |
| 125 | callbacks_copy = self.callbacks.copy() |
| 126 | for callback in callbacks_copy: |
| 127 | try: |
| 128 | callback(self._value) |
| 129 | except Exception: |
| 130 | # Ignore exceptions in callbacks |
| 131 | pass |
| 132 | # Update the stable value after callbacks are called |
| 133 | self._stable_value = self._value |
| 134 | |
| 10 | 135 | def add_callback(self, callback): |
| 11 | | pass |
| 12 | | |
| 136 | # Edge Case: Allow adding same callback multiple times (but store only once) |
| 137 | self.callbacks.add(callback) |
| 138 | |
| 13 | 139 | def remove_callback(self, callback): |
| 14 | | pass |
| 15 | | |
| 140 | # Edge Case: Removing non-existent callback should not raise error |
| 141 | self.callbacks.discard(callback) |
| 142 | |
| 143 | def _add_dependent(self, dependent): |
| 144 | self.dependents.add(dependent) |
| 145 | |
| 146 | def _remove_dependent(self, dependent): |
| 147 | self.dependents.discard(dependent) |
| 148 | |
| 149 | # Handled Edge Cases: Setting same value should not trigger updates, Prevent infinite recursion in case of circular dependencies, Handle possible exceptions during computation, Handle callback modification during notification, Allow adding same callback multiple times, Removing non-existent callback should not raise error |