-
Notifications
You must be signed in to change notification settings - Fork 6
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 all 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,59 @@ | ||
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: | ||
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) | ||
) | ||
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
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 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.) 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() | ||
|
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
Settings
type?Combined with ensuring that the
app_settings
variable is typed asSettings
I 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.cast
the assignment ofAppSettings
, but I think that's ok if so.I'm guessing that making
Settings
a 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
Settings
is a protocol mypy doesn't like theOverrides
class not having the members. If that's the blocker I think the fix is probably totype: ignore
it 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.