Skip to content

Commit b235120

Browse files
committed
Merge branch 'allow-runtime-settings-overrides'
2 parents 6174195 + 80d2fd4 commit b235120

File tree

13 files changed

+127
-77
lines changed

13 files changed

+127
-77
lines changed
Lines changed: 87 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,103 @@
1-
from typing import Dict, Union, Mapping, TypeVar, Callable, Optional, Sequence
1+
"""
2+
Internal API facade over Django settings.
23
3-
from django.conf import settings
4+
This provides a typed interface over Django settings as well as handling default
5+
values for settings not set by users and supporting the addition of overrides in
6+
some management commands.
7+
"""
8+
9+
from typing import Any, Dict, List, Union, Callable, Optional, Sequence
10+
11+
from typing_extensions import Protocol
12+
13+
from django.conf import settings as django_settings
414

515
from . import constants
616
from .types import Logger, QueueName
717

8-
T = TypeVar('T')
918

19+
class LongNameAdapter:
20+
def __init__(self, target: Any) -> None:
21+
self.target = target
22+
23+
def __getattr__(self, name: str) -> Any:
24+
return getattr(self.target, f'{constants.SETTING_NAME_PREFIX}{name}')
25+
26+
27+
class Settings(Protocol):
28+
WORKERS: Dict[QueueName, int]
29+
BACKEND: str
30+
LOGGER_FACTORY: Union[str, Callable[[str], Logger]]
31+
32+
# Allow per-queue overrides of the backend.
33+
BACKEND_OVERRIDES: Dict[QueueName, str]
34+
MIDDLEWARE: Sequence[str]
35+
36+
# Apps to ignore when looking for tasks. Apps must be specified as the dotted
37+
# name used in `INSTALLED_APPS`. This is expected to be useful when you need to
38+
# have a file called `tasks.py` within an app, but don't want
39+
# django-lightweight-queue to import that file.
40+
# Note: this _doesn't_ prevent tasks being registered from these apps.
41+
IGNORE_APPS: Sequence[str]
42+
43+
REDIS_HOST: str
44+
REDIS_PORT: int
45+
REDIS_PASSWORD: Optional[str]
46+
REDIS_DATABASE: int
47+
REDIS_PREFIX: str
48+
49+
ENABLE_PROMETHEUS: bool
50+
# Workers will export metrics on this port, and ports following it
51+
PROMETHEUS_START_PORT: int
52+
53+
ATOMIC_JOBS: bool
54+
55+
56+
class LayeredSettings(Settings, Protocol):
57+
def add_layer(self, layer: Settings) -> None:
58+
...
59+
60+
61+
class Defaults(Settings):
62+
WORKERS: Dict[QueueName, int] = {}
63+
BACKEND = 'django_lightweight_queue.backends.synchronous.SynchronousBackend'
64+
LOGGER_FACTORY = 'logging.getLogger'
65+
66+
BACKEND_OVERRIDES: Dict[QueueName, str] = {}
67+
MIDDLEWARE = ('django_lightweight_queue.middleware.logging.LoggingMiddleware',)
68+
69+
IGNORE_APPS: Sequence[str] = ()
70+
71+
REDIS_HOST = '127.0.0.1'
72+
REDIS_PORT = 6379
73+
REDIS_PASSWORD = None
74+
REDIS_DATABASE = 0
75+
REDIS_PREFIX = ""
1076

11-
def setting(suffix: str, default: T) -> T:
12-
attr_name = '{}{}'.format(constants.SETTING_NAME_PREFIX, suffix)
13-
return getattr(settings, attr_name, default)
77+
ENABLE_PROMETHEUS = False
1478

79+
PROMETHEUS_START_PORT = 9300
1580

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

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

27-
# Allow per-queue overrides of the backend.
28-
BACKEND_OVERRIDES = setting('BACKEND_OVERRIDES', {}) # type: Mapping[QueueName, str]
84+
class AppSettings:
85+
def __init__(self, layers: List[Settings]) -> None:
86+
self._layers = layers
2987

30-
MIDDLEWARE = setting('MIDDLEWARE', (
31-
'django_lightweight_queue.middleware.logging.LoggingMiddleware',
32-
'django_lightweight_queue.middleware.transaction.TransactionMiddleware',
33-
)) # type: Sequence[str]
88+
def add_layer(self, layer: Settings) -> None:
89+
self._layers.append(layer)
3490

35-
# Apps to ignore when looking for tasks. Apps must be specified as the dotted
36-
# name used in `INSTALLED_APPS`. This is expected to be useful when you need to
37-
# have a file called `tasks.py` within an app, but don't want
38-
# django-lightweight-queue to import that file.
39-
# Note: this _doesn't_ prevent tasks being registered from these apps.
40-
IGNORE_APPS = setting('IGNORE_APPS', ()) # type: Sequence[str]
91+
def __getattr__(self, name: str) -> Any:
92+
# reverse so that later layers override earlier ones
93+
for layer in reversed(self._layers):
94+
if hasattr(layer, name):
95+
return getattr(layer, name)
4196

42-
# Backend-specific settings
43-
REDIS_HOST = setting('REDIS_HOST', '127.0.0.1') # type: str
44-
REDIS_PORT = setting('REDIS_PORT', 6379) # type: int
45-
REDIS_PASSWORD = setting('REDIS_PASSWORD', None) # type: Optional[str]
46-
REDIS_DATABASE = setting('REDIS_DATABASE', 0) # type: int
47-
REDIS_PREFIX = setting('REDIS_PREFIX', '') # type: str
97+
raise AttributeError(f"Sorry, '{name}' is not a valid setting.")
4898

49-
ENABLE_PROMETHEUS = setting('ENABLE_PROMETHEUS', False) # type: bool
50-
# Workers will export metrics on this port, and ports following it
51-
PROMETHEUS_START_PORT = setting('PROMETHEUS_START_PORT', 9300) # type: int
5299

53-
ATOMIC_JOBS = setting('ATOMIC_JOBS', True) # type: bool
100+
app_settings: LayeredSettings = AppSettings(layers=[
101+
Defaults(),
102+
LongNameAdapter(django_settings),
103+
])

django_lightweight_queue/backends/redis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
import redis
55

6-
from .. import app_settings
76
from ..job import Job
87
from .base import BackendWithClear, BackendWithPauseResume
98
from ..types import QueueName, WorkerNumber
109
from ..utils import block_for_time
10+
from ..app_settings import app_settings
1111

1212

1313
class RedisBackend(BackendWithPauseResume, BackendWithClear):

django_lightweight_queue/backends/reliable_redis.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import redis
55

6-
from .. import app_settings
76
from ..job import Job
87
from .base import (
98
BackendWithClear,
@@ -12,6 +11,7 @@
1211
)
1312
from ..types import QueueName, WorkerNumber
1413
from ..utils import block_for_time, get_worker_numbers
14+
from ..app_settings import app_settings
1515
from ..progress_logger import ProgressLogger, NULL_PROGRESS_LOGGER
1616

1717
# Work around https://github.com/python/mypy/issues/9914. Name needs to match

django_lightweight_queue/exposition.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
from prometheus_client.exposition import MetricsHandler
88

9-
from . import app_settings
109
from .types import QueueName, WorkerNumber
10+
from .app_settings import app_settings
1111

1212

1313
def get_config_response(

django_lightweight_queue/management/commands/queue_configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any
22

3-
from ... import app_settings
43
from ...utils import get_backend, get_queue_counts
4+
from ...app_settings import app_settings
55
from ...command_utils import CommandWithExtraSettings
66
from ...cron_scheduler import get_cron_config
77

django_lightweight_queue/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import subprocess
55
from typing import Dict, Tuple, Callable, Optional
66

7-
from . import app_settings
87
from .types import Logger, QueueName, WorkerNumber
98
from .utils import get_backend, set_process_title
109
from .exposition import metrics_http_server
10+
from .app_settings import app_settings
1111
from .machine_types import Machine
1212
from .cron_scheduler import (
1313
CronScheduler,

django_lightweight_queue/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
Optional,
1313
)
1414

15-
from . import app_settings
1615
from .job import Job
1716
from .types import QueueName
1817
from .utils import get_backend, contribute_implied_queue_name
18+
from .app_settings import app_settings
1919

2020
TCallable = TypeVar('TCallable', bound=Callable[..., Any])
2121

django_lightweight_queue/utils.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
from django.core.exceptions import MiddlewareNotUsed
2222
from django.utils.module_loading import module_has_submodule
2323

24-
from . import constants, app_settings
24+
from . import constants
2525
from .types import Logger, QueueName, WorkerNumber
26+
from .app_settings import Defaults, app_settings, LongNameAdapter
2627

2728
if TYPE_CHECKING:
2829
from .backends.base import BaseBackend
@@ -47,18 +48,15 @@ def with_prefix(names: Iterable[str]) -> Set[str]:
4748
for name in names
4849
)
4950

50-
setting_names = get_setting_names(app_settings)
51+
setting_names = get_setting_names(Defaults())
5152
extra_names = get_setting_names(extra_settings)
5253

5354
unexpected_names = extra_names - with_prefix(setting_names)
5455
if unexpected_names:
5556
unexpected_str = "' ,'".join(unexpected_names)
5657
warnings.warn("Ignoring unexpected setting(s) '{}'.".format(unexpected_str))
5758

58-
override_names = extra_names - unexpected_names
59-
for name in override_names:
60-
short_name = name[len(constants.SETTING_NAME_PREFIX):]
61-
setattr(app_settings, short_name, getattr(extra_settings, name))
59+
app_settings.add_layer(LongNameAdapter(extra_settings))
6260

6361

6462
@lru_cache()

django_lightweight_queue/worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
from django.db import connections, transaction
1414

15-
from . import app_settings
1615
from .types import QueueName, WorkerNumber
1716
from .utils import get_logger, get_backend, set_process_title
17+
from .app_settings import app_settings
1818
from .backends.base import BaseBackend
1919

2020
if app_settings.ENABLE_PROMETHEUS:

tests/test_extra_settings.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import importlib
21
from typing import Optional
32
from pathlib import Path
3+
from unittest import mock
44

55
from django.test import SimpleTestCase
66

@@ -27,11 +27,19 @@ def length(self, queue: QueueName) -> int:
2727
class ExtraSettingsTests(SimpleTestCase):
2828
def setUp(self) -> None:
2929
get_backend.cache_clear()
30+
31+
self.settings: app_settings.Settings = app_settings.AppSettings([app_settings.Defaults()])
32+
self._settings_patch = mock.patch(
33+
'django_lightweight_queue.utils.app_settings',
34+
new=self.settings,
35+
)
36+
self._settings_patch.start()
37+
3038
super().setUp()
3139

3240
def tearDown(self) -> None:
33-
importlib.reload(app_settings)
3441
get_backend.cache_clear()
42+
self._settings_patch.stop()
3543
super().tearDown()
3644

3745
def test_updates_settings(self) -> None:
@@ -40,20 +48,20 @@ def test_updates_settings(self) -> None:
4048
backend = get_backend('test-queue')
4149
self.assertIsInstance(backend, TestBackend)
4250

43-
self.assertEqual('a very bad password', app_settings.REDIS_PASSWORD)
51+
self.assertEqual('a very bad password', self.settings.REDIS_PASSWORD)
4452

4553
def test_warns_about_unexpected_settings(self) -> None:
4654
with self.assertWarnsRegex(Warning, r'Ignoring unexpected setting.+\bNOT_REDIS_PASSWORD\b'):
4755
load_extra_settings(str(TESTS_DIR / '_demo_extra_settings_unexpected.py'))
4856

49-
self.assertEqual('expected', app_settings.REDIS_PASSWORD)
57+
self.assertEqual('expected', self.settings.REDIS_PASSWORD)
5058

5159
def test_updates_settings_with_falsey_values(self) -> None:
5260
load_extra_settings(str(TESTS_DIR / '_demo_extra_settings.py'))
5361
load_extra_settings(str(TESTS_DIR / '_demo_extra_settings_falsey.py'))
5462

55-
self.assertIsNone(app_settings.REDIS_PASSWORD)
56-
self.assertFalse(app_settings.ATOMIC_JOBS)
63+
self.assertIsNone(self.settings.REDIS_PASSWORD)
64+
self.assertFalse(self.settings.ATOMIC_JOBS)
5765

5866
def test_rejects_missing_file(self) -> None:
5967
with self.assertRaises(FileNotFoundError):

0 commit comments

Comments
 (0)