Source code for httk.core.datastream.textstream_file_view
import io
from collections.abc import Iterator
from typing import Any, NoReturn, Self
from ..views import unwrap
from .textstream_api import TextstreamAPI
from .textstream_backend import TextstreamBackend
from .textstream_like import TextstreamLike
from .textstream_view import TextstreamView
[docs]
class TextstreamFileView(TextstreamView, io.TextIOBase, TextstreamAPI):
r"""
A view presenting an underlying data streaming backend via the full io.TextIOBase API, which is a superset of TextstreamAPI.
:param obj: Text-stream source to present through the text file API.
:param \**hints: Backend-selection, encoding, and compression hints.
"""
_backend: TextstreamBackend
_readline_buffer: str
def __new__(cls, obj: TextstreamLike, **hints: Any) -> Self:
if isinstance(obj, cls):
return obj
backend = cls._prepare_backend(obj, hints)
instance = super().__new__(cls)
instance._backend = backend
instance._readline_buffer = ""
return instance
def __init__(self, obj: TextstreamLike, **hints: Any) -> None:
pass
[docs]
def unwrap(self) -> Any:
"""Return the raw representation of the wrapped backend.
:return: The backend's most raw available representation.
"""
return unwrap(self._backend)
@property
[docs]
def name(self) -> str | None:
"""Return the backend's source name when one exists."""
return self._backend.name
@property
[docs]
def closed(self) -> bool:
"""Report whether the backend is closed."""
return self._backend.closed
[docs]
def close(self) -> None:
"""Close the underlying backend."""
self._backend.close()
[docs]
def readable(self) -> bool:
"""Report that the view supports reading.
:return: Always ``True``.
"""
return True
[docs]
def writable(self) -> bool:
"""Report that the view does not support writing.
:return: Always ``False``.
"""
return False
[docs]
def seekable(self) -> bool:
"""Report whether the backend supports seeking and telling.
:return: Whether both operations are available.
"""
return hasattr(self._backend, "seek") and hasattr(self._backend, "tell")
[docs]
def flush(self) -> None:
"""Flush the backend when it provides flushing.
:raises ValueError: If the view is closed.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
flush = getattr(self._backend, "flush", None)
if flush:
flush()
[docs]
def read(self, size: int | None = -1) -> str:
"""Read up to ``size`` characters, or all remaining characters when ``size`` is negative.
:param size: Maximum number of characters to read; ``None`` also means all remaining characters.
:return: The text read from the stream.
:raises ValueError: If the view is closed.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
if size is None or size < 0:
if self._readline_buffer:
prefix = self._readline_buffer
self._readline_buffer = ""
return prefix + self._backend.read()
return self._backend.read()
if size == 0:
return ""
if self._readline_buffer:
if len(self._readline_buffer) >= size:
out = self._readline_buffer[:size]
self._readline_buffer = self._readline_buffer[size:]
return out
prefix = self._readline_buffer
self._readline_buffer = ""
return prefix + self._backend.read(size - len(prefix))
return self._backend.read(size)
[docs]
def readline(self, size: int | None = -1) -> str: # type: ignore[override] ## https://github.com/python/mypy/issues/9643
"""Read one line, optionally limited to ``size`` characters.
:param size: Maximum number of characters to read; ``None`` means no limit.
:return: The line read, including its newline when present.
:raises ValueError: If the view is closed.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
if size == 0:
return ""
if size is None:
size = -1
parts: list[str] = []
total = 0
while True:
if self._readline_buffer:
chunk = self._readline_buffer
self._readline_buffer = ""
else:
to_read = 8192
if size >= 0:
remaining = size - total
if remaining <= 0:
break
to_read = min(to_read, remaining)
chunk = self._backend.read(to_read)
if chunk == "":
break
newline_pos = chunk.find("\n")
if newline_pos != -1:
newline_pos += 1
take = chunk[:newline_pos]
rest = chunk[newline_pos:]
if size >= 0 and total + len(take) > size:
cutoff = size - total
parts.append(take[:cutoff])
self._readline_buffer = take[cutoff:] + rest
break
parts.append(take)
self._readline_buffer = rest
break
if size >= 0 and total + len(chunk) > size:
cutoff = size - total
parts.append(chunk[:cutoff])
self._readline_buffer = chunk[cutoff:]
break
parts.append(chunk)
total += len(chunk)
return "".join(parts)
[docs]
def readlines(self, hint: int = -1) -> list[str]: # type: ignore[override] ## https://github.com/python/mypy/issues/9643
"""Read lines until EOF or until the accumulated size reaches ``hint``.
:param hint: Approximate minimum number of characters to collect, or a negative value for no limit.
:return: The lines read from the stream.
:raises ValueError: If the view is closed.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
lines: list[str] = []
total = 0
while True:
line = self.readline()
if line == "":
break
lines.append(line)
total += len(line)
if hint >= 0 and total >= hint:
break
return lines
def __iter__(self) -> Iterator[str]: # type: ignore[override] ## https://github.com/python/mypy/issues/9643
"""Iterate over the stream one line at a time.
:return: This stream as its line iterator.
"""
return self
def __next__(self) -> str: # type: ignore[override] ## https://github.com/python/mypy/issues/9643
"""Return the next line from the stream.
:return: The next line.
:raises StopIteration: When the stream is exhausted.
"""
line = self.readline()
if line == "":
raise StopIteration
return line
[docs]
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
"""Move the stream position and discard buffered line data.
:param offset: Position adjustment interpreted according to ``whence``.
:param whence: Reference point for ``offset``.
:return: The resulting absolute stream position.
:raises ValueError: If the view is closed.
:raises io.UnsupportedOperation: If the backend is not seekable.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
seek = getattr(self._backend, "seek", None)
if not seek:
raise io.UnsupportedOperation("underlying stream is not seekable")
pos = seek(offset, whence)
self._readline_buffer = ""
return pos
[docs]
def tell(self) -> int:
"""Return the logical stream position before buffered line data.
:return: The logical absolute stream position.
:raises ValueError: If the view is closed.
:raises io.UnsupportedOperation: If the backend does not support telling.
"""
if self.closed:
raise ValueError("I/O operation on closed file.")
tell = getattr(self._backend, "tell") # noqa: B009 # tell is an optional dynamic backend capability
if not tell:
raise io.UnsupportedOperation("underlying stream does not support tell()")
pos = tell()
return pos - len(self._readline_buffer)
[docs]
def detach(self) -> NoReturn:
"""Reject detaching because the view owns its backend interface.
:raises io.UnsupportedOperation: Always, because detaching is unsupported.
"""
raise io.UnsupportedOperation("detach")
@property
[docs]
def encoding(self) -> str | None: # type: ignore[override] ## these should not be writable
"""Return the backend's text encoding when available."""
return getattr(self._backend, "encoding", None)
@property
[docs]
def errors(self) -> str | None: # type: ignore[override] ## these should not be writable
"""Return the backend's error handling mode when available."""
return getattr(self._backend, "errors", None)
@property
[docs]
def newlines(self) -> str | tuple[str, ...] | None: # type: ignore[override] ## these should not be writable
"""Return the newline conventions observed by the backend when available."""
return getattr(self._backend, "newlines", None)