initial commit
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user