Mike Slinn
Mike Slinn

Django Apps, AppConfig, OscarConfig and Sub-Apps

Published 2021-02-24.
Time to read: 3 minutes.

This page is part of the django collection, categorized under Django, Django-Oscar.

Let’s start off today’s exploration of Django and django-oscar by learning about Django apps.

Django contains a registry of installed applications that stores configuration and provides introspection. This registry is called apps, and it’s available in django.apps:
>>> from django.apps import apps
>>> apps.get_app_config('admin').verbose_name
'Administration' 

To replicate the interactive session shown in the Django documentation, we can launch a Python interactive shell by using the shell subcommand of the Frobshop manage.py. Regular readers of this blog will recognize Frobshop as the django-oscar tutorial e-commerce site that I’ve been working through.

Shell
(aw) $ ./manage.py shell
Python 3.8.6 (default, Sep 25 2020, 09:36:53)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.apps import apps

>>> apps
<django.apps.registry.Apps object at 0x7ffb21210340>

>>> type(apps)
<class 'django.apps.registry.Apps'>

>>> admin = apps.get_app_config('admin')

>>> admin.verbose_name
'Administration' 

Notice that django.apps has type <class 'django.apps.registry.Apps'> and resides at memory address 0x7ffb21210340.

While we are here, we might as well look around. Tab completion is a wonderful thing; it is a good way to discover properties and methods of Python classes and objects.

>>> admin.tab
admin.apps            admin.import_models(  admin.name
admin.create(         admin.label           admin.path
admin.default_site    admin.models          admin.ready(
admin.get_model(      admin.models_module   admin.verbose_name
admin.get_models(     admin.module

>>> admin.apps
<django.apps.registry.Apps object at 0x7f8da050f9a0>

>>> admin.apps.tab
admin.apps.all_models
admin.apps.app_configs
admin.apps.apps_ready
admin.apps.check_apps_ready(
admin.apps.check_models_ready(
admin.apps.clear_cache(
admin.apps.do_pending_operations(
admin.apps.get_app_config(
admin.apps.get_app_configs(
admin.apps.get_containing_app_config(
admin.apps.get_model(
admin.apps.get_models(
admin.apps.get_registered_model(
admin.apps.get_swappable_settings_name(
admin.apps.is_installed(
admin.apps.lazy_model_operation(
admin.apps.loading
admin.apps.models_ready
admin.apps.populate(
admin.apps.ready
admin.apps.ready_event
admin.apps.register_model(
admin.apps.set_available_apps(
admin.apps.set_installed_apps(
admin.apps.stored_app_configs
admin.apps.unset_available_apps(
admin.apps.unset_installed_apps( 
😁

But wait, there is more!

>>> admin.default_site
'django.contrib.admin.sites.AdminSite' 

>>> admin.label
'admin'

>>> admin.models
{'logentry': <class 'django.contrib.admin.models.LogEntry'>}

>>> admin.models_module
<module 'django.contrib.admin.models' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/models.py'>

>>> admin.module
<module 'django.contrib.admin' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/__init__.py'>

>>> admin.name
'django.contrib.admin'

>>> admin.path
>>> '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin' 
😁

django-oscar AppConfigs

We can display a list of all the default django-oscar AppConfig subclasses:

>>> configs = apps.get_app_configs()

>>> configs
dict_values([<AdminConfig: admin>, <AuthConfig: auth>, <ContentTypesConfig: contenttypes>, <SessionsConfig: sessions>, <MessagesConfig: messages>, <StaticFilesConfig: staticfiles>, <SitesConfig: sites>, <FlatPagesConfig: flatpages>, <Shop: oscar>, <AnalyticsConfig: analytics>, <CheckoutConfig: checkout>, <AddressConfig: address>, <ShippingConfig: shipping>, <CatalogueConfig: catalogue>, <CatalogueReviewsConfig: reviews>, <CommunicationConfig: communication>, <PartnerConfig: partner>, <BasketConfig: basket>, <PaymentConfig: payment>, <OfferConfig: offer>, <OrderConfig: order>, <CustomerConfig: customer>, <SearchConfig: search>, <VoucherConfig: voucher>, <WishlistsConfig: wishlists>, <DashboardConfig: dashboard>, <ReportsDashboardConfig: reports_dashboard>, <UsersDashboardConfig: users_dashboard>, <OrdersDashboardConfig: orders_dashboard>, <CatalogueDashboardConfig: catalogue_dashboard>, <OffersDashboardConfig: offers_dashboard>, <PartnersDashboardConfig: partners_dashboard>, <PagesDashboardConfig: pages_dashboard>, <RangesDashboardConfig: ranges_dashboard>, <ReviewsDashboardConfig: reviews_dashboard>, <VouchersDashboardConfig: vouchers_dashboard>, <CommunicationsDashboardConfig: communications_dashboard>, <ShippingDashboardConfig: shipping_dashboard>, <AppConfig: widget_tweaks>, <HaystackConfig: haystack>, <AppConfig: treebeard>, <AppConfig: thumbnail>, <AppConfig: django_tables2>, <DebugToolbarConfig: debug_toolbar>]) 

Let’s dissect configs, which is of type dict_values. It would be useful to obtain a list of all 44 django-oscar AppConfig labels.

>>> labels = [c.label for c in configs]

>>> print(", ".join(labels))
admin, auth, contenttypes, sessions, messages, staticfiles, sites, flatpages, oscar, analytics, checkout, address, shipping, catalogue, reviews, communication, partner, basket, payment, offer, order, customer, search, voucher, wishlists, dashboard, reports_dashboard, users_dashboard, orders_dashboard, catalogue_dashboard, offers_dashboard, partners_dashboard, pages_dashboard, ranges_dashboard, reviews_dashboard, vouchers_dashboard, communications_dashboard, shipping_dashboard, widget_tweaks, haystack, treebeard, thumbnail, django_tables2, debug_toolbar

>>> len(labels)
44 

It would also be useful to see a list of all 13 django-oscar AppConfig labels that contain the word dashboard.

>>> dashboard_labels = sorted([label for label in labels if "dashboard" in label])

>>> print(", ".join(dashboard_labels))
catalogue_dashboard, communications_dashboard, dashboard, offers_dashboard, orders_dashboard, pages_dashboard, partners_dashboard, ranges_dashboard, reports_dashboard, reviews_dashboard, shipping_dashboard, users_dashboard, vouchers_dashboard

>>> len(dashboard_labels)
13 

Finally, it would be useful to see a list of all django-oscar AppConfig labels and the corresponding names. I’ve made the labels bold to make reading easier. The dashboard app names are of particular interest, so I highlighted them.

>>> configs = list(apps.get_app_configs())

>>> label_name_tuples = sorted([(c.label, c.name) for c in configs], key=lambda x: x[0])

>>> [print(f'<b>{label}</b>: {name}')
  for (label, name) in label_name_tuples]
address: oscar.apps.address
admin: django.contrib.admin
analytics: oscar.apps.analytics
auth: django.contrib.auth
basket: oscar.apps.basket
catalogue: oscar.apps.catalogue
catalogue_dashboard: oscar.apps.dashboard.catalogue
checkout: oscar.apps.checkout
communication: oscar.apps.communication
communications_dashboard: oscar.apps.dashboard.communications
contenttypes: django.contrib.contenttypes
customer: oscar.apps.customer
dashboard: oscar.apps.dashboard
debug_toolbar: debug_toolbar
django_tables2: django_tables2
flatpages: django.contrib.flatpages
haystack: haystack
messages: django.contrib.messages
offer: oscar.apps.offer
offers_dashboard: oscar.apps.dashboard.offers
order: oscar.apps.order
orders_dashboard: oscar.apps.dashboard.orders
oscar: oscar
pages_dashboard: oscar.apps.dashboard.pages
partner: oscar.apps.partner
partners_dashboard: oscar.apps.dashboard.partners
payment: oscar.apps.payment
ranges_dashboard: oscar.apps.dashboard.ranges
reports_dashboard: oscar.apps.dashboard.reports
reviews: oscar.apps.catalogue.reviews
reviews_dashboard: oscar.apps.dashboard.reviews
search: oscar.apps.search
sessions: django.contrib.sessions
shipping: oscar.apps.shipping
shipping_dashboard: oscar.apps.dashboard.shipping
sites: django.contrib.sites
staticfiles: django.contrib.staticfiles
thumbnail: sorl.thumbnail
treebeard: treebeard
users_dashboard: oscar.apps.dashboard.users
voucher: oscar.apps.voucher
vouchers_dashboard: oscar.apps.dashboard.vouchers
widget_tweaks: widget_tweaks
wishlists: oscar.apps.wishlists
[None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None] 

Let’s take a closer look at the first AppConfig subclass, AdminConfig. I’ve made its property names bold to make reading easier.

>>> config = list(apps.get_app_configs())[0]

>>> type(config)
<class 'django.contrib.admin.apps.AdminConfig'>

>>> dict = config.__dict__

>>> [print(f"{key}={dict[key]}") for key in dict]
name=django.contrib.admin
module=<module 'django.contrib.admin' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/__init__.py'>
apps=<django.apps.registry.Apps object at 0x7f8da050f9a0>
label=admin
path=/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin
models_module=<module 'django.contrib.admin.models' from '/var/work/django/oscar/lib/python3.8/site-packages/django/contrib/admin/models.py'>
models={'logentry': <class 'django.contrib.admin.models.LogEntry'>}
[None, None, None, None, None, None, None] 

The CatalogConfig app

The source code for CatalogConfig is located within the django-oscar GitHub project at django-oscar/src/oscar/apps/catalogue/apps.py, and just consists of a Python class definition with a docstring:

class CatalogueConfig(CatalogueOnlyConfig, CatalogueReviewsOnlyConfig):
    """
    Composite class combining Products with Reviews
    """

The remainder of the file (apps.py) consists of the source for the CatalogueOnlyConfig and CatalogueReviewsOnlyConfig classes. Both of those classes subclass OscarConfig, and only have 2 methods: get_urls() and ready(), which is a Django AppConfig method.

OscarConfig

OscarConfig subclasses AppConfig and mixes in OscarConfigMixin:

class OscarConfig(OscarConfigMixin, AppConfig):
    """
    Base Oscar app configuration.
    This is subclassed by each app to provide a customisable container for its
    configuration, URL configurations, and permissions.
    """

The django-oscar docs describe OscarConfig this way:

Oscar’s apps organise their URLs and associated views using a “OscarConfig” class instance. This works in a similar way to Django’s admin app, and allows Oscar projects to subclass and customised URLs and views.

The above is a terrible explanation. OscarConfig should be used instead of django.apps.AppConfig as the parent of the autogenerated Config classes that result from manage.py startapp. The source code has a comment that says OscarConfig extends AppConfig to also provide URL configurations and permissions; this is explained here.

For example, this was generated for one of my apps:

myWebApp/myApp/apps.py
from django.apps import AppConfig

class CoreConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'core'

This is the same code, but subclasses OscarConfig

myWebApp/myApp/apps.py
from oscar.core.application import OscarConfig

class CoreConfig(OscarConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'core'

ready()

The Django docs describe the ready() method like this:

Subclasses can override this method to perform initialization tasks such as registering signals. It is called as soon as the registry is fully populated.

Although you can’t import models at the module-level where AppConfig classes are defined, you can import them in ready(), using either an import statement or get_model().

If you’re registering model signals, you can refer to the sender by its string label instead of using the model class itself.

However, django-oscar AppConfig subclasses usually set view URLs in ready().

Psst: I talk about ready() in this post, where I trace through the django-oscar startup sequence.

Sub-Apps

The Django documentation does not use the term sub-app, and it is rather vague on the subject, however the data structures are unambiguous. Let’s look at oscar.apps.catalogue.apps – it is different from the django.apps we looked at in detail above because it contains CatalogConfig’s sub-apps (it is an empty container because CatalogConfig has no sub-apps).

Shell
(aw) $ ./manage.py shell
Python 3.8.6 (default, Sep 25 2020, 09:36:53)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> oscar.apps.catalogue.apps as apps

>>> apps
<module 'oscar.apps.catalogue.apps' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/catalogue/apps.py'>

>>> type(apps)
<class 'module'>

>>> apps.tab
apps.CatalogueConfig(             apps.get_class(
apps.CatalogueOnlyConfig(         apps.include(
apps.CatalogueReviewsOnlyConfig(  apps.path(
apps.OscarConfig(                 apps.re_path(
apps.apps 

Notice that oscar.apps.catalogue.apps.apps is a back-reference to django.apps which we saw at the beginning of the blog post, with the same memory address (0x7ffb21210340) and the same type (<class 'django.apps.registry.Apps'>).

>>> apps.apps
<django.apps.registry.Apps object at 0x7ffb21210340>

>>> type(apps.apps)
<class 'django.apps.registry.Apps'> 

Dashboard Sub-Apps

Exploring with the REPL

As we have seen, the django-oscar Dashboard app has 12 sub-apps. Messing with the import statement allows us to walk through the sub-apps easily.

>>> import oscar.apps.dashboard as d
>>> d.tab
d.apps                d.orders              d.shipping
d.catalogue           d.pages               d.tables
d.communications      d.partners            d.users
d.default_app_config  d.ranges              d.views
d.models              d.reports             d.vouchers
d.offers              d.reviews             d.widgets

>>> d.apps
>>> <module 'oscar.apps.dashboard.apps' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/dashboard/apps.py'>

>>>  type(d.apps)
<class 'module'> 

Let’s look at the dashboard.catalogue sub-app:

>>> d.catalogue
<module 'oscar.apps.dashboard.catalogue' from '/var/work/django/oscar/lib/python3.8/site-packages/oscar/apps/dashboard/catalogue/__init__.py'>

>>> d.catalogue.tab
d.catalogue.apps                d.catalogue.models
d.catalogue.default_app_config  d.catalogue.tables
d.catalogue.forms               d.catalogue.views
d.catalogue.formsets            d.catalogue.widgets
d.catalogue.mixins 

Examining the Source Code

The source code for DashboardConfig shows the dashboard sub-apps clearly:

def ready(self):
    self.index_view = get_class('dashboard.views', 'IndexView')
    self.login_view = get_class('dashboard.views', 'LoginView')

    self.catalogue_app = apps.get_app_config('catalogue_dashboard')
    self.reports_app = apps.get_app_config('reports_dashboard')
    self.orders_app = apps.get_app_config('orders_dashboard')
    self.users_app = apps.get_app_config('users_dashboard')
    self.pages_app = apps.get_app_config('pages_dashboard')
    self.partners_app = apps.get_app_config('partners_dashboard')
    self.offers_app = apps.get_app_config('offers_dashboard')
    self.ranges_app = apps.get_app_config('ranges_dashboard')
    self.reviews_app = apps.get_app_config('reviews_dashboard')
    self.vouchers_app = apps.get_app_config('vouchers_dashboard')
    self.comms_app = apps.get_app_config('communications_dashboard')
    self.shipping_app = apps.get_app_config('shipping_dashboard')

The source code for all the dashboard apps has 12 subdirectories, one for each sub-app, as expected. The catalogue sub-app contains all the files one would expect in a Django app:

Shell
$ cd src/oscar/apps/dashboard/catalogue/

$ ls
__init__.py  apps.py  forms.py  formsets.py  mixins.py  models.py
tables.py  views.py  widgets.py 

Django App Loading and Initialization

Django uses dynamic discovery and loading. Read about app initialization.

Making A Sub-App

According to the Django documentation, the syntax for creating a sub-app is:

Shell
(aw) $ ./manage.py startapp name [directory]

Here is how to create a sub-app within my_webapp/my_app

Shell
(aw) $ mkdir -p ~/Code/my_webapp/my_app/my_sub_app

(aw) $ ./manage.py startapp my_sub_app ~/Code/my_webapp/my_app/my_sub_app