import sys from contextlib import contextmanager from typing import TYPE_CHECKING, Any, Generator, Optional try: from contextvars import ContextVar except ImportError: from prompt_toolkit.eventloop.dummy_contextvars import ContextVar # type: ignore if TYPE_CHECKING: from prompt_toolkit.input.base import Input from prompt_toolkit.output.base import Output from .application import Application __all__ = [ "AppSession", "get_app_session", "get_app", "get_app_or_none", "set_app", "create_app_session", "create_app_session_from_tty", ] class AppSession: """ An AppSession is an interactive session, usually connected to one terminal. Within one such session, interaction with many applications can happen, one after the other. The input/output device is not supposed to change during one session. Warning: Always use the `create_app_session` function to create an instance, so that it gets activated correctly. :param input: Use this as a default input for all applications running in this session, unless an input is passed to the `Application` explicitely. :param output: Use this as a default output. """ def __init__( self, input: Optional["Input"] = None, output: Optional["Output"] = None ) -> None: self._input = input self._output = output # The application will be set dynamically by the `set_app` context # manager. This is called in the application itself. self.app: Optional["Application[Any]"] = None def __repr__(self) -> str: return f"AppSession(app={self.app!r})" @property def input(self) -> "Input": if self._input is None: from prompt_toolkit.input.defaults import create_input self._input = create_input() return self._input @property def output(self) -> "Output": if self._output is None: from prompt_toolkit.output.defaults import create_output self._output = create_output() return self._output _current_app_session: ContextVar["AppSession"] = ContextVar( "_current_app_session", default=AppSession() ) def get_app_session() -> AppSession: return _current_app_session.get() def get_app() -> "Application[Any]": """ Get the current active (running) Application. An :class:`.Application` is active during the :meth:`.Application.run_async` call. We assume that there can only be one :class:`.Application` active at the same time. There is only one terminal window, with only one stdin and stdout. This makes the code significantly easier than passing around the :class:`.Application` everywhere. If no :class:`.Application` is running, then return by default a :class:`.DummyApplication`. For practical reasons, we prefer to not raise an exception. This way, we don't have to check all over the place whether an actual `Application` was returned. (For applications like pymux where we can have more than one `Application`, we'll use a work-around to handle that.) """ session = _current_app_session.get() if session.app is not None: return session.app from .dummy import DummyApplication return DummyApplication() def get_app_or_none() -> Optional["Application[Any]"]: """ Get the current active (running) Application, or return `None` if no application is running. """ session = _current_app_session.get() return session.app @contextmanager def set_app(app: "Application[Any]") -> Generator[None, None, None]: """ Context manager that sets the given :class:`.Application` active in an `AppSession`. This should only be called by the `Application` itself. The application will automatically be active while its running. If you want the application to be active in other threads/coroutines, where that's not the case, use `contextvars.copy_context()`, or use `Application.context` to run it in the appropriate context. """ session = _current_app_session.get() previous_app = session.app session.app = app try: yield finally: session.app = previous_app @contextmanager def create_app_session( input: Optional["Input"] = None, output: Optional["Output"] = None ) -> Generator[AppSession, None, None]: """ Create a separate AppSession. This is useful if there can be multiple individual `AppSession`s going on. Like in the case of an Telnet/SSH server. This functionality uses contextvars and requires at least Python 3.7. """ if sys.version_info <= (3, 6): raise RuntimeError("Application sessions require Python 3.7.") # If no input/output is specified, fall back to the current input/output, # whatever that is. if input is None: input = get_app_session().input if output is None: output = get_app_session().output # Create new `AppSession` and activate. session = AppSession(input=input, output=output) token = _current_app_session.set(session) try: yield session finally: _current_app_session.reset(token) @contextmanager def create_app_session_from_tty() -> Generator[AppSession, None, None]: """ Create `AppSession` that always prefers the TTY input/output. Even if `sys.stdin` and `sys.stdout` are connected to input/output pipes, this will still use the terminal for interaction (because `sys.stderr` is still connected to the terminal). Usage:: from prompt_toolkit.shortcuts import prompt with create_app_session_from_tty(): prompt('>') """ from prompt_toolkit.input.defaults import create_input from prompt_toolkit.output.defaults import create_output input = create_input(always_prefer_tty=True) output = create_output(always_prefer_tty=True) with create_app_session(input=input, output=output) as app_session: yield app_session