220 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
	
from __future__ import annotations
 | 
						|
 | 
						|
import code
 | 
						|
import sys
 | 
						|
import typing as t
 | 
						|
from contextvars import ContextVar
 | 
						|
from types import CodeType
 | 
						|
 | 
						|
from markupsafe import escape
 | 
						|
 | 
						|
from .repr import debug_repr
 | 
						|
from .repr import dump
 | 
						|
from .repr import helper
 | 
						|
 | 
						|
_stream: ContextVar[HTMLStringO] = ContextVar("werkzeug.debug.console.stream")
 | 
						|
_ipy: ContextVar[_InteractiveConsole] = ContextVar("werkzeug.debug.console.ipy")
 | 
						|
 | 
						|
 | 
						|
class HTMLStringO:
 | 
						|
    """A StringO version that HTML escapes on write."""
 | 
						|
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self._buffer: list[str] = []
 | 
						|
 | 
						|
    def isatty(self) -> bool:
 | 
						|
        return False
 | 
						|
 | 
						|
    def close(self) -> None:
 | 
						|
        pass
 | 
						|
 | 
						|
    def flush(self) -> None:
 | 
						|
        pass
 | 
						|
 | 
						|
    def seek(self, n: int, mode: int = 0) -> None:
 | 
						|
        pass
 | 
						|
 | 
						|
    def readline(self) -> str:
 | 
						|
        if len(self._buffer) == 0:
 | 
						|
            return ""
 | 
						|
        ret = self._buffer[0]
 | 
						|
        del self._buffer[0]
 | 
						|
        return ret
 | 
						|
 | 
						|
    def reset(self) -> str:
 | 
						|
        val = "".join(self._buffer)
 | 
						|
        del self._buffer[:]
 | 
						|
        return val
 | 
						|
 | 
						|
    def _write(self, x: str) -> None:
 | 
						|
        self._buffer.append(x)
 | 
						|
 | 
						|
    def write(self, x: str) -> None:
 | 
						|
        self._write(escape(x))
 | 
						|
 | 
						|
    def writelines(self, x: t.Iterable[str]) -> None:
 | 
						|
        self._write(escape("".join(x)))
 | 
						|
 | 
						|
 | 
						|
class ThreadedStream:
 | 
						|
    """Thread-local wrapper for sys.stdout for the interactive console."""
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def push() -> None:
 | 
						|
        if not isinstance(sys.stdout, ThreadedStream):
 | 
						|
            sys.stdout = t.cast(t.TextIO, ThreadedStream())
 | 
						|
 | 
						|
        _stream.set(HTMLStringO())
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def fetch() -> str:
 | 
						|
        try:
 | 
						|
            stream = _stream.get()
 | 
						|
        except LookupError:
 | 
						|
            return ""
 | 
						|
 | 
						|
        return stream.reset()
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def displayhook(obj: object) -> None:
 | 
						|
        try:
 | 
						|
            stream = _stream.get()
 | 
						|
        except LookupError:
 | 
						|
            return _displayhook(obj)  # type: ignore
 | 
						|
 | 
						|
        # stream._write bypasses escaping as debug_repr is
 | 
						|
        # already generating HTML for us.
 | 
						|
        if obj is not None:
 | 
						|
            _ipy.get().locals["_"] = obj
 | 
						|
            stream._write(debug_repr(obj))
 | 
						|
 | 
						|
    def __setattr__(self, name: str, value: t.Any) -> None:
 | 
						|
        raise AttributeError(f"read only attribute {name}")
 | 
						|
 | 
						|
    def __dir__(self) -> list[str]:
 | 
						|
        return dir(sys.__stdout__)
 | 
						|
 | 
						|
    def __getattribute__(self, name: str) -> t.Any:
 | 
						|
        try:
 | 
						|
            stream = _stream.get()
 | 
						|
        except LookupError:
 | 
						|
            stream = sys.__stdout__  # type: ignore[assignment]
 | 
						|
 | 
						|
        return getattr(stream, name)
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return repr(sys.__stdout__)
 | 
						|
 | 
						|
 | 
						|
# add the threaded stream as display hook
 | 
						|
_displayhook = sys.displayhook
 | 
						|
sys.displayhook = ThreadedStream.displayhook
 | 
						|
 | 
						|
 | 
						|
class _ConsoleLoader:
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self._storage: dict[int, str] = {}
 | 
						|
 | 
						|
    def register(self, code: CodeType, source: str) -> None:
 | 
						|
        self._storage[id(code)] = source
 | 
						|
        # register code objects of wrapped functions too.
 | 
						|
        for var in code.co_consts:
 | 
						|
            if isinstance(var, CodeType):
 | 
						|
                self._storage[id(var)] = source
 | 
						|
 | 
						|
    def get_source_by_code(self, code: CodeType) -> str | None:
 | 
						|
        try:
 | 
						|
            return self._storage[id(code)]
 | 
						|
        except KeyError:
 | 
						|
            return None
 | 
						|
 | 
						|
 | 
						|
class _InteractiveConsole(code.InteractiveInterpreter):
 | 
						|
    locals: dict[str, t.Any]
 | 
						|
 | 
						|
    def __init__(self, globals: dict[str, t.Any], locals: dict[str, t.Any]) -> None:
 | 
						|
        self.loader = _ConsoleLoader()
 | 
						|
        locals = {
 | 
						|
            **globals,
 | 
						|
            **locals,
 | 
						|
            "dump": dump,
 | 
						|
            "help": helper,
 | 
						|
            "__loader__": self.loader,
 | 
						|
        }
 | 
						|
        super().__init__(locals)
 | 
						|
        original_compile = self.compile
 | 
						|
 | 
						|
        def compile(source: str, filename: str, symbol: str) -> CodeType | None:
 | 
						|
            code = original_compile(source, filename, symbol)
 | 
						|
 | 
						|
            if code is not None:
 | 
						|
                self.loader.register(code, source)
 | 
						|
 | 
						|
            return code
 | 
						|
 | 
						|
        self.compile = compile  # type: ignore[assignment]
 | 
						|
        self.more = False
 | 
						|
        self.buffer: list[str] = []
 | 
						|
 | 
						|
    def runsource(self, source: str, **kwargs: t.Any) -> str:  # type: ignore
 | 
						|
        source = f"{source.rstrip()}\n"
 | 
						|
        ThreadedStream.push()
 | 
						|
        prompt = "... " if self.more else ">>> "
 | 
						|
        try:
 | 
						|
            source_to_eval = "".join(self.buffer + [source])
 | 
						|
            if super().runsource(source_to_eval, "<debugger>", "single"):
 | 
						|
                self.more = True
 | 
						|
                self.buffer.append(source)
 | 
						|
            else:
 | 
						|
                self.more = False
 | 
						|
                del self.buffer[:]
 | 
						|
        finally:
 | 
						|
            output = ThreadedStream.fetch()
 | 
						|
        return f"{prompt}{escape(source)}{output}"
 | 
						|
 | 
						|
    def runcode(self, code: CodeType) -> None:
 | 
						|
        try:
 | 
						|
            exec(code, self.locals)
 | 
						|
        except Exception:
 | 
						|
            self.showtraceback()
 | 
						|
 | 
						|
    def showtraceback(self) -> None:
 | 
						|
        from .tbtools import DebugTraceback
 | 
						|
 | 
						|
        exc = t.cast(BaseException, sys.exc_info()[1])
 | 
						|
        te = DebugTraceback(exc, skip=1)
 | 
						|
        sys.stdout._write(te.render_traceback_html())  # type: ignore
 | 
						|
 | 
						|
    def showsyntaxerror(self, filename: str | None = None) -> None:
 | 
						|
        from .tbtools import DebugTraceback
 | 
						|
 | 
						|
        exc = t.cast(BaseException, sys.exc_info()[1])
 | 
						|
        te = DebugTraceback(exc, skip=4)
 | 
						|
        sys.stdout._write(te.render_traceback_html())  # type: ignore
 | 
						|
 | 
						|
    def write(self, data: str) -> None:
 | 
						|
        sys.stdout.write(data)
 | 
						|
 | 
						|
 | 
						|
class Console:
 | 
						|
    """An interactive console."""
 | 
						|
 | 
						|
    def __init__(
 | 
						|
        self,
 | 
						|
        globals: dict[str, t.Any] | None = None,
 | 
						|
        locals: dict[str, t.Any] | None = None,
 | 
						|
    ) -> None:
 | 
						|
        if locals is None:
 | 
						|
            locals = {}
 | 
						|
        if globals is None:
 | 
						|
            globals = {}
 | 
						|
        self._ipy = _InteractiveConsole(globals, locals)
 | 
						|
 | 
						|
    def eval(self, code: str) -> str:
 | 
						|
        _ipy.set(self._ipy)
 | 
						|
        old_sys_stdout = sys.stdout
 | 
						|
        try:
 | 
						|
            return self._ipy.runsource(code)
 | 
						|
        finally:
 | 
						|
            sys.stdout = old_sys_stdout
 |