hack-house/.venv/lib/python3.12/site-packages/sanic/cli/console.py
leetcrypt bb1d662ee1 chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:29:14 -07:00

310 lines
9.0 KiB
Python

from __future__ import annotations
import atexit
import concurrent.futures
import sys
import threading
import time
import traceback
try:
import termios
TERMIOS_AVAILABLE = True
except ImportError:
TERMIOS_AVAILABLE = False
termios = None # type: ignore
from ast import PyCF_ALLOW_TOP_LEVEL_AWAIT
from asyncio import iscoroutine, new_event_loop
from code import InteractiveConsole
from collections.abc import Sequence
from contextlib import suppress
from types import FunctionType
from typing import Any, NamedTuple
import sanic
from sanic import Request, Sanic
from sanic.compat import Header
from sanic.helpers import Default
from sanic.http.constants import Stage
from sanic.log import Colors
from sanic.models.ctx_types import REPLContext
from sanic.models.protocol_types import TransportProtocol
from sanic.response.types import HTTPResponse
try:
from httpx import Client
HTTPX_AVAILABLE = True
class SanicClient(Client):
def __init__(self, app: Sanic):
base_url = app.get_server_location(
app.state.server_info[0].settings
)
super().__init__(base_url=base_url)
except ImportError:
HTTPX_AVAILABLE = False
try:
import readline # noqa
except ImportError:
print(
"Module 'readline' not available. History navigation will be limited.",
file=sys.stderr,
)
repl_app: Sanic | None = None
repl_response: HTTPResponse | None = None
class REPLProtocol(TransportProtocol):
def __init__(self):
self.stage = Stage.IDLE
self.request_body = True
def respond(self, response):
global repl_response
repl_response = response
response.stream = self
return response
async def send(self, data, end_stream): ...
class Result(NamedTuple):
request: Request
response: HTTPResponse
def make_request(
url: str = "/",
headers: dict[str, Any] | Sequence[tuple[str, str]] | None = None,
method: str = "GET",
body: str | None = None,
):
assert repl_app, "No Sanic app has been registered."
headers = headers or {}
protocol = REPLProtocol()
request = Request( # type: ignore
url.encode(),
Header(headers),
"1.1",
method,
protocol,
repl_app,
)
if body is not None:
request.body = body.encode()
request.stream = protocol # type: ignore
request.conn_info = None
return request
async def respond(request) -> HTTPResponse:
assert repl_app, "No Sanic app has been registered."
await repl_app.handle_request(request)
assert repl_response
return repl_response
async def do(
url: str = "/",
headers: dict[str, Any] | Sequence[tuple[str, str]] | None = None,
method: str = "GET",
body: str | None = None,
) -> Result:
request = make_request(url, headers, method, body)
response = await respond(request)
return Result(request, response)
def _variable_description(name: str, desc: str, type_desc: str) -> str:
return (
f" - {Colors.BOLD + Colors.SANIC}{name}{Colors.END}: {desc} - "
f"{Colors.BOLD + Colors.BLUE}{type_desc}{Colors.END}"
)
class SanicREPL(InteractiveConsole):
def __init__(self, app: Sanic, start: Default | None = None):
global repl_app
repl_app = app
locals_available = {
"app": app,
"sanic": sanic,
"do": do,
}
user_locals = {
user_local.name: user_local.var for user_local in app.repl_ctx
}
client_availability = ""
variable_descriptions = [
_variable_description(
"app", REPLContext.BUILTINS["app"], str(app)
),
_variable_description(
"sanic", REPLContext.BUILTINS["sanic"], "import sanic"
),
_variable_description(
"do", REPLContext.BUILTINS["do"], "Result(request, response)"
),
]
user_locals_descriptions = [
_variable_description(
user_local.name, user_local.desc, str(type(user_local.var))
)
for user_local in app.repl_ctx
]
if HTTPX_AVAILABLE:
locals_available["client"] = SanicClient(app)
variable_descriptions.append(
_variable_description(
"client",
REPLContext.BUILTINS["client"],
"from httpx import Client",
),
)
else:
client_availability = (
f"\n{Colors.YELLOW}The HTTP client has been disabled. "
"To enable it, install httpx:\n\t"
f"pip install httpx{Colors.END}\n"
)
super().__init__(locals={**locals_available, **user_locals})
self.compile.compiler.flags |= PyCF_ALLOW_TOP_LEVEL_AWAIT
self.loop = new_event_loop()
self._start = start
self._pause_event = threading.Event()
self._started_event = threading.Event()
self._interact_thread = threading.Thread(
target=self._console,
daemon=True,
)
self._monitor_thread = threading.Thread(
target=self._monitor,
daemon=True,
)
self._async_thread = threading.Thread(
target=self.loop.run_forever,
daemon=True,
)
self.app = app
self.resume()
self.exit_message = "Closing the REPL."
self.banner_message = "\n".join(
[
f"\n{Colors.BOLD}Welcome to the Sanic interactive console{Colors.END}", # noqa: E501
client_availability,
"The following objects are available for your convenience:", # noqa: E501
*variable_descriptions,
]
+ (
[
"\nREPL Context:",
*user_locals_descriptions,
]
if user_locals_descriptions
else []
)
+ [
"\nThe async/await keywords are available for use here.", # noqa: E501
f"To exit, press {Colors.BOLD}CTRL+C{Colors.END}, "
f"{Colors.BOLD}CTRL+D{Colors.END}, or type {Colors.BOLD}exit(){Colors.END}.\n", # noqa: E501
]
)
def pause(self):
if self.is_paused():
return
self._pause_event.clear()
def resume(self):
self._pause_event.set()
def runsource(self, source, filename="<input>", symbol="single"):
if source.strip() == "exit()":
self._shutdown()
return False
if self.is_paused():
print("Console is paused. Please wait for it to be resumed.")
return False
return super().runsource(source, filename, symbol)
def runcode(self, code):
future = concurrent.futures.Future()
async def callback():
func = FunctionType(code, self.locals)
try:
result = func()
if iscoroutine(result):
result = await result
except BaseException:
traceback.print_exc()
result = False
future.set_result(result)
self.loop.call_soon_threadsafe(self.loop.create_task, callback())
return future.result()
def is_paused(self):
return not self._pause_event.is_set()
def _console(self):
self._started_event.set()
self.interact(banner=self.banner_message, exitmsg=self.exit_message)
self._shutdown()
def _setup_terminal(self):
assert termios is not None
with suppress(termios.error, AttributeError):
fd = sys.stdin.fileno()
old_attrs = termios.tcgetattr(fd)
atexit.register(
termios.tcsetattr, fd, termios.TCSADRAIN, old_attrs
)
def _monitor(self):
if isinstance(self._start, Default):
if TERMIOS_AVAILABLE and sys.stdin.isatty():
self._setup_terminal()
enter = f"{Colors.BOLD + Colors.SANIC}ENTER{Colors.END}"
start = input(f"\nPress {enter} at anytime to start the REPL.\n\n")
if start:
return
try:
while True:
if not self._started_event.is_set():
self.app.manager.wait_for_ack()
self._interact_thread.start()
elif self.app.manager._all_workers_ack() and self.is_paused():
self.resume()
print(sys.ps1, end="", flush=True)
elif (
not self.app.manager._all_workers_ack()
and not self.is_paused()
):
self.pause()
time.sleep(0.1)
except (ConnectionResetError, BrokenPipeError):
pass
def _shutdown(self):
self.app.manager.monitor_publisher.send("__TERMINATE__")
def run(self):
self._monitor_thread.start()
self._async_thread.start()