Skip to content

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

Closed
wants to merge 16 commits into from

Conversation

itsthejoker
Copy link
Contributor

@itsthejoker itsthejoker commented Jun 24, 2022

Expanding from #61

The settings for django-lightweight-queue expect and require that all settings are laid out in a single file and are all ready at runtime, which is not an accurate assumption for projects that use environment variables to determine which settings to load at runtime. This PR refactors the internal project settings to be similar to that of django-cors-headers, though this one is more complex because django-lightweight-queue expects to be able to rewrite every setting on its own.

This is a stacked PR with #64 and this one should be merged first.

Copy link
Contributor

@PeterJCLaw PeterJCLaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together and breaking this out. I can definitely see why this would be a useful change 😄 (at the very least it would make override_settings work properly, which would simplify the tests!).

Unfortunately I have a concern about the semantics of the settings overriding -- see the inline comment about what happens if one of the settings is overridden to a falsey value.
I've included comments from a first pass at the whole PR here, however I suggest we work out what the solution to the overriding is before addressing the other comments as any refactor could affect whether/how they apply.

Comment on lines 54 to 57
@mock.patch(
'django_lightweight_queue.app_settings.BACKEND',
'django_lightweight_queue.app_settings.Settings.BACKEND',
new='django_lightweight_queue.backends.redis.RedisBackend',
)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 override_settings decorator rather than patching the settings?

Copy link
Contributor Author

@itsthejoker itsthejoker Jun 25, 2022

Choose a reason for hiding this comment

The 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 test_pause_resume_unsupported_backend (the immediate next test) fails. I have essentially zero experience with unittest (all of my personal and professional projects use pytest) -- do you know why this error could be happening?

The newly failing test returns the following, but I'm not sure why:

Traceback (most recent call last):
  File "/home/joe/code/django-lightweight-queue/tests/test_pause_resume.py", line 87, in test_pause_resume_unsupported_backend
    with self.assertRaises(CommandError):
AssertionError: CommandError not raised

Copy link
Contributor

Choose a reason for hiding this comment

The 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 override_settings. Haven't tested it, but that might be worth a try. (I believe there is a base class which doesn't try to do any database stuff, which is likely the one we'd want if we do go that direction).

@itsthejoker
Copy link
Contributor Author

@PeterJCLaw I've refactored it to flesh out your suggestion, but I'm a little concerned that the readability has gone down. (Very pleased about the decrease in length, though.) Ready for review again.

Copy link
Contributor

@PeterJCLaw PeterJCLaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making those changes. It definitely turned out a bit more complex than I'd imagined, which doesn't feel great for settings which one would hope ought to be simple! At least the complexity is in one place though, which I think makes up for it.

I think this is nearly there though I'm concerned that we've lost the type checking around the usage of the settings (mypy can't really see through the __getattr__ usage), which I think would be great to keep if possible.

Comment on lines +37 to +41

class Overrides(Settings):
"""Container for holding internally overridden settings."""
pass

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding: what does this provide over using Settings() directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the _uses_short_names key from the base class so that we look up the right things, and I didn't want to duplicate it in case it ever needed to change 🤷

Comment on lines +14 to +33
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
Copy link
Contributor

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 as Settings 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 of AppSettings, 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 to app_settings, but perhaps it doesn't?)

Copy link
Contributor

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 the Overrides class not having the members. If that's the blocker I think the fix is probably to type: 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)

Copy link
Contributor Author

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.

@itsthejoker
Copy link
Contributor Author

itsthejoker commented Jul 13, 2022

Re: type checking: I admit that I have no idea how to properly pass the type hints through that __getattr__ call -- we passed the limit of my skills appeasing mypy long ago in this process. I'm open to trying whatever, but if you have an idea how to make it work then would you be open to implementing it? At this point I've spent about as much time fighting mypy errors as I have actually writing code and it's quite frustrating.

@PeterJCLaw
Copy link
Contributor

Re: type checking: I admit that I have no idea how to properly pass the type hints through that __getattr__ call -- we passed the limit of my skills appeasing mypy long ago in this process. I'm open to trying whatever, but if you have an idea how to make it work then would you be open to implementing it? At this point I've spent about as much time fighting mypy errors as I have actually writing code and it's quite frustrating.

Sure, I'm happy to have a go at this. I realise that mypy errors can be a little obtuse at times! Thanks for your efforts getting us this far :)

@PeterJCLaw
Copy link
Contributor

Quick update -- I haven't forgotten this, just haven't had time recently.

@PeterJCLaw-mns
Copy link
Contributor

Thanks for building this & sorry it's taken so long to reach a conclusion here. We've ended up extending this in #70 which I've now merged. Hopefully that resolves the use-cases you had in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants