fix: unstructure enum values recursively through the converter#760
fix: unstructure enum values recursively through the converter#760gaoflow wants to merge 2 commits into
Conversation
When an enum's members have other enums (or complex types) as their values—and the enum has no `_value_` type annotation—`enum_unstructure_factory` returned `e.value` directly instead of running it through the converter. This caused enum-valued enums to produce non-JSON-serializable objects (the raw nested Enum) instead of the fully unstructured result. The typed-enum branch already called `converter.unstructure(e.value)`. Apply the same approach unconditionally: the distinction between typed and untyped enum is not needed for unstructuring, since both paths call `converter.unstructure`. Fixes python-attrs#679.
Merging this PR will degrade performance by 14.57%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_unstructure_attrs_lists[UnstructureStrategy.AS_DICT-Converter] |
86.8 µs | 107.3 µs | -19.1% |
| ❌ | test_unstructure_simple_enum[Converter] |
15.6 µs | 18.9 µs | -17.68% |
| ❌ | test_unstructure_simple_int_enum[MsgspecJsonConverter] |
15.8 µs | 18.5 µs | -14.75% |
| ❌ | test_unstructure_simple_int_enum[OrjsonConverter] |
15.8 µs | 18.5 µs | -14.6% |
| ❌ | test_unstructure_simple_enum[MsgspecJsonConverter] |
15.6 µs | 18.1 µs | -14.14% |
| ❌ | test_unstructure_simple_enum[OrjsonConverter] |
15.6 µs | 18 µs | -13.26% |
| ❌ | test_unstructure_attrs_primitives[UnstructureStrategy.AS_DICT-Converter] |
41.2 µs | 47 µs | -12.35% |
| ❌ | test_unstructure_simple_int_enum[Converter] |
16 µs | 17.9 µs | -10.36% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing gaoflow:fix-679-enum-value-enum-unstructure (1bdcc47) with main (3d07082)1
Footnotes
|
That's what I was afraid of - by always calling unstructure, we tank the very common hot path for simpler enums. I think the factory should be smarter, and default to unstructure only under certain cases. |
|
Thanks, agreed. I updated the factory so simple enum values keep the direct The recursive path now covers the cases from #679: enum-valued members, tuples/collections/mappings containing enums, and attrs/dataclass values. I also added a test that registers an overriding Verification run locally: |
Summary
Fixes #679.
When an enum's members have other enums as values (no
_value_annotation),enum_unstructure_factoryreturnede.valuedirectly, bypassing the converter.This meant the inner enum was never unstructured—it was returned as a raw
Enumobject, which causes
json.dumps(and other serializers) to fail.Before:
After:
Root cause
enum_unstructure_factoryhad two branches:_value_annotation): calledconverter.unstructure(e.value)— correctlambda e: e.value— skipped the converter entirelyThe distinction is unnecessary for unstructuring. The fix collapses both branches to
always call
converter.unstructure(e.value).Primitive enum values (int, str) pass through the converter unchanged, so existing
behavior is preserved.
Test plan
tests/test_enums.py) all passtest_unstructure_enum_with_enum_valuesadded and passing