django-orchestra/orchestra/contrib/settings/parser.py

171 lines
4.9 KiB
Python

import ast
import copy
import json
import os
import re
from django.utils.translation import ugettext_lazy as _
from django.utils.functional import Promise
from orchestra.utils.paths import get_project_dir
from . import Setting
class Remove(object):
""" used to signal a setting remove """
pass
def get_settings_file():
return os.path.join(get_project_dir(), 'settings.py')
def _find_updates(changes, settings_file):
""" find all updates needed for applying changes on settings_file content """
with open(settings_file, 'rb') as handler:
p = ast.parse(handler.read())
updates = []
for elem in p.body:
if updates and updates[-1][-1] is None:
updates[-1][-1] = elem.lineno-1
targets = getattr(elem, 'targets', None)
if targets:
var_name = targets[0].id
if var_name in changes:
updates.append([var_name, elem.lineno, None])
return updates
class LazyUgettextRepr(object):
def __init__(self, value):
self.value = value
def __repr__(self):
return '_("%s")' % self.value
def __len__(self):
return len(repr(self.value))
class NotSupported(object):
def __repr__(self):
return 'Serialization not supported'
def __len__(self):
return 0
def get_eval_context():
return {
'NotSupported': NotSupported,
'_': _,
}
def serialize(obj, init=True):
if isinstance(obj, NotSupported):
return obj
elif isinstance(obj, Promise):
_obj = LazyUgettextRepr(obj)
elif isinstance(obj, dict):
_obj = {}
for name, value in obj.items():
name = serialize(name, init=False)
value = serialize(value, init=False)
if isinstance(name, NotSupported) or isinstance(value, NotSupported):
return NotSupported()
_obj[name] = value
elif isinstance(obj, (tuple, list)):
_obj = []
for nested in obj:
nested = serialize(nested, init=False)
if isinstance(nested, NotSupported):
return nested
_obj.append(nested)
_obj = type(obj)(_obj)
elif isinstance(obj, (str, bool, int, float)):
_obj = obj
else:
_obj = NotSupported()
return repr(_obj) if init else _obj
def _format_setting(name, value):
if isinstance(value, Remove):
return ""
try:
value = json.dumps(value, indent=4)
except TypeError:
value = serialize(value)
return "{name} = {value}".format(name=name, value=value)
def validate_changes(changes):
for name, value in changes.items():
if not isinstance(value, Remove):
try:
setting = Setting.settings[name]
except KeyError:
pass
else:
setting.validate_value(value)
def apply(changes, settings_file=get_settings_file()):
""" returns settings_file content with applied changes """
validate_changes(changes)
updates = _find_updates(changes, settings_file)
content = []
_changes = copy.copy(changes)
inside = False
lineno = None
if updates:
name, lineno, end = updates.pop(0)
# update existing variable declarations
with open(settings_file, 'r') as handler:
for num, line in enumerate(handler.readlines(), 1):
line = line.rstrip()
if num == lineno:
value = _changes.pop(name)
line = _format_setting(name, value)
if line:
content.append(line)
inside = True
comments = []
lastend = end
try:
name, lineno, end = updates.pop(0)
except IndexError:
if lastend is None:
break
if not inside:
content.append(line)
else:
# Discard lines since variable will be replaced
# But save comments and blank lines
if re.match(r'^\s*(#.*)*\s*$', line):
comments.append(line)
else:
comments = []
# End of variable declaration
if num == lastend:
content.extend(comments)
inside = False
# insert new variables at the end of file
for name, value in _changes.items():
content.append(_format_setting(name, value))
return '\n'.join(content)
def save(changes, settings_file=get_settings_file(), backup=True):
""" apply changes to project.settings file, saving a backup """
new_settings = apply(changes, settings_file)
tmp_settings_file = settings_file + '.tmp'
with open(tmp_settings_file, 'w') as handle:
handle.write(new_settings)
if backup:
os.rename(settings_file, settings_file + '.backup')
os.rename(tmp_settings_file, settings_file)