动态加载Django应用程序

18 投票
5 回答
13054 浏览
提问于 2025-04-18 08:30

在运行时动态加载Django应用程序是否可能?通常情况下,应用程序是在初始化时加载的,使用的是settings.py中的INSTALLED_APPS元组。但是,是否可以在运行时加载额外的应用程序呢?我在不同的情况下遇到了这个问题。例如,在测试时,我希望能够动态加载或卸载应用程序。

为了让问题更具体,假设我有一个名为apps的目录,我把我的应用程序放在这里,我希望能够自动安装任何新放进去的应用,而不需要手动编辑settings.py。

这其实很简单。根据以下示例代码

Django: Dynamically add apps as plugin, building urls and other settings automatically

我们可以在settings.py中放入以下代码,这样就可以循环遍历应用目录中的所有子目录名称,并像这样增加INSTALLED_APPS元组:

APPS_DIR = '/path_to/apps/'

for item in os.listdir(APPS_DIR):
    if os.path.isdir(os.path.join(APPS_DIR, item)):
        app_name = 'apps.%s' % item
    if app_name not in INSTALLED_APPS:
        INSTALLED_APPS += (app_name, )

之后,如果我在Django的命令行界面中,我可以做一些事情,比如

from django.conf import settings

这样应用程序就会在settings.INSTALLED_APPS中列出。如果我执行

from django.core import management
management.call_command('syncdb', interactive=False)

这将为这些应用程序创建必要的数据库表。

但是,如果我现在在apps/目录中添加更多应用,而不重启,这些新应用不会出现在settings.INSTALLED_APPS中,因此后续调用syncdb将没有效果。

我想知道是否有办法在不重启的情况下重新加载设置并加载/安装新应用。

我尝试直接导入我的settings.py,也就是 from myproject import settings

然后在任何app目录更改后使用Python内置的reload来重新加载settings。虽然settings.INSTALLED_APPS现在已经更改为包含新添加的应用,但这最终没有任何区别。例如,

from django.db import models
models.get_apps()

只显示原来的应用,而不显示新添加的应用,同样

management.call_command('syncdb', interactive=False)

也看不到新添加的应用。

正如我上面所说,我特别考虑这种情况是在测试中,我希望动态添加或移除应用。

附言:我正在使用Django 1.6,但根据@RickyA的建议,我看到Django在1.7中对应用程序的处理有一些重大变化。

https://docs.djangoproject.com/en/1.7/ref/applications/

我仍然不确定这对我面临的问题意味着什么。

5 个回答

-1

没错!在Python中,几乎所有事情都是可能的。你可以使用os.walk()这个功能来获取你应用程序路径下的所有文件夹、子文件夹和文件,这样就能找到你所有的应用程序,包括那些嵌套在里面的。

def get_installed_apps():
    from os import walk, chdir, getcwd
    previous_path = getcwd()
    master = []
    APPS_ROOT_PATH = '/my/project/apps/folder'
    chdir(APPS_ROOT_PATH)
    for root, directories, files in walk(top=getcwd(), topdown=False):
        for file in files:
            if 'apps.py' in file:
                app_path = f"{root.replace(BASE_DIR + '/', '').replace('/', '.')}.apps"
                print(app_path)
                master.append(app_path)
    chdir(previous_path)
    return master


print(get_installed_apps())
3

在Django 1.7及以上版本(包括当前的4.1版本)中,可以通过一个叫做 override_settings() 的装饰器,动态地加载卸载应用程序,特别是在测试中。

@override_settings(INSTALLED_APPS=[...])  # added or removed some apps
class MyTest(TestCase):
    # some tests with these apps
    def test_foo(self):
        pass

这个功能自2014年9月就已经可以使用了,早于提问的时间。

提问中提到的许多其他问题也可以通过 django.apps 在Django 1.7及以上版本中解决。总的来说:在当前的Django中,启动时的动态配置很简单。但在启动后进行动态加载和卸载不建议在生产环境中使用,尽管它可能最终能工作。

7

在Django 2.2版本中,这对我有效

from collections import OrderedDict
from django.apps import apps
from django.conf import settings
from django.core import management

new_app_name = "my_new_app"

settings.INSTALLED_APPS += (new_app_name, )
apps.app_configs = OrderedDict()
apps.apps_ready = apps.models_ready = apps.loading = apps.ready = False
apps.clear_cache()
apps.populate(settings.INSTALLED_APPS)

management.call_command('makemigrations', new_app_name, interactive=False)

management.call_command('migrate', new_app_name, interactive=False)
15

关于Django 1.8的更新,讲的是如何加载一个还没有加载的应用

from collections import OrderedDict
from django.apps import apps
from django.conf import settings
from django.core import management

new_app_name = "my_new_app"

settings.INSTALLED_APPS += (new_app_name, )
# To load the new app let's reset app_configs, the dictionary
# with the configuration of loaded apps
apps.app_configs = OrderedDict()
# set ready to false so that populate will work 
apps.ready = False
# re-initialize them all; is there a way to add just one without reloading them all?
apps.populate(settings.INSTALLED_APPS)

# now I can generate the migrations for the new app
management.call_command('makemigrations', new_app_name, interactive=False)
# and migrate it
management.call_command('migrate', new_app_name, interactive=False)
4

为了回答我自己的问题...

虽然我没有一个完全通用的解决方案,但我有一个足够用的办法,可以在测试时动态加载应用。

这个基本的解决方案很简单,我是在一个小小的bixly博客上找到的。

继续我上面的例子,如果我在django的命令行界面中,想要添加并加载一些新应用,这些应用是我刚放到apps目录里的,我可以这样做:

import os
from django.conf import settings
from django.db.models import loading
from django.core import management

APPS_DIR = '/path_to/apps/'

for item in os.listdir(APPS_DIR):
    if os.path.isdir(os.path.join(APPS_DIR, item)):
        app_name = 'apps.%s' % item
    if app_name not in settings.INSTALLED_APPS:
        settings.INSTALLED_APPS += (app_name, )

然后再这样:

loading.cache.loaded = False
management.call_command('syncdb', interactive=False)

撰写回答