Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All pending dacite PRs - merged and released as dacite2 #1

Merged
merged 19 commits into from
Dec 5, 2022
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
2 changes: 1 addition & 1 deletion .github/workflows/code_check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ '3.7', '3.8', '3.9', '3.10' ]
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11' ]

name: Python ${{ matrix.python-version }}
steps:
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.pytest_cache/
.mypy_cache/
*.pyc
dist/
*.egg-info/
build/
build/
/venv/
/.idea/
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
dist: xenial
language: python
python:
- 3.6
- 3.7
- 3.8
- 3.9-dev
- 3.9
- 3.10
- 3.11
install:
- pip install -e .[dev]
script:
- pytest --cov=dacite
- black --check .
- mypy dacite
- mypy
- pylint dacite
after_success:
coveralls
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.0b1] - 2022-11-28

### Added

- Add explicit `__all__` configuration
- Support [PEP 604] unions through `types.UnionType` #184
- numpy.typing.NDArray dataclass members #156
- Add `allow_missing_fields_as_none` option for missing fields #199
- Ability to customize `from_dict` per dataclass #122
- more general type hint for `type_hooks` #120
- Add Generic Hook Types #83
- Add stricter mypy type check flags #76

[PEP 604]: https://peps.python.org/pep-0604/

### Fixed

- Do not suppress `KeyError` in a type hook
- `from_dict` in case of frozen dataclass with non-init default #196
- empty tuple used for Sequence type #175
- Type_hooks not applied to InitVar fields #172
- extract_optional for Optional of Union. #164
- change Data from Dict to Mapping #159
- Fix encoding of PKG-INFO file #86

## [1.6.0] - 2020-11-30

Expand Down
55 changes: 52 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ $ pip install dacite

## Requirements

Minimum Python version supported by `dacite` is 3.6.
Minimum Python version supported by `dacite` is 3.7.

## Quick start

Expand Down Expand Up @@ -235,8 +235,7 @@ assert result == B(a_list=[A(x='test1', y=1), A(x='test2', y=2)])

You can use `Config.type_hooks` argument if you want to transform the input
data of a data class field with given type into the new value. You have to
pass a following mapping: `{Type: callable}`, where `callable` is a
`Callable[[Any], Any]`.
pass a mapping of type `Mapping[Type, Callable[[Any], Any]`.

```python
@dataclass
Expand All @@ -258,6 +257,56 @@ If a data class field type is a `Optional[T]` you can pass both -
collections, e.g. when a field has type `List[T]` you can use `List[T]` to
transform whole collection or `T` to transform each item.

To target all types use `Any`. Targeting collections without their sub-types
will target all collections of those types, such as `List` and `Dict`.

```python
@dataclass
class ShoppingCart:
store: str
item_ids: List[int]

data = {
'store': '7-Eleven',
'item_ids': [1, 2, 3],
}

def print_value(value):
print(value)
return value

def print_collection(collection):
for item in collection:
print(item)
return collection

result = from_dict(
data_class=ShoppingCart,
data=data,
config=Config(
type_hooks={
Any: print_value,
List: print_collection
}
)
)
```

prints

```
7-Eleven
[1, 2, 3]
1
2
3
```

If a data class field type is a `Optional[T]` you can pass both -
`Optional[T]` or just `T` - as a key in `type_hooks`. The same with generic
collections, e.g. when a field has type `List[T]` you can use `List[T]` to
transform whole collection or `T` to transform each item.

### Casting

It's a very common case that you want to create an instance of a field type
Expand Down
5 changes: 3 additions & 2 deletions dacite/config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from dataclasses import dataclass, field
from typing import Dict, Any, Callable, Optional, Type, List
from typing import Dict, Any, Callable, Optional, Type, List, Union, Mapping


@dataclass
class Config:
type_hooks: Dict[Type, Callable[[Any], Any]] = field(default_factory=dict)
type_hooks: Mapping[Union[Type, object], Callable[[Any], Any]] = field(default_factory=dict)
cast: List[Type] = field(default_factory=list)
forward_references: Optional[Dict[str, Any]] = None
check_types: bool = True
strict: bool = False
strict_unions_match: bool = False
allow_missing_fields_as_none: bool = False
25 changes: 17 additions & 8 deletions dacite/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import copy
from dataclasses import is_dataclass
from itertools import zip_longest
from typing import TypeVar, Type, Optional, get_type_hints, Mapping, Any
from typing import TypeVar, Type, Optional, Mapping, Any

from dacite.config import Config
from dacite.data import Data
from dacite.data import Data, DictData
from dacite.dataclasses import get_default_value_for_field, create_instance, DefaultValueNotFoundError, get_fields
from dacite.exceptions import (
ForwardReferenceError,
Expand All @@ -27,6 +27,7 @@
is_init_var,
extract_init_var,
is_set,
get_data_class_hints,
)

T = TypeVar("T")
Expand All @@ -40,11 +41,11 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
:param config: a configuration of the creation process
:return: an instance of a data class
"""
init_values: Data = {}
post_init_values: Data = {}
init_values: DictData = {}
post_init_values: DictData = {}
config = config or Config()
try:
data_class_hints = get_type_hints(data_class, globalns=config.forward_references)
data_class_hints = get_data_class_hints(data_class, globalns=config.forward_references)
except NameError as error:
raise ForwardReferenceError(str(error))
data_class_fields = get_fields(data_class)
Expand All @@ -67,12 +68,16 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None)
raise
if config.check_types and not is_instance(value, field.type):
raise WrongTypeError(field_path=field.name, field_type=field.type, value=value)
elif not field.init:
# If the non-init field isn't in the dict, let the dataclass handle default, to ensure
# we won't get errors in the case of frozen dataclasses, as issue #195 highlights.
continue
else:
try:
value = get_default_value_for_field(field)
value = get_default_value_for_field(
field, allow_missing_fields_as_none=config.allow_missing_fields_as_none
)
except DefaultValueNotFoundError:
if not field.init:
continue
raise MissingValueError(field.name)
if field.init:
init_values[field.name] = value
Expand All @@ -96,6 +101,8 @@ def _build_value(type_: Type, data: Any, config: Config) -> Any:
_build_value(type_=extract_generic(type_)[0], data=single_val, config=config) for single_val in data
)
elif is_dataclass(type_) and is_instance(data, Data):
if hasattr(type_, "from_dict"):
return type_.from_dict(data=data, config=config)
return from_dict(data_class=type_, data=data, config=config)
return data

Expand Down Expand Up @@ -137,6 +144,8 @@ def _build_value_for_collection(collection: Type, data: Any, config: Config) ->
item_type = extract_generic(collection, defaults=(Any, Any))[1]
return data_type((key, _build_value(type_=item_type, data=value, config=config)) for key, value in data.items())
elif is_instance(data, tuple):
if not data:
return data_type()
types = extract_generic(collection)
if len(types) == 2 and types[1] == Ellipsis:
return data_type(_build_value(type_=types[0], data=item, config=config) for item in data)
Expand Down
5 changes: 3 additions & 2 deletions dacite/data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Dict, Any
from typing import Dict, Mapping, Any

Data = Dict[str, Any]
Data = Mapping[str, Any]
DictData = Dict[str, Any]
10 changes: 5 additions & 5 deletions dacite/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ class DefaultValueNotFoundError(Exception):
pass


def get_default_value_for_field(field: Field) -> Any:
def get_default_value_for_field(field: Field, allow_missing_fields_as_none: bool = False) -> Any:
if field.default != MISSING:
return field.default
elif field.default_factory != MISSING: # type: ignore
return field.default_factory() # type: ignore
elif is_optional(field.type):
elif field.default_factory != MISSING:
return field.default_factory()
elif is_optional(field.type) or allow_missing_fields_as_none:
return None
raise DefaultValueNotFoundError()


def create_instance(data_class: Type[T], init_values: Data, post_init_values: Data) -> T:
instance = data_class(**init_values)
instance: T = data_class(**init_values)
for key, value in post_init_values.items():
setattr(instance, key, value)
return instance
Expand Down
Loading