Skip to content

Commit 5ce8c59

Browse files
authored
Add typing (#283)
1 parent 4420021 commit 5ce8c59

34 files changed

+550
-269
lines changed

pyproject.toml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ dependencies = [
4141
"django>=5",
4242
"croniter>=2.0",
4343
"click~=8.2",
44+
"fakeredis",
4445
]
4546

4647
[project.optional-dependencies]
47-
yaml = ["pyyaml~=6.0"]
48+
yaml = ["pyyaml~=6.0", "types-PyYAML>=6.0.12.20250516"]
4849
valkey = ["valkey>=6.0.2,<7"]
4950
sentry = ["sentry-sdk~=2.19"]
5051

@@ -61,6 +62,8 @@ dev = [
6162
"coverage~=7.6",
6263
"fakeredis~=2.28",
6364
"pyyaml>=6,<7",
65+
"mypy>=1.16.0",
66+
"types-croniter>=6.0.0.20250411",
6467
]
6568

6669
[tool.hatch.build.targets.sdist]
@@ -84,3 +87,16 @@ quote-style = "double"
8487
indent-style = "space"
8588
skip-magic-trailing-comma = false
8689
line-ending = "auto"
90+
91+
92+
[tool.mypy]
93+
packages = ['scheduler', ]
94+
exclude = ["scheduler/tests/.*\\.py",
95+
"scheduler/migrations/.*\\.py",
96+
"testproject/.*\\.py",
97+
"testproject/tests/.*\\.py"]
98+
strict = true
99+
follow_imports = "silent"
100+
ignore_missing_imports = true
101+
scripts_are_modules = true
102+
check_untyped_defs = true

scheduler/admin/ephemeral_models.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
1+
from typing import Any
2+
13
from django.contrib import admin
4+
from django.http import HttpResponse, HttpRequest
25

36
from scheduler import views
47
from scheduler.models.ephemeral_models import Queue, Worker
58

69

710
class ImmutableAdmin(admin.ModelAdmin):
8-
def has_add_permission(self, request):
11+
def has_add_permission(self, request: HttpRequest) -> bool:
912
return False # Hide the admin "+ Add" link for Queues
1013

11-
def has_change_permission(self, request, obj=None):
14+
def has_change_permission(self, request: HttpRequest, obj: Any = None) -> bool:
1215
return True
1316

14-
def has_module_permission(self, request):
17+
def has_module_permission(self, request: HttpRequest) -> bool:
1518
"""Returns True if the given request has any permission in the given app label.
1619
1720
Can be overridden by the user in subclasses. In such case, it should return True if the given request has
1821
permission to view the module on the admin index page and access the module's index page. Overriding it does
1922
not restrict access to the add, change or delete views. Use `ModelAdmin.has_(add|change|delete)_permission` for
2023
that.
2124
"""
22-
return request.user.has_module_perms("django-tasks-scheduler")
25+
return request.user.has_module_perms("django-tasks-scheduler") # type: ignore
2326

2427

2528
@admin.register(Queue)
2629
class QueueAdmin(ImmutableAdmin):
2730
"""Admin View for queues"""
2831

29-
def changelist_view(self, request, extra_context=None):
32+
def changelist_view(self, request: HttpRequest, extra_context: Any = None) -> HttpResponse:
3033
"""The 'change list' admin view for this model."""
3134
return views.stats(request)
3235

@@ -35,6 +38,6 @@ def changelist_view(self, request, extra_context=None):
3538
class WorkerAdmin(ImmutableAdmin):
3639
"""Admin View for workers"""
3740

38-
def changelist_view(self, request, extra_context=None):
41+
def changelist_view(self, request: HttpRequest, extra_context: Any = None) -> HttpResponse:
3942
"""The 'change list' admin view for this model."""
4043
return views.workers_list(request)

scheduler/admin/task_admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib import admin, messages
44
from django.contrib.contenttypes.admin import GenericStackedInline
55
from django.db.models import QuerySet
6-
from django.http import HttpRequest
6+
from django.http import HttpRequest, HttpResponse
77
from django.utils import timezone, formats
88
from django.utils.translation import gettext_lazy as _
99

@@ -137,7 +137,7 @@ def task_schedule(self, o: Task) -> str:
137137
def next_run(self, o: Task) -> str:
138138
return get_next_cron_time(o.cron_string)
139139

140-
def change_view(self, request: HttpRequest, object_id, form_url="", extra_context=None):
140+
def change_view(self, request: HttpRequest, object_id, form_url="", extra_context=None) -> HttpResponse:
141141
extra = extra_context or {}
142142
obj = self.get_object(request, object_id)
143143
try:

scheduler/helpers/callback.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, func: Union[str, Callable[..., Any]], timeout: Optional[int]
3030
def name(self) -> str:
3131
return f"{self.func.__module__}.{self.func.__qualname__}"
3232

33-
def __call__(self, *args, **kwargs):
33+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
3434
from scheduler.settings import SCHEDULER_CONFIG
3535

3636
with SCHEDULER_CONFIG.DEATH_PENALTY_CLASS(self.timeout, JobTimeoutException):

scheduler/helpers/queues/getters.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
from typing import Set
22

33
from scheduler.redis_models.worker import WorkerModel
4-
from scheduler.settings import SCHEDULER_CONFIG, get_queue_names, get_queue_configuration, QueueConfiguration, logger
5-
from scheduler.types import ConnectionErrorTypes, BrokerMetaData, Broker
4+
from scheduler.settings import SCHEDULER_CONFIG, get_queue_names, get_queue_configuration, logger
5+
from scheduler.types import ConnectionErrorTypes, BrokerMetaData, Broker, ConnectionType, QueueConfiguration
66
from .queue_logic import Queue
77

8-
98
_BAD_QUEUE_CONFIGURATION = set()
109

1110

12-
def _get_connection(config: QueueConfiguration, use_strict_broker=False):
11+
def _get_connection(config: QueueConfiguration, use_strict_broker: bool = False) -> ConnectionType:
1312
"""Returns a Broker connection to use based on parameters in SCHEDULER_QUEUES"""
1413
if SCHEDULER_CONFIG.BROKER == Broker.FAKEREDIS:
1514
import fakeredis
@@ -32,7 +31,7 @@ def _get_connection(config: QueueConfiguration, use_strict_broker=False):
3231
sentinel_kwargs = config.SENTINEL_KWARGS or {}
3332
SentinelClass = BrokerMetaData[(SCHEDULER_CONFIG.BROKER, use_strict_broker)].sentinel_type
3433
sentinel = SentinelClass(config.SENTINELS, sentinel_kwargs=sentinel_kwargs, **connection_kwargs)
35-
return sentinel.master_for(
34+
return sentinel.master_for( # type: ignore
3635
service_name=config.MASTER_NAME,
3736
redis_class=broker_cls,
3837
)
@@ -47,7 +46,7 @@ def _get_connection(config: QueueConfiguration, use_strict_broker=False):
4746
)
4847

4948

50-
def get_queue(name="default") -> Queue:
49+
def get_queue(name: str = "default") -> Queue:
5150
"""Returns an DjangoQueue using parameters defined in `SCHEDULER_QUEUES`"""
5251
queue_settings = get_queue_configuration(name)
5352
is_async = queue_settings.ASYNC

scheduler/helpers/queues/queue_logic.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
)
2020
from scheduler.redis_models import JobStatus, SchedulerLock, Result, ResultType, JobModel
2121
from scheduler.settings import logger, SCHEDULER_CONFIG
22-
from scheduler.types import ConnectionType, FunctionReferenceType, Self
22+
from scheduler.types import ConnectionType, FunctionReferenceType, Self, PipelineType
2323

2424

2525
class InvalidJobOperation(Exception):
@@ -30,6 +30,10 @@ class NoSuchJobError(Exception):
3030
pass
3131

3232

33+
class NoSuchRegistryError(Exception):
34+
pass
35+
36+
3337
def perform_job(job_model: JobModel, connection: ConnectionType) -> Any: # noqa
3438
"""The main execution method. Invokes the job function with the job arguments.
3539
@@ -45,17 +49,17 @@ def perform_job(job_model: JobModel, connection: ConnectionType) -> Any: # noqa
4549
coro_result = loop.run_until_complete(result)
4650
result = coro_result
4751
if job_model.success_callback:
48-
job_model.success_callback(job_model, connection, result) # type: ignore
52+
job_model.success_callback(job_model, connection, result)
4953
return result
5054
except:
5155
if job_model.failure_callback:
52-
job_model.failure_callback(job_model, connection, *sys.exc_info()) # type: ignore
56+
job_model.failure_callback(job_model, connection, *sys.exc_info())
5357
raise
5458
finally:
5559
assert job_model is _job_stack.pop()
5660

5761

58-
_job_stack = []
62+
_job_stack: List[JobModel] = []
5963

6064

6165
class Queue:
@@ -68,14 +72,14 @@ class Queue:
6872
queued="queued_job_registry",
6973
)
7074

71-
def __init__(self, connection: Optional[ConnectionType], name: str, is_async: bool = True) -> None:
75+
def __init__(self, connection: ConnectionType, name: str, is_async: bool = True) -> None:
7276
"""Initializes a Queue object.
7377
7478
:param name: The queue name
7579
:param connection: Broker connection
7680
:param is_async: Whether jobs should run "async" (using the worker).
7781
"""
78-
self.connection = connection
82+
self.connection: ConnectionType = connection
7983
self.name = name
8084
self._is_async = is_async
8185
self.queued_job_registry = QueuedJobRegistry(connection=self.connection, name=self.name)
@@ -85,11 +89,11 @@ def __init__(self, connection: Optional[ConnectionType], name: str, is_async: bo
8589
self.scheduled_job_registry = ScheduledJobRegistry(connection=self.connection, name=self.name)
8690
self.canceled_job_registry = CanceledJobRegistry(connection=self.connection, name=self.name)
8791

88-
def __len__(self):
92+
def __len__(self) -> int:
8993
return self.count
9094

9195
@property
92-
def scheduler_pid(self) -> int:
96+
def scheduler_pid(self) -> Optional[int]:
9397
lock = SchedulerLock(self.name)
9498
pid = lock.value(self.connection)
9599
return int(pid.decode()) if pid is not None else None
@@ -155,11 +159,11 @@ def count(self) -> int:
155159
res += getattr(self, registry).count(connection=self.connection)
156160
return res
157161

158-
def get_registry(self, name: str) -> Union[None, JobNamesRegistry]:
162+
def get_registry(self, name: str) -> JobNamesRegistry:
159163
name = name.lower()
160164
if name in Queue.REGISTRIES:
161-
return getattr(self, Queue.REGISTRIES[name])
162-
return None
165+
return getattr(self, Queue.REGISTRIES[name]) # type: ignore
166+
raise NoSuchRegistryError(f"Unknown registry name {name}")
163167

164168
def get_all_job_names(self) -> List[str]:
165169
res = list()
@@ -178,22 +182,21 @@ def get_all_jobs(self) -> List[JobModel]:
178182
def create_and_enqueue_job(
179183
self,
180184
func: FunctionReferenceType,
181-
args: Union[Tuple, List, None] = None,
182-
kwargs: Optional[Dict] = None,
185+
args: Union[Tuple[Any, ...], List[Any], None] = None,
186+
kwargs: Optional[Dict[str, Any]] = None,
183187
when: Optional[datetime] = None,
184188
timeout: Optional[int] = None,
185189
result_ttl: Optional[int] = None,
186190
job_info_ttl: Optional[int] = None,
187191
description: Optional[str] = None,
188192
name: Optional[str] = None,
189193
at_front: bool = False,
190-
meta: Optional[Dict] = None,
194+
meta: Optional[Dict[str, Any]] = None,
191195
on_success: Optional[Callback] = None,
192196
on_failure: Optional[Callback] = None,
193197
on_stopped: Optional[Callback] = None,
194198
task_type: Optional[str] = None,
195199
scheduled_task_id: Optional[int] = None,
196-
pipeline: Optional[ConnectionType] = None,
197200
) -> JobModel:
198201
"""Creates a job to represent the delayed function call and enqueues it.
199202
:param when: When to schedule the job (None to enqueue immediately)
@@ -212,7 +215,6 @@ def create_and_enqueue_job(
212215
:param on_stopped: Callback for on stopped
213216
:param task_type: The task type
214217
:param scheduled_task_id: The scheduled task id
215-
:param pipeline: The Broker Pipeline
216218
:returns: The enqueued Job
217219
"""
218220
status = JobStatus.QUEUED if when is None else JobStatus.SCHEDULED
@@ -236,7 +238,7 @@ def create_and_enqueue_job(
236238
scheduled_task_id=scheduled_task_id,
237239
)
238240
if when is None:
239-
job_model = self.enqueue_job(job_model, connection=pipeline, at_front=at_front)
241+
job_model = self.enqueue_job(job_model, at_front=at_front)
240242
elif isinstance(when, datetime):
241243
job_model.save(connection=self.connection)
242244
self.scheduled_job_registry.schedule(self.connection, job_model.name, when)
@@ -246,7 +248,7 @@ def create_and_enqueue_job(
246248

247249
def job_handle_success(
248250
self, job: JobModel, result: Any, job_info_ttl: int, result_ttl: int, connection: ConnectionType
249-
):
251+
) -> None:
250252
"""Saves and cleanup job after successful execution"""
251253
job.after_execution(
252254
job_info_ttl,
@@ -264,7 +266,7 @@ def job_handle_success(
264266
ttl=result_ttl,
265267
)
266268

267-
def job_handle_failure(self, status: JobStatus, job: JobModel, exc_string: str, connection: ConnectionType):
269+
def job_handle_failure(self, status: JobStatus, job: JobModel, exc_string: str, connection: ConnectionType) -> None:
268270
# Does not set job status since the job might be stopped
269271
job.after_execution(
270272
SCHEDULER_CONFIG.DEFAULT_FAILURE_TTL,
@@ -304,10 +306,7 @@ def run_sync(self, job: JobModel) -> JobModel:
304306

305307
@classmethod
306308
def dequeue_any(
307-
cls,
308-
queues: List[Self],
309-
timeout: Optional[int],
310-
connection: Optional[ConnectionType] = None,
309+
cls, queues: List[Self], timeout: Optional[int], connection: ConnectionType
311310
) -> Tuple[Optional[JobModel], Optional[Self]]:
312311
"""Class method returning a Job instance at the front of the given set of Queues, where the order of the queues
313312
is important.
@@ -410,19 +409,19 @@ def delete_job(self, job_name: str, expire_job_model: bool = True) -> None:
410409
pass
411410

412411
def enqueue_job(
413-
self, job_model: JobModel, connection: Optional[ConnectionType] = None, at_front: bool = False
412+
self, job_model: JobModel, pipeline: Optional[PipelineType] = None, at_front: bool = False
414413
) -> JobModel:
415414
"""Enqueues a job for delayed execution without checking dependencies.
416415
417416
If Queue is instantiated with is_async=False, job is executed immediately.
418417
:param job_model: The job redis model
419-
:param connection: The Redis Pipeline
418+
:param pipeline: The Broker Pipeline
420419
:param at_front: Whether to enqueue the job at the front
421420
422421
:returns: The enqueued JobModel
423422
"""
424423

425-
pipe = connection if connection is not None else self.connection.pipeline()
424+
pipe: PipelineType = pipeline if pipeline is not None else self.connection.pipeline()
426425
job_model.started_at = None
427426
job_model.ended_at = None
428427
job_model.status = JobStatus.QUEUED

0 commit comments

Comments
 (0)