Mesa 3.0.3__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.
- mesa/__init__.py +4 -6
- mesa/agent.py +48 -25
- mesa/batchrunner.py +14 -1
- mesa/datacollection.py +1 -6
- mesa/examples/__init__.py +2 -2
- mesa/examples/advanced/epstein_civil_violence/app.py +5 -0
- mesa/examples/advanced/pd_grid/app.py +5 -0
- mesa/examples/advanced/sugarscape_g1mt/app.py +7 -2
- mesa/examples/basic/boid_flockers/app.py +5 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
- mesa/examples/basic/conways_game_of_life/app.py +5 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
- mesa/examples/basic/schelling/app.py +5 -0
- mesa/examples/basic/virus_on_network/app.py +5 -0
- mesa/experimental/__init__.py +17 -10
- mesa/experimental/cell_space/__init__.py +19 -7
- mesa/experimental/cell_space/cell.py +22 -37
- mesa/experimental/cell_space/cell_agent.py +12 -1
- mesa/experimental/cell_space/cell_collection.py +14 -1
- mesa/experimental/cell_space/discrete_space.py +15 -64
- mesa/experimental/cell_space/grid.py +74 -4
- mesa/experimental/cell_space/network.py +13 -1
- mesa/experimental/cell_space/property_layer.py +444 -0
- mesa/experimental/cell_space/voronoi.py +13 -1
- mesa/experimental/devs/__init__.py +20 -2
- mesa/experimental/devs/eventlist.py +19 -1
- mesa/experimental/devs/simulator.py +24 -8
- mesa/experimental/mesa_signals/__init__.py +23 -0
- mesa/experimental/mesa_signals/mesa_signal.py +485 -0
- mesa/experimental/mesa_signals/observable_collections.py +133 -0
- mesa/experimental/mesa_signals/signals_util.py +52 -0
- mesa/mesa_logging.py +190 -0
- mesa/model.py +17 -74
- mesa/visualization/__init__.py +2 -2
- mesa/visualization/mpl_space_drawing.py +2 -2
- mesa/visualization/solara_viz.py +12 -0
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/METADATA +3 -4
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/RECORD +43 -44
- mesa/experimental/UserParam.py +0 -67
- mesa/experimental/components/altair.py +0 -81
- mesa/experimental/components/matplotlib.py +0 -242
- mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
- mesa/experimental/devs/examples/wolf_sheep.py +0 -250
- mesa/experimental/solara_viz.py +0 -453
- mesa/time.py +0 -391
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/WHEEL +0 -0
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.0.3.dist-info → mesa-3.1.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
"""
|
|
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
|
-
"""
|
|
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", "
|
|
24
|
+
__all__ = ["ABMSimulator", "DEVSimulator", "Priority", "SimulationEvent"]
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
"""
|
|
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
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
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
|