initial commit
This commit is contained in:
+24
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
PyQt5>=5.15.0
|
||||||
|
sympy>=1.12
|
||||||
|
|
||||||
Reference in New Issue
Block a user