"""SymPy Math App — desktop calculator / REPL powered by SymPy and PyQt5.""" from __future__ import annotations import re import sys from typing import Any import sympy from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QApplication, QHBoxLayout, QLabel, QLineEdit, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget, ) _SEPARATOR = "-" * 50 _ASSIGNMENT_RE = re.compile(r"^\s*\w+\s*=(?!=)") class HistoryLineEdit(QLineEdit): """QLineEdit with Up / Down arrow command-history navigation.""" def __init__(self, parent: QWidget | None = None) -> None: super().__init__(parent) self._entries: list[str] = [] self._cursor: int = 0 @property def entries(self) -> list[str]: return list(self._entries) def append(self, command: str) -> None: """Record *command*, skipping consecutive duplicates.""" if command and (not self._entries or self._entries[-1] != command): self._entries.append(command) self._cursor = len(self._entries) def reset_cursor(self) -> None: self._cursor = len(self._entries) def keyPressEvent(self, event) -> None: # noqa: N802 – Qt naming if event.key() == Qt.Key_Up: self._navigate(-1) event.accept() elif event.key() == Qt.Key_Down: self._navigate(1) event.accept() else: super().keyPressEvent(event) def _navigate(self, direction: int) -> None: if not self._entries: return new_index = self._cursor + direction if 0 <= new_index < len(self._entries): self._cursor = new_index self.setText(self._entries[new_index]) elif new_index >= len(self._entries): self._cursor = len(self._entries) self.clear() class MathWindow(QMainWindow): """Main application window for the SymPy calculator.""" _BUTTONS = ("Evaluate", "Clear", "Show Variables", "Show History") def __init__(self) -> None: super().__init__() self._namespace: dict[str, Any] = sympy.__dict__.copy() self._build_ui() # -- UI construction ------------------------------------------------------- def _build_ui(self) -> None: self.setWindowTitle("SymPy Math App") self.setGeometry(100, 100, 600, 500) root = QWidget() self.setCentralWidget(root) layout = QVBoxLayout(root) layout.addWidget(QLabel("Enter SymPy command:")) self._command_input = HistoryLineEdit(self) self._command_input.setPlaceholderText( 'e.g., x = Symbol("x"), or x**2 + 2*x + 1, or diff(x**2, x)' ) self._command_input.returnPressed.connect(self._evaluate) layout.addWidget(self._command_input) button_row = QHBoxLayout() handlers = (self._evaluate, self._clear, self._show_variables, self._show_history) for label, handler in zip(self._BUTTONS, handlers): btn = QPushButton(label) btn.clicked.connect(handler) button_row.addWidget(btn) layout.addLayout(button_row) layout.addWidget(QLabel("Results:")) self._results_display = QTextEdit() self._results_display.setReadOnly(True) self._results_display.setPlaceholderText("Results will appear here...") layout.addWidget(self._results_display) # -- helpers --------------------------------------------------------------- def _append_result(self, *lines: str) -> None: for line in lines: self._results_display.append(line) @staticmethod def _format_value(value: Any) -> str: if isinstance(value, sympy.Basic): return sympy.pretty(value) return str(value) # -- slots ----------------------------------------------------------------- def _evaluate(self) -> None: command = self._command_input.text().strip() if not command: self._append_result("Error: Please enter a command.") return self._command_input.append(command) try: if _ASSIGNMENT_RE.match(command): self._execute_assignment(command) else: self._evaluate_expression(command) self._command_input.clear() except Exception as exc: self._append_result(f"Command: {command}", f"Error: {exc}", _SEPARATOR) def _execute_assignment(self, command: str) -> None: exec(command, {"__builtins__": {}}, self._namespace) # noqa: S102 self._append_result(f"Command: {command}", "Variable set successfully.", _SEPARATOR) def _evaluate_expression(self, command: str) -> None: try: result = sympy.sympify(command, locals=self._namespace) except (sympy.SympifyError, SyntaxError, TypeError): result = eval(command, {"__builtins__": {}}, self._namespace) # noqa: S307 self._append_result( f"Command: {command}", f"Result:\n{self._format_value(result)}", _SEPARATOR, ) def _clear(self) -> None: self._command_input.clear() self._results_display.clear() self._command_input.reset_cursor() def _show_variables(self) -> None: builtin_keys = set(sympy.__dict__) user_vars = { name: val for name, val in self._namespace.items() if name not in builtin_keys and not name.startswith("_") } if not user_vars: self._append_result("No variables defined.", _SEPARATOR) return self._append_result("Current Variables:") for name, value in user_vars.items(): self._append_result(f" {name} = {self._format_value(value)}") self._append_result(_SEPARATOR) def _show_history(self) -> None: entries = self._command_input.entries if not entries: self._append_result("No command history yet.", _SEPARATOR) return self._append_result("Command History:") for index, cmd in enumerate(entries, 1): self._append_result(f" {index}. {cmd}") self._append_result( _SEPARATOR, "Tip: Use Up/Down arrow keys to navigate through history.", _SEPARATOR, ) def main() -> None: app = QApplication(sys.argv) window = MathWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()