I have 2 python class that depend on each other (a circular dependency)
- The class
FontFilecan contain multipleFontFace(1 to n). - The class
FontFacecontain 1FontFile.
Should I refactor the code so there isn't a circular reference between my 2 classes? If so, how? I don't see how I could do it.
Note: The FontFaceList ensures that the attribute font_file of the class FontFace is always correctly set.
from __future__ import annotations
from .factory_font_face import FactoryFontFace
from os import PathLike
from os.path import realpath
from time import time
from typing import Iterable, TYPE_CHECKING
if TYPE_CHECKING:
from .font_face import FontFace
class FontFaceList(list):
def __init__(self: FontFaceList, font_file: FontFile, font_faces: Iterable[FontFace]):
self.font_file = font_file
for font_face in font_faces:
if not isinstance(font_face, FontFace):
raise TypeError(f"The value is not of type {FontFace}")
font_face.font_file = self.font_file
super().__init__(font_faces)
def append(self: FontFaceList, value: FontFace):
if not isinstance(value, FontFace):
raise TypeError(f"The value is not of type {FontFace}")
value.font_file = self.font_file
super().append(value)
def extend(self: FontFaceList, iterable):
for font_face in iterable:
if not isinstance(font_face, FontFace):
raise TypeError(f"The value is not of type {FontFace}")
font_face.font_file = self.font_file
super().extend(iterable)
def insert(self: FontFaceList, i, value: FontFace):
if not isinstance(value, FontFace):
raise TypeError(f"The value is not of type {FontFace}")
value.font_file = self.font_file
super().insert(i, value)
class FontFile:
def __init__(
self: FontFile,
filename: PathLike[str],
font_faces: Iterable[FontFace],
last_loaded_time: float = time()
) -> FontFile:
self.filename = realpath(filename)
self.font_faces = FontFaceList(self, font_faces)
self.last_loaded_time = last_loaded_time
@classmethod
def from_font_path(cls: FontFile, filename: PathLike[str]) -> FontFile:
font_faces = FactoryFontFace.from_font_path(filename)
return cls(filename, font_faces)
from __future__ import annotations
from .name import Name
from typing import List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .font_file import FontFile
class FontFace():
def __init__(
self: FontFace,
font_index: int,
family_names: List[Name],
exact_names: List[Name],
weight: int,
is_italic: bool,
is_glyph_emboldened: bool,
font_type: FontType,
font_file: Optional[FontFile] = None,
) -> FontFace:
self.font_index = font_index
self.family_names = family_names
self.exact_names = exact_names
self.weight = weight
self.is_italic = is_italic
self.is_glyph_emboldened = is_glyph_emboldened
self.font_type = font_type
self.font_file = font_file
There is no circular dependency here.
FontFaceonly referencesFontFilein type annotations, which you've already correctly hidden behindif TYPE_CHECKINGandfrom __future__ import annotations.FontFile, in turn, referencesFontFacein type annotations andisinstancechecks, which does makeFontFiledepend directly onFontFace. But it's not circular since the reverse dependency is not in play at runtime.That being said, I think your runtime checks are being perhaps a bit too defensive. You've already got type annotations ensuring that the iterable contains only
FontFaces. If you're not running a type checker, then those annotations are a waste of space. If you are running a type checker, then you should trust those annotations and don't need the runtime checks.And on top of that, even if you didn't have the annotations, it's sort of contrary to Python zen to artificially require "my class only works with the concrete class
FontFace". Python is a big proponent of duck typing, so if I have a class that looks a lot like aFontFace(read: has a mutablefont_fileslot on it), then why shouldn't it work with yourFontFileclass?So while there isn't a circular dependency at runtime, as far as the Python interpreter is concerned, I still think there are improvements to be made here. You're artificially coupling the two classes in ways that, at least from the minimal example I see here, are unnecessary. Consider the two classes in isolation and ask the questions:
FontFile?FontFace?Maybe the resulting interfaces will surprise you with their simplicity. And if you come out the other side thinking "These two classes truly do directly depend on each other, and I refuse to let anyone else substitute their own class for one part", then perhaps they should be in one module. If they truly are that tightly coupled by design, and that's the best way to do it in your use case, then they should probably be living in one file right next to each other.