Source code for modelly.viz.console.manager

"""Console state for the command console: history entries and code execution."""

from collections.abc import Callable
from dataclasses import dataclass

from modelly.viz.console.interpreter import InteractiveConsole
from modelly.viz.utils import force_update

_TIPS_TEXT = "Available Console Commands:\n1. Press Enter to execute a command\n2. Type 'cls' to clear the console screen (it doesn't delete past variables and functions)\n3. Type 'history' to view previous commands\n4. Press Enter on an empty line to complete a multiline block\n5. Use proper indentation for multiline blocks\n6. The console will show '..: ' for continuation lines"
_HIDDEN_HISTORY_COMMANDS = ("[history]", "[tips]", "")


[docs] @dataclass class ConsoleEntry: """A class to store command console entries. Attributes: command (str): The command entered output (str): The output of the command is_error (bool): Whether the entry represents an error is_continuation (bool): Whether the entry is a continuation of previous command """ command: str output: str = "" is_error: bool = False is_continuation: bool = False def __repr__(self): """Return a string representation of the ConsoleEntry.""" return f"ConsoleEntry({self.command}, {self.output}, {self.is_error}, {self.is_continuation})"
[docs] class ConsoleManager: """A console manager for executing Python code interactively. This class provides functionality to execute Python code in an interactive console environment, maintain command history, and handle multi-line code blocks. Attributes: locals_dict (dict): Dictionary containing local variables available to the console console (InteractiveConsole): Python's interactive console instance buffer (list): Buffer for storing multi-line code blocks history (list[ConsoleEntry]): List of console entries containing commands and their outputs Special commands: ``history`` shows the command history, ``cls`` clears the console screen and ``tips`` shows the available commands and usage tips. Example: >>> console = ConsoleManager(model=my_model) >>> console.execute_code("print('hello world')", set_input_callback) """ def __init__(self, model=None, additional_imports=None): """Initialize the console manager with the provided model and imports.""" # Create locals dictionary with model and imports locals_dict = {} if model is not None: locals_dict["model"] = model if additional_imports: locals_dict.update(additional_imports) self.locals_dict = locals_dict self.console = InteractiveConsole(locals_dict) self.buffer = [] self.history: list[ConsoleEntry] = [] self.history_index = -1 self.current_input = ""
[docs] def execute_code( self, code_line: str, set_input_text: Callable[[str], None] ) -> None: """Execute the provided code line and update the console history.""" # Custom Commands: history, clear and tips if code_line == "history": self._show_history() elif code_line == "cls": self.clear_console() elif code_line == "tips": self._append_entry("[tips]", _TIPS_TEXT) elif not code_line.strip(): self._handle_empty_line() else: self._run_line(code_line) set_input_text("")
def _append_entry(self, command: str, output: str) -> None: """Append a completed, non-error entry to the history.""" self.history.append( ConsoleEntry( command=command, output=output, is_error=False, is_continuation=False, ) ) def _show_history(self) -> None: """Append an entry listing the previous commands and their outputs.""" # Get the current history except the custom commands cur_his = [ ( f"Command: {entry.command}, \nOutput: {entry.output if entry.output else None}\n" ) for entry in self.history if entry.command not in _HIDDEN_HISTORY_COMMANDS ] self._append_entry("[history]", "\n".join(cur_his) if cur_his else "No history") def _drop_continuations(self) -> None: """Remove the redundant continuation entries of the buffered block.""" for _ in range(len(self.buffer) - 1): self.history.pop() def _complete_block(self, full_code: str, output: str, error: str) -> None: """Turn the last history entry into the completed multi-line block.""" if self.history: self.history[-1].command = full_code self.history[-1].output = error if error else output self.history[-1].is_error = bool(error) self.history[-1].is_continuation = False self.buffer = [] def _handle_empty_line(self) -> None: """Complete the buffered block, or add a blank entry when nothing is buffered.""" if not self.buffer: # Empty line with no buffer - just add a blank entry self.history.append(ConsoleEntry(command="")) return full_code = "\n".join(self.buffer) _more, (output, error) = self.console.push("") self._drop_continuations() self._complete_block(full_code, output, error) def _buffer_line(self, code_line: str) -> None: """Buffer the start or continuation of a multi-line block.""" self.buffer.append(code_line) self.history.append( ConsoleEntry( command=code_line, output="", # Don't show partial output for incomplete blocks is_error=False, is_continuation=True, ) ) def _run_line(self, code_line: str) -> None: """Execute one line, buffering it when it opens or continues a block.""" more, (output, error) = self.console.push(code_line) # Force update to display any changes to the model force_update() if more: self._buffer_line(code_line) elif not self.buffer: # Normal single-line command self.history.append( ConsoleEntry( command=code_line, output=error if error else output, is_error=bool(error), is_continuation=False, ) ) else: self._drop_continuations() self.buffer.append(code_line) self._complete_block("\n".join(self.buffer), output, error)
[docs] def clear_console(self) -> None: """Clear the console history and reset the console state.""" self.history.clear() self.buffer.clear() self.history_index = -1 self.current_input = "" # Reset the console while maintaining the locals dictionary self.console = InteractiveConsole(self.locals_dict)
[docs] def get_entries(self) -> list[ConsoleEntry]: """Get the list of console entries.""" return self.history
[docs] def prev_command( self, current_text: str, set_input_text: Callable[[str], None] ) -> None: """Navigate to previous command in history.""" if not self.history: return # Save the current input if self.history_index == -1: self.current_input = current_text # Move up in history if self.history_index == -1: self.history_index = len(self.history) - 1 elif self.history_index > 0: self.history_index -= 1 # Set text to the historical command if 0 <= self.history_index < len(self.history): set_input_text(self.history[self.history_index].command)
[docs] def next_command(self, set_input_text: Callable[[str], None]) -> None: """Navigate to next command in history.""" if self.history_index == -1: return # Not in history navigation mode # Move down in history self.history_index += 1 # If we've moved past the end of history, restore the saved input if self.history_index >= len(self.history): self.history_index = -1 set_input_text(self.current_input) else: set_input_text(self.history[self.history_index].command)