.. Django Classy Settings documentation master file, created by sphinx-quickstart on Thu Jul 24 13:53:10 2014. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Django Classy Settings ====================== .. rubric:: Stay classy, Django. Credits ------- This work was originally inspired by the work of Jessie O'Connor. Overview -------- Class-based settings make it easy for you to manage multiple settings profiles for your Django project, without ever copying values. Interdependant values, values sourced from the env, even calculated values are no problem, since you have the full power of Python and class inheritance. Contents -------- .. toctree:: :maxdepth: 1 api changelog Examples -------- Below is a default Django `settings.py` that has been adjusted to show how you might use `django-classy-settings`: .. code-block:: python :caption: settings.py :linenos: :emphasize-lines: 15,89-132,135-143,145-147 """ Django settings for dummy project. Generated by 'django-admin startproject' using Django 3.2.15. For more information on this file, see https://docs.djangoproject.com/en/3.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ from pathlib import Path from cbs import BaseSettings, env # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ ALLOWED_HOSTS = [] ROOT_URLCONF = 'dummy.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'dummy.wsgi.application' # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ STATIC_URL = '/static/' # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' class Settings(BaseSettings): # Allow production to override the secret key, but fall-back to something consistent. SECRET_KEY = env('django-insecure-kcdhlitokdqc7s7p7)^jm)55%p@frm#l39nzko458#1!6cu*$$') # DEBUG defaults to True, but can be overridden by env var `DJANGO_DEBUG` DEBUG = env.bool(True, prefix='DJANGO_') # Simple cases that don't need `self` can even use a lambda MEDIA_ROOT = env(lambda self: BASE_DIR / 'media') # Methods will be transparently invoked by the __getattr__ implementation def INSTALLED_APPS(self): return list(filter(None, [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Conditionally include an app 'debug_toolbar' if self.DEBUG else None, ])) def MIDDLEWARE(self): return list(filter(None, [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # Conditionally include a middleware 'debug_toolbar.middleware.DebugToolbarMiddleware' if self.DEBUG else False, ])) # Parse the URL into a database config dict. DEFAULT_DATABASE = env.dburl('sqlite:///db.sqlite') def DATABASES(self) return { 'default': self.DEFAULT_DATABASE, } class ProdSettings(Settings): # Override DEBUG = False # Values that *must* be provided in the environment. @env def STATIC_ROOT(self): raise ValueError('STATIC_ROOT not supplied!') # The `use` method will find the right sub-class of ``BaseSettings`` to use # Based on the value of the `DJANGO_MODE` env var. __getattr__, __dir__ = Settings.use() Now when you start Django, it will use all of your global settings, and any from ``Settings``. You can switch to using the ``ProdSettings`` by setting the `DJANGO_MODE` environment variable: .. code-block:: bash # Use default Settings $ ./manage.py shell # Use ProdSettings $ DJANGO_MODE=prod ./manage.py shell ``BaseSettings.use()`` picks the ``BaseSettings`` sub-class named ``{DJANGO_MODE.title()}Settings``. .. note:: Since the registry of subclasses is on ``BaseSettings``, you can call ``.use()`` on any sub-class and it will behave the same. Which settings to move? ======================= Generally, only move settings which are either environment driven, or need per-mode control. Because of the precedence rules of Python's module level ``__getattr__`` function, any settings declared outside a class can *not* be overridden by a class-based setting. .. code-block:: python GLOBAL = "global" class Settings(BaseSettings): GLOBAL = "local" # This setting will never be used The `env` property ------------------ To help with environment driven settings there is the `env` property decorator. The simplest use case is with an immediate value: .. code-block:: python class Settings(BaseSettings): FOO = env('default') __getattr__, __dir__ = BaseSettings.use() In this case, if the `FOO` environment variable is set, then ``settings.FOO`` will yield its value. Otherwise, it will be ``"default"``. You can optionally override the environment variable name to look up by passing a ``key`` argument: .. code-block:: python class Settings(BaseSettings): FOO = env('default', key='BAR') Additionally, you can define a prefix for the environment variable: .. code-block:: python class Settings(BaseSettings): FOO = env('default', prefix='MY_') # looks up env var MY_FOO If you need a type other than ``str``, you can pass a ``cast`` callable, which will be passed the value. .. code-block:: python class Settings(BaseSettings): FOO = env('default', cast=int) Pre-defined cast types ~~~~~~~~~~~~~~~~~~~~~~ For convenience, there are several built-in pre-defined cast types, accessible via the ``env`` decorator. .. code-block:: python env.bool # Treats ("y", "yes", "on", "t", "true", "1") as True, and ("n", "no", "off", "f", "false", "0") as False env.int # Use the int constructor env.dburl # Converts URLs to Django DATABASES entries. env.list # splits on ',', and strips each value env.tuple # as above, but yields a tuple In all cases, if the default value passed is a string, it will be passed to the cast function. As a decorator ============== Additionally, ``env`` will function as a decorator. This allows you to put some logic into deciding the default value. .. code-block:: python class Settings(BaseSettings): DEBUG = True @env.int def FOO(self): return 1 if self.DEBUG else 2 Mandatory environment variable ============================== Should you require a value to *always* be supplied by environment variable, have your method raise a ``ValueError`` .. code-block:: python class Settings(BaseSettings): @env.int def REQUIRED(self): raise ValueError() Avoiding repeated prefixes ========================== To avoid having to specify a prefix on multiple related variables, ``env`` will yield a ``partial`` when no default is provided. Let's say, for instance, you want all of your environment variables prefixed with `DJANGO_` .. code-block:: python # Common prefix for DJANGO related settings denv = env['DJANGO_'] class Settings(BaseSettings): DEBUG = denv(True) # Will look for DJANGO_DEBUG in env Now setting ``DJANGO_DEBUG=f`` will disable debug mode.