{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-23-01-00",
   "metadata": {},
   "source": [
    "# 23-01 · Калькулятор\n",
    "\n",
    "Практика к разделу [«Домашний проект A: калькулятор с Tkinter»](../../site/chapters/glava-23/23-hw-01-kalkulyator.html). Полный файл — `projects/tkinter/calculator/calculator.py`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "setup-23-01",
   "metadata": {},
   "source": [
    "## Reproducible local environment\n",
    "\n",
    "These appendix projects live in the course repository, not in SafeSort:\n",
    "\n",
    "```bash\n",
    "git clone https://github.com/Cartesian-School/python-from-zero.git\n",
    "cd python-from-zero\n",
    "python3.14 -m venv .venv\n",
    "source .venv/bin/activate\n",
    "# Windows PowerShell: .venv\\Scripts\\Activate.ps1\n",
    "python -m pip install -U pip\n",
    "python -m pip install pytest pygame-ce jupyter ipykernel\n",
    "python -m ipykernel install --user --name course-py314 --display-name \"Course Python 3.14\"\n",
    "jupyter lab\n",
    "```\n",
    "\n",
    "Select **Course Python 3.14**. On Linux, Tkinter may also require the OS\n",
    "package `python3-tk`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "diagnostic-23-01",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "print(sys.executable)\n",
    "print(Path.cwd())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-03",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "Проверить безопасное вычисление выражений через `vychislit_vyrazhenie()` и последовательность нажатий кнопок, как это делал бы настоящий пользователь."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-04",
   "metadata": {},
   "source": [
    "## Example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-01-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "\n",
    "sys.path.insert(0, \"../../projects/tkinter/calculator\")\n",
    "import calculator as c\n",
    "\n",
    "print(c.vychislit_vyrazhenie(\"2+2\"))\n",
    "print(c.vychislit_vyrazhenie(\"10-3*2\"))\n",
    "print(c.vychislit_vyrazhenie(\"(1+2)*3\"))\n",
    "print(c.vychislit_vyrazhenie(\"7/2\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-06",
   "metadata": {},
   "source": [
    "## Проверка результата"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-01-07",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert c.vychislit_vyrazhenie(\"2+2\") == \"4\"\n",
    "assert c.vychislit_vyrazhenie(\"10-3*2\") == \"4\"\n",
    "assert c.vychislit_vyrazhenie(\"(1+2)*3\") == \"9\"\n",
    "assert c.vychislit_vyrazhenie(\"7/2\") == \"3.5\"\n",
    "print(\"Верно: базовые вычисления совпадают с ожидаемыми.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-08",
   "metadata": {},
   "source": [
    "## Эксперимент — некорректный и опасный ввод не приводит к падению"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-01-09",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(c.vychislit_vyrazhenie(\"5/0\"))\n",
    "print(c.vychislit_vyrazhenie(\"2+*3\"))\n",
    "print(c.vychislit_vyrazhenie(\"import os\"))\n",
    "\n",
    "assert c.vychislit_vyrazhenie(\"5/0\") == \"Ошибка\"\n",
    "assert c.vychislit_vyrazhenie(\"2+*3\") == \"Ошибка\"\n",
    "assert c.vychislit_vyrazhenie(\"import os\") == \"Ошибка\"\n",
    "print(\"Верно: деление на ноль, синтаксическая ошибка и посторонний код — везде «Ошибка».\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-10",
   "metadata": {},
   "source": [
    "## Почему это безопасно — два независимых барьера\n",
    "\n",
    "1. **Белый список символов**: функция сначала проверяет `set(vyrazhenie) <= DOPUSTIMYE_SIMVOLY` — строка должна состоять только из цифр, `+ - * / ( ) .` и пробелов. Буквы в `\"import os\"` не входят в этот список, поэтому функция возвращает `\"Ошибка\"` ещё **до** вызова `ast.parse()` — до разбора дело просто не доходит.\n",
    "2. **Разбор через `ast.parse(..., mode=\"eval\")`**: строка вроде `\"2+*3\"` состоит только из разрешённых символов, но не является корректным арифметическим выражением — `ast.parse()` поднимает `SyntaxError`, который тоже перехватывается и превращается в `\"Ошибка\"`. А то, что всё-таки разобралось, дополнительно проверяется узел за узлом в `vychislit_uzel()`: разрешены только числа, `+ - * /` и унарный минус — никаких имён и вызовов."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-11",
   "metadata": {},
   "source": [
    "## Starter\n",
    "\n",
    "Заполните отмеченное место. Неизменённый starter не проходит tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "task-23-01",
   "metadata": {
    "tags": [
     "exercise",
     "starter"
    ]
   },
   "outputs": [],
   "source": [
    "sostoyanie = c.SostoyanieKalkulyatora()\n",
    "\n",
    "# TODO: передайте объекту четыре символа \"12+8\", затем нажмите равно.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-13",
   "metadata": {},
   "source": [
    "## Task\n",
    "\n",
    "Смоделируйте нажатия `1`, `2`, `+`, `8`, `=`. В task cell должен остаться объект `sostoyanie` с результатом `20`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-14",
   "metadata": {},
   "source": [
    "## Tests\n",
    "\n",
    "Запустите после task cell: есть основной пример и хотя бы один крайний случай."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tests-23-01",
   "metadata": {
    "tags": [
     "exercise-tests"
    ]
   },
   "outputs": [],
   "source": [
    "assert sostoyanie.na_ekrane() == \"20\"\n",
    "\n",
    "# Edge case: ошибка вычисления тоже должна стать состоянием экрана, а не падением.\n",
    "kraj = c.SostoyanieKalkulyatora()\n",
    "for simvol in \"5/0\":\n",
    "    kraj.na_cifru_ili_znak_nazhali(simvol)\n",
    "kraj.na_ravno_nazhali()\n",
    "assert kraj.na_ekrane() == \"Ошибка\"\n",
    "print(\"Tests passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-16",
   "metadata": {},
   "source": [
    "## Hint\n",
    "\n",
    "Сначала вызывайте `na_cifru_ili_znak_nazhali()` для каждого символа, затем один раз `na_ravno_nazhali()`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-01-17",
   "metadata": {},
   "source": [
    "## Solution\n",
    "\n",
    "<details><summary>Показать решение после собственной попытки</summary>\n",
    "\n",
    "```python\n",
    "sostoyanie = c.SostoyanieKalkulyatora()\n",
    "for simvol in \"12+8\":\n",
    "    sostoyanie.na_cifru_ili_znak_nazhali(simvol)\n",
    "\n",
    "assert sostoyanie.na_ekrane() == \"12+8\"\n",
    "sostoyanie.na_ravno_nazhali()\n",
    "\n",
    "assert sostoyanie.na_ekrane() == \"20\"\n",
    "print(\"Верно: 12 + 8 = 20 на экране калькулятора.\")\n",
    "```\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
