Skip to content

Commit d8cf7e8

Browse files
committed
Merge branch 'types'
2 parents df21136 + cdc567b commit d8cf7e8

31 files changed

+612
-193
lines changed

.circleci/config.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ jobs:
8080
source $HOME/.poetry/env
8181
poetry run flake8
8282
83+
type-check:
84+
<<: *parametrised-python-executor
85+
86+
steps:
87+
- checkout
88+
89+
- *install-poetry
90+
- *restore-dependencies-cache
91+
- *install-dependencies
92+
- *save-dependencies-cache
93+
94+
- run:
95+
name: Run Mypy
96+
command: |
97+
source $HOME/.poetry/env
98+
poetry run ./script/type-check
99+
83100
deploy:
84101
executor:
85102
name: python
@@ -108,6 +125,9 @@ workflows:
108125
- lint:
109126
name: lint-py<< matrix.python-version >>-Django<< matrix.django-version >>
110127
<<: *version-matrix
128+
- type-check:
129+
name: type-check-py<< matrix.python-version >>-Django<< matrix.django-version >>
130+
<<: *version-matrix
111131

112132

113133
build-test-deploy:
@@ -130,11 +150,21 @@ workflows:
130150
branches:
131151
ignore: /.*/
132152

153+
- type-check:
154+
name: type-check-py<< matrix.python-version >>-Django<< matrix.django-version >>
155+
<<: *version-matrix
156+
filters:
157+
tags:
158+
only: /v[0-9]+(\.[0-9]+)*/
159+
branches:
160+
ignore: /.*/
161+
133162
- deploy:
134163
context: thread-pypi
135164
requires:
136165
- build-test
137166
- lint
167+
- type-check
138168
filters:
139169
tags:
140170
only: /v[0-9]+(\.[0-9]+)*/
Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
1+
from typing import Dict, Union, Mapping, TypeVar, Callable, Sequence
2+
13
from django.conf import settings
24

35
from . import constants
6+
from .types import Logger, QueueName
7+
8+
T = TypeVar('T')
49

510

6-
def setting(suffix, default):
11+
def setting(suffix: str, default: T) -> T:
712
attr_name = '{}{}'.format(constants.SETTING_NAME_PREFIX, suffix)
813
return getattr(settings, attr_name, default)
914

1015

11-
WORKERS = setting('WORKERS', {})
12-
BACKEND = setting('BACKEND', 'django_lightweight_queue.backends.synchronous.SynchronousBackend')
16+
WORKERS = setting('WORKERS', {}) # type: Dict[QueueName, int]
17+
BACKEND = setting(
18+
'BACKEND',
19+
'django_lightweight_queue.backends.synchronous.SynchronousBackend',
20+
) # type: str
1321

14-
LOGGER_FACTORY = setting('LOGGER_FACTORY', 'logging.getLogger')
22+
LOGGER_FACTORY = setting(
23+
'LOGGER_FACTORY',
24+
'logging.getLogger',
25+
) # type: Union[str, Callable[[str], Logger]]
1526

1627
# Allow per-queue overrides of the backend.
17-
BACKEND_OVERRIDES = setting('BACKEND_OVERRIDES', {})
28+
BACKEND_OVERRIDES = setting('BACKEND_OVERRIDES', {}) # type: Mapping[QueueName, str]
1829

1930
MIDDLEWARE = setting('MIDDLEWARE', (
2031
'django_lightweight_queue.middleware.logging.LoggingMiddleware',
2132
'django_lightweight_queue.middleware.transaction.TransactionMiddleware',
22-
))
33+
)) # type: Sequence[str]
2334

2435
# Apps to ignore when looking for tasks. Apps must be specified as the dotted
2536
# name used in `INSTALLED_APPS`. This is expected to be useful when you need to
2637
# have a file called `tasks.py` within an app, but don't want
2738
# django-lightweight-queue to import that file.
2839
# Note: this _doesn't_ prevent tasks being registered from these apps.
29-
IGNORE_APPS = setting('IGNORE_APPS', ())
40+
IGNORE_APPS = setting('IGNORE_APPS', ()) # type: Sequence[str]
3041

3142
# Backend-specific settings
32-
REDIS_HOST = setting('REDIS_HOST', '127.0.0.1')
33-
REDIS_PORT = setting('REDIS_PORT', 6379)
34-
REDIS_PREFIX = setting('REDIS_PREFIX', '')
43+
REDIS_HOST = setting('REDIS_HOST', '127.0.0.1') # type: str
44+
REDIS_PORT = setting('REDIS_PORT', 6379) # type: int
45+
REDIS_PREFIX = setting('REDIS_PREFIX', '') # type: str
3546

36-
ENABLE_PROMETHEUS = setting('ENABLE_PROMETHEUS', False)
47+
ENABLE_PROMETHEUS = setting('ENABLE_PROMETHEUS', False) # type: bool
3748
# Workers will export metrics on this port, and ports following it
38-
PROMETHEUS_START_PORT = setting('PROMETHEUS_START_PORT', 9300)
49+
PROMETHEUS_START_PORT = setting('PROMETHEUS_START_PORT', 9300) # type: int
3950

40-
ATOMIC_JOBS = setting('ATOMIC_JOBS', True)
51+
ATOMIC_JOBS = setting('ATOMIC_JOBS', True) # type: bool

django_lightweight_queue/apps.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
class DjangoLightweightQueueConfig(AppConfig):
77
name = 'django_lightweight_queue'
88

9-
def ready(self):
9+
def ready(self) -> None:
1010
load_all_tasks()
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
from abc import ABCMeta, abstractmethod
2+
from typing import Tuple, TypeVar, Optional
23

34
from ..job import Job
5+
from ..types import QueueName, WorkerNumber
6+
from ..progress_logger import ProgressLogger, NULL_PROGRESS_LOGGER
7+
8+
# Work around https://github.com/python/mypy/issues/9914. Name needs to match
9+
# that in progress_logger.py.
10+
T = TypeVar('T')
411

512

613
class BaseBackend(metaclass=ABCMeta):
7-
def startup(self, queue: str) -> None:
14+
def startup(self, queue: QueueName) -> None:
815
pass
916

1017
@abstractmethod
11-
def enqueue(self, job: Job, queue: str) -> None:
18+
def enqueue(self, job: Job, queue: QueueName) -> None:
1219
raise NotImplementedError()
1320

1421
@abstractmethod
15-
def dequeue(self, queue: str, worker_num: int, timeout: float) -> Job:
22+
def dequeue(self, queue: QueueName, worker_num: WorkerNumber, timeout: int) -> Optional[Job]:
1623
raise NotImplementedError()
1724

1825
@abstractmethod
19-
def length(self, queue: str) -> int:
26+
def length(self, queue: QueueName) -> int:
2027
raise NotImplementedError()
2128

22-
def processed_job(self, queue: str, worker_num: int, job: Job) -> None:
29+
def processed_job(self, queue: QueueName, worker_num: WorkerNumber, job: Job) -> None:
2330
pass
31+
32+
33+
class BackendWithDeduplicate(BaseBackend, metaclass=ABCMeta):
34+
@abstractmethod
35+
def deduplicate(
36+
self,
37+
queue: QueueName,
38+
*,
39+
progress_logger: ProgressLogger = NULL_PROGRESS_LOGGER
40+
) -> Tuple[int, int]:
41+
raise NotImplementedError()

django_lightweight_queue/backends/debug_web.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import urllib
1+
import urllib.parse
22

33
from django.conf import settings
44
from django.shortcuts import reverse
55

6+
from ..job import Job
67
from .base import BaseBackend
8+
from ..types import QueueName, WorkerNumber
79

810

911
class DebugWebBackend(BaseBackend):
@@ -17,14 +19,14 @@ class DebugWebBackend(BaseBackend):
1719
See the docstring of that view for information (and limitations) about it.
1820
"""
1921

20-
def enqueue(self, job, queue):
22+
def enqueue(self, job: Job, queue: QueueName) -> None:
2123
path = reverse('django-lightweight-queue:debug-run')
2224
query_string = urllib.parse.urlencode({'job': job.to_json()})
2325
url = "{}{}?{}".format(settings.SITE_URL, path, query_string)
2426
print(url)
2527

26-
def dequeue(self, queue, worker_num, timeout):
28+
def dequeue(self, queue: QueueName, worker_num: WorkerNumber, timeout: float) -> None:
2729
pass
2830

29-
def length(self, queue):
31+
def length(self, queue: QueueName) -> int:
3032
return 0

django_lightweight_queue/backends/redis.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,39 @@
1+
from typing import Optional
2+
13
import redis
24

35
from .. import app_settings
46
from ..job import Job
57
from .base import BaseBackend
8+
from ..types import QueueName, WorkerNumber
69

710

811
class RedisBackend(BaseBackend):
912
"""
1013
This backend has at-most-once semantics.
1114
"""
1215

13-
def __init__(self):
16+
def __init__(self) -> None:
1417
self.client = redis.StrictRedis(
1518
host=app_settings.REDIS_HOST,
1619
port=app_settings.REDIS_PORT,
1720
)
1821

19-
def enqueue(self, job, queue):
22+
def enqueue(self, job: Job, queue: QueueName) -> None:
2023
self.client.lpush(self._key(queue), job.to_json().encode('utf-8'))
2124

22-
def dequeue(self, queue, worker_num, timeout):
23-
try:
24-
_, data = self.client.brpop(self._key(queue), timeout)
25+
def dequeue(self, queue: QueueName, worker_num: WorkerNumber, timeout: int) -> Optional[Job]:
26+
raw = self.client.brpop(self._key(queue), timeout)
27+
if raw is None:
28+
return None
2529

26-
return Job.from_json(data.decode('utf-8'))
27-
except TypeError:
28-
pass
30+
_, data = raw
31+
return Job.from_json(data.decode('utf-8'))
2932

30-
def length(self, queue):
33+
def length(self, queue: QueueName) -> int:
3134
return self.client.llen(self._key(queue))
3235

33-
def _key(self, queue):
36+
def _key(self, queue: QueueName) -> str:
3437
if app_settings.REDIS_PREFIX:
3538
return '{}:django_lightweight_queue:{}'.format(
3639
app_settings.REDIS_PREFIX,

django_lightweight_queue/backends/reliable_redis.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
from typing import Dict, List, Tuple, TypeVar, Optional
2+
13
import redis
24

35
from .. import app_settings
46
from ..job import Job
5-
from .base import BaseBackend
7+
from .base import BackendWithDeduplicate
8+
from ..types import QueueName, WorkerNumber
69
from ..utils import get_worker_numbers
7-
from ..progress_logger import NULL_PROGRESS_LOGGER
10+
from ..progress_logger import ProgressLogger, NULL_PROGRESS_LOGGER
11+
12+
# Work around https://github.com/python/mypy/issues/9914. Name needs to match
13+
# that in progress_logger.py.
14+
T = TypeVar('T')
815

916

10-
class ReliableRedisBackend(BaseBackend):
17+
class ReliableRedisBackend(BackendWithDeduplicate):
1118
"""
1219
This backend manages a per-queue-per-worker 'processing' queue. E.g. if we
1320
had a queue called 'django_lightweight_queue:things', and two workers, we
@@ -29,13 +36,13 @@ class ReliableRedisBackend(BaseBackend):
2936
This backend has at-least-once semantics.
3037
"""
3138

32-
def __init__(self):
39+
def __init__(self) -> None:
3340
self.client = redis.StrictRedis(
3441
host=app_settings.REDIS_HOST,
3542
port=app_settings.REDIS_PORT,
3643
)
3744

38-
def startup(self, queue):
45+
def startup(self, queue: QueueName) -> None:
3946
main_queue_key = self._key(queue)
4047

4148
pattern = self._prefix_key(
@@ -53,7 +60,7 @@ def startup(self, queue):
5360
)
5461
processing_queue_keys = current_processing_queue_keys - expected_processing_queue_keys
5562

56-
def move_processing_jobs_to_main(pipe):
63+
def move_processing_jobs_to_main(pipe: redis.client.Pipeline) -> None:
5764
# Collect all the data we need to add, before adding the data back
5865
# to the main queue of and clearing the processing queues
5966
# atomically, so if this crashes, we don't lose jobs
@@ -79,10 +86,10 @@ def move_processing_jobs_to_main(pipe):
7986
*processing_queue_keys,
8087
)
8188

82-
def enqueue(self, job, queue):
89+
def enqueue(self, job: Job, queue: QueueName) -> None:
8390
self.client.lpush(self._key(queue), job.to_json().encode('utf-8'))
8491

85-
def dequeue(self, queue, worker_number, timeout):
92+
def dequeue(self, queue: QueueName, worker_number: WorkerNumber, timeout: int) -> Optional[Job]:
8693
main_queue_key = self._key(queue)
8794
processing_queue_key = self._processing_key(queue, worker_number)
8895

@@ -104,7 +111,9 @@ def dequeue(self, queue, worker_number, timeout):
104111
if data:
105112
return Job.from_json(data.decode('utf-8'))
106113

107-
def processed_job(self, queue, worker_number, job):
114+
return None
115+
116+
def processed_job(self, queue: QueueName, worker_number: WorkerNumber, job: Job) -> None:
108117
data = job.to_json().encode('utf-8')
109118

110119
self.client.lrem(
@@ -113,10 +122,15 @@ def processed_job(self, queue, worker_number, job):
113122
value=data,
114123
)
115124

116-
def length(self, queue):
125+
def length(self, queue: QueueName) -> int:
117126
return self.client.llen(self._key(queue))
118127

119-
def deduplicate(self, queue, *, progress_logger=NULL_PROGRESS_LOGGER):
128+
def deduplicate(
129+
self,
130+
queue: QueueName,
131+
*,
132+
progress_logger: ProgressLogger = NULL_PROGRESS_LOGGER
133+
) -> Tuple[int, int]:
120134
"""
121135
Deduplicate the given queue by comparing the jobs in a manner which
122136
ignores their created timestamps.
@@ -137,7 +151,7 @@ def deduplicate(self, queue, *, progress_logger=NULL_PROGRESS_LOGGER):
137151

138152
# A mapping of job_identity -> list of raw_job data; the entries in the
139153
# latter list are ordered from newest to oldest
140-
jobs = {}
154+
jobs = {} # type: Dict[str, List[bytes]]
141155

142156
progress_logger.info("Collecting jobs")
143157

@@ -163,20 +177,20 @@ def deduplicate(self, queue, *, progress_logger=NULL_PROGRESS_LOGGER):
163177

164178
return original_size, self.client.llen(main_queue_key)
165179

166-
def _key(self, queue):
180+
def _key(self, queue: QueueName) -> str:
167181
key = 'django_lightweight_queue:{}'.format(queue)
168182

169183
return self._prefix_key(key)
170184

171-
def _processing_key(self, queue, worker_number):
185+
def _processing_key(self, queue: QueueName, worker_number: WorkerNumber) -> str:
172186
key = 'django_lightweight_queue:{}:processing:{}'.format(
173187
queue,
174188
worker_number,
175189
)
176190

177191
return self._prefix_key(key)
178192

179-
def _prefix_key(self, key):
193+
def _prefix_key(self, key: str) -> str:
180194
if app_settings.REDIS_PREFIX:
181195
return '{}:{}'.format(
182196
app_settings.REDIS_PREFIX,

0 commit comments

Comments
 (0)