Skip to content

Commit

Permalink
refactor: added Capabilities and BroadcastConnection interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ntamas committed Sep 4, 2024
1 parent 0d98ce8 commit 8b76352
Show file tree
Hide file tree
Showing 5 changed files with 265 additions and 31 deletions.
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,73 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Added `BroadcastConnection` interface to mark connections that support
broadcasting.

- Added `get_connection_capabilities()` to query the capabilities of a
connection. This replaces the earlier undocumented `can_send` class
properties.

## [7.2.1]

### Fixed

- BroadcastUDPListenerConnection now binds to the broadcast address.

## [7.2.0]

### Added

- Added support for binding a UDP connection to a specific interface.

## [7.1.0]

### Added

- Added logging middleware for connections.

## [7.0.0]

### Changed

- Updated Trio to 0.24, which introduces breaking changes in the exception
handling, hence it deserves a major version bump.

## [6.2.0]

### Removed

- Removed non-public `_broadcast_ttl` from UDPListenerConnection.

## [6.1.0]

### Added

- Added `create_loopback_connection_pair()` to create local loopback connections
for testing purposes.

## [6.0.0]

### Changed

- `flockwave-net` dependency bumped to 4.0.

## [5.3.0]

### Changed

- `DummyConnection` is now readable and writable.

## [5.2.0]

### Added

- Added `FDConnection` as a base class.

## [5.1.0] - 2022-09-20

### Added
Expand Down
46 changes: 42 additions & 4 deletions src/flockwave/connections/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from functools import partial
from trio import CancelScope, Event, Nursery, TASK_STATUS_IGNORED, wrap_file
from trio_util import AsyncBool
from typing import Any, Callable, Generic, Optional, TypeVar
from typing import Any, Callable, Generic, Optional, Protocol, TypeVar


__all__ = (
Expand Down Expand Up @@ -164,6 +164,21 @@ class RWConnection(ReadableConnection[RT], WritableConnection[WT]):
pass


class BroadcastConnection(Connection, Generic[T]):
"""Interface specification for connection objects that support
broadcasting.
"""

@abstractmethod
async def broadcast(self, data: T):
"""Broadcasts the given data on the connection.
Parameters:
data: the data to write
"""
raise NotImplementedError


class ConnectionBase(Connection):
"""Base class for stateful connection objects.
Expand Down Expand Up @@ -319,6 +334,16 @@ async def _close(self) -> None:
raise NotImplementedError


class AsyncFileLike(Protocol):
"""Interface specification for asynchronous file-like objects that can be
used in conjunction with FDConnectionBase_.
"""

async def flush(self) -> None: ...
async def read(self, size: int = -1) -> bytes: ...
async def write(self, data: bytes) -> None: ...


class FDConnectionBase(ConnectionBase, RWConnection[bytes, bytes], metaclass=ABCMeta):
"""Base class for connection objects that have an underlying numeric
file handle or file-like object.
Expand All @@ -335,12 +360,23 @@ class FDConnectionBase(ConnectionBase, RWConnection[bytes, bytes], metaclass=ABC
"""
)

_file_handle: Optional[int] = None
"""The file handle associated to the connection."""

_file_handle_owned: bool = False
"""Specifies whether the file handle is owned by this connection and should
be closed when the connection is closed.
"""

_file_object: Optional[AsyncFileLike] = None
"""The file-like object associated to the connection."""

autoflush: bool = False
"""Whether to flush the file handle automatically after each write."""

def __init__(self, autoflush: bool = False):
"""Constructor."""
super().__init__()
self._file_handle: Optional[int] = None
self._file_object = None
self._file_handle_owned: bool = False
self.autoflush = bool(autoflush)

def fileno(self) -> Optional[int]:
Expand Down Expand Up @@ -476,6 +512,7 @@ async def read(self, size: int = -1) -> bytes:
the data that was read, or an empty bytes object if the end of file
was reached
"""
assert self._file_object is not None
return await self._file_object.read(size)

async def write(self, data: bytes) -> None:
Expand All @@ -484,6 +521,7 @@ async def write(self, data: bytes) -> None:
Parameters:
data: the data to write
"""
assert self._file_object is not None
await self._file_object.write(data)
if self.autoflush:
await self.flush()
Expand Down
60 changes: 60 additions & 0 deletions src/flockwave/connections/capabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

from typing import Protocol, TypedDict, TYPE_CHECKING, runtime_checkable

if TYPE_CHECKING:
from .base import Connection

__all__ = ("Capabilities", "CapabilitySupport", "get_connection_capabilities")


class Capabilities(TypedDict):
"""Typed dictionary that contains information about the capabilities of a
connection.
"""

can_broadcast: bool
"""Stores whether the connection can send broadcast packets."""

can_receive: bool
"""Stores whether the connection can receive data."""

can_send: bool
"""Stores whether the connection can send data."""


@runtime_checkable
class CapabilitySupport(Protocol):
"""Interface specification for class-level properties that must be present
on a Connection_ object to provide _overridden_ information about the
capabilities of the connection.
In the absence of the method described in this protocol, the capabilities
of the connection will be inferred from its inheritance hierarchy.
"""

def _get_capabilities(self) -> Capabilities:
"""Returns the capabilities of a connection.
Capabilities are not expected to change during the lifetime of a
connection.
"""
...


def get_connection_capabilities(conn: Connection) -> Capabilities:
"""Returns the capabilities of the given connection.
Args:
conn: the connection whose capabilities are queried
"""
from .base import BroadcastConnection, ReadableConnection, WritableConnection

if isinstance(conn, CapabilitySupport):
return conn._get_capabilities()
else:
return {
"can_broadcast": isinstance(conn, BroadcastConnection),
"can_receive": isinstance(conn, ReadableConnection),
"can_send": isinstance(conn, WritableConnection),
}
16 changes: 16 additions & 0 deletions src/flockwave/connections/errors.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
__all__ = (
"AddressError",
"ConnectionError",
"UnknownConnectionTypeError",
"UnknownMiddlewareTypeError",
)


class AddressError(RuntimeError):
"""Base class for addressing-related errors."""

pass


class ConnectionError(RuntimeError):
"""Base class for connection-related errors."""

pass


class NoBroadcastAddressError(AddressError):
"""Error thrown when there is no broadcast address associated to a connection
and it would be needed for a broadcast operation.
"""

def __init__(self, message: str = ""):
super().__init__(message or "No broadcast address")


class UnknownConnectionTypeError(RuntimeError):
"""Exception thrown when trying to construct a connection with an
unknown type.
Expand Down
Loading

0 comments on commit 8b76352

Please sign in to comment.