Mesa 3.1.0.dev0__py3-none-any.whl → 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Mesa might be problematic. Click here for more details.

Files changed (57) hide show
  1. mesa/__init__.py +3 -3
  2. mesa/agent.py +48 -0
  3. mesa/batchrunner.py +14 -1
  4. mesa/datacollection.py +1 -6
  5. mesa/examples/__init__.py +2 -2
  6. mesa/examples/advanced/epstein_civil_violence/app.py +5 -0
  7. mesa/examples/advanced/pd_grid/agents.py +2 -1
  8. mesa/examples/advanced/pd_grid/app.py +5 -0
  9. mesa/examples/advanced/pd_grid/model.py +3 -5
  10. mesa/examples/advanced/sugarscape_g1mt/agents.py +12 -65
  11. mesa/examples/advanced/sugarscape_g1mt/app.py +24 -19
  12. mesa/examples/advanced/sugarscape_g1mt/model.py +45 -52
  13. mesa/examples/advanced/wolf_sheep/agents.py +3 -1
  14. mesa/examples/advanced/wolf_sheep/model.py +17 -16
  15. mesa/examples/basic/boid_flockers/app.py +5 -0
  16. mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
  17. mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
  18. mesa/examples/basic/conways_game_of_life/app.py +5 -0
  19. mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
  20. mesa/examples/basic/schelling/agents.py +11 -5
  21. mesa/examples/basic/schelling/app.py +6 -1
  22. mesa/examples/basic/virus_on_network/app.py +5 -0
  23. mesa/experimental/__init__.py +17 -10
  24. mesa/experimental/cell_space/__init__.py +19 -7
  25. mesa/experimental/cell_space/cell.py +22 -37
  26. mesa/experimental/cell_space/cell_agent.py +12 -1
  27. mesa/experimental/cell_space/cell_collection.py +18 -3
  28. mesa/experimental/cell_space/discrete_space.py +15 -64
  29. mesa/experimental/cell_space/grid.py +74 -4
  30. mesa/experimental/cell_space/network.py +13 -1
  31. mesa/experimental/cell_space/property_layer.py +444 -0
  32. mesa/experimental/cell_space/voronoi.py +13 -1
  33. mesa/experimental/devs/__init__.py +20 -2
  34. mesa/experimental/devs/eventlist.py +19 -1
  35. mesa/experimental/devs/simulator.py +24 -8
  36. mesa/experimental/mesa_signals/__init__.py +23 -0
  37. mesa/experimental/mesa_signals/mesa_signal.py +485 -0
  38. mesa/experimental/mesa_signals/observable_collections.py +133 -0
  39. mesa/experimental/mesa_signals/signals_util.py +52 -0
  40. mesa/mesa_logging.py +190 -0
  41. mesa/model.py +17 -23
  42. mesa/visualization/__init__.py +2 -2
  43. mesa/visualization/mpl_space_drawing.py +8 -5
  44. mesa/visualization/solara_viz.py +49 -11
  45. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/METADATA +1 -1
  46. mesa-3.1.1.dist-info/RECORD +94 -0
  47. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/WHEEL +1 -1
  48. mesa/experimental/UserParam.py +0 -67
  49. mesa/experimental/components/altair.py +0 -81
  50. mesa/experimental/components/matplotlib.py +0 -242
  51. mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
  52. mesa/experimental/devs/examples/wolf_sheep.py +0 -250
  53. mesa/experimental/solara_viz.py +0 -453
  54. mesa-3.1.0.dev0.dist-info/RECORD +0 -94
  55. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/entry_points.txt +0 -0
  56. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/LICENSE +0 -0
  57. {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,485 @@
1
+ """Core implementation of Mesa's reactive programming system.
2
+
3
+ This module provides the foundational classes for Mesa's observable/reactive programming
4
+ functionality:
5
+
6
+ - BaseObservable: Abstract base class defining the interface for all observables
7
+ - Observable: Main class for creating observable properties that emit change signals
8
+ - Computable: Class for properties that automatically update based on other observables
9
+ - HasObservables: Mixin class that enables an object to contain and manage observables
10
+ - All: Helper class for subscribing to all signals from an observable
11
+
12
+ The module implements a robust reactive system where changes to observable properties
13
+ automatically trigger updates to dependent computed values and notify subscribed
14
+ observers. This enables building models with complex interdependencies while maintaining
15
+ clean separation of concerns.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import contextlib
21
+ import functools
22
+ import weakref
23
+ from abc import ABC, abstractmethod
24
+ from collections import defaultdict, namedtuple
25
+ from collections.abc import Callable
26
+ from typing import Any
27
+
28
+ from mesa.experimental.mesa_signals.signals_util import AttributeDict, create_weakref
29
+
30
+ __all__ = ["All", "Computable", "HasObservables", "Observable"]
31
+
32
+ _hashable_signal = namedtuple("_HashableSignal", "instance name")
33
+
34
+ CURRENT_COMPUTED: Computed | None = None # the current Computed that is evaluating
35
+ PROCESSING_SIGNALS: set[tuple[str,]] = set()
36
+
37
+
38
+ class BaseObservable(ABC):
39
+ """Abstract base class for all Observables."""
40
+
41
+ @abstractmethod
42
+ def __init__(self, fallback_value=None):
43
+ """Initialize a BaseObservable."""
44
+ super().__init__()
45
+ self.public_name: str
46
+ self.private_name: str
47
+
48
+ # fixme can we make this an inner class enum?
49
+ # or some SignalTypes helper class?
50
+ # its even more complicated. Ideally you can define
51
+ # signal_types throughout the class hierarchy and they are just
52
+ # combined together.
53
+ # while we also want to make sure that any signal being emitted is valid for that class
54
+ self.signal_types: set = set()
55
+ self.fallback_value = fallback_value
56
+
57
+ def __get__(self, instance: HasObservables, owner):
58
+ value = getattr(instance, self.private_name)
59
+
60
+ if CURRENT_COMPUTED is not None:
61
+ # there is a computed dependent on this Observable, so let's add
62
+ # this Observable as a parent
63
+ CURRENT_COMPUTED._add_parent(instance, self.public_name, value)
64
+
65
+ # fixme, this can be done more cleanly
66
+ # problem here is that we cannot use self (i.e., the observable), we need to add the instance as well
67
+ PROCESSING_SIGNALS.add(_hashable_signal(instance, self.public_name))
68
+
69
+ return value
70
+
71
+ def __set_name__(self, owner: HasObservables, name: str):
72
+ self.public_name = name
73
+ self.private_name = f"_{name}"
74
+ # owner.register_observable(self)
75
+
76
+ @abstractmethod
77
+ def __set__(self, instance: HasObservables, value):
78
+ # this only emits an on change signal, subclasses need to specify
79
+ # this in more detail
80
+ instance.notify(
81
+ self.public_name,
82
+ getattr(instance, self.private_name, self.fallback_value),
83
+ value,
84
+ "change",
85
+ )
86
+
87
+ def __str__(self):
88
+ return f"{self.__class__.__name__}: {self.public_name}"
89
+
90
+
91
+ class Observable(BaseObservable):
92
+ """Observable class."""
93
+
94
+ def __init__(self, fallback_value=None):
95
+ """Initialize an Observable."""
96
+ super().__init__(fallback_value=fallback_value)
97
+
98
+ self.signal_types: set = {
99
+ "change",
100
+ }
101
+
102
+ def __set__(self, instance: HasObservables, value): # noqa D103
103
+ if (
104
+ CURRENT_COMPUTED is not None
105
+ and _hashable_signal(instance, self.public_name) in PROCESSING_SIGNALS
106
+ ):
107
+ raise ValueError(
108
+ f"cyclical dependency detected: Computed({CURRENT_COMPUTED.name}) tries to change "
109
+ f"{instance.__class__.__name__}.{self.public_name} while also being dependent it"
110
+ )
111
+
112
+ super().__set__(instance, value) # send the notify
113
+ setattr(instance, self.private_name, value)
114
+
115
+ PROCESSING_SIGNALS.clear() # we have notified our children, so we can clear this out
116
+
117
+
118
+ class Computable(BaseObservable):
119
+ """A Computable that is depended on one or more Observables.
120
+
121
+ .. code-block:: python
122
+
123
+ class MyAgent(Agent):
124
+ wealth = Computable()
125
+
126
+ def __init__(self, model):
127
+ super().__init__(model)
128
+ wealth = Computed(func, args, kwargs)
129
+
130
+ """
131
+
132
+ # fixme, with new _register_observable thing
133
+ # we can do computed without a descriptor, but then you
134
+ # don't have attribute like access, you would need to do a call operation to get the value
135
+
136
+ def __init__(self):
137
+ """Initialize a Computable."""
138
+ super().__init__()
139
+
140
+ # fixme have 2 signal: change and is_dirty?
141
+ self.signal_types: set = {
142
+ "change",
143
+ }
144
+
145
+ def __get__(self, instance, owner): # noqa: D105
146
+ computed = getattr(instance, self.private_name)
147
+ old_value = computed._value
148
+
149
+ if CURRENT_COMPUTED is not None:
150
+ CURRENT_COMPUTED._add_parent(instance, self.public_name, old_value)
151
+
152
+ new_value = computed()
153
+
154
+ if new_value != old_value:
155
+ instance.notify(
156
+ self.public_name,
157
+ old_value,
158
+ new_value,
159
+ "change",
160
+ )
161
+ return new_value
162
+ else:
163
+ return old_value
164
+
165
+ def __set__(self, instance: HasObservables, value: Computed): # noqa D103
166
+ if not isinstance(value, Computed):
167
+ raise ValueError("value has to be a Computable instance")
168
+
169
+ setattr(instance, self.private_name, value)
170
+ value.name = self.public_name
171
+ value.owner = instance
172
+ getattr(
173
+ instance, self.public_name
174
+ ) # force evaluation of the computed to build the dependency graph
175
+
176
+
177
+ class Computed:
178
+ def __init__(self, func: Callable, *args, **kwargs):
179
+ self.func = func
180
+ self.args = args
181
+ self.kwargs = kwargs
182
+ self._is_dirty = True
183
+ self._first = True
184
+ self._value = None
185
+ self.name: str = "" # set by Computable
186
+ self.owner: HasObservables # set by Computable
187
+
188
+ self.parents: weakref.WeakKeyDictionary[HasObservables, dict[str, Any]] = (
189
+ weakref.WeakKeyDictionary()
190
+ )
191
+
192
+ def __str__(self):
193
+ return f"COMPUTED: {self.name}"
194
+
195
+ def _set_dirty(self, signal):
196
+ if not self._is_dirty:
197
+ self._is_dirty = True
198
+ self.owner.notify(self.name, self._value, None, "change")
199
+
200
+ def _add_parent(
201
+ self, parent: HasObservables, name: str, current_value: Any
202
+ ) -> None:
203
+ """Add a parent Observable.
204
+
205
+ Args:
206
+ parent: the HasObservable instance to which the Observable belongs
207
+ name: the public name of the Observable
208
+ current_value: the current value of the Observable
209
+
210
+ """
211
+ parent.observe(name, All(), self._set_dirty)
212
+
213
+ try:
214
+ self.parents[parent][name] = current_value
215
+ except KeyError:
216
+ self.parents[parent] = {name: current_value}
217
+
218
+ def _remove_parents(self):
219
+ """Remove all parent Observables."""
220
+ # we can unsubscribe from everything on each parent
221
+ for parent in self.parents:
222
+ parent.unobserve(All(), All(), self._set_dirty)
223
+
224
+ def __call__(self):
225
+ global CURRENT_COMPUTED # noqa: PLW0603
226
+
227
+ if self._is_dirty:
228
+ changed = False
229
+
230
+ if self._first:
231
+ # fixme might be a cleaner solution for this
232
+ # basically, we have no parents.
233
+ changed = True
234
+ self._first = False
235
+
236
+ # we might be dirty but values might have changed
237
+ # back and forth in our parents so let's check to make sure we
238
+ # really need to recalculate
239
+ if not changed:
240
+ for parent in self.parents.keyrefs():
241
+ # does parent still exist?
242
+ if parent := parent():
243
+ # if yes, compare old and new values for all
244
+ # tracked observables on this parent
245
+ for name, old_value in self.parents[parent].items():
246
+ new_value = getattr(parent, name)
247
+ if new_value != old_value:
248
+ changed = True
249
+ break # we need to recalculate
250
+ else:
251
+ # trick for breaking cleanly out of nested for loops
252
+ # see https://stackoverflow.com/questions/653509/breaking-out-of-nested-loops
253
+ continue
254
+ break
255
+ else:
256
+ # one of our parents no longer exists
257
+ changed = True
258
+ break
259
+
260
+ if changed:
261
+ # the dependencies of the computable function might have changed
262
+ # so, we rebuilt
263
+ self._remove_parents()
264
+
265
+ old = CURRENT_COMPUTED
266
+ CURRENT_COMPUTED = self
267
+
268
+ try:
269
+ self._value = self.func(*self.args, **self.kwargs)
270
+ except Exception as e:
271
+ raise e
272
+ finally:
273
+ CURRENT_COMPUTED = old
274
+
275
+ self._is_dirty = False
276
+
277
+ return self._value
278
+
279
+
280
+ class All:
281
+ """Helper constant to subscribe to all Observables."""
282
+
283
+ def __init__(self): # noqa: D107
284
+ self.name = "all"
285
+
286
+ def __copy__(self): # noqa: D105
287
+ return self
288
+
289
+ def __deepcopy__(self, memo): # noqa: D105
290
+ return self
291
+
292
+
293
+ class HasObservables:
294
+ """HasObservables class."""
295
+
296
+ # we can't use a weakset here because it does not handle bound methods correctly
297
+ # also, a list is faster for our use case
298
+ subscribers: dict[str, dict[str, list]]
299
+ observables: dict[str, set[str]]
300
+
301
+ def __init__(self, *args, **kwargs) -> None:
302
+ """Initialize a HasObservables."""
303
+ super().__init__(*args, **kwargs)
304
+ self.subscribers = defaultdict(functools.partial(defaultdict, list))
305
+ self.observables = dict(descriptor_generator(self))
306
+
307
+ def _register_signal_emitter(self, name: str, signal_types: set[str]):
308
+ """Helper function to register an Observable.
309
+
310
+ This method can be used to register custom signals that are emitted by
311
+ the class for a given attribute, but which cannot be covered by the Observable descriptor
312
+
313
+ Args:
314
+ name: the name of the signal emitter
315
+ signal_types: the set of signals that might be emitted
316
+
317
+ """
318
+ self.observables[name] = signal_types
319
+
320
+ def observe(
321
+ self,
322
+ name: str | All,
323
+ signal_type: str | All,
324
+ handler: Callable,
325
+ ):
326
+ """Subscribe to the Observable <name> for signal_type.
327
+
328
+ Args:
329
+ name: name of the Observable to subscribe to
330
+ signal_type: the type of signal on the Observable to subscribe to
331
+ handler: the handler to call
332
+
333
+ Raises:
334
+ ValueError: if the Observable <name> is not registered or if the Observable
335
+ does not emit the given signal_type
336
+
337
+ """
338
+ # fixme should name/signal_type also take a list of str?
339
+ if not isinstance(name, All):
340
+ if name not in self.observables:
341
+ raise ValueError(
342
+ f"you are trying to subscribe to {name}, but this Observable is not known"
343
+ )
344
+ else:
345
+ names = [
346
+ name,
347
+ ]
348
+ else:
349
+ names = self.observables.keys()
350
+
351
+ for name in names:
352
+ if not isinstance(signal_type, All):
353
+ if signal_type not in self.observables[name]:
354
+ raise ValueError(
355
+ f"you are trying to subscribe to a signal of {signal_type} "
356
+ f"on Observable {name}, which does not emit this signal_type"
357
+ )
358
+ else:
359
+ signal_types = [
360
+ signal_type,
361
+ ]
362
+ else:
363
+ signal_types = self.observables[name]
364
+
365
+ ref = create_weakref(handler)
366
+ for signal_type in signal_types:
367
+ self.subscribers[name][signal_type].append(ref)
368
+
369
+ def unobserve(self, name: str | All, signal_type: str | All, handler: Callable):
370
+ """Unsubscribe to the Observable <name> for signal_type.
371
+
372
+ Args:
373
+ name: name of the Observable to unsubscribe from
374
+ signal_type: the type of signal on the Observable to unsubscribe to
375
+ handler: the handler that is unsubscribing
376
+
377
+ """
378
+ names = (
379
+ [
380
+ name,
381
+ ]
382
+ if not isinstance(name, All)
383
+ else self.observables.keys()
384
+ )
385
+
386
+ for name in names:
387
+ # we need to do this here because signal types might
388
+ # differ for name so for each name we need to check
389
+ if isinstance(signal_type, All):
390
+ signal_types = self.observables[name]
391
+ else:
392
+ signal_types = [
393
+ signal_type,
394
+ ]
395
+ for signal_type in signal_types:
396
+ with contextlib.suppress(KeyError):
397
+ remaining = []
398
+ for ref in self.subscribers[name][signal_type]:
399
+ if subscriber := ref(): # noqa: SIM102
400
+ if subscriber != handler:
401
+ remaining.append(ref)
402
+ self.subscribers[name][signal_type] = remaining
403
+
404
+ def clear_all_subscriptions(self, name: str | All):
405
+ """Clears all subscriptions for the observable <name>.
406
+
407
+ if name is All, all subscriptions are removed
408
+
409
+ Args:
410
+ name: name of the Observable to unsubscribe for all signal types
411
+
412
+ """
413
+ if not isinstance(name, All):
414
+ with contextlib.suppress(KeyError):
415
+ del self.subscribers[name]
416
+ # ignore when unsubscribing to Observables that have no subscription
417
+ else:
418
+ self.subscribers = defaultdict(functools.partial(defaultdict, list))
419
+
420
+ def notify(
421
+ self,
422
+ observable: str,
423
+ old_value: Any,
424
+ new_value: Any,
425
+ signal_type: str,
426
+ **kwargs,
427
+ ):
428
+ """Emit a signal.
429
+
430
+ Args:
431
+ observable: the public name of the observable emitting the signal
432
+ old_value: the old value of the observable
433
+ new_value: the new value of the observable
434
+ signal_type: the type of signal to emit
435
+ kwargs: additional keyword arguments to include in the signal
436
+
437
+ """
438
+ signal = AttributeDict(
439
+ name=observable,
440
+ old=old_value,
441
+ new=new_value,
442
+ owner=self,
443
+ type=signal_type,
444
+ **kwargs,
445
+ )
446
+
447
+ self._mesa_notify(signal)
448
+
449
+ def _mesa_notify(self, signal: AttributeDict):
450
+ """Send out the signal.
451
+
452
+ Args:
453
+ signal: the signal
454
+
455
+ Notes:
456
+ signal must contain name and type attributes because this is how observers are stored.
457
+
458
+ """
459
+ # we put this into a helper method, so we can emit signals with other fields
460
+ # then the default ones in notify.
461
+ observable = signal.name
462
+ signal_type = signal.type
463
+
464
+ # because we are using a list of subscribers
465
+ # we should update this list to subscribers that are still alive
466
+ observers = self.subscribers[observable][signal_type]
467
+ active_observers = []
468
+ for observer in observers:
469
+ if active_observer := observer():
470
+ active_observer(signal)
471
+ active_observers.append(observer)
472
+ # use iteration to also remove inactive observers
473
+ self.subscribers[observable][signal_type] = active_observers
474
+
475
+
476
+ def descriptor_generator(obj) -> [str, BaseObservable]:
477
+ """Yield the name and signal_types for each Observable defined on obj."""
478
+ # we need to traverse the entire class hierarchy to properly get
479
+ # also observables defined in super classes
480
+ for base in type(obj).__mro__:
481
+ base_dict = vars(base)
482
+
483
+ for entry in base_dict.values():
484
+ if isinstance(entry, BaseObservable):
485
+ yield entry.public_name, entry.signal_types
@@ -0,0 +1,133 @@
1
+ """Observable collection types that emit signals when modified.
2
+
3
+ This module extends Mesa's reactive programming capabilities to collection types like
4
+ lists. Observable collections emit signals when items are added, removed, or modified,
5
+ allowing other components to react to changes in the collection's contents.
6
+
7
+ The module provides:
8
+ - ObservableList: A list descriptor that emits signals on modifications
9
+ - SignalingList: The underlying list implementation that manages signal emission
10
+
11
+ These classes enable building models where components need to track and react to
12
+ changes in collections of agents, resources, or other model elements.
13
+ """
14
+
15
+ from collections.abc import Iterable, MutableSequence
16
+ from typing import Any
17
+
18
+ from .mesa_signal import BaseObservable, HasObservables
19
+
20
+ __all__ = [
21
+ "ObservableList",
22
+ ]
23
+
24
+
25
+ class ObservableList(BaseObservable):
26
+ """An ObservableList that emits signals on changes to the underlying list."""
27
+
28
+ def __init__(self):
29
+ """Initialize the ObservableList."""
30
+ super().__init__()
31
+ self.signal_types: set = {"remove", "replace", "change", "insert", "append"}
32
+ self.fallback_value = []
33
+
34
+ def __set__(self, instance: "HasObservables", value: Iterable):
35
+ """Set the value of the descriptor attribute.
36
+
37
+ Args:
38
+ instance: The instance on which to set the attribute.
39
+ value: The value to set the attribute to.
40
+
41
+ """
42
+ super().__set__(instance, value)
43
+ setattr(
44
+ instance,
45
+ self.private_name,
46
+ SignalingList(value, instance, self.public_name),
47
+ )
48
+
49
+
50
+ class SignalingList(MutableSequence[Any]):
51
+ """A basic lists that emits signals on changes."""
52
+
53
+ __slots__ = ["data", "name", "owner"]
54
+
55
+ def __init__(self, iterable: Iterable, owner: HasObservables, name: str):
56
+ """Initialize a SignalingList.
57
+
58
+ Args:
59
+ iterable: initial values in the list
60
+ owner: the HasObservables instance on which this list is defined
61
+ name: the attribute name to which this list is assigned
62
+
63
+ """
64
+ self.owner: HasObservables = owner
65
+ self.name: str = name
66
+ self.data = list(iterable)
67
+
68
+ def __setitem__(self, index: int, value: Any) -> None:
69
+ """Set item to index.
70
+
71
+ Args:
72
+ index: the index to set item to
73
+ value: the item to set
74
+
75
+ """
76
+ old_value = self.data[index]
77
+ self.data[index] = value
78
+ self.owner.notify(self.name, old_value, value, "replace", index=index)
79
+
80
+ def __delitem__(self, index: int) -> None:
81
+ """Delete item at index.
82
+
83
+ Args:
84
+ index: The index of the item to remove
85
+
86
+ """
87
+ old_value = self.data
88
+ del self.data[index]
89
+ self.owner.notify(self.name, old_value, None, "remove", index=index)
90
+
91
+ def __getitem__(self, index) -> Any:
92
+ """Get item at index.
93
+
94
+ Args:
95
+ index: The index of the item to retrieve
96
+
97
+ Returns:
98
+ the item at index
99
+ """
100
+ return self.data[index]
101
+
102
+ def __len__(self) -> int:
103
+ """Return the length of the list."""
104
+ return len(self.data)
105
+
106
+ def insert(self, index, value):
107
+ """Insert value at index.
108
+
109
+ Args:
110
+ index: the index to insert value into
111
+ value: the value to insert
112
+
113
+ """
114
+ self.data.insert(index, value)
115
+ self.owner.notify(self.name, None, value, "insert", index=index)
116
+
117
+ def append(self, value):
118
+ """Insert value at index.
119
+
120
+ Args:
121
+ index: the index to insert value into
122
+ value: the value to insert
123
+
124
+ """
125
+ index = len(self.data)
126
+ self.data.append(value)
127
+ self.owner.notify(self.name, None, value, "append", index=index)
128
+
129
+ def __str__(self):
130
+ return self.data.__str__()
131
+
132
+ def __repr__(self):
133
+ return self.data.__repr__()
@@ -0,0 +1,52 @@
1
+ """Utility functions and classes for Mesa's signals implementation.
2
+
3
+ This module provides helper functionality used by Mesa's reactive programming system:
4
+
5
+ - AttributeDict: A dictionary subclass that allows attribute-style access to its contents
6
+ - create_weakref: Helper function to properly create weak references to different types
7
+
8
+ These utilities support the core signals implementation by providing reference
9
+ management and convenient data structures used throughout the reactive system.
10
+ """
11
+
12
+ import weakref
13
+
14
+ __all__ = [
15
+ "AttributeDict",
16
+ "create_weakref",
17
+ ]
18
+
19
+
20
+ class AttributeDict(dict):
21
+ """A dict with attribute like access.
22
+
23
+ Each value can be accessed as if it were an attribute with its key as attribute name
24
+
25
+ """
26
+
27
+ # I want our signals to act like traitlet signals, so this is inspired by trailets Bunch
28
+ # and some stack overflow posts.
29
+ __setattr__ = dict.__setitem__
30
+ __delattr__ = dict.__delitem__
31
+
32
+ def __getattr__(self, key): # noqa: D105
33
+ try:
34
+ return self.__getitem__(key)
35
+ except KeyError as e:
36
+ # we need to go from key error to attribute error
37
+ raise AttributeError(key) from e
38
+
39
+ def __dir__(self): # noqa: D105
40
+ # allows us to easily access all defined attributes
41
+ names = dir({})
42
+ names.extend(self.keys())
43
+ return names
44
+
45
+
46
+ def create_weakref(item, callback=None):
47
+ """Helper function to create a correct weakref for any item."""
48
+ if hasattr(item, "__self__"):
49
+ ref = weakref.WeakMethod(item, callback)
50
+ else:
51
+ ref = weakref.ref(item, callback)
52
+ return ref