# Домашнє завдання · Файли й pathlib

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Усі три рівні працюють із тим самим прайсом, що й лекція. Три правила, які діють
скрізь і не обговорюються:

- кожен `open` у текстовому режимі має **явний** `encoding="utf-8"`;
- жодного `f.close()` руками — тільки `with`;
- шляхи будуються через `pathlib`, а не склеюються рядками.

Рівні 2 і 3 краще робити у **тимчасовій теці** (`tempfile.mkdtemp`), як у практиці,
щоб експерименти з видаленням і перейменуванням нічого не зачепили.

---

## 🟢 Рівень 1 — База

Зроби теку проєкту з двома файлами: `prays.txt` і `zvit.py`.

У `prays.txt` запиши **щонайменше вісім** товарів у форматі `назва;ціна`, по рядку
на товар, з українськими назвами.

`zvit.py` має прочитати прайс **обходом циклом**, порахувати кількість товарів,
загальну суму й найдорожчий товар, а потім записати звіт у `zvit.txt` — по рядку
на товар плюс три підсумкові рядки.

Найважливіша частина: шлях до `prays.txt` будуй від **самого скрипта**, а не від
поточної теки:

```python
ТЕКА_СКРИПТА = Path(__file__).parent
прайс = ТЕКА_СКРИПТА / "prays.txt"
```

**Зроблено, якщо:** виконуються всі три перевірки.

1. `cd /` і потім `python3 /повний/шлях/до/zvit.py` — скрипт знаходить свій прайс
   і не падає з `FileNotFoundError`.
2. `zvit.txt`, відкритий у редакторі чи браузері, показує українські назви, а не
   мотлох.
3. `len(Path("zvit.txt").read_text(encoding="utf-8").splitlines())` дорівнює
   `кількість_товарів + 3` — тобто жоден `\n` не загубився і жоден зайвий не додався.

---

## 🟡 Рівень 2 — Плюс

Тепер про кодування — на числах, а не на віру.

1. Візьми той самий рядок прайсу й запиши його у **три** файли: `utf8.txt`
   (`encoding="utf-8"`), `cp1251.txt` (`encoding="cp1251"`) і `bite.txt`, куди
   поклади сирі байти через `Path("bite.txt").write_bytes(b"\xff\xfe\x00\x41")`.
2. Напиши функцію `прочитати_будь_як(шлях)`, яка спершу пробує `utf-8`, а при
   `UnicodeDecodeError` — `cp1251`. Якщо не вийшло й так, вона має **кинути виняток
   далі**, а не повертати `None` і не мовчати.
3. Порівняй розміри: скільки байтів займає той самий текст у кожному кодуванні
   й скільки в ньому символів.

**Зроблено, якщо:** у тебе є чотири записані результати.

- `assert прочитати_будь_як(utf8) == прочитати_будь_як(cp1251)` проходить —
  два різні файли дають той самий рядок;
- `прочитати_будь_як(bite)` **падає**, і ти можеш назвати тип винятку;
- три числа: байтів у UTF-8, байтів у cp1251, символів у рядку — і ти можеш
  пояснити одним реченням, звідки береться різниця між першим і третім;
- окремо перевір, що станеться, якщо `utf8.txt` прочитати з `encoding="cp1251"`:
  запиши, який рядок вийшов і чи був виняток.

---

## 🔴 Рівень 3 — Виклик

Напиши функцію `перейменувати_розширення(тека, було, стало, сухий_запуск=True)`.
Вона рекурсивно знаходить у теці **всі** файли з розширенням `було` і перейменовує
їх на `стало`, лишаючи назву незмінною. Повертає список пар `(старий, новий)`.

Три вимоги:

- `сухий_запуск=True` (за замовчуванням) **нічого не змінює** — лише повертає
  список того, що зробила б;
- обхід дерева впорядкований (`sorted`), інакше результат плаватиме між запусками;
- якщо цільове імʼя вже зайняте, файл пропускається, а його імʼя потрапляє
  в окремий список конфліктів.

Побудуй для перевірки дерево щонайменше з **трьох рівнів** і пʼятьма файлами `.txt`,
серед яких обовʼязково буде пара `nazva.txt` + вже наявний `nazva.md` — щоб конфлікт
справді стався.

**Зроблено, якщо:**

1. Список файлів у дереві **до** сухого запуску й **після** нього однаковий:
   `assert до == після` (порівнюй `sorted(тека.rglob("*"))`).
2. Сухий запуск повернув рівно 5 пар, а справжній — 4 перейменування й 1 конфлікт.
3. Повторний справжній виклик повертає **порожній** список: перейменовувати вже нічого.
4. Ти можеш пояснити двома реченнями, чому `сухий_запуск=True` стоїть саме
   за замовчуванням, а не навпаки.

---

## Підказки

- **`Path.with_suffix` уже все вміє.** `Path("dani/zvit.txt").with_suffix(".md")`
  дає `dani/zvit.md` — рахувати індекси крапки не треба. Але памʼятай про
  `prays.2026.csv` із лекції: `with_suffix` замінює лише **останнє** розширення.
- **`rglob` — це `glob` із `**/`.** `тека.rglob("*.txt")` знайде `.txt` на всіх
  рівнях; `тека.glob("*.txt")` — лише на верхньому. Прогони обидва й порівняй
  довжину списків, це найшвидший спосіб запамʼятати різницю.
- **Перейменовує не `rename`, а `replace`.** `Path.rename` у Windows падає, якщо
  цільовий файл існує, а `Path.replace` тихо його затирає. Оскільки нам конфлікт
  треба **помітити**, перевіряй `новий.exists()` самостійно **перед** викликом,
  а не покладайся на поведінку методу.
- **Не перейменовуй під час обходу.** `for файл in тека.rglob("*.txt")` віддає
  генератор, який читає диск на ходу; змінювати теку в цей момент — надійний спосіб
  отримати непередбачуваний результат. Спершу `список = sorted(тека.rglob(...))`,
  потім цикл по готовому списку.
- **Кодування не вгадується.** У рівні 2 порядок спроб `utf-8` → `cp1251` не
  випадковий: у зворотному порядку `cp1251` прочитає **будь-що** без винятку, і до
  `utf-8` черга ніколи не дійде. Це той самий висновок, що в інтерактиві 3.
