Rename the import package app to fluksio
A wheel whose top-level module is `app` collides with anything else in a user's venv, so the package that is about to be published takes the name it is published under. Only the Python package moves; the repo, the Docker WORKDIR and the compose project keep theirs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Capture what a node prints, so the editor can show it.
|
||||
|
||||
Node code is written by the user and ``print`` is the obvious way to look at a
|
||||
value, but a node runs on a pool thread and its output would otherwise land
|
||||
unattributed in the server log. ``sys.stdout`` is replaced once by a tee that
|
||||
also hands what it is given to whichever capture is active on *this* thread —
|
||||
so only node executions are captured, and everything else passes through
|
||||
untouched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
import traceback
|
||||
from collections.abc import Callable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from typing import TextIO
|
||||
|
||||
# Set for the duration of one node execution, on the thread running it.
|
||||
_sink: ContextVar[Callable[[str], None] | None] = ContextVar(
|
||||
"fluksio_node_log_sink", default=None
|
||||
)
|
||||
|
||||
|
||||
class _Tee(io.TextIOBase):
|
||||
"""Writes through to the real stream, and to the active capture."""
|
||||
|
||||
def __init__(self, real: TextIO) -> None:
|
||||
self._real = real
|
||||
|
||||
def write(self, text: str) -> int:
|
||||
sink = _sink.get()
|
||||
if sink is not None and text:
|
||||
sink(text)
|
||||
return self._real.write(text)
|
||||
|
||||
def flush(self) -> None:
|
||||
self._real.flush()
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self._real.isatty()
|
||||
|
||||
|
||||
def install() -> None:
|
||||
"""Put the tee in place. Safe to call more than once."""
|
||||
if not isinstance(sys.stdout, _Tee):
|
||||
sys.stdout = _Tee(sys.stdout)
|
||||
if not isinstance(sys.stderr, _Tee):
|
||||
sys.stderr = _Tee(sys.stderr)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture(sink: Callable[[str], None]) -> Iterator[None]:
|
||||
"""Send everything printed on this thread to ``sink`` for the duration."""
|
||||
token = _sink.set(sink)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_sink.reset(token)
|
||||
|
||||
|
||||
def node_traceback() -> str:
|
||||
"""The exception being handled, from the node's own code onward.
|
||||
|
||||
The frames above it are the engine calling the node, which is noise to the
|
||||
person who wrote it — the same trimming ``_short_error`` does for the one
|
||||
line shown on the node itself.
|
||||
"""
|
||||
exc_type, exc, tb = sys.exc_info()
|
||||
if exc is None:
|
||||
return ""
|
||||
# A node running out of process already trimmed its own; the frames on this
|
||||
# side are the RPC that carried it.
|
||||
remote = getattr(exc, "remote_traceback", "")
|
||||
if remote:
|
||||
return str(remote)
|
||||
frames = traceback.extract_tb(tb)
|
||||
start = next(
|
||||
(i for i, frame in enumerate(frames) if frame.filename.startswith("<node ")),
|
||||
0,
|
||||
)
|
||||
return "".join(
|
||||
["Traceback (most recent call last):\n"]
|
||||
+ traceback.format_list(frames[start:])
|
||||
+ traceback.format_exception_only(exc_type, exc)
|
||||
)
|
||||
|
||||
|
||||
class Collector:
|
||||
"""Gathers one execution's output, with a ceiling on how much it keeps."""
|
||||
|
||||
__slots__ = ("chunks", "truncated", "_limit", "_size", "_max_bytes")
|
||||
|
||||
def __init__(self, limit: int = 200, max_bytes: int = 8192) -> None:
|
||||
self.chunks: list[str] = []
|
||||
self.truncated = False
|
||||
self._limit = limit
|
||||
self._max_bytes = max_bytes
|
||||
self._size = 0
|
||||
|
||||
def __call__(self, text: str) -> None:
|
||||
if len(self.chunks) >= self._limit or self._size >= self._max_bytes:
|
||||
self.truncated = True
|
||||
return
|
||||
self.chunks.append(text)
|
||||
self._size += len(text)
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
out = "".join(self.chunks)
|
||||
return out[: self._max_bytes] if len(out) > self._max_bytes else out
|
||||
Reference in New Issue
Block a user