Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Circular module dependencies in Python/Django with split-up models

Tags:

python

django

I understand how to break up models, and I understand why circular module dependencies blow things up, but I've run across a problem where breaking up a model into separate files appears to be causing circular dependencies. Here's an exerpt from the code, and I'll follow it up with the traceback from the failing process:

elearning/tasks.py

from celery.task import task

@task
def decompress(pk):
    from elearning.models import Elearning
    Elearning.objects.get(pk=pk).decompress()

elearning/models.py

from competency.models import CompetencyProduct
from core.helpers import ugc_elearning
from elearning.fields import ArchiveFileField

class Elearning(CompetencyProduct):

    archive = ArchiveFileField(upload_to=ugc_elearning)

    def decompress(self):

        import zipfile

        src = self.archive.path
        dst = src.replace(".zip","")

        print "Decompressing %s to %s" % (src, dst)

        zipfile.ZipFile(src).extractall(dst)

ecom/models/products.py

from django.db import models
from django.utils.translation import ugettext_lazy as _

from core.models import Slugable, Unique
from django_factory.models import Factory
from core.helpers import ugc_photos

class Product(Slugable, Unique, Factory):

    photo          = models.ImageField(upload_to=ugc_photos, width_field="photo_width", height_field="photo_height", blank=True)
    photo_width    = models.PositiveIntegerField(blank=True, null=True, default=0)
    photo_height   = models.PositiveIntegerField(blank=True, null=True, default=0)
    description    = models.TextField()
    price          = models.DecimalField(max_digits=16, decimal_places=2)
    created        = models.DateTimeField(auto_now_add=True)
    modified       = models.DateTimeField(auto_now=True)

ecom/models/__init__.py

from django.contrib.auth.models import User
from django.db import models

from ecom.models.products import Product, Credit, Subscription
from ecom.models.permissions import Permission
from ecom.models.transactions import Transaction, DebitTransaction, CreditTransaction, AwardTransaction, FinancialTransaction, PermissionTransaction, BundleTransaction

competency/models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _

from core.models import Slugable, Unique
from ecom.models import Product
from rating.models import Rated
from trainer.models import Trainer

class Competency(Slugable, Unique):

    class Meta:
        verbose_name = _("Competency")
        verbose_name_plural = _("Competencies")

    description = models.TextField()



class CompetencyProduct(Product, Rated):

    class Meta:
        verbose_name = _("Product")
        verbose_name_plural = _("Products")

    release  = models.DateField(auto_now_add=True)

    trainers     = models.ManyToManyField(Trainer)
    competencies = models.ManyToManyField(Competency, related_name="provided_by")
    requirements = models.ManyToManyField(Competency, related_name="required_for", blank=True, null=True)
    forsale      = models.BooleanField("For Sale", default=True)

ecom/models/permissions.py

from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import ugettext_lazy as _

from treebeard.mp_tree import MP_Node

from collective.models import Collective
from course.models import Course
from ecom.models.products import Product

class Permission(MP_Node):

    class Meta:
        app_label = "ecom"

    product      = models.ForeignKey(Product, related_name="permissions")
    user         = models.ForeignKey(User, related_name="permissions")
    collective   = models.ForeignKey(Collective, null=True)
    course       = models.ForeignKey(Course, null=True)
    redistribute = models.BooleanField(default=False)
    created      = models.DateTimeField(auto_now_add=True)
    modified     = models.DateTimeField(auto_now=True)
    accessed     = models.DateTimeField(auto_now=True)

course/models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _

from competency.models import CompetencyProduct
from ecom.models import Product
from rating.models import Rated

class Chapter(models.Model):
    seq  = models.PositiveIntegerField(name="Sequence", help_text="Chapter number")
    name = models.CharField(max_length=128)
    note = models.CharField(max_length=128)



class Course(Product, Rated):

    level    = models.PositiveIntegerField(choices=CompetencyProduct.LEVELS)
    chapters = models.ManyToManyField(Chapter)



class Bundle(models.Model):

    class Meta:
        unique_together = (("product", "chapter"),)

    product = models.ForeignKey(Product, related_name="bundles")
    chapter = models.ForeignKey(Chapter, related_name="bundles")
    amount  = models.PositiveIntegerField()
    seq     = models.PositiveIntegerField(name="Sequence", default=1)

From what I can see, there's no explicit circular recursion here, save for the required references in __init__.py which appears to be where things are blowing up in my code. Here's the traceback:

  File "/path/to/project/virtualenv/lib/python2.6/site-packages/celery/execute/trace.py", line 47, in trace
    return cls(states.SUCCESS, retval=fun(*args, **kwargs))
  File "/path/to/project/virtualenv/lib/python2.6/site-packages/celery/app/task/__init__.py", line 247, in __call__
    return self.run(*args, **kwargs)
  File "/path/to/project/virtualenv/lib/python2.6/site-packages/celery/app/__init__.py", line 175, in run
    return fun(*args, **kwargs)
  File "/path/to/project/django/myproj/elearning/tasks.py", line 5, in decompress
    from elearning.models import Elearning
  File "/path/to/project/django/myproj/elearning/models.py", line 2, in <module>
    from competency.models import CompetencyProduct
  File "/path/to/project/django/myproj/competency/models.py", line 5, in <module>
    from ecom.models import Product
  File "/path/to/project/django/myproj/ecom/models/__init__.py", line 5, in <module>
    from ecom.models.permissions import Permission
  File "/path/to/project/django/myproj/ecom/models/permissions.py", line 8, in <module>
    from course.models import Course
  File "/path/to/project/django/myproj/course/models.py", line 4, in <module>
    from competency.models import CompetencyProduct
ImportError: cannot import name CompetencyProduct

All I'm trying to do here is import that Elearning model, which is a subclass of CompetencyProduct, and in turn, Product. However, because Product comes from a break-up of the larger ecom/models.py, the ecom/__init__.py file contains the obligatory import of all of the broken-out models, including Permission which has to import Course which requires CompetencyProduct.

The wacky thing is that the whole site works pefectly. Logins, purchases, everything. This problem only occurs when I'm trying to run celery in the background and a new task is loaded or I try to run a shell script using the Django environment.

Is my only option here to remove Permission from the ecom app, or there a better, smarter way to handle this? Additionally, any comments on how I've laid out the project in general are appreciated.

like image 827
Daniel Quinn Avatar asked Nov 07 '11 15:11

Daniel Quinn


People also ask

What is circular dependency in Django?

Circular dependencies are imports in different Python modules from each other. You should never cross-import from the different models.py files, because that causes serious stability issues. Instead, if you have interdependencies, you should use the actions described in this recipe.

How do you fix a circular import error in Python?

If the error occurs due to a circular dependency, it can be resolved by moving the imported classes to a third file and importing them from this file. If the error occurs due to a misspelled name, the name of the class in the Python file should be verified and corrected.

What is circular import in Python?

Python Circular Imports A circular import occurs when two or more modules depend on each other. In this example, m2.py depends on m1.py and m1.py depends on m2.py . module dependency (Created by. Xiaoxu Gao. ) # m1.py.


1 Answers

Your problem is that Permission imports Product, but both are imported in ecom/models/__init__.py. You should find a way to either have these two models in the same file or separate them into two apps.

like image 150
Chris Pratt Avatar answered Sep 28 '22 13:09

Chris Pratt