Repository: MLShukai/pamiq-core
Pull request #128: ThreadEventMixinを継承し、呼び出し処理を実装
Author: login: Geson-anko
Created: 2025-03-28T04:36:36Z | Merged: 2025-03-29T06:23:59Z

# Pull Request

## 内容

#124 ThreadEventMixinをinteractionモジュールとTrainerモジュールに適用しました。

<!-- ビューの変更がある場合はスクショによる比較などがあるとわかりやすい -->

## 他のコード・機能への影響

- [x] なし
- [ ] あり

<!-- ある場合は記述 -->

<!-- この関数を変更したのでこの機能にも影響がある、など -->

## Submit前の確認項目

<!-- PRをSubmitする前に確認する項目 -->

- [x] タイトルは一目でわかるようにし、説明文は PR を簡潔に説明するようにしましたか?
- [x] あなたの PR が、異なる変更を束ねたものではなく、ひとつのことだけを行うものであることを確認しましたか?
- [x] この PR で導入されたすべての変更点を記述、リストアップしましたか?
- [x] PR を`make run`コマンドでローカルでテストしましたか?

## 補足

<!-- レビューをする際に見てほしい点、ローカル環境で試す際の注意点、など -->



Diff:
diff --git a/src/pamiq_core/interaction/agent.py b/src/pamiq_core/interaction/agent.py
index 8175152a..15ff3ca5 100644
--- a/src/pamiq_core/interaction/agent.py
+++ b/src/pamiq_core/interaction/agent.py
@@ -4,11 +4,14 @@
 from pamiq_core.data import DataCollector, DataCollectorsDict
 from pamiq_core.model import InferenceModel, InferenceModelsDict
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 from .event_mixin import InteractionEventMixin
 
 
-class Agent[ObsType, ActType](ABC, InteractionEventMixin, PersistentStateMixin):
+class Agent[ObsType, ActType](
+    ABC, InteractionEventMixin, PersistentStateMixin, ThreadEventMixin
+):
     """Base agent class for decision making.
 
     An agent receives observations from an environment and decides on
diff --git a/src/pamiq_core/interaction/env.py b/src/pamiq_core/interaction/env.py
index 8b5213f0..620cb0a5 100644
--- a/src/pamiq_core/interaction/env.py
+++ b/src/pamiq_core/interaction/env.py
@@ -1,11 +1,14 @@
 from abc import ABC, abstractmethod
 
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 from .event_mixin import InteractionEventMixin
 
 
-class Environment[ObsType, ActType](ABC, InteractionEventMixin, PersistentStateMixin):
+class Environment[ObsType, ActType](
+    ABC, InteractionEventMixin, PersistentStateMixin, ThreadEventMixin
+):
     """Base environment class for agent interaction."""
 
     @abstractmethod
diff --git a/src/pamiq_core/interaction/interactions.py b/src/pamiq_core/interaction/interactions.py
index 47fe6734..4f21e238 100644
--- a/src/pamiq_core/interaction/interactions.py
+++ b/src/pamiq_core/interaction/interactions.py
@@ -2,6 +2,7 @@
 from typing import Self, override
 
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 from .agent import Agent
 from .env import Environment
@@ -9,7 +10,9 @@
 from .interval_adjustors import IntervalAdjustor, SleepIntervalAdjustor
 
 
-class Interaction[ObsType, ActType](InteractionEventMixin, PersistentStateMixin):
+class Interaction[ObsType, ActType](
+    InteractionEventMixin, PersistentStateMixin, ThreadEventMixin
+):
     """Class that combines an agent and environment to create an interaction
     loop.
 
@@ -89,6 +92,20 @@ def load_state(self, path: Path) -> None:
         self.agent.load_state(path / "agent")
         self.environment.load_state(path / "environment")
 
+    @override
+    def on_paused(self) -> None:
+        """The method to be called when the thread is paused."""
+        super().on_paused()
+        self.agent.on_paused()
+        self.environment.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """The method to be called when the thread is resumed."""
+        super().on_resumed()
+        self.agent.on_resumed()
+        self.environment.on_resumed()
+
 
 class FixedIntervalInteraction[ObsType, ActType](Interaction[ObsType, ActType]):
     """Interaction class that executes steps at fixed time intervals.
diff --git a/src/pamiq_core/interaction/modular_env.py b/src/pamiq_core/interaction/modular_env.py
index 61df31c8..14f18152 100644
--- a/src/pamiq_core/interaction/modular_env.py
+++ b/src/pamiq_core/interaction/modular_env.py
@@ -3,12 +3,13 @@
 from typing import override
 
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 from .env import Environment
 from .event_mixin import InteractionEventMixin
 
 
-class Sensor[T](ABC, InteractionEventMixin, PersistentStateMixin):
+class Sensor[T](ABC, InteractionEventMixin, PersistentStateMixin, ThreadEventMixin):
     """Abstract base class for sensors that read data from the environment.
 
     This class provides an interface for reading observations from
@@ -26,7 +27,7 @@ def read(self) -> T:
         ...
 
 
-class Actuator[T](ABC, InteractionEventMixin, PersistentStateMixin):
+class Actuator[T](ABC, InteractionEventMixin, PersistentStateMixin, ThreadEventMixin):
     """Abstract base class for actuators that affect the environment.
 
     This class provides an interface for operating actuators based on
@@ -126,3 +127,17 @@ def load_state(self, path: Path) -> None:
         """
         self.sensor.load_state(path / "sensor")
         self.actuator.load_state(path / "actuator")
+
+    @override
+    def on_paused(self) -> None:
+        """The method to be called when the thread is paused."""
+        super().on_paused()
+        self.sensor.on_paused()
+        self.actuator.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """The method to be called when the thread is resumed."""
+        super().on_resumed()
+        self.sensor.on_resumed()
+        self.actuator.on_resumed()
diff --git a/src/pamiq_core/interaction/wrappers.py b/src/pamiq_core/interaction/wrappers.py
index 21a835b2..da18ff97 100644
--- a/src/pamiq_core/interaction/wrappers.py
+++ b/src/pamiq_core/interaction/wrappers.py
@@ -7,9 +7,10 @@
 from pamiq_core.interaction.event_mixin import InteractionEventMixin
 from pamiq_core.interaction.modular_env import Actuator, Sensor
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
-class Wrapper[T, W](ABC, InteractionEventMixin, PersistentStateMixin):
+class Wrapper[T, W](ABC, InteractionEventMixin, PersistentStateMixin, ThreadEventMixin):
     """Base wrapper class for transforming values.
 
     This abstract class provides an interface for wrapping and
@@ -167,6 +168,22 @@ def load_state(self, path: Path) -> None:
         self._obs_wrapper.load_state(path / "obs_wrapper")
         self._act_wrapper.load_state(path / "act_wrapper")
 
+    @override
+    def on_paused(self) -> None:
+        """The method to be called when the thread is paused."""
+        super().on_paused()
+        self.env.on_paused()
+        self._obs_wrapper.on_paused()
+        self._act_wrapper.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """The method to be called when the thread is on_resumed."""
+        super().on_resumed()
+        self.env.on_resumed()
+        self._obs_wrapper.on_resumed()
+        self._act_wrapper.on_resumed()
+
 
 class SensorWrapper[T, W](Sensor[W]):
     """Wrapper for Sensor that transforms sensor readings.
@@ -229,6 +246,20 @@ def load_state(self, path: Path) -> None:
         self.sensor.load_state(path / "sensor")
         self._wrapper.load_state(path / "wrapper")
 
+    @override
+    def on_paused(self) -> None:
+        """The method to be called when the thread is paused."""
+        super().on_paused()
+        self.sensor.on_paused()
+        self._wrapper.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """The method to be called when the thread is resumed."""
+        super().on_resumed()
+        self.sensor.on_resumed()
+        self._wrapper.on_resumed()
+
 
 class ActuatorWrapper[T, W](Actuator[W]):
     """Wrapper for Actuator that transforms actions.
@@ -290,3 +321,17 @@ def load_state(self, path: Path) -> None:
         """
         self.actuator.load_state(path / "actuator")
         self._wrapper.load_state(path / "wrapper")
+
+    @override
+    def on_paused(self) -> None:
+        """The method to be called when the thread is paused."""
+        super().on_paused()
+        self.actuator.on_paused()
+        self._wrapper.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """The method to be called when the thread is resumed."""
+        super().on_resumed()
+        self.actuator.on_resumed()
+        self._wrapper.on_resumed()
diff --git a/src/pamiq_core/threads/__init__.py b/src/pamiq_core/threads/__init__.py
index 177dee3c..7e7b8314 100644
--- a/src/pamiq_core/threads/__init__.py
+++ b/src/pamiq_core/threads/__init__.py
@@ -3,6 +3,7 @@
     ReadOnlyController,
     ReadOnlyThreadStatus,
     ThreadController,
+    ThreadEventMixin,
     ThreadStatus,
     ThreadStatusesHandler,
 )
@@ -18,4 +19,5 @@
     "ThreadStatus",
     "ReadOnlyThreadStatus",
     "ThreadStatusesHandler",
+    "ThreadEventMixin",
 ]
diff --git a/src/pamiq_core/trainer/base.py b/src/pamiq_core/trainer/base.py
index e4764321..43682079 100644
--- a/src/pamiq_core/trainer/base.py
+++ b/src/pamiq_core/trainer/base.py
@@ -6,9 +6,10 @@
 from pamiq_core.data import DataUser, DataUsersDict
 from pamiq_core.model import TrainingModel, TrainingModelsDict
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
-class Trainer(ABC, PersistentStateMixin):
+class Trainer(ABC, PersistentStateMixin, ThreadEventMixin):
     """Abstract base trainer class.
 
     The `run` method is called repeatedly in the training thread.
diff --git a/src/pamiq_core/trainer/container.py b/src/pamiq_core/trainer/container.py
index b8560abf..6d077f31 100644
--- a/src/pamiq_core/trainer/container.py
+++ b/src/pamiq_core/trainer/container.py
@@ -5,10 +5,11 @@
 from pamiq_core.data import DataUsersDict
 from pamiq_core.model import TrainingModelsDict
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 from pamiq_core.trainer import Trainer
 
 
-class TrainersDict(OrderedDict[str, Trainer], PersistentStateMixin):
+class TrainersDict(OrderedDict[str, Trainer], PersistentStateMixin, ThreadEventMixin):
     """A container class for trainers."""
 
     def attach_training_models_dict(
@@ -43,3 +44,17 @@ def load_state(self, path: Path) -> None:
         """Load states of each trainer from the path."""
         for name, trainer in self.items():
             trainer.load_state(path / name)
+
+    @override
+    def on_paused(self) -> None:
+        """Propagate pause event to all trainers."""
+        super().on_paused()
+        for trainer in self.values():
+            trainer.on_paused()
+
+    @override
+    def on_resumed(self) -> None:
+        """Propagate resume event to all trainers."""
+        super().on_resumed()
+        for trainer in self.values():
+            trainer.on_resumed()
diff --git a/tests/pamiq_core/interaction/test_agent.py b/tests/pamiq_core/interaction/test_agent.py
index 5b43a7cd..bfdffe03 100644
--- a/tests/pamiq_core/interaction/test_agent.py
+++ b/tests/pamiq_core/interaction/test_agent.py
@@ -8,6 +8,7 @@
 from pamiq_core.interaction.event_mixin import InteractionEventMixin
 from pamiq_core.model import InferenceModel, InferenceModelsDict
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
 class AgentImpl(Agent[str, int]):
@@ -36,6 +37,7 @@ def test_agent_inherits(self):
         """Test that Agent Super Class."""
         assert issubclass(Agent, InteractionEventMixin)
         assert issubclass(Agent, PersistentStateMixin)
+        assert issubclass(Agent, ThreadEventMixin)
 
     def test_abstract_method(self):
         """Test agent's abstract method."""
diff --git a/tests/pamiq_core/interaction/test_env.py b/tests/pamiq_core/interaction/test_env.py
index 88b3d8a1..f5a119dc 100644
--- a/tests/pamiq_core/interaction/test_env.py
+++ b/tests/pamiq_core/interaction/test_env.py
@@ -1,6 +1,7 @@
 from pamiq_core.interaction.env import Environment
 from pamiq_core.interaction.event_mixin import InteractionEventMixin
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
 class TestEnvironment:
@@ -10,6 +11,7 @@ def test_environment_inherits_from_required_mixins(self):
         """Test Environment subclass."""
         assert issubclass(Environment, InteractionEventMixin)
         assert issubclass(Environment, PersistentStateMixin)
+        assert issubclass(Environment, ThreadEventMixin)
 
     def test_abstract_methods(self):
         """Test abstract methods of Environment."""
diff --git a/tests/pamiq_core/interaction/test_interactions.py b/tests/pamiq_core/interaction/test_interactions.py
index f8be5507..e29d3277 100644
--- a/tests/pamiq_core/interaction/test_interactions.py
+++ b/tests/pamiq_core/interaction/test_interactions.py
@@ -91,6 +91,20 @@ def test_load_state(
         mock_agent.load_state.assert_called_once_with(load_path / "agent")
         mock_environment.load_state.assert_called_once_with(load_path / "environment")
 
+    def test_on_paused(self, interaction: Interaction, mock_agent, mock_environment):
+        """Test that on_paused() calls on_paused on both agent and
+        environment."""
+        interaction.on_paused()
+        mock_agent.on_paused.assert_called_once_with()
+        mock_environment.on_paused.assert_called_once_with()
+
+    def test_on_resumed(self, interaction: Interaction, mock_agent, mock_environment):
+        """Test that on_resumed() calls on_resumed on both agent and
+        environment."""
+        interaction.on_resumed()
+        mock_agent.on_resumed.assert_called_once_with()
+        mock_environment.on_resumed.assert_called_once_with()
+
 
 class TestFixedIntervalInteraction:
     """Tests for FixedIntervalInteraction class."""
diff --git a/tests/pamiq_core/interaction/test_modular_env.py b/tests/pamiq_core/interaction/test_modular_env.py
index c2445f2a..e1ecf58c 100644
--- a/tests/pamiq_core/interaction/test_modular_env.py
+++ b/tests/pamiq_core/interaction/test_modular_env.py
@@ -7,6 +7,7 @@
 from pamiq_core.interaction.event_mixin import InteractionEventMixin
 from pamiq_core.interaction.modular_env import Actuator, ModularEnvironment, Sensor
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
 class TestSensor:
@@ -17,6 +18,7 @@ def test_sensor_inheritance(self):
         assert issubclass(Sensor, ABC)
         assert issubclass(Sensor, InteractionEventMixin)
         assert issubclass(Sensor, PersistentStateMixin)
+        assert issubclass(Sensor, ThreadEventMixin)
 
     def test_abstract_methods(self):
         """Test that Sensor has the correct abstract methods."""
@@ -31,6 +33,7 @@ def test_actuator_inheritance(self):
         assert issubclass(Actuator, ABC)
         assert issubclass(Actuator, InteractionEventMixin)
         assert issubclass(Actuator, PersistentStateMixin)
+        assert issubclass(Actuator, ThreadEventMixin)
 
     def test_abstract_methods(self):
         """Test that Actuator has the correct abstract methods."""
@@ -115,3 +118,17 @@ def test_load_state(self, env, mock_sensor, mock_actuator, tmp_path: Path):
 
         mock_sensor.load_state.assert_called_once_with(load_path / "sensor")
         mock_actuator.load_state.assert_called_once_with(load_path / "actuator")
+
+    def test_on_paused(self, env, mock_sensor, mock_actuator):
+        """Test that on_paused() calls on_paused on both sensor and
+        actuator."""
+        env.on_paused()
+        mock_sensor.on_paused.assert_called_once_with()
+        mock_actuator.on_paused.assert_called_once_with()
+
+    def test_on_resumed(self, env, mock_sensor, mock_actuator):
+        """Test that on_resumed() calls on_resumed on both sensor and
+        actuator."""
+        env.on_resumed()
+        mock_sensor.on_resumed.assert_called_once_with()
+        mock_actuator.on_resumed.assert_called_once_with()
diff --git a/tests/pamiq_core/interaction/test_wrappers.py b/tests/pamiq_core/interaction/test_wrappers.py
index 185c4125..a1e9ae19 100644
--- a/tests/pamiq_core/interaction/test_wrappers.py
+++ b/tests/pamiq_core/interaction/test_wrappers.py
@@ -13,6 +13,7 @@
     Wrapper,
 )
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 
 
 class WrapperImpl(Wrapper[int, str]):
@@ -30,6 +31,7 @@ def test_inheritance(self):
         """Test that Wrapper inherits from correct base classes."""
         assert issubclass(Wrapper, InteractionEventMixin)
         assert issubclass(Wrapper, PersistentStateMixin)
+        assert issubclass(Wrapper, ThreadEventMixin)
 
     def test_abstract_methods(self):
         """Test that Wrapper has the correct abstract methods."""
@@ -170,6 +172,30 @@ def test_with_function_wrappers(self, mock_env):
         env_wrapper.affect("direct_action")
         mock_env.affect.assert_called_with("func_unwrapped_direct_action")
 
+    def test_on_paused(self, env_wrapper, mock_env, obs_wrapper, act_wrapper, mocker):
+        """Test that on_paused calls on_paused on all components."""
+        # Spy on the on_paused methods
+        obs_on_paused_spy = mocker.spy(obs_wrapper, "on_paused")
+        act_on_paused_spy = mocker.spy(act_wrapper, "on_paused")
+
+        env_wrapper.on_paused()
+
+        mock_env.on_paused.assert_called_once_with()
+        obs_on_paused_spy.assert_called_once_with()
+        act_on_paused_spy.assert_called_once_with()
+
+    def test_on_resumed(self, env_wrapper, mock_env, obs_wrapper, act_wrapper, mocker):
+        """Test that on_resumed calls on_resumed on all components."""
+        # Spy on the on_resumed methods
+        obs_on_resumed_spy = mocker.spy(obs_wrapper, "on_resumed")
+        act_on_resumed_spy = mocker.spy(act_wrapper, "on_resumed")
+
+        env_wrapper.on_resumed()
+
+        mock_env.on_resumed.assert_called_once_with()
+        obs_on_resumed_spy.assert_called_once_with()
+        act_on_resumed_spy.assert_called_once_with()
+
 
 class TestSensorWrapper:
     """Test suite for the SensorWrapper class."""
@@ -254,6 +280,26 @@ def test_with_function_wrapper(self, mock_sensor):
         result = sensor_wrapper.read()
         assert result == "func_processed_raw_sensor_data"
 
+    def test_on_paused(self, sensor_wrapper, mock_sensor, wrapper, mocker):
+        """Test that on_paused calls on_paused on all components."""
+        # Spy on the on_paused method
+        wrapper_on_paused_spy = mocker.spy(wrapper, "on_paused")
+
+        sensor_wrapper.on_paused()
+
+        mock_sensor.on_paused.assert_called_once_with()
+        wrapper_on_paused_spy.assert_called_once_with()
+
+    def test_on_resumed(self, sensor_wrapper, mock_sensor, wrapper, mocker):
+        """Test that on_resumed calls on_resumed on all components."""
+        # Spy on the on_resumed method
+        wrapper_on_resumed_spy = mocker.spy(wrapper, "on_resumed")
+
+        sensor_wrapper.on_resumed()
+
+        mock_sensor.on_resumed.assert_called_once_with()
+        wrapper_on_resumed_spy.assert_called_once_with()
+
 
 class TestActuatorWrapper:
     """Test suite for the ActuatorWrapper class."""
@@ -341,3 +387,23 @@ def test_with_function_wrapper(self, mock_actuator):
         # Test operation
         actuator_wrapper.operate("direct_action")
         mock_actuator.operate.assert_called_once_with("func_transformed_direct_action")
+
+    def test_on_paused(self, actuator_wrapper, mock_actuator, wrapper, mocker):
+        """Test that on_paused calls on_paused on all components."""
+        # Spy on the on_paused method
+        wrapper_on_paused_spy = mocker.spy(wrapper, "on_paused")
+
+        actuator_wrapper.on_paused()
+
+        mock_actuator.on_paused.assert_called_once_with()
+        wrapper_on_paused_spy.assert_called_once_with()
+
+    def test_on_resumed(self, actuator_wrapper, mock_actuator, wrapper, mocker):
+        """Test that on_resumed calls on_resumed on all components."""
+        # Spy on the on_resumed method
+        wrapper_on_resumed_spy = mocker.spy(wrapper, "on_resumed")
+
+        actuator_wrapper.on_resumed()
+
+        mock_actuator.on_resumed.assert_called_once_with()
+        wrapper_on_resumed_spy.assert_called_once_with()
diff --git a/tests/pamiq_core/trainer/test_base.py b/tests/pamiq_core/trainer/test_base.py
index c35e9733..7c545763 100644
--- a/tests/pamiq_core/trainer/test_base.py
+++ b/tests/pamiq_core/trainer/test_base.py
@@ -7,6 +7,7 @@
 from pamiq_core.data import DataUser, DataUsersDict
 from pamiq_core.model import InferenceModel, TrainingModel, TrainingModelsDict
 from pamiq_core.state_persistence import PersistentStateMixin
+from pamiq_core.threads import ThreadEventMixin
 from pamiq_core.trainer import Trainer
 
 
@@ -29,6 +30,7 @@ def train(self) -> None:
 class TestTrainer:
     def test_trainer_subclasses(self):
         assert issubclass(Trainer, PersistentStateMixin)
+        assert issubclass(Trainer, ThreadEventMixin)
 
     def test_abstract_methods(self):
         assert Trainer.__abstractmethods__ == frozenset({"train"})
diff --git a/tests/pamiq_core/trainer/test_container.py b/tests/pamiq_core/trainer/test_container.py
index 2cc6193f..3a7d6a2a 100644
--- a/tests/pamiq_core/trainer/test_container.py
+++ b/tests/pamiq_core/trainer/test_container.py
@@ -89,3 +89,29 @@ def test_load_state(
         trainers_dict.load_state(path)
         for name, trainer in trainers.items():
             trainer.load_state.assert_called_once_with(path / name)
+
+    def test_on_paused(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_paused calls on_paused on all trainers."""
+        # Call on_paused
+        trainers_dict.on_paused()
+
+        # Verify each trainer's on_paused was called
+        for trainer in trainers.values():
+            trainer.on_paused.assert_called_once_with()
+
+    def test_on_resumed(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_resumed calls on_resumed on all trainers."""
+        # Call on_resumed
+        trainers_dict.on_resumed()
+
+        # Verify each trainer's on_resumed was called
+        for trainer in trainers.values():
+            trainer.on_resumed.assert_called_once_with()


Reviews:
user: login: sugiyama34
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T13:38:40Z
user: login: Geson-anko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T02:11:27Z
user: login: Geson-anko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T02:11:42Z
user: login: Geson-anko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T02:11:58Z
user: login: Geson-anko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-29T02:15:21Z
user: login: sugiyama34
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T02:56:21Z
user: login: sugiyama34
body: 拝見しました！

LGTM です！！！
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T02:57:04Z

Review comments:
diff_hunk: @@ -91,6 +91,20 @@ def test_load_state(
         mock_agent.load_state.assert_called_once_with(load_path / "agent")
         mock_environment.load_state.assert_called_once_with(load_path / "environment")
 
+    def test_on_paused(self, interaction: Interaction, mock_agent, mock_environment):
+        """Test that on_paused() calls on_paused on both agent and
+        environment."""
+        interaction.on_paused()
+        mock_agent.on_paused.assert_called_once_with()
+        mock_environment.on_paused.assert_called_once_with()
+
path: tests/pamiq_core/interaction/test_interactions.py
user: login: sugiyama34
body: Interaction の `on_paused()` では、`super().on_paused()` も実行されていますが、(l98 in src/pamiq_core/interaction/interactions.py )
こちらの `assert_called_with_once_with()` は不要ですか？

他のテストケースについても同様の疑問があります！
created_at: 2025-03-28T12:52:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +89,29 @@ def test_load_state(
         trainers_dict.load_state(path)
         for name, trainer in trainers.items():
             trainer.load_state.assert_called_once_with(path / name)
+
+    def test_on_paused(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_paused calls on_paused on all trainers."""
path: tests/pamiq_core/trainer/test_container.py
user: login: sugiyama34
body: docstring で "on_paused" が連続して登場して一瞬意味が取れなかったので、AI 君に update してもらいました。
もしよければ、こちらをお使いいただければと思います！

```suggestion
        """Test that calling trainers_dict.on_paused() invokes the on_paused method on every trainer."""
```
created_at: 2025-03-28T13:24:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -89,3 +89,29 @@ def test_load_state(
         trainers_dict.load_state(path)
         for name, trainer in trainers.items():
             trainer.load_state.assert_called_once_with(path / name)
+
+    def test_on_paused(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_paused calls on_paused on all trainers."""
+        # Call on_paused
+        trainers_dict.on_paused()
+
+        # Verify each trainer's on_paused was called
+        for trainer in trainers.values():
+            trainer.on_paused.assert_called_once_with()
+
+    def test_on_resumed(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_resumed calls on_resumed on all trainers."""
path: tests/pamiq_core/trainer/test_container.py
user: login: sugiyama34
body: 同上です

```suggestion
        """Test that calling trainers_dict.on_resumed() invokes the on_resumed method on every trainer."""
```
created_at: 2025-03-28T13:25:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -224,3 +224,29 @@ def test_save_and_load_state(self, trainer: TrainerImpl, tmp_path: Path) -> None
         trainer._previous_training_time = 0
         trainer.load_state(test_path)
         assert trainer._previous_training_time == float("-inf")
+
+    def test_on_paused(
+        self, trainer_attached: TrainerImpl, mocker: MockerFixture
+    ) -> None:
+        """Test that on_paused calls the implementation method."""
+        # Spy on the on_paused method
+        on_paused_spy = mocker.spy(trainer_attached, "on_paused")
+
+        # Call on_paused
+        trainer_attached.on_paused()
+
+        # Verify it was called
+        on_paused_spy.assert_called_once_with()
+
path: tests/pamiq_core/trainer/test_base.py
user: login: sugiyama34
body: このテストについて質問があります！

このテストはどういう意味があるのでしょうか？

`on_paused_spy` は  `trainer_attached` の `on_paused()` の spy なので、
したの行で `trainer_attached.on_paused()` を実行した後に
`on_paused_spy.assert_called_once_with()` をやっても、
必ず通るテストになっているような気がするのですが、、、。

`Trainer` class （や `TrainerImpl` class）は mixin を入れているだけなので、issubclass を用いたテストだけでいい気がしますが、いかがでしょうか？
created_at: 2025-03-28T13:35:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -224,3 +224,29 @@ def test_save_and_load_state(self, trainer: TrainerImpl, tmp_path: Path) -> None
         trainer._previous_training_time = 0
         trainer.load_state(test_path)
         assert trainer._previous_training_time == float("-inf")
+
+    def test_on_paused(
+        self, trainer_attached: TrainerImpl, mocker: MockerFixture
+    ) -> None:
+        """Test that on_paused calls the implementation method."""
+        # Spy on the on_paused method
+        on_paused_spy = mocker.spy(trainer_attached, "on_paused")
+
+        # Call on_paused
+        trainer_attached.on_paused()
+
+        # Verify it was called
+        on_paused_spy.assert_called_once_with()
+
path: tests/pamiq_core/trainer/test_base.py
user: login: sugiyama34
body: （直後の `test_on_resumed()` も同様です）
created_at: 2025-03-28T13:35:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -224,3 +224,29 @@ def test_save_and_load_state(self, trainer: TrainerImpl, tmp_path: Path) -> None
         trainer._previous_training_time = 0
         trainer.load_state(test_path)
         assert trainer._previous_training_time == float("-inf")
+
+    def test_on_paused(
+        self, trainer_attached: TrainerImpl, mocker: MockerFixture
+    ) -> None:
+        """Test that on_paused calls the implementation method."""
+        # Spy on the on_paused method
+        on_paused_spy = mocker.spy(trainer_attached, "on_paused")
+
+        # Call on_paused
+        trainer_attached.on_paused()
+
+        # Verify it was called
+        on_paused_spy.assert_called_once_with()
+
path: tests/pamiq_core/trainer/test_base.py
user: login: Geson-anko
body: ありがとうございます。 雑に作ってミスりました。すみません。
https://github.com/MLShukai/pamiq-core/pull/128/commits/3373791a5b07192201b25baa5dd4371cdb90b91f
created_at: 2025-03-29T02:11:27Z
author_association: MEMBER
diff_hunk: @@ -89,3 +89,29 @@ def test_load_state(
         trainers_dict.load_state(path)
         for name, trainer in trainers.items():
             trainer.load_state.assert_called_once_with(path / name)
+
+    def test_on_paused(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_paused calls on_paused on all trainers."""
+        # Call on_paused
+        trainers_dict.on_paused()
+
+        # Verify each trainer's on_paused was called
+        for trainer in trainers.values():
+            trainer.on_paused.assert_called_once_with()
+
+    def test_on_resumed(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_resumed calls on_resumed on all trainers."""
path: tests/pamiq_core/trainer/test_container.py
user: login: Geson-anko
body: ありがとうございます 🙇‍♂️ 
created_at: 2025-03-29T02:11:42Z
author_association: MEMBER
diff_hunk: @@ -89,3 +89,29 @@ def test_load_state(
         trainers_dict.load_state(path)
         for name, trainer in trainers.items():
             trainer.load_state.assert_called_once_with(path / name)
+
+    def test_on_paused(
+        self,
+        trainers_dict: TrainersDict,
+        trainers: dict[str, Trainer],
+    ) -> None:
+        """Test that on_paused calls on_paused on all trainers."""
path: tests/pamiq_core/trainer/test_container.py
user: login: Geson-anko
body: ありがとうございます 🙇‍♂️ 
created_at: 2025-03-29T02:11:58Z
author_association: MEMBER
diff_hunk: @@ -91,6 +91,20 @@ def test_load_state(
         mock_agent.load_state.assert_called_once_with(load_path / "agent")
         mock_environment.load_state.assert_called_once_with(load_path / "environment")
 
+    def test_on_paused(self, interaction: Interaction, mock_agent, mock_environment):
+        """Test that on_paused() calls on_paused on both agent and
+        environment."""
+        interaction.on_paused()
+        mock_agent.on_paused.assert_called_once_with()
+        mock_environment.on_paused.assert_called_once_with()
+
path: tests/pamiq_core/interaction/test_interactions.py
user: login: Geson-anko
body: super().on_paused() は空のメソッドなのでおまじない的に呼んでいるだけなんですよね。もし `on_paused`や`on_resumed`にデフォルト実装がされた場合に、それを適用できるようにしたい気持ちもあります。

しかし、インターフェイスとして定義された基底クラスのデフォルト実装を変えるは本当に邪悪なので、する予定はありません。

結論、別になくても良い、というところですね。
created_at: 2025-03-29T02:15:21Z
author_association: MEMBER
diff_hunk: @@ -91,6 +91,20 @@ def test_load_state(
         mock_agent.load_state.assert_called_once_with(load_path / "agent")
         mock_environment.load_state.assert_called_once_with(load_path / "environment")
 
+    def test_on_paused(self, interaction: Interaction, mock_agent, mock_environment):
+        """Test that on_paused() calls on_paused on both agent and
+        environment."""
+        interaction.on_paused()
+        mock_agent.on_paused.assert_called_once_with()
+        mock_environment.on_paused.assert_called_once_with()
+
path: tests/pamiq_core/interaction/test_interactions.py
user: login: sugiyama34
body: なるほど、勉強になります、ありがとうございます！！！

> しかし、インターフェイスとして定義された基底クラスのデフォルト実装を変えるは本当に邪悪なので、する予定はありません。

なんとなく、とてもなにかがあるオーラを感じました。
勉強になります！
created_at: 2025-03-29T02:56:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: sugiyama34
created_at: 2025-03-28T13:41:41Z
body: @Geson-anko すみません、comment でレビュー出してしまいましたが、修正の確認いただけると助かります！

他、コードになっていない部分で質問があります！

- 呼び出し実装**も**あるクラスについては、`issubclass` のテストをやっていないですが、それで十分でしょうか？
  - 私も充分だと思っています！
  - 以下の理由で充分だと思っているので、この理解で良いか教えていただけると嬉しいです！
    - 呼び出し実装の方のテストがあるから、`issubclass` は不要、
    - なぜなら、`ThreadEventMixin` はインターフェイスの提供なので、実装があるならそれで十分だから
author_association: CONTRIBUTOR
user: login: sugiyama34
created_at: 2025-03-28T13:42:50Z
body: 備忘録：テストした内容

3. Regression Check（既存機能への悪影響を確認）
	- 他 mixin に同名の関数がないことを確認
		- [x] InteractionEventMixin
		- [x] PersistentStateMixin
		- [x] OrderedDict


1. テストケースは充分か
	- issubclass の追加
		- [ ] Interactionクラス: 継承, Agent, Environmentのイベントを呼び出し
		- [x] Agent: 継承
		- [x] Environment: 継承
		- [ ] ModularEnvironment: 継承, Sensor, Actuatorのイベントを呼び出し
		- [x] Sensor: 継承
		- [x] Actuator: 継承
		- [x] Wrapper: 継承
		- [ ] SensorWrapper: 呼び出し実装
		- [ ] ActuatorWrapper: 呼び出し実装
		- [ ] EnvironmentWrapper: 呼び出し実装
		- [x] Trainer: 継承
			- これがあったほうが良いのでは？　という review comment 済
		- [ ] TrainersDict: 継承, Trainerの呼び出し実装
	- 呼び出し実装は mock と assert_called_once_with
		- Interactionクラス: 継承, Agent, Environmentのイベントを呼び出し
			- [x] on_paused
			- [x] on_resumed
		- ModularEnvironment: 継承, Sensor, Actuatorのイベントを呼び出し
			- [x] on_paused
			- [x] on_resumed
		- SensorWrapper: 呼び出し実装
			- [x] on_paused
			- [x] on_resumed
		- ActuatorWrapper: 呼び出し実装
			- [x] on_paused
			- [x] on_resumed
		- EnvironmentWrapper: 呼び出し実装
			- [x] on_paused
			- [x] on_resumed
		- TrainersDict: 継承, Trainerの呼び出し実装
			- [x] on_paused
			- [x] on_resumed
author_association: CONTRIBUTOR
user: login: Geson-anko
created_at: 2025-03-29T02:21:21Z
body: > 呼び出し実装もあるクラスについては、issubclass のテストをやっていないですが、それで十分でしょうか？

はい、要りません。メソッドが定義されているからです。 厳密には `issubclass`のテストは必要ですが、Mixinの目的は確実に特定のメソッドを生やすためのものなので、目的は果たされています。

author_association: MEMBER