cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Composable custom class converters for attrs, dataclasses and friends.

Results 137 cattrs issues
Sort by recently updated
recently updated
newest added

really drop python<=3.7 support

[{"_id":"6717b322af4e91dcd806dbbb","body":"I'll take this, but `typing.List` is still a thing in 3.8 ;)\n\nI can rerun when you fix the tests.","issue_id":1715494468159,"origin_id":2064685386,"user_origin_id":1909233,"create_time":1713461714,"update_time":1713461723,"id":1729606434057,"updated_at":"2024-10-22T14:13:54.057000Z","created_at":"2024-10-22T14:13:54.057000Z"}] comment

Filter all python code over `pyupgrade --py38-plus`.

TYPE_CHECKING and init=False

[{"_id":"6717b2fa06908c53a3074a4f","body":"I assume you're using `from __future__ import annotations`?\r\n\r\n_cattrs_ calls `attrs.resolve_types` on every class it sees, which calls into `typing.get_type_hints` which explodes. I don't think there's an easy way for us to be selective about which fields we want resolved, unfortunately. _cattrs_ only wants `init=True` fields in this case, but I don't think there's an easy way to communicate that to _attrs_.","issue_id":1715494468178,"origin_id":2057228576,"user_origin_id":1909233,"create_time":1713197504,"update_time":1713197504,"id":1729606394709,"updated_at":"2024-10-22T14:13:14.709000Z","created_at":"2024-10-22T14:13:14.709000Z"},{"_id":"6717b2fa06908c53a3074a50","body":"Hi @Tinche, as always: thanks for connecting so quickly \ud83d\ude43 \ud83e\udd47 \r\n\r\n> I assume you're using `from __future__ import annotations`?\r\n\r\nYes, absolutely. Somehow got lost when copy-pasting.\r\n\r\n> _cattrs_ calls `attrs.resolve_types` on every class it sees, which calls into `typing.get_type_hints` which explodes. I don't think there's an easy way for us to be selective about which fields we want resolved, unfortunately. _cattrs_ only wants `init=True` fields in this case, but I don't think there's an easy way to communicate that to _attrs_.\r\n\r\nThe sequence of calls sounds logical and is what I would have expected to happen in the back. However \u2013 and perhaps this is a stupid question because I don't know the `cattrs` internals in depth \u2013 why is this an `attrs` issue? In my naive opinion: couldn't this problem be avoided by \"simply\" calling `attrs.resolve_types` only on those types that actually need to be resolved? That is, for any class in a given code, it's either \"class needs to be serialized -> cattrs\/converter needs to bother\" or \"class needs no serialization, e.g. because it only appears with `init=False` -> cattrs\/converter can ignore it\". So if `resolve_types` was only called on the former, the problem wouldn't arise, right?\r\n","issue_id":1715494468178,"origin_id":2057703787,"user_origin_id":23265127,"create_time":1713211229,"update_time":1713211229,"id":1729606394716,"updated_at":"2024-10-22T14:13:14.715000Z","created_at":"2024-10-22T14:13:14.715000Z"},{"_id":"6717b2fa06908c53a3074a51","body":"Here's the actual piece of code that calls `attrs.resolve_types`: https:\/\/github.com\/python-attrs\/cattrs\/blob\/898e59cc4076328b985d67ae989bc0f96578a9b5\/src\/cattrs\/gen\/__init__.py#L102. So we don't do it always, we only do it if there is a string annotation in one of the fields.\r\n\r\nTo help with clarity, let's distinguish between _classes_ and _fields_. We call `attrs.resolve_types` on classes, which resolves their fields so we can use them.\r\n\r\nSo in this case, cattrs will call `attrs.resolve_types(Outer)` because that will ensure the following call of `attrs.fields(Outer)` will return the proper metadata (that's just how attrs introspection works in general).\r\n\r\nNow, we could make this condition a little more sophisticated and account for `init=False` fields, but in your example, presumably `Outer` would have some other attributes you would want serialized. Those annotations would need to be de-stringified to work. And we can't call `resolve_types` and tell it to just resolve `init` fields.","issue_id":1715494468178,"origin_id":2057737679,"user_origin_id":1909233,"create_time":1713212546,"update_time":1713212546,"id":1729606394723,"updated_at":"2024-10-22T14:13:14.722000Z","created_at":"2024-10-22T14:13:14.722000Z"},{"_id":"6717b2fa06908c53a3074a52","body":"Ah ok, now I get it, thanks for the clarification \ud83d\udc4d\ud83c\udffc So it is an `attrs` issue, indeed ... Given that `cattrs` is the companion package of `attrs`, do you think there is a chance to tackle it from that side to enable such use cases for `cattrs`? After all, I've understood that both packages deeply care about performance and this really *is* a performance-related issue ;)","issue_id":1715494468178,"origin_id":2057747264,"user_origin_id":23265127,"create_time":1713212889,"update_time":1713212889,"id":1729606394728,"updated_at":"2024-10-22T14:13:14.727000Z","created_at":"2024-10-22T14:13:14.727000Z"},{"_id":"6717b2fa06908c53a3074a53","body":"Just had a quick look at the source code of `resolve_types` and noticed that it takes an optional `attribs` parameter that in fact let's control which fields to loop over:\r\nhttps:\/\/github.com\/python-attrs\/attrs\/blob\/5ce5d8278f162ec7542a251091427fd88e538554\/src\/attr\/_funcs.py#L463C9-L463C66\r\nIn principle, that would offer an easy solution, no? So instead of calling `resolve_types` with the class only, one could filter down to `init=True` attributes only and thus solve it from the `cattrs` side \ud83e\udd14 \r\n\r\n","issue_id":1715494468178,"origin_id":2057825893,"user_origin_id":23265127,"create_time":1713215868,"update_time":1713215868,"id":1729606394734,"updated_at":"2024-10-22T14:13:14.733000Z","created_at":"2024-10-22T14:13:14.733000Z"},{"_id":"6717b2fa06908c53a3074a54","body":"Before we give up on this issue, quickly pulling in @hynek with the hope he could share his opinion on the `attrs` side of things. \r\n\r\n@hynek, quick summary for you, so you don't need to go through the entire chain of messages. The described problem is roughly:\r\n* We have some `attrs` fields that are not relevant for serialization via `cattrs` (e.g. they are `init=False` \/ should not be part of the serialized object)\r\n* The types of these fields are defined only in `if TYPE_CHECKING` because they belong to some heavy third-party dependency that we import lazily only when really needed at runtime\r\n* As far as serialization is concerned, `cattrs` should be happy because it doesn't need to bother about these attributes at all.\r\n* However, because it needs to call `attrs.resolve_types` on the class, we run into a problem, since the types are not (yet) available at the time of the call\r\n\r\nCan we somehow circumvent the problem? My current (unsatisfactory workaround) is that I omit the types from the code and only have them in a comment for me as a reminder, losing the ability to fully type-check my classes.","issue_id":1715494468178,"origin_id":2273369937,"user_origin_id":23265127,"create_time":1723034145,"update_time":1723034145,"id":1729606394741,"updated_at":"2024-10-22T14:13:14.740000Z","created_at":"2024-10-22T14:13:14.740000Z"},{"_id":"6717b2fa06908c53a3074a55","body":"Heh you\u2019re asking the guy who asks @Tinche whenever he runs into a tricky typing problem. I\u2019m afraid his replies are as good as it gets.","issue_id":1715494468178,"origin_id":2294735314,"user_origin_id":41240,"create_time":1723879214,"update_time":1723879214,"id":1729606394747,"updated_at":"2024-10-22T14:13:14.747000Z","created_at":"2024-10-22T14:13:14.747000Z"}] comment

* cattrs version: 23.2.3 * Python version: 3.9.18 * Operating System: macOS ### Description Hi, I think I'm facing a similar situation as described in #160 but with a few...

enhancement

How to hook into structuring of a simple dict?

[{"_id":"6717b3214febc2515c110cdf","body":"Could you provide a minimal example in code?","issue_id":1715494468185,"origin_id":2005118956,"user_origin_id":1909233,"create_time":1710799136,"update_time":1710799136,"id":1729606433893,"updated_at":"2024-10-22T14:13:53.892000Z","created_at":"2024-10-22T14:13:53.892000Z"},{"_id":"6717b3214febc2515c110ce0","body":"Sorry for the delay...\r\nI don't know if it is a minimal one, but here is my example:\r\n```python\r\nfrom datetime import datetime\r\nfrom typing import Any, Final, Type, TypeVar\r\n\r\nimport attrs\r\nfrom cattrs.preconf.json import make_converter\r\nfrom cattrs.strategies import configure_tagged_union\r\n\r\nT = TypeVar('T')\r\n\r\n\r\[email protected]\r\nclass Sub:\r\n foo: str = 'bar'\r\n # and some more fields (incl. other attrs types)\r\n\r\n\r\[email protected]\r\nclass A:\r\n some: str = 'a'\r\n sub: Sub = Sub()\r\n\r\n\r\[email protected]\r\nclass B:\r\n some: str = 'b'\r\n sub: Sub = Sub()\r\n\r\n\r\nFrameData = dict | A | B\r\n\r\n\r\[email protected]\r\nclass Frame:\r\n data: FrameData\r\n\r\n\r\n_CUSTOMIZED_STRUCTURE_TYPES: Final[set] = {\r\n datetime,\r\n dict,\r\n Frame,\r\n # and some more...\r\n}\r\n\r\n_TIMESTAMP_FORMAT: Final[str] = '%Y%m%d_%H%M%S'\r\n\r\n\r\ndef _structure(data: dict[str, Any] | str, to_type: Type[T]) -> T:\r\n match to_type:\r\n case t if t is datetime:\r\n return datetime.strptime(data, _TIMESTAMP_FORMAT)\r\n case t if t is dict:\r\n return _structure_dict(data)\r\n case t if t is Frame:\r\n data.pop('to_add', None)\r\n return conv.structure_attrs_fromdict(data, Frame)\r\n case _:\r\n raise NotImplementedError(f'Unsupported type: {str(to_type)}.')\r\n\r\n\r\ndef _structure_dict(data: dict[str, Any]) -> dict[str, Any]:\r\n structured: dict[str, Any] = data.copy()\r\n for k, v in structured.items():\r\n if isinstance(v, str):\r\n try:\r\n structured[k] = datetime.strptime(v, _TIMESTAMP_FORMAT)\r\n except ValueError:\r\n continue\r\n # something is needed here to call the converter for structuring the other values of the dict\r\n return structured\r\n\r\n\r\nconv = make_converter()\r\n\r\nfor data_type in _CUSTOMIZED_STRUCTURE_TYPES:\r\n conv.register_structure_hook(data_type, lambda data, to_type: _structure(data, to_type))\r\n\r\nconfigure_tagged_union(union=FrameData,\r\n converter=conv,\r\n tag_name='_type',\r\n tag_generator=lambda t: t.__name__.casefold(),\r\n default=dict)\r\n```\r\nAs a result of this:\r\n```python\r\nf='{\"data\": {\"a\": {\"some\": \"a\", \"sub\": {\"foo\": \"bar\"}}, \"ts\": \"20240320_010203\", \"_type\": \"dict\"}}'\r\nprint(conv.loads(f, Frame))\r\n```\r\nI get this output:\r\n`Frame(data={'a': {'some': 'a', 'sub': {'foo': 'bar'}}, 'ts': datetime.datetime(2024, 3, 20, 1, 2, 3)})`\r\n\r\nBut what I need is this output:\r\n`Frame(data={'a': A(some='a', sub=Sub(foo='bar')), 'ts': datetime.datetime(2024, 3, 20, 1, 2, 3)})`","issue_id":1715494468185,"origin_id":2009742465,"user_origin_id":114678436,"create_time":1710945819,"update_time":1710945819,"id":1729606433900,"updated_at":"2024-10-22T14:13:53.899000Z","created_at":"2024-10-22T14:13:53.899000Z"},{"_id":"6717b3214febc2515c110ce1","body":"Hi Tin,\r\nIs there anything else I should add or is it just a busy schedule?","issue_id":1715494468185,"origin_id":2043260453,"user_origin_id":114678436,"create_time":1712596154,"update_time":1712596154,"id":1729606433905,"updated_at":"2024-10-22T14:13:53.905000Z","created_at":"2024-10-22T14:13:53.905000Z"},{"_id":"6717b3214febc2515c110ce2","body":"Hey,\r\n\r\nyeah sorry I got sidetracked by other things.\r\n\r\n> But since the simple dict objects can also contain attrs instances, it must be possible to call the structuring recursively again.\r\n\r\nThis is going to be complicated without modeling this more precisely. How do you know a nested dict is supposed to be converter into a class instance and not left as a dict? If a nested dict *always* means `A | B`, then it gets easier.\r\n\r\n> `Frame(data={'a': {'some': 'a', 'sub': {'foo': 'bar'}}, 'ts': datetime.datetime(2024, 3, 20, 1, 2, 3)})`\r\n\r\nThat looks correct given the input. Even if we assume `data['a']` is logically typed as `FrameData`, it has no `_type` field and so will default to a dict. In other words, how can we tell `data['a']` is supposed to be `A`?","issue_id":1715494468185,"origin_id":2048394493,"user_origin_id":1909233,"create_time":1712781433,"update_time":1712781448,"id":1729606433910,"updated_at":"2024-10-22T14:13:53.910000Z","created_at":"2024-10-22T14:13:53.910000Z"},{"_id":"6717b3214febc2515c110ce3","body":"> > But since the simple dict objects can also contain attrs instances, it must be possible to call the structuring recursively again.\r\n> \r\n> This is going to be complicated without modeling this more precisely. How do you know a nested dict is supposed to be converter into a class instance and not left as a dict? If a nested dict _always_ means `A | B`, then it gets easier.\r\n\r\nI now see that my example is misleading.\r\nMy sentence (which you quoted) referred to the following line in the `_structure_dict` function:\r\n```python\r\n# something is needed here to call the converter for structuring the other values of the dict\r\nreturn structured\r\n```\r\n\r\nThe goal is not to convert an arbitrary dict into an attrs instance. But dict-values, which in turn can be attrs instances (with datetime), should also be converted accordingly. However, they are not recognized as such before the datetime conversion (due to the special format).\r\nTo achieve this, the converter would have to be called again within the `_structure_dict` function. Something like this:\r\n```python\r\n# something is needed here to call the converter for structuring the other values of the dict\r\nreturn conv.structure(structured, dict)\r\n```\r\nOf course, this doesn't work because it creates an endless loop.\r\nWith attrs classes I use `conv.structure_attrs_fromdict` to achieve this (as in the case of `Frame`).\r\n\r\nIf necessary, I can rework the example. (But that will certainly not be until the week after next.)","issue_id":1715494468185,"origin_id":2052044898,"user_origin_id":114678436,"create_time":1712937828,"update_time":1712937828,"id":1729606433915,"updated_at":"2024-10-22T14:13:53.914000Z","created_at":"2024-10-22T14:13:53.914000Z"},{"_id":"6717b3214febc2515c110ce4","body":"Yeah, a simplified example would be good.\r\n\r\n> To achieve this, the converter would have to be called again within the _structure_dict function. Something like this: ... Of course, this doesn't work because it creates an endless loop.\r\n\r\nYou can just call `_structure_dict` on each value yourself, right? You don't even need to jump back into cattrs. It won't create an endless loop since it will stop when there are no values.","issue_id":1715494468185,"origin_id":2053744630,"user_origin_id":1909233,"create_time":1713038438,"update_time":1713038438,"id":1729606433919,"updated_at":"2024-10-22T14:13:53.919000Z","created_at":"2024-10-22T14:13:53.919000Z"}] comment

* cattrs version: 23.2.3 * Python version: 3.11 * Operating System: Windows (dev)/Linux (prod) Hey there! I have a few attrs classes. Some are members of a (tagged) union, together...

more-info-needed

How to recursively unstructure with hooks?

[{"_id":"6717b3304febc2515c110d09","body":"Here's the problem:\r\n\r\n```python\r\nconv.register_unstructure_hook(\r\n Frame,\r\n lambda obj: {\"to_add\": \"something special\", **conv.unstructure_attrs_asdict(obj)},\r\n)\r\n```\r\n\r\nIf you call into `conv.unstructure_attrs_asdict` directly your `_get_unstructure_without_nones` won't get called (that function will simply do all the work).\r\n\r\nTry this instead:\r\n\r\n```python\r\nbase_frame_hook = _get_unstructure_without_nones(Frame)\r\nconv.register_unstructure_hook(\r\n Frame, lambda obj: {\"to_add\": \"something special\", **base_frame_hook(obj)}\r\n)\r\n```","issue_id":1715494468193,"origin_id":1997860034,"user_origin_id":1909233,"create_time":1710433891,"update_time":1710433891,"id":1729606448169,"updated_at":"2024-10-22T14:14:08.168000Z","created_at":"2024-10-22T14:14:08.168000Z"},{"_id":"6717b3304febc2515c110d0a","body":"Greatly appreciated help at light speed!\r\n\r\nWhat would it look like if the thing is a bit more complex? E.g. multiple hook factories and separate helpers for the hooks instead of an inline lambda definition.\r\nI would be reluctant to couple the `Frame` hook with the hook factories.","issue_id":1715494468193,"origin_id":1997886986,"user_origin_id":114678436,"create_time":1710434725,"update_time":1710434725,"id":1729606448173,"updated_at":"2024-10-22T14:14:08.173000Z","created_at":"2024-10-22T14:14:08.173000Z"},{"_id":"6717b3304febc2515c110d0b","body":"Can you give me a more complex example?","issue_id":1715494468193,"origin_id":1999707991,"user_origin_id":1909233,"create_time":1710510545,"update_time":1710510545,"id":1729606448180,"updated_at":"2024-10-22T14:14:08.180000Z","created_at":"2024-10-22T14:14:08.180000Z"},{"_id":"6717b3304febc2515c110d0c","body":"Let's take this as a starting point:\r\n\r\n```python\r\nfrom datetime import datetime\r\nfrom functools import singledispatch\r\nfrom types import UnionType\r\nfrom typing import Any, Final, get_origin, get_args\r\n\r\nimport attrs\r\nfrom cattrs.gen import make_dict_unstructure_fn\r\nfrom cattrs.preconf.json import JsonConverter, make_converter\r\nfrom cattrs.strategies import configure_tagged_union\r\n\r\[email protected]\r\nclass Sub:\r\n foo: str = 'bar'\r\n # and some more fields (incl. other attrs types)\r\n\r\[email protected]\r\nclass A:\r\n to_keep: str = 'foo'\r\n to_skip: str|None = None\r\n sub: Sub = Sub()\r\n\r\[email protected]\r\nclass B:\r\n to_keep: str = 'bar'\r\n to_skip: str|None = None\r\n sub: Sub = Sub()\r\n\r\nFrameData = dict | A | B\r\n\r\[email protected]\r\nclass Frame:\r\n data: FrameData\r\n to_skip: str|None = None\r\n\r\n_CUSTOMIZED_UNSTRUCTURE_TYPES: Final[set] = {\r\n datetime,\r\n Frame,\r\n Sub,\r\n # and some more...\r\n}\r\n\r\n_TAGGED_UNIONS: Final[set] = {\r\n FrameData,\r\n # and some more...\r\n}\r\n\r\n_TIMESTAMP_FORMAT: Final[str] = '%Y%m%d_%H%M%S'\r\n\r\nconv = make_converter()\r\n\r\ndef _contains_nonetype(type_):\r\n if get_origin(type_) is UnionType:\r\n return type(None) in get_args(type_)\r\n return type_ is type(None)\r\n\r\ndef _is_attrs_with_none_defaults(type_: type) -> bool:\r\n return attrs.has(type_) and any(_contains_nonetype(a.type) and a.default is None\r\n for a in attrs.fields(type_))\r\n\r\ndef _get_unstructure_without_nones(cls):\r\n unstructure = make_dict_unstructure_fn(cl=cls, converter=conv)\r\n fields = [a.name for a in attrs.fields(cls) if _contains_nonetype(a.type) and a.default is None]\r\n\r\n def unstructure_without_nones(obj):\r\n unstructured = unstructure(obj)\r\n for field in fields:\r\n if unstructured[field] is None:\r\n unstructured.pop(field)\r\n return unstructured\r\n\r\n return unstructure_without_nones\r\n\r\n@singledispatch\r\ndef _unstructure(obj: Any) -> dict | str:\r\n raise NotImplementedError(f'Unsupported type: {type(obj)}.')\r\n\r\n@_unstructure.register\r\ndef _(obj: datetime) -> str:\r\n return obj.strftime(_TIMESTAMP_FORMAT)\r\n\r\n@_unstructure.register\r\ndef _(obj: Frame) -> dict:\r\n base_unstructure = _get_unstructure_without_nones(Frame)\r\n return {'to_add': 'something special', **base_unstructure(obj)}\r\n\r\n@_unstructure.register\r\ndef _(obj: Sub) -> dict:\r\n modified_dict = {\r\n # re-arrange data structure...\r\n }\r\n return conv.unstructure(modified_dict)\r\n\r\nconv.register_unstructure_hook_factory(predicate=_is_attrs_with_none_defaults, factory=_get_unstructure_without_nones)\r\n\r\nfor data_type in _CUSTOMIZED_UNSTRUCTURE_TYPES:\r\n conv.register_unstructure_hook(data_type, lambda obj: _unstructure(obj))\r\n\r\nfor tagged_union in _TAGGED_UNIONS:\r\n configure_tagged_union(union=tagged_union, converter=conv, tag_name='_type', tag_generator=lambda t: t.__name__.casefold(), default=dict)\r\n```\r\n\r\nNow I would like to add another hook factory, e.g. the `to_camel_case` thing from the docs.\r\nHow could I integrate this? And later on I might need to add another one (and another one...). ;-)\r\n","issue_id":1715494468193,"origin_id":1999869382,"user_origin_id":114678436,"create_time":1710515308,"update_time":1710518867,"id":1729606448187,"updated_at":"2024-10-22T14:14:08.187000Z","created_at":"2024-10-22T14:14:08.187000Z"},{"_id":"6717b3304febc2515c110d0d","body":"Hi Tin,\r\nIs there anything else I should add or is it just a busy schedule?","issue_id":1715494468193,"origin_id":2043260055,"user_origin_id":114678436,"create_time":1712596139,"update_time":1712596139,"id":1729606448192,"updated_at":"2024-10-22T14:14:08.192000Z","created_at":"2024-10-22T14:14:08.192000Z"}] comment

* cattrs version: 23.2.3 * Python version: 3.11 * Operating System: Windows (dev)/Linux (prod) Hey there, I have put together this small example to illustrate my issue. Context is JSON...

[macOS] Error when trying to run tests: `ImportError: cannot import name 'CodecOptions' from 'bson'`

[{"_id":"6717b3404febc2515c110d11","body":"Is your virtual environment set up properly, and did you use PDM (the packaging tool we use) to set it up using the lockfile? It's failing to import `CodecOptions` from the `bson` module, which leads me to believe you either have the `bson` package installed (it should be `pymongo` instead), or the wrong version of `pymongo`.","issue_id":1715494468199,"origin_id":2005122142,"user_origin_id":1909233,"create_time":1710799248,"update_time":1710799248,"id":1729606464803,"updated_at":"2024-10-22T14:14:24.803000Z","created_at":"2024-10-22T14:14:24.803000Z"},{"_id":"6717b3404febc2515c110d12","body":"> which leads me to believe you either have the `bson` package installed (it should be `pymongo` instead)\r\n\r\n@Tinche Yeah, this is it, thank you. Let me see if the latter builds.","issue_id":1715494468199,"origin_id":2005212228,"user_origin_id":92015510,"create_time":1710802565,"update_time":1710802565,"id":1729606464808,"updated_at":"2024-10-22T14:14:24.807000Z","created_at":"2024-10-22T14:14:24.807000Z"}] comment

* cattrs version: 23.2.3 * Python version: 3.12 * Operating System: macOS 10.6 ### Description Running tests does not seem to work. ### What I Did ``` ---> Testing py312-cattrs...

more-info-needed

* cattrs version: 23.2.3 * Python version: 3.11.8 * Operating System: OSX ### Description I'm using cattrs to destructure and restructure a complex hierarchy of classes. It is *mostly* working....

WIP: Expand docs on unstructuring

[{"_id":"6717b33eaf4e91dcd806dc3d","body":"Hi!\r\n\r\nI completely dropped the ball on this and I apologize for that, I got sidetracked by other things and forgot about this.\r\n\r\nI will try to integrate this into cattrs soon as the last thing I do before releasing the next version.\r\n\r\n> In particular, is there a place for demo code that will be executed?\r\n\r\nI assume you mean is the code in the documentation tested? We use doctests extensively so those can be used for this purpose. I don't insist on them though.\r\n\r\n> Should I add tests to verify that the behaviour I am describing in the docs is really what happens?\r\n\r\nIf you feel like they'd be valuable and feel confident enough to write them. Otherwise I'd skip em.\r\n\r\n> I'm not sure how to run the doctests, particularly in the docs\/ directory.\r\n\r\nI believe our tox setup can run the docs. Otherwise, I go into the docs directory and run `make doctest`.","issue_id":1715494468223,"origin_id":2239463218,"user_origin_id":1909233,"create_time":1721402919,"update_time":1721402919,"id":1729606462438,"updated_at":"2024-10-22T14:14:22.438000Z","created_at":"2024-10-22T14:14:22.438000Z"}] comment

I ran into some confusing situations trying to unstructure some objects I had defined (#513 ) and it was suggested I create a PR clarifying the docs. This PR attempts...

* cattrs version: 23.2.3 * Python version: 3.10.12 * Operating System: Ubuntu-22.04 * attrs version: 23.2.0 I mentioned this at the end of [another ticket](https://github.com/python-attrs/cattrs/issues/535), but I thought it deserved...

Inheriting overrides

[{"_id":"6852bf4d73e5de02fd0a2ca8","body":"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.\r\n\r\nHere's an approach I played around with. First, we can register a hook factory for all subclasses of `FruitBasket`.\r\n\r\n```python\r\nconverter.register_unstructure_hook_factory(\r\n lambda t: issubclass(t, FruitBasket),\r\n lambda t: make_dict_unstructure_fn(\r\n t, converter, apples=cattrs.override(rename=\"bananas\")\r\n ),\r\n)\r\n```\r\n\r\nNow, `FruitBasket` and all subclasses (including MixedBasket) will get hooks that rename apples to bananas.\r\n\r\nAs for also customizing MixedBasket. I refactored a little to this:\r\n\r\n```python\r\nBASE_OVERRIDES = {\"apples\": cattrs.override(rename=\"bananas\")}\r\n\r\nconverter = cattrs.Converter()\r\nconverter.register_unstructure_hook_factory(\r\n lambda t: issubclass(t, FruitBasket),\r\n lambda t: make_dict_unstructure_fn(t, converter, **BASE_OVERRIDES),\r\n)\r\nconverter.register_unstructure_hook(\r\n MixedBasket,\r\n make_dict_unstructure_fn(\r\n MixedBasket,\r\n converter,\r\n oranges=cattrs.override(rename=\"peaches\"),\r\n **BASE_OVERRIDES,\r\n ),\r\n)\r\n```\r\n\r\nNow 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.\r\n\r\nLet me know what you think.","issue_id":1716834452190,"origin_id":2125943676,"user_origin_id":1909233,"create_time":1716420772,"update_time":1716420772,"id":1750253389441,"updated_at":"2025-06-18T13:29:49.440000Z","created_at":"2025-06-18T13:29:49.440000Z"},{"_id":"6852bf4d73e5de02fd0a2ca9","body":"Hi @Tinche, appreciate very much that you took the time to think about the problem \ud83e\udd47 \r\n\r\nThe 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?","issue_id":1716834452190,"origin_id":2134671525,"user_origin_id":23265127,"create_time":1716886049,"update_time":1716886049,"id":1750253389447,"updated_at":"2025-06-18T13:29:49.447000Z","created_at":"2025-06-18T13:29:49.447000Z"},{"_id":"6852bf4d73e5de02fd0a2caa","body":"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.\r\n\r\nIn 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:\r\n```python\r\nfrom cattrs.gen import make_dict_unstructure_fn\r\n\r\nhook = make_dict_unstructure_fn(..., a=override(rename=...))\r\n```\r\nthe overrides can be fetched:\r\n```python\r\n>>> print(hook.overrides)\r\n{\"a\": AttributeOverride(...)}\r\n```\r\n\r\nWe can leverage this.\r\n\r\nFirst we need a base unstructure hook for `FruitBasket`, and only `FruitBasket` (no subclasses). We apply the apples -> bananas rename here.\r\n\r\n```python\r\nconv = cattrs.Converter()\r\n\r\nconv.register_unstructure_hook_func(\r\n lambda t: t is FruitBasket,\r\n make_dict_unstructure_fn(\r\n FruitBasket, conv, apples=cattrs.override(rename=\"bananas\")\r\n ),\r\n)\r\n```\r\n\r\nSimple 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:\r\n\r\n```python\r\[email protected]_unstructure_hook_factory(\r\n lambda type: type is not FruitBasket and issubclass(type, FruitBasket)\r\n)\r\ndef fruit_baskets_hook_factory(cl: type[FruitBasket], converter: cattrs.Converter):\r\n parent = cl.mro()[1]\r\n parent_hook = converter.get_unstructure_hook(parent)\r\n overrides = parent_hook.overrides\r\n return make_dict_unstructure_fn(cl, converter, **overrides)\r\n```\r\n\r\nWhat 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.\r\n\r\nSo now,\r\n```python\r\n>>> print(conv.unstructure(MixedBasket(1, 2)))\r\n{'bananas': 1, 'oranges': 2}\r\n```\r\n\r\nThe `bananas` rename gets propagated.\r\n\r\nWe still need one more thing: the ability to add overrides to subclasses and have those overrides propagated. We can define our own function:\r\n\r\n```python\r\ndef register_unstructure_hook(\r\n cl: type[FruitBasket], conv: cattrs.Converter, **overrides: AttributeOverride\r\n):\r\n parent = cl.mro()[1]\r\n parent_hook = conv.get_unstructure_hook(parent)\r\n overrides = parent_hook.overrides | overrides\r\n conv.register_unstructure_hook_func(\r\n lambda t: t is cl, make_dict_unstructure_fn(cl, conv, **overrides)\r\n )\r\n```\r\nIt works similarly, combining the provided overrides with overrides from the parent hook.\r\n\r\nThen:\r\n```python\r\n>>> register_unstructure_hook(MixedBasket, conv, oranges=cattrs.override(rename=\"peaches\"))\r\n>>> print(conv.unstructure(MixedBasket(1, 2)))\r\n{'bananas': 1, 'peaches': 2}\r\n```\r\n\r\nAnd because of the hook, subclasses will inherit the overrides.\r\n\r\nThis is pretty nifty, and I might make this a strategy in the future.","issue_id":1716834452190,"origin_id":2143609518,"user_origin_id":1909233,"create_time":1717280084,"update_time":1717280084,"id":1750253389455,"updated_at":"2025-06-18T13:29:49.454000Z","created_at":"2025-06-18T13:29:49.454000Z"}] comment

* cattrs version: 23.2.3 * Python version: 3.9 * Operating System: macOS ### Description I am wondering if `cattrs` provides a clever built-in mechanism reuse/inherit overrides from another class. It...