Skip to content

Commit 6d669e5

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents 43ffbed + bf76c1c commit 6d669e5

File tree

8 files changed

+127
-32
lines changed

8 files changed

+127
-32
lines changed

mesa/experimental/mesa_signals/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,23 @@
1010
when modified.
1111
"""
1212

13-
from .mesa_signal import All, Computable, Computed, HasObservables, Observable
14-
from .observable_collections import ObservableList
13+
from .mesa_signal import (
14+
All,
15+
Computable,
16+
Computed,
17+
HasObservables,
18+
Observable,
19+
SignalType,
20+
)
21+
from .observable_collections import ListSignalType, ObservableList
1522

1623
__all__ = [
1724
"All",
1825
"Computable",
1926
"Computed",
2027
"HasObservables",
28+
"ListSignalType",
2129
"Observable",
2230
"ObservableList",
31+
"SignalType",
2332
]

mesa/experimental/mesa_signals/mesa_signal.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Computable: Class for properties that automatically update based on other observables
99
- HasObservables: Mixin class that enables an object to contain and manage observables
1010
- All: Helper class for subscribing to all signals from an observable
11+
- SignalType: Enum defining the types of signals that can be emitted
1112
1213
The module implements a robust reactive system where changes to observable properties
1314
automatically trigger updates to dependent computed values and notify subscribed
@@ -23,11 +24,46 @@
2324
from abc import ABC, abstractmethod
2425
from collections import defaultdict, namedtuple
2526
from collections.abc import Callable
27+
from enum import Enum
2628
from typing import Any
2729

2830
from mesa.experimental.mesa_signals.signals_util import AttributeDict, create_weakref
2931

30-
__all__ = ["All", "Computable", "HasObservables", "Observable"]
32+
__all__ = ["All", "Computable", "HasObservables", "Observable", "SignalType"]
33+
34+
35+
class SignalType(str, Enum):
36+
"""Enumeration of signal types that observables can emit.
37+
38+
This enum provides type-safe signal type definitions with IDE autocomplete support.
39+
Inherits from str for backward compatibility with existing string-based code.
40+
41+
Attributes:
42+
CHANGE: Emitted when an observable's value changes.
43+
44+
Examples:
45+
>>> from mesa.experimental.mesa_signals import Observable, HasObservables, SignalType
46+
>>> class MyModel(HasObservables):
47+
... value = Observable()
48+
... def __init__(self):
49+
... super().__init__()
50+
... self._value = 0
51+
>>> model = MyModel()
52+
>>> model.observe("value", SignalType.CHANGE, lambda s: print(s.new))
53+
>>> model.value = 10
54+
10
55+
56+
Note:
57+
String-based signal types are still supported for backward compatibility:
58+
>>> model.observe("value", "change", handler) # Still works
59+
"""
60+
61+
CHANGE = "change"
62+
63+
def __str__(self):
64+
"""Return the string value of the signal type."""
65+
return self.value
66+
3167

3268
_hashable_signal = namedtuple("_HashableSignal", "instance name")
3369

@@ -44,14 +80,7 @@ def __init__(self, fallback_value=None):
4480
super().__init__()
4581
self.public_name: str
4682
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()
83+
self.signal_types: set[SignalType | str] = set()
5584
self.fallback_value = fallback_value
5685

5786
def __get__(self, instance: HasObservables, owner):
@@ -81,7 +110,7 @@ def __set__(self, instance: HasObservables, value):
81110
self.public_name,
82111
getattr(instance, self.private_name, self.fallback_value),
83112
value,
84-
"change",
113+
SignalType.CHANGE,
85114
)
86115

87116
def __str__(self):
@@ -95,8 +124,8 @@ def __init__(self, fallback_value=None):
95124
"""Initialize an Observable."""
96125
super().__init__(fallback_value=fallback_value)
97126

98-
self.signal_types: set = {
99-
"change",
127+
self.signal_types: set[SignalType | str] = {
128+
SignalType.CHANGE,
100129
}
101130

102131
def __set__(self, instance: HasObservables, value): # noqa D103
@@ -137,9 +166,8 @@ def __init__(self):
137166
"""Initialize a Computable."""
138167
super().__init__()
139168

140-
# fixme have 2 signal: change and is_dirty?
141-
self.signal_types: set = {
142-
"change",
169+
self.signal_types: set[SignalType | str] = {
170+
SignalType.CHANGE,
143171
}
144172

145173
def __get__(self, instance, owner): # noqa: D105

mesa/experimental/mesa_signals/observable_collections.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
allowing other components to react to changes in the collection's contents.
66
77
The module provides:
8+
- ListSignalType: Enum defining signal types for list collections
89
- ObservableList: A list descriptor that emits signals on modifications
910
- SignalingList: The underlying list implementation that manages signal emission
1011
@@ -13,22 +14,75 @@
1314
"""
1415

1516
from collections.abc import Iterable, MutableSequence
17+
from enum import Enum
1618
from typing import Any
1719

1820
from .mesa_signal import BaseObservable, HasObservables
1921

2022
__all__ = [
23+
"ListSignalType",
2124
"ObservableList",
2225
]
2326

2427

28+
class ListSignalType(str, Enum):
29+
"""Enumeration of signal types that observable lists can emit.
30+
31+
Provides list-specific signal types with IDE autocomplete and type safety.
32+
Inherits from str for backward compatibility with existing string-based code.
33+
Includes all list-specific signals (INSERT, APPEND, REMOVE, REPLACE) plus
34+
the base CHANGE signal inherited from the observable protocol.
35+
36+
Note on Design:
37+
This enum does NOT extend SignalType because Python Enums cannot be extended
38+
once they have members defined. Instead, we include CHANGE as a member here
39+
to maintain compatibility. The string inheritance provides value equality:
40+
ListSignalType.CHANGE == SignalType.CHANGE == "change" (all True).
41+
42+
Attributes:
43+
CHANGE: Emitted when the list itself is replaced/assigned.
44+
INSERT: Emitted when an item is inserted into the list.
45+
APPEND: Emitted when an item is appended to the list.
46+
REMOVE: Emitted when an item is removed from the list.
47+
REPLACE: Emitted when an item is replaced/modified in the list.
48+
49+
Examples:
50+
>>> from mesa.experimental.mesa_signals import ObservableList, HasObservables, ListSignalType
51+
>>> class MyModel(HasObservables):
52+
... items = ObservableList()
53+
... def __init__(self):
54+
... super().__init__()
55+
... self.items = []
56+
>>> model = MyModel()
57+
>>> model.observe("items", ListSignalType.INSERT, lambda s: print(f"Inserted {s.new}"))
58+
>>> model.items.insert(0, "first")
59+
Inserted first
60+
61+
Note:
62+
String-based signal types are still supported for backward compatibility:
63+
>>> model.observe("items", "insert", handler) # Still works
64+
Also compatible with SignalType.CHANGE since both equal "change" as strings.
65+
"""
66+
67+
CHANGE = "change"
68+
INSERT = "insert"
69+
APPEND = "append"
70+
REMOVE = "remove"
71+
REPLACE = "replace"
72+
73+
def __str__(self):
74+
"""Return the string value of the signal type."""
75+
return self.value
76+
77+
2578
class ObservableList(BaseObservable):
2679
"""An ObservableList that emits signals on changes to the underlying list."""
2780

2881
def __init__(self):
2982
"""Initialize the ObservableList."""
3083
super().__init__()
31-
self.signal_types: set = {"remove", "replace", "change", "insert", "append"}
84+
# Use all members of ListSignalType enum
85+
self.signal_types: set = set(ListSignalType)
3286
self.fallback_value = []
3387

3488
def __set__(self, instance: "HasObservables", value: Iterable):
@@ -75,7 +129,9 @@ def __setitem__(self, index: int, value: Any) -> None:
75129
"""
76130
old_value = self.data[index]
77131
self.data[index] = value
78-
self.owner.notify(self.name, old_value, value, "replace", index=index)
132+
self.owner.notify(
133+
self.name, old_value, value, ListSignalType.REPLACE, index=index
134+
)
79135

80136
def __delitem__(self, index: int) -> None:
81137
"""Delete item at index.
@@ -86,7 +142,9 @@ def __delitem__(self, index: int) -> None:
86142
"""
87143
old_value = self.data
88144
del self.data[index]
89-
self.owner.notify(self.name, old_value, None, "remove", index=index)
145+
self.owner.notify(
146+
self.name, old_value, None, ListSignalType.REMOVE, index=index
147+
)
90148

91149
def __getitem__(self, index) -> Any:
92150
"""Get item at index.
@@ -112,7 +170,7 @@ def insert(self, index, value):
112170
113171
"""
114172
self.data.insert(index, value)
115-
self.owner.notify(self.name, None, value, "insert", index=index)
173+
self.owner.notify(self.name, None, value, ListSignalType.INSERT, index=index)
116174

117175
def append(self, value):
118176
"""Insert value at index.
@@ -124,7 +182,7 @@ def append(self, value):
124182
"""
125183
index = len(self.data)
126184
self.data.append(value)
127-
self.owner.notify(self.name, None, value, "append", index=index)
185+
self.owner.notify(self.name, None, value, ListSignalType.APPEND, index=index)
128186

129187
def __str__(self):
130188
return self.data.__str__()

mesa/visualization/backends/altair_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def collect_agent_data(
108108
if isinstance(portray_input, dict):
109109
warnings.warn(
110110
(
111-
"Returning a dict from agent_portrayal is deprecated. "
111+
"Returning a dict from agent_portrayal is deprecated and will be removed in Mesa 4.0. "
112112
"Please return an AgentPortrayalStyle instance instead. "
113113
"For more information, refer to the migration guide: "
114114
"https://mesa.readthedocs.io/latest/migration_guide.html#defining-portrayal-components"

mesa/visualization/backends/matplotlib_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def collect_agent_data(self, space, agent_portrayal, default_size=None):
110110
if isinstance(portray_input, dict):
111111
warnings.warn(
112112
(
113-
"Returning a dict from agent_portrayal is deprecated. "
113+
"Returning a dict from agent_portrayal is deprecated and will be removed in Mesa 4.0. "
114114
"Please return an AgentPortrayalStyle instance instead. "
115115
"For more information, refer to the migration guide: "
116116
"https://mesa.readthedocs.io/latest/migration_guide.html#defining-portrayal-components"

mesa/visualization/mpl_space_drawing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def get_agent_pos(agent, space):
105105
if isinstance(portray_input, dict):
106106
warnings.warn(
107107
(
108-
"Returning a dict from agent_portrayal is deprecated. "
108+
"Returning a dict from agent_portrayal is deprecated and will be removed in Mesa 4.0. "
109109
"Please return an AgentPortrayalStyle instance instead. "
110110
"For more information, refer to the migration guide: "
111111
"https://mesa.readthedocs.io/latest/migration_guide.html#defining-portrayal-components"
@@ -304,7 +304,7 @@ def style_callable(layer_object: Any):
304304

305305
warnings.warn(
306306
(
307-
"The propertylayer_portrayal dict is deprecated. "
307+
"The propertylayer_portrayal dict is deprecated and will be removed in Mesa 4.0. "
308308
"Please use a callable that returns a PropertyLayerStyle instance instead. "
309309
"For more information, refer to the migration guide: "
310310
"https://mesa.readthedocs.io/latest/migration_guide.html#defining-portrayal-components"

mesa/visualization/space_renderer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,9 @@ def draw_agents(self, agent_portrayal=None, **kwargs):
257257
"""
258258
if agent_portrayal is not None:
259259
warnings.warn(
260-
"Passing agent_portrayal to draw_agents() is deprecated. "
260+
"Passing agent_portrayal to draw_agents() is deprecated and will be removed in Mesa 4.0. "
261261
"Use setup_agents(agent_portrayal, **kwargs) before calling draw_agents().",
262-
PendingDeprecationWarning,
262+
FutureWarning,
263263
stacklevel=2,
264264
)
265265
self.agent_portrayal = agent_portrayal
@@ -299,9 +299,9 @@ def draw_propertylayer(self, propertylayer_portrayal=None):
299299
"""
300300
if propertylayer_portrayal is not None:
301301
warnings.warn(
302-
"Passing propertylayer_portrayal to draw_propertylayer() is deprecated. "
302+
"Passing propertylayer_portrayal to draw_propertylayer() is deprecated and will be removed in Mesa 4.0. "
303303
"Use setup_propertylayer(propertylayer_portrayal) before calling draw_propertylayer().",
304-
PendingDeprecationWarning,
304+
FutureWarning,
305305
stacklevel=2,
306306
)
307307
self.propertylayer_portrayal = propertylayer_portrayal
@@ -325,7 +325,7 @@ def style_callable(layer_object):
325325

326326
warnings.warn(
327327
(
328-
"The propertylayer_portrayal dict is deprecated. "
328+
"The propertylayer_portrayal dict is deprecated and will be removed in Mesa 4.0. "
329329
"Please use a callable that returns a PropertyLayerStyle instance instead. "
330330
"For more information, refer to the migration guide: "
331331
"https://mesa.readthedocs.io/latest/migration_guide.html#defining-portrayal-components"

tests/visualization/test_space_renderer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def test_property_layer_style_instance():
197197
sr.backend_renderer = MagicMock()
198198

199199
style = PropertyLayerStyle(color="blue")
200-
sr.draw_propertylayer(style)
200+
sr.setup_propertylayer(style).draw_propertylayer()
201201

202202
# Verify that the backend renderer's draw_propertylayer was called
203203
sr.backend_renderer.draw_propertylayer.assert_called_once()

0 commit comments

Comments
 (0)