如何修复 Python 中的循环导入错误

How to fix this circular import error in Python

我已经为目前 returns 原始 JSON 数据的 REST API 构建了一个 Python 包装器。我的下一步是将 JSON 数据转换为 Python 对象。出于这个原因,我制作了一个资源 class,其中包含将字典转换为相关对象的所有必要方法。

如何修复此循环导入错误? 我考虑过从日历 class 中删除 periods 字段,但我限制了 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)

编辑 根据戴维斯的评论(感谢您的建议),我尝试将所有文​​件组合成一个 resources.py。尽管 from __future__ import annotations,仍然出现错误 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)

我们需要from __future__ import annotations推迟注释的评估并支持注释中的前向引用。

但这使得字段的类型成为字符串而不是数据类中的类型。

要解决此问题,我们必须 eval 字段的类型以获取它的类型。

并且因为 eval 需要访问它评估的对象并且为了使事情更简单,我将所有内容集成到一个模块中。

我检查一个字段的类型是否是 __post_init__ 中的 str 然后只有 eval 它以避免 eval 如果 Python 的未来版本是一个类型改变行为。这是针对 Python 3.8.

from __future__ import annotations
from dataclasses import dataclass, fields, field
from datetime import datetime, date


@dataclass
class Resource:
    def __post_init__(self, *_):
        for f in fields(self):
            value = getattr(self, f.name)
            field_type = f.type
            if isinstance(f.type, str):
                field_type = eval(f.type)
            # if f.type is list and value is not None:
            if field_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, field_type))
                # 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


@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 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 PeriodType(Resource):
    periodTypeSeq: str = field(default=None)
    name: str = field(default=None)
    description: str = field(default=None)
    level: int = field(default=None)