Open offbyone opened 1 month ago
I also have this need, djp.Position(..., before/after=...)
seems like it would work just as well for INSTALLED_APPS :)
After toying around over the last day in our (large-ish) codebase with a bunch of existing plugin hooks, I've found I like explicit pure functional insertion of settings more than the registering via mutation, but imo both have to exist to cover all cases (e.g. plugin that removes an INSTALLED_APP in favor of a different one):
some_plugin.py
:
@djp.hookimpl
def get_INSTALLED_APPS():
# simple case: add some apps to the list
return ['django_plugin_simple_header']
@djp.hookimpl
def register_INSTALLED_APPS(INSTALLED_APPS):
# complex case: mutate list to remove/swap out some other app, change ordering, etc.
if 'some_app_to_replace' in INSTALLED_APPS:
position = INSTALLED_APPS.index('some_app_to_replace')
INSTALLED_APPS[position] = 'some_new_app'
if 'some_app_to_remove' in INSTALLED_APPS:
INSTLALED_APPS.remove('some_app_to_remove')
See:
djp
+ lots more hooks implemented)settings.py
usage:
INSTALLED_APPS = [
# Django default apps, things that need to be loaded first, etc.
'daphne',
'django.contrib.admin',
...
# ArchiveBox plugins
*djp.get_plugins_INSTALLLED_APPS(), # register by explicitly inserting apps in the middle (pure, no mutation)
# 3rd-party apps from PyPI that need to be loaded last
'admin_data_views',
'django_extensions',
'django_huey',
..
# bonus: create djp/apps.py that implements a .ready() to call plugin .ready hookimpls
# so plugins can add a hook there too to run code after django setup is complete
'djp',
]
# djp.get_register_INSTALLLED_APPS(INSTALLED_APPS) # mutate list in-place (called at the end by register_plugins_settings(globals())
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.RemoteUserBackend',
'django.contrib.auth.backends.ModelBackend',
*djp.get_plugins_AUTHENTICATION_BACKENDS(),
]
STATICFILES_DIRS = [
*djp.get_plugins_STATICFILES_DIRS(),
str(PACKAGE_DIR / TEMPLATES_DIR_NAME / 'static'),
]
# ... etc.
# at the end:
djp.register_plugins_settings(globals()) # this can access/mutate the entire settings obj
djp/__init__.py
:
def get_plugins_INSTALLLED_APPS():
"""get the list of all the plugin provided INSTALLED_APPS, user defines where they go in list in settings.py"""
return itertools.chain(*pm.hook.get_INSTALLED_APPS())
def register_plugins_INSTALLLED_APPS(INSTALLED_APPS):
"""Mutate the INSTALLED_APPS list in place to edit/remove/add apps in a specific position in the list"""
pm.hook.register_INSTALLED_APPS(INSTALLED_APPS=INSTALLED_APPS)
# etc. ...
def register_plugins_settings(settings):
# convert settings dict to a benedict so we can set values using settings.attr = xyz notation
settings_as_obj = benedict(settings, keypath_separator=None)
# set default values for settings that are used by plugins
settings_as_obj.INSTALLED_APPS = settings_as_obj.get('INSTALLED_APPS', [])
settings_as_obj.MIDDLEWARE = settings_as_obj.get('MIDDLEWARE', [])
settings_as_obj.AUTHENTICATION_BACKENDS = settings_as_obj.get('AUTHENTICATION_BACKENDS', [])
settings_as_obj.STATICFILES_DIRS = settings_as_obj.get('STATICFILES_DIRS', [])
settings_as_obj.TEMPLATE_DIRS = settings_as_obj.get('TEMPLATE_DIRS', [])
settings_as_obj.DJANGO_HUEY = settings_as_obj.get('DJANGO_HUEY', {'queues': {}})
settings_as_obj.ADMIN_DATA_VIEWS = settings_as_obj.get('ADMIN_DATA_VIEWS', {'URLS': []})
# call all the hook functions that mutate the settings values in-place
register_plugins_INSTALLLED_APPS(settings_as_obj.INSTALLED_APPS)
register_plugins_MIDDLEWARE(settings_as_obj.MIDDLEWARE)
register_plugins_AUTHENTICATION_BACKENDS(settings_as_obj.AUTHENTICATION_BACKENDS)
register_plugins_STATICFILES_DIRS(settings_as_obj.STATICFILES_DIRS)
register_plugins_TEMPLATE_DIRS(settings_as_obj.TEMPLATE_DIRS)
register_plugins_DJANGO_HUEY(settings_as_obj.DJANGO_HUEY)
register_plugins_ADMIN_DATA_VIEWS(settings_as_obj.ADMIN_DATA_VIEWS)
# calls Plugin.settings(settings) on each registered plugin
pm.hook.settings(settings=settings_as_obj)
# then finally update the settings globals() object will all the new settings
settings.update(settings_as_obj)
I like this approach becuase I want to encourage the djp.get_XYZ
functions to be used more than the djp.register_XYZ
functions. The pure getters are easier to read and understand at a glance, the ordering is explicit, and you don't have to drill down to the stack to see what they change.
When there's inter-dependence between things then djp.register_XYZ
variants are available, but you don't have to use them and it's better if mutation is only used when absolutely needed!
I've got a plugin whose app has to be ready before any of the other apps that use it, but those apps' names won't be known at code time, so I want to anchor it to the sessions app, or something like that (as an aside, boy howdy I wish I could state dependencies in django apps, so that I could guarantee that
$other_app.ready()
was called before mine!)It'd be grand if position support was generalized from the middleware to the apps list.