Why apps have to have unique names? [related with subapps and name collision]

167 views
Skip to first unread message

Jorge C. Leitão

unread,
Jun 3, 2013, 4:58:59 AM6/3/13
to django-d...@googlegroups.com
Hi.

The motivation to this question is that if I want to implement "copies" of an app within an website (for instance, a stackoverflow-like where each app is a Q&A for its own, but with modifications on templates, urls, etc.), as far as I'm understanding: (1) each "copy" has to change its name, names of all calls of templates, views, etc due to name collision and (2) each copy must be an app on its own without any dependencies on other apps with models/templates/urls, because otherwise the dependent apps would also have to be "copied", names changed, etc.

In a previous post I suggested generalizing templates and url search to be used within subapps (apps with folder inside another app), which I found was a way of allowing what I wanted to do.
There was a criticism that during the discussion that I (mostly) ignored, but while re-reading it, I understood its meaning and its importance to a discussion by itself.

The criticism was from Shai Berger, that correctly pointed out that Django uses unique app names [1], and what I didn't understood at the time was that this forbids any possibility of using subapps the way I was suggesting or in any way that might help.

Motivated by that criticism, I want to ask: why apps have to have unique names?

To try to answer this question on the code, I did a simple search on Django code. This is far from rigorous, but gives some nice results. The search was simple: regex of r"settings.INSTALLED_APP" and regex of r".split('.')[-2]". Afterwards, I read the code to understand which dependencies it has on the labeling. Here are the results:

## Usage of settings.INSTALLED_APP (regex: settings.INSTALLED_APP)
# To check admin existence, uses full path (import_module)
/django/contrib/admin/__init__.py:24:     for app in settings.INSTALLED_APPS:
# To check specific app existence, uses full path (import_module)
/django/contrib/admin/templatetags/admin_static.py:6: if 'django.contrib.staticfiles' in settings.INSTALLED_APPS:
/django/contrib/comments/__init__.py:16:     if comments_app not in settings.INSTALLED_APPS:
/django/contrib/gis/tests/__init__.py:113:         settings.INSTALLED_APPS = list(self.old_installed) + new_installed
/django/contrib/gis/tests/__init__.py:125:         settings.INSTALLED_APPS = self.old_installed
/django/contrib/messages/tests/base.py:16:         'django.contrib.auth' not in settings.INSTALLED_APPS,
/django/contrib/messages/tests/base.py:214:             lambda app:app!='django.contrib.messages', settings.INSTALLED_APPS),
/django/contrib/messages/tests/base.py:239:             lambda app:app!='django.contrib.messages', settings.INSTALLED_APPS),
/django/contrib/sitemaps/tests/flatpages.py:8:     @skipUnless("django.contrib.flatpages" in settings.INSTALLED_APPS,
/django/contrib/sitemaps/tests/http.py:88:     @skipUnless("django.contrib.sites" in settings.INSTALLED_APPS,
# for finding static; use full path of the app
/django/contrib/staticfiles/finders.py:122:             apps = settings.INSTALLED_APPS
# for binding commands to the management. Requires unique app to avoid commands collision.
/django/core/management/__init__.py:101:             apps = settings.INSTALLED_APPS
# for binding commands to the management. Requires unique app to avoid commands collision.
/django/core/management/__init__.py:319:                     options += [(a.split('.')[-1], 0) for a in settings.INSTALLED_APPS]
# for importing the 'management' module within each installed app, to register dispatcher events.
/django/core/management/commands/flush.py:35:         for app_name in settings.INSTALLED_APPS:
# for importing the 'management' module within each installed app, to register dispatcher events (lacks DRY principle: command used is the same as previous file.)
/django/core/management/commands/syncdb.py:38:         for app_name in settings.INSTALLED_APPS:
# for the 'models' of the db. It uses django.utils.importlib.import_module on the full app's path. Class uses _label_for for defining a "name" for the app, which requires unique app
/django/db/models/loading.py:61:             for app_name in settings.INSTALLED_APPS:
/django/db/models/loading.py:143:             for app_name in settings.INSTALLED_APPS:
# To check whether the model is installed. It uses full paths of the package
/django/db/models/options.py:71:         self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
# To find templatetags. It stores the full path of the module
/django/template/base.py:1271:         for app_module in ['django'] + list(settings.INSTALLED_APPS):
# To find templates. It stores the full path of the module
/django/template/loaders/app_directories.py:19: for app in settings.INSTALLED_APPS:
# To find templates. Depends on the pkg_resources.resource_string to tell wether it returns app name of full app name
/django/template/loaders/eggs.py:23:             for app in settings.INSTALLED_APPS:
# To check specific app existence, uses full path (import_module)
/django/test/client.py:353:         if 'django.contrib.sessions' in settings.INSTALLED_APPS:
/django/test/client.py:501:                 and 'django.contrib.sessions' in settings.INSTALLED_APPS:
# for finding "locale/". Why it uses reverse of settings.INSTALLED_APPS ? Uses full app (import_module) path
/django/utils/translation/trans_real.py:159:         for appname in reversed(settings.INSTALLED_APPS):
# for building the debug page. Not relevant.
/django/views/debug.py:745: {{ settings.INSTALLED_APPS|pprint }}
/django/views/debug.py:936: {{ settings.INSTALLED_APPS|pprint }}
# for translating javascript. Uses full path (import_module)
/django/views/i18n.py:189:     packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS]

## usage of app by app.__name__.split('.')[-2] (regex: split('.')[-2])
# deprecated
/django/core/management/commands/reset.py:31:         app_name = app.__name__.split('.')[-2]
# for listing apps and models for syncdb. It requires unique app name.
/django/core/management/commands/syncdb.py:67:             (app.__name__.split('.')[-2],
# Only used for printing in case verbosity >= 2
/django/core/management/sql.py:184:         app_name = app.__name__.split('.')[-2]
# Used for labeling app "app_label" of model.
# Notice that app_label is also used in '/django/db/models/loading.py'

/django/db/models/base.py:54:             kwargs = {"app_label": model_module.__name__.split('.')[-2]}
/django/db/models/loading.py:77:         return app_mod.__name__.split('.')[-2]

At first glance, I would say that the way django "finds" the app could be transformed into a function to simplify code; there are a lot of calls "app_mod.__name__.split('.')[-2]" that could be transformed into a function e.g. in django.utils.get_app_label(app)', should this deserve a ticket?

Secondly, by the code, the unique app constraint seems to be a design decision and not something that django's current implementation could not properly handle.

This leads to the question, what is the reasoning for this design decision?

Thanks,
Jorge

[1] https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps

Aymeric Augustin

unread,
Jun 3, 2013, 5:24:38 AM6/3/13
to django-d...@googlegroups.com
2013/6/3 Jorge C. Leitão <jorgeca...@gmail.com>

Motivated by that criticism, I want to ask: why apps have to have unique names?

Hi Jorge,

Django assumes that a model can be identified by (app_name, model_name). Long ago — 8 years ago — it didn't seem to be a problem.

I'm not sure your research covers all the consequences of this assumption. Anything that hits the app cache depends on this assumption:
- the target of foreign keys can be expressed as 'app_name.ModelName'
- the fixture format identifies models with app_name.ModelName
- several settings have a value in the form app_name.ModelName
- django.contrib.contenttypes uses (app_name, model_name) to identify each model
- since permissions are tied to content types, this assumption bleeds to django.contrib.auth
- etc.

Lifting the requirement of uniqueness would require switching anything that uses the app_name.ModelName format to full.path.to.app.ModelName. That would be quite disruptive, not only for Django, but for the entire ecosystem of apps, starting with contrib apps. The real question is -- what's the story for authors of pluggable apps ? for users of the framework ?

As far as I know, the explanation for this behavior lies in history more than in a design decision. Until now, no one judged this to be enough of a problem to make a proposal to fix it. To be honest, it's frighteningly hard to fix. Even app-loading (ticket #3591) doesn't tackle it.

--
Aymeric.

Yishai Beeri

unread,
Jun 3, 2013, 6:04:34 AM6/3/13
to django-d...@googlegroups.com, Aymeric Augustin
Is this not almost trivial to work around by creating a new module
'newapp', importing the original app code from it, and then using
'path.to.newapp' as a new "copy" of the app in INSTALLED_APPS?

Yishai

Michal Petrucha

unread,
Jun 3, 2013, 7:36:49 AM6/3/13
to django-d...@googlegroups.com
On Mon, Jun 03, 2013 at 01:04:34PM +0300, Yishai Beeri wrote:
> Is this not almost trivial to work around by creating a new module
> 'newapp', importing the original app code from it, and then using
> 'path.to.newapp' as a new "copy" of the app in INSTALLED_APPS?

Not really. At least not for models. There is some magic, I'm not sure
if it's in AppCache or in the model metaclass, which inspects the
import path in which the model class is located and uses it to
identify the model class. That means, if you just import another
application's models, they will still be recoginzed as belonging to
the original application.

Michal
signature.asc

Patryk Zawadzki

unread,
Jun 3, 2013, 9:06:28 AM6/3/13
to django-d...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages