File: test_bases_meta.py

package info (click to toggle)
dataclass-wizard 0.37.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 2,924 kB
  • sloc: python: 17,189; makefile: 126; javascript: 23
file content (421 lines) | stat: -rw-r--r-- 13,137 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
import logging
from dataclasses import dataclass, field
from datetime import datetime, date
from typing import Optional, List
from unittest.mock import ANY

import pytest
from pytest_mock import MockerFixture

from dataclass_wizard.bases import META
from dataclass_wizard import JSONWizard, EnvWizard
from dataclass_wizard.bases_meta import BaseJSONWizardMeta
from dataclass_wizard.enums import LetterCase, DateTimeTo
from dataclass_wizard.errors import ParseError
from dataclass_wizard.utils.type_conv import date_to_timestamp


log = logging.getLogger(__name__)


@pytest.fixture
def mock_meta_initializers(mocker: MockerFixture):
    return mocker.patch('dataclass_wizard.bases_meta.META_INITIALIZER')


@pytest.fixture
def mock_bind_to(mocker: MockerFixture):
    return mocker.patch(
        'dataclass_wizard.bases_meta.BaseJSONWizardMeta.bind_to')


@pytest.fixture
def mock_env_bind_to(mocker: MockerFixture):
    return mocker.patch(
        'dataclass_wizard.bases_meta.BaseEnvWizardMeta.bind_to')


@pytest.fixture
def mock_get_dumper(mocker: MockerFixture):
    return mocker.patch('dataclass_wizard.bases_meta.get_dumper')


def test_merge_meta_with_or():
    """We are able to merge two Meta classes using the __or__ method."""
    class A(BaseJSONWizardMeta):
        debug_enabled = True
        key_transform_with_dump = 'CAMEL'
        marshal_date_time_as = None
        tag = None
        json_key_to_field = {'k1': 'v1'}

    class B(BaseJSONWizardMeta):
        debug_enabled = False
        key_transform_with_load = 'SNAKE'
        marshal_date_time_as = DateTimeTo.TIMESTAMP
        tag = 'My Test Tag'
        json_key_to_field = {'k2': 'v2'}

    # Merge the two Meta config together
    merged_meta: META = A | B

    # Assert we are a subclass of A, which subclasses from `BaseJSONWizardMeta`
    assert issubclass(merged_meta, BaseJSONWizardMeta)
    assert issubclass(merged_meta, A)
    assert merged_meta is not A

    # Assert Meta fields are merged from A and B as expected (with priority
    # given to A)
    assert 'CAMEL' == merged_meta.key_transform_with_dump == A.key_transform_with_dump
    assert 'SNAKE' == merged_meta.key_transform_with_load == B.key_transform_with_load
    assert None is merged_meta.marshal_date_time_as is A.marshal_date_time_as
    assert True is merged_meta.debug_enabled is A.debug_enabled
    # Assert that special attributes are only copied from A
    assert None is merged_meta.tag is A.tag
    assert {'k1': 'v1'} == merged_meta.json_key_to_field == A.json_key_to_field

    # Assert A and B have not been mutated
    assert A.key_transform_with_load is None
    assert B.key_transform_with_load == 'SNAKE'
    assert B.json_key_to_field == {'k2': 'v2'}
    # Assert that Base class attributes have not been mutated
    assert BaseJSONWizardMeta.key_transform_with_load is None
    assert BaseJSONWizardMeta.json_key_to_field is None


def test_merge_meta_with_and():
    """We are able to merge two Meta classes using the __or__ method."""
    class A(BaseJSONWizardMeta):
        debug_enabled = True
        key_transform_with_dump = 'CAMEL'
        marshal_date_time_as = None
        tag = None
        json_key_to_field = {'k1': 'v1'}

    class B(BaseJSONWizardMeta):
        debug_enabled = False
        key_transform_with_load = 'SNAKE'
        marshal_date_time_as = DateTimeTo.TIMESTAMP
        tag = 'My Test Tag'
        json_key_to_field = {'k2': 'v2'}

    # Merge the two Meta config together
    merged_meta: META = A & B

    # Assert we are a subclass of A, which subclasses from `BaseJSONWizardMeta`
    assert issubclass(merged_meta, BaseJSONWizardMeta)
    assert merged_meta is A

    # Assert Meta fields are merged from A and B as expected (with priority
    # given to A)
    assert 'CAMEL' == merged_meta.key_transform_with_dump == A.key_transform_with_dump
    assert 'SNAKE' == merged_meta.key_transform_with_load == B.key_transform_with_load
    assert DateTimeTo.TIMESTAMP is merged_meta.marshal_date_time_as is A.marshal_date_time_as
    assert False is merged_meta.debug_enabled is A.debug_enabled
    # Assert that special attributes are copied from B
    assert 'My Test Tag' == merged_meta.tag == A.tag
    assert {'k2': 'v2'} == merged_meta.json_key_to_field == A.json_key_to_field

    # Assert A has been mutated
    assert A.key_transform_with_load == B.key_transform_with_load == 'SNAKE'
    assert B.json_key_to_field == {'k2': 'v2'}
    # Assert that Base class attributes have not been mutated
    assert BaseJSONWizardMeta.key_transform_with_load is None
    assert BaseJSONWizardMeta.json_key_to_field is None


def test_meta_initializer_runs_as_expected(mock_log):
    """
    Optional flags passed in when subclassing :class:`JSONWizard.Meta`
    are correctly applied as expected.
    """

    @dataclass
    class MyClass(JSONWizard):

        class Meta(JSONWizard.Meta):
            debug_enabled = True
            json_key_to_field = {
                '__all__': True,
                'my_json_str': 'myCustomStr',
                'anotherJSONField': 'myCustomStr'
            }
            marshal_date_time_as = DateTimeTo.TIMESTAMP
            key_transform_with_load = 'Camel'
            key_transform_with_dump = LetterCase.SNAKE

        myStr: Optional[str]
        myCustomStr: str
        myDate: date
        listOfInt: List[int] = field(default_factory=list)
        isActive: bool = False
        myDt: Optional[datetime] = None

    assert 'DEBUG Mode is enabled' in mock_log.text

    string = """
    {
        "my_str": 20,
        "my_json_str": "test that this is mapped to 'myCustomStr'",
        "ListOfInt": ["1", "2", 3],
        "isActive": "true",
        "my_dt": "2020-01-02T03:04:05",
        "my_date": "2010-11-30"
    }
    """
    c = MyClass.from_json(string)

    log.debug(repr(c))
    log.debug('Prettified JSON: %s', c)

    expected_dt = datetime(2020, 1, 2, 3, 4, 5)
    expected_date = date(2010, 11, 30)

    assert c.myStr == '20'
    assert c.myCustomStr == "test that this is mapped to 'myCustomStr'"
    assert c.listOfInt == [1, 2, 3]
    assert c.isActive
    assert c.myDate == expected_date
    assert c.myDt == expected_dt

    d = c.to_dict()

    # Assert all JSON keys are converted to snake case
    expected_json_keys = ['my_str', 'list_of_int', 'is_active',
                          'my_date', 'my_dt', 'my_json_str']
    assert all(k in d for k in expected_json_keys)

    # Assert that date and datetime objects are serialized to timestamps (int)
    assert isinstance(d['my_date'], int)
    assert d['my_date'] == date_to_timestamp(expected_date)
    assert isinstance(d['my_dt'], int)
    assert d['my_dt'] == round(expected_dt.timestamp())


def test_json_key_to_field_when_add_is_a_falsy_value():
    """
    The `json_key_to_field` attribute is specified when subclassing
    :class:`JSONWizard.Meta`, but the `__all__` field a falsy value.

    Added for code coverage.
    """

    @dataclass
    class MyClass(JSONWizard):

        class Meta(JSONWizard.Meta):
            json_key_to_field = {
                '__all__': False,
                'my_json_str': 'myCustomStr',
                'anotherJSONField': 'myCustomStr'
            }
            key_transform_with_dump = LetterCase.SNAKE

        myCustomStr: str

    # note: this is only expected to run at most once
    # assert 'DEBUG Mode is enabled' in mock_log.text

    string = """
    {
        "my_json_str": "test that this is mapped to 'myCustomStr'"
    }
    """
    c = MyClass.from_json(string)

    log.debug(repr(c))
    log.debug('Prettified JSON: %s', c)

    assert c.myCustomStr == "test that this is mapped to 'myCustomStr'"

    d = c.to_dict()

    # Assert that the default key transform is used when converting the
    # dataclass to JSON.
    assert 'my_json_str' not in d
    assert 'my_custom_str' in d
    assert d['my_custom_str'] == "test that this is mapped to 'myCustomStr'"


def test_meta_config_is_not_implicitly_shared_between_dataclasses():

    @dataclass
    class MyFirstClass(JSONWizard):

        class _(JSONWizard.Meta):
            debug_enabled = True
            marshal_date_time_as = DateTimeTo.TIMESTAMP
            key_transform_with_load = 'Camel'
            key_transform_with_dump = LetterCase.SNAKE

        myStr: str

    @dataclass
    class MySecondClass(JSONWizard):

        my_str: Optional[str]
        my_date: date
        list_of_int: List[int] = field(default_factory=list)
        is_active: bool = False
        my_dt: Optional[datetime] = None

    string = """
    {"My_Str": "hello world"}
    """

    c = MyFirstClass.from_json(string)

    log.debug(repr(c))
    log.debug('Prettified JSON: %s', c)

    assert c.myStr == 'hello world'

    d = c.to_dict()
    assert 'my_str' in d
    assert d['my_str'] == 'hello world'

    string = """
    {
        "my_str": 20,
        "ListOfInt": ["1", "2", 3],
        "isActive": "true",
        "my_dt": "2020-01-02T03:04:05",
        "my_date": "2010-11-30"
    }
    """
    c = MySecondClass.from_json(string)

    log.debug(repr(c))
    log.debug('Prettified JSON: %s', c)

    expected_dt = datetime(2020, 1, 2, 3, 4, 5)
    expected_date = date(2010, 11, 30)

    assert c.my_str == '20'
    assert c.list_of_int == [1, 2, 3]
    assert c.is_active
    assert c.my_date == expected_date
    assert c.my_dt == expected_dt

    d = c.to_dict()

    # Assert all JSON keys are converted to snake case
    expected_json_keys = ['myStr', 'listOfInt', 'isActive',
                          'myDate', 'myDt']
    assert all(k in d for k in expected_json_keys)

    # Assert that date and datetime objects are serialized to timestamps (int)
    assert isinstance(d['myDate'], str)
    assert d['myDate'] == expected_date.isoformat()
    assert isinstance(d['myDt'], str)
    assert d['myDt'] == expected_dt.isoformat()


def test_meta_initializer_is_called_when_meta_is_an_inner_class(
        mock_meta_initializers):
    """
    Meta Initializer `dict` should be updated when `Meta` is an inner class.
    """

    class _(JSONWizard):
        class _(JSONWizard.Meta):
            debug_enabled = True

    mock_meta_initializers.__setitem__.assert_called_once()


def test_env_meta_initializer_not_called_when_meta_is_not_an_inner_class(
        mock_meta_initializers, mock_env_bind_to):
    """
    Meta Initializer `dict` should *not* be updated when `Meta` has no outer
    class.
    """

    class _(EnvWizard.Meta):
        debug_enabled = True

    mock_meta_initializers.__setitem__.assert_not_called()
    mock_env_bind_to.assert_called_once_with(ANY, create=False)


def test_meta_initializer_not_called_when_meta_is_not_an_inner_class(
        mock_meta_initializers, mock_bind_to):
    """
    Meta Initializer `dict` should *not* be updated when `Meta` has no outer
    class.
    """

    class _(JSONWizard.Meta):
        debug_enabled = True

    mock_meta_initializers.__setitem__.assert_not_called()
    mock_bind_to.assert_called_once_with(ANY, create=False)


def test_meta_initializer_errors_when_key_transform_with_load_is_invalid():
    """
    Test when an invalid value for the ``key_transform_with_load`` attribute
    is specified when sub-classing from :class:`JSONWizard.Meta`.

    """
    with pytest.raises(ParseError):

        @dataclass
        class _(JSONWizard):
            class Meta(JSONWizard.Meta):
                key_transform_with_load = 'Hello'

            my_str: Optional[str]
            list_of_int: List[int] = field(default_factory=list)


def test_meta_initializer_errors_when_key_transform_with_dump_is_invalid():
    """
    Test when an invalid value for the ``key_transform_with_dump`` attribute
    is specified when sub-classing from :class:`JSONWizard.Meta`.

    """
    with pytest.raises(ParseError):

        @dataclass
        class _(JSONWizard):
            class Meta(JSONWizard.Meta):
                key_transform_with_dump = 'World'

            my_str: Optional[str]
            list_of_int: List[int] = field(default_factory=list)


def test_meta_initializer_errors_when_marshal_date_time_as_is_invalid():
    """
    Test when an invalid value for the ``marshal_date_time_as`` attribute
    is specified when sub-classing from :class:`JSONWizard.Meta`.

    """
    with pytest.raises(ParseError):

        @dataclass
        class _(JSONWizard):
            class Meta(JSONWizard.Meta):
                marshal_date_time_as = 'iso'

            my_str: Optional[str]
            list_of_int: List[int] = field(default_factory=list)


def test_meta_initializer_is_noop_when_marshal_date_time_as_is_iso_format(mock_get_dumper):
    """
    Test that it's a noop when the value for ``marshal_date_time_as``
    is `ISO_FORMAT`, which is the default conversion method for the dumper
    otherwise.

    """
    @dataclass
    class _(JSONWizard):
        class Meta(JSONWizard.Meta):
            marshal_date_time_as = 'ISO Format'

        my_str: Optional[str]
        list_of_int: List[int] = field(default_factory=list)

    mock_get_dumper().register_dump_hook.assert_not_called()