'How to fix this circular import error in Python

I have build a Python wrapper for a REST API that currently returns the raw JSON data. My next step is to convert the JSON data into Python objects. For this reason, I've made a Resource class with all the necessary methods to convert a dict into the relevant object.

How do I fix this circular import error? I have thought of removing the periods field from the Calendar class, but them I'm limiting the functionality of the API.

# test.py
from my_project.endpoints import Calendars
from my_project.resources import Calendar

calendars = Calendars(username, password).list()
calendars = [Calendar(**calendar) for calendar in calendars]

# ImportError: cannot import name 'Calendar' from partially initialized module 'my_project.model.resources.calendar' (most likely due to a circular import)
# my_project/client.py
from dataclasses import dataclass, fields

@dataclass
class Resource:
    def __post_init__(self, *_):
        for f in fields(self):
            value = getattr(self, f.name)
            if f.type is list and value is not None:
                astype = f.metadata.get("type", str)
                setattr(self, f.name, [self.__cast(v, astype) for v in value])
            else:
                setattr(self, f.name, self.__cast(value, f.type))

    @classmethod
    def __cast(cls, value, astype):
        if value is None:
            return None
        if astype is datetime:
            return datetime.fromisoformat(value)
        elif astype is date:
            return datetime.fromisoformat(value).date()
        elif astype == cls.__name__:
            key = cls._key()
            return cls(**{key: value})
        elif issubclass(astype, Resource) and isinstance(value, dict):
            return astype(**value)
        elif issubclass(astype, Resource) and isinstance(value, (str, int)):
            return astype(**{astype._key(): value})
        else:
            return astype(value)

    @classmethod
    def _key(cls) -> tuple:
        for f in fields(cls):
            if f.metadata.get("key"):
                return f.name
# my_project/resources.py
from my_project.model.resources.calendar import Calendar
from my_project.model.resources.period import Period
from my_project.model.resources.periodType import PeriodType

__all__ = ["Calendar", "Period", "PeriodType"]
# my_project/model/resources/calender.py
from my_project.model.resources.period import Period

@dataclass
class Calendar(Resource):
    calenderSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    periods: list = field(default=None, metadata={"type": Period})
# my_project/model/resources/period.py
from my_project.model.resources.calendar import Calendar
from my_project.model.resources.periodType import PeriodType

@dataclass
class Period(Resource):
    periodSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    periodType: PeriodType = field(default=None)
    calendar: Calendar = field(default=None)

Edit As per the comment of Davis (thanks for the suggestion), I tried combining all the files into a single resources.py. Despite the from __future__ import annotations, still got the error NameError: name 'Period' is not defined.

# my_project/resources.py
from __future__ import annotations

from dataclasses import InitVar, dataclass, field
from datetime import date, datetime

from sapcommissions import Resource


@dataclass
class Calendar(Resource):
    calenderSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    periods: list = field(default=None, metadata={"type": Period}) # error here


@dataclass
class Period(Resource):
    periodSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    periodType: PeriodType = field(default=None)
    calendar: Calendar = field(default=None)


@dataclass
class PeriodType(Resource):
    periodTypeSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    level: int = field(default=None)


Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source