-
-
Notifications
You must be signed in to change notification settings - Fork 118
structure raises a KeyError when optional attribute is missing #249
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
Comments
Hello! You'll need to add a default value yourself to get this behavior. So just change your class to be @define
class Dog:
name: str
nickname: Optional[str] = None cattrs generally avoids making these calls for you. Maybe you'd like the field to default to an empty string instead? So you're expected to provide the default value. |
@Tinche ahh, thanks so much for the quick reply! I thought I must have been missing something simple 🤦 . Should it maybe be added to the example in the README? @define
class Dog:
cuteness: int
chip: Optional[DogMicrochip] |
Sure! Want to send a quick PR? :) |
Hi! I encountered the same problem, but using default Each reordering is a breaking change which is a no-go in my case. This also can't be solved by a custom I managed to solve it by modifying
This works for me, because I always want to set missing Optional fields to None, but I am not sure if this is a generally acceptable solution. What is your opinion? How would you approach the "can't use BTW: Thanks for this great library :-) |
Hello!
But attrs classes and dataclasses must have fields with defaults after the fields with no defaults anyway, right?
You're welcome, thanks for using ;) |
My class has no defaults and looks like this: @frozen
class MyClass:
a_float: float
maybe_int: Optional[int]
a_string: str I might receive data (without If I wanted to use @frozen
class MyClass:
a_float: float
a_string: str
maybe_int: Optional[int] = None which breaks |
Ah I see, alright. And you'd like to do this just for Optionals? Gonna think about this a little. |
Maybe as well for Here is the behavior I am currently able to get by using the adjustment provided above. from typing import Any, Optional, Union
import pytest
from attr import frozen, has
from cattr import GenConverter
from cattrs.errors import ClassValidationError
CONVERTER = GenConverter()
def custom_optional_hook_factory(cls):
# Custom make_dict_structure_fn which calls .get() instead of [] when needed
return make_dict_structure_fn_optional(
cls,
CONVERTER,
)
CONVERTER.register_structure_hook_factory(has, custom_optional_hook_factory)
@frozen
class WithOptionals:
required_field: bool
un: Union[bool, str]
un_opt: Union[bool, str, Optional[int]]
a: Optional[int]
b: Optional[float] = None
c: Optional[str] = "custom default"
def test_structure_with_optionals() -> None:
# Unions must have custom hooks
CONVERTER.register_structure_hook(
Union[bool, str], lambda v, _: v if isinstance(v, bool) else str(v)
)
def structure_union_bool_str_optint(v: Any, _: Any) -> Union[bool, str, Optional[int]]:
if isinstance(v, bool):
return v
if isinstance(v, int):
return v
if v is None:
return None
return str(v)
CONVERTER.register_structure_hook(
Union[bool, str, Optional[int]], structure_union_bool_str_optint
)
# required_field is not present and is not Optional
with pytest.raises(ClassValidationError):
CONVERTER.structure({}, WithOptionals)
# required_field is present, but un field is not present and is not Optional
with pytest.raises(ClassValidationError):
CONVERTER.structure({"required_field": True}, WithOptionals)
assert CONVERTER.structure(
{"required_field": True, "un": "hey"}, WithOptionals
) == WithOptionals(
required_field=True,
un="hey",
un_opt=None, # not present in dict and is implicitly set to None
a=None, # not present in dict and is implicitly set to None
b=None, # not present in dict and has default None
c="custom default", # not present in dict and has custom default
)
assert CONVERTER.structure(
{"required_field": True, "b": 0.1, "un": False}, WithOptionals
) == WithOptionals(
required_field=True,
un=False,
un_opt=None,
a=None,
b=0.1, # present in dict
c="custom default",
)
assert CONVERTER.structure(
{"required_field": True, "a": 123, "b": 0.1, "un": True}, WithOptionals
) == WithOptionals(
required_field=True,
un=True,
un_opt=None,
a=123, # present in dict
b=0.1, # present in dict
c="custom default",
)
assert CONVERTER.structure(
{
"required_field": True,
"a": 123,
"b": 0.1,
"c": "hey",
"un": "test",
"un_opt": "i am here",
},
WithOptionals,
) == WithOptionals(
required_field=True,
un="test",
un_opt="i am here",
a=123, # present in dict
b=0.1, # present in dict
c="hey", # present in dict
)
Thanks :) |
Description
Passing a dictionary to
structure
that has missing entries forOptional
values results in an error. Instead I would expect the instance to get built successfully with the optional property unset.What I Did
Example code:
The result:
The text was updated successfully, but these errors were encountered: