Файловая ошибка стала ошибкой приложения: сохраняем причину через raise from
Сервис читает конфигурацию через файловый адаптер. Вызывающему коду важно знать, что конфигурация недоступна, а не разбирать все возможные OSError. Автор вводит ConfigUnavailable, но при переводе ошибки оставляет только str(error). В диагностике теряется явная связь с исходным исключением, а тест начинает сравнивать платформозависимый текст сообщения.
raise NewError(...) from original устанавливает явную причину исключения. У вызывающего остаётся предметный тип для решения о поведении, а в traceback видна цепочка. В тесте можно отдельно проверить понятное сообщение нового уровня и тип __cause__, не закрепляя весь системный текст ошибки.
Исключение на границе слоя
Переводить исключение стоит на границе ответственности. Функция чтения знает путь и формат, поэтому может добавить контекст конфигурации. Внутри узкого try находится только операция, ошибки которой действительно нужно преобразовать. Если обернуть весь сервис одним except Exception, программный дефект легко превратится в ложное сообщение «конфигурация недоступна».
from pathlib import Path
from tempfile import TemporaryDirectory
class ConfigUnavailable(Exception):
pass
def read_settings(path: Path) -> str:
try:
return path.read_text(encoding="utf-8")
except OSError as error:
raise ConfigUnavailable(f"missing settings: {path.name}") from error
with TemporaryDirectory() as directory:
try:
read_settings(Path(directory) / "absent.json")
except ConfigUnavailable as error:
assert isinstance(error.__cause__, FileNotFoundError)
print(error)
else:
raise AssertionError("missing file must fail")
Категории отказа и исходная причина
Пример запускается без внешних пакетов на Python 3.12 и печатает missing settings: absent.json. TemporaryDirectory даёт изолированное место, где указанный файл действительно отсутствует. Если потерять from error, значение __cause__ перестанет быть FileNotFoundError, и проверка заметит изменение контракта диагностики.
Не нужно создавать новый тип исключения в каждом методе. Выберите несколько ошибок, на которые вызывающие действительно реагируют по-разному: недоступное хранилище, конфликт изменения, неправильные входные данные. Детали вроде errno остаются в причине. Внешний слой решает, какое сообщение можно показать человеку и что записать в журнал.
raise from None решает другую задачу: скрывает вывод цепочки при стандартном отображении. Это бывает уместно для интерфейса, где низкоуровневая причина лишь шумит, но использовать его по умолчанию не стоит. Сначала определите, кому нужна диагностика. Не заменяйте сохранение причины её ручным включением в длинную строку — структурированная связь полезнее при анализе.
Проверьте успешное чтение, отсутствие файла и другой OSError. Если повреждённый JSON должен означать неверную конфигурацию, добавьте отдельную обработку JSONDecodeError с собственным сообщением. Тогда код отличает недоступность ресурса от ошибки содержимого, а тесты описывают эти решения. Слой приложения получает стабильные категории отказов, не теряя информацию, необходимую для поиска причины.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
