Skip to content

Commit f80ac0d

Browse files
committed
Add Python 3.10 and 3.11 compatibility
1 parent 0ebb6c3 commit f80ac0d

File tree

15 files changed

+537
-66
lines changed

15 files changed

+537
-66
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: ["3.12", "3.13"]
17+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1818
backend:
1919
- name: "Redis 6.2, redis-py <5"
2020
redis-version: "6.2"
@@ -62,7 +62,7 @@ jobs:
6262
- name: Install uv and set Python version
6363
uses: astral-sh/setup-uv@v5
6464
with:
65-
python-version: "3.12"
65+
python-version: "3.10"
6666
enable-cache: true
6767
cache-dependency-glob: "pyproject.toml"
6868

pyproject.toml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ name = "pydocket"
77
dynamic = ["version"]
88
description = "A distributed background task system for Python functions"
99
readme = { file = "README.md", content-type = "text/markdown" }
10-
requires-python = ">=3.12"
10+
requires-python = ">=3.10"
1111
license = { file = "LICENSE" }
1212
authors = [{ name = "Chris Guidry", email = "guid@omg.lol" }]
1313
classifiers = [
1414
"Development Status :: 4 - Beta",
1515
"Programming Language :: Python :: 3",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
1618
"Programming Language :: Python :: 3.12",
1719
"Programming Language :: Python :: 3.13",
1820
"License :: OSI Approved :: MIT License",
@@ -21,13 +23,15 @@ classifiers = [
2123
]
2224
dependencies = [
2325
"cloudpickle>=3.1.1",
26+
"exceptiongroup>=1.2.0; python_version < '3.11'",
2427
"opentelemetry-api>=1.30.0",
2528
"opentelemetry-exporter-prometheus>=0.51b0",
2629
"prometheus-client>=0.21.1",
2730
"python-json-logger>=3.2.1",
2831
"redis>=4.6",
2932
"rich>=13.9.4",
3033
"typer>=0.15.1",
34+
"typing_extensions>=4.12.0",
3135
"uuid7>=0.1.0",
3236
]
3337

@@ -39,7 +43,7 @@ dev = [
3943
# This fixes xpending_range to return all 4 required fields (message_id, consumer,
4044
# time_since_delivered, times_delivered) instead of just 2, matching Redis behavior
4145
"fakeredis[lua] @ git+https://github.com/zzstoatzz/fakeredis-py.git@fix-xpending-range-fields",
42-
"ipython>=9.0.1",
46+
"ipython>=8.0.0",
4347
"mypy>=1.14.1",
4448
"opentelemetry-distro>=0.51b0",
4549
"opentelemetry-exporter-otlp>=1.30.0",
@@ -81,14 +85,13 @@ allow-direct-references = true
8185
packages = ["src/docket"]
8286

8387
[tool.ruff]
84-
target-version = "py312"
88+
target-version = "py310"
8589

8690
[tool.pytest.ini_options]
8791
addopts = [
8892
"--numprocesses=logical",
8993
"--maxprocesses=4",
9094
"--cov=src/docket",
91-
"--cov=tests",
9295
"--cov-report=term-missing",
9396
"--cov-branch",
9497
]

src/docket/annotations.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import abc
22
import inspect
3-
from typing import Any, Iterable, Mapping, Self
3+
from typing import Any, Iterable, Mapping
4+
5+
from typing_extensions import Self
46

57
from .instrumentation import CACHE_SIZE
68

src/docket/cli.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
)
2626

2727

28-
class LogLevel(enum.StrEnum):
28+
class LogLevel(str, enum.Enum):
2929
DEBUG = "DEBUG"
3030
INFO = "INFO"
3131
WARNING = "WARNING"
3232
ERROR = "ERROR"
3333
CRITICAL = "CRITICAL"
3434

3535

36-
class LogFormat(enum.StrEnum):
36+
class LogFormat(str, enum.Enum):
3737
RICH = "rich"
3838
PLAIN = "plain"
3939
JSON = "json"
@@ -111,7 +111,7 @@ def set_logging_format(format: LogFormat) -> None:
111111

112112

113113
def set_logging_level(level: LogLevel) -> None:
114-
logging.getLogger().setLevel(level)
114+
logging.getLogger().setLevel(level.value)
115115

116116

117117
def handle_strike_wildcard(value: str) -> str | None:
@@ -347,7 +347,7 @@ def strike(
347347
value_ = interpret_python_value(value)
348348
if parameter:
349349
function_name = f"{function or '(all tasks)'}"
350-
print(f"Striking {function_name} {parameter} {operator} {value_!r}")
350+
print(f"Striking {function_name} {parameter} {operator.value} {value_!r}")
351351
else:
352352
print(f"Striking {function}")
353353

@@ -436,7 +436,7 @@ def restore(
436436
value_ = interpret_python_value(value)
437437
if parameter:
438438
function_name = f"{function or '(all tasks)'}"
439-
print(f"Restoring {function_name} {parameter} {operator} {value_!r}")
439+
print(f"Restoring {function_name} {parameter} {operator.value} {value_!r}")
440440
else:
441441
print(f"Restoring {function}")
442442

src/docket/dependencies.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ async def greet_customer(customer_id: int, name: str = Depends(customer_name)) -
161161

162162

163163
class _TaskLogger(Dependency):
164-
async def __aenter__(self) -> logging.LoggerAdapter[logging.Logger]:
164+
async def __aenter__(self) -> "logging.LoggerAdapter[logging.Logger]":
165165
execution = self.execution.get()
166166
logger = logging.getLogger(f"docket.task.{execution.function.__name__}")
167167
return logging.LoggerAdapter(
@@ -174,7 +174,7 @@ async def __aenter__(self) -> logging.LoggerAdapter[logging.Logger]:
174174
)
175175

176176

177-
def TaskLogger() -> logging.LoggerAdapter[logging.Logger]:
177+
def TaskLogger() -> "logging.LoggerAdapter[logging.Logger]":
178178
"""A dependency to access a logger for the currently executing task. The logger
179179
will automatically inject contextual information such as the worker and docket
180180
name, the task key, and the current execution attempt number.
@@ -183,11 +183,11 @@ def TaskLogger() -> logging.LoggerAdapter[logging.Logger]:
183183
184184
```python
185185
@task
186-
async def my_task(logger: LoggerAdapter[Logger] = TaskLogger()) -> None:
186+
async def my_task(logger: "LoggerAdapter[Logger]" = TaskLogger()) -> None:
187187
logger.info("Hello, world!")
188188
```
189189
"""
190-
return cast(logging.LoggerAdapter[logging.Logger], _TaskLogger())
190+
return cast("logging.LoggerAdapter[logging.Logger]", _TaskLogger())
191191

192192

193193
class ForcedRetry(Exception):

src/docket/docket.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
NoReturn,
1818
ParamSpec,
1919
Protocol,
20-
Self,
2120
Sequence,
2221
TypedDict,
2322
TypeVar,
2423
cast,
2524
overload,
2625
)
2726

27+
from typing_extensions import Self
28+
2829
import redis.exceptions
2930
from opentelemetry import propagate, trace
3031
from redis.asyncio import ConnectionPool, Redis

src/docket/execution.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,9 @@
33
import inspect
44
import logging
55
from datetime import datetime
6-
from typing import (
7-
Any,
8-
Awaitable,
9-
Callable,
10-
Hashable,
11-
Literal,
12-
Mapping,
13-
Self,
14-
cast,
15-
)
6+
from typing import Any, Awaitable, Callable, Hashable, Literal, Mapping, cast
7+
8+
from typing_extensions import Self
169

1710
import cloudpickle # type: ignore[import]
1811
import opentelemetry.context
@@ -35,6 +28,12 @@ def get_signature(function: Callable[..., Any]) -> inspect.Signature:
3528
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
3629
return _signature_cache[function]
3730

31+
signature_attr = getattr(function, "__signature__", None)
32+
if isinstance(signature_attr, inspect.Signature):
33+
_signature_cache[function] = signature_attr
34+
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
35+
return signature_attr
36+
3837
signature = inspect.signature(function)
3938
_signature_cache[function] = signature
4039
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
@@ -161,7 +160,7 @@ def compact_signature(signature: inspect.Signature) -> str:
161160
return ", ".join(parameters)
162161

163162

164-
class Operator(enum.StrEnum):
163+
class Operator(str, enum.Enum):
165164
EQUAL = "=="
166165
NOT_EQUAL = "!="
167166
GREATER_THAN = ">"

src/docket/instrumentation.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,14 @@ def metrics_server(
193193
yield
194194
return
195195

196-
from wsgiref.types import WSGIApplication
196+
import sys
197+
from typing import Any
198+
199+
# wsgiref.types was added in Python 3.11
200+
if sys.version_info >= (3, 11): # pragma: no cover
201+
from wsgiref.types import WSGIApplication
202+
else: # pragma: no cover
203+
WSGIApplication = Any # type: ignore[misc,assignment]
197204

198205
from prometheus_client import REGISTRY
199206
from prometheus_client.exposition import (

src/docket/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
async def trace(
1818
message: str,
19-
logger: logging.LoggerAdapter[logging.Logger] = TaskLogger(),
19+
logger: "logging.LoggerAdapter[logging.Logger]" = TaskLogger(),
2020
docket: Docket = CurrentDocket(),
2121
worker: Worker = CurrentWorker(),
2222
execution: Execution = CurrentExecution(),
@@ -46,7 +46,7 @@ async def fail(
4646

4747

4848
async def sleep(
49-
seconds: float, logger: logging.LoggerAdapter[logging.Logger] = TaskLogger()
49+
seconds: float, logger: "logging.LoggerAdapter[logging.Logger]" = TaskLogger()
5050
) -> None:
5151
logger.info("Sleeping for %s seconds", seconds)
5252
await asyncio.sleep(seconds)

src/docket/worker.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import time
77
from datetime import datetime, timedelta, timezone
88
from types import TracebackType
9-
from typing import (
10-
Coroutine,
11-
Mapping,
12-
Protocol,
13-
Self,
14-
cast,
15-
)
9+
from typing import Coroutine, Mapping, Protocol, cast
10+
11+
if sys.version_info < (3, 11): # pragma: no cover
12+
from exceptiongroup import ExceptionGroup
13+
14+
from typing_extensions import Self
1615

1716
from opentelemetry import trace
1817
from opentelemetry.trace import Status, StatusCode, Tracer
@@ -167,16 +166,18 @@ async def run(
167166
for task_path in tasks:
168167
docket.register_collection(task_path)
169168

170-
async with Worker(
171-
docket=docket,
172-
name=name,
173-
concurrency=concurrency,
174-
redelivery_timeout=redelivery_timeout,
175-
reconnection_delay=reconnection_delay,
176-
minimum_check_interval=minimum_check_interval,
177-
scheduling_resolution=scheduling_resolution,
178-
schedule_automatic_tasks=schedule_automatic_tasks,
179-
) as worker:
169+
async with (
170+
Worker( # pragma: no branch - context manager exit varies across interpreters
171+
docket=docket,
172+
name=name,
173+
concurrency=concurrency,
174+
redelivery_timeout=redelivery_timeout,
175+
reconnection_delay=reconnection_delay,
176+
minimum_check_interval=minimum_check_interval,
177+
scheduling_resolution=scheduling_resolution,
178+
schedule_automatic_tasks=schedule_automatic_tasks,
179+
) as worker
180+
):
180181
if until_finished:
181182
await worker.run_until_finished()
182183
else:

0 commit comments

Comments
 (0)