From 4003a588ba8f9aaa5c8f78fdb101753aed573d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veith=20R=C3=B6thlingsh=C3=B6fer?= Date: Tue, 14 Jan 2020 10:51:58 +0100 Subject: [PATCH 1/4] Fix nested optional type when loading from schema, including test cases --- dataclasses_json/core.py | 5 ++++- dataclasses_json/mm.py | 4 ++++ dataclasses_json/utils.py | 6 ++++-- tests/entities.py | 21 ++++++++++++++++++++- tests/test_schema.py | 17 +++++++++++++++-- 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/dataclasses_json/core.py b/dataclasses_json/core.py index b0a58173..6f603c69 100644 --- a/dataclasses_json/core.py +++ b/dataclasses_json/core.py @@ -235,7 +235,10 @@ def _decode_generic(type_, value, infer_missing): except TypeError: res = type_(xs) else: # Optional or Union - if _is_optional(type_) and len(type_.__args__) == 2: # Optional + if not hasattr(type_, "__args__"): + # Any, just accept + res = value + elif _is_optional(type_) and len(type_.__args__) == 2: # Optional type_arg = type_.__args__[0] if is_dataclass(type_arg) or is_dataclass(value): res = _decode_dataclass(type_arg, value, infer_missing) diff --git a/dataclasses_json/mm.py b/dataclasses_json/mm.py index 815b2455..54fa999c 100644 --- a/dataclasses_json/mm.py +++ b/dataclasses_json/mm.py @@ -119,6 +119,7 @@ def _deserialize(self, value, attr, data, **kwargs): typing.Dict: fields.Dict, typing.Tuple: fields.Tuple, typing.Callable: fields.Function, + typing.Any: fields.Raw, dict: fields.Dict, list: fields.List, str: fields.Str, @@ -249,6 +250,9 @@ def inner(type_, options): args = [inner(a, {}) for a in getattr(type_, '__args__', []) if a is not type(None)] + if _is_optional(type_): + options["allow_none"] = True + if origin in TYPES: return TYPES[origin](*args, **options) diff --git a/dataclasses_json/utils.py b/dataclasses_json/utils.py index c8f93818..cdf63cc7 100644 --- a/dataclasses_json/utils.py +++ b/dataclasses_json/utils.py @@ -1,7 +1,7 @@ import inspect import sys from datetime import datetime, timezone -from typing import Collection, Mapping, Optional, TypeVar +from typing import Collection, Mapping, Optional, TypeVar, Any def _get_type_cons(type_): @@ -88,7 +88,9 @@ def _is_new_type(type_): def _is_optional(type_): - return _issubclass_safe(type_, Optional) or _hasargs(type_, type(None)) + return (_issubclass_safe(type_, Optional) or + _hasargs(type_, type(None)) or + type_ is Any) def _is_mapping(type_): diff --git a/tests/entities.py b/tests/entities.py index 866fe638..449b17b7 100644 --- a/tests/entities.py +++ b/tests/entities.py @@ -10,7 +10,8 @@ Set, Tuple, TypeVar, - Union) + Union, + Any) from uuid import UUID from marshmallow import fields @@ -246,3 +247,21 @@ class DataClassWithOptionalDecimal: @dataclass class DataClassWithOptionalUuid: a: Optional[UUID] + + +@dataclass_json +@dataclass +class DataClassWithNestedAny: + a: Dict[str, Any] + + +@dataclass_json +@dataclass +class DataClassWithNestedOptionalAny: + a: Dict[str, Optional[Any]] + + +@dataclass_json +@dataclass +class DataClassWithNestedOptional: + a: Dict[str, Optional[int]] diff --git a/tests/test_schema.py b/tests/test_schema.py index e5652429..2545821d 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,9 +1,10 @@ -from .entities import DataClassDefaultListStr, DataClassDefaultOptionalList, DataClassList, DataClassOptional +from .entities import (DataClassDefaultListStr, DataClassDefaultOptionalList, DataClassList, DataClassOptional, + DataClassWithNestedOptional, DataClassWithNestedOptionalAny, DataClassWithNestedAny) from .test_letter_case import CamelCasePerson, KebabCasePerson, SnakeCasePerson, FieldNamePerson - test_do_list = """[{}, {"children": [{"name": "a"}, {"name": "b"}]}]""" test_list = '[{"children": [{"name": "a"}, {"name": "b"}]}]' +nested_optional_data = '{"a": {"test": null}}' class TestSchema: @@ -27,3 +28,15 @@ def test_letter_case(self): for cls in (CamelCasePerson, KebabCasePerson, SnakeCasePerson, FieldNamePerson): p = cls('Alice') assert p.to_dict() == cls.schema().dump(p) + + def test_nested_optional(self): + DataClassWithNestedOptional.schema().loads(nested_optional_data) + assert True + + def test_nested_optional_any(self): + DataClassWithNestedOptionalAny.schema().loads(nested_optional_data) + assert True + + def test_nested_any_accepts_optional(self): + DataClassWithNestedAny.schema().loads(nested_optional_data) + assert True From 9d6abce060a72eff8c80cab6fee24befe82e6483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veith=20R=C3=B6thlingsh=C3=B6fer?= Date: Thu, 30 Apr 2020 10:59:06 +0200 Subject: [PATCH 2/4] Group undefined parameter test cases --- tests/test_undefined_parameters.py | 615 +++++++++++++++-------------- 1 file changed, 319 insertions(+), 296 deletions(-) diff --git a/tests/test_undefined_parameters.py b/tests/test_undefined_parameters.py index 58740dcb..9f6e1f09 100644 --- a/tests/test_undefined_parameters.py +++ b/tests/test_undefined_parameters.py @@ -5,7 +5,8 @@ import marshmallow from dataclasses_json.core import Json -from dataclasses_json.api import dataclass_json, LetterCase, Undefined, DataClassJsonMixin +from dataclasses_json.api import dataclass_json, LetterCase, Undefined, \ + DataClassJsonMixin from dataclasses_json import CatchAll from dataclasses_json.undefined import UndefinedParameterError @@ -39,6 +40,13 @@ class DontCareAPIDump: data: Dict[str, Any] +@dataclass_json(undefined=Undefined.WARN) +@dataclass +class WarnApiDump: + endpoint: str + data: Dict[str, Any] + + @pytest.fixture def valid_response() -> Dict[Any, Json]: return {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} @@ -77,310 +85,325 @@ def boss_json(): return boss_json -def test_undefined_parameters_catch_all_invalid_back(invalid_response): - dump = UnknownAPIDump.from_dict(invalid_response) - inverse_dict = dump.to_dict() - assert inverse_dict == invalid_response - - -def test_undefined_parameters_catch_all_valid(valid_response): - dump = UnknownAPIDump.from_dict(valid_response) - assert dump.catch_all == {} - - -def test_undefined_parameters_catch_all_no_field(invalid_response): - with pytest.raises(UndefinedParameterError): - UnknownAPIDumpNoCatchAllField.from_dict(invalid_response) - - -def test_undefined_parameters_catch_all_multiple_fields(invalid_response): - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass() - class UnknownAPIDumpMultipleCatchAll: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll - catch_all2: CatchAll - - with pytest.raises(UndefinedParameterError): - UnknownAPIDumpMultipleCatchAll.from_dict(invalid_response) - - -def test_undefined_parameters_catch_all_works_with_letter_case(invalid_response_camel_case): - @dataclass_json(undefined=Undefined.INCLUDE, letter_case=LetterCase.CAMEL) - @dataclass() - class UnknownAPIDumpCamelCase: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll - - dump = UnknownAPIDumpCamelCase.from_dict(invalid_response_camel_case) - assert {"undefinedFieldName": [1, 2, 3]} == dump.catch_all - assert invalid_response_camel_case == dump.to_dict() - - -def test_undefined_parameters_catch_all_raises_if_initialized_with_catch_all_field_name(valid_response): - valid_response["catch_all"] = "some-value" - with pytest.raises(UndefinedParameterError): - UnknownAPIDump.from_dict(valid_response) - - -def test_undefined_parameters_catch_all_initialized_with_dict_and_more_unknown(invalid_response): - invalid_response["catch_all"] = {"someValue": "some-stuff"} - dump = UnknownAPIDump.from_dict(invalid_response) - assert dump.catch_all == {"someValue": "some-stuff", "undefined_field_name": [1, 2, 3]} - - -def test_undefined_parameters_raise_invalid(invalid_response): - with pytest.raises(UndefinedParameterError): - WellKnownAPIDump.from_dict(invalid_response) - - -def test_undefined_parameters_raise_valid(valid_response): - assert valid_response == WellKnownAPIDump.from_dict(valid_response).to_dict() - - -def test_undefined_parameters_ignore(valid_response, invalid_response): - from_valid = DontCareAPIDump.from_dict(valid_response) - from_invalid = DontCareAPIDump.from_dict(invalid_response) - assert from_valid == from_invalid - - -def test_undefined_parameters_ignore_to_dict(invalid_response, valid_response): - dump = DontCareAPIDump.from_dict(invalid_response) - dump_dict = dump.to_dict() - assert valid_response == dump_dict - - -def test_undefined_parameters_ignore_nested_schema(boss_json): - @dataclass_json(undefined=Undefined.EXCLUDE) - @dataclass(frozen=True) - class Minion: - name: str - - @dataclass_json(undefined=Undefined.EXCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - - boss = Boss.schema().loads(boss_json) - assert len(boss.minions) == 2 - assert boss.minions == [Minion(name="evil minion"), Minion(name="very evil minion")] - - -def test_undefined_parameters_raise_nested_schema(boss_json): - @dataclass_json(undefined=Undefined.RAISE) - @dataclass(frozen=True) - class Minion: - name: str - - @dataclass_json(undefined=Undefined.EXCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - - with pytest.raises(marshmallow.exceptions.ValidationError): - Boss.schema().loads(boss_json) - - -def test_undefined_parameters_catch_all_nested_schema(boss_json): - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Minion: - name: str - catch_all: CatchAll - - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - catch_all: CatchAll - - boss = Boss.schema().loads(boss_json) - assert {"UNKNOWN_PROPERTY": "value"} == boss.catch_all - assert {"UNKNOWN_PROPERTY": "value"} == boss.minions[0].catch_all - assert {} == boss.minions[1].catch_all - - -def test_undefined_parameters_catch_all_schema_dump(boss_json): - import json +class TestCatchAllUndefinedParameters: - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Minion: - name: str - catch_all: CatchAll + def test_undefined_parameters_catch_all_invalid_back(self, + invalid_response): + dump = UnknownAPIDump.from_dict(invalid_response) + inverse_dict = dump.to_dict() + assert inverse_dict == invalid_response - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - catch_all: CatchAll + def test_undefined_parameters_catch_all_valid(self, valid_response): + dump = UnknownAPIDump.from_dict(valid_response) + assert dump.catch_all == {} - boss = Boss.schema().loads(boss_json) - assert json.loads(boss_json) == Boss.schema().dump(boss) - assert "".join(boss_json.replace('\n', '').split()) == "".join(Boss.schema().dumps(boss).replace('\n', '').split()) + def test_undefined_parameters_catch_all_no_field(self, invalid_response): + with pytest.raises(UndefinedParameterError): + UnknownAPIDumpNoCatchAllField.from_dict(invalid_response) - -def test_undefined_parameters_catch_all_schema_roundtrip(boss_json): - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Minion: - name: str - catch_all: CatchAll - - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - catch_all: CatchAll - - boss1 = Boss.schema().loads(boss_json) - dumped_s = Boss.schema().dumps(boss1) - boss2 = Boss.schema().loads(dumped_s) - assert boss1 == boss2 - - -def test_undefined_parameters_catch_all_ignore_mix_nested_schema(boss_json): - @dataclass_json(undefined=Undefined.EXCLUDE) - @dataclass(frozen=True) - class Minion: - name: str - - @dataclass_json(undefined=Undefined.INCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - catch_all: CatchAll - - boss = Boss.schema().loads(boss_json) - assert Minion(name="evil minion") == boss.minions[0] - assert Minion(name="very evil minion") == boss.minions[1] - assert {"UNKNOWN_PROPERTY": "value"} == boss.catch_all - - -def test_it_works_from_string(invalid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpFromString: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll - - dump = UnknownAPIDumpFromString.from_dict(invalid_response) - assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all - - -def test_string_only_accepts_valid_actions(): - with pytest.raises(UndefinedParameterError): - @dataclass_json(undefined="not sure what this is supposed to do") + def test_undefined_parameters_catch_all_multiple_fields(self, + invalid_response): + @dataclass_json(undefined=Undefined.INCLUDE) @dataclass() - class WontWork: + class UnknownAPIDumpMultipleCatchAll: endpoint: str + data: Dict[str, Any] + catch_all: CatchAll + catch_all2: CatchAll + with pytest.raises(UndefinedParameterError): + UnknownAPIDumpMultipleCatchAll.from_dict(invalid_response) -def test_undefined_parameters_raises_with_default_argument_and_supplied_catch_all_name(invalid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpDefault: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll = None - - invalid_response["catch_all"] = "this should not happen" - with pytest.raises(UndefinedParameterError): - UnknownAPIDumpDefault.from_dict(invalid_response) - - -def test_undefined_parameters_doesnt_raise_with_default(valid_response, invalid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpDefault: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll = None - - from_valid = UnknownAPIDumpDefault.from_dict(valid_response) - from_invalid = UnknownAPIDumpDefault.from_dict(invalid_response) - assert from_valid.catch_all is None - assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all - - -def test_undefined_parameters_doesnt_raise_with_default_factory(valid_response, invalid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpDefault(DataClassJsonMixin): - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll = field(default_factory=dict) - - from_valid = UnknownAPIDumpDefault.from_dict(valid_response) - from_invalid = UnknownAPIDumpDefault.from_dict(invalid_response) - assert from_valid.catch_all == {} - assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all - - -def test_undefined_parameters_catch_all_init_valid(valid_response): - dump = UnknownAPIDump(**valid_response) - assert dump.catch_all == {} - - -def test_undefined_parameters_catch_all_init_invalid(invalid_response): - dump = UnknownAPIDump(**invalid_response) - assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all - - -def test_undefined_parameters_catch_all_init_args(): - dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", undefined="123") - assert dump.endpoint == "some-endpoint" - assert dump.data == {"some-data": "foo"} - assert dump.catch_all == {'_UNKNOWN0': 'unknown1', '_UNKNOWN1': 'unknown2', "undefined": "123"} - - -def test_undefined_parameters_catch_all_init_args_kwargs_mixed(): - dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", catch_all={"bar": "example"}, - undefined="123") - assert dump.endpoint == "some-endpoint" - assert dump.data == {"some-data": "foo"} - assert dump.catch_all == {'_UNKNOWN0': 'unknown1', '_UNKNOWN1': 'unknown2', "bar": "example", "undefined": "123"} - - -def test_undefined_parameters_ignore_init_args(): - dump = DontCareAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", undefined="123") - assert dump.endpoint == "some-endpoint" - assert dump.data == {"some-data": "foo"} - - -def test_undefined_parameters_ignore_init_invalid(invalid_response, valid_response): - dump_invalid = DontCareAPIDump(**invalid_response) - dump_valid = DontCareAPIDump(**valid_response) - assert dump_valid == dump_invalid - - -def test_undefined_parameters_raise_init(invalid_response): - with pytest.raises(TypeError): - WellKnownAPIDump(**invalid_response) - - -def test_undefined_parameters_catch_all_default_no_undefined(valid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpDefault: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll = None + def test_undefined_parameters_catch_all_works_with_letter_case(self, + invalid_response_camel_case): + @dataclass_json(undefined=Undefined.INCLUDE, + letter_case=LetterCase.CAMEL) + @dataclass() + class UnknownAPIDumpCamelCase: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll + + dump = UnknownAPIDumpCamelCase.from_dict(invalid_response_camel_case) + assert {"undefinedFieldName": [1, 2, 3]} == dump.catch_all + assert invalid_response_camel_case == dump.to_dict() + + def test_undefined_parameters_catch_all_raises_if_initialized_with_catch_all_field_name( + self, + valid_response): + valid_response["catch_all"] = "some-value" + with pytest.raises(UndefinedParameterError): + UnknownAPIDump.from_dict(valid_response) + + def test_undefined_parameters_catch_all_initialized_with_dict_and_more_unknown( + self, + invalid_response): + invalid_response["catch_all"] = {"someValue": "some-stuff"} + dump = UnknownAPIDump.from_dict(invalid_response) + assert dump.catch_all == {"someValue": "some-stuff", + "undefined_field_name": [1, 2, 3]} + + def test_undefined_parameters_raises_with_default_argument_and_supplied_catch_all_name( + self, + invalid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpDefault: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll = None - dump = UnknownAPIDumpDefault.from_dict(valid_response) - assert dump.to_dict() == valid_response + invalid_response["catch_all"] = "this should not happen" + with pytest.raises(UndefinedParameterError): + UnknownAPIDumpDefault.from_dict(invalid_response) + def test_undefined_parameters_doesnt_raise_with_default(self, + valid_response, + invalid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpDefault: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll = None + + from_valid = UnknownAPIDumpDefault.from_dict(valid_response) + from_invalid = UnknownAPIDumpDefault.from_dict(invalid_response) + assert from_valid.catch_all is None + assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all + + def test_undefined_parameters_doesnt_raise_with_default_factory(self, + valid_response, + invalid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpDefault(DataClassJsonMixin): + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll = field(default_factory=dict) + + from_valid = UnknownAPIDumpDefault.from_dict(valid_response) + from_invalid = UnknownAPIDumpDefault.from_dict(invalid_response) + assert from_valid.catch_all == {} + assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all + + def test_undefined_parameters_catch_all_nested_schema(self, boss_json): + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Minion: + name: str + catch_all: CatchAll + + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + catch_all: CatchAll + + boss = Boss.schema().loads(boss_json) + assert {"UNKNOWN_PROPERTY": "value"} == boss.catch_all + assert {"UNKNOWN_PROPERTY": "value"} == boss.minions[0].catch_all + assert {} == boss.minions[1].catch_all + + def test_undefined_parameters_catch_all_schema_dump(self, boss_json): + import json + + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Minion: + name: str + catch_all: CatchAll + + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + catch_all: CatchAll + + boss = Boss.schema().loads(boss_json) + assert json.loads(boss_json) == Boss.schema().dump(boss) + assert "".join(boss_json.replace('\n', '').split()) == "".join( + Boss.schema().dumps(boss).replace('\n', '').split()) + + def test_undefined_parameters_catch_all_schema_roundtrip(self, boss_json): + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Minion: + name: str + catch_all: CatchAll + + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + catch_all: CatchAll + + boss1 = Boss.schema().loads(boss_json) + dumped_s = Boss.schema().dumps(boss1) + boss2 = Boss.schema().loads(dumped_s) + assert boss1 == boss2 + + def test_it_works_from_string(self, invalid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpFromString: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll + + dump = UnknownAPIDumpFromString.from_dict(invalid_response) + assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all + + def test_undefined_parameters_catch_all_init_valid(self, valid_response): + dump = UnknownAPIDump(**valid_response) + assert dump.catch_all == {} + + def test_undefined_parameters_catch_all_init_invalid(self, + invalid_response): + dump = UnknownAPIDump(**invalid_response) + assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all + + def test_undefined_parameters_catch_all_init_args(self): + dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", + "unknown2", undefined="123") + assert dump.endpoint == "some-endpoint" + assert dump.data == {"some-data": "foo"} + assert dump.catch_all == {'_UNKNOWN0': 'unknown1', + '_UNKNOWN1': 'unknown2', + "undefined": "123"} + + def test_undefined_parameters_catch_all_init_args_kwargs_mixed(self): + dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", + "unknown2", catch_all={"bar": "example"}, + undefined="123") + assert dump.endpoint == "some-endpoint" + assert dump.data == {"some-data": "foo"} + assert dump.catch_all == {'_UNKNOWN0': 'unknown1', + '_UNKNOWN1': 'unknown2', + "bar": "example", "undefined": "123"} + + def test_undefined_parameters_catch_all_default_no_undefined(self, + valid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpDefault: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll = None -def test_undefined_parameters_catch_all_default_factory_init_converts_factory(valid_response): - @dataclass_json(undefined="include") - @dataclass() - class UnknownAPIDumpDefault: - endpoint: str - data: Dict[str, Any] - catch_all: CatchAll = field(default_factory=dict) + dump = UnknownAPIDumpDefault.from_dict(valid_response) + assert dump.to_dict() == valid_response - dump = UnknownAPIDumpDefault(**valid_response) - assert dump.catch_all == {} \ No newline at end of file + def test_undefined_parameters_catch_all_default_factory_init_converts_factory( + self, + valid_response): + @dataclass_json(undefined="include") + @dataclass() + class UnknownAPIDumpDefault: + endpoint: str + data: Dict[str, Any] + catch_all: CatchAll = field(default_factory=dict) + + dump = UnknownAPIDumpDefault(**valid_response) + assert dump.catch_all == {} + + +class TestRaiseUndefinedParameters: + + def test_undefined_parameters_raise_invalid(self, invalid_response): + with pytest.raises(UndefinedParameterError): + WellKnownAPIDump.from_dict(invalid_response) + + def test_undefined_parameters_raise_valid(self, valid_response): + assert valid_response == WellKnownAPIDump.from_dict( + valid_response).to_dict() + + def test_undefined_parameters_raise_nested_schema(self, boss_json): + @dataclass_json(undefined=Undefined.RAISE) + @dataclass(frozen=True) + class Minion: + name: str + + @dataclass_json(undefined=Undefined.EXCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + + with pytest.raises(marshmallow.exceptions.ValidationError): + Boss.schema().loads(boss_json) + + def test_undefined_parameters_raise_init(self, invalid_response): + with pytest.raises(TypeError): + WellKnownAPIDump(**invalid_response) + + +class TestIgnoreUndefinedParameters: + + def test_undefined_parameters_ignore(self, valid_response, + invalid_response): + from_valid = DontCareAPIDump.from_dict(valid_response) + from_invalid = DontCareAPIDump.from_dict(invalid_response) + assert from_valid == from_invalid + + def test_undefined_parameters_ignore_to_dict(self, invalid_response, + valid_response): + dump = DontCareAPIDump.from_dict(invalid_response) + dump_dict = dump.to_dict() + assert valid_response == dump_dict + + def test_undefined_parameters_ignore_nested_schema(self, boss_json): + @dataclass_json(undefined=Undefined.EXCLUDE) + @dataclass(frozen=True) + class Minion: + name: str + + @dataclass_json(undefined=Undefined.EXCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + + boss = Boss.schema().loads(boss_json) + assert len(boss.minions) == 2 + assert boss.minions == [Minion(name="evil minion"), + Minion(name="very evil minion")] + + def test_undefined_parameters_ignore_init_args(self): + dump = DontCareAPIDump("some-endpoint", {"some-data": "foo"}, + "unknown1", + "unknown2", undefined="123") + assert dump.endpoint == "some-endpoint" + assert dump.data == {"some-data": "foo"} + + def test_undefined_parameters_ignore_init_invalid(self, invalid_response, + valid_response): + dump_invalid = DontCareAPIDump(**invalid_response) + dump_valid = DontCareAPIDump(**valid_response) + assert dump_valid == dump_invalid + + +class TestMiscellaneousUndefinedParameters: + + def test_undefined_parameters_catch_all_ignore_mix_nested_schema(self, + boss_json): + @dataclass_json(undefined=Undefined.EXCLUDE) + @dataclass(frozen=True) + class Minion: + name: str + + @dataclass_json(undefined=Undefined.INCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + catch_all: CatchAll + + boss = Boss.schema().loads(boss_json) + assert Minion(name="evil minion") == boss.minions[0] + assert Minion(name="very evil minion") == boss.minions[1] + assert {"UNKNOWN_PROPERTY": "value"} == boss.catch_all + + def test_string_only_accepts_valid_actions(self): + with pytest.raises(UndefinedParameterError): + @dataclass_json(undefined="not sure what this is supposed to do") + @dataclass() + class WontWork: + endpoint: str From 9949cb79e31c14f740a3936256714512cf660c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veith=20R=C3=B6thlingsh=C3=B6fer?= Date: Thu, 30 Apr 2020 11:19:29 +0200 Subject: [PATCH 3/4] Rename undefined parameter test cases to be more descriptive --- tests/test_undefined_parameters.py | 120 ++++++++++++++--------------- 1 file changed, 56 insertions(+), 64 deletions(-) diff --git a/tests/test_undefined_parameters.py b/tests/test_undefined_parameters.py index 9f6e1f09..b435b7e9 100644 --- a/tests/test_undefined_parameters.py +++ b/tests/test_undefined_parameters.py @@ -87,22 +87,21 @@ def boss_json(): class TestCatchAllUndefinedParameters: - def test_undefined_parameters_catch_all_invalid_back(self, - invalid_response): + def test_it_dumps_undefined_parameters_back(self, invalid_response): dump = UnknownAPIDump.from_dict(invalid_response) inverse_dict = dump.to_dict() assert inverse_dict == invalid_response - def test_undefined_parameters_catch_all_valid(self, valid_response): + def test_dump_has_no_undefined_parameters_if_not_given(self, + valid_response): dump = UnknownAPIDump.from_dict(valid_response) assert dump.catch_all == {} - def test_undefined_parameters_catch_all_no_field(self, invalid_response): + def test_it_requires_a_catch_all_field(self, invalid_response): with pytest.raises(UndefinedParameterError): UnknownAPIDumpNoCatchAllField.from_dict(invalid_response) - def test_undefined_parameters_catch_all_multiple_fields(self, - invalid_response): + def test_it_requires_exactly_one_catch_all_field(self, invalid_response): @dataclass_json(undefined=Undefined.INCLUDE) @dataclass() class UnknownAPIDumpMultipleCatchAll: @@ -114,8 +113,7 @@ class UnknownAPIDumpMultipleCatchAll: with pytest.raises(UndefinedParameterError): UnknownAPIDumpMultipleCatchAll.from_dict(invalid_response) - def test_undefined_parameters_catch_all_works_with_letter_case(self, - invalid_response_camel_case): + def test_it_works_with_letter_case(self, invalid_response_camel_case): @dataclass_json(undefined=Undefined.INCLUDE, letter_case=LetterCase.CAMEL) @dataclass() @@ -128,24 +126,21 @@ class UnknownAPIDumpCamelCase: assert {"undefinedFieldName": [1, 2, 3]} == dump.catch_all assert invalid_response_camel_case == dump.to_dict() - def test_undefined_parameters_catch_all_raises_if_initialized_with_catch_all_field_name( - self, - valid_response): + def test_catch_all_field_name_cant_be_a_primitive_parameter(self, + valid_response): valid_response["catch_all"] = "some-value" with pytest.raises(UndefinedParameterError): UnknownAPIDump.from_dict(valid_response) - def test_undefined_parameters_catch_all_initialized_with_dict_and_more_unknown( - self, - invalid_response): + def test_catch_all_field_can_be_initialized_with_dict(self, + invalid_response): invalid_response["catch_all"] = {"someValue": "some-stuff"} dump = UnknownAPIDump.from_dict(invalid_response) assert dump.catch_all == {"someValue": "some-stuff", "undefined_field_name": [1, 2, 3]} - def test_undefined_parameters_raises_with_default_argument_and_supplied_catch_all_name( - self, - invalid_response): + def test_it_raises_with_default_argument_and_catch_all_field_name(self, + invalid_response): @dataclass_json(undefined="include") @dataclass() class UnknownAPIDumpDefault: @@ -157,9 +152,8 @@ class UnknownAPIDumpDefault: with pytest.raises(UndefinedParameterError): UnknownAPIDumpDefault.from_dict(invalid_response) - def test_undefined_parameters_doesnt_raise_with_default(self, - valid_response, - invalid_response): + def test_catch_all_field_can_have_default(self, valid_response, + invalid_response): @dataclass_json(undefined="include") @dataclass() class UnknownAPIDumpDefault: @@ -172,9 +166,8 @@ class UnknownAPIDumpDefault: assert from_valid.catch_all is None assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all - def test_undefined_parameters_doesnt_raise_with_default_factory(self, - valid_response, - invalid_response): + def test_catch_all_field_can_have_default_factory(self, valid_response, + invalid_response): @dataclass_json(undefined="include") @dataclass() class UnknownAPIDumpDefault(DataClassJsonMixin): @@ -187,7 +180,7 @@ class UnknownAPIDumpDefault(DataClassJsonMixin): assert from_valid.catch_all == {} assert {"undefined_field_name": [1, 2, 3]} == from_invalid.catch_all - def test_undefined_parameters_catch_all_nested_schema(self, boss_json): + def test_it_works_with_nested_schemata(self, boss_json): @dataclass_json(undefined=Undefined.INCLUDE) @dataclass(frozen=True) class Minion: @@ -205,7 +198,7 @@ class Boss: assert {"UNKNOWN_PROPERTY": "value"} == boss.minions[0].catch_all assert {} == boss.minions[1].catch_all - def test_undefined_parameters_catch_all_schema_dump(self, boss_json): + def test_it_dumps_nested_schemata_correctly(self, boss_json): import json @dataclass_json(undefined=Undefined.INCLUDE) @@ -225,7 +218,7 @@ class Boss: assert "".join(boss_json.replace('\n', '').split()) == "".join( Boss.schema().dumps(boss).replace('\n', '').split()) - def test_undefined_parameters_catch_all_schema_roundtrip(self, boss_json): + def test_it_preserves_nested_schemata_in_roundtrip(self, boss_json): @dataclass_json(undefined=Undefined.INCLUDE) @dataclass(frozen=True) class Minion: @@ -254,16 +247,15 @@ class UnknownAPIDumpFromString: dump = UnknownAPIDumpFromString.from_dict(invalid_response) assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all - def test_undefined_parameters_catch_all_init_valid(self, valid_response): + def test_it_works_with_valid_dict_expansion(self, valid_response): dump = UnknownAPIDump(**valid_response) assert dump.catch_all == {} - def test_undefined_parameters_catch_all_init_invalid(self, - invalid_response): + def test_it_works_with_invalid_dict_expansion(self, invalid_response): dump = UnknownAPIDump(**invalid_response) assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all - def test_undefined_parameters_catch_all_init_args(self): + def test_it_creates_dummy_keys_for_init_args(self): dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", undefined="123") assert dump.endpoint == "some-endpoint" @@ -272,7 +264,7 @@ def test_undefined_parameters_catch_all_init_args(self): '_UNKNOWN1': 'unknown2', "undefined": "123"} - def test_undefined_parameters_catch_all_init_args_kwargs_mixed(self): + def test_it_creates_dummy_keys_for_init_args_kwargs_mix(self): dump = UnknownAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", catch_all={"bar": "example"}, undefined="123") @@ -282,8 +274,9 @@ def test_undefined_parameters_catch_all_init_args_kwargs_mixed(self): '_UNKNOWN1': 'unknown2', "bar": "example", "undefined": "123"} - def test_undefined_parameters_catch_all_default_no_undefined(self, - valid_response): + def test_it_doesnt_dump_the_default_value_without_undefined_parameters( + self, + valid_response): @dataclass_json(undefined="include") @dataclass() class UnknownAPIDumpDefault: @@ -294,9 +287,8 @@ class UnknownAPIDumpDefault: dump = UnknownAPIDumpDefault.from_dict(valid_response) assert dump.to_dict() == valid_response - def test_undefined_parameters_catch_all_default_factory_init_converts_factory( - self, - valid_response): + def test_it_dumps_default_factory_without_undefined_parameters(self, + valid_response): @dataclass_json(undefined="include") @dataclass() class UnknownAPIDumpDefault: @@ -310,48 +302,34 @@ class UnknownAPIDumpDefault: class TestRaiseUndefinedParameters: - def test_undefined_parameters_raise_invalid(self, invalid_response): + def test_it_raises_with_undefined_parameters(self, invalid_response): with pytest.raises(UndefinedParameterError): WellKnownAPIDump.from_dict(invalid_response) - def test_undefined_parameters_raise_valid(self, valid_response): + def test_it_doesnt_raise_with_known_parameters(self, valid_response): assert valid_response == WellKnownAPIDump.from_dict( valid_response).to_dict() - def test_undefined_parameters_raise_nested_schema(self, boss_json): - @dataclass_json(undefined=Undefined.RAISE) - @dataclass(frozen=True) - class Minion: - name: str - - @dataclass_json(undefined=Undefined.EXCLUDE) - @dataclass(frozen=True) - class Boss: - minions: List[Minion] - - with pytest.raises(marshmallow.exceptions.ValidationError): - Boss.schema().loads(boss_json) - - def test_undefined_parameters_raise_init(self, invalid_response): + def test_it_has_python_semantics_in_init(self, invalid_response): with pytest.raises(TypeError): WellKnownAPIDump(**invalid_response) class TestIgnoreUndefinedParameters: - def test_undefined_parameters_ignore(self, valid_response, - invalid_response): + def test_it_ignores_undefined_parameters(self, valid_response, + invalid_response): from_valid = DontCareAPIDump.from_dict(valid_response) from_invalid = DontCareAPIDump.from_dict(invalid_response) assert from_valid == from_invalid - def test_undefined_parameters_ignore_to_dict(self, invalid_response, - valid_response): + def test_it_does_not_dump_undefined_parameters(self, invalid_response, + valid_response): dump = DontCareAPIDump.from_dict(invalid_response) dump_dict = dump.to_dict() assert valid_response == dump_dict - def test_undefined_parameters_ignore_nested_schema(self, boss_json): + def test_it_ignores_nested_schemata(self, boss_json): @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass(frozen=True) class Minion: @@ -367,15 +345,15 @@ class Boss: assert boss.minions == [Minion(name="evil minion"), Minion(name="very evil minion")] - def test_undefined_parameters_ignore_init_args(self): + def test_it_ignores_undefined_init_args(self): dump = DontCareAPIDump("some-endpoint", {"some-data": "foo"}, "unknown1", "unknown2", undefined="123") assert dump.endpoint == "some-endpoint" assert dump.data == {"some-data": "foo"} - def test_undefined_parameters_ignore_init_invalid(self, invalid_response, - valid_response): + def test_it_ignores_undefined_init_kwargs(self, invalid_response, + valid_response): dump_invalid = DontCareAPIDump(**invalid_response) dump_valid = DontCareAPIDump(**valid_response) assert dump_valid == dump_invalid @@ -383,8 +361,22 @@ def test_undefined_parameters_ignore_init_invalid(self, invalid_response, class TestMiscellaneousUndefinedParameters: - def test_undefined_parameters_catch_all_ignore_mix_nested_schema(self, - boss_json): + def test_it_raises_with_nested_schemata(self, boss_json): + @dataclass_json(undefined=Undefined.RAISE) + @dataclass(frozen=True) + class Minion: + name: str + + @dataclass_json(undefined=Undefined.EXCLUDE) + @dataclass(frozen=True) + class Boss: + minions: List[Minion] + + with pytest.raises(marshmallow.exceptions.ValidationError): + Boss.schema().loads(boss_json) + + def test_it_works_with_catch_all_ignore_mix_nested_schemata(self, + boss_json): @dataclass_json(undefined=Undefined.EXCLUDE) @dataclass(frozen=True) class Minion: @@ -401,7 +393,7 @@ class Boss: assert Minion(name="very evil minion") == boss.minions[1] assert {"UNKNOWN_PROPERTY": "value"} == boss.catch_all - def test_string_only_accepts_valid_actions(self): + def test_it_only_acceps_valid_actions_as_string(self): with pytest.raises(UndefinedParameterError): @dataclass_json(undefined="not sure what this is supposed to do") @dataclass() From 22a9ef0486eb61d0e3600596bddf8adc3608c302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veith=20R=C3=B6thlingsh=C3=B6fer?= Date: Thu, 30 Apr 2020 12:10:08 +0200 Subject: [PATCH 4/4] Implement Undefined.WARN --- README.md | 25 ++++++++++-- dataclasses_json/api.py | 3 ++ dataclasses_json/undefined.py | 60 ++++++++++++++++++++++++++++ tests/test_undefined_parameters.py | 64 ++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd7dc8bf..ca56b11c 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,24 @@ dump_dict = {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}, "u dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} ``` -3. You can save them in a catch-all field and do whatever needs to be done later. Simply set the `undefined` +3. You can issue a `RuntimeWarning` for undefined parameters by setting the `undefined` keyword to `Undefined.WARN` + (`WARN` as a case-insensitive string works as well). Note that you will not be able to retrieve them using `to_dict`: + + ```python + from dataclasses_json import Undefined + + @dataclass_json(undefined=Undefined.WARN) + @dataclass() + class WarnAPIDump: + endpoint: str + data: Dict[str, Any] + + dump = WarnAPIDump.from_dict(dump_dict) # WarnAPIDump(endpoint='some_api_endpoint', data={'foo': 1, 'bar': '2'}) + # RuntimeWarning("Received undefined initialization arguments (, {'undefined_field_name': [1, 2, 3]})") + dump.to_dict() # {"endpoint": "some_api_endpoint", "data": {"foo": 1, "bar": "2"}} + ``` + +4. You can save them in a catch-all field and do whatever needs to be done later. Simply set the `undefined` keyword to `Undefined.INCLUDE` (`'INCLUDE'` as a case-insensitive string works as well) and define a field of type `CatchAll` where all unknown values will end up. This simply represents a dictionary that can hold anything. @@ -398,11 +415,13 @@ of type `CatchAll` where all unknown values will end up. - When specifying a default (or a default factory) for the the `CatchAll`-field, e.g. `unknown_things: CatchAll = None`, the default value will be used instead of an empty dict if there are no undefined parameters. - Calling __init__ with non-keyword arguments resolves the arguments to the defined fields and writes everything else into the catch-all field. -4. All 3 options work as well using `schema().loads` and `schema().dumps`, as long as you don't overwrite it by specifying `schema(unknown=)`. +4. The `INCLUDE, EXCLUDE, RAISE` options work as well using `schema().loads` and `schema().dumps`, as long as you don't overwrite it by specifying `schema(unknown=)`. marshmallow uses the same 3 keywords ['include', 'exclude', 'raise'](https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields). +Marshmallow does not have an equivalent of `WARN`, so when using `schema().loads` we fall back to ignoring undefined parameters. -5. All 3 operations work as well using `__init__`, e.g. `UnknownAPIDump(**dump_dict)` will **not** raise a `TypeError`, but write all unknown values to the field tagged as `CatchAll`. +5. All 4 operations work as well using `__init__`, e.g. `UnknownAPIDump(**dump_dict)` will **not** raise a `TypeError`, but write all unknown values to the field tagged as `CatchAll`. Classes tagged with `EXCLUDE` will also simply ignore unknown parameters. Note that classes tagged as `RAISE` still raise a `TypeError`, and **not** a `UndefinedParameterError` if supplied with unknown keywords. + Classes tagged with `WARN, IGNORE` will resolve keyword arguments before resolving arguments. ### Explanation diff --git a/dataclasses_json/api.py b/dataclasses_json/api.py index 7f36a916..1dbd0d35 100644 --- a/dataclasses_json/api.py +++ b/dataclasses_json/api.py @@ -104,6 +104,9 @@ def schema(cls: Type[A], if unknown is None: undefined_parameter_action = _undefined_parameter_action_safe(cls) if undefined_parameter_action is not None: + if undefined_parameter_action == Undefined.WARN: + # mm has no warn implementation, so we resolve to ignore + undefined_parameter_action = Undefined.EXCLUDE # We can just make use of the same-named mm keywords unknown = undefined_parameter_action.name.lower() diff --git a/dataclasses_json/undefined.py b/dataclasses_json/undefined.py index 96ac284f..031384a9 100644 --- a/dataclasses_json/undefined.py +++ b/dataclasses_json/undefined.py @@ -5,6 +5,7 @@ from dataclasses import Field, fields from typing import Any, Callable, Dict, Optional, Tuple from enum import Enum +import warnings from marshmallow import ValidationError @@ -73,6 +74,64 @@ def handle_from_dict(cls, kvs: Dict) -> Dict[str, Any]: return known +class _WarnUndefinedParameters(_UndefinedParameterAction): + """ + This action issues a RuntimeWarning if it encounters an undefined + parameter during initialization. + The class is then initialized with the known parameters. + """ + + @staticmethod + def handle_from_dict(cls, kvs: Dict[Any, Any]) -> Dict[str, Any]: + known, unknown = \ + _UndefinedParameterAction._separate_defined_undefined_kvs( + cls=cls, kvs=kvs) + if len(unknown) > 0: + msg = f"Received undefined initialization arguments {unknown}" + warnings.warn(msg, category=RuntimeWarning) + return known + + @staticmethod + def create_init(obj) -> Callable: + original_init = obj.__init__ + init_signature = inspect.signature(original_init) + + @functools.wraps(obj.__init__) + def _warn_init(self, *args, **kwargs): + known_kwargs, unknown_kwargs = \ + _CatchAllUndefinedParameters._separate_defined_undefined_kvs( + obj, kwargs) + num_params_takeable = len( + init_signature.parameters) - 1 # don't count self + num_args_takeable = num_params_takeable - len(known_kwargs) + + known_args = args[:num_args_takeable] + unknown_args = args[num_args_takeable:] + bound_parameters = init_signature.bind_partial(self, *known_args, + **known_kwargs) + bound_parameters.apply_defaults() + + arguments = bound_parameters.arguments + arguments.pop("self", None) + final_parameters = \ + _WarnUndefinedParameters.handle_from_dict(obj, arguments) + + if unknown_args or unknown_kwargs: + args_message = "" + if unknown_args: + args_message = f"{unknown_args}" + kwargs_message = "" + if unknown_kwargs: + kwargs_message = f"{unknown_kwargs}" + message = f"Received undefined initialization arguments" \ + f" ({args_message}, {kwargs_message})" + warnings.warn(message=message, category=RuntimeWarning) + + original_init(self, **final_parameters) + + return _warn_init + + CatchAll = Optional[CatchAllVar] @@ -264,6 +323,7 @@ class Undefined(Enum): INCLUDE = _CatchAllUndefinedParameters RAISE = _RaiseUndefinedParameters EXCLUDE = _IgnoreUndefinedParameters + WARN = _WarnUndefinedParameters class UndefinedParameterError(ValidationError): diff --git a/tests/test_undefined_parameters.py b/tests/test_undefined_parameters.py index b435b7e9..9f52806b 100644 --- a/tests/test_undefined_parameters.py +++ b/tests/test_undefined_parameters.py @@ -1,3 +1,4 @@ +import warnings from dataclasses import dataclass, field from typing import Any, Dict, List @@ -315,6 +316,69 @@ def test_it_has_python_semantics_in_init(self, invalid_response): WellKnownAPIDump(**invalid_response) +class TestWarnUndefinedParameters: + + def test_it_raises_with_undefined_parameters(self, invalid_response): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + WarnApiDump.from_dict(invalid_response) + + assert len(w) == 1 + assert issubclass(w[-1].category, RuntimeWarning) + expected_message = "Received undefined initialization arguments " \ + "{'undefined_field_name': [1, 2, 3]}" + assert expected_message == w[-1].message.args[0] + + def test_it_doesnt_raise_with_known_parameters(self, valid_response): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + roundtrip = WarnApiDump.from_dict(valid_response).to_dict() + + assert valid_response == roundtrip + assert len(w) == 0 + + def test_it_warns_in_init_kwargs(self, invalid_response): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + WarnApiDump(**invalid_response) + + assert len(w) == 1 + assert issubclass(w[-1].category, RuntimeWarning) + expected_message = "Received undefined initialization arguments " \ + "(, {'undefined_field_name': [1, 2, 3]})" + assert w[-1].message.args[0] == expected_message + + def test_it_warns_init_args_kwargs_mixed_preferring_kwargs(self, + invalid_response): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + dump = WarnApiDump("some-arg", **invalid_response) + + assert dump.endpoint == invalid_response["endpoint"] + assert dump.data == invalid_response["data"] + assert len(w) == 1 + assert issubclass(w[-1].category, RuntimeWarning) + expected_message = "Received undefined initialization arguments " \ + "(('some-arg',), {'undefined_field_name': [1, 2, 3]})" + assert w[-1].message.args[0] == expected_message + + def test_it_ignores_when_using_schema(self, invalid_response): + import json + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + dump = WarnApiDump.schema().loads(json.dumps(invalid_response)) + + assert len(w) == 0 + assert dump.endpoint == invalid_response["endpoint"] + assert dump.data == invalid_response["data"] + + class TestIgnoreUndefinedParameters: def test_it_ignores_undefined_parameters(self, valid_response,