Skip to content

Commit 72b3d0a

Browse files
authored
Merge branch 'master' into shorten-tracebacks
2 parents e2ca0c6 + 9fa6a84 commit 72b3d0a

17 files changed

Lines changed: 490 additions & 119 deletions

.github/workflows/ci_master.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ on:
44
push:
55
branches:
66
- master
7+
workflow_dispatch:
78

89
jobs:
910
ci:
1011
uses: ./.github/workflows/code_check.yaml
11-
secrets:
12-
benchmark_token: ${{ secrets.BENCHMARK_TOKEN }}
12+
secrets: inherit
1313
with:
1414
publish_performance: true
1515
store_benchmark: true

.github/workflows/ci_pr.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ name: CI Pull Request
22

33
on:
44
pull_request:
5+
workflow_dispatch:
56

67
jobs:
78
ci:
89
uses: ./.github/workflows/code_check.yaml
9-
secrets:
10-
benchmark_token: ${{ secrets.BENCHMARK_TOKEN }}
10+
secrets: inherit
1111
with:
1212
publish_performance: false
1313
store_benchmark: false

.github/workflows/code_check.yaml

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ name: Code Check
22

33
on:
44
workflow_call:
5-
secrets:
6-
benchmark_token:
7-
required: true
85
inputs:
96
publish_performance:
107
required: true
@@ -15,16 +12,16 @@ on:
1512

1613
jobs:
1714
check:
18-
runs-on: ubuntu-latest
15+
runs-on: ubuntu-22.04
1916
strategy:
2017
matrix:
21-
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11']
18+
python-version: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
2219

2320
name: Python ${{ matrix.python-version }}
2421
steps:
25-
- uses: actions/checkout@v3
22+
- uses: actions/checkout@v4
2623
- name: Set up Python ${{ matrix.python-version }}
27-
uses: actions/setup-python@v3
24+
uses: actions/setup-python@v5
2825
with:
2926
python-version: ${{ matrix.python-version }}
3027
architecture: x64
@@ -44,10 +41,10 @@ jobs:
4441
tool: 'pytest'
4542
output-file-path: benchmark.json
4643
save-data-file: ${{ inputs.store_benchmark }}
47-
github-token: ${{ secrets.benchmark_token }}
44+
github-token: ${{ secrets.GITHUB_TOKEN }}
4845
auto-push: ${{ inputs.publish_performance }}
4946
benchmark-data-dir-path: performance/${{ matrix.python-version }}
5047
comment-always: false
5148
alert-threshold: '130%'
52-
comment-on-alert: false
53-
fail-on-alert: true
49+
comment-on-alert: true
50+
fail-on-alert: false

.github/workflows/publish.yaml

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,41 @@ on:
44
types: [published]
55

66
jobs:
7-
publish:
7+
build:
88
runs-on: ubuntu-latest
99
steps:
10-
- uses: actions/checkout@v3
11-
- uses: actions/setup-python@v3
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-python@v5
1212
with:
1313
python-version: '3.11'
1414
architecture: x64
1515
- name: Build package
1616
run: |
1717
python -m pip install --upgrade build
1818
python -m build
19-
- name: Publish package to PyPI
20-
uses: pypa/gh-action-pypi-publish@release/v1
19+
- name: Store the distribution packages
20+
uses: actions/upload-artifact@v4
2121
with:
22-
user: __token__
23-
password: ${{ secrets.PYPI_API_TOKEN }}
22+
name: python-package-distributions
23+
path: dist/
24+
25+
publish-to-pypi:
26+
name: Publish to PyPI
27+
needs:
28+
- build
29+
runs-on: ubuntu-latest
30+
environment:
31+
name: pypi
32+
url: https://pypi.org/p/dacite
33+
permissions:
34+
id-token: write # IMPORTANT: mandatory for trusted publishing
35+
steps:
36+
- name: Download dist
37+
uses: actions/download-artifact@v4
38+
with:
39+
name: python-package-distributions
40+
path: dist/
41+
- name: Publish package to PyPI
42+
uses: pypa/gh-action-pypi-publish@release/v1
43+
with:
44+
password: ${{ secrets.PYPI_API_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ build/
66
.idea
77

88
venv*
9+
dacite-env*
910

1011
.benchmarks
1112
benchmark.json

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- [Support generics](https://github.com/konradhalas/dacite/pull/272)
11+
- Change type definition for `Data` in order to be more permissive
1012
- Fix issues with caching internal function calls
1113

1214
## [1.8.1] - 2023-05-12

README.md

Lines changed: 109 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ assert user == User(name='John', age=30, is_active=True)
5454
Dacite supports following features:
5555

5656
- nested structures
57-
- (basic) types checking
57+
- (basic) type checking
5858
- optional fields (i.e. `typing.Optional`)
5959
- unions
60+
- generics
6061
- forward references
6162
- collections
6263
- custom type hooks
64+
- case conversion
6365

6466
## Motivation
6567

@@ -109,6 +111,7 @@ Configuration is a (data) class with following fields:
109111
- `check_types`
110112
- `strict`
111113
- `strict_unions_match`
114+
- `convert_key`
112115

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

239+
### Generics
240+
241+
Dacite supports generics: (multi-)generic dataclasses, but also dataclasses that inherit from a generic dataclass, or dataclasses that have a generic dataclass field.
242+
243+
```python
244+
T = TypeVar('T')
245+
U = TypeVar('U')
246+
247+
@dataclass
248+
class X:
249+
a: str
250+
251+
252+
@dataclass
253+
class A(Generic[T, U]):
254+
x: T
255+
y: list[U]
256+
257+
data = {
258+
'x': {
259+
'a': 'foo',
260+
},
261+
'y': [1, 2, 3]
262+
}
263+
264+
result = from_dict(data_class=A[X, int], data=data)
265+
266+
assert result == A(x=X(a='foo'), y=[1,2,3])
267+
268+
269+
@dataclass
270+
class B(A[X, int]):
271+
z: str
272+
273+
data = {
274+
'x': {
275+
'a': 'foo',
276+
},
277+
'y': [1, 2, 3],
278+
'z': 'bar'
279+
}
280+
281+
result = from_dict(data_class=B, data=data)
282+
283+
assert result == B(x=X(a='foo'), y=[1,2,3], z='bar')
284+
285+
286+
@dataclass
287+
class C:
288+
z: A[X, int]
289+
290+
data = {
291+
'z': {
292+
'x': {
293+
'a': 'foo',
294+
},
295+
'y': [1, 2, 3],
296+
}
297+
}
298+
299+
result = from_dict(data_class=C, data=data)
300+
301+
assert result == C(z=A(x=X(a='foo'), y=[1,2,3]))
302+
```
303+
236304
### Type hooks
237305

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

316-
### Types checking
384+
### Type checking
317385

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

324388
```python
325-
T = TypeVar('T')
326-
327-
328-
class X(Generic[T]):
329-
pass
330-
331-
332389
@dataclass
333390
class A:
334-
x: X[str]
335-
336-
337-
x = X[str]()
391+
x: str
338392

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

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

412+
## Convert key
413+
414+
You can pass a callable to the `convert_key` configuration parameter to convert camelCase to snake_case.
415+
416+
```python
417+
def to_camel_case(key: str) -> str:
418+
first_part, *remaining_parts = key.split('_')
419+
return first_part + ''.join(part.title() for part in remaining_parts)
420+
421+
@dataclass
422+
class Person:
423+
first_name: str
424+
last_name: str
425+
426+
data = {
427+
'firstName': 'John',
428+
'lastName': 'Doe'
429+
}
430+
431+
result = from_dict(Person, data, Config(convert_key=to_camel_case))
432+
433+
assert result == Person(first_name='John', last_name='Doe')
434+
```
435+
357436
## Exceptions
358437

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

393472
Clone `dacite` repository:
394473

395-
```
396-
$ git clone [email protected]:konradhalas/dacite.git
474+
```bash
475+
git clone [email protected]:konradhalas/dacite.git
397476
```
398477

399478
Create and activate virtualenv in the way you like:
400479

401-
```
402-
$ python3 -m venv dacite-env
403-
$ source dacite-env/bin/activate
480+
```bash
481+
python3 -m venv dacite-env
482+
source dacite-env/bin/activate
404483
```
405484

406485
Install all `dacite` dependencies:
407486

408-
```
409-
$ pip install -e .[dev]
487+
```bash
488+
pip install -e .[dev]
410489
```
411490

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

414-
```
415-
$ pre-commit install
493+
```bash
494+
pre-commit install
416495
```
417496

418497
To run tests you just have to fire:
419498

420-
```
421-
$ pytest
499+
```bash
500+
pytest
422501
```
423502

424503
### Performance testing

dacite/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Config:
1919
check_types: bool = True
2020
strict: bool = False
2121
strict_unions_match: bool = False
22+
convert_key: Callable[[str], str] = field(default_factory=lambda: lambda x: x)
2223

2324
@cached_property
2425
def hashable_forward_references(self) -> Optional[FrozenDict]:

0 commit comments

Comments
 (0)