149 lines
4.3 KiB
Python
149 lines
4.3 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
from collections.abc import Callable, Mapping
|
||
|
from io import SEEK_SET, UnsupportedOperation
|
||
|
from os import PathLike
|
||
|
from pathlib import Path
|
||
|
from typing import Any, BinaryIO, cast
|
||
|
|
||
|
from .. import (
|
||
|
BrokenResourceError,
|
||
|
ClosedResourceError,
|
||
|
EndOfStream,
|
||
|
TypedAttributeSet,
|
||
|
to_thread,
|
||
|
typed_attribute,
|
||
|
)
|
||
|
from ..abc import ByteReceiveStream, ByteSendStream
|
||
|
|
||
|
|
||
|
class FileStreamAttribute(TypedAttributeSet):
|
||
|
#: the open file descriptor
|
||
|
file: BinaryIO = typed_attribute()
|
||
|
#: the path of the file on the file system, if available (file must be a real file)
|
||
|
path: Path = typed_attribute()
|
||
|
#: the file number, if available (file must be a real file or a TTY)
|
||
|
fileno: int = typed_attribute()
|
||
|
|
||
|
|
||
|
class _BaseFileStream:
|
||
|
def __init__(self, file: BinaryIO):
|
||
|
self._file = file
|
||
|
|
||
|
async def aclose(self) -> None:
|
||
|
await to_thread.run_sync(self._file.close)
|
||
|
|
||
|
@property
|
||
|
def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
|
||
|
attributes: dict[Any, Callable[[], Any]] = {
|
||
|
FileStreamAttribute.file: lambda: self._file,
|
||
|
}
|
||
|
|
||
|
if hasattr(self._file, "name"):
|
||
|
attributes[FileStreamAttribute.path] = lambda: Path(self._file.name)
|
||
|
|
||
|
try:
|
||
|
self._file.fileno()
|
||
|
except UnsupportedOperation:
|
||
|
pass
|
||
|
else:
|
||
|
attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno()
|
||
|
|
||
|
return attributes
|
||
|
|
||
|
|
||
|
class FileReadStream(_BaseFileStream, ByteReceiveStream):
|
||
|
"""
|
||
|
A byte stream that reads from a file in the file system.
|
||
|
|
||
|
:param file: a file that has been opened for reading in binary mode
|
||
|
|
||
|
.. versionadded:: 3.0
|
||
|
"""
|
||
|
|
||
|
@classmethod
|
||
|
async def from_path(cls, path: str | PathLike[str]) -> FileReadStream:
|
||
|
"""
|
||
|
Create a file read stream by opening the given file.
|
||
|
|
||
|
:param path: path of the file to read from
|
||
|
|
||
|
"""
|
||
|
file = await to_thread.run_sync(Path(path).open, "rb")
|
||
|
return cls(cast(BinaryIO, file))
|
||
|
|
||
|
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||
|
try:
|
||
|
data = await to_thread.run_sync(self._file.read, max_bytes)
|
||
|
except ValueError:
|
||
|
raise ClosedResourceError from None
|
||
|
except OSError as exc:
|
||
|
raise BrokenResourceError from exc
|
||
|
|
||
|
if data:
|
||
|
return data
|
||
|
else:
|
||
|
raise EndOfStream
|
||
|
|
||
|
async def seek(self, position: int, whence: int = SEEK_SET) -> int:
|
||
|
"""
|
||
|
Seek the file to the given position.
|
||
|
|
||
|
.. seealso:: :meth:`io.IOBase.seek`
|
||
|
|
||
|
.. note:: Not all file descriptors are seekable.
|
||
|
|
||
|
:param position: position to seek the file to
|
||
|
:param whence: controls how ``position`` is interpreted
|
||
|
:return: the new absolute position
|
||
|
:raises OSError: if the file is not seekable
|
||
|
|
||
|
"""
|
||
|
return await to_thread.run_sync(self._file.seek, position, whence)
|
||
|
|
||
|
async def tell(self) -> int:
|
||
|
"""
|
||
|
Return the current stream position.
|
||
|
|
||
|
.. note:: Not all file descriptors are seekable.
|
||
|
|
||
|
:return: the current absolute position
|
||
|
:raises OSError: if the file is not seekable
|
||
|
|
||
|
"""
|
||
|
return await to_thread.run_sync(self._file.tell)
|
||
|
|
||
|
|
||
|
class FileWriteStream(_BaseFileStream, ByteSendStream):
|
||
|
"""
|
||
|
A byte stream that writes to a file in the file system.
|
||
|
|
||
|
:param file: a file that has been opened for writing in binary mode
|
||
|
|
||
|
.. versionadded:: 3.0
|
||
|
"""
|
||
|
|
||
|
@classmethod
|
||
|
async def from_path(
|
||
|
cls, path: str | PathLike[str], append: bool = False
|
||
|
) -> FileWriteStream:
|
||
|
"""
|
||
|
Create a file write stream by opening the given file for writing.
|
||
|
|
||
|
:param path: path of the file to write to
|
||
|
:param append: if ``True``, open the file for appending; if ``False``, any
|
||
|
existing file at the given path will be truncated
|
||
|
|
||
|
"""
|
||
|
mode = "ab" if append else "wb"
|
||
|
file = await to_thread.run_sync(Path(path).open, mode)
|
||
|
return cls(cast(BinaryIO, file))
|
||
|
|
||
|
async def send(self, item: bytes) -> None:
|
||
|
try:
|
||
|
await to_thread.run_sync(self._file.write, item)
|
||
|
except ValueError:
|
||
|
raise ClosedResourceError from None
|
||
|
except OSError as exc:
|
||
|
raise BrokenResourceError from exc
|