diff --git a/injection/core/module.py b/injection/core/module.py index 0b21b48..9b27335 100644 --- a/injection/core/module.py +++ b/injection/core/module.py @@ -48,12 +48,12 @@ def _format_type(cls: type) -> str: """ -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class ContainerEvent(Event, ABC): on_container: Container -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class ContainerDependenciesUpdated(ContainerEvent): references: set[type] @@ -68,27 +68,39 @@ def __str__(self) -> str: ) -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class ModuleEvent(Event, ABC): on_module: Module -@dataclass(slots=True) +@dataclass(frozen=True) class ModuleEventProxy(ModuleEvent): event: Event def __str__(self) -> str: return f"`{self.on_module}` has propagated an event: {self.origin}" - @property + @cached_property def origin(self) -> Event: if isinstance(self.event, ModuleEventProxy): return self.event.origin return self.event + @property + def is_circular(self) -> bool: + origin = self.origin + return isinstance(origin, ModuleEvent) and origin.on_module is self.on_module + + @property + def previous_module(self) -> Module | None: + if isinstance(self.event, ModuleEvent): + return self.event.on_module -@dataclass(slots=True) + return None + + +@dataclass(frozen=True, slots=True) class ModuleAdded(ModuleEvent): module_added: Module @@ -96,7 +108,7 @@ def __str__(self) -> str: return f"`{self.on_module}` now uses `{self.module_added}`." -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class ModuleRemoved(ModuleEvent): module_removed: Module @@ -104,7 +116,7 @@ def __str__(self) -> str: return f"`{self.on_module}` no longer uses `{self.module_removed}`." -@dataclass(slots=True) +@dataclass(frozen=True, slots=True) class ModulePriorityUpdated(ModuleEvent): module_updated: Module priority: ModulePriorities @@ -352,10 +364,9 @@ def change_priority(self, module: Module, priority: ModulePriorities): * **HIGH**: The module concerned becomes the most important of the modules used. """ - if self.__move_module(module, priority): - event = ModulePriorityUpdated(self, module, priority) - self.notify(event) - + self.__move_module(module, priority) + event = ModulePriorityUpdated(self, module, priority) + self.notify(event) return self def add_listener(self, listener: EventListener): @@ -368,6 +379,13 @@ def remove_listener(self, listener: EventListener): def on_event(self, event: Event, /): self_event = ModuleEventProxy(self, event) + + if self_event.is_circular: + raise ModuleError( + "Circular dependency between two modules: " + f"`{self_event.previous_module}` and `{self}`." + ) + self.notify(self_event) def notify(self, event: Event): @@ -375,15 +393,15 @@ def notify(self, event: Event): self.__channel.dispatch(event) return self - def __move_module(self, module: Module, priority: ModulePriorities) -> bool: + def __move_module(self, module: Module, priority: ModulePriorities): last = priority == ModulePriorities.LOW try: self.__modules.move_to_end(module, last=last) - except KeyError: - return False - - return True + except KeyError as exc: + raise ModuleError( + f"`{module}` can't be found in the modules used by `{self}`." + ) from exc """ diff --git a/tests/core/test_module.py b/tests/core/test_module.py index 867ccd8..7111e52 100644 --- a/tests/core/test_module.py +++ b/tests/core/test_module.py @@ -86,6 +86,13 @@ def test_use_with_self_raise_module_error(self, module, event_history): event_history.assert_length(0) + def test_use_with_circular_dependency_raise_module_error(self, module): + second_module = Module() + module.use(second_module) + + with pytest.raises(ModuleError): + second_module.use(module) + def test_use_with_module_already_in_use_raise_module_error( self, module, @@ -170,5 +177,6 @@ def test_change_priority_with_success(self, module, event_history): def test_change_priority_with_module_not_found(self, module, event_history): second_module = Module() - module.change_priority(second_module, ModulePriorities.HIGH) - event_history.assert_length(0) + + with pytest.raises(ModuleError): + module.change_priority(second_module, ModulePriorities.HIGH)