fixed subscription table

This commit is contained in:
2025-02-02 00:02:31 -05:00
parent a1ab31acfe
commit ef5f57e678
5389 changed files with 686710 additions and 28 deletions

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from .application import Application
from .current import (
AppSession,
create_app_session,
create_app_session_from_tty,
get_app,
get_app_or_none,
get_app_session,
set_app,
)
from .dummy import DummyApplication
from .run_in_terminal import in_terminal, run_in_terminal
__all__ = [
# Application.
"Application",
# Current.
"AppSession",
"get_app_session",
"create_app_session",
"create_app_session_from_tty",
"get_app",
"get_app_or_none",
"set_app",
# Dummy.
"DummyApplication",
# Run_in_terminal
"in_terminal",
"run_in_terminal",
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
from typing import TYPE_CHECKING, Any, Generator
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`
explicitly.
:param output: Use this as a default output.
"""
def __init__(
self, input: Input | None = None, output: Output | None = 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: Application[Any] | None = 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() -> Application[Any] | None:
"""
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: Input | None = None, output: Output | None = 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 a Telnet/SSH server.
"""
# If no input/output is specified, fall back to the current input/output,
# if there was one that was set/created for the current session.
# (Note that we check `_input`/`_output` and not `input`/`output`. This is
# because we don't want to accidently create a new input/output objects
# here and store it in the "parent" `AppSession`. Especially, when
# combining pytest's `capsys` fixture and `create_app_session`, sys.stdin
# and sys.stderr are patched for every test, so we don't want to leak
# those outputs object across `AppSession`s.)
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

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from typing import Callable
from prompt_toolkit.eventloop import InputHook
from prompt_toolkit.formatted_text import AnyFormattedText
from prompt_toolkit.input import DummyInput
from prompt_toolkit.output import DummyOutput
from .application import Application
__all__ = [
"DummyApplication",
]
class DummyApplication(Application[None]):
"""
When no :class:`.Application` is running,
:func:`.get_app` will run an instance of this :class:`.DummyApplication` instead.
"""
def __init__(self) -> None:
super().__init__(output=DummyOutput(), input=DummyInput())
def run(
self,
pre_run: Callable[[], None] | None = None,
set_exception_handler: bool = True,
handle_sigint: bool = True,
in_thread: bool = False,
inputhook: InputHook | None = None,
) -> None:
raise NotImplementedError("A DummyApplication is not supposed to run.")
async def run_async(
self,
pre_run: Callable[[], None] | None = None,
set_exception_handler: bool = True,
handle_sigint: bool = True,
slow_callback_duration: float = 0.5,
) -> None:
raise NotImplementedError("A DummyApplication is not supposed to run.")
async def run_system_command(
self,
command: str,
wait_for_enter: bool = True,
display_before_text: AnyFormattedText = "",
wait_text: str = "",
) -> None:
raise NotImplementedError
def suspend_to_background(self, suspend_group: bool = True) -> None:
raise NotImplementedError

View File

@@ -0,0 +1,117 @@
"""
Tools for running functions on the terminal above the current application or prompt.
"""
from __future__ import annotations
from asyncio import Future, ensure_future
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Awaitable, Callable, TypeVar
from prompt_toolkit.eventloop import run_in_executor_with_context
from .current import get_app_or_none
__all__ = [
"run_in_terminal",
"in_terminal",
]
_T = TypeVar("_T")
def run_in_terminal(
func: Callable[[], _T], render_cli_done: bool = False, in_executor: bool = False
) -> Awaitable[_T]:
"""
Run function on the terminal above the current application or prompt.
What this does is first hiding the prompt, then running this callable
(which can safely output to the terminal), and then again rendering the
prompt which causes the output of this function to scroll above the
prompt.
``func`` is supposed to be a synchronous function. If you need an
asynchronous version of this function, use the ``in_terminal`` context
manager directly.
:param func: The callable to execute.
:param render_cli_done: When True, render the interface in the
'Done' state first, then execute the function. If False,
erase the interface first.
:param in_executor: When True, run in executor. (Use this for long
blocking functions, when you don't want to block the event loop.)
:returns: A `Future`.
"""
async def run() -> _T:
async with in_terminal(render_cli_done=render_cli_done):
if in_executor:
return await run_in_executor_with_context(func)
else:
return func()
return ensure_future(run())
@asynccontextmanager
async def in_terminal(render_cli_done: bool = False) -> AsyncGenerator[None, None]:
"""
Asynchronous context manager that suspends the current application and runs
the body in the terminal.
.. code::
async def f():
async with in_terminal():
call_some_function()
await call_some_async_function()
"""
app = get_app_or_none()
if app is None or not app._is_running:
yield
return
# When a previous `run_in_terminal` call was in progress. Wait for that
# to finish, before starting this one. Chain to previous call.
previous_run_in_terminal_f = app._running_in_terminal_f
new_run_in_terminal_f: Future[None] = Future()
app._running_in_terminal_f = new_run_in_terminal_f
# Wait for the previous `run_in_terminal` to finish.
if previous_run_in_terminal_f is not None:
await previous_run_in_terminal_f
# Wait for all CPRs to arrive. We don't want to detach the input until
# all cursor position responses have been arrived. Otherwise, the tty
# will echo its input and can show stuff like ^[[39;1R.
if app.output.responds_to_cpr:
await app.renderer.wait_for_cpr_responses()
# Draw interface in 'done' state, or erase.
if render_cli_done:
app._redraw(render_as_done=True)
else:
app.renderer.erase()
# Disable rendering.
app._running_in_terminal = True
# Detach input.
try:
with app.input.detach():
with app.input.cooked_mode():
yield
finally:
# Redraw interface again.
try:
app._running_in_terminal = False
app.renderer.reset()
app._request_absolute_cursor_position()
app._redraw()
finally:
# (Check for `.done()`, because it can be that this future was
# cancelled.)
if not new_run_in_terminal_f.done():
new_run_in_terminal_f.set_result(None)