Сообщить об ошибке.

Когда использование readline уместно в Python

Содержание:

Когда уместен readline в Python, почему, какие есть ограничения, и типовые паттерны (с комментариями), которые дают ожидаемое поведение на реальных терминалах.

Что делает readline и почему это важно

readline - стандартный модуль, который подключает к вводу строк редактирование в стиле shell, историю, автодополнение и ряд "хуков" на этапе ввода. Ключевой момент: настройки readline влияют не только на REPL, но и на поведение встроенного input() - то есть на любые ваши интерактивные приглашения.

Где и когда readline уместен

Интерактивные консольные утилиты (CLI) с "живым" вводом

Уместно, когда пользователь реально вводит команды в терминале:

  • оболочки администрирования, внутренние тулзы, отладочные консоли;
  • mini-REPL для доменной логики (например, ввод SQL-подобных команд, запросов к API, управления стендом);
  • мастера/визарды, где нужно исправлять ввод, возвращаться по истории.

Обоснование: readline резко повышает UX (история, перемещение по строке, повтор команд), а стоимость интеграции низкая (обычно достаточно import readline + настройка).

Командные интерпретаторы на базе cmd.Cmd

Если вы используете cmd, то при наличии загруженного readline ввод автоматически "наследует" редактирование и историю в bash-стиле. Это прямой, каноничный кейс.

Настройка интерактивного Python (PYTHONSTARTUP)

Если вам нужно включить completion/историю в интерактивных сессиях - readline и rlcompleter являются стандартным путём.Дополнительно: CPython сам может автоматически включать tab-completion и историю "если доступно на платформе" (важно учитывать при дублировании настроек).

Когда readline НЕуместен или требует оговорок

  1. Неинтерактивный ввод (пайпы, файлы, CI): там readline либо бесполезен, либо создаёт неожиданные сайд-эффекты. Практика: включать только при sys.stdin.isatty().
  2. Кроссплатформенность: readline в документации явно отмечен как модуль "Unix". На Windows он может отсутствовать/вести себя иначе (в зависимости от сборки/окружения).
  3. macOS и libedit вместо GNU Readline: на macOS под капотом может быть libedit, у него отличается конфигурация и часть поведения completion-индексов; нужно учитывать различия биндингов и init-файла.
  4. Секреты/чувствительные данные: история может "утечь" в файл. Если вы принимаете токены/пароли/ключи - либо не используйте историю, либо фильтруйте и/или отключайте auto-history, либо используйте getpass для скрытого ввода.

Лучшие практики

Практика A: Подключать только в интерактивном TTY и "мягко"

  • try/except ImportError (или более общий Exception для редких сборок)
  • sys.stdin.isatty() до настройки

Зачем: исключаете падения на платформах без readline и убираете "магические" эффекты в пайпах.

Практика B: Явно задавать биндинги и учитывать libedit

На macOS возможен libedit; документация рекомендует различать по "libedit" в readline.__doc__, и указывает, что init-файл для libedit - ~/.editrc (а для GNU Readline типично ~/.inputrc).

Практика C: Делать историю персистентной и ограниченной

Используйте:

  • read_history_file() / write_history_file() (по умолчанию ~/.history)
  • set_history_length(n) - чтобы ограничить размер; write_history_file() использует это значение для усечения
  • запись через atexit (классический паттерн прямо в документации как "Example")

Практика D: Контролировать auto-history

Если вы строите свой ввод/команды, часто полезно отключить автоматическое добавление и добавлять в историю только "безопасные" команды:

  • readline.set_auto_history(False) (auto-history по умолчанию включён в CPython).

Практика E: Для completion задавать разделители и учитывать различия индексов

Если completion не для питоновских идентификаторов, настройте delimiters (set_completer_delims). При этом get_begidx()/get_endidx() могут отличаться между libedit и libreadline, что важно для контекстного completion.

Паттерны использования (с комментариями)

"Включить удобный ввод" для вашей CLI (минимально)

import sys

def enable_line_editing():
    # Включаем только в интерактивном терминале (не в пайпе/файле)
    if not sys.stdin.isatty():
        return

    try:
        import readline # Unix-модуль; может отсутствовать на некоторых платформах
    except Exception:
        return

    # Минимально: наличие readline уже улучшает input() (история/редактирование)
    # Дальше - опциональная настройка биндингов.

Tab-completion + Python-идентификаторы через rlcompleter

rlcompleter предоставляет completion-функцию, которую передают в readline.set_completer(). На Unix при импорте rlcompleter он автоматически создаёт completer и устанавливает его как текущий.

import sys

def enable_python_completion():
    if not sys.stdin.isatty():
        return
    try:
        import readline
        import rlcompleter
    except Exception:
        return

    # Различаем GNU readline и libedit (актуально на macOS)
    is_libedit = "libedit" in (readline.__doc__ or "")
    if is_libedit:
        # libedit использует другой синтаксис биндингов
        readline.parse_and_bind("bind ^I rl_complete")
    else:
        readline.parse_and_bind("tab: complete")

Персистентная история для вашей утилиты (устойчивый "боевой" паттерн)

import atexit
import os
import sys

def enable_history(app_name: str = "mytool", limit: int = 2000):
    if not sys.stdin.isatty():
        return
    try:
        import readline
    except Exception:
        return

    # Лучше хранить историю отдельно от ~/.history, чтобы не смешивать контексты
    history_path = os.path.join(os.path.expanduser("~"), f".{app_name}_history")

    # Ограничиваем размер истории (write_history_file() будет усекать по этому лимиту)
    readline.set_history_length(limit)

    # Аккуратно читаем историю, если файл существует
    try:
        readline.read_history_file(history_path)
    except FileNotFoundError:
        pass # первый запуск - это нормально

    # Пишем историю при выходе
    def save():
        try:
            readline.write_history_file(history_path)
        except Exception:
            # В проде можно логировать; падать при завершении не стоит
            pass

    atexit.register(save)

Используемые вызовы и семантика (дефолты, усечение по set_history_length, чтение/запись history-файла) описаны в документации readline.

Доменно-специфический completion (команды/аргументы)

import sys

def enable_command_completion(commands: list[str]):
    if not sys.stdin.isatty():
        return
    try:
        import readline
    except Exception:
        return

    # Часто полезно: пробел завершает "слово", чтобы completion работал по первому токену
    readline.set_completer_delims(" \t\n")

    def completer(text: str, state: int):
        # text - то, что уже набрано в текущем "слове"
        # state = 0,1,2,... пока не вернём None
        matches = [c for c in commands if c.startswith(text)]
        try:
            return matches[state]
        except IndexError:
            return None

    readline.set_completer(completer)
    readline.parse_and_bind("tab: complete")

Контракт set_completer(function) и сигнатура function(text, state) формально описаны в документации readline.

Интеграция с cmd.Cmd: "правильная" архитектура для интерактивной оболочки

В cmd completion обычно делают методами complete_<command>, а readline даёт историю/редактирование при вводе. Важно: эффект редактирования/истории появляется автоматически, если readline загружен.

Короткий чек-лист выбора

  • Нужен "живой" ввод в терминале и удобство пользователя? Да => readline уместен.
  • Скрипт работает в пайпах/CI или читает stdin как поток? Нет => не включать (или включать строго при isatty).
  • Требуется кроссплатформенность (включая Windows) без сюрпризов? Рассмотреть альтернативы (например, prompt_toolkit) или делать graceful fallback без readline.
  • Ввод может содержать секреты? Отключить/фильтровать историю (set_auto_history(False) + ручное добавление) либо не использовать readline для таких полей.