Skip to content

Commit 60dc187

Browse files
authored
Add py314 to test matrix (#467)
1 parent b0b5df7 commit 60dc187

File tree

6 files changed

+336
-245
lines changed

6 files changed

+336
-245
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ jobs:
167167
fail-fast: false
168168

169169
matrix:
170-
python-version: ["3.10", "3.11", "3.12", "3.13"]
170+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
171171
postgres-version: ["13", "14", "15", "16", "17"]
172172
os: [ubuntu-24.04]
173173

pgqueuer/executors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import asyncio
66
import dataclasses
77
import functools
8+
import inspect
89
import random
910
from abc import ABC, abstractmethod
1011
from datetime import datetime, timedelta, timezone
@@ -49,8 +50,8 @@ def is_async_callable(obj: object) -> bool:
4950
while isinstance(obj, functools.partial):
5051
obj = obj.func
5152

52-
return asyncio.iscoroutinefunction(obj) or (
53-
callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
53+
return inspect.iscoroutinefunction(obj) or (
54+
callable(obj) and inspect.iscoroutinefunction(getattr(obj, "__call__", None))
5455
)
5556

5657

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ classifiers = [
3232
"Programming Language :: Python :: 3.11",
3333
"Programming Language :: Python :: 3.12",
3434
"Programming Language :: Python :: 3.13",
35+
"Programming Language :: Python :: 3.14",
3536
"Programming Language :: Python",
3637
"Topic :: Database",
3738
"Topic :: Software Development :: Libraries :: Python Modules",
@@ -46,7 +47,7 @@ dependencies = [
4647
"pydantic>=2.0.0",
4748
"tabulate>=0.9.0",
4849
"typer>=0.15.1",
49-
"uvloop>=0.21.0; sys_platform != 'win32'",
50+
"uvloop>=0.22.0; sys_platform != 'win32'",
5051
"async-timeout>=5.0.1",
5152
]
5253

test/conftest.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import asyncio
44
import os
55
import uuid
6-
from typing import AsyncGenerator
7-
from urllib.parse import urlparse, urlunparse
6+
from typing import Any, AsyncGenerator
7+
from urllib.parse import quote, urlparse, urlunparse
88

99
import asyncpg
1010
import psycopg
@@ -19,11 +19,53 @@
1919
except ModuleNotFoundError:
2020
uvloop = None # type: ignore[assignment]
2121

22-
from testcontainers.postgres import PostgresContainer
22+
from testcontainers.core.container import DockerContainer
23+
from testcontainers.core.wait_strategies import LogMessageWaitStrategy
2324

2425
from pgqueuer.db import SyncPsycopgDriver
2526

2627

28+
class PGQueuerPostgresContainer(DockerContainer):
29+
"""Postgres container with modern wait strategy support."""
30+
31+
def __init__(
32+
self,
33+
image: str = "postgres:latest",
34+
port: int = 5432,
35+
username: str | None = None,
36+
password: str | None = None,
37+
dbname: str | None = None,
38+
*,
39+
driver: str | None = "psycopg2",
40+
**kwargs: Any,
41+
) -> None:
42+
super().__init__(image=image, **kwargs)
43+
self.port = port
44+
self.username = username or os.environ.get("POSTGRES_USER", "test")
45+
self.password = password or os.environ.get("POSTGRES_PASSWORD", "test")
46+
self.dbname = dbname or os.environ.get("POSTGRES_DB", "test")
47+
self.driver_suffix = f"+{driver}" if driver else ""
48+
49+
self.with_exposed_ports(port)
50+
self.with_env("POSTGRES_USER", self.username)
51+
self.with_env("POSTGRES_PASSWORD", self.password)
52+
self.with_env("POSTGRES_DB", self.dbname)
53+
self.waiting_for(LogMessageWaitStrategy("database system is ready to accept connections"))
54+
55+
def get_connection_url(self, host: str | None = None) -> str:
56+
if self._container is None:
57+
msg = "container has not been started"
58+
raise RuntimeError(msg)
59+
60+
host = host or self.get_container_host_ip()
61+
port = self.get_exposed_port(self.port)
62+
quoted_password = quote(self.password, safe=" +")
63+
return (
64+
f"postgresql{self.driver_suffix}://{self.username}:{quoted_password}"
65+
f"@{host}:{port}/{self.dbname}"
66+
)
67+
68+
2769
@pytest.fixture(scope="session", autouse=True)
2870
def event_loop_policy() -> asyncio.AbstractEventLoopPolicy:
2971
"""Provide uvloop if available; fallback to default policy."""
@@ -63,7 +105,7 @@ async def postgres_container() -> AsyncGenerator[str, None]:
63105
] + (["-c", "vacuum_buffer_usage_limit=8MB"] if int(postgres_version) >= 16 else [])
64106

65107
container = (
66-
PostgresContainer(f"postgres:{postgres_version}", driver=None)
108+
PGQueuerPostgresContainer(f"postgres:{postgres_version}", driver=None)
67109
.with_command(commands)
68110
.with_kwargs(tmpfs={"/var/lib/pg/data": "rw"})
69111
.with_envs(PGDATA="/var/lib/pg/data")

test/test_factories.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ async def test_run_factory_with_invalid_input() -> None:
188188
"""
189189
invalid_input: str = "not a valid input"
190190

191-
with pytest.raises(TypeError, match="object str can't be used in 'await' expression"):
191+
with pytest.raises(TypeError) as exc_info:
192192
async with run_factory(invalid_input): # type: ignore[arg-type]
193193
pass # This line should not be reached
194+
195+
assert str(exc_info.value) in {
196+
"object str can't be used in 'await' expression",
197+
"'str' object can't be awaited",
198+
}

0 commit comments

Comments
 (0)