2025, Dec 21 06:01
Как заставить doctest проходить с цветным выводом в Python
Почему doctest падает на цветном выводе в Python и как это исправить: ANSI-последовательности, colorama, плейсхолдеры, обёртка inject_docs, работающий пример.
Проверять консольный вывод с doctest обычно просто — пока не появляется цвет. Как только в stdout попадают ANSI-последовательности, то, что для человека выглядит одинаково, перестаёт совпадать посимвольно для тестового раннера. Ниже — краткое объяснение, почему doctest спотыкается на цветном выводе и как сделать так, чтобы ожидаемый текст точно соответствовал реальному, не жертвуя читаемостью.
Проблема
Нужно проверить напечатанный контент. Одна функция выводит обычный текст, другая — тот же текст, но с цветом. В doctest для цветного варианта нас интересует видимая часть, однако проверка падает, потому что фактический вывод содержит невидимые цветовые коды.
from colorama import Fore
def emit_text():
"""Prints out the result.
>>> emit_text()
Hello world
"""
print('Hello world')
def emit_text_with_color():
"""Prints out the result.
>>> emit_text_with_color()
Hello world
"""
print(Fore.GREEN + 'Hello world' + Fore.RESET)
if __name__ == '__main__':
import doctest
doctest.testmod()
Когда doctest выполняет второй пример, он сообщает о несовпадении, хотя строки на экране выглядят идентично.
Почему doctest падает
doctest сравнивает дословно: что напечатал ваш код, с тем, что записано в докстринге. Цветной вариант выводит текст вместе с ANSI-последовательностями, которые добавляет colorama, тогда как ожидаемый фрагмент в докстринге — это простой текст. Невидимые последовательности делают фактический вывод длиннее и иным, отсюда и провал проверки.
Решение
Закодируйте маркеры цвета прямо в докстринге с помощью лёгкой обёртки, которая подставляет в плейсхолдеры реальные управляющие коды до запуска doctest. Так докстринг становится источником истины: и видимое содержимое, и цветовое оформление отражены явно и согласованы с реальным выводом программы.
from colorama import Fore
def inject_docs(fn):
fn.__doc__ = fn.__doc__.format(**{"GREEN": Fore.GREEN, "RESET": Fore.RESET})
return fn
def emit_text():
"""Prints out the result.
>>> emit_text()
Hello world
"""
print("Hello world")
@inject_docs
def emit_text_with_color():
"""Prints out the result.
>>> emit_text_with_color()
{GREEN}Hello world{RESET}
"""
print(Fore.GREEN + "Hello world" + Fore.RESET)
if __name__ == "__main__":
import doctest
doctest.testmod()
Здесь плейсхолдеры {GREEN} и {RESET} заменяются фактическими значениями Fore.GREEN и Fore.RESET до сравнения вывода в doctest. В итоге тест проходит, оставаясь читаемым и самообъясняющимся.
Почему это важно
Пользовательский опыт в консоли нередко опирается на цвет, чтобы передавать состояние, важность или акцент. Если doctest игнорирует цвет, он со временем расходится с тем, что программа реально выводит. А если сравнивает простой текст с цветным выводом, тест падает по причинам, не связанным с самим сообщением. Явно закладывая цветовое намерение в doctest, вы сохраняете ожидания близкими к реальности и избегаете тонких поломок, связанных с форматированием.
За подробностями о подстановке переменных в докстринги см. ответы здесь: ссылка.
Итог
Проверяя с doctest функции, которые печатают цветной текст, считайте форматирование частью контракта. Используйте плейсхолдеры в докстринге и разворачивайте их при импорте, чтобы согласовать ожидаемый и фактический вывод. Такой подход делает doctest точнее, документацию честнее и избавляет от падений тестов из‑за того, что терминал добавил немного цвета.