diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 900834d..81db761 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] services: zookeeper: diff --git a/broadcaster/__init__.py b/broadcaster/__init__.py index b5dd0bf..a41bbeb 100644 --- a/broadcaster/__init__.py +++ b/broadcaster/__init__.py @@ -1,5 +1,5 @@ -from ._base import Broadcast, Event from ._backends.base import BroadcastBackend +from ._base import Broadcast, Event __version__ = "0.2.0" __all__ = ["Broadcast", "Event", "BroadcastBackend"] diff --git a/broadcaster/_backends/kafka.py b/broadcaster/_backends/kafka.py index 18b88d2..e577769 100644 --- a/broadcaster/_backends/kafka.py +++ b/broadcaster/_backends/kafka.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing from urllib.parse import urlparse @@ -10,7 +12,7 @@ class KafkaBackend(BroadcastBackend): def __init__(self, url: str): self._servers = [urlparse(url).netloc] - self._consumer_channels: typing.Set = set() + self._consumer_channels: set[str] = set() async def connect(self) -> None: self._producer = AIOKafkaProducer(bootstrap_servers=self._servers) diff --git a/broadcaster/_backends/memory.py b/broadcaster/_backends/memory.py index 5a9fa53..bfd0c44 100644 --- a/broadcaster/_backends/memory.py +++ b/broadcaster/_backends/memory.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import typing @@ -7,10 +9,10 @@ class MemoryBackend(BroadcastBackend): def __init__(self, url: str): - self._subscribed: typing.Set = set() + self._subscribed: set[str] = set() async def connect(self) -> None: - self._published: asyncio.Queue = asyncio.Queue() + self._published: asyncio.Queue[Event] = asyncio.Queue() async def disconnect(self) -> None: pass diff --git a/broadcaster/_backends/postgres.py b/broadcaster/_backends/postgres.py index 47ef4f6..7769962 100644 --- a/broadcaster/_backends/postgres.py +++ b/broadcaster/_backends/postgres.py @@ -13,7 +13,7 @@ def __init__(self, url: str): async def connect(self) -> None: self._conn = await asyncpg.connect(self._url) - self._listen_queue: asyncio.Queue = asyncio.Queue() + self._listen_queue: asyncio.Queue[Event] = asyncio.Queue() async def disconnect(self) -> None: await self._conn.close() diff --git a/broadcaster/_base.py b/broadcaster/_base.py index 997b82a..0166034 100644 --- a/broadcaster/_base.py +++ b/broadcaster/_base.py @@ -1,14 +1,8 @@ +from __future__ import annotations + import asyncio from contextlib import asynccontextmanager -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - AsyncIterator, - Dict, - Optional, - cast, -) +from typing import TYPE_CHECKING, Any, AsyncGenerator, AsyncIterator, cast from urllib.parse import urlparse if TYPE_CHECKING: # pragma: no cover @@ -21,11 +15,7 @@ def __init__(self, channel: str, message: str) -> None: self.message = message def __eq__(self, other: object) -> bool: - return ( - isinstance(other, Event) - and self.channel == other.channel - and self.message == other.message - ) + return isinstance(other, Event) and self.channel == other.channel and self.message == other.message def __repr__(self) -> str: return f"Event(channel={self.channel!r}, message={self.message!r})" @@ -36,14 +26,12 @@ class Unsubscribed(Exception): class Broadcast: - def __init__( - self, url: Optional[str] = None, *, backend: Optional["BroadcastBackend"] = None - ) -> None: + def __init__(self, url: str | None = None, *, backend: BroadcastBackend | None = None) -> None: assert url or backend, "Either `url` or `backend` must be provided." self._backend = backend or self._create_backend(cast(str, url)) - self._subscribers: Dict[str, Any] = {} + self._subscribers: dict[str, set[asyncio.Queue[Event | None]]] = {} - def _create_backend(self, url: str) -> "BroadcastBackend": + def _create_backend(self, url: str) -> BroadcastBackend: parsed_url = urlparse(url) if parsed_url.scheme in ("redis", "rediss"): from broadcaster._backends.redis import RedisBackend @@ -66,7 +54,7 @@ def _create_backend(self, url: str) -> "BroadcastBackend": return MemoryBackend(url) raise ValueError(f"Unsupported backend: {parsed_url.scheme}") - async def __aenter__(self) -> "Broadcast": + async def __aenter__(self) -> Broadcast: await self.connect() return self @@ -94,8 +82,8 @@ async def publish(self, channel: str, message: Any) -> None: await self._backend.publish(channel, message) @asynccontextmanager - async def subscribe(self, channel: str) -> AsyncIterator["Subscriber"]: - queue: asyncio.Queue = asyncio.Queue() + async def subscribe(self, channel: str) -> AsyncIterator[Subscriber]: + queue: asyncio.Queue[Event | None] = asyncio.Queue() try: if not self._subscribers.get(channel): @@ -114,10 +102,10 @@ async def subscribe(self, channel: str) -> AsyncIterator["Subscriber"]: class Subscriber: - def __init__(self, queue: asyncio.Queue) -> None: + def __init__(self, queue: asyncio.Queue[Event | None]) -> None: self._queue = queue - async def __aiter__(self) -> Optional[AsyncGenerator]: + async def __aiter__(self) -> AsyncGenerator[Event | None, None] | None: try: while True: yield await self.get() diff --git a/pyproject.toml b/pyproject.toml index 10f7cc5..c4e8036 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "anyio>=3.4.0,<5", @@ -48,14 +49,17 @@ include = [ ] [tool.ruff] -ignore = [] line-length = 120 -select = ["E","F","W"] -[tool.ruff.isort] +[tool.ruff.lint] +select = ["E", "F", "I", "FA", "UP"] + +[tool.ruff.lint.isort] combine-as-imports = true [tool.mypy] +strict = true +python_version = "3.8" disallow_untyped_defs = true ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index dcf0fcf..ed2926b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,17 @@ -e .[redis,postgres,kafka] # Documentation -mkdocs==1.3.1 +mkdocs==1.5.3 +mkdocs-material==9.5.12 mkautodoc==0.2.0 -mkdocs-material==8.4.0 # Packaging -build==0.10.0 -twine==4.0.1 +build==1.1.1 +twine==5.0.0 # Tests & Linting -ruff==0.0.277 -black==22.6.0 -coverage==6.4.4 -mypy==0.971 -pytest==7.1.2 -pytest-asyncio==0.19.0 \ No newline at end of file +ruff==0.3.5 +coverage==7.4.3 +mypy==1.8.0 +pytest==8.0.2 +pytest-asyncio==0.23.6 \ No newline at end of file diff --git a/scripts/check b/scripts/check index e514548..d8fb02b 100755 --- a/scripts/check +++ b/scripts/check @@ -8,6 +8,6 @@ export SOURCE_FILES="broadcaster tests" set -x -${PREFIX}black --check --diff --target-version=py37 $SOURCE_FILES -${PREFIX}ruff check $SOURCE_FILES +${PREFIX}ruff format --check --diff $SOURCE_FILES ${PREFIX}mypy $SOURCE_FILES +${PREFIX}ruff check $SOURCE_FILES diff --git a/scripts/lint b/scripts/lint index 4751285..cb718d0 100755 --- a/scripts/lint +++ b/scripts/lint @@ -1,12 +1,12 @@ #!/bin/sh -e export PREFIX="" -if [ -d 'venv' ]; then +if [ -d 'venv' ] ; then export PREFIX="venv/bin/" fi export SOURCE_FILES="broadcaster tests" set -x -${PREFIX}ruff --fix $SOURCE_FILES -${PREFIX}black --target-version=py37 $SOURCE_FILES +${PREFIX}ruff format $SOURCE_FILES +${PREFIX}ruff check --fix $SOURCE_FILES \ No newline at end of file diff --git a/tests/test_broadcast.py b/tests/test_broadcast.py index 4cf9e45..b516ee2 100644 --- a/tests/test_broadcast.py +++ b/tests/test_broadcast.py @@ -1,16 +1,19 @@ -import pytest -import typing +from __future__ import annotations + import asyncio +import typing + +import pytest from broadcaster import Broadcast, BroadcastBackend, Event class CustomBackend(BroadcastBackend): def __init__(self, url: str): - self._subscribed: typing.Set = set() + self._subscribed: set[str] = set() async def connect(self) -> None: - self._published: asyncio.Queue = asyncio.Queue() + self._published: asyncio.Queue[Event] = asyncio.Queue() async def disconnect(self) -> None: pass @@ -54,9 +57,7 @@ async def test_redis(): @pytest.mark.asyncio async def test_postgres(): - async with Broadcast( - "postgres://postgres:postgres@localhost:5432/broadcaster" - ) as broadcast: + async with Broadcast("postgres://postgres:postgres@localhost:5432/broadcaster") as broadcast: async with broadcast.subscribe("chatroom") as subscriber: await broadcast.publish("chatroom", "hello") event = await subscriber.get() @@ -95,7 +96,5 @@ async def test_unknown_backend(): @pytest.mark.asyncio async def test_needs_url_or_backend(): - with pytest.raises( - AssertionError, match="Either `url` or `backend` must be provided." - ): + with pytest.raises(AssertionError, match="Either `url` or `backend` must be provided."): Broadcast() diff --git a/tests/test_unsubscribe.py b/tests/test_unsubscribe.py index 30f928b..ae89401 100644 --- a/tests/test_unsubscribe.py +++ b/tests/test_unsubscribe.py @@ -1,4 +1,5 @@ import pytest + from broadcaster import Broadcast