Skip to content

Commit

Permalink
various cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
quaquel committed Sep 27, 2024
1 parent b7c405c commit 291c721
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 29 deletions.
62 changes: 33 additions & 29 deletions mesa/experimental/signals/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

import contextlib
import functools
import itertools
import weakref
from abc import ABC, abstractmethod
from collections import defaultdict, namedtuple
from collections.abc import Callable
from typing import Any

from .signals_util import create_weakref

__all__ = ["Observable", "HasObservables", "All", "Computable"]

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

CURRENT_COMPUTED: Computed | None = None # the current Computed that is evaluating
PROCESSING_SIGNALS: set[tuple[str,]] = (
set()
) # fixme what to put here, we can't put the observable, but it is the name and has_observable combination
)


class BaseObservable(ABC):
Expand Down Expand Up @@ -76,7 +77,7 @@ class Observable(BaseObservable):
# fixme, how do we "traverse" the tree
# do we go by layer, or by branch? it seems signals goes by layer with its batch construction

def __init__(self):
def __init__(self, fallback_value=None):
"""Initialize an Observable."""
super().__init__()

Check warning on line 82 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L82

Added line #L82 was not covered by tests

Expand All @@ -85,20 +86,16 @@ def __init__(self):
self.signal_types: set = {

Check warning on line 86 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L86

Added line #L86 was not covered by tests
"on_change",
}
self.fallback_value = None # fixme, should this be user specifiable
self.fallback_value = fallback_value

Check warning on line 89 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L89

Added line #L89 was not covered by tests

def __set__(self, instance: HasObservables, value): # noqa D103
if (
CURRENT_COMPUTED is not None
and _hashable_signal(instance, self.public_name) in PROCESSING_SIGNALS
):
# fixme make cyclical dependency explict
# so CURRENT_COMPUTED tries to modified self while being dependent on self
raise ValueError(

Check warning on line 96 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L96

Added line #L96 was not covered by tests
f"cyclical dependency detected: Computed({CURRENT_COMPUTED.name}) tries to change "
f"{instance.__class__.__name__}.{self.public_name} while being dependent "
""
f"on {instance.__class__.__name__}.{self.public_name}"
f"{instance.__class__.__name__}.{self.public_name} while also being dependent it"
)

setattr(instance, self.private_name, value)

Check warning on line 101 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L101

Added line #L101 was not covered by tests
Expand All @@ -111,11 +108,6 @@ def __set__(self, instance: HasObservables, value): # noqa D103
class Computable(BaseObservable):
"""A Computable that is depended on one or more Observables.
fixme how to make this work with Descriptors?
just to it as with ObservableList and SingalingList
so have a Computable and Computed class
declare the Computable at the top
assign the Computed in the instance
.. code-block:: python
Expand All @@ -124,7 +116,7 @@ class MyAgent(Agent):
def __init__(self, model):
super().__init__(model)
wealth = some_callable, args, kwargs # wip
wealth = Computed(func, args, kwargs)
"""

Expand Down Expand Up @@ -174,7 +166,7 @@ def __init__(self, func: Callable, *args, **kwargs):
self._value = None
self.name: str = "" # set by Computable

Check warning on line 167 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L161-L167

Added lines #L161 - L167 were not covered by tests

self.parents: weakref.WeakKeyDictionary[HasObservables, dict[str], Any] = (
self.parents: weakref.WeakKeyDictionary[HasObservables, dict[str, Any]] = (

Check warning on line 169 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L169

Added line #L169 was not covered by tests
weakref.WeakKeyDictionary()
)

Expand Down Expand Up @@ -282,17 +274,17 @@ class HasObservables:
"""HasObservables class."""

observables: dict[str, BaseObservable] = {}
subscribers: dict[str, dict[str, weakref.WeakSet]]

# we can't use a weakset here because it does not handle bound methods correctly
# also, a list is faster for our use case
subscribers: dict[str, dict[str, list]]

def __new__(cls, *args, **kwargs): # noqa D102
# fixme dirty hack because super does not work on agents
obj = super().__new__(cls)

Check warning on line 284 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L284

Added line #L284 was not covered by tests

# some kind of nested dict
# we have the name of observable as a key
# we have signal_type as a key
# we want weakrefs for the callable

# subscribers is a nested defaultdict
# obj.subscribers[observable_name][signal_type] = list of weakref handlers
obj.subscribers = defaultdict(functools.partial(defaultdict, list))

Check warning on line 288 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L288

Added line #L288 was not covered by tests

return obj

Check warning on line 290 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L290

Added line #L290 was not covered by tests
Expand Down Expand Up @@ -340,7 +332,7 @@ def observe(
else:
names = self.observables.keys()

Check warning on line 333 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L333

Added line #L333 was not covered by tests

# fixme, see unsubscribe, but event types differ accross names
# fixme, see unsubscribe, but event types differ across names
if not isinstance(signal_type, All):
if signal_type not in self.observables[name].signal_types:
raise ValueError(

Check warning on line 338 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L338

Added line #L338 was not covered by tests
Expand All @@ -354,13 +346,23 @@ def observe(
else:
signal_types = self.observables[name].signal_types

Check warning on line 347 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L347

Added line #L347 was not covered by tests

for name, signal_type in itertools.product(names, signal_types):
# fixme, we might built our own weakSet that handles this internally....
if hasattr(handler, "__self__"):
ref = weakref.WeakMethod(handler)
for name in names:
if not isinstance(signal_type, All):
if signal_type not in self.observables[name].signal_types:
raise ValueError(

Check warning on line 352 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L352

Added line #L352 was not covered by tests
f"you are trying to subscribe to a signal of {signal_type}"
f"on Observable {name}, which does not emit this signal_type"
)
else:
signal_types = [

Check warning on line 357 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L357

Added line #L357 was not covered by tests
signal_type,
]
else:
ref = weakref.ref(handler)
self.subscribers[name][signal_type].append(ref)
signal_types = self.observables[name].signal_types

Check warning on line 361 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L361

Added line #L361 was not covered by tests

ref = create_weakref(handler)

Check warning on line 363 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L363

Added line #L363 was not covered by tests
for signal_type in signal_types:
self.subscribers[name][signal_type].append(ref)

Check warning on line 365 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L365

Added line #L365 was not covered by tests

def unobserve(self, name: str | All, signal_type: str | All):
"""Unsubscribe to the Observable <name> for signal_type.
Expand Down Expand Up @@ -428,6 +430,8 @@ def notify(self, observable: str, old_value: Any, new_value: Any, signal_type: s
# attribute access. This will be richer than the current Signal named tuple
signal = Signal(self, observable, old_value, new_value, signal_type)

Check warning on line 431 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L431

Added line #L431 was not covered by tests

# because we are using a list of subscribers
# we should update this list to subscribers that are still alive
observers = self.subscribers[observable][signal_type]
active_observers = []

Check warning on line 436 in mesa/experimental/signals/signal.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signal.py#L435-L436

Added lines #L435 - L436 were not covered by tests
for observer in observers:
Expand Down
12 changes: 12 additions & 0 deletions mesa/experimental/signals/signals_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import weakref

__all__ = ["create_weakref"]


def create_weakref(item, callback=None):
"""Helper function to create a correct weakref for any item"""
if hasattr(item, "__self__"):
ref = weakref.WeakMethod(item, callback)

Check warning on line 9 in mesa/experimental/signals/signals_util.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signals_util.py#L9

Added line #L9 was not covered by tests
else:
ref = weakref.ref(item, callback)
return ref

Check warning on line 12 in mesa/experimental/signals/signals_util.py

View check run for this annotation

Codecov / codecov/patch

mesa/experimental/signals/signals_util.py#L11-L12

Added lines #L11 - L12 were not covered by tests

0 comments on commit 291c721

Please sign in to comment.