@@ -458,13 +458,40 @@ def ensure(feature: str, *, prompt: bool = True) -> None:
458458
459459 if prompt and sys .stdin .isatty () and sys .stdout .isatty ():
460460 spec_list = ", " .join (missing )
461+ prompt_msg = (
462+ f"\n Feature { feature !r} requires: { spec_list } \n "
463+ f"Install into the active venv now? [Y/n] "
464+ )
465+ answer = "n"
461466 try :
462- answer = input (
463- f"\n Feature { feature !r} requires: { spec_list } \n "
464- f"Install into the active venv now? [Y/n] "
465- ).strip ().lower ()
466- except (EOFError , KeyboardInterrupt ):
467- answer = "n"
467+ # When prompt_toolkit owns the terminal, bare input() deadlocks
468+ # because keystrokes go to prompt_toolkit, not stdin. Temporarily
469+ # release the terminal via run_in_terminal so input() can read.
470+ from prompt_toolkit .application .current import get_app_or_none
471+ app = get_app_or_none ()
472+ except Exception :
473+ app = None # prompt_toolkit not installed or detection failed
474+
475+ if app is not None :
476+ try :
477+ from prompt_toolkit .application import run_in_terminal
478+
479+ def _read_input ():
480+ nonlocal answer
481+ try :
482+ answer = input (prompt_msg ).strip ().lower ()
483+ except (EOFError , KeyboardInterrupt ):
484+ answer = "n"
485+
486+ run_in_terminal (_read_input )()
487+ except Exception :
488+ # Fallback: skip prompt if run_in_terminal fails
489+ answer = "n"
490+ else :
491+ try :
492+ answer = input (prompt_msg ).strip ().lower ()
493+ except (EOFError , KeyboardInterrupt ):
494+ answer = "n"
468495 if answer and answer not in {"y" , "yes" }:
469496 raise FeatureUnavailable (
470497 feature , missing , "user declined install at prompt"
0 commit comments