Search code examples
pythonpython-attrspython-cattrs

How do you get cattrs to unstructure nested structures?


In Python, with the libraries attrs and cattrs, I have a nested structure defined as follows:

@attrs.define
class Score:
    added: datetime
    value: float


@attrs.define
class Entry:
    score: Score
    tags: List[str]
    added: Optional[datetime] = None


@attrs.define
class Entries:
    entries: Dict[date, Entry] = attrs.Factory(dict)

With this data:

data = {
    'entries': {
        '2024-01-01': {
            'score': {'added': '2023-02-03 04:03:00', 'value': 80.3},
            'tags': ['meatball', 'salami', 'jerky'],
        },
        # … more days
    }
}

And some un/structure hooks to handle the date and datetime types:

cattrs.register_unstructure_hook(datetime, lambda dt: datetime.strptime(dt, '%Y-%m-%d %H:%M:%S'))
cattrs.register_structure_hook(datetime, lambda dt, _: str(dt))
cattrs.register_unstructure_hook(date, lambda dt: datetime.strptime(dt, '%Y-%m-%d').date())
cattrs.register_structure_hook(date, lambda dt, _: str(dt))

Now when I destructure an instance of Entries, the resulting dict does not have the date and datetime objects in string form:

structured = cattrs.structure(data, Entries)

cattrs.unstructure(structured) == {
    'entries': {
        datetime.date(2024, 1, 1): {
            'score': {
                'added': datetime.datetime(2023, 2, 3, 4, 3),
                'value': 80.3
            },
            'tags': ['meatball', 'salami', 'jerky'],
            'added': None
        }
    }
}

How can I get cattrs to stringify date and datetime objects recursively?


Solution

  • Your hooks don't seem correct. Your unstructure hooks should make the types simpler (so, from datetimes to strings), and your structure hooks should add structure to the data (so, from strings to datetimes).

    So, for the unstructure hooks:

    # Takes a datetime, returns a string
    cattrs.register_unstructure_hook(datetime, lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S"))
    
    # Takes a date, returns a string
    cattrs.register_unstructure_hook(date, lambda dt: dt.strftime("%Y-%m-%d"))
    

    and for the structure hooks:

    # Takes a string, returns a datetime
    cattrs.register_structure_hook(
        datetime, lambda dt, _: datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
    )
    
    # Takes a string, returns a date
    cattrs.register_structure_hook(
        date, lambda dt, _: datetime.strptime(dt, "%Y-%m-%d").date()
    )
    

    After applying these modifications your example seems to work. Good luck!