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 58740dcb..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
@@ -5,7 +6,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 +41,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 +86,380 @@ 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
-
+class TestCatchAllUndefinedParameters:
-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_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_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_ignore_nested_schema(boss_json):
- @dataclass_json(undefined=Undefined.EXCLUDE)
- @dataclass(frozen=True)
- class Minion:
- name: str
+ def test_it_requires_a_catch_all_field(self, invalid_response):
+ with pytest.raises(UndefinedParameterError):
+ UnknownAPIDumpNoCatchAllField.from_dict(invalid_response)
- @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
+ def test_it_requires_exactly_one_catch_all_field(self, invalid_response):
+ @dataclass_json(undefined=Undefined.INCLUDE)
+ @dataclass()
+ class UnknownAPIDumpMultipleCatchAll:
+ endpoint: str
+ data: Dict[str, Any]
+ catch_all: CatchAll
+ catch_all2: CatchAll
- @dataclass_json(undefined=Undefined.INCLUDE)
- @dataclass(frozen=True)
- class Minion:
- name: str
- catch_all: CatchAll
+ with pytest.raises(UndefinedParameterError):
+ UnknownAPIDumpMultipleCatchAll.from_dict(invalid_response)
- @dataclass_json(undefined=Undefined.INCLUDE)
- @dataclass(frozen=True)
- class Boss:
- minions: List[Minion]
- catch_all: CatchAll
+ def test_it_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_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_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_it_raises_with_default_argument_and_catch_all_field_name(self,
+ invalid_response):
+ @dataclass_json(undefined="include")
+ @dataclass()
+ class UnknownAPIDumpDefault:
+ endpoint: str
+ data: Dict[str, Any]
+ catch_all: CatchAll = None
- 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())
+ invalid_response["catch_all"] = "this should not happen"
+ with pytest.raises(UndefinedParameterError):
+ UnknownAPIDumpDefault.from_dict(invalid_response)
+ def test_catch_all_field_can_have_default(self, valid_response,
+ invalid_response):
+ @dataclass_json(undefined="include")
+ @dataclass()
+ class UnknownAPIDumpDefault:
+ endpoint: str
+ data: Dict[str, Any]
+ catch_all: CatchAll = None
-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
+ 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
- @dataclass_json(undefined=Undefined.INCLUDE)
- @dataclass(frozen=True)
- class Boss:
- minions: List[Minion]
- catch_all: CatchAll
+ def test_catch_all_field_can_have_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_it_works_with_nested_schemata(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_it_dumps_nested_schemata_correctly(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_it_preserves_nested_schemata_in_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_it_works_with_valid_dict_expansion(self, valid_response):
+ dump = UnknownAPIDump(**valid_response)
+ assert dump.catch_all == {}
+
+ 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_it_creates_dummy_keys_for_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_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")
+ 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_it_doesnt_dump_the_default_value_without_undefined_parameters(
+ self,
+ valid_response):
+ @dataclass_json(undefined="include")
+ @dataclass()
+ class UnknownAPIDumpDefault:
+ endpoint: str
+ data: Dict[str, Any]
+ catch_all: CatchAll = None
- boss1 = Boss.schema().loads(boss_json)
- dumped_s = Boss.schema().dumps(boss1)
- boss2 = Boss.schema().loads(dumped_s)
- assert boss1 == boss2
+ dump = UnknownAPIDumpDefault.from_dict(valid_response)
+ assert dump.to_dict() == valid_response
-
-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_it_dumps_default_factory_without_undefined_parameters(self,
+ valid_response):
+ @dataclass_json(undefined="include")
@dataclass()
- class WontWork:
+ class UnknownAPIDumpDefault:
endpoint: str
+ data: Dict[str, Any]
+ catch_all: CatchAll = field(default_factory=dict)
+
+ dump = UnknownAPIDumpDefault(**valid_response)
+ assert dump.catch_all == {}
-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
+class TestRaiseUndefinedParameters:
- invalid_response["catch_all"] = "this should not happen"
- with pytest.raises(UndefinedParameterError):
- UnknownAPIDumpDefault.from_dict(invalid_response)
+ def test_it_raises_with_undefined_parameters(self, invalid_response):
+ with pytest.raises(UndefinedParameterError):
+ WellKnownAPIDump.from_dict(invalid_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_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
+ def test_it_has_python_semantics_in_init(self, invalid_response):
+ with pytest.raises(TypeError):
+ WellKnownAPIDump(**invalid_response)
- 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
+class TestWarnUndefinedParameters:
-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)
+ def test_it_raises_with_undefined_parameters(self, invalid_response):
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
- 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
+ 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_undefined_parameters_catch_all_init_valid(valid_response):
- dump = UnknownAPIDump(**valid_response)
- assert dump.catch_all == {}
+ 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()
-def test_undefined_parameters_catch_all_init_invalid(invalid_response):
- dump = UnknownAPIDump(**invalid_response)
- assert {"undefined_field_name": [1, 2, 3]} == dump.catch_all
+ 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")
-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"}
+ 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_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_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)
-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"}
+ 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
-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
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ dump = WarnApiDump.schema().loads(json.dumps(invalid_response))
-def test_undefined_parameters_raise_init(invalid_response):
- with pytest.raises(TypeError):
- WellKnownAPIDump(**invalid_response)
+ assert len(w) == 0
+ assert dump.endpoint == invalid_response["endpoint"]
+ assert dump.data == invalid_response["data"]
-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
+class TestIgnoreUndefinedParameters:
- dump = UnknownAPIDumpDefault.from_dict(valid_response)
- assert dump.to_dict() == valid_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_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_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)
+ def test_it_ignores_nested_schemata(self, boss_json):
+ @dataclass_json(undefined=Undefined.EXCLUDE)
+ @dataclass(frozen=True)
+ class Minion:
+ name: str
- dump = UnknownAPIDumpDefault(**valid_response)
- assert dump.catch_all == {}
\ No newline at end of file
+ @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_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_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
+
+
+class TestMiscellaneousUndefinedParameters:
+
+ 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:
+ 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_only_acceps_valid_actions_as_string(self):
+ with pytest.raises(UndefinedParameterError):
+ @dataclass_json(undefined="not sure what this is supposed to do")
+ @dataclass()
+ class WontWork:
+ endpoint: str