commit a18eaf29fe959284dd4095b367e6cb512a47c6f3 Author: litoq Date: Wed Apr 1 23:22:35 2026 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3602bdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Virtual Environment +venv/ +env/ +ENV/ +.venv/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca6b6da --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# SymPy Math App + +Desktop calculator and REPL for [SymPy](https://www.sympy.org/) built with PyQt5. Enter expressions and assignments in one window; results render with SymPy’s pretty printer. + +## Features + +- Evaluate SymPy expressions and run assignments in a persistent namespace +- Command history with **Up** / **Down** in the input field +- **Show Variables** — user-defined names only +- **Show History** — numbered list of prior commands +- **Clear** — input and results pane + +## Requirements + +- **Python** 3.8 or newer (recommended; matches SymPy’s supported versions) +- **pip** + +| Package | Version | +| ---------- | --------- | +| PyQt5 | ≥ 5.15.0 | +| SymPy | ≥ 1.12 | + +## Quick start + +```bash +python -m venv venv +``` + +Activate the venv (see [Installation](#installation)), then: + +```bash +pip install -r requirements.txt +python main.py +``` + +## Installation + +### 1. Create a virtual environment + +**Windows** + +```powershell +python -m venv venv +``` + +**Linux / macOS** + +```bash +python3 -m venv venv +``` + +### 2. Activate it + +**Windows (PowerShell)** + +```powershell +.\venv\Scripts\Activate.ps1 +``` + +If execution is blocked: + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +**Windows (Command Prompt)** + +```cmd +venv\Scripts\activate.bat +``` + +**Linux / macOS** + +```bash +source venv/bin/activate +``` + +### 3. Install dependencies + +```bash +pip install -r requirements.txt +``` + +## Usage + +1. Run `python main.py`. +2. Type a SymPy command and press **Enter** or click **Evaluate**. + +**Examples** + +- `x = Symbol("x")` then `x**2 + 2*x + 1` +- `diff(x**2, x)` (after defining `x`) +- `integrate(sin(x), x)` + +Assignments use `=`; other lines are evaluated as expressions (via `sympify` / `eval` in the SymPy namespace). **Clear** resets the input and output text only; it does not remove defined variables from the session. + +Deactivate the venv when finished: + +```bash +deactivate +``` + +## Project layout + +``` +math-app/ +├── main.py # Application entry point +├── requirements.txt # Locked dependency ranges +├── README.md +└── venv/ # Local virtualenv (create locally; do not commit) +``` + +## Security note + +The app evaluates user input with `exec` / `eval` in a restricted builtins context but **not** in a sandbox. Only run commands you trust. diff --git a/main.py b/main.py new file mode 100644 index 0000000..eceba51 --- /dev/null +++ b/main.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..79ad2a0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyQt5>=5.15.0 +sympy>=1.12 +