initial commit

This commit is contained in:
2026-04-01 23:22:35 +03:00
commit a18eaf29fe
4 changed files with 349 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
"""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()