allow aliases

This commit is contained in:
Samuel Colvin
2017-05-31 13:41:53 +01:00
parent 9ecae91dad
commit ea88afb212
7 changed files with 86 additions and 16 deletions
+5 -2
View File
@@ -25,10 +25,13 @@ class BaseSettings(BaseModel):
"""
d = {}
for name, field in self.__fields__.items():
env_name = self.config.env_prefix + field.name.upper()
if field.alt_alias:
env_name = field.alias
else:
env_name = self.config.env_prefix + field.name.upper()
env_var = os.getenv(env_name, None)
if env_var:
d[name] = env_var
d[field.alias] = env_var
return d
class Config:
+15 -7
View File
@@ -21,13 +21,14 @@ class Shape(IntEnum):
class Field:
__slots__ = ('type_', 'key_type_', 'sub_fields', 'key_field', 'validators', 'default',
'required', 'name', 'description', 'info', 'validate_always', 'allow_none', 'shape', 'multipart')
__slots__ = ('type_', 'key_type_', 'sub_fields', 'key_field', 'validators', 'default', 'required',
'name', 'alias', 'description', 'info', 'validate_always', 'allow_none', 'shape', 'multipart')
def __init__(
self, *,
name: str=None,
name: str,
type_: Type,
alias: str=None,
class_validators: dict=None,
default: Any=None,
required: bool=False,
@@ -35,6 +36,7 @@ class Field:
description: str=None):
self.name: str = name
self.alias: str = alias or name
self.type_: type = type_
self.key_type_: type = None
self.validate_always: bool = getattr(self.type_, 'validate_always', False)
@@ -51,16 +53,22 @@ class Field:
self._prepare(class_validators or {})
@classmethod
def infer(cls, *, name, value, annotation, class_validators):
def infer(cls, *, name, value, annotation, class_validators, field_config):
required = value == Ellipsis
return cls(
name=name,
type_=annotation,
alias=field_config and field_config.get('alias'),
class_validators=class_validators,
default=None if required else value,
required=required,
name=name
description=field_config and field_config.get('description'),
)
@property
def alt_alias(self):
return self.name != self.alias
def _prepare(self, class_validators):
if self.default is not None and self.type_ is None:
self.type_ = type(self.default)
@@ -134,9 +142,9 @@ class Field:
name=f'key_{self.name}'
)
if self.sub_fields is None and getattr(self.type_, '__origin__', None):
if self.sub_fields is None and getattr(self.type_, '__origin__', False):
self.multipart = True
self.sub_fields = self.sub_fields or [Field(
self.sub_fields = [Field(
type_=self.type_,
class_validators=class_validators,
default=self.default,
+12 -6
View File
@@ -15,6 +15,7 @@ class BaseConfig:
validate_all = False
ignore_extra = True
allow_extra = False
fields = None
def inherit_config(self_config, parent_config) -> BaseConfig:
@@ -47,14 +48,19 @@ class MetaModel(type):
class_validators = {n: f for n, f in namespace.items()
if n.startswith('validate_') and isinstance(f, FunctionType)}
config_fields = config.fields or {}
for var_name, value in namespace.items():
if var_name.startswith('_') or isinstance(value, TYPE_BLACKLIST):
continue
field_config = config_fields.get(var_name)
if isinstance(field_config, str):
field_config = {'alias': field_config}
field = Field.infer(
name=var_name,
value=value,
annotation=annotations and annotations.get(var_name),
class_validators=class_validators,
field_config=field_config,
)
fields[var_name] = field
namespace.update(
@@ -112,11 +118,11 @@ class BaseModel(metaclass=MetaModel):
def _process_values(self, values):
for name, field in self.__fields__.items():
value = values.get(name, MISSING)
self._process_value(name, field, value)
value = values.get(field.alias, MISSING)
self._process_value(name, field.alias, field, value)
if not self.config.ignore_extra or self.config.allow_extra:
extra = values.keys() - self.__fields__.keys()
extra = values.keys() - {f.alias for f in self.__fields__.values()}
if extra:
if self.config.allow_extra:
for field in extra:
@@ -131,13 +137,13 @@ class BaseModel(metaclass=MetaModel):
if self.config.raise_exception and self.__errors__:
raise ValidationError(self.__errors__)
def _process_value(self, name, field, value):
def _process_value(self, name, alias, field, value):
if value is MISSING:
if self.config.validate_all or field.validate_always:
value = field.default
else:
if field.required:
self.__errors__[name] = MISSING_ERROR
self.__errors__[alias] = MISSING_ERROR
else:
self.__values__[name] = field.default
# could skip this if the attributes equals field.default, would it be quicker?
@@ -146,7 +152,7 @@ class BaseModel(metaclass=MetaModel):
value, errors = field.validate(value, self)
if errors:
self.__errors__[name] = errors
self.__errors__[alias] = errors
self.__values__[name] = value
setattr(self, name, value)
+1 -1
View File
@@ -2,4 +2,4 @@ from distutils.version import StrictVersion
__all__ = ['VERSION']
VERSION = StrictVersion('0.0.7')
VERSION = StrictVersion('0.0.8')
+26
View File
@@ -359,3 +359,29 @@ def test_any_dict():
assert Model(v={1: 'foobar'}).values == {'v': {1: 'foobar'}}
assert Model(v={123: 456}).values == {'v': {123: 456}}
assert Model(v={2: [1, 2, 3]}).values == {'v': {2: [1, 2, 3]}}
def test_infer_alias():
class Model(BaseModel):
a = 'foobar'
class Config:
fields = {'a': '_a'}
assert Model(_a='different').a == 'different'
def test_alias_error():
class Model(BaseModel):
a = 123
class Config:
fields = {'a': '_a'}
assert Model(_a='123').a == 123
with pytest.raises(ValidationError) as exc_info:
Model(_a='foo')
assert """\
1 error validating input
_a:
invalid literal for int() with base 10: 'foo' (error_type=ValueError track=int)""" == str(exc_info.value)
+15
View File
@@ -243,3 +243,18 @@ def test_any():
assert AnyModel().a == 10
assert AnyModel(a='foobar').a == 'foobar'
def test_alias():
class Model(BaseModel):
a = 'foobar'
class Config:
fields = {
'a': {'alias': '_a'}
}
assert Model().a == 'foobar'
assert Model().values == {'a': 'foobar'}
assert Model(_a='different').a == 'different'
assert Model(_a='different').values == {'a': 'different'}
+12
View File
@@ -32,3 +32,15 @@ apple:
def test_other_setting(env):
with pytest.raises(ValidationError):
SimpleSettings(apple='a', foobar=42)
def test_env_with_aliass(env):
class Settings(BaseSettings):
apple: str = ...
class Config:
fields = {
'apple': 'BOOM'
}
env.set('BOOM', 'hello')
assert Settings().apple == 'hello'