Search code examples
pythonobjectpython-datetimepython-dataclasses

Converting arguments to custom objects using dataclasses package


I recently discovered the dataclasses Python package. I'm running into an issue when using custom classes in my type annotation. I've got a simple example below.

When the Entry class gets passed the location argument, the value of that argument should be used to construct a Location object. Similarly, when the Entry class gets passed a string for the creationDate argument, it should be parsed (using dateutil.parser.parse) to create a datetime.datetime object. In my code, the location and creationDate arguments are not converted to the Location and datetime.datetime objects. I'm not sure how to make this work. Please advise.

Granted, I could do this without using the dataclasses package. It would add more boilerplate code. I'm also using this as an excuse to learn the dataclasses package so I can use it more efficiently the next time.

Try it


import datetime
import dateutil.parser
import dataclasses
import inspect

@dataclasses.dataclass
class Location():
    latitude: float
    longitude: float

@dataclasses.dataclass
class Entry():
    """
    A single DayOne entry
    """
    creationDate: datetime.datetime = \
        dataclasses.field(default_factory=dateutil.parser.parse)
    location: Location = None

    @classmethod
    def factory(cls, **kwargs):
        class_fields = {k:v for k,v in kwargs.items()
                        if k in inspect.signature(cls).parameters}

        return cls(**class_fields)

if __name__ == "__main__":
    print("Converting from dayone to jekyll\n")


    args = {
        "creationDate": '2022-05-30T04:44:33Z',
        "location": {
            'latitude': -37.8721,
            'longitude': 175.6829,
            'named': 'Hobbiton'
        },
        "text": "In a hole in the ground there lived a hobbit. Not a nasty, dirty, wet hole, filled with the ends of worms and an oozy smell, nor yet a dry, bare, sandy hole with nothing in it to sit down on or to eat: it was a hobbit-hole, and that means comfort."
    }

    entry = Entry.factory(**args)
    print(type(entry.location))
    print(type(entry.creationDate))

Solution

  • One option could be to use dataclass-wizard, which is a bit more lightweight than pydantic. It uses typing-extensions module for earlier python versions, but in 3.10+ it only relies on core python stdlib.

    Usage:

    from __future__ import annotations  # can be removed in 3.10+
    
    import datetime
    # import dateutil.parser
    import dataclasses
    from pprint import pprint
    
    from dataclass_wizard import JSONWizard
    
    
    @dataclasses.dataclass
    class Location:
        latitude: float
        longitude: float
        named: str | None = None
    
    
    @dataclasses.dataclass
    class Entry(JSONWizard):
        """
        A single DayOne entry
        """
        creation_date: datetime.datetime
        location: Location | None = None
    
    
    if __name__ == "__main__":
        print("Converting from dayone to jekyll\n")
    
        args = {
            "creationDate": '2022-05-30T04:44:33Z',
            "location": {
                'latitude': -37.8721,
                'longitude': 175.6829,
                'named': 'Hobbiton'
            },
            "text": "In a hole in the ground there lived a hobbit. Not a nasty, dirty, wet hole, filled with the ends of worms and an oozy smell, nor yet a dry, bare, sandy hole with nothing in it to sit down on or to eat: it was a hobbit-hole, and that means comfort."
        }
    
        entry = Entry.from_dict(args)
        print(type(entry.location))
        print(type(entry.creation_date))
        print()
    
        print('Object:')
        pprint(entry)
    

    Result:

    Converting from dayone to jekyll
    
    <class '__main__.Location'>
    <class 'datetime.datetime'>
    
    Object:
    Entry(creation_date=datetime.datetime(2022, 5, 30, 4, 44, 33, tzinfo=datetime.timezone.utc),
          location=Location(latitude=-37.8721, longitude=175.6829, named='Hobbiton'))
    

    Side note: I haven't actually thought of using dateutil.parser.parse to parse date strings, though that might be a good idea coinidentally. The current implementation uses datetime.fromisoformat which does work well enough in the general use case.