As the title indicates, my issue is with circular imports in the helpers.py from one of my apps. Here’s the directory structure and some code:
/gameskedge
/apps
/users
/teams
/web
/westmarches (has _init_.py and empty models.py
/games (has __init__.py and models )
/helpers.py (this is where I have the issue)
/profiles (has __init__.py and profile models)
/schedules (has __init__.py and models )
/gameskedge (project folder with settings.py)
here’s some code:
profiles/models.py
from django.db import models
from gameskedge import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from apps.teams.models import BaseTeamModel
class Profile(BaseTeamModel):
"""Profiles link Characters to Teams Users (users.CustomUser Class) through Rosters"""
player = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
name = models.CharField(max_length=32, default=player, blank=True, null=True)
is_active = models.BooleanField(default=False, null=False, blank=False)
is_default = models.BooleanField(default=False, null=False, blank=False)
gamesessions = models.ForeignKey('westmarches_games.GameSession', on_delete=models.PROTECT, null=True, blank=True)
def __str__(self):
return f'{self.name} [{self.player}]'
...
games/models.py
import uuid
from django.db import models
from apps.teams.models import BaseTeamModel
from apps.westmarches.profiles.models import Profile
class GameSession(BaseTeamModel):
"""A Game Session with players"""
GAME_SESSION_STATUS = [
('new', 'New'),
('open', 'Open'),
('closed', 'Closed'),
('full', 'Full'),
('inprogress', 'In progress'),
('completed', 'Completed'),
('cancelled', 'Cancelled')
]
GAME_REQUEST_STATUS = [
('created', 'Created'),
('requested', 'Requested'),
('accepted', 'Accepted'),
('rejected', 'Rejected')
]
session_status = models.CharField(max_length=10, null=True, blank=False, choices=GAME_SESSION_STATUS, default='new')
request_status = models.CharField(max_length=9, null=True, blank=False, choices=GAME_REQUEST_STATUS, default='created')
start_datetime = models.DateTimeField(default=None, blank=False, null=False)
end_time = models.TimeField(default=None, blank=False, null=False)
requester = models.ForeignKey(Profile, related_name='requests', on_delete=models.CASCADE, default=None, blank=True, null=True)
officiant = models.ForeignKey(Profile, on_delete=models.CASCADE, default=None, blank=True, null=True)
members = models.ManyToManyField(Profile, related_name='profiles', through='GameGroup', blank=True)
def __str__(self):
return f'{self.start_datetime}'
...
class GameGroup(BaseTeamModel):
"A set of profiles invited to a GameSession"
game_session = models.ForeignKey(GameSession, on_delete=models.CASCADE)
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
class GameInvite(BaseTeamModel):
"""An invitation to a GameSession"""
INVITE_STATUS = [
('composed', 'Composed'),
('delivered', 'Delivered'),
('received', 'Received'),
('accepted', 'Accepted'),
('rejected', 'Rejected'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable = False)
status = models.CharField(max_length=11, null=True, blank=False, choices=INVITE_STATUS, default='composed')
sender = models.ForeignKey(Profile, on_delete=models.CASCADE, null=True, blank=True)
game_session = models.ForeignKey(GameSession, on_delete=models.PROTECT, related_name='game_invites')
recipient = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name="received_invites", null=True, blank=True)
received_date = models.DateTimeField(default=None, blank=False, null=False)
response_date = models.DateTimeField(default=None, blank=False, null=False)
def __str__(self):
return f'{self.game_session}:'
After I added this code, I started to get the circular import issues:
games/helpers.py
from .models import GameInvite, GameSession
from apps.westmarches.profiles.models import Profile
def create_game_session(self, start_datetime, end_time, requester, officiant):
game_session = GameSession.objects.create(start_datetime, end_time, requester, officiant)
game_invite = self.create_game_invite(game_session, requester, officiant)
self.send_invitation(game_invite)
return game_session
def create_game_invite(game_session: GameSession, recipient: Profile, sender: Profile=None):
actual_sender = sender or game_session.requester
new_game_invite = GameInvite.objects.create(
status='composed',
sender=actual_sender,
game_session=game_session,
recipient=recipient
)
game_session.game_invites.add(new_game_invite)
game_session.save()
return new_game_invite
def process_game_invites(game_session: GameSession):
for game_invite in get_unsent_invites(game_session):
send_invitation(game_invite)
def send_invitation(game_invite: GameInvite, recipient: Profile=None):
receiver = recipient or Profile.objects.get(pk=game_invite.recipient.pk)
receiver.game_invites.add(game_invite)
receiver.save()
game_invite.status = 'delivered'
game_invite.save()
def get_unsent_invites(game_session: GameSession):
return GameSession.objects.get(pk=game_session.pk).filter(status='composed')
It’s clear that the top level imports in helpers.py are the issue. At the top of games/models.py we have the import for apps.westmarches.profiles.models.Profile, so when I import Profile as well as GameSession and GameInvite in the helpers, it’s obviously the cause.
I know I can import the modules needed on a per function bases under helpers.py, or that I can remove those imports on the models themselves and replace my orm definitions like fk’s and m2m’s, etc… using the module and class names as single quoted strings, but both of those feel like workarounds that are missing a larger more important point about project organization.
What are my options for refactoring or reorganizing this code to address the issue?
One thought was that I should have a core app somewhere that contains all the helpers, and it would make sense for that to be under gameskedge/apps/westmarches/core but that won’t do anything to alleviate the circular import issue. I’ll still have it, just coming from core/helpers.py instead of games/helpers.py
At the moment, these helpers are all focused entirely on implementing business logic, so I think helpers.py or utils.py is the right place for them. But the actual business logic they’re providing is tied to creating a GameSession and updating/sending invitations to other players. Based on what they’re doing, maybe they’re not helpers, and I’m missing something in terms or best practices/favored patterns for creating and configuring model objects after initial creation.
Any help is welcome at this point. I’m mainly looking to understand what I may be missing in terms of a better way to approach this as well as heading off design decisions that may become impactful at a point down the road when fixing them requires a lot more effort.