Comments (3)
So I gave this a little bit of thought. I don't think cattrs has anything that can particularly help you here, but I don't think we really need much from cattrs anyway.
Here's an approach I played around with. First, we can register a hook factory for all subclasses of FruitBasket
.
converter.register_unstructure_hook_factory(
lambda t: issubclass(t, FruitBasket),
lambda t: make_dict_unstructure_fn(
t, converter, apples=cattrs.override(rename="bananas")
),
)
Now, FruitBasket
and all subclasses (including MixedBasket) will get hooks that rename apples to bananas.
As for also customizing MixedBasket. I refactored a little to this:
BASE_OVERRIDES = {"apples": cattrs.override(rename="bananas")}
converter = cattrs.Converter()
converter.register_unstructure_hook_factory(
lambda t: issubclass(t, FruitBasket),
lambda t: make_dict_unstructure_fn(t, converter, **BASE_OVERRIDES),
)
converter.register_unstructure_hook(
MixedBasket,
make_dict_unstructure_fn(
MixedBasket,
converter,
oranges=cattrs.override(rename="peaches"),
**BASE_OVERRIDES,
),
)
Now every subclass of FruitBasket gets bananas, and MixedBaskets in particular also get peaches. We just lean on how dictionaries compose in Python. There's a way to do this using function composition too but it seems more complex for little gain.
Let me know what you think.
from cattrs.
Hi @Tinche, appreciate very much that you took the time to think about the problem 🥇
The solution you propose works, of course, and is pretty much exactly how I would have set it up without having access to better alternatives. However, I must admit that I don't consider it very elegant: The problem I see here is that it basically requires to mirror the entire class hierarchy to a second hierarchy of nested dictionaries, all of which needs to be manually maintained. For the very simple scenario above, we only need BASE_OVERRIDES
, but now consider the my real use case, where FruitBasket
not only has one but several subclasses, each of which also has subclasses etc. To consistently keep track of all overrides when going down the class hierarchy, you need an additional dict for each class you pass along the way. That doesn't seem like a neat solution to the problem... Ideally, I'd rather want to somehow make use of the overrides that have already been registered with converter
when calling make_dict_unstructure_fn
or somehow access them in another way. Do you see the issue?
from cattrs.
I've been giving this more thought since it's an interesting problem. I have a proposal ready, but it requires cattrs 24.1, which is still unreleased.
In cattrs 24.1, the hooks generated by cattrs.gen
contain their overrides on an overrides
attribute on the actual function. So when you do this:
from cattrs.gen import make_dict_unstructure_fn
hook = make_dict_unstructure_fn(..., a=override(rename=...))
the overrides can be fetched:
>>> print(hook.overrides)
{"a": AttributeOverride(...)}
We can leverage this.
First we need a base unstructure hook for FruitBasket
, and only FruitBasket
(no subclasses). We apply the apples -> bananas rename here.
conv = cattrs.Converter()
conv.register_unstructure_hook_func(
lambda t: t is FruitBasket,
make_dict_unstructure_fn(
FruitBasket, conv, apples=cattrs.override(rename="bananas")
),
)
Simple so far. But subclasses (like MixedBasket
) won't inherit the overrides. So now we define a hook factory for all subclasses of FruitBasket
(but not FruitBasket
exactly) that looks like this:
@conv.register_unstructure_hook_factory(
lambda type: type is not FruitBasket and issubclass(type, FruitBasket)
)
def fruit_baskets_hook_factory(cl: type[FruitBasket], converter: cattrs.Converter):
parent = cl.mro()[1]
parent_hook = converter.get_unstructure_hook(parent)
overrides = parent_hook.overrides
return make_dict_unstructure_fn(cl, converter, **overrides)
What this hook does is the following: for every subclass of FruitBasket
, it gets the hook for the parent from the converter, gets the overrides from that hook, and uses those overrides to create the hook for the subclass.
So now,
>>> print(conv.unstructure(MixedBasket(1, 2)))
{'bananas': 1, 'oranges': 2}
The bananas
rename gets propagated.
We still need one more thing: the ability to add overrides to subclasses and have those overrides propagated. We can define our own function:
def register_unstructure_hook(
cl: type[FruitBasket], conv: cattrs.Converter, **overrides: AttributeOverride
):
parent = cl.mro()[1]
parent_hook = conv.get_unstructure_hook(parent)
overrides = parent_hook.overrides | overrides
conv.register_unstructure_hook_func(
lambda t: t is cl, make_dict_unstructure_fn(cl, conv, **overrides)
)
It works similarly, combining the provided overrides with overrides from the parent hook.
Then:
>>> register_unstructure_hook(MixedBasket, conv, oranges=cattrs.override(rename="peaches"))
>>> print(conv.unstructure(MixedBasket(1, 2)))
{'bananas': 1, 'peaches': 2}
And because of the hook, subclasses will inherit the overrides.
This is pretty nifty, and I might make this a strategy in the future.
from cattrs.
Related Issues (20)
- Dataclass structuring fails with misleading error message.
- Default field conversion conflicts with user-defined field converter function HOT 3
- [macOS] Error when trying to run tests: `ImportError: cannot import name 'CodecOptions' from 'bson'` HOT 2
- How to hook into structuring of a simple dict? HOT 6
- Incorrect dispatch in Python 3.12 HOT 4
- Why does structuring of datetime in attrs class need a structure hook? HOT 1
- `prefer_attrib_converters` doesn't work when the field is aliased HOT 2
- Register structure hook only for optional types HOT 2
- TYPE_CHECKING and init=False HOT 5
- Problem with tagged union example HOT 3
- Subclass disambiguation for nested structures. HOT 3
- Unstructuring generics HOT 5
- 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 5
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.