Когда уместен readline в Python, почему, какие есть ограничения, и типовые паттерны (с комментариями), которые дают ожидаемое поведение на реальных терминалах.
readline и почему это важноreadline - стандартный модуль, который подключает к вводу строк редактирование в стиле shell, историю, автодополнение и ряд "хуков" на этапе ввода. Ключевой момент: настройки readline влияют не только на REPL, но и на поведение встроенного input() - то есть на любые ваши интерактивные приглашения.
readline уместенУместно, когда пользователь реально вводит команды в терминале:
Обоснование: readline резко повышает UX (история, перемещение по строке, повтор команд), а стоимость интеграции низкая (обычно достаточно import readline + настройка).
cmd.CmdЕсли вы используете cmd, то при наличии загруженного readline ввод автоматически "наследует" редактирование и историю в bash-стиле. Это прямой, каноничный кейс.
Если вам нужно включить completion/историю в интерактивных сессиях - readline и rlcompleter являются стандартным путём.Дополнительно: CPython сам может автоматически включать tab-completion и историю "если доступно на платформе" (важно учитывать при дублировании настроек).
readline НЕуместен или требует оговорокreadline либо бесполезен, либо создаёт неожиданные сайд-эффекты. Практика: включать только при sys.stdin.isatty().readline в документации явно отмечен как модуль "Unix". На Windows он может отсутствовать/вести себя иначе (в зависимости от сборки/окружения).libedit вместо GNU Readline: на macOS под капотом может быть libedit, у него отличается конфигурация и часть поведения completion-индексов; нужно учитывать различия биндингов и init-файла.getpass для скрытого ввода.try/except ImportError (или более общий Exception для редких сборок)sys.stdin.isatty() до настройкиЗачем: исключаете падения на платформах без readline и убираете "магические" эффекты в пайпах.
libeditНа macOS возможен libedit; документация рекомендует различать по "libedit" в readline.__doc__, и указывает, что init-файл для libedit - ~/.editrc (а для GNU Readline типично ~/.inputrc).
Используйте:
read_history_file() / write_history_file() (по умолчанию ~/.history)set_history_length(n) - чтобы ограничить размер; write_history_file() использует это значение для усеченияatexit (классический паттерн прямо в документации как "Example")Если вы строите свой ввод/команды, часто полезно отключить автоматическое добавление и добавлять в историю только "безопасные" команды:
readline.set_auto_history(False) (auto-history по умолчанию включён в CPython).Если completion не для питоновских идентификаторов, настройте delimiters (set_completer_delims). При этом get_begidx()/get_endidx() могут отличаться между libedit и libreadline, что важно для контекстного completion.
import sys def enable_line_editing(): # Включаем только в интерактивном терминале (не в пайпе/файле) if not sys.stdin.isatty(): return try: import readline # Unix-модуль; может отсутствовать на некоторых платформах except Exception: return # Минимально: наличие readline уже улучшает input() (история/редактирование) # Дальше - опциональная настройка биндингов.
rlcompleterrlcompleter предоставляет 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.
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 уместен.isatty).prompt_toolkit) или делать graceful fallback без readline.set_auto_history(False) + ручное добавление) либо не использовать readline для таких полей.