From 9b35d20c5e30db39507ebe541d8f7e92799dc4b4 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 18 Oct 2026 13:10:50 +0200 Subject: [PATCH 1/3] Accept schedule tuples in `TaskTiger.task()`. `periodic()` and `cron_expr()` return a `(function, args)` tuple, and the README tells users to give this tuple to `schedule`, for example `@tiger.task(schedule=periodic(hours=1))`. But `task()` declares `schedule` as `Optional[Callable]`, so type checkers reject this usual call. TaskTiger accepts both forms at runtime. `Task._queue_for_next_period()` calls a callable directly, and it unpacks a tuple into a function and its arguments. Declare the same two forms in the type hint. The new `ScheduleStrategy` alias names the tuple form, as `RetryStrategy` does for retry methods. --- tasktiger/schedule.py | 8 +++++--- tasktiger/tasktiger.py | 8 +++++++- tasktiger/types.py | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tasktiger/schedule.py b/tasktiger/schedule.py index 35aa85f6..dae11bbe 100644 --- a/tasktiger/schedule.py +++ b/tasktiger/schedule.py @@ -1,5 +1,7 @@ import datetime -from typing import Callable, Optional, Tuple +from typing import Optional + +from .types import ScheduleStrategy __all__ = ["periodic", "cron_expr"] @@ -40,7 +42,7 @@ def periodic( weeks: int = 0, start_date: Optional[datetime.datetime] = None, end_date: Optional[datetime.datetime] = None, -) -> Tuple[Callable[..., Optional[datetime.datetime]], Tuple]: +) -> ScheduleStrategy: """ Periodic task schedule: Use to schedule a task to run periodically, starting from start_date (or None to be active immediately) until end_date @@ -94,7 +96,7 @@ def cron_expr( expr: str, start_date: Optional[datetime.datetime] = None, end_date: Optional[datetime.datetime] = None, -) -> Tuple[Callable[..., Optional[datetime.datetime]], Tuple]: +) -> ScheduleStrategy: """ Periodic task schedule via cron expression: Use to schedule a task to run periodically, starting from start_date (or None to be active immediately) until end_date diff --git a/tasktiger/tasktiger.py b/tasktiger/tasktiger.py index aeb611b9..29cc4cc8 100644 --- a/tasktiger/tasktiger.py +++ b/tasktiger/tasktiger.py @@ -36,6 +36,7 @@ from .retry import fixed from .runner import BaseRunner from .task import Task +from .types import ScheduleStrategy from .worker import LOCK_REDIS_KEY, Worker __all__ = ["TaskTiger"] @@ -315,7 +316,12 @@ def task( retry_method: Optional[ Union[Callable[[int], float], Tuple[Callable[..., float], Tuple]] ] = None, - schedule: Optional[Callable] = None, + schedule: Optional[ + Union[ + Callable[[datetime.datetime], Optional[datetime.datetime]], + ScheduleStrategy, + ] + ] = None, batch: bool = False, max_queue_size: Optional[int] = None, max_stored_executions: Optional[int] = None, diff --git a/tasktiger/types.py b/tasktiger/types.py index c0e39498..635790b2 100644 --- a/tasktiger/types.py +++ b/tasktiger/types.py @@ -1,3 +1,5 @@ -from typing import Callable, Tuple +import datetime +from typing import Any, Callable, Optional, Tuple RetryStrategy = Tuple[Callable[..., float], Tuple] +ScheduleStrategy = Tuple[Callable[..., Optional[datetime.datetime]], Tuple[Any, ...]] From 5be20a6a8043575f11575e96cba8c2e9586b598e Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 18 Oct 2026 13:11:08 +0200 Subject: [PATCH 2/3] Type `Task.retry_method` as the stored retry method. `Task.retry_method` declares that it returns a `RetryStrategy`, which is a `(function, args)` tuple. But it returns the retry method in the form that the task stores: the import path of the function as a string, and the arguments. For example, `("tasktiger.retry:_fixed", (2, 3))`. When a worker loads the task from Redis, the arguments are a list, because they come from JSON. Thus, type checkers reject correct code that gives this string to `import_attribute()`, as the worker does. Add a `SerializedRetryStrategy` alias for the stored form. Use it for `Task.retry_method`, and for `serialize_retry_method()`, which makes this form. --- tasktiger/_internal.py | 4 ++-- tasktiger/task.py | 4 ++-- tasktiger/types.py | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tasktiger/_internal.py b/tasktiger/_internal.py index 01c913e4..dc22bb61 100644 --- a/tasktiger/_internal.py +++ b/tasktiger/_internal.py @@ -15,13 +15,13 @@ Iterable, List, Optional, - Tuple, Type, TypedDict, Union, ) from .exceptions import TaskImportError +from .types import SerializedRetryStrategy if TYPE_CHECKING: from .task import Task @@ -134,7 +134,7 @@ def reversed_dotted_parts(s: str) -> Iterable[str]: yield s[:idx] -def serialize_retry_method(retry_method: Any) -> Tuple[str, Tuple]: +def serialize_retry_method(retry_method: Any) -> SerializedRetryStrategy: if callable(retry_method): return (serialize_func_name(retry_method), ()) else: diff --git a/tasktiger/task.py b/tasktiger/task.py index d5b5bb5b..b6b78872 100644 --- a/tasktiger/task.py +++ b/tasktiger/task.py @@ -32,7 +32,7 @@ ) from .exceptions import QueueFullException, TaskImportError, TaskNotFound from .runner import BaseRunner, get_runner_class -from .types import RetryStrategy +from .types import SerializedRetryStrategy if TYPE_CHECKING: from . import TaskTiger @@ -237,7 +237,7 @@ def unique_key(self) -> Optional[str]: return self._data.get("unique_key") @property - def retry_method(self) -> Optional[RetryStrategy]: + def retry_method(self) -> Optional[SerializedRetryStrategy]: if "retry_method" in self._data: retry_func, retry_args = self._data["retry_method"] return retry_func, retry_args diff --git a/tasktiger/types.py b/tasktiger/types.py index 635790b2..76f9ccd6 100644 --- a/tasktiger/types.py +++ b/tasktiger/types.py @@ -1,5 +1,9 @@ import datetime -from typing import Any, Callable, Optional, Tuple +from typing import Any, Callable, Optional, Sequence, Tuple RetryStrategy = Tuple[Callable[..., float], Tuple] ScheduleStrategy = Tuple[Callable[..., Optional[datetime.datetime]], Tuple[Any, ...]] + +# A retry strategy as stored with a task. The function is replaced with its +# import path, and the arguments come back as a list after a JSON round trip. +SerializedRetryStrategy = Tuple[str, Sequence[Any]] From d5548ab7d765c85a2cc612b86d94063071451db9 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 18 Oct 2026 13:11:26 +0200 Subject: [PATCH 3/3] Replace bare `Tuple` types in retry method hints. A bare `Tuple` is a tuple of any length with items of any type, but it does not say so. Write `Tuple[Any, ...]` for the arguments in `RetryStrategy`, so that the hint is explicit. The `retry_method` parameters of `Task()`, `TaskTiger.task()`, and `TaskTiger.delay()` wrote out the same tuple type in full. Use the `RetryStrategy` alias there. The accepted types do not change. --- tasktiger/task.py | 6 ++---- tasktiger/tasktiger.py | 10 +++------- tasktiger/types.py | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/tasktiger/task.py b/tasktiger/task.py index b6b78872..be4de253 100644 --- a/tasktiger/task.py +++ b/tasktiger/task.py @@ -32,7 +32,7 @@ ) from .exceptions import QueueFullException, TaskImportError, TaskNotFound from .runner import BaseRunner, get_runner_class -from .types import SerializedRetryStrategy +from .types import RetryStrategy, SerializedRetryStrategy if TYPE_CHECKING: from . import TaskTiger @@ -55,9 +55,7 @@ def __init__( lock_key: Optional[Collection[str]] = None, retry: Optional[bool] = None, retry_on: Optional[Collection[Type[BaseException]]] = None, - retry_method: Optional[ - Union[Callable[[int], float], Tuple[Callable[..., float], Tuple]] - ] = None, + retry_method: Optional[Union[Callable[[int], float], RetryStrategy]] = None, max_queue_size: Optional[int] = None, max_stored_executions: Optional[int] = None, runner_class: Optional[Type["BaseRunner"]] = None, diff --git a/tasktiger/tasktiger.py b/tasktiger/tasktiger.py index 29cc4cc8..4820f3bc 100644 --- a/tasktiger/tasktiger.py +++ b/tasktiger/tasktiger.py @@ -36,7 +36,7 @@ from .retry import fixed from .runner import BaseRunner from .task import Task -from .types import ScheduleStrategy +from .types import RetryStrategy, ScheduleStrategy from .worker import LOCK_REDIS_KEY, Worker __all__ = ["TaskTiger"] @@ -313,9 +313,7 @@ def task( lock_key: Optional[Collection[str]] = None, retry: Optional[bool] = None, retry_on: Optional[Collection[Type[BaseException]]] = None, - retry_method: Optional[ - Union[Callable[[int], float], Tuple[Callable[..., float], Tuple]] - ] = None, + retry_method: Optional[Union[Callable[[int], float], RetryStrategy]] = None, schedule: Optional[ Union[ Callable[[datetime.datetime], Optional[datetime.datetime]], @@ -447,9 +445,7 @@ def delay( when: Optional[Union[datetime.datetime, datetime.timedelta]] = None, retry: Optional[bool] = None, retry_on: Optional[Collection[Type[BaseException]]] = None, - retry_method: Optional[ - Union[Callable[[int], float], Tuple[Callable[..., float], Tuple]] - ] = None, + retry_method: Optional[Union[Callable[[int], float], RetryStrategy]] = None, max_queue_size: Optional[int] = None, max_stored_executions: Optional[int] = None, runner_class: Optional[Type["BaseRunner"]] = None, diff --git a/tasktiger/types.py b/tasktiger/types.py index 76f9ccd6..848386e4 100644 --- a/tasktiger/types.py +++ b/tasktiger/types.py @@ -1,7 +1,7 @@ import datetime from typing import Any, Callable, Optional, Sequence, Tuple -RetryStrategy = Tuple[Callable[..., float], Tuple] +RetryStrategy = Tuple[Callable[..., float], Tuple[Any, ...]] ScheduleStrategy = Tuple[Callable[..., Optional[datetime.datetime]], Tuple[Any, ...]] # A retry strategy as stored with a task. The function is replaced with its