1
0
mirror of https://github.com/MarkParker5/STARK.git synced 2025-10-05 22:47:03 +02:00
Files
STARK/stark/core/commands_context.py
Mark Parker d4f2efda46 add a test
2025-09-22 18:09:18 +02:00

205 lines
7.3 KiB
Python

from __future__ import annotations
import warnings
from dataclasses import dataclass
from types import AsyncGeneratorType, GeneratorType
from typing import Any, Protocol, runtime_checkable
import anyio
from asyncer import syncify
from asyncer._main import TaskGroup
from ..general.dependencies import DependencyManager, default_dependency_manager
from .command import (
AsyncResponseHandler,
Command,
CommandRunner,
Response,
ResponseHandler,
ResponseOptions,
)
from .commands_manager import CommandsManager, SearchResult
@dataclass
class CommandsContextLayer:
commands: list[Command]
parameters: dict[str, Any]
@runtime_checkable
class CommandsContextDelegate(Protocol):
async def commands_context_did_receive_response(self, response: Response): pass
def remove_response(self, response: Response): pass
class CommandsContext:
is_stopped = False
commands_manager: CommandsManager
dependency_manager: DependencyManager
last_response: Response | None = None
fallback_command: Command | None = None
_delegate: CommandsContextDelegate | None = None
_response_queue: list[Response]
_context_queue: list[CommandsContextLayer]
_task_group: TaskGroup
def __init__(self, task_group: TaskGroup, commands_manager: CommandsManager, dependency_manager: DependencyManager = default_dependency_manager):
assert isinstance(task_group, TaskGroup), task_group
assert isinstance(commands_manager, CommandsManager)
assert isinstance(dependency_manager, DependencyManager)
self.commands_manager = commands_manager
self._context_queue = [self.root_context]
self._response_queue = []
self._task_group = task_group
self.dependency_manager = dependency_manager
self.dependency_manager.add_dependency(None, AsyncResponseHandler, self)
self.dependency_manager.add_dependency(None, ResponseHandler, SyncResponseHandler(self))
self.dependency_manager.add_dependency('inject_dependencies', None, self.inject_dependencies)
@property
def delegate(self):
return self._delegate
@delegate.setter
def delegate(self, delegate: CommandsContextDelegate | None):
assert isinstance(delegate, CommandsContextDelegate) or delegate is None
self._delegate = delegate
@property
def root_context(self):
return CommandsContextLayer(self.commands_manager.commands, {})
async def process_string(self, string: str):
if not self._context_queue:
self._context_queue.append(self.root_context)
# search commands
search_results = []
while self._context_queue:
current_context = self._context_queue[0]
search_results = await self.commands_manager.search(string = string, commands = current_context.commands)
if search_results:
break
else:
self._context_queue.pop(0)
if not search_results and self.fallback_command and (matches := await self.fallback_command.pattern.match(string)):
for match in matches:
search_results = [SearchResult(
command = self.fallback_command,
match_result = match,
index = 0
)]
for search_result in search_results or []:
parameters = current_context.parameters
parameters.update(search_result.match_result.parameters)
parameters.update(self.dependency_manager.resolve(search_result.command._runner))
self.run_command(search_result.command, parameters)
def inject_dependencies(self, runner: Command[CommandRunner] | CommandRunner) -> CommandRunner:
def injected_func(**kwargs) -> ResponseOptions:
kwargs.update(self.dependency_manager.resolve(runner._runner if isinstance(runner, Command) else runner))
return runner(**kwargs) # type: ignore
return injected_func # type: ignore
def run_command(self, command: Command, parameters: dict[str, Any] = {}):
async def command_runner():
command_return = await command(parameters)
if isinstance(command_return, Response):
await self.respond(command_return)
elif isinstance(command_return, AsyncGeneratorType):
async for response in command_return:
if response:
await self.respond(response)
elif isinstance(command_return, GeneratorType):
message = f'[WARNING] Command {command} is a sync GeneratorType that is not fully supported and may block the main thread. ' + \
'Consider using the ResponseHandler.respond() or async approach instead.'
warnings.warn(message, UserWarning)
for response in command_return:
if response:
await self.respond(response)
elif command_return is None:
pass
else:
raise TypeError(f'Command {command} returned {command_return} of type {type(command_return)} instead of Response or AsyncGeneratorType[Response]')
self._task_group.soonify(command_runner)()
# ResponseHandler
async def respond(self, response: Response): # async forces to run in main thread
assert isinstance(response, Response)
self._response_queue.append(response)
async def unrespond(self, response: Response):
if response in self._response_queue:
self._response_queue.remove(response)
self.delegate.remove_response(response)
async def pop_context(self):
self._context_queue.pop(0)
# Context
def pop_to_root_context(self):
self._context_queue = [self.root_context]
def add_context(self, context: CommandsContextLayer):
self._context_queue.insert(0, context)
# ResponseQueue
async def handle_responses(self):
self.is_stopped = False
while not self.is_stopped:
while self._response_queue:
await self._process_response(self._response_queue.pop(0))
await anyio.sleep(0.01)
def stop(self):
self.is_stopped = True
async def _process_response(self, response: Response):
if response is Response.repeat_last and self.last_response:
await self._process_response(self.last_response)
return
if response is not Response.repeat_last:
self.last_response = response
if response.commands:
newContext = CommandsContextLayer(response.commands, response.parameters)
self._context_queue.insert(0, newContext)
await self.delegate.commands_context_did_receive_response(response)
class SyncResponseHandler: # needs for changing thread from worker to main in commands ran with asyncify
async_response_handler: ResponseHandler
def __init__(self, async_response_handler: ResponseHandler):
self.async_response_handler = async_response_handler
# ResponseHandler
def respond(self, response: Response):
syncify(self.async_response_handler.respond)(response)
def unrespond(self, response: Response):
syncify(self.async_response_handler.unrespond)(response)
def pop_context(self):
syncify(self.async_response_handler.pop_context)()