Skip to content

Commit 4b2ec6a

Browse files
committed
updates to tests
1 parent 5e9e15f commit 4b2ec6a

File tree

2 files changed

+68
-23
lines changed

2 files changed

+68
-23
lines changed

mesa/experimental/mesa_signals/mesa_signal.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,15 @@ def __init__(self):
118118
"""Initialize a Computable."""
119119
super().__init__()
120120

121+
# fixme have 2 signal: change and is_dirty?
122+
self.signal_types: set = {
123+
"change",
124+
}
125+
121126
def __get__(self, instance, owner): # noqa: D105
122127
computed = getattr(instance, self.private_name)
123128
old_value = computed._value
124129

125-
# fixme, we are not detecting if one computable is dependent on another
126130
if CURRENT_COMPUTED is not None:
127131
CURRENT_COMPUTED._add_parent(instance, self.public_name, old_value)
128132

@@ -139,11 +143,14 @@ def __get__(self, instance, owner): # noqa: D105
139143
else:
140144
return old_value
141145

142-
def __set__(self, instance: HasObservables, value): # noqa D103
143-
# no on change event?
146+
def __set__(self, instance: HasObservables, value: Computed): # noqa D103
147+
if not isinstance(value, Computed):
148+
raise ValueError("value has to be a Computable instance")
149+
144150
setattr(instance, self.private_name, value)
145151
value.name = self.public_name
146152
value.owner = instance
153+
getattr(instance, self.public_name) # force evaluation of the computed to build the dependency graph
147154

148155

149156
class Computed:
@@ -191,7 +198,7 @@ def _remove_parents(self):
191198
"""Remove all parent Observables."""
192199
# we can unsubscribe from everything on each parent
193200
for parent in self.parents:
194-
parent.unobserve(All(), All())
201+
parent.unobserve(All(), All(), self._set_dirty)
195202

196203
def __call__(self):
197204
global CURRENT_COMPUTED # noqa: PLW0603
@@ -208,25 +215,26 @@ def __call__(self):
208215
# we might be dirty but values might have changed
209216
# back and forth in our parents so let's check to make sure we
210217
# really need to recalculate
211-
for parent in self.parents.keyrefs():
212-
# does parent still exist?
213-
if parent := parent():
214-
# if yes, compare old and new values for all
215-
# tracked observables on this parent
216-
for name, old_value in self.parents[parent].items():
217-
new_value = getattr(parent, name)
218-
if new_value != old_value:
219-
changed = True
220-
break # we need to recalculate
218+
if not changed:
219+
for parent in self.parents.keyrefs():
220+
# does parent still exist?
221+
if parent := parent():
222+
# if yes, compare old and new values for all
223+
# tracked observables on this parent
224+
for name, old_value in self.parents[parent].items():
225+
new_value = getattr(parent, name)
226+
if new_value != old_value:
227+
changed = True
228+
break # we need to recalculate
229+
else:
230+
# trick for breaking cleanly out of nested for loops
231+
# see https://stackoverflow.com/questions/653509/breaking-out-of-nested-loops
232+
continue
233+
break
221234
else:
222-
# trick for breaking cleanly out of nested for loops
223-
# see https://stackoverflow.com/questions/653509/breaking-out-of-nested-loops
224-
continue
225-
break
226-
else:
227-
# one of our parents no longer exists
228-
changed = True
229-
break
235+
# one of our parents no longer exists
236+
changed = True
237+
break
230238

231239
if changed:
232240
# the dependencies of the computable function might have changed

tests/test_mesa_signals.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,41 @@ class MyAgent(Agent, HasObservables):
247247

248248
def __init__(self, model, value):
249249
super().__init__(model)
250-
some_attribute = Computed(lambda x: x, self) # noqa: F841
250+
self.some_other_attribute = value
251+
self.some_attribute = Computed(lambda x: x.some_other_attribute*2, self)
252+
253+
model = Model(seed=42)
254+
agent = MyAgent(model, 10)
255+
assert agent.some_attribute == 20
256+
257+
handler = Mock()
258+
agent.observe("some_attribute", "change", handler)
259+
agent.some_other_attribute = 9 # we change the dependency of computed
260+
handler.assert_called_once()
261+
agent.unobserve("some_attribute", "change", handler)
262+
263+
handler = Mock()
264+
agent.observe("some_attribute", "change", handler)
265+
assert agent.some_attribute == 18 # this forces a re-evaluation of the value of computed
266+
handler.assert_called_once() # and so, our change handler should be called
267+
agent.unobserve("some_attribute", "change", handler)
268+
269+
# cyclical dependencies
270+
def computed_func(agent):
271+
# this creates a cyclical dependency
272+
# our computed is dependent on o1, but also modifies o1
273+
agent.o1 = agent.o1 - 1
274+
class MyAgent(Agent, HasObservables):
275+
c1 = Computable()
276+
o1 = Observable()
277+
278+
def __init__(self, model, value):
279+
super().__init__(model)
280+
self.o1 = value
281+
self.c1 = Computed(computed_func, self)
282+
283+
model = Model(seed=42)
284+
with pytest.raises(ValueError):
285+
MyAgent(model, 10)
286+
287+
# parents disappearing

0 commit comments

Comments
 (0)