Skip to content

Commit

Permalink
Merge 4b72e08 into b407d7b
Browse files Browse the repository at this point in the history
  • Loading branch information
bpeake-illuscio committed Jan 21, 2019
2 parents b407d7b + 4b72e08 commit 0672f25
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 2 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Dacite supports following features:
- (basic) types checking
- optional fields (i.e. `typing.Optional`)
- unions
- forward references
- collections
- values casting and transformation
- remapping of fields names
Expand Down Expand Up @@ -97,6 +98,7 @@ Configuration is a (data) class with following fields:
- `prefixed`
- `cast`
- `transform`
- `forward references`

The examples below show all features of `from_dict` function and usage
of all `Config` parameters.
Expand Down Expand Up @@ -224,6 +226,25 @@ result = from_dict(data_class=B, data=data)
assert result == B(a_list=[A(x='test1', y=1), A(x='test2', y=2)])
```

### Forward References

Definition of forward references can be passed as a `{'name': Type}` mapping to
`Config.forward_references`. This dict is passed to `typing.get_type_hints()` as the
`globalns` param when evaluating each field's type.

```python
@dataclass
class X:
y: "Y"

@dataclass
class Y:
s: str

data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))
```

### Remapping

If your input data key does not match with a data class field name, you
Expand Down Expand Up @@ -363,6 +384,8 @@ required field
(a field name or a input data key) for a configuration
- `UnionMatchError` - raised when provided data does not match any type
of `Union`
- `ForwardReferenceError` - raised when undefined forward reference encountered in
dataclass

## Authors

Expand Down
14 changes: 13 additions & 1 deletion dacite.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import fields, MISSING, is_dataclass, Field, dataclass, field as dc_field
from typing import Dict, Any, TypeVar, Type, Union, Callable, List, Collection, Optional, Set, Mapping, Tuple
from typing import Dict, Any, TypeVar, Type, Union, Callable, List, Collection, Optional, Set, Mapping, Tuple, get_type_hints
import copy


class DaciteError(Exception):
Expand Down Expand Up @@ -35,13 +36,18 @@ def __init__(self, parameter: str, available_choices: Set[str], value: str) -> N
self.value = value


class ForwardReferenceError(DaciteError):
pass


@dataclass
class Config:
remap: Dict[str, str] = dc_field(default_factory=dict)
prefixed: Dict[str, str] = dc_field(default_factory=dict)
cast: List[str] = dc_field(default_factory=list)
transform: Dict[str, Callable[[Any], Any]] = dc_field(default_factory=dict)
flattened: List[str] = dc_field(default_factory=list)
forward_references: Optional[Dict[str, Any]] = None


T = TypeVar('T')
Expand All @@ -60,7 +66,13 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
init_values: Data = {}
post_init_values: Data = {}
_validate_config(data_class, data, config)
try:
data_class_hints = get_type_hints(data_class, globalns=config.forward_references)
except NameError as error:
raise ForwardReferenceError(str(error))
for field in fields(data_class):
field = copy.copy(field)
field.type = data_class_hints[field.name]
value, is_default = _get_value_for_field(field, data, config)
if not is_default:
if value is not None:
Expand Down
72 changes: 71 additions & 1 deletion tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass, field
from typing import Optional, List, Set, Union, Any, Dict

from dacite import from_dict, Config, WrongTypeError, MissingValueError, InvalidConfigurationError, UnionMatchError
from dacite import from_dict, Config, WrongTypeError, MissingValueError, InvalidConfigurationError, UnionMatchError, ForwardReferenceError


def test_from_dict_from_correct_data():
Expand Down Expand Up @@ -886,3 +886,73 @@ class X:
result = from_dict(X, {'s': 'test'})

assert result == x


def test_forward_reference():

@dataclass
class X:
y: "Y"

@dataclass
class Y:
s: str

data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))


def test_forward_reference_in_union():

@dataclass
class X:
y: Union["Y", str]

@dataclass
class Y:
s: str

data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))


def test_forward_reference_in_list():

@dataclass
class X:
y: List["Y"]

@dataclass
class Y:
s: str

data = from_dict(X, {"y": [{"s": "text"}]}, Config(forward_references={"Y": Y}))
assert data == X([Y("text")])


def test_forward_reference_in_dict():

@dataclass
class X:
y: Dict[str, "Y"]

@dataclass
class Y:
s: str

data = from_dict(X, {"y": {"key": {"s": "text"}}}, Config(forward_references={"Y": Y}))
assert data == X({"key": Y("text")})


def test_forward_reference_error():

@dataclass
class X:
y: "Y"

@dataclass
class Y:
s: str

with pytest.raises(ForwardReferenceError):
from_dict(X, {"y": {"s": "text"}})

0 comments on commit 0672f25

Please sign in to comment.