How to nest a list of attrs classes into an attrs class

103 Views Asked by At

I have a list of dicts and I'd like to use python-attrs to convert them into classes.

Here's the sample data:

[[characters]]
first_name = 'Duffy'
last_name = 'Duck'

[[characters]]
first_name = 'Bugs'
last_name = 'Bunny'

[[characters]]
first_name = 'Sylvester'
last_name = 'Pussycat'

[[characters]]
first_name = 'Elmar'
last_name = 'Fudd'

[[characters]]
first_name = 'Tweety'
last_name = 'Bird'

[[characters]]
first_name = 'Sam'
last_name = 'Yosemite'

[[characters]]
first_name = 'Wile E.'
last_name = 'Coyote'

[[characters]]
first_name = 'Road'
last_name = 'Runner'

This will then turn into a dictionary after reading the content:

{'characters': [{'first_name': 'Duffy', 'last_name': 'Duck'},
  {'first_name': 'Bugs', 'last_name': 'Bunny'},
  {'first_name': 'Sylvester', 'last_name': 'Pussycat'},
  {'first_name': 'Elmar', 'last_name': 'Fudd'},
  {'first_name': 'Tweety', 'last_name': 'Bird'},
  {'first_name': 'Sam', 'last_name': 'Yosemite'},
  {'first_name': 'Wile E.', 'last_name': 'Coyote'},
  {'first_name': 'Road', 'last_name': 'Runner'}]}

My classes look like this:

@define(kw_only=True)
class Character:
    first_name: str
    last_name: str

@define
class LooneyToons:
    characters: List[Character] = field(factory=list, converter=Character)

But it does not work: TypeError: Character.__init__() takes 1 positional argument but 2 were given

Of course I could modify the class a bit and use this code (which works):

@define
class LooneyToons:
    characters: List[Character]

> LooneyToons([Character(**x) for x in d['characters']])
LooneyToons(characters=[Character(first_name='Duffy', last_name='Duck'), Character(first_name='Bugs', last_name='Bunny'), Character(first_name='Sylvester', last_name='Pussycat'), Character(first_name='Elmar', last_name='Fudd'), Character(first_name='Tweety', last_name='Bird'), Character(first_name='Sam', last_name='Yosemite'), Character(first_name='Wile E.', last_name='Coyote'), Character(first_name='Road', last_name='Runner')])

But it would be more elegant (from my point of view) to handle this within the class LooneyToons by just giving d['characters'] as argument to the class.

Any hints for me? I already checked out cattrs but I don't get the point on how it may be useful in my case.

2

There are 2 best solutions below

0
Tin Tvrtković On BEST ANSWER

I'm the author of cattrs (and a maintainer of attrs ;).

The problem is your converter argument on LooneyToons.characters. Here's the solution without it:

from typing import List

from attrs import define, field

from cattrs import structure

d = {
    "characters": [
        {"first_name": "Duffy", "last_name": "Duck"},
        {"first_name": "Bugs", "last_name": "Bunny"},
        {"first_name": "Sylvester", "last_name": "Pussycat"},
        {"first_name": "Elmar", "last_name": "Fudd"},
        {"first_name": "Tweety", "last_name": "Bird"},
        {"first_name": "Sam", "last_name": "Yosemite"},
        {"first_name": "Wile E.", "last_name": "Coyote"},
        {"first_name": "Road", "last_name": "Runner"},
    ]
}


@define(kw_only=True)
class Character:
    first_name: str
    last_name: str


@define
class LooneyToons:
    characters: List[Character] = field(factory=list)


structure(d, LooneyToons)

If you insist on using a converter, it'll have to be a little more sophisticated than just Character (see other answer). But I suggest just removing it.

0
brillenheini On

A possible solution could be:

def converter(value):
    list_of_characters=[]
    if isinstance(value, list):
        for v in value:
            if isinstance(v, dict):
                list_of_characters.append(Character(**v))
    if isinstance(value, dict):
        list_of_characters.append(Character(**value))
    return list_of_characters

@define
class LooneyToons:
    characters: List[Character] = field(factory=list, converter=converter)

If I provide the data now, the output is:

>>> LooneyToons(d['characters'])
LooneyToons(characters=[Character(first_name='Duffy', last_name='Duck'), Character(first_name='Bugs', last_name='Bunny'), Character(first_name='Sylvester', last_name='Pussycat'), Character(first_name='Elmar', last_name='Fudd'), Character(first_name='Tweety', last_name='Bird'), Character(first_name='Sam', last_name='Yosemite'), Character(first_name='Wile E.', last_name='Coyote'), Character(first_name='Road', last_name='Runner')])

>>> l = LooneyToons(d['characters'])
>>> l.characters[0]
Character(first_name='Duffy', last_name='Duck')

Don't know if this is a good solution or if there are better ones out there (please share them, if you have one), but it's working for my case.