Circular import when using get_user_model() inside a custom BaseUserManager

Hi everyone!

I have been creating a sample Saas and wanted to organize my project from the start in a neat & tidy way. so I decided to move the apps I create into apps directory in the following way:

D:.
ª   uv.lock
ª           
+---.vscode
ª       settings.json
ª       
+---apps
ª   ª   __init__.py
ª   ª   
ª   +---accounts
ª   ª   ª   admin.py
ª   ª   ª   apps.py
ª   ª   ª   forms.py
ª   ª   ª   manager.py
ª   ª   ª   models.py
ª   ª   ª   tasks.py
ª   ª   ª   tests.py
ª   ª   ª   theme.py
ª   ª   ª   urls.py
ª   ª   ª   views.py
ª   ª   ª   __init__.py
ª   ª   ª   
ª   ª   +---migrations
ª   ª   ª       0001_initial.py
ª   ª   ª       0002_remove_user_username.py
ª   ª   ª       0003_user_avatar_user_preferences.py
ª   ª   ª       0004_alter_user_preferences.py
ª   ª   ª       __init__.py
ª   ª   ª       
ª   ª           
ª   +---billing
ª   ª   ª   admin.py
ª   ª   ª   apps.py
ª   ª   ª   mixins.py
ª   ª   ª   models.py
ª   ª   ª   services.py
ª   ª   ª   signals.py
ª   ª   ª   tasks.py
ª   ª   ª   tests.py
ª   ª   ª   urls.py
ª   ª   ª   views.py
ª   ª   ª   webhooks.py
ª   ª   ª   __init__.py
ª   ª   ª   
ª   ª   +---migrations
ª   ª   ª       0001_initial.py
ª   ª   ª       0002_alter_subscription_options.py
ª   ª   ª       0003_subscription_screenshots_used.py
ª   ª   ª       0004_rename_price_cents_plan_price_usd.py
ª   ª   ª       0005_plan_plan_desc_plan_plan_features.py
ª   ª   ª       __init__.py
ª   ª           
ª   +---cards
ª   ª   ª   admin.py
ª   ª   ª   apps.py
ª   ª   ª   browser.py
ª   ª   ª   extractor.py
ª   ª   ª   models.py
ª   ª   ª   tasks.py
ª   ª   ª   tests.py
ª   ª   ª   urls.py
ª   ª   ª   views.py
ª   ª   ª   __init__.py
ª   ª   ª   
ª   ª   +---migrations
ª   ª   ª       0001_initial.py
ª   ª   ª       0002_card_platform_alter_card_image.py
ª   ª   ª       0003_platform_card_completed_at_card_error_message_and_more.py
ª   ª   ª       0004_alter_card_platform.py
ª   ª   ª       0005_card_post_datetime.py
ª   ª   ª       0006_alter_card_post_datetime.py
ª   ª   ª       0007_platform_extractor_key.py
ª   ª   ª       __init__.py
ª   ª   
ª   +---dashboard
ª   ª   ª   admin.py
ª   ª   ª   apps.py
ª   ª   ª   forms.py
ª   ª   ª   models.py
ª   ª   ª   tests.py
ª   ª   ª   urls.py
ª   ª   ª   views.py
ª   ª   ª   __init__.py
ª   ª   ª   
ª   ª   +---migrations
ª   ª   ª       __init__.py
ª   ª           
ª   +---landing
ª   ª   ª   admin.py
ª   ª   ª   apps.py
ª   ª   ª   models.py
ª   ª   ª   tests.py
ª   ª   ª   urls.py
ª   ª   ª   views.py
ª   ª   ª   __init__.py
ª   ª   ª   
ª   ª   +---migrations
ª   ª   ª       __init__.py
ª   ª   ª       
ª           
+---config
ª   ª   asgi.py
ª   ª   Celery.py
ª   ª   context_processors.py
ª   ª   settings.py
ª   ª   test_runner.py
ª   ª   urls.py
ª   ª   wsgi.py
ª   ª   __init__.py
ª   
+---logs
+---media
+---static
+---templates                

Now this led to some complications which I have worked my way around.

Now I was creating a new UserManager instead of the default one, so I did the following

# apps/accounts/manager.py

from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth import get_user_model


User = get_user_model()

class UserManager(BaseUserManager):

    def normalize_email(cls, email):
        """
        Normalize the email address by lowercasing the domain part of it.
        """
        email = email or ""
        try:
            email_name, domain_part = email.strip().rsplit("@", 1)
        except ValueError:
            pass
        else:
            email = email_name.lower() + "@" + domain_part.lower()
        return email

    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("The given email must be set")
        email = self.normalize_email(email)
        user = User(email=email, **extra_fields)
        user.password = make_password(password)
        return user

    def create_superuser(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        if extra_fields.get("is_staff") is not True:
            raise ValueError("Superuser must have is_staff=True.")
        if extra_fields.get("is_superuser") is not True:
            raise ValueError("Superuser must have is_superuser=True.")

        return self.create_user(email, password, **extra_fields)

which introduced circular dependency and raised a strange error @ AUTH_USER_MODEL.

# settings.py

INSTALLED_APPS = [
    ...
    'apps.landing',
    'apps.dashboard',
    'apps.accounts',
    'apps.cards',
    'apps.billing'
]

...

AUTH_USER_MODEL = 'accounts.User'

this raises the following error:

django.core.exceptions.ImproperlyConfigured: AUTH_USER_MODEL refers to model 'accounts.User' that has not been installed

of course I tried

AUTH_USER_MODEL = 'apps.accounts.User'

which raises

ValueError: Invalid model reference 'apps.accounts.User'. String model references must be of the form 'app_label.ModelName'.

I don’t understand how get_user_model could mess up all these, and how to fix this problem?
Thanks for the help.

It looks like you have an apps.py in your accounts app. What is the label set to on the AppConfig?

Because your User model probably import its manager. I guess you have in models.py, something like:

from .manager import UserManager

class User(...):
    objects = UserManager()

When the models.py module is imported (which is done when resolving setting AUTH_USER_MODEL), it begins by importing the manager.py module (as per the import line), which calls get_user_model() which imports apps.accounts.models.py, hence the circular import.

In your manager, you do not need to explicitly use the User class returned by get_user_model. The manager already has a reference to the model using self.model. So, by replacing the use of User by self.model and removing call to get_user_model, it should avoid the circular import

None, only set the name into ‘apps.accounts’

It worked, thanks for your help, I totally missed the self.model attribute.