title: Circular module dependencies in Python/Django with split-up models
body: <p>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:</p>
  
  <p><code>elearning/tasks.py</code></p>
  
  <pre><code>from celery.task import task
  
  @task
  def decompress(pk):
      from elearning.models import Elearning
      Elearning.objects.get(pk=pk).decompress()
  </code></pre>
  
  <p><code>elearning/models.py</code></p>
  
  <pre><code>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)
  </code></pre>
  
  <p><code>ecom/models/products.py</code></p>
  
  <pre><code>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)
  </code></pre>
  
  <p><code>ecom/models/__init__.py</code></p>
  
  <pre><code>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
  </code></pre>
  
  <p><code>competency/models.py</code></p>
  
  <pre><code>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)
  </code></pre>
  
  <p><code>ecom/models/permissions.py</code></p>
  
  <pre><code>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)
  </code></pre>
  
  <p><code>course/models.py</code></p>
  
  <pre><code>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)
  </code></pre>
  
  <p>From what I can see, there's no explicit circular recursion here, save for the required references in <code>__init__.py</code> which appears to be where things are blowing up in my code.  Here's the traceback:</p>
  
  <pre><code>  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 &lt;module&gt;
      from competency.models import CompetencyProduct
    File "/path/to/project/django/myproj/competency/models.py", line 5, in &lt;module&gt;
      from ecom.models import Product
    File "/path/to/project/django/myproj/ecom/models/__init__.py", line 5, in &lt;module&gt;
      from ecom.models.permissions import Permission
    File "/path/to/project/django/myproj/ecom/models/permissions.py", line 8, in &lt;module&gt;
      from course.models import Course
    File "/path/to/project/django/myproj/course/models.py", line 4, in &lt;module&gt;
      from competency.models import CompetencyProduct
  ImportError: cannot import name CompetencyProduct
  </code></pre>
  
  <p>All I'm trying to do here is import that <code>Elearning</code> model, which is a subclass of <code>CompetencyProduct</code>, and in turn, <code>Product</code>.  However, because <code>Product</code> comes from a break-up of the larger <code>ecom/models.py</code>, the <code>ecom/__init__.py</code> file contains the obligatory import of all of the broken-out models, including <code>Permission</code> which has to import <code>Course</code> which requires <code>CompetencyProduct</code>.</p>
  
  <p>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.</p>
  
  <p>Is my only option here to remove <code>Permission</code> from the <code>ecom</code> 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.</p>