-
Notifications
You must be signed in to change notification settings - Fork 5
Expand Settings to handle different Django Architectures #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
8d396f2
c00635f
3b9e3a6
737f80d
0b202ac
97b36d4
76631b4
73cfefe
6ee7218
c9c5b84
fc48298
ecbcad3
05ebae5
d29f36a
af4eb29
496828a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,52 +1,58 @@ | ||
| from typing import Dict, Union, Mapping, TypeVar, Callable, Optional, Sequence | ||
| from typing import Any, Dict, List, Union, Callable, Optional, Sequence | ||
|
|
||
| from django.conf import settings | ||
| from django.conf import settings as django_settings | ||
|
|
||
| from . import constants | ||
| from .types import Logger, QueueName | ||
|
|
||
| T = TypeVar('T') | ||
|
|
||
|
|
||
| def setting(suffix: str, default: T) -> T: | ||
| attr_name = '{}{}'.format(constants.SETTING_NAME_PREFIX, suffix) | ||
| return getattr(settings, attr_name, default) | ||
|
|
||
|
|
||
| WORKERS = setting('WORKERS', {}) # type: Dict[QueueName, int] | ||
| BACKEND = setting( | ||
| 'BACKEND', | ||
| 'django_lightweight_queue.backends.synchronous.SynchronousBackend', | ||
| ) # type: str | ||
|
|
||
| LOGGER_FACTORY = setting( | ||
| 'LOGGER_FACTORY', | ||
| 'logging.getLogger', | ||
| ) # type: Union[str, Callable[[str], Logger]] | ||
|
|
||
| # Allow per-queue overrides of the backend. | ||
| BACKEND_OVERRIDES = setting('BACKEND_OVERRIDES', {}) # type: Mapping[QueueName, str] | ||
|
|
||
| MIDDLEWARE = setting('MIDDLEWARE', ( | ||
| 'django_lightweight_queue.middleware.logging.LoggingMiddleware', | ||
| 'django_lightweight_queue.middleware.transaction.TransactionMiddleware', | ||
| )) # type: Sequence[str] | ||
|
|
||
| # Apps to ignore when looking for tasks. Apps must be specified as the dotted | ||
| # name used in `INSTALLED_APPS`. This is expected to be useful when you need to | ||
| # have a file called `tasks.py` within an app, but don't want | ||
| # django-lightweight-queue to import that file. | ||
| # Note: this _doesn't_ prevent tasks being registered from these apps. | ||
| IGNORE_APPS = setting('IGNORE_APPS', ()) # type: Sequence[str] | ||
|
|
||
| # Backend-specific settings | ||
| REDIS_HOST = setting('REDIS_HOST', '127.0.0.1') # type: str | ||
| REDIS_PORT = setting('REDIS_PORT', 6379) # type: int | ||
| REDIS_PASSWORD = setting('REDIS_PASSWORD', None) # type: Optional[str] | ||
| REDIS_PREFIX = setting('REDIS_PREFIX', '') # type: str | ||
|
|
||
| ENABLE_PROMETHEUS = setting('ENABLE_PROMETHEUS', False) # type: bool | ||
| # Workers will export metrics on this port, and ports following it | ||
| PROMETHEUS_START_PORT = setting('PROMETHEUS_START_PORT', 9300) # type: int | ||
|
|
||
| ATOMIC_JOBS = setting('ATOMIC_JOBS', True) # type: bool | ||
| class Settings: | ||
| _uses_short_names: bool = True # used later in checking for values | ||
|
|
||
|
|
||
| class Defaults(Settings): | ||
| WORKERS: Dict[QueueName, int] = {} | ||
| BACKEND: str = 'django_lightweight_queue.backends.synchronous.SynchronousBackend' | ||
| LOGGER_FACTORY: Union[str, Callable[[str], Logger]] = 'logging.getLogger' | ||
| # Allow per-queue overrides of the backend. | ||
| BACKEND_OVERRIDES: Dict[QueueName, str] = {} | ||
| MIDDLEWARE: Sequence[str] = ('django_lightweight_queue.middleware.logging.LoggingMiddleware',) | ||
| # Apps to ignore when looking for tasks. Apps must be specified as the dotted | ||
| # name used in `INSTALLED_APPS`. This is expected to be useful when you need to | ||
| # have a file called `tasks.py` within an app, but don't want | ||
| # django-lightweight-queue to import that file. | ||
| # Note: this _doesn't_ prevent tasks being registered from these apps. | ||
| IGNORE_APPS: Sequence[str] = () | ||
| REDIS_HOST: str = '127.0.0.1' | ||
| REDIS_PORT: int = 6379 | ||
| REDIS_PASSWORD: Optional[str] = None | ||
| REDIS_PREFIX: str = "" | ||
| ENABLE_PROMETHEUS: bool = False | ||
| # Workers will export metrics on this port, and ports following it | ||
| PROMETHEUS_START_PORT: int = 9300 | ||
| ATOMIC_JOBS: bool = True | ||
|
|
||
|
|
||
| class AppSettings: | ||
| def __init__(self, layers: List[Settings]) -> None: | ||
| self._layers = layers | ||
|
|
||
| def add_layer(self, layer: Settings) -> None: # to be called by `load_extra_config` | ||
|
itsthejoker marked this conversation as resolved.
Outdated
|
||
| self._layers.append(layer) | ||
|
|
||
| def __getattr__(self, name: str) -> Any: | ||
| # reverse so that later layers override earlier ones | ||
| for layer in reversed(self._layers): | ||
| # check to see if the layer is internal or external | ||
| use_short_names = getattr(layer, "_uses_short_names", False) | ||
| attr_name = ( | ||
| name if use_short_names else | ||
| '{}{}'.format(constants.SETTING_NAME_PREFIX, name) | ||
|
itsthejoker marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
itsthejoker marked this conversation as resolved.
|
||
| if hasattr(layer, attr_name): | ||
| return getattr(layer, attr_name) | ||
|
|
||
| raise AttributeError(f"Sorry, '{name}' is not a valid setting.") | ||
|
|
||
|
|
||
| app_settings = AppSettings(layers=[Defaults(), django_settings]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,9 @@ | |
| from django.core.exceptions import MiddlewareNotUsed | ||
| from django.utils.module_loading import module_has_submodule | ||
|
|
||
| from . import constants, app_settings | ||
| from . import constants | ||
| from .types import Logger, QueueName, WorkerNumber | ||
| from .app_settings import Settings, app_settings | ||
|
|
||
| if TYPE_CHECKING: | ||
| from .backends.base import BaseBackend | ||
|
|
@@ -33,6 +34,11 @@ | |
|
|
||
|
|
||
| def load_extra_config(file_path: str) -> None: | ||
|
|
||
| class Overrides(Settings): | ||
| """Container for holding internally overridden settings.""" | ||
| pass | ||
|
|
||
|
Comment on lines
+37
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my understanding: what does this provide over using
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two reasons: it makes mypy not throw a fit and since we search by getattr, starting with an empty class means that we can add whatever settings we want and if the getattr fails then it'll fall through to the next layer. (There was originally a different comment here but it took me a minute to remember why I did it this way.)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need the |
||
| extra_settings = imp.load_source('extra_settings', file_path) | ||
|
|
||
| def get_setting_names(module: object) -> Set[str]: | ||
|
|
@@ -53,9 +59,14 @@ def with_prefix(names: Iterable[str]) -> Set[str]: | |
| warnings.warn("Ignoring unexpected setting(s) '{}'.".format(unexpected_str)) | ||
|
|
||
| override_names = extra_names - unexpected_names | ||
|
|
||
| overrides = Overrides() | ||
|
|
||
| for name in override_names: | ||
| short_name = name[len(constants.SETTING_NAME_PREFIX):] | ||
| setattr(app_settings, short_name, getattr(extra_settings, name)) | ||
| setattr(overrides, short_name, getattr(extra_settings, name)) | ||
|
|
||
| app_settings.add_layer(overrides) | ||
|
|
||
|
|
||
| @lru_cache() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,7 +52,7 @@ def setUp(self) -> None: | |
| # Can't use override_settings due to the copying of the settings values into | ||
| # module values at startup. | ||
| @mock.patch( | ||
| 'django_lightweight_queue.app_settings.BACKEND', | ||
| 'django_lightweight_queue.app_settings.Defaults.BACKEND', | ||
| new='django_lightweight_queue.backends.redis.RedisBackend', | ||
| ) | ||
|
Comment on lines
54
to
57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that the settings are dynamically loaded, what would you think about moving these to using Django's
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this test, swapping to @override_settings(
LIGHTWEIGHT_QUEUE_BACKEND='django_lightweight_queue.backends.redis.RedisBackend'
)
def test_pause_resume(self) -> None:
......lets this test pass, but it reports that The newly failing test returns the following, but I'm not sure why:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, hrm, I wonder if we need to change the test case to inherit from one of Django's base test classes in order to use
itsthejoker marked this conversation as resolved.
|
||
| def test_pause_resume(self) -> None: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we include these members, without values, on the base
Settingstype?Combined with ensuring that the
app_settingsvariable is typed asSettingsI think would that keep type-checking at the places in the code where the settings get used, which I think we do want.I realise we might then need to
typing.castthe assignment ofAppSettings, but I think that's ok if so.I'm guessing that making
Settingsa protocol didn't work for some reason? (I'd assumed that would avoid the need for a cast in the assignment toapp_settings, but perhaps it doesn't?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aaah, trying this a bit myself I can see that if
Settingsis a protocol mypy doesn't like theOverridesclass not having the members. If that's the blocker I think the fix is probably totype: ignoreit in that one place rather than sacrificing the settings type checking throughout.(I thiiink there might also be a route involving a
LongNameAdapter(see other comment) which solves the typing more completely, though that's possibly a lot more involved as a change so happy to leave that for now if there's a simpler path)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Honestly most of this silliness is related to mypy -- if I had a quarter for each time I've googled something working on this repo so far and found an open bug ticket on the mypy repo, I'd have a very nice dinner out for myself.
I'm fine with ignoring or omitting types wherever needed as long as the values themselves are clear, but it's not my repo so it's whatever's acceptable by you all.