diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index da6d6ad..e9067e1 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -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: diff --git a/pydantic/fields.py b/pydantic/fields.py index 83c1d4d..b9de7ea 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -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, diff --git a/pydantic/main.py b/pydantic/main.py index f4f7c96..d0b080d 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -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) diff --git a/pydantic/version.py b/pydantic/version.py index ba9f5d1..5bcd046 100644 --- a/pydantic/version.py +++ b/pydantic/version.py @@ -2,4 +2,4 @@ from distutils.version import StrictVersion __all__ = ['VERSION'] -VERSION = StrictVersion('0.0.7') +VERSION = StrictVersion('0.0.8') diff --git a/tests/test_complex.py b/tests/test_complex.py index b1ae244..1c612a1 100644 --- a/tests/test_complex.py +++ b/tests/test_complex.py @@ -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) diff --git a/tests/test_main.py b/tests/test_main.py index d7162b9..29c2b7a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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'} diff --git a/tests/test_settings.py b/tests/test_settings.py index a93d7e3..1856a93 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -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'