Format: text/x-python
⬇ Download File (5.0 KB)
   1  from __future__ import annotations
   2  
   3  import codecs
   4  from collections.abc import Callable, Mapping
   5  from dataclasses import InitVar, dataclass, field
   6  from typing import Any
   7  
   8  from ..abc import (
   9      AnyByteReceiveStream,
  10      AnyByteSendStream,
  11      AnyByteStream,
  12      ObjectReceiveStream,
  13      ObjectSendStream,
  14      ObjectStream,
  15  )
  16  
  17  
  18  @dataclass(eq=False)
  19  class TextReceiveStream(ObjectReceiveStream[str]):
  20      """
  21      Stream wrapper that decodes bytes to strings using the given encoding.
  22  
  23      Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any
  24      completely received unicode characters as soon as they come in.
  25  
  26      :param transport_stream: any bytes-based receive stream
  27      :param encoding: character encoding to use for decoding bytes to strings (defaults
  28          to ``utf-8``)
  29      :param errors: handling scheme for decoding errors (defaults to ``strict``; see the
  30          `codecs module documentation`_ for a comprehensive list of options)
  31  
  32      .. _codecs module documentation:
  33          https://docs.python.org/3/library/codecs.html#codec-objects
  34      """
  35  
  36      transport_stream: AnyByteReceiveStream
  37      encoding: InitVar[str] = "utf-8"
  38      errors: InitVar[str] = "strict"
  39      _decoder: codecs.IncrementalDecoder = field(init=False)
  40  
  41      def __post_init__(self, encoding: str, errors: str) -> None:
  42          decoder_class = codecs.getincrementaldecoder(encoding)
  43          self._decoder = decoder_class(errors=errors)
  44  
  45      async def receive(self) -> str:
  46          while True:
  47              chunk = await self.transport_stream.receive()
  48              decoded = self._decoder.decode(chunk)
  49              if decoded:
  50                  return decoded
  51  
  52      async def aclose(self) -> None:
  53          await self.transport_stream.aclose()
  54          self._decoder.reset()
  55  
  56      @property
  57      def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
  58          return self.transport_stream.extra_attributes
  59  
  60  
  61  @dataclass(eq=False)
  62  class TextSendStream(ObjectSendStream[str]):
  63      """
  64      Sends strings to the wrapped stream as bytes using the given encoding.
  65  
  66      :param AnyByteSendStream transport_stream: any bytes-based send stream
  67      :param str encoding: character encoding to use for encoding strings to bytes
  68          (defaults to ``utf-8``)
  69      :param str errors: handling scheme for encoding errors (defaults to ``strict``; see
  70          the `codecs module documentation`_ for a comprehensive list of options)
  71  
  72      .. _codecs module documentation:
  73          https://docs.python.org/3/library/codecs.html#codec-objects
  74      """
  75  
  76      transport_stream: AnyByteSendStream
  77      encoding: InitVar[str] = "utf-8"
  78      errors: str = "strict"
  79      _encoder: Callable[..., tuple[bytes, int]] = field(init=False)
  80  
  81      def __post_init__(self, encoding: str) -> None:
  82          self._encoder = codecs.getencoder(encoding)
  83  
  84      async def send(self, item: str) -> None:
  85          encoded = self._encoder(item, self.errors)[0]
  86          await self.transport_stream.send(encoded)
  87  
  88      async def aclose(self) -> None:
  89          await self.transport_stream.aclose()
  90  
  91      @property
  92      def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
  93          return self.transport_stream.extra_attributes
  94  
  95  
  96  @dataclass(eq=False)
  97  class TextStream(ObjectStream[str]):
  98      """
  99      A bidirectional stream that decodes bytes to strings on receive and encodes strings
 100      to bytes on send.
 101  
 102      Extra attributes will be provided from both streams, with the receive stream
 103      providing the values in case of a conflict.
 104  
 105      :param AnyByteStream transport_stream: any bytes-based stream
 106      :param str encoding: character encoding to use for encoding/decoding strings to/from
 107          bytes (defaults to ``utf-8``)
 108      :param str errors: handling scheme for encoding errors (defaults to ``strict``; see
 109          the `codecs module documentation`_ for a comprehensive list of options)
 110  
 111      .. _codecs module documentation:
 112          https://docs.python.org/3/library/codecs.html#codec-objects
 113      """
 114  
 115      transport_stream: AnyByteStream
 116      encoding: InitVar[str] = "utf-8"
 117      errors: InitVar[str] = "strict"
 118      _receive_stream: TextReceiveStream = field(init=False)
 119      _send_stream: TextSendStream = field(init=False)
 120  
 121      def __post_init__(self, encoding: str, errors: str) -> None:
 122          self._receive_stream = TextReceiveStream(
 123              self.transport_stream, encoding=encoding, errors=errors
 124          )
 125          self._send_stream = TextSendStream(
 126              self.transport_stream, encoding=encoding, errors=errors
 127          )
 128  
 129      async def receive(self) -> str:
 130          return await self._receive_stream.receive()
 131  
 132      async def send(self, item: str) -> None:
 133          await self._send_stream.send(item)
 134  
 135      async def send_eof(self) -> None:
 136          await self.transport_stream.send_eof()
 137  
 138      async def aclose(self) -> None:
 139          await self._send_stream.aclose()
 140          await self._receive_stream.aclose()
 141  
 142      @property
 143      def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
 144          return {
 145              **self._send_stream.extra_attributes,
 146              **self._receive_stream.extra_attributes,
 147          }
 148