Reputation: 527
I have 2 python class that depend on each other (a circular dependency)
FontFile
can contain multiple FontFace
(1 to n).FontFace
contain 1 FontFile
.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
Upvotes: 1
Views: 97
Reputation: 70357
There is no circular dependency here. FontFace
only references FontFile
in type annotations, which you've already correctly hidden behind if TYPE_CHECKING
and from __future__ import annotations
. FontFile
, in turn, references FontFace
in type annotations and isinstance
checks, which does make FontFile
depend directly on FontFace
. 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 FontFace
s. 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 a FontFace
(read: has a mutable font_file
slot on it), then why shouldn't it work with your FontFile
class?
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.
Upvotes: 1