summaryrefslogtreecommitdiff
path: root/django/core/apps.py
blob: 5f37d8d879a607d823a8d92fd0f8f6547be9d517 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule

import imp
import sys
import os
import threading


class MultipleInstancesReturned(Exception):
    "The function returned multiple App instances with the same label"
    pass

class App(object):
    """
    An App in Django is a python package that:
        - is listen in the INSTALLED_APPS setting
        - has a models.py file that with class(es) subclassing ModelBase
    """
    def __init__(self, name):
        self.name = name
        # errors raised when trying to import the app
        self.errors = []
        self.models = []

    def __repr__(self):
        return '<App: %s>' % self.name

class AppCache(object):
    """
    A cache that stores installed applications and their models. Used to
    provide reverse-relations and for app introspection (e.g. admin).
    """
    # Use the Borg pattern to share state between all instances. Details at
    # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531.
    __shared_state = dict(
        # List of App instances
        app_instances = [],

        # Mapping of app_labels to a dictionary of model names to model code.
        app_models = SortedDict(),

        # -- Everything below here is only used when populating the cache --
        loaded = False,
        handled = {},
        postponed = [],
        nesting_level = 0,
        write_lock = threading.RLock(),
        _get_models_cache = {},
    )

    def __init__(self):
        self.__dict__ = self.__shared_state

    def _populate(self):
        """
        Fill in all the cache information. This method is threadsafe, in the
        sense that every caller will see the same state upon return, and if the
        cache is already initialised, it does no work.
        """
        if self.loaded:
            return
        self.write_lock.acquire()
        try:
            if self.loaded:
                return
            for app_name in settings.INSTALLED_APPS:
                if app_name in self.handled:
                    continue
                self.load_app(app_name, True)
            if not self.nesting_level:
                for app_name in self.postponed:
                    self.load_app(app_name)
                self.loaded = True
        finally:
            self.write_lock.release()

    def load_app(self, app_name, can_postpone=False):
        """
        Loads the app with the provided fully qualified name, and returns the
        model module.
        """
        self.handled[app_name] = None
        self.nesting_level += 1

        try:
            app_module = import_module(app_name)
        except ImportError:
            # If the import fails, we assume it was because an path to a
            # class was passed (e.g. "foo.bar.MyApp")
            # We split the app_name by the rightmost dot to get the path
            # and classname, and then try importing it again
            if not '.' in app_name:
                raise
            app_name, app_classname = app_name.rsplit('.', 1)
            app_module = import_module(app_name)
            app_class = getattr(app_module, app_classname)
        else:
            app_class = App

        # check if an app instance with that name already exists
        app_instance = self.find_app(app_name)
        if not app_instance:
            if '.' in app_name:
                app_instance_name = app_name.rsplit('.', 1)[1]
            else:
                app_instance_name = app_name
            app_instance = app_class(app_instance_name)
            self.app_instances.append(app_instance)

        # check if the app instance specifies a path to models
        # if not, we use the models.py file from the package dir
        try:
            models_path = app_instance.models_path
        except AttributeError:
            models_path = '%s.models' % app_name

        try:
            models = import_module(models_path)
        except ImportError:
            self.nesting_level -= 1
            # If the app doesn't have a models module, we can just ignore the
            # ImportError and return no models for it.
            if not module_has_submodule(app_module, 'models'):
                return None
            # But if the app does have a models module, we need to figure out
            # whether to suppress or propagate the error. If can_postpone is
            # True then it may be that the package is still being imported by
            # Python and the models module isn't available yet. So we add the
            # app to the postponed list and we'll try it again after all the
            # recursion has finished (in populate). If can_postpone is False
            # then it's time to raise the ImportError.
            else:
                if can_postpone:
                    self.postponed.append(app_name)
                    return None
                else:
                    raise

        self.nesting_level -= 1
        app_instance.models_module = models
        return models

    def find_app(self, name):
        "Returns the App instance that matches name"
        if '.' in name:
            name = name.rsplit('.', 1)[1]
        for app in self.app_instances:
            if app.name == name:
                return app

    def create_app(self, name):
        """create an app instance"""
        name = name.split('.')[-1]
        app = self.find_app(name)
        if not app:
            app = App(name)
            self.app_instances.append(app)
        return app

    def app_cache_ready(self):
        """
        Returns true if the model cache is fully populated.

        Useful for code that wants to cache the results of get_models() for
        themselves once it is safe to do so.
        """
        return self.loaded

    def get_apps(self):
        "Returns a list of all installed modules that contain models."
        self._populate()
        return [app.models_module for app in self.app_instances\
                if hasattr(app, 'models_module')]

    def get_app(self, app_label, emptyOK=False):
        """
        Returns the module containing the models for the given app_label. If
        the app has no models in it and 'emptyOK' is True, returns None.
        """
        self._populate()
        self.write_lock.acquire()
        try:
            for app_name in settings.INSTALLED_APPS:
                if app_label == app_name.split('.')[-1]:
                    mod = self.load_app(app_name, False)
                    if mod is None:
                        if emptyOK:
                            return None
                    else:
                        return mod
            raise ImproperlyConfigured("App with label %s could not be found" % app_label)
        finally:
            self.write_lock.release()

    def get_app_errors(self):
        "Returns the map of known problems with the INSTALLED_APPS."
        self._populate()
        errors = {}
        for app in self.app_instances:
            if app.errors:
                errors.update({app.label: app.errors})
        return errors

    def get_models(self, app_mod=None, include_auto_created=False, include_deferred=False):
        """
        Given a module containing models, returns a list of the models.
        Otherwise returns a list of all installed models.

        By default, auto-created models (i.e., m2m models without an
        explicit intermediate table) are not included. However, if you
        specify include_auto_created=True, they will be.

        By default, models created to satisfy deferred attribute
        queries are *not* included in the list of models. However, if
        you specify include_deferred, they will be.
        """
        cache_key = (app_mod, include_auto_created, include_deferred)
        try:
            return self._get_models_cache[cache_key]
        except KeyError:
            pass
        self._populate()
        if app_mod:
            app_label = app_mod.__name__.split('.')[-2]
            app = self.find_app(app_label)
            if app:
                app_list = [app]
        else:
            app_list = self.app_instances
        model_list = []
        for app in app_list:
            models = app.models
            model_list.extend(
                model for model in models
                if ((not model._deferred or include_deferred)
                    and (not model._meta.auto_created or include_auto_created))
            )
        self._get_models_cache[cache_key] = model_list
        return model_list

    def get_model(self, app_label, model_name, seed_cache=True):
        """
        Returns the model matching the given app_label and case-insensitive
        model_name.

        Returns None if no model is found.
        """
        if seed_cache:
            self._populate()
        app = self.find_app(app_label)
        if app:
            for model in app.models:
                if model_name.lower() == model._meta.object_name.lower():
                    return model

    def register_models(self, app_label, *models):
        """
        Register a set of models as belonging to an app.
        """
        app_instance = self.find_app(app_label)
        if not app_instance:
            raise ImproperlyConfigured('Could not find App instance with label "%s". '
                                       'Please check your INSTALLED_APPS setting'
                                       % app_label)
        for model in models:
            # Store as 'name: model' pair in a dictionary
            # in the models list of the App instance
            model_name = model._meta.object_name.lower()
            model_dict = self.app_models.setdefault(app_label, SortedDict())
            if model_name in model_dict:
                # The same model may be imported via different paths (e.g.
                # appname.models and project.appname.models). We use the source
                # filename as a means to detect identity.
                fname1 = os.path.abspath(sys.modules[model.__module__].__file__)
                fname2 = os.path.abspath(sys.modules[model_dict[model_name].__module__].__file__)
                # Since the filename extension could be .py the first time and
                # .pyc or .pyo the second time, ignore the extension when
                # comparing.
                if os.path.splitext(fname1)[0] == os.path.splitext(fname2)[0]:
                    continue
            model_dict[model_name] = model
            app_instance.models.append(model)
        self._get_models_cache.clear()

cache = AppCache()