Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
.idea

venv*
dacite-env*

.benchmarks
benchmark.json
139 changes: 109 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ assert user == User(name='John', age=30, is_active=True)
Dacite supports following features:

- nested structures
- (basic) types checking
- (basic) type checking
- optional fields (i.e. `typing.Optional`)
- unions
- generics
- forward references
- collections
- custom type hooks
- case conversion

## Motivation

Expand Down Expand Up @@ -109,6 +111,7 @@ Configuration is a (data) class with following fields:
- `check_types`
- `strict`
- `strict_unions_match`
- `convert_key`

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

### Generics

Dacite supports generics: (multi-)generic dataclasses, but also dataclasses that inherit from a generic dataclass, or dataclasses that have a generic dataclass field.

```python
T = TypeVar('T')
U = TypeVar('U')

@dataclass
class X:
a: str


@dataclass
class A(Generic[T, U]):
x: T
y: list[U]

data = {
'x': {
'a': 'foo',
},
'y': [1, 2, 3]
}

result = from_dict(data_class=A[X, int], data=data)

assert result == A(x=X(a='foo'), y=[1,2,3])


@dataclass
class B(A[X, int]):
z: str

data = {
'x': {
'a': 'foo',
},
'y': [1, 2, 3],
'z': 'bar'
}

result = from_dict(data_class=B, data=data)

assert result == B(x=X(a='foo'), y=[1,2,3], z='bar')


@dataclass
class C:
z: A[X, int]

data = {
'z': {
'x': {
'a': 'foo',
},
'y': [1, 2, 3],
}
}

result = from_dict(data_class=C, data=data)

assert result == C(z=A(x=X(a='foo'), y=[1,2,3]))
```

### Type hooks

You can use `Config.type_hooks` argument if you want to transform the input
Expand Down Expand Up @@ -313,30 +381,17 @@ data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y}))
assert data == X(Y("text"))
```

### Types checking
### Type checking

There are rare cases when `dacite` built-in type checker can not validate
your types (e.g. custom generic class) or you have such functionality
covered by other library and you don't want to validate your types twice.
In such case you can disable type checking with `Config(check_types=False)`.
By default types checking is enabled.
If you want to trade-off type checking for speed, you can disabled type checking by setting `check_types` to `False`.

```python
T = TypeVar('T')


class X(Generic[T]):
pass


@dataclass
class A:
x: X[str]


x = X[str]()
x: str

assert from_dict(A, {'x': x}, config=Config(check_types=False)) == A(x=x)
# won't throw an error even though the type is wrong
from_dict(A, {'x': 4}, config=Config(check_types=False))
```

### Strict mode
Expand All @@ -354,6 +409,30 @@ returns instance of this type. It means that it's possible that there are other
matching types further on the `Union` types list. With `strict_unions_match`
only a single match is allowed, otherwise `dacite` raises `StrictUnionMatchError`.

## Convert key

You can pass a callable to the `convert_key` configuration parameter to convert camelCase to snake_case.

```python
def to_camel_case(key: str) -> str:
first_part, *remaining_parts = key.split('_')
return first_part + ''.join(part.title() for part in remaining_parts)

@dataclass
class Person:
first_name: str
last_name: str

data = {
'firstName': 'John',
'lastName': 'Doe'
}

result = from_dict(Person, data, Config(convert_key=to_camel_case))

assert result == Person(first_name='John', last_name='Doe')
```

## Exceptions

Whenever something goes wrong, `from_dict` will raise adequate
Expand Down Expand Up @@ -392,33 +471,33 @@ first within an issue.

Clone `dacite` repository:

```
$ git clone [email protected]:konradhalas/dacite.git
```bash
git clone [email protected]:konradhalas/dacite.git
```

Create and activate virtualenv in the way you like:

```
$ python3 -m venv dacite-env
$ source dacite-env/bin/activate
```bash
python3 -m venv dacite-env
source dacite-env/bin/activate
```

Install all `dacite` dependencies:

```
$ pip install -e .[dev]
```bash
pip install -e .[dev]
```

And, optionally but recommended, install pre-commit hook for black:

```
$ pre-commit install
```bash
pre-commit install
```

To run tests you just have to fire:

```
$ pytest
```bash
pytest
```

### Performance testing
Expand Down
1 change: 1 addition & 0 deletions dacite/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Config:
check_types: bool = True
strict: bool = False
strict_unions_match: bool = False
convert_key: Callable[[str], str] = field(default_factory=lambda: lambda x: x)

@cached_property
def hashable_forward_references(self) -> Optional[FrozenDict]:
Expand Down
24 changes: 17 additions & 7 deletions dacite/core.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from dataclasses import is_dataclass
from itertools import zip_longest
from typing import TypeVar, Type, Optional, get_type_hints, Mapping, Any, Collection, MutableMapping
from typing import TypeVar, Type, Optional, Mapping, Any, Collection, MutableMapping

from dacite.cache import cache
from dacite.config import Config
from dacite.data import Data
from dacite.dataclasses import (
get_default_value_for_field,
DefaultValueNotFoundError,
get_fields,
is_frozen,
)
from dacite.exceptions import (
Expand All @@ -33,6 +32,8 @@
is_subclass,
)

from dacite.generics import get_concrete_type_hints, get_fields, orig

T = TypeVar("T")


Expand All @@ -47,21 +48,26 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
init_values: MutableMapping[str, Any] = {}
post_init_values: MutableMapping[str, Any] = {}
config = config or Config()

try:
data_class_hints = cache(get_type_hints)(data_class, localns=config.hashable_forward_references)
data_class_hints = cache(get_concrete_type_hints)(data_class, localns=config.hashable_forward_references)
except NameError as error:
raise ForwardReferenceError(str(error))

data_class_fields = cache(get_fields)(data_class)

if config.strict:
extra_fields = set(data.keys()) - {f.name for f in data_class_fields}
if extra_fields:
raise UnexpectedDataError(keys=extra_fields)

for field in data_class_fields:
field_type = data_class_hints[field.name]
if field.name in data:
key = config.convert_key(field.name)

if key in data:
try:
field_data = data[field.name]
value = _build_value(type_=field_type, data=field_data, config=config)
value = _build_value(type_=field_type, data=data[key], config=config)
except DaciteFieldError as error:
error.update_path(field.name)
raise
Expand All @@ -74,13 +80,17 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
if not field.init:
continue
raise MissingValueError(field.name)

if field.init:
init_values[field.name] = value
elif not is_frozen(data_class):
post_init_values[field.name] = value

instance = data_class(**init_values)

for key, value in post_init_values.items():
setattr(instance, key, value)

return instance


Expand All @@ -95,7 +105,7 @@ def _build_value(type_: Type, data: Any, config: Config) -> Any:
data = _build_value_for_union(union=type_, data=data, config=config)
elif is_generic_collection(type_):
data = _build_value_for_collection(collection=type_, data=data, config=config)
elif cache(is_dataclass)(type_) and isinstance(data, Mapping):
elif cache(is_dataclass)(orig(type_)) and isinstance(data, Mapping):
data = from_dict(data_class=type_, data=data, config=config)
for cast_type in config.cast:
if is_subclass(type_, cast_type):
Expand Down
Loading
Loading