Mesa 3.0.2__py3-none-any.whl → 3.1.0__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 (50) hide show
  1. mesa/__init__.py +4 -6
  2. mesa/agent.py +48 -25
  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/app.py +5 -0
  8. mesa/examples/advanced/sugarscape_g1mt/app.py +7 -2
  9. mesa/examples/basic/boid_flockers/app.py +5 -0
  10. mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
  11. mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
  12. mesa/examples/basic/conways_game_of_life/app.py +5 -0
  13. mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
  14. mesa/examples/basic/schelling/app.py +5 -0
  15. mesa/examples/basic/virus_on_network/app.py +5 -0
  16. mesa/experimental/__init__.py +17 -10
  17. mesa/experimental/cell_space/__init__.py +19 -7
  18. mesa/experimental/cell_space/cell.py +22 -37
  19. mesa/experimental/cell_space/cell_agent.py +12 -1
  20. mesa/experimental/cell_space/cell_collection.py +18 -3
  21. mesa/experimental/cell_space/discrete_space.py +15 -64
  22. mesa/experimental/cell_space/grid.py +74 -4
  23. mesa/experimental/cell_space/network.py +13 -1
  24. mesa/experimental/cell_space/property_layer.py +444 -0
  25. mesa/experimental/cell_space/voronoi.py +13 -1
  26. mesa/experimental/devs/__init__.py +20 -2
  27. mesa/experimental/devs/eventlist.py +19 -1
  28. mesa/experimental/devs/simulator.py +24 -8
  29. mesa/experimental/mesa_signals/__init__.py +23 -0
  30. mesa/experimental/mesa_signals/mesa_signal.py +485 -0
  31. mesa/experimental/mesa_signals/observable_collections.py +133 -0
  32. mesa/experimental/mesa_signals/signals_util.py +52 -0
  33. mesa/mesa_logging.py +190 -0
  34. mesa/model.py +17 -74
  35. mesa/visualization/__init__.py +2 -2
  36. mesa/visualization/mpl_space_drawing.py +2 -2
  37. mesa/visualization/solara_viz.py +23 -5
  38. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/METADATA +3 -4
  39. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/RECORD +43 -44
  40. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/WHEEL +1 -1
  41. mesa/experimental/UserParam.py +0 -67
  42. mesa/experimental/components/altair.py +0 -81
  43. mesa/experimental/components/matplotlib.py +0 -242
  44. mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
  45. mesa/experimental/devs/examples/wolf_sheep.py +0 -250
  46. mesa/experimental/solara_viz.py +0 -453
  47. mesa/time.py +0 -391
  48. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/entry_points.txt +0 -0
  49. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/licenses/LICENSE +0 -0
  50. {mesa-3.0.2.dist-info → mesa-3.1.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,4 +1,16 @@
1
- """Support for Voronoi meshed grids."""
1
+ """Cell spaces based on Voronoi tessellation around seed points.
2
+
3
+ Creates irregular spatial divisions by building cells around seed points,
4
+ where each cell contains the area closer to its seed than any other.
5
+ Features:
6
+ - Organic-looking spaces from point sets
7
+ - Automatic neighbor determination
8
+ - Area-based cell capacities
9
+ - Natural regional divisions
10
+
11
+ Useful for models requiring irregular but mathematically meaningful spatial
12
+ divisions, like territories, service areas, or natural regions.
13
+ """
2
14
 
3
15
  from collections.abc import Sequence
4
16
  from itertools import combinations
@@ -1,6 +1,24 @@
1
- """Support for event scheduling."""
1
+ """Core event management functionality for Mesa's discrete event simulation system.
2
+
3
+ This module provides the foundational data structures and classes needed for event-based
4
+ simulation in Mesa. The EventList class is a priority queue implementation that maintains
5
+ simulation events in chronological order while respecting event priorities. Key features:
6
+
7
+ - Priority-based event ordering
8
+ - Weak references to prevent memory leaks from canceled events
9
+ - Efficient event insertion and removal using a heap queue
10
+ - Support for event cancellation without breaking the heap structure
11
+
12
+ The module contains three main components:
13
+ - Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW)
14
+ - SimulationEvent: A class representing individual events with timing and execution details
15
+ - EventList: A heap-based priority queue managing the chronological ordering of events
16
+
17
+ The implementation supports both pure discrete event simulation and hybrid approaches
18
+ combining agent-based modeling with event scheduling.
19
+ """
2
20
 
3
21
  from .eventlist import Priority, SimulationEvent
4
22
  from .simulator import ABMSimulator, DEVSimulator
5
23
 
6
- __all__ = ["ABMSimulator", "DEVSimulator", "SimulationEvent", "Priority"]
24
+ __all__ = ["ABMSimulator", "DEVSimulator", "Priority", "SimulationEvent"]
@@ -1,4 +1,22 @@
1
- """Eventlist which is at the core of event scheduling."""
1
+ """Core event management functionality for Mesa's discrete event simulation system.
2
+
3
+ This module provides the foundational data structures and classes needed for event-based
4
+ simulation in Mesa. The EventList class is a priority queue implementation that maintains
5
+ simulation events in chronological order while respecting event priorities. Key features:
6
+
7
+ - Priority-based event ordering
8
+ - Weak references to prevent memory leaks from canceled events
9
+ - Efficient event insertion and removal using a heap queue
10
+ - Support for event cancellation without breaking the heap structure
11
+
12
+ The module contains three main components:
13
+ - Priority: An enumeration defining event priority levels (HIGH, DEFAULT, LOW)
14
+ - SimulationEvent: A class representing individual events with timing and execution details
15
+ - EventList: A heap-based priority queue managing the chronological ordering of events
16
+
17
+ The implementation supports both pure discrete event simulation and hybrid approaches
18
+ combining agent-based modeling with event scheduling.
19
+ """
2
20
 
3
21
  from __future__ import annotations
4
22
 
@@ -1,20 +1,36 @@
1
- """Provides several simulator classes.
2
-
3
- A Simulator is responsible for executing a simulation model. It controls time advancement and enables event scheduling.
4
-
5
-
1
+ """Simulator implementations for different time advancement approaches in Mesa.
2
+
3
+ This module provides simulator classes that control how simulation time advances and how
4
+ events are executed. It supports both discrete-time and continuous-time simulations through
5
+ three main classes:
6
+
7
+ - Simulator: Base class defining the core simulation control interface
8
+ - ABMSimulator: A simulator for agent-based models that combines fixed time steps with
9
+ event scheduling. Uses integer time units and automatically schedules model.step()
10
+ - DEVSimulator: A pure discrete event simulator using floating-point time units for
11
+ continuous time simulation
12
+
13
+ Key features:
14
+ - Flexible time units (integer or float)
15
+ - Event scheduling using absolute or relative times
16
+ - Priority-based event execution
17
+ - Support for running simulations for specific durations or until specific end times
18
+
19
+ The simulators enable Mesa models to use traditional time-step based approaches, pure
20
+ event-driven approaches, or hybrid combinations of both.
6
21
  """
7
22
 
8
23
  from __future__ import annotations
9
24
 
10
25
  import numbers
11
26
  from collections.abc import Callable
12
- from typing import Any
13
-
14
- from mesa import Model
27
+ from typing import TYPE_CHECKING, Any
15
28
 
16
29
  from .eventlist import EventList, Priority, SimulationEvent
17
30
 
31
+ if TYPE_CHECKING:
32
+ from mesa import Model
33
+
18
34
 
19
35
  class Simulator:
20
36
  """The Simulator controls the time advancement of the model.
@@ -0,0 +1,23 @@
1
+ """Mesa Signals (Observables) package that provides reactive programming capabilities.
2
+
3
+ This package enables tracking changes to properties and state in Mesa models through a
4
+ reactive programming paradigm. It enables building models where components can observe
5
+ and react to changes in other components' state.
6
+
7
+ The package provides the core Observable classes and utilities needed to implement
8
+ reactive patterns in agent-based models. This includes capabilities for watching changes
9
+ to attributes, computing derived values, and managing collections that emit signals
10
+ when modified.
11
+ """
12
+
13
+ from .mesa_signal import All, Computable, Computed, HasObservables, Observable
14
+ from .observable_collections import ObservableList
15
+
16
+ __all__ = [
17
+ "All",
18
+ "Computable",
19
+ "Computed",
20
+ "HasObservables",
21
+ "Observable",
22
+ "ObservableList",
23
+ ]
@@ -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