Comments (5)
It's intended behavior, and I can explain why.
Converter.register_unstructure_hook
uses a functools.singledispatch
under the hood, and so the hooks registered using it follow singledispatch semantics. These loosely map to how the isinstance
and issubclass
built-ins work.
issubclass(GenericContainer[int], GenericContainer)
doesn't return True (in fact it throws an exception, but cattrs can ignore this).
This means your hook won't trigger in the commented out case.
The solution is to switch to register_unstructure_hook_func
. These are significantly more powerful and can handle anything using a predicate function.
Like this:
from typing import Any, Generic, TypeVar, get_origin
from attrs import define, field
import cattrs
converter = cattrs.Converter()
T = TypeVar("T")
@define
class GenericContainer(Generic[T]):
instance: T = field()
@define
class UsesContainer:
container: GenericContainer[int] = field()
def container_unstructure_hook(obj):
fun = cattrs.gen.make_dict_unstructure_fn(GenericContainer, converter)
return {"added_field": "abc", **fun(obj)}
def is_generic_container(type: Any) -> bool:
return type is GenericContainer or get_origin(type) is GenericContainer
converter.register_unstructure_hook_func(
is_generic_container, container_unstructure_hook
)
uses_component_factory = UsesContainer(GenericContainer(0))
print(converter.unstructure(uses_component_factory))
You might ask yourself why cattrs doesn't do this automatically, and the answer is I really dislike libraries that guess a lot on behalf of the user. I think in the long term the little added magic causes more bad surprises than good, and I prefer to err on the side of simplicity rather than ease.
You can go a step further and use a hook factory instead. This will be much faster. Like this:
from typing import Any, Generic, TypeVar, get_origin
from attrs import define, field
import cattrs
converter = cattrs.Converter()
T = TypeVar("T")
@define
class GenericContainer(Generic[T]):
instance: T = field()
@define
class UsesContainer:
container: GenericContainer[int] = field()
def container_unstructure_hook_factory(obj):
fun = cattrs.gen.make_dict_unstructure_fn(GenericContainer, converter)
def customized(obj):
return {"added_field": "abc", **fun(obj)}
return customized
def is_generic_container(type: Any) -> bool:
return type is GenericContainer or get_origin(type) is GenericContainer
converter.register_unstructure_hook_factory(
is_generic_container, container_unstructure_hook_factory
)
uses_component_factory = UsesContainer(GenericContainer(0))
print(converter.unstructure(uses_component_factory))
Let me know if you have any more question, but I'll close this for now to keep the number of issues manageable.
from cattrs.
Hi @Tinche, perfect, thanks! Makes absolutely sense. I was aware of singledispatch, but simply did not anticipate that issubclass(GenericContainer[int], GenericContainer)
would return False
!
That part of the code now works without problems, but the real context is a bit more involved, as there is an additional base class involved. The problem that I now have is that the internal instance of MyAttrsClass
is not being unstructured:
from typing import Any, Generic, TypeVar, get_origin
import cattrs
from attrs import define, field
converter = cattrs.Converter()
T = TypeVar("T")
@define
class MyAttrsClass:
number: int = field()
@define
class BaseGenericContainer(Generic[T]):
pass
@define
class GenericContainer(BaseGenericContainer[T]):
instance: T = field()
@define
class UsesContainer:
container: GenericContainer[MyAttrsClass] = field()
def container_unstructure_hook(obj):
fun = cattrs.gen.make_dict_unstructure_fn(GenericContainer, converter)
return {"added_field": "abc", **fun(obj)}
def is_generic_container(type: Any) -> bool:
return type is GenericContainer or get_origin(type) is GenericContainer
converter.register_unstructure_hook_func(
is_generic_container, container_unstructure_hook
)
uses_container = UsesContainer(GenericContainer(MyAttrsClass(0)))
print(converter.unstructure(uses_container))
This gives
{'container': {'added_field': 'abc', 'instance': MyAttrsClass(number=0)}}
I guess I somehow need to specify the specific type of GenericContainer
during unstructuring?
from cattrs.
Quick follow-up: I think I got it working by extracting the original hook first + changing the base classes of GenericContainer
:
from typing import Any, Generic, TypeVar, get_origin
import cattrs
from attrs import define, field
converter = cattrs.Converter()
T = TypeVar("T")
@define
class MyAttrsClass:
number: int = field()
@define
class BaseGenericContainer(Generic[T]):
pass
@define
class GenericContainer(BaseGenericContainer, Generic[T]):
instance: T = field()
@define
class UsesContainer:
container: GenericContainer[MyAttrsClass] = field()
hook = converter.get_unstructure_hook(GenericContainer)
def container_unstructure_hook(obj):
return {"added_field": "abc", **hook(obj)}
def is_generic_container(type: Any) -> bool:
return type is GenericContainer or get_origin(type) is GenericContainer
converter.register_unstructure_hook_func(
is_generic_container, container_unstructure_hook
)
uses_container = UsesContainer(GenericContainer(MyAttrsClass(0)))
print(converter.unstructure(uses_container))
@Tinche: in case you find the time, I'd still highly appreciate a quick "yes, this is it" to confirm this is the intended way. Also, to be honest, I still don't quite understand why I need to write
class GenericContainer(BaseGenericContainer, Generic[T])
instead of simply
class GenericContainer(BaseGenericContainer[T]):
from cattrs.
Referencing back to your example in #537 (comment),
the issue there is you're calling cattrs.gen.make_dict_unstructure_fn(GenericContainer, converter)
in all cases. Since GenericContainer
is a generic class, this is equivalent to GenericContainer[Any]
, and cattrs handles Any
by just letting it through. A hook for GenericContainer[int]
will be different from GenericContainer[MyAttrsClass]
, and we need to take that into account somehow.
The best way is to use hook factories. Hook factories are just one more level - instead of registering a hook with a converter, we register a function that receives a type and returns a hook. It also has the benefit of being much faster since make_dict_unstructure_fn
is called once per class, not once per unstructure.
Here's your code with minimal modifications:
from typing import Any, Generic, TypeVar, get_origin
from attrs import define, field
import cattrs
converter = cattrs.Converter()
T = TypeVar("T")
@define
class MyAttrsClass:
number: int = field()
@define
class BaseGenericContainer(Generic[T]):
pass
@define
class GenericContainer(BaseGenericContainer[T]):
instance: T = field()
@define
class UsesContainer:
container: GenericContainer[MyAttrsClass] = field()
def container_unstructure_hook_fact(type):
fun = cattrs.gen.make_dict_unstructure_fn(type, converter)
def hook(obj) -> dict:
return {"added_field": "abc", **fun(obj)}
return hook
def is_generic_container(type: Any) -> bool:
return type is GenericContainer or get_origin(type) is GenericContainer
converter.register_unstructure_hook_factory(
is_generic_container, container_unstructure_hook_fact
)
uses_container = UsesContainer(GenericContainer(MyAttrsClass(0)))
print(converter.unstructure(uses_container))
from cattrs.
Seems like hook factories are pretty much always the answer 😄 But yes, makes perfect sense! Thanks so much for diggin' through the code example!! 👏🏼
from cattrs.
Related Issues (20)
- Inheriting overrides HOT 3
- 23.2.3: test suite fails with pytest 8.2.1 HOT 5
- Register multiple hooks for a class HOT 3
- Nested class structure HOT 4
- Derived class disambiguating fails, but only sometimes. HOT 4
- Fields with init=False Don't Get Serialized HOT 3
- Calling `include_subclasses()` prevents later structure hooks from working HOT 1
- Python 3.13.0b2: 4 tests failuires HOT 7
- base64 pre-config option HOT 1
- Still not able to structure using an alias HOT 3
- Preconfigured union for a "plain" python data structures
- immutable Mapping hooks HOT 4
- TypedDict unstructuring skips value unions HOT 4
- Hookable attrs unstructuring HOT 4
- Unsupported type: typing.Union[int, typing.List[int], str]. Register a structure hook for it. HOT 2
- Hooks for fields of attrs classes are being ignored when defining hook for class itself HOT 2
- Hook factories for built in types HOT 1
- `make_unstructure_dict_unstructure_fn` does not honor `use_class_methods`
- [`help`] Understanding structuring hooks HOT 2
- Using `register_unstructure_hook` to perform attributes renaming HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from cattrs.