Files
2026-04-01 23:22:35 +03:00

208 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()