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 точнее, документацию честнее и избавляет от падений тестов из‑за того, что терминал добавил немного цвета.