Skip to content

Commit

Permalink
Make django settings easier. change naming patterns. (#5)
Browse files Browse the repository at this point in the history
Make django settings easier. change naming patterns.
---------

Co-authored-by: Aaron <[email protected]>
  • Loading branch information
rabbit-aaron and Aaron authored Aug 28, 2023
1 parent 1ddaa2d commit 93665ea
Show file tree
Hide file tree
Showing 20 changed files with 438 additions and 900 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.0
current_version = 1.0.0
commit = True
tag = True

Expand Down
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[flake8]
max-line-length = 88
extend-ignore = E203
extend-ignore = E501
22 changes: 12 additions & 10 deletions .github/workflows/action.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
on: push

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/[email protected]
with:
extra_args: --all
ci:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python: ["3.6", "3.7", "3.8", "3.9"]
python: ["3.9", "3.10", "3.11"]
container:
image: python:${{ matrix.python }}
steps:
- name: checkout repository
uses: actions/checkout@v2
- name: virtualenv for poetry
run: python -m venv ~/.poetry && ~/.poetry/bin/pip install --upgrade pip
- name: install poetry in virtualenv
run: ~/.poetry/bin/pip install poetry
uses: actions/checkout@v3
- name: install poetry
run: python -m venv ~/.poetry && ~/.poetry/bin/pip install --upgrade pip && ~/.poetry/bin/pip install poetry
- name: install python dependencies
run: ~/.poetry/bin/poetry install
- name: allow access to the virtualenv
run: echo PATH="$(~/.poetry/bin/poetry run printenv VIRTUAL_ENV)/bin:${PATH}" >> $GITHUB_ENV
- name: black
run: black --check .
- name: flake8
run: flake8
- name: pytest
run: pytest --cov=ragdoll --cov-report term-missing tests/
- name: build
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.mypy_cache/
.pytest_cache/
.idea/
__pycache__/
1 change: 1 addition & 0 deletions .poetry-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.6.1
37 changes: 22 additions & 15 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
repos:
- repo: local
hooks:
- id: black
name: black
language: python
entry: black
require_serial: true
types_or: [python, pyi]
- id: flake8
name: flake8
language: python
entry: flake8
require_serial: true
types_or: [python, pyi]
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
hooks:
- id: pyupgrade
args: [--py39-plus]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
hooks:
- id: mypy

default_language_version:
python: python3.9
python: python3

2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.9.6
3.9
125 changes: 73 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ Ragdoll aims to solve one problem and one problem only, with no external depende
# Getting started
```python
# settings.py
from ragdoll.env import EnvSetting, IntEnv, StrEnv, BoolEnv
from ragdoll import env

class MySetting(EnvSetting):
class MySetting(env.EnvSetting):
# default_value is a positional argument, so it can be omitted
WORKER_COUNT = IntEnv(default_value=3)
HOST_NAME = StrEnv("localhost")
WORKER_COUNT = env.Int(default_value=3)
HOST_NAME = env.Str("localhost")

# no default value specified, if the environment variable SECRET_KEY is not set, it will raise ragdoll.errors.ImproperlyConfigured
SECRET_KEY = StrEnv()
DEBUG = BoolEnv(False)
SECRET_KEY = env.Str()
DEBUG = env.Bool(False)
```

Then to access these variables
Expand All @@ -41,38 +41,34 @@ MySetting.DEBUG # True
MySetting.SECRET_KEY # 'meow'
```
# Django
Django load settings by running `dir(<SETTINGS_MODULE>)`,
I took advantage of that and magically inject a `__dir__` method to the settings module.
This makes things simpler for Django settings

```python
# settings.py
from ragdoll.django import DjangoEnvSetting
from ragdoll.env import BoolEnv, StrEnv, IntEnv

class MySetting(DjangoEnvSetting):
# These variables will be exposed as a module (global) variable automatically
# Django will pick them up like any other settings variables
# Keep in mind that all Django settings must be in upper case,
# otherwise Django will not load them
DEBUG = BoolEnv(False)
DOMAIN_NAME = StrEnv("example.com")
MAX_CONNECTION_COUNT = IntEnv()

# if export=False, this variable will not be automatically exported to the module
# so you cannot directly access it via django.conf.settings
# this could be useful if you need this value as a part of some other settings
DB_HOST = StrEnv(export=False)
DB_NAME = StrEnv(export=False)
DB_USER = StrEnv(export=False)
DB_PASSWORD = StrEnv(export=False)
from ragdoll.django import env

# Keep in mind that all Django settings must be in upper case,
# otherwise Django will not load them
DEBUG = env.Bool(False)
DOMAIN_NAME = env.Str("example.com")
MAX_CONNECTION_COUNT = env.Int()

# if you do not want django to pick up the settings
# simply use lower case variables
db_host = env.Str()
db_name = env.Str()
db_user = env.Str()
db_password = env.Str()

# The settings that are not exported can be access via the MySetting class
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': MySetting.DB_NAME,
'USER': MySetting.DB_USER,
'PASSWORD': MySetting.DB_PASSWORD,
'HOST': MySetting.DB_HOST,
'NAME': db_name,
'USER': db_user,
'PASSWORD': db_password,
'HOST': db_host,
'PORT': '5432',
}
}
Expand Down Expand Up @@ -105,28 +101,51 @@ settings.DATABASES
# }
# }
```
### IMPORTANT: THIS WILL NOT WORK
Since the injected `__dir__` function only look at global variables, nested settings will not work,
You must assign the settings to a global variable first, then use it in nested settings.

```python
######################
# THIS WILL NOT WORK #
######################

from ragdoll.django import env

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env.Str(),
'USER': env.Str(),
'PASSWORD': env.Str(),
'HOST': env.Str(),
'PORT': '5432',
}
}
```


# Using ragdoll with python-dotenv
```python
# settings.py
from dotenv import load_dotenv
from ragdoll.env import EnvSetting, BoolEnv
from ragdoll import env

# just ensure load_dotenv is called before the class is defined
load_dotenv()

class MySettings(EnvSetting):
DEBUG = BoolEnv()
class MySettings(env.EnvSetting):
DEBUG = env.Bool()
```
alternatively, you can turn off `auto_configure` to defer the loading of variables

```python
from dotenv import load_dotenv
from ragdoll.env import EnvSetting, BoolEnv
from ragdoll import env

class MySetting(EnvSetting):
class MySetting(env.EnvSetting):
auto_configure = False
DEBUG = BoolEnv()
DEBUG = env.Bool()

load_dotenv()
MySetting.configure()
Expand All @@ -138,9 +157,9 @@ MySetting.configure()
```python
# my_fields.py
from typing import List
from ragdoll.env import BaseEnvEntry
from ragdoll import env

class CommaSeparatedEnv(BaseEnvEntry):
class CommaSeparatedStr(env.BaseEnvEntry):
"""This field turns comma separated values into a list of strings"""
def to_python(self, value:str) -> List[str]:
return value.split(",")
Expand All @@ -151,11 +170,11 @@ To use it:
# assuming the this environment variables is set
# ALLOWED_HOSTS=example.com,example.org

from ragdoll.env import EnvSetting
from my_fields import CommaSeparatedEnv
from ragdoll import env
from my_fields import CommaSeparatedStr

class MyEnvSetting(EnvSetting):
ALLOWED_HOSTS = CommaSeparatedEnv()
class MyEnvSetting(env.EnvSetting):
ALLOWED_HOSTS = CommaSeparatedStr()

MyEnvSetting.ALLOWED_HOSTS # ['example.com', 'example.org']
```
Expand All @@ -164,38 +183,40 @@ MyEnvSetting.ALLOWED_HOSTS # ['example.com', 'example.org']
```python
# my_fields.py
import dj_database_url
frmo ragdoll.env import BaseEnvEntry

class DbUrlEnv(BaseEnvEntry):
from ragdoll.django import env
from ragdoll.env import BaseEnvEntry


# Note the DjangoEnvEntryMixin here, that's where the __dir__ injection happens
# it's very important to include it if this is intended for Django settings.
class DbUrl(env.DjangoEnvEntryMixin, BaseEnvEntry):
"""This field turns database URL into a Django DATABASES setting dictionary"""
def to_python(self, value:str) -> dict:
return {"default": dj_database_url.parse(value)}
```
To use it:
```python
# settings.py
from ragdoll.django import DjangoEnvSetting
from my_fields import DbUrlEnv
from my_fields import DbUrl

class MySetting(DjangoEnvSetting):
DATABASES = DbUrlEnv("postgres://ragdoll:meow@localhost:5432/ragdoll")
DATABASES = DbUrl("postgres://ragdoll:meow@localhost:5432/ragdoll")
```

## Validations

```python
# my_fields.py
import dj_database_url
from ragdoll.env import BaseEnvEntry
from ragdoll import env
from ragdool.errors import ImproperlyConfigured

class HexIntEnv(BaseEnvEntry):
class HexInt(env.BaseEnvEntry):
"""This field turns a hex string into an integer"""
def to_python(self, value:str) -> int:
try:
return int(value, base=16)
except ValueError:
# if ImproperlyConfigured is raised, ragdoll will collect all of the errors
# if ImproperlyConfigured is raised, ragdoll will collect all the errors
# and re-raise them all at once
raise ImproperlyConfigured(f"{self.name} must be a hexadecimal value, e.g. '0x1000'")
```
Loading

0 comments on commit 93665ea

Please sign in to comment.