python-attrs / python-attrs/cattrs

Literal discriminator ignores override(rename=...), so union structuring fails with KeyError

Open
#779 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
1.1k
Forks
159
Avg merge
12h 21m
Merged PRs (30d)
6

Description

When a union's members are discriminated by a Literal field, create_default_dis_func looks that field up in the payload under its original attribute name, ignoring any override(rename=...) registered for it. Structuring the union then fails with a bare KeyError, even though each member structures fine on its own.

The unique-field path of the same function does honour renames (via _usable_attribute_names), so this is an inconsistency between the two disambiguation strategies rather than a general limitation.

Reproduction

from typing import Literal, Union
from attrs import define
from cattrs import Converter
from cattrs.gen import make_dict_structure_fn, override

@define
class A:
    kind: Literal["a"]
    a_val: int

@define
class B:
    kind: Literal["b"]
    b_val: int

c = Converter()
for cl in (A, B):
    c.register_structure_hook(
        cl, make_dict_structure_fn(cl, c, kind=override(rename="type"))
    )

c.structure({"type": "a", "a_val": 1}, A)            # A(kind='a', a_val=1)  -- fine
c.structure({"type": "a", "a_val": 1}, Union[A, B])  # KeyError: 'kind'

Expected

A(kind='a', a_val=1), the same as structuring A directly.

Actual

KeyError: 'kind'

Notes

For contrast, the unique-field strategy handles the equivalent rename correctly — this is what test_field_renaming in tests/test_disambiguators.py already covers:

@define
class C:
    c_val: int

@define
class D:
    d_val: int

c3 = Converter()
c3.register_structure_hook(C, make_dict_structure_fn(C, c3, c_val=override(rename="cv")))
c3.register_structure_hook(D, make_dict_structure_fn(D, c3, d_val=override(rename="dv")))
c3.structure({"cv": 1}, Union[C, D])  # C(c_val=1) -- correct

In create_default_dis_func, overrides is computed up front but the use_literals branch builds its candidate field names from the raw at.name and looks up data[best_discriminator] with that same raw name; only the later unique-key branch passes overrides through _usable_attribute_names.

Tested on main (5bf7c97). I have a fix and will open a PR shortly.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at create_default_dis_func and read tests/test_disambiguators.py, especially test_field_renaming. Reproduce the Literal-discriminator case with the renamed field, then add coverage showing union structuring matches direct structuring and no longer raises KeyError.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, testing
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.