Django library: define custom settings [duplicate] - python

The Django documentation mentions that you can add your own settings to django.conf.settings. So if my project's settings.py defines
APPLES = 1
I can access that with settings.APPLES in my apps in that project.
But if my settings.py doesn't define that value, accessing settings.APPLES obviously won't work. Is there some way to define a default value for APPLES that is used if there is no explicit setting in settings.py?
I'd like best to define the default value in the module/package that requires the setting.

In my apps, I have a seperate settings.py file. In that file I have a get() function that does a look up in the projects settings.py file and if not found returns the default value.
from django.conf import settings
def get(key, default):
return getattr(settings, key, default)
APPLES = get('APPLES', 1)
Then where I need to access APPLES I have:
from myapp import settings as myapp_settings
myapp_settings.APPLES
This allows an override in the projects settings.py, getattr will check there first and return the value if the attribute is found or use the default defined in your apps settings file.

How about just:
getattr(app_settings, 'SOME_SETTING', 'default value')

Here are two solutions. For both you can set settings.py files in your applications and fill them with default values.
Configure default value for a single application
Use from MYAPP import settings instead of from django.conf import settings in your code.
Edit YOURAPP/__init__.py:
from django.conf import settings as user_settings
from . import settings as default_settings
class AppSettings:
def __getattr__(self, name):
# If the setting you want is filled by the user, let's use it.
if hasattr(user_settings, name):
return getattr(user_settings, name)
# If the setting you want has a default value, let's use it.
if hasattr(default_settings, name):
return getattr(default_settings, name)
raise AttributeError("'Settings' object has no attribute '%s'" % name)
settings = AppSettings()
Configure default values for a whole project
Use from MYPROJECT import settings instead of from django.conf import settings in your code.
Edit MYPROJECT/MYPROJECT/__init__.py
import os, sys, importlib
from . import settings as user_settings
def get_local_apps():
"""Returns the locally installed apps names"""
apps = []
for app in user_settings.INSTALLED_APPS:
path = os.path.join(user_settings.BASE_DIR, app)
if os.path.exists(path) and app != __name__:
apps.append(sys.modules[app])
return apps
class AppSettings:
SETTINGS_MODULE = 'settings'
def __getattr__(self, setting_name):
# If the setting you want is filled by the user, let's use it.
if hasattr(user_settings, setting_name):
return getattr(user_settings, setting_name)
# Let's check every local app loaded by django.
for app in get_local_apps():
module_source = os.path.join(app.__path__[0], "%s.py" % self.SETTINGS_MODULE)
module_binary = os.path.join(app.__path__[0], "%s.pyc" % self.SETTINGS_MODULE)
if os.path.exists(module_source) or os.path.exists(module_binary):
module = importlib.import_module("%s.%s" % (app.__name__, self.SETTINGS_MODULE))
# Let's take the first default value for this setting we can find in any app
if hasattr(module, setting_name):
return getattr(module, setting_name)
raise AttributeError("'Settings' object has no attribute '%s'" % setting_name)
settings = AppSettings()
This solution may seem more easier to install, but it does not guarantee that the good default value will be returned. If several applications declare the same variable in their settings.py, you can not be sure which one will return the default value you asked.

Starting from Mike's answer, I now wrapped the default setting handling into a class with easy to use interface.
Helper module:
from django.conf import settings
class SettingsView(object):
class Defaults(object):
pass
def __init__(self):
self.defaults = SettingsView.Defaults()
def __getattr__(self, name):
return getattr(settings, name, getattr(self.defaults, name))
Usage:
from localconf import SettingsView
settings = SettingsView()
settings.defaults.APPLES = 1
print settings.APPLES
This prints the value from django.conf.settings, or the default if it isn't set there. This settings object can also be used to access all the standard setting values.

I recently had the same problem and created a Django app that is designed to be used for exactly such a case. It allows you to define default values for certain settings. It then first checks whether the setting is set in the global settings file. If not, it will return the default value.
I've extended it to also allow for some type checking or pre handling of the default value (e.g. a dotted class path can be converted to the class itself on load)
The app can be found at: https://pypi.python.org/pypi?name=django-pluggableappsettings&version=0.2.0&:action=display

Related

How can a flask Addon overwrite a template?

I created a flask addon using "flask fab create-addon".
I would like to change the template appbuilder/general/security/login_oauth.html so I have:
templates
appbuilder
general
security
login_oauth.html
But when I load the host application my version of login_oauth.html is not loaded. I tried registering a blueprint as in this post with the following code:
from flask import Blueprint
bp = Blueprint('fab_addon_fslogin', __name__, template_folder='templates')
class MyAddOnManager(BaseManager):
def __init__(self, appbuilder):
"""
Use the constructor to setup any config keys specific for your app.
"""
super(MyAddOnManager, self).__init__(appbuilder)
self.appbuilder.get_app.config.setdefault("MYADDON_KEY", "SOME VALUE")
self.appbuilder.register_blueprint(bp)
def register_views(self):
"""
This method is called by AppBuilder when initializing, use it to add you views
"""
pass
def pre_process(self):
pass
def post_process(self):
pass
But register_blueprint(bp) return:
File "/home/cquiros/data/projects2017/personal/software/superset/addons/fab_addon_fslogin/fab_addon_fslogin/manager.py", line 24, in __init__
self.appbuilder.register_blueprint(bp)
File "/home/cquiros/data/projects2017/personal/software/superset/env_superset/lib/python3.8/site-packages/Flask_AppBuilder-3.3.0-py3.8.egg/flask_appbuilder/base.py", line 643, in register_blueprint
baseview.create_blueprint(
AttributeError: 'Blueprint' object has no attribute 'create_blueprint'
Not much information out there on how to do this. Any clue is appreciated
If you want to customize login_oauth.html, The easiest way is that adding it into your app directly not addon.
That means the login_oauth.html should be put in this path.
YourApp
- app
-- templates
--- appbuilder
---- general
----- security
------ login_oauth.html
For solution of addons, the function self.appbuilder.register_blueprint is for views not for the object which Blueprint fuction returns. It should be replaced with
self.appbuilder.get_app.register_blueprint(Blueprint('fab_addon_fslogin', __name__, template_folder='templates'))
Keep the blueprint registration and
try to rename the login_oauth.html to login_oauth_xxxx.html which at
{your python packages root path}\site-packages\flask_appbuilder\templates\appbuilder\general\security
That will let the template be overwrote as you need. I guess that the template searching order of app-builder package is greater than addons. The searching order of blueprints depend on order of registrations
Finally, I have found a trick without renaming the file, you could try the following
class MyAddOnManager(BaseManager):
def __init__(self, appbuilder):
"""
Use the constructor to setup any config keys specific for your app.
"""
super(MyAddOnManager, self).__init__(appbuilder)
self.appbuilder.get_app.config.setdefault('MYADDON_KEY', 'SOME VALUE')
self.static_bp = Blueprint('fab_addon_fslogin', __name__, template_folder='templates')
def register_views(self):
"""
This method is called by AppBuilder when initializing, use it to add you views
"""
pass
def pre_process(self):
self.appbuilder.get_app.register_blueprint(self.static_bp)
blueprint_order = self.appbuilder.get_app._blueprint_order
# move blueprint order of addon to top
blueprint_order.insert(0, blueprint_order.pop())
def post_process(self):
pass
Reference:
flask-appbuilder Customizing

Best practice for defining default app settings for custom django module?

Is there a defined best practice for defining custom settings that come with a django app.
What I have at the moment is a separate file called app_settings.py that looks like this:
from django.conf import settings
# MY_SETTING_1 is required and will brake the application if not defined
try:
MY_SETTING_1 = str(getattr(settings, 'MY_SETTING_1'))
except AttributeError:
raise ImproperlyConfigured ('MY_SETTING_1 not defined in settings.py.')
# MY_SETTING_2 is not required and has a default value
MY_SETTING_2 = getattr(settings, 'MY_SETTING_2', ['default_value'])
Then I am importing this in my views.py, and using it like this:
from my_app import app_settings
print (app_settings.MY_SETTING_1)
print (app_settings.MY_SETTING_2)
This works ok, but I am not quite happy with the design. I am assuming that I can somehow use the class defined in apps.py, but I am not sure how.
Is there a best (or better) practice for this?
You could try https://pypi.org/project/typed_app_settings/.
Example:
# my_app/app_settings.py
from typed_app_settings import UndefinedValue, typed_app_settings_dict
#typed_app_settings_dict("MY_APP")
class Settings:
MY_SETTING_1: str = UndefinedValue()
MY_SETTING_2: str = "default_value"
settings = Settings()
Then in the view you would just use it like this.
from my_app.app_settings import settings
print(app_settings.MY_SETTING_1)
print(app_settings.MY_SETTING_2)

How do I allow a function to be redefined in Django?

I have a 'core' Django product that includes default implementations of common tasks, but I want to allow that implementation to be redefined (or customised if that makes it easier).
For example in the core product, I might have a view which allows a user to click a button to resend 'all notifications':
# in core/views.py
... imports etc...
from core.tasks import resend_notifications
def handle_user_resend_request(request, user_id):
user = get_object_or_404(id=user_id)
if request.method == 'POST':
for follower in user.followers:
resend_notifications(follower.id)
... etc etc ...
# in core/tasks.py
... imports etc...
def resend_notifications(id):
send_email(User.objects.get(id=id))
And then in some deployments of this product, perhaps the 'resend_notifications' needs to look like:
# in customer_specific/tasks.py
... imports etc ...
def resend_notifications(id):
person = User.objects.get(id=id)
if '#super-hack.email.com' in person.email:
# This is not a real email, send via the magic portal
send_via_magic(person)
else:
send_email(person)
# and send via fax for good measure
send_fax(person)
How do I get the resend_notifications function in the views.py file to point to the customer_specific version?
Should I be defining this in the Django config and sharing access that way? What if the tasks are actually Celery tasks?
NB: The tasks I have are actually defined as Celery tasks (I removed this extra detail because I think the question is more general). I have tried with a custom decorator tag that mutates a global object, but that is definitely not the way to go for a number of reasons.
PS: I feel like this is a dependency injection question, but that is not a common thing in Django.
In a similar situation I ended up going for a solution like so – I put this on my Organization model in the application (equiv of a GitHub organization).
#property
def forms(self):
if self.ldap:
from portal.ldap import forms
else:
from portal.users import forms
return forms
I essentially wanted to use different form classes if the organization the authenticated user belongs to has LDAP configured – and thus the create/invite user forms needed to be different.
I then overwrote get_form_class in the appropriate views like so:
def get_form_class(self):
return self.request.user.organization.forms.CreateUserForm
I imagine you might want to do something similar in your scenario, wrap your function(s) in a proxy abstraction that determines which version to use – be that based on env vars, settings or the request.
This ended up being solved via a Django settings object that can be reconfigured by the deployment config. It was largely inspired by the technique here: settings.py from django-rest-framework.
For example, I have a settings file like this in my project:
yourproject/settings.py
"""
Settings for <YOUR PROJECT> are all namespaced in the YOUR_PROJECT config option.
For example your project's config file (usually called `settings.py` or 'production.py') might look like this:
YOUR_PROJECT = {
'PROCESS_TASK': (
'your_project.tasks.process_task',
)
}
This module provides the `yourproject_settings` object, that is used
to access settings, checking for user settings first, then falling
back to the defaults.
"""
# This file was effectively borrow from https://github.com/tomchristie/django-rest-framework/blob/8385ae42c06b8e68a714cb67b7f0766afe316883/rest_framework/settings.py
from __future__ import unicode_literals
from django.conf import settings
from django.utils.module_loading import import_string
DEFAULTS = {
'RESEND_NOTIFICATIONS_TASK': 'core.tasks.resend_notifications',
}
# List of settings that may be in string import notation.
IMPORT_STRINGS = (
'RESEND_NOTIFICATIONS_TASK',
)
MANDATORY_SETTINGS = (
'RESEND_NOTIFICATIONS_TASK',
)
def perform_import(val, setting_name):
"""
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
if val is None:
return None
if callable(val):
return val
if isinstance(val, (list, tuple)):
return [perform_import(item, setting_name) for item in val]
try:
return import_string(val)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
class YourProjectSettings(object):
"""
A settings object, that allows settings to be accessed as properties.
For example:
from your_project.settings import yourproject_settings as the_settings
print(the_settings.RESEND_NOTIFICATIONS_TASK)
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
"""
namespace = 'YOUR_PROJECT'
def __init__(self, mandatory=None, defaults=None, import_strings=None):
self.mandatory = mandatory or MANDATORY_SETTINGS
self.defaults = defaults or DEFAULTS
self.import_strings = import_strings or IMPORT_STRINGS
self.__check_settings()
#property
def user_settings(self):
if not hasattr(self, '_user_settings'):
self._user_settings = getattr(settings, self.__class__.namespace, {})
return self._user_settings
def __getattr__(self, attr):
if attr not in self.defaults and attr not in self.mandatory:
raise AttributeError("Invalid Pyrite setting: '%s'" % attr)
try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]
# Coerce import strings into classes
if attr in self.import_strings:
val = perform_import(val, attr)
# Cache the result
setattr(self, attr, val)
return val
def __check_settings(self):
for setting in self.mandatory:
if setting not in self.user_settings:
raise RuntimeError(
'The "{}" setting is required as part of the configuration for "{}", but has not been supplied.'.format(
setting, self.__class__.namespace))
yourproject_settings = YourProjectSettings(MANDATORY_SETTINGS, DEFAULTS, IMPORT_STRINGS)
This allows me to either:
Use the default value (i.e. 'core.tasks.resend_notications'); OR
To redefine the binding in my config file:
site_config/special.py
... other django settings like DB / DEBUG / Static files etc
YOUR_PROJECT = {
'RESEND_NOTIFICATIONS_TASK': 'customer_specific.tasks.resend_notifications',
}
... etc. ...
Then in my view function, I access the correct function via the settings:
core/views.py
... imports etc...
from yourproject.settings import yourproject_settings as my_settings
def handle_user_resend_request(request, user_id):
user = get_object_or_404(id=user_id)
if request.method == 'POST':
for follower in user.followers:
my_settings.RESEND_NOTIFICATIONS_TASK(follower.id)
... etc etc ...

'MyAppConfig' must supply a name attribute

i am try to run some piece of code when starting an apache server or django development server. I am using the AppConfig class to do this..(following the docs).
But while starting the server i am get the following error.
django.core.exceptions.ImproperlyConfigured: '{app_name}.apps.MyAppConfig' must supply a name attribute.
following is the sample code:
from django.apps import AppConfig
class MyAppConfig(AppConfig):
def ready():
#my stuff
I don't have any idea about the error.
The docs you linked to tell you what name should be:
AppConfig.name
Full Python path to the application, e.g. 'django.contrib.admin'.
This attribute defines which application the configuration applies to. It must be set in all AppConfig subclasses.
It must be unique across a Django project.
So if your app is call app_name, then you need to set name = 'app_name'.
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = 'app_name'
def ready(self):
# my stuff

How to overwrite django app settings in unittests?

Django allows overwriting settings, when running tests through SimpleTestCase.settings() (https://docs.djangoproject.com/en/1.8/topics/testing/tools/#django.test.SimpleTestCase.settings). That works fine when I try to overwrite one of Django's settings.
The app I want to run tests for carries its own settings in an app-specific settings.py with the following structure to allow overwriting the app-specific settings in the project wide settings.py:
from django.conf import settings
APP_SETTING1 = getattr(settings, 'APP_SETTING1', 'foo')
The following dummy code in a test shows the problem:
from django.test import TestCase
from django.conf import settings as django_settings
from my_app import settings
class MyTestCase(TestCase):
def test_something(self):
with self.settings(APP_SETTING1='bar'):
print(django_settings.APP_SETTING1) # bar
print(settings.APP_SETTING1) # foo
Why isn't that working and how can I make it work?
you can use django-zero-settings, it lets you define your default settings, and will update them with user settings provided, it also works fine in tests settings overrides, alongside that it auto import string, lets you define removed settings and pre-check user settings too.
as in your case, you can define app settings like this:
from zero_settings import ZeroSettings
app_settings = ZeroSettings(
key="APP",
defaults={
"SETTING1": "foo"
},
)
then in your tests you can use it like:
from django.test import TestCase
from django.conf import settings as django_settings
from my_app import app_settings
class MyTestCase(TestCase):
def test_something(self):
with self.settings(APP={"SETTING1": "bar"}):
print(django_settings.APP["SETTING1"]) # bar
print(app_settings.SETTING1) # bar
self.assertEqual(django_settings.APP["SETTING1"], app_settings.SETTING1)
because that ZeroSettings uses cache and will cache user settings on the first attempt to get key, you may need to set it off before getting keys, to make sure you get the latest updates:
app_settings = ZeroSettings(
key="APP",
defaults={
"SETTING1": "foo"
},
use_cache=False,
)
or clear the cache manually:
class MyTestCase(TestCase):
def test_something(self):
print(app_settings.SETTING1) # foo
with self.settings(APP={"SETTING1": "bar"}):
app_settings._clear_cache()
print(django_settings.APP["SETTING1"]) # bar
print(app_settings.SETTING1) # bar
self.assertEqual(django_settings.APP["SETTING1"], app_settings.SETTING1)
Ideally you should mock settings using unittest library incorporated into Django. Alternatively, you can do this, but it is better to patch those:
https://docs.djangoproject.com/en/2.1/topics/settings/#custom-default-settings
Actually, there are good practices for handling multiple settings files. The basic rules are:
1) Do not import from settings.py file directly:
# BAD - avoid this
from app.settings import SOME_ENV
Because if in the end, someone will try to use custom settings some parts of the old settings will be applied somewhere.
# GOOD
from django.conf import settings
print(settings.SOME_ENV)
In this case, you could be sure that actual settings were used.
2) Split up settings files for different environments, usually, it accomplished via creating several files:
settings/base.py
DEBUG = False
AWS_STORAGE_PREFIX = 'local'
...
settings/dev.py
from settings.base import *
DEBUG = True
AWS_STORAGE_PREFIX = 'dev'
DATABASE = {# your settings for dev env}
...
It is also quite usual to create a separated file for test runner because it gives you more power of controlling your test env and so on:
python manage.py test --settings=app.settings.testrunner
Test instance has bound settings' context manager, so you can override any variables that you need for testing:
class MyTestCase(TestCase):
def test_something(self):
# Old settings
with self.settings(SOME_ENV_VAR='overridden'):
# Overridden settings
pass
...
You can do it directly in apps using a certain setting
settings.py:
TEST_ENVIRONMENT_SETTING = True # Test setting for my app
apps.py:
from django.conf import settings
class MyAppConfig(AppConfig):
def ready(self):
if getattr(settings, 'TEST_ENVIRONMENT_SETTING', False):
# Do stuff or not do stuff
Or you can directly set this setting in your TestCase using
setattr(settings, 'TEST_ENVIRONMENT_SETTING', True)
I'm struggling with the same problem, and I think the only solution is to change approach and use only django.conf.settings directly.
The reason is that if you use a custom settings, your custom settings is taken too early from settings, before override_settings or TestCase.settings is called.

Categories

Resources