python-attrs / python-attrs/cattrs

KeyError when using configure_tagged_union with existing field

Open
#483 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

  • cattrs version: 23.2.3
  • Python version: 3.11.5
  • Operating System: Windows 10
Description

I am trying to create a union type which uses an existing key to determine the type to use.

Looking at an example object, the "type" key word is used by the application to set up the correct type of device, and I want to use it to enable additional configuration settings:

    {
        "device": {
            "type": "special",
            "hostname": "hostname",
            "port": 1234
        }
    }

When I use configure_tagged_union(..., tag_name="type" ...), the "type" key seems to be consumed and is no longer available for the Device data class. This is causing a key error during the structure process.

What do I need to do to keep the "type" data available and use it for the tagged union?

What I Did

Example code:

import attrs
from cattrs import Converter
from cattrs.strategies import configure_tagged_union


@attrs.define
class SpecialDevice:
    hostname: str
    port: int
    type: str


@attrs.define
class StandardDevice:
    hostname: str
    type: str


Device = SpecialDevice | StandardDevice


@attrs.define
class Settings:
    device: Device


def tag_generator(t) -> str:
    return {
        SpecialDevice: "special",
        StandardDevice: "standard"
    }[t]


c = Converter()
configure_tagged_union(Device, c, tag_name="type", tag_generator=tag_generator, default=StandardDevice)
settings = c.structure(
    {
        "device": {
            "type": "special",
            "hostname": "hostname",
            "port": 1234
        }
    }, Settings)
print(settings)

Traceback:

  + Exception Group Traceback (most recent call last):
  |   File "AppData\Roaming\JetBrains\PyCharm2023.3\scratches\scratch.py", line 36, in <module>
  |     settings = c.structure(
  |                ^^^^^^^^^^^^
  |   File ".virtualenvs\gantry-control-1-NYP4DbyD\Lib\site-packages\cattrs\converters.py", line 332, in structure
  |     return self._structure_func.dispatch(cl)(obj, cl)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "<cattrs generated structure __main__.Settings>", line 9, in structure_Settings
  | cattrs.errors.ClassValidationError: While structuring Settings (1 sub-exception)
  +-+---------------- 1 ----------------
    | Exception Group Traceback (most recent call last):
    |   File "<cattrs generated structure __main__.Settings>", line 5, in structure_Settings
    |   File ".virtualenvs\gantry-control-1-NYP4DbyD\Lib\site-packages\cattrs\converters.py", line 632, in _structure_union
    |     return handler(obj, union)
    |            ^^^^^^^^^^^^^^^^^^^
    |   File ".virtualenvs\gantry-control-1-NYP4DbyD\Lib\site-packages\cattrs\strategies\_unions.py", line 106, in structure_tagged_union
    |     return _tag_to_hook[val.pop(_tag_name)](val)
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File ".virtualenvs\gantry-control-1-NYP4DbyD\Lib\site-packages\cattrs\strategies\_unions.py", line 57, in structure_union_member
    |     return _h(val, _cl)
    |            ^^^^^^^^^^^^
    |   File "<cattrs generated structure __main__.SpecialDevice>", line 19, in structure_SpecialDevice
    | cattrs.errors.ClassValidationError: While structuring SpecialDevice (1 sub-exception)
    | Structuring class Settings @ attribute device
    +-+---------------- 1 ----------------
      | Traceback (most recent call last):
      |   File "<cattrs generated structure __main__.SpecialDevice>", line 15, in structure_SpecialDevice
      | KeyError: 'type'
      | Structuring class SpecialDevice @ attribute type
      +------------------------------------

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 with cattrs/strategies/_unions.py, especially configure_tagged_union and structure_tagged_union, then reproduce the provided Settings example. Check how the tag value is removed before the selected class is structured. Done means the union still dispatches on "type" while the selected device class can receive that field, with a regression test covering the example.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.