【问题标题】:Django app defaults?Django应用程序默认值?
【发布时间】:2017-02-14 19:36:46
【问题描述】:

我正在寻找一种让应用程序默认值和设置易于使用、不易出错且开销很小的方法。

目前我将其组织如下:

myapp/defaults.py
    # application defaults
    import sys
    if sys.platform == 'win32':
         MYAPP_HOME_ROOT = os.path.dirname(os.environ['USERPROFILE'])
    else:
         MYAPP_HOME_ROOT = '/home'

在我的项目中,我有:

mysite/settings.py
    from myapp.defaults import *       # import all default from myapp
    MYAPP_HOME_ROOT = '/export/home'   # overriding myapp.defaults 

通过此设置,我可以以常规 django 方式(from django.conf import settingssettings.XXX)导入和使用设置。

update-3(我们为什么需要这个)

  1. 默认设置(“默认值”):
    1. 如果可以通过覆盖一组合理的默认设置来配置应用程序,则使用起来会更方便。
    2. 应用程序“具有领域知识”,因此尽可能提供合理的默认值是有意义的。
    3. 应用程序的用户需要提供每个应用程序所需的所有设置并不方便,覆盖一小部分并保留其余部分的默认值就足够了。
    4. 如果默认值可以对环境做出反应,这非常很有用。当DEBUG 为 True 时,您通常会想做一些不同的事情,但任何其他全局设置都可能有用:例如MYAPP_IDENTICON_DIR = os.path.join(settings.MEDIA_ROOT, 'identicons') (https://en.wikipedia.org/wiki/Identicon)
    5. 项目(站点/全局)设置必须覆盖应用程序默认值,即在(全局)settings.py 文件中为其站点定义MYAPP_IDENTICON_DIR = 's3://myappbucket.example.com/identicons' 的用户应获取此值,而不是应用程序的默认值。
    6. 任何接近使用设置的正常方式 (import .. settings; settings.FOO) 的解决方案都优于需要新语法的解决方案(因为新语法会出现分歧,我们将获得新的和独特的方式来在应用程序之间使用设置)。
    7. python 的禅意大概适用于这里:
      • 如果实现难以解释,那就是个坏主意。
      • 如果实现易于解释,那可能是个好主意。

(原帖提出以下两个关键问题,未说明上述假设..)

问题 #1: 但是,当为应用程序运行单元测试时,没有站点,因此 settings 不会有任何 myapp.defaults

问题 #2: 如果myapp.defaults 需要使用设置中的任何内容(例如settings.DEBUG),也存在一个大问题,因为您无法从defaults.py 导入设置(因为这将是一个循环导入)。

为了解决问题 #1,我创建了一个间接层:

myapp/conf.py
    from . import defaults
    from django.conf import settings

    class Conf(object):
        def __getattr__(self, attr):
            try:
                return getattr(settings, attr)
            except AttributeError:
                return getattr(defaults, attr)
    conf = Conf()  # !<-- create Conf instance

及用法:

myapp/views.py
    from .conf import conf as settings
    ...
    print settings.MYAPP_HOME_ROOT   # will print '/export/home' when used from mysite

这允许conf.py 文件也可以使用“空”设置文件,并且 myapp 代码可以继续使用熟悉的settings.XXX

它不能解决问题 #2,根据例如定义应用程序设置settings.DEBUG。我目前的解决方案是添加到Conf 类:

    from . import defaults
    from django.conf import settings

    class Conf(object):
        def __getattr__(self, attr):
            try:
                return getattr(settings, attr)
            except AttributeError:
                return getattr(defaults, attr)

        if settings.DEBUG:
            MYAPP_HOME_ROOT = '/Users'
        else:
            MYAPP_HOME_ROOT = '/data/media'
    conf = Conf()  # !<-- create Conf instance

但这并不令人满意,因为 mysite 不能再覆盖该设置,并且 myapp 的默认值/设置现在分布在两个文件中...

有更简单的方法吗?

update-4:“只需使用 django 测试运行器..”

您正在测试的应用程序依赖于 Django 框架 - 您无法绕过这样一个事实,即您需要先引导框架才能测试应用程序。引导过程的一部分是创建一个默认的 settings.py 并进一步使用 django 提供的测试运行器来确保您的应用正在它们可能运行的环境中进行测试。

虽然这听起来应该是真的,但实际上并没有多大意义,例如没有默认的settings.py 这样的东西(至少对于可重用的应用程序来说不是)。在谈论集成测试时,使用 site/settings/apps/database(s)/cache(s)/resource-limits/etc 测试应用程序是有意义的。它将在生产中遇到。然而,对于单元测试,我们只想测试我们正在查看的代码单元 - 尽可能少的外部依赖项(和设置)。 Django 测试运行器确实并且应该模拟框架的主要部分,因此不能说它在任何“真实”环境中运行。

虽然 Django 测试运行器很棒,但它无法处理的问题还有很多。对我们来说,两个大问题是(i)顺序运行测试非常慢,以至于测试套件变得未使用(并行运行时小于 5 分钟,顺序运行时几乎一个小时),(ii)有时您只需要在 big 数据库(我们将昨晚的备份恢复到可以运行测试的测试数据库——对于固定装置来说太大了)。

制作nose、py.test、twill、Selenium 和任何模糊测试工具的人确实非常了解测试,因为这是他们唯一的关注点。不能借鉴他们的集体经验将是一种耻辱。

我不是第一个遇到此问题的人,而且似乎没有简单或通用的解决方案。以下是两个具有不同解决方案的项目:

更新,python-oidc-provider 方法

python-oidc-provider 包 (https://github.com/juanifioren/django-oidc-provider) 有另一种创造性的方式来解决 app-settings/defaults 问题。它使用属性在myapp/settings.py 文件中定义默认值:

from django.conf import settings
class DefaultSettings(object):
    @property
    def MYAPP_HOME_ROOT(self):
        return ...

default_settings = DefaultSettings() 

def get(name):
    value = None
    try:
        value = getattr(default_settings, name)
        value = getattr(settings, name)
    except AttributeError:
        if value is None:
            raise Exception("Missing setting: " + name)

使用 myapp 中的设置变为:

from myapp import settings
print settings.get('MYAPP_HOME_ROOT')

好: 解决了问题 #2(在定义默认值时使用设置),解决了问题 #1(使用测试中的默认设置)。

错误: 访问设置的语法不同(settings.get('FOO') 与普通的settings.FOO),myapp 无法为将在 myapp 之外使用的设置提供默认值(您从 @987654351 获得的设置@ 将不包含来自 myapp 的任何默认值)。外部代码可以通过 from myapp import settings 获取常规设置和 myapp 默认值,但如果有多个应用程序想要这样做,这将失效...

Update2,django-appconf 包: (注:与 Django 的 AppConfig 无关..)

使用django-appconfig,在myapp/conf.py 中创建应用设置(需要提前加载,因此您应该从models.py 导入conf.py 文件——因为它是提前加载的):

from django.conf import settings
from appconf import AppConf
class MyAppConf(AppConf):
    HOME_ROOT = '...'

用法:

from myapp.conf import settings
print settings.MYAPP_HOME_ROOT

AppConf 将自动添加MYAPP_ 前缀,并自动检测MYAPP_HOME_ROOT 是否已在项目设置中重新定义/覆盖。

pro: 使用简单,解决了问题 #1(从测试访问应用程序设置)和问题 #2(在定义默认值时使用设置)。只要 conf.py 文件提前加载,外部代码应该能够使用 myapp 中定义的默认值。

con: 非常神奇。 conf.py 中设置的名称与其用法不同(因为 appconf 自动添加了MYAPP_ 前缀)。外部/不透明依赖。

【问题讨论】:

  • 除非您可以将其过滤为一个问题而不是长时间的讨论;我担心你的问题可能会因为“太宽泛”而被关闭。我建议通过django bug tracker 处理这个问题。
  • 问题很简单:“在可重复使用的应用程序中,您如何明智地进行设置?”其他所有内容都只是足够的背景知识,可以解释为什么解决玩具问题的解决方案还不够,而且没有任何明显的解决方案可供每个人使用。
  • 在使用myapp/defaults.py 解决方案时,为什么不能在from django.conf import settings 内使用class Conf 解决方案(顺便说一句,规范方式)?顺便说一句,所有其他解决方案(除了第一个和第一个)都会生成错误的文档。
  • @nitely 因为您网站的 settings.py(当您说 from django.conf import settings 时导入)需要执行 from myapp.defaults import *(以便每个站点可以覆盖应用程序设置)。如果myapp/defaults.py 转身导入from django.conf import settings,那么你就有了循环导入。
  • @thebjorn 不要在您的网站设置中使用from myapp.defaults import *。这就是class Conf 的全部意义所在

标签: python django


【解决方案1】:

当我构建应用程序时,我尝试以 mixin 的形式定义功能。如果可能,每个设置都应该由一个功能 mixin 选择。

所以在你上面的例子中:

from django.conf import settings


class AppHomeRootMixin:
    home_root = getattr(settings, "MYAPP_HOME_ROOT", "/default/path/here")

用法:

class MyView(AppHomeRootMixin, TemplateView):

    def dispatch(self, *args, **kwargs):
        print(self.home_root)
        return super().dispatch(*args, **kwargs)

这对于其他开发人员来说真的很容易准确地看到正在发生的事情,减少了对第三方或第一方“魔法”的需求,并允许我们从功能的角度来思考问题,而不是从个人设置。

我只是觉得Django的设置层应该尽量简单,任何复杂的都应该是视图层的责任。我遇到了很多由其他开发人员创建的令人困惑的设置配置,而这些配置占用了我很多时间,当时没有人可以解释幕后发生的事情。

【讨论】:

    【解决方案2】:

    我编写了一个用于管理应用程序设置的 django 包,名为 django-pluggableappsettings

    它是一个包,允许您为设置定义合理的默认值,但还添加了类型检查或可调用设置等高级功能。它利用元类来轻松定义应用程序设置。当然,这会为您的项目添加一个外部依赖项。

    编辑 1:

    示例用法如下:

    使用 pip 安装包:

    pip install django-pluggableappsettings
    

    在您的任何项目文件中创建您的 AppSettings 类。例如。在“app_settings.py”中。

    app_settings.py

    from django_pluggableappsettings import AppSettings, Setting, IntSetting
    
    class MyAppSettings(AppSettings):
        MY_SETTING = Setting('DEFAULT_VALUE')
        # Checks for "MY_SETTING" in django.conf.settings and 
        # returns 'DEFAULT_VALUE' if it is not present
    
        ANOTHER_SETTING = IntSetting(42, aliases=['OTHER_SETTING_NAME'])
        # Checks for "ANOTHER_SETTING" or "OTHER_SETTING_NAME" in
        # django.conf.settings and returns 42 if it is not present.
        # also validates that the given value is of type int
    

    在任何文件中导入您的MyAppSettings 并访问其属性

    from mypackage.app_settings import MyAppSettings
    MyAppSettings.MY_SETTING
    MyAppSettings.ANOTHER_SETTING
    

    请注意,这些设置仅在首次访问时初始化,因此如果您从不访问某个设置,则永远不会检查其在 django.conf.settings 中的存在。

    【讨论】:

      【解决方案3】:

      我刚刚根据所有要求创建了django-app-defaults。它基本上是问题(class Conf(object):)中强调的第二种方法的概括。

      用法:

      # my_app/defaults.py
      
      # `django.conf.settings` or any other module can be imported if needed
      
      # required
      DEFAULT_SETTINGS_MODULE = True
      
      # define default settings below
      MY_DEFAULT_SETTING = "yey"
      

      然后在您的项目中的任何地方:

      from app_defaults import settings
      
      print(settings.MY_DEFAULT_SETTING)
      # yey
      
      # All `django.conf.settings` are also available
      print(settings.DEBUG)
      # True
      

      要为单个应用而不是所有应用加载默认设置,只需执行以下操作:

      # Note: when apps or modules are explicitly passed,
      # the `DEFAULT_SETTINGS_MODULE` var is not required
      
      from app_defaults import Settings
      
      settings = Settings(apps=["my_app"])
      
      # or
      
      from my_app import defaults
      settings = Settings(modules=[defaults])
      

      【讨论】:

      • 我喜欢这种方法。
      【解决方案4】:

      问题 #1:为应用程序运行单元测试时,没有站点 但是,因此设置不会有任何 myapp.defaults。

      使用 django 附带的测试框架解决了这个问题(请参阅the documentation),因为它会为您正确引导测试环境。

      请记住,django 测试始终使用DEBUG=False 运行

      问题2:如果myapp.defaults需要使用,还有一个大问题 设置中的任何内容(例如 settings.DEBUG),因为您无法导入 defaults.py 中的设置(因为那将是循环导入)。

      这不是一个真正的问题;一旦你从myapp.defaults 中的myapp.settings 导入,settings 中的所有内容都将在范围内。所以你不要导入DEBUG,它已经在全局范围内可供你使用。

      【讨论】:

      • 测试框架如何知道它需要做from myapp.defaults import *myapp.settings 是什么/应该是什么?
      • 我也不确定我会使用哪个manage.py,因为manage.py 属于项目/站点,而不是通用应用程序的一部分。我是否需要使用我的可重用应用程序生成/分发项目..?目前我通过py.test 运行应用程序测试,通常使用pytest-django 包。
      • 只有一个manage.py 管理整个项目。应用程序没有 django 提供的 manage.py 文件;这不会阻止您在应用中创建名为 manage.py 的文件,但这可能会产生意想不到的副作用。
      • 我想知道我们是不是在互相交谈..?我正在开发/测试与多个 (10+) 项目/站点一起使用的多个 (100+) 可重用应用程序。重要的是可以单独测试应用程序,即不需要站点/项目(或者如果需要,只需一个简单创建的项目)。我看不出manage.py test 将如何帮助我,因为它需要我在我的应用程序的顶层创建一个manage.py 和一个settings.py(看起来并不习惯)。即使我这样做了,我也看不出这将如何解决可覆盖的默认值(#1)或在默认值(#2)中访问 settings.py..
      • 您正在测试的应用程序依赖于 Django 框架 - 您无法绕过这样一个事实,即您需要先引导框架才能测试应用程序。引导过程的一部分是创建默认的settings.py,并进一步使用 django 提供的测试运行程序来确保您的应用程序正在它们可能运行的环境中进行测试。
      猜你喜欢
      • 1970-01-01
      • 2011-01-21
      • 2018-02-18
      • 1970-01-01
      • 2011-11-14
      • 2017-06-06
      • 2013-06-05
      • 2015-08-01
      • 2010-11-23
      相关资源
      最近更新 更多