Skip to content

Commit

Permalink
feat(field): add dump_alias and load_alias
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Jul 23, 2020
1 parent e985857 commit 98e91c7
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 3 deletions.
1 change: 1 addition & 0 deletions changes/624-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
add `dump_alias` and `load_alias` options for `Field`
2 changes: 2 additions & 0 deletions docs/usage/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ It has the following arguments:
Among other purposes, this can be used to set dynamic default values.
It is forbidden to set both `default` and `default_factory`.
* `alias`: the public name of the field
* `dump_alias`: only used for dumping (`.dict()` and `.json()`), takes precedence over `alias`.
* `load_alias`: only used for loading, takes precedence over `alias`
* `title`: if omitted, `field_name.title()` is used
* `description`: if omitted and the annotation is a sub-model,
the docstring of the sub-model will be used
Expand Down
42 changes: 41 additions & 1 deletion pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class FieldInfo(Representation):
'default',
'default_factory',
'alias',
'_dump_alias',
'_load_alias',
'alias_priority',
'title',
'description',
Expand All @@ -92,6 +94,8 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
self.default_factory = kwargs.pop('default_factory', None)
self.alias = kwargs.pop('alias', None)
self.alias_priority = kwargs.pop('alias_priority', 2 if self.alias else None)
self._dump_alias = kwargs.pop('dump_alias', None)
self._load_alias = kwargs.pop('load_alias', None)
self.title = kwargs.pop('title', None)
self.description = kwargs.pop('description', None)
self.const = kwargs.pop('const', None)
Expand All @@ -107,12 +111,30 @@ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
self.regex = kwargs.pop('regex', None)
self.extra = kwargs

@property
def dump_alias(self) -> str:
return self._dump_alias or self.alias

@dump_alias.setter
def dump_alias(self, x: str) -> None:
self._dump_alias = x

@property
def load_alias(self) -> str:
return self._load_alias or self.alias

@load_alias.setter
def load_alias(self, x: str) -> None:
self._load_alias = x


def Field(
default: Any = Undefined,
*,
default_factory: Optional[NoArgAnyCallable] = None,
alias: str = None,
dump_alias: str = None,
load_alias: str = None,
title: str = None,
description: str = None,
const: bool = None,
Expand All @@ -137,6 +159,8 @@ def Field(
:param default_factory: callable that will be called when a default value is needed for this field
If both `default` and `default_factory` are set, an error is raised.
:param alias: the public name of the field
:param dump_alias: only used for dumping (.dict() and .json()), takes precedence over alias
:param load_alias: only used for loading, takes precedence over alias
:param title: can be any string, used in the schema
:param description: can be any string, used in the schema
:param const: this field is required and *must* take it's default value
Expand Down Expand Up @@ -165,6 +189,8 @@ def Field(
default,
default_factory=default_factory,
alias=alias,
dump_alias=dump_alias,
load_alias=load_alias,
title=title,
description=description,
const=const,
Expand Down Expand Up @@ -270,6 +296,14 @@ def __init__(
self.model_config.prepare_field(self)
self.prepare()

@property
def dump_alias(self) -> str:
return self.field_info.dump_alias or self.alias

@property
def load_alias(self) -> str:
return self.field_info.load_alias or self.alias

def get_default(self) -> Any:
if self.default_factory is not None:
value = self.default_factory()
Expand Down Expand Up @@ -309,7 +343,7 @@ def infer(
return cls(
name=name,
type_=annotation,
alias=field_info.alias,
alias=field_info.load_alias,
class_validators=class_validators,
default=value,
default_factory=field_info.default_factory,
Expand All @@ -323,11 +357,17 @@ def set_config(self, config: Type['BaseConfig']) -> None:
info_from_config = config.get_field_info(self.name)
config.prepare_field(self)
new_alias = info_from_config.get('alias')
new_dump_alias = info_from_config.get('dump_alias')
new_load_alias = info_from_config.get('load_alias')
new_alias_priority = info_from_config.get('alias_priority') or 0
if new_alias and new_alias_priority >= (self.field_info.alias_priority or 0):
self.field_info.alias = new_alias
self.field_info.alias_priority = new_alias_priority
self.alias = new_alias
if new_dump_alias:
self.field_info.dump_alias = new_dump_alias
if new_load_alias:
self.field_info.load_alias = new_load_alias

@property
def alt_alias(self) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion pydantic/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ def _iter(
):
continue
if by_alias and field_key in self.__fields__:
dict_key = self.__fields__[field_key].alias
dict_key = self.__fields__[field_key].dump_alias
else:
dict_key = field_key
if to_dict or value_include or value_exclude:
Expand Down
4 changes: 3 additions & 1 deletion tests/test_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,12 @@ class Child(Parent):
y: int

class Config:
fields = {'y': 'y2', 'x': 'x2'}
fields = {'y': 'y2', 'x': {'alias': 'x2', 'dump_alias': 'dumpx', 'load_alias': 'loadx'}}

assert Child.__fields__['y'].alias == 'y2'
assert Child.__fields__['x'].alias == 'x2'
assert Child.__fields__['x'].dump_alias == 'dumpx'
assert Child.__fields__['x'].load_alias == 'loadx'


def test_alias_generator_parent():
Expand Down
54 changes: 54 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,60 @@ class Config:
}


def test_dump_alias():
class Model(BaseModel):
a: str = Field(..., dump_alias='dumpme')

assert Model(a='foobar').a == 'foobar'
assert Model(a='foobar').dict() == {'a': 'foobar'}
assert Model(a='foobar').dict(by_alias=True) == {'dumpme': 'foobar'}
assert Model(a='foobar').json(by_alias=True) == '{"dumpme": "foobar"}'


def test_dump_alias_over_alias():
class Model(BaseModel):
a: str = Field(..., alias='pikalias', dump_alias='dumpme')

assert Model(pikalias='foobar').a == 'foobar'
assert Model(pikalias='foobar').dict() == {'a': 'foobar'}
assert Model(pikalias='foobar').dict(by_alias=True) == {'dumpme': 'foobar'}


def test_dump_alias_over_alias_config():
class Model(BaseModel):
a: str

class Config:
fields = {'a': {'alias': 'pikalias', 'dump_alias': 'dumpme'}}

assert Model(pikalias='foobar').a == 'foobar'
assert Model(pikalias='foobar').dict() == {'a': 'foobar'}
assert Model(pikalias='foobar').dict(by_alias=True) == {'dumpme': 'foobar'}


def test_load_alias():
class Model(BaseModel):
a: str = Field(..., alias='pikalias', load_alias='loadme')

assert Model(loadme='foobar').a == 'foobar'
assert Model(loadme='foobar').dict() == {'a': 'foobar'}
assert Model(loadme='foobar').dict(by_alias=True) == {'pikalias': 'foobar'}
assert Model(loadme='foobar').json(by_alias=True) == '{"pikalias": "foobar"}'


def test_dump_load_alias():
class Model(BaseModel):
a: str

class Config:
fields = {'a': {'load_alias': 'loadme', 'dump_alias': 'dumpme'}}

assert Model(loadme='foobar').a == 'foobar'
assert Model(loadme='foobar').dict() == {'a': 'foobar'}
assert Model(loadme='foobar').dict(by_alias=True) == {'dumpme': 'foobar'}
assert Model(loadme='foobar').json(by_alias=True) == '{"dumpme": "foobar"}'


def test_population_by_field_name():
class Model(BaseModel):
a: str
Expand Down

0 comments on commit 98e91c7

Please sign in to comment.