Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

New undefined parameter action: Warn #212

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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=<a marshmallow value>)`.
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=<a marshmallow value>)`.
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

Expand Down
3 changes: 3 additions & 0 deletions dataclasses_json/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
60 changes: 60 additions & 0 deletions dataclasses_json/undefined.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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]


Expand Down Expand Up @@ -264,6 +323,7 @@ class Undefined(Enum):
INCLUDE = _CatchAllUndefinedParameters
RAISE = _RaiseUndefinedParameters
EXCLUDE = _IgnoreUndefinedParameters
WARN = _WarnUndefinedParameters


class UndefinedParameterError(ValidationError):
Expand Down
Loading