Source code for progressbar.terminal.stream

"""`typing.TextIO` wrappers that intercept or redirect stream writes.

`TextIOOutputWrapper` is a pass-through base that delegates every
`TextIO` operation to a wrapped stream unchanged. Concrete wrappers
subclass it and override only the operation(s) they need to change
(typically `write`). Two concrete wrappers:

- `LineOffsetStreamWrapper` writes a fixed number of lines above the
  current cursor position instead of at it, used by `ProgressBar`'s
  `line_offset=` argument.
- `LastLineStream` discards everything but the most recently written
  line, used by `MultiBar` to capture a bar's rendered output without
  letting it reach the terminal directly.

Every non-underscore name here is re-exported by `progressbar.terminal`
(``from .stream import *``, no ``__all__``). `LineOffsetStreamWrapper`
is also part of the top-level `progressbar` public API.
"""

from __future__ import annotations

import sys
import typing
from collections.abc import Generator, Iterable, Iterator
from types import TracebackType

from progressbar import base


[docs] class TextIOOutputWrapper(base.TextIO): # pragma: no cover """Pass-through `TextIO` base that delegates to a wrapped stream. Every operation forwards to `self.stream` unchanged, and subclasses override only what they need to intercept. `write` is left unimplemented: this class is meant to be subclassed, not used as-is. """ def __init__(self, stream: base.TextIO) -> None: """Store the stream to delegate to.""" self.stream = stream
[docs] def close(self) -> None: """Delegate to the wrapped stream.""" self.stream.close()
[docs] def fileno(self) -> int: """Delegate to the wrapped stream.""" return self.stream.fileno()
[docs] def flush(self) -> None: """Delegate to the wrapped stream.""" self.stream.flush()
[docs] def isatty(self) -> bool: """Delegate to the wrapped stream.""" return self.stream.isatty()
[docs] def read(self, __n: int = -1) -> str: """Delegate to the wrapped stream.""" return self.stream.read(__n)
[docs] def readable(self) -> bool: """Delegate to the wrapped stream.""" return self.stream.readable()
[docs] def readline(self, __limit: int = -1) -> str: """Delegate to the wrapped stream.""" return self.stream.readline(__limit)
[docs] def readlines(self, __hint: int = -1) -> list[str]: """Delegate to the wrapped stream.""" return self.stream.readlines(__hint)
[docs] def seek(self, __offset: int, __whence: int = 0) -> int: """Delegate to the wrapped stream.""" return self.stream.seek(__offset, __whence)
[docs] def seekable(self) -> bool: """Delegate to the wrapped stream.""" return self.stream.seekable()
[docs] def tell(self) -> int: """Delegate to the wrapped stream.""" return self.stream.tell()
[docs] def truncate(self, __size: int | None = None) -> int: """Delegate to the wrapped stream.""" return self.stream.truncate(__size)
[docs] def writable(self) -> bool: """Delegate to the wrapped stream.""" return self.stream.writable()
[docs] def writelines(self, __lines: Iterable[str]) -> None: """Delegate to the wrapped stream.""" return self.stream.writelines(__lines)
def __next__(self) -> str: """Delegate to the wrapped stream.""" return self.stream.__next__() def __iter__(self) -> Iterator[str]: """Delegate to the wrapped stream.""" return self.stream.__iter__() def __exit__( self, __t: type[BaseException] | None, __value: BaseException | None, __traceback: TracebackType | None, ) -> None: """Delegate to the wrapped stream.""" return self.stream.__exit__(__t, __value, __traceback) def __enter__(self) -> base.TextIO: """Delegate to the wrapped stream.""" return self.stream.__enter__()
[docs] class LineOffsetStreamWrapper(TextIOOutputWrapper): """Writes land a fixed number of lines above the cursor. Each `write` moves the cursor up `lines` rows, writes there, then moves back down, leaving the cursor where it started. Used by `ProgressBar`'s `line_offset=` argument to draw a bar above other terminal output instead of at the current line. """ #: ANSI "cursor up one line" (CSI ``F``). UP = '\033[F' #: ANSI "cursor down one line" (CSI ``B``). DOWN = '\033[B' def __init__( self, lines: int = 0, stream: typing.TextIO = sys.stderr ) -> None: """Store the offset and the stream writes are redirected to. Args: lines: Number of lines above the current cursor position to write to. stream: The underlying stream to write to. """ self.lines = lines super().__init__(stream)
[docs] def write(self, data: str) -> int: """Write `data` `self.lines` rows above the cursor. Moves the cursor up, writes `data` with trailing newlines stripped (so the write itself doesn't move the cursor), then moves back down to restore the original position. Args: data: Text to write. Returns: The length of `data` before newline-stripping, so callers can detect short writes. """ written = len(data) data = data.rstrip('\n') # Move the cursor up self.stream.write(self.UP * self.lines) # Print a carriage return to reset the cursor position self.stream.write('\r') # Print the data without newlines so we don't change the position self.stream.write(data) # Move the cursor down self.stream.write(self.DOWN * self.lines) self.flush() return written
[docs] class LastLineStream(TextIOOutputWrapper): """Discards everything but the most recently written line. `MultiBar` rebinds a bar's `fd` to one of these so the bar's own redraws never reach the terminal directly. The multibar instead reads `.line` back out and places it on that bar's row of the combined frame. """ #: The most recently written line. Only `write`, `writelines` and #: `truncate` touch this. Every other inherited method still #: operates on the wrapped stream. line: str = ''
[docs] def seekable(self) -> bool: """Always `False`: there is nothing to seek within.""" return False
[docs] def readable(self) -> bool: """Always `True`: `.line` can always be read back.""" return True
[docs] def read(self, __n: int = -1) -> str: """Return `.line`, or its first `__n` characters.""" if __n < 0: return self.line else: return self.line[:__n]
[docs] def readline(self, __limit: int = -1) -> str: """Return `.line`, or its first `__limit` characters.""" if __limit < 0: return self.line else: return self.line[:__limit]
[docs] def write(self, data: str) -> int: """Replace `.line` with `data`.""" self.line = data return len(data)
[docs] def truncate(self, __size: int | None = None) -> int: """Clear `.line`, or cut it to its first `__size` characters.""" if __size is None: self.line = '' else: self.line = self.line[:__size] return len(self.line)
def __iter__(self) -> Generator[str, typing.Any, typing.Any]: """Yield `.line` as the stream's sole line.""" yield self.line
[docs] def writelines(self, __lines: Iterable[str]) -> None: """Keep only the last of `__lines`, discarding the rest. Deliberate: `MultiBar` only ever wants a bar's most recent rendered line, not its history. """ line = '' # Walk through the lines and take the last one for line in __lines: # noqa: B007 pass self.line = line