diff --git a/royalnet/engineer/command.py b/royalnet/engineer/command.py index ee171550..2d918112 100644 --- a/royalnet/engineer/command.py +++ b/royalnet/engineer/command.py @@ -114,13 +114,20 @@ class FullCommand: def __call__(self, *, _sentry: s.Sentry, **kwargs) -> t.Awaitable[t.Optional[c.ConversationProtocol]]: """ - .. todo:: Document this. + :return: Get the awaitable of the :class:`.FullCommand`. + + .. seealso:: :meth:`.run` """ return self.run(_sentry=_sentry, **kwargs) async def run(self, *, _sentry: s.Sentry, **kwargs) -> t.Optional[c.ConversationProtocol]: """ - .. todo:: Document this. + Run the command. + + :param _sentry: The :class:`~royalnet.engineer.sentry.Sentry` the command should use to receive + :class:`~royalnet.engineer.bullet.projectiles.Projectile`\\ s. + :param kwargs: Additional kwargs to pass to the :attr:`.teleported_f` . + :return: :data:`None` or another :class:`~royalnet.engineer.conversation.Conversation` to switch to. """ log.debug(f"Awaiting a bullet...") diff --git a/royalnet/engineer/dispenser.py b/royalnet/engineer/dispenser.py index f23b2f15..e6994f76 100644 --- a/royalnet/engineer/dispenser.py +++ b/royalnet/engineer/dispenser.py @@ -46,7 +46,7 @@ class Dispenser: A :class:`list` of all the running sentries of this dispenser. """ - self._locked_by: t.List[ConversationProtocol] = [] + self.locked_by: t.List[ConversationProtocol] = [] """ The conversation that are currently locking this dispenser. @@ -91,10 +91,10 @@ class Dispenser: """ log.debug(f"Trying to run: {conv!r}") - if self._locked_by: - log.debug(f"Dispenser is locked by {self._locked_by!r}, refusing to run {conv!r}") + if self.locked_by: + log.debug(f"Dispenser is locked by {self.locked_by!r}, refusing to run {conv!r}") raise LockedDispenserError( - f"The Dispenser is currently locked and cannot start any new Conversation.", self._locked_by) + f"The Dispenser is currently locked and cannot start any new Conversation.", self.locked_by) log.debug(f"Running: {conv!r}") with self.sentry() as sentry: @@ -114,16 +114,16 @@ class Dispenser: :param conv: The conversation that requested the lock. - .. seealso:: :attr:`._locked_by` + .. seealso:: :attr:`.locked_by` """ log.debug(f"Adding lock: {conv!r}") - self._locked_by.append(conv) + self.locked_by.append(conv) try: yield finally: log.debug(f"Clearing lock: {conv!r}") - self._locked_by.remove(conv) + self.locked_by.remove(conv) __all__ = ( diff --git a/royalnet/engineer/pda/implementations/base.py b/royalnet/engineer/pda/implementations/base.py index 4b46f7a2..4467c905 100644 --- a/royalnet/engineer/pda/implementations/base.py +++ b/royalnet/engineer/pda/implementations/base.py @@ -7,7 +7,7 @@ import abc import contextlib import asyncio import logging -from royalnet.engineer.dispenser import Dispenser +from royalnet.engineer.dispenser import Dispenser, LockedDispenserError if t.TYPE_CHECKING: from royalnet.engineer.conversation import ConversationProtocol @@ -154,9 +154,9 @@ class ConversationListImplementation(PDAImplementation, metaclass=abc.ABCMeta): def _create_dispenser(self) -> "Dispenser": """ - Create a new dispenser. + Create a new :class:`~royalnet.engineer.dispenser.Dispenser` . - :return: The created dispenser. + :return: The created :class:`~royalnet.engineer.dispenser.Dispenser` . """ self.log.debug(f"Creating new dispenser...") @@ -164,7 +164,11 @@ class ConversationListImplementation(PDAImplementation, metaclass=abc.ABCMeta): def get_or_create_dispenser(self, key: "DispenserKey") -> "Dispenser": """ - .. todo:: Document this. + Try to :meth:`.get_dispenser` the :class:`~royalnet.engineer.dispenser.Dispenser` with the specified key, or + :meth:`._create_dispenser` a new one if it isn't available. + + :param key: The key of the :class:`~royalnet.engineer.dispenser.Dispenser` . + :return: The retrieved or created :class:`~royalnet.engineer.dispenser.Dispenser` . """ if key not in self.dispensers: @@ -202,7 +206,12 @@ class ConversationListImplementation(PDAImplementation, metaclass=abc.ABCMeta): @contextlib.asynccontextmanager async def _kwargs(self, kwargs: t.Kwargs, remaining: list["PDAExtension"]) -> t.Kwargs: """ - .. todo:: Document this. + :func:`contextlib.asynccontextmanager` factory used internally to recurse the generation and cleanup of + :meth:`kwargs` . + + :param kwargs: The current ``kwargs`` . + :param remaining: The extensions that haven't been processed yet. + :return: The corresponding :func:`contextlib.asynccontextmanager`\\ . """ if len(remaining) == 0: @@ -277,17 +286,59 @@ class ConversationListImplementation(PDAImplementation, metaclass=abc.ABCMeta): async def _run_conversation(self, dispenser: "Dispenser", conv: "ConversationProtocol") -> None: """ - .. todo:: Document this. + Run the passed :class:`~royalnet.engineer.conversation.Conversation` in the passed + :class:`~royalnet.engineer.dispenser.Dispenser`\\ , while passing the :meth:`.kwargs` provided by the + :class:`.PDA` . + + :param dispenser: The :class:`~royalnet.engineer.dispenser.Dispenser` to run the + :class:`~royalnet.engineer.conversation.Conversation` in. + :param conv: The :class:`~royalnet.engineer.conversation.Conversation` to run. """ - async with self.kwargs(conv=conv) as kwargs: - self.log.debug(f"Running {conv!r} in {dispenser!r}...") - await dispenser.run(conv=conv, **kwargs) + try: + async with self.kwargs(conv=conv) as kwargs: + self.log.debug(f"Running {conv!r} in {dispenser!r}...") + await dispenser.run(conv=conv, **kwargs) + except Exception as exception: + try: + await self._handle_conversation_exc(dispenser=dispenser, conv=conv, exception=exception) + except Exception as exception: + self.log.error(f"Failed to handle conversation exception: {exception!r}") - async def _run_all_conversations(self, dispenser: "Dispenser") -> list[asyncio.Task]: + @abc.abstractmethod + async def _handle_conversation_exc( + self, + dispenser: "Dispenser", + conv: "ConversationProtocol", + exception: Exception + ) -> None: """ - .. todo:: Document this. + Handle :exc:`Exception`\\ s that were not caught by :class:`~royalnet.engineer.conversation.Conversation`\\ s. + + :param dispenser: The dispenser which hosted the :class:`~royalnet.engineer.conversation.Conversation`\\ . + :param conv: The :class:`~royalnet.engineer.conversation.Conversation` which didn't catch the error. + :param exception: The :class:`Exception` that was raised. """ + raise NotImplementedError() + + async def _schedule_conversations(self, dispenser: "Dispenser") -> list[asyncio.Task]: + """ + Schedule the execution of instance of all the :class:`~royalnet.engineer.conversation.Conversation`\\ s listed + in :attr:`.conversations` in the specified :class:`~royalnet.engineer.dispenser.Dispenser`\\ . + + :param dispenser: The :class:`~royalnet.engineer.dispenser.Dispenser` to run the + :class:`~royalnet.engineer.conversation.Conversation`\\ s in. + :return: The :class:`list` of :class:`asyncio.Task`\\ s that were created. + + .. seealso:: :meth:`._run_conversation` + """ + + if dispenser.locked_by: + self.log.warning("Tried to run a Conversation in a locked Dispenser!") + raise LockedDispenserError( + f"The Dispenser is currently locked and cannot start any new Conversation.", + dispenser.locked_by + ) self.log.info(f"Running in {dispenser!r} all conversations...") @@ -319,7 +370,7 @@ class ConversationListImplementation(PDAImplementation, metaclass=abc.ABCMeta): dispenser = self.get_or_create_dispenser(key=key) self.log.debug(f"Running all conversations...") - await self._run_all_conversations(dispenser=dispenser) + await self._schedule_conversations(dispenser=dispenser) self.log.debug(f"Putting {projectile!r} in {dispenser!r}...") await dispenser.put(projectile)