Source code for glorpen.config.fields.base
# -*- coding: utf-8 -*-
'''
.. moduleauthor:: Arkadiusz Dzięgiel <arkadiusz.dziegiel@glorpen.pl>
'''
from glorpen.config.exceptions import ValidationError, CircularDependency
import contextlib
def _as_list(v):
"""Returns given object wrapped in list if not already"""
if not isinstance(v, (list)):
return [v]
return v
[docs]@contextlib.contextmanager
def path_validation_error(after=None, before=None):
"""Adds given path to validation error to make exceptions easy to read"""
if after:
after = _as_list(after)
if before:
before = _as_list(before)
try:
yield
except ValidationError as e:
if not hasattr(e, "_partial_path"):
e._partial_path = []
if after:
e._partial_path.extend(after)
if before:
e._partial_path = before + e._partial_path
raise e
[docs]def resolve(obj, config):
"""Returns real object value (resolved) if applicable"""
if isinstance(obj, ResolvableObject):
return obj.resolve(config)
return obj
[docs]class ResolvableObject(object):
"""Configuration value ready to be resolved.
Callbacks are registered by calling :attr:`.on_resolve`.
To each callback are passed:
* currently handled value
* :class:`.Config` instance
By using :class:`.Config` instance you can realize value based on any other value in configuration.
If value is invalid, callback should raise :class:`.ValidationError` with appropriate error message.
"""
_resolving = False
def __init__(self, o):
super(ResolvableObject, self).__init__()
self.o = o
self.callbacks = []
[docs] def on_resolve(self, f):
"""Registers given callback to run when resolving values.
Passed function should accept following arguments:
- value - may be a value or :class:`.ResolvableObject`
- a :class:`.Config` instance
"""
self.callbacks.append(f)
[docs] def resolve(self, config):
"""Resolves value with given config"""
if not hasattr(self, "_resolved_value"):
self._resolved_value = self._do_resolve(config)
return self._resolved_value
def _do_resolve(self, config):
if self._resolving:
raise CircularDependency()
v = self.o
self._resolving = True
for c in self.callbacks:
v = resolve(c(v, config), config)
self._resolving = False
return v
[docs]class Field(object):
"""Single field in configuration file.
Custom fields should register own resolvers/validators/normalizers by extending :meth:`.make_resolvable`.
For handling registered callbacks, see :class:`.ResolvableObject`.
"""
[docs] def resolve(self, v, checked=False):
"""Wraps value in :class:`.ResolvableObject` optionally checking whether provided value is supported."""
if not checked:
if not self.is_value_supported(v):
raise ValidationError("Not supported value %r" % v)
r = ResolvableObject(v)
self.make_resolvable(r)
return r
[docs] def make_resolvable(self, r):
"""Used to register normalizes in current :class:`.ResolvableObject`."""
pass
[docs] def is_value_supported(self, value):
"""Checks if provided value is supported by this field"""
return False
class _UnsetValue(): pass
[docs]class FieldWithDefault(Field):
"""Base class for nullable fields with defaults."""
def __init__(self, default=_UnsetValue, allow_blank=False):
super(FieldWithDefault, self).__init__()
self.default_value = default
self.allow_blank = allow_blank
def has_valid_default(self):
return self.default_value is not _UnsetValue
def resolve(self, v, **kwargs):
if not self.allow_blank and v is None:
if self.has_valid_default():
v = self.default_value
else:
raise ValidationError("Blank value is not allowed.")
return super(FieldWithDefault, self).resolve(v, **kwargs)
def is_value_supported(self, value):
return value is None