Format: text/x-python
⬇ Download File (5.9 KB)
   1  from __future__ import annotations
   2  
   3  import ssl
   4  import typing
   5  
   6  import trio
   7  
   8  from .._exceptions import (
   9      ConnectError,
  10      ConnectTimeout,
  11      ExceptionMapping,
  12      ReadError,
  13      ReadTimeout,
  14      WriteError,
  15      WriteTimeout,
  16      map_exceptions,
  17  )
  18  from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
  19  
  20  
  21  class TrioStream(AsyncNetworkStream):
  22      def __init__(self, stream: trio.abc.Stream) -> None:
  23          self._stream = stream
  24  
  25      async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
  26          timeout_or_inf = float("inf") if timeout is None else timeout
  27          exc_map: ExceptionMapping = {
  28              trio.TooSlowError: ReadTimeout,
  29              trio.BrokenResourceError: ReadError,
  30              trio.ClosedResourceError: ReadError,
  31          }
  32          with map_exceptions(exc_map):
  33              with trio.fail_after(timeout_or_inf):
  34                  data: bytes = await self._stream.receive_some(max_bytes=max_bytes)
  35                  return data
  36  
  37      async def write(self, buffer: bytes, timeout: float | None = None) -> None:
  38          if not buffer:
  39              return
  40  
  41          timeout_or_inf = float("inf") if timeout is None else timeout
  42          exc_map: ExceptionMapping = {
  43              trio.TooSlowError: WriteTimeout,
  44              trio.BrokenResourceError: WriteError,
  45              trio.ClosedResourceError: WriteError,
  46          }
  47          with map_exceptions(exc_map):
  48              with trio.fail_after(timeout_or_inf):
  49                  await self._stream.send_all(data=buffer)
  50  
  51      async def aclose(self) -> None:
  52          await self._stream.aclose()
  53  
  54      async def start_tls(
  55          self,
  56          ssl_context: ssl.SSLContext,
  57          server_hostname: str | None = None,
  58          timeout: float | None = None,
  59      ) -> AsyncNetworkStream:
  60          timeout_or_inf = float("inf") if timeout is None else timeout
  61          exc_map: ExceptionMapping = {
  62              trio.TooSlowError: ConnectTimeout,
  63              trio.BrokenResourceError: ConnectError,
  64          }
  65          ssl_stream = trio.SSLStream(
  66              self._stream,
  67              ssl_context=ssl_context,
  68              server_hostname=server_hostname,
  69              https_compatible=True,
  70              server_side=False,
  71          )
  72          with map_exceptions(exc_map):
  73              try:
  74                  with trio.fail_after(timeout_or_inf):
  75                      await ssl_stream.do_handshake()
  76              except Exception as exc:  # pragma: nocover
  77                  await self.aclose()
  78                  raise exc
  79          return TrioStream(ssl_stream)
  80  
  81      def get_extra_info(self, info: str) -> typing.Any:
  82          if info == "ssl_object" and isinstance(self._stream, trio.SSLStream):
  83              # Type checkers cannot see `_ssl_object` attribute because trio._ssl.SSLStream uses __getattr__/__setattr__.
  84              # Tracked at https://github.com/python-trio/trio/issues/542
  85              return self._stream._ssl_object  # type: ignore[attr-defined]
  86          if info == "client_addr":
  87              return self._get_socket_stream().socket.getsockname()
  88          if info == "server_addr":
  89              return self._get_socket_stream().socket.getpeername()
  90          if info == "socket":
  91              stream = self._stream
  92              while isinstance(stream, trio.SSLStream):
  93                  stream = stream.transport_stream
  94              assert isinstance(stream, trio.SocketStream)
  95              return stream.socket
  96          if info == "is_readable":
  97              socket = self.get_extra_info("socket")
  98              return socket.is_readable()
  99          return None
 100  
 101      def _get_socket_stream(self) -> trio.SocketStream:
 102          stream = self._stream
 103          while isinstance(stream, trio.SSLStream):
 104              stream = stream.transport_stream
 105          assert isinstance(stream, trio.SocketStream)
 106          return stream
 107  
 108  
 109  class TrioBackend(AsyncNetworkBackend):
 110      async def connect_tcp(
 111          self,
 112          host: str,
 113          port: int,
 114          timeout: float | None = None,
 115          local_address: str | None = None,
 116          socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
 117      ) -> AsyncNetworkStream:
 118          # By default for TCP sockets, trio enables TCP_NODELAY.
 119          # https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream
 120          if socket_options is None:
 121              socket_options = []  # pragma: no cover
 122          timeout_or_inf = float("inf") if timeout is None else timeout
 123          exc_map: ExceptionMapping = {
 124              trio.TooSlowError: ConnectTimeout,
 125              trio.BrokenResourceError: ConnectError,
 126              OSError: ConnectError,
 127          }
 128          with map_exceptions(exc_map):
 129              with trio.fail_after(timeout_or_inf):
 130                  stream: trio.abc.Stream = await trio.open_tcp_stream(
 131                      host=host, port=port, local_address=local_address
 132                  )
 133                  for option in socket_options:
 134                      stream.setsockopt(*option)  # type: ignore[attr-defined] # pragma: no cover
 135          return TrioStream(stream)
 136  
 137      async def connect_unix_socket(
 138          self,
 139          path: str,
 140          timeout: float | None = None,
 141          socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
 142      ) -> AsyncNetworkStream:  # pragma: nocover
 143          if socket_options is None:
 144              socket_options = []
 145          timeout_or_inf = float("inf") if timeout is None else timeout
 146          exc_map: ExceptionMapping = {
 147              trio.TooSlowError: ConnectTimeout,
 148              trio.BrokenResourceError: ConnectError,
 149              OSError: ConnectError,
 150          }
 151          with map_exceptions(exc_map):
 152              with trio.fail_after(timeout_or_inf):
 153                  stream: trio.abc.Stream = await trio.open_unix_socket(path)
 154                  for option in socket_options:
 155                      stream.setsockopt(*option)  # type: ignore[attr-defined] # pragma: no cover
 156          return TrioStream(stream)
 157  
 158      async def sleep(self, seconds: float) -> None:
 159          await trio.sleep(seconds)  # pragma: nocover
 160