模型使用时覆盖 Django 中的设置

Overriding settings in Django when used by the models

我们正在为 Speedy Net and Speedy Match 使用 Django(当前为 Django 2.1)。我们的一些设置被模型使用。例如:

class USER_SETTINGS(object):
    MIN_USERNAME_LENGTH = 6
    MAX_USERNAME_LENGTH = 40

    MIN_SLUG_LENGTH = 6
    MAX_SLUG_LENGTH = 200

    # Users can register from age 0 to 180, but can't be kept on the site after age 250.
    MIN_AGE_ALLOWED_IN_MODEL = 0  # In years.
    MAX_AGE_ALLOWED_IN_MODEL = 250  # In years.

    MIN_AGE_ALLOWED_IN_FORMS = 0  # In years.
    MAX_AGE_ALLOWED_IN_FORMS = 180  # In years.

    MIN_PASSWORD_LENGTH = 8
    MAX_PASSWORD_LENGTH = 120

    MAX_NUMBER_OF_FRIENDS_ALLOWED = 800

    PASSWORD_VALIDATORS = [
        {
            'NAME': 'speedy.core.accounts.validators.PasswordMinLengthValidator',
        },
        {
            'NAME': 'speedy.core.accounts.validators.PasswordMaxLengthValidator',
        },
    ]

(在 https://github.com/speedy-net/speedy-net/blob/staging/speedy/net/settings/global_settings.py 中定义)。然后在我使用的模型中:

from django.conf import settings as django_settings

class User(ValidateUserPasswordMixin, PermissionsMixin, Entity, AbstractBaseUser):
    settings = django_settings.USER_SETTINGS

(然后在class中使用settings的属性,比如settings.MIN_SLUG_LENGTH)。

问题是,当我尝试在测试中覆盖此类设置时(您可以在 上查看我的问题和答案),User.settings 保持不变并且不会被我的设置覆盖试图覆盖。这是一个问题,因为在模型中我将 settings.MIN_SLUG_LENGTH 传递给验证器,其他模型也将其他值传递给验证器。是否可以以在生产和测试中都使用正确设置的方式定义模型和设置,包括当我想覆盖它们时?

我知道 https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings 中的这句话:

Warning

The settings file contains some settings that are only consulted during initialization of Django internals. If you change them with override_settings, the setting is changed if you access it via the django.conf.settings module, however, Django’s internals access it differently. Effectively, using override_settings() or modify_settings() with these settings is probably not going to do what you expect it to do.

We do not recommend altering the DATABASES setting. Altering the CACHES setting is possible, but a bit tricky if you are using internals that make using of caching, like django.contrib.sessions. For example, you will have to reinitialize the session backend in a test that uses cached sessions and overrides CACHES.

Finally, avoid aliasing your settings as module-level constants as override_settings() won’t work on such values since they are only evaluated the first time the module is imported.

据我所知,在这种情况下哪些是相关的,但是我如何以可以覆盖它们的方式定义设置?

speedy/core/base/test/models.py 中的函数 _1___set_up 是一种使测试工作的解决方法,但这是一种技巧,我认为从长远来看这不是一个好的解决方案。

你引用的问题:

avoid aliasing your settings as module-level constants as override_settings() won’t work on such values since they are only evaluated the first time the module is imported.

有 3 种解决方法,其中 方法 1 > 方法 3 > 方法 2.

方式 1:不要使用 class 属性作为别名,而是使用 classproperty

推荐;可以说是正确的方法。

  • 专业版:最具表现力,更易于调试。
  • 缺点:模型中有更多代码。
from django.utils.decorators import classproperty

class User(PermissionsMixin, Entity, AbstractBaseUser):
    # settings = django_settings.USER_SETTINGS
    @classproperty
    def settings(cls):
        return django_settings.USER_SETTINGS

警告:Class 依赖于 settings class 属性的属性将不起作用。

尽管 方式 2 允许以下代码仍然有效,但这些代码是在 class 定义(导入)时评估的,不能根据 [=18 进行合理更改=],除非他们也是 classproperty

AGE_VALID_VALUES_IN_MODEL = range(settings.MIN_AGE_ALLOWED_IN_MODEL, settings.MAX_AGE_ALLOWED_IN_MODEL)
AGE_VALID_VALUES_IN_FORMS = range(settings.MIN_AGE_ALLOWED_IN_FORMS, settings.MAX_AGE_ALLOWED_IN_FORMS)

方式 2:补丁设置 class 所以实例读取 django_settings

不推荐; 影响 USER_SETTINGS 的运行时评估也在生产中,而不仅仅是在测试中 (@hynekcer)

  • 专业版:模型中没有代码更改。
  • 缺点:表达能力最差,更难调试。

  1. 定义函数overridable_settings:
def overridable_settings(settings_class):
    old__getattribute__ = settings_class.__getattribute__
    settings_name = settings_class.__name__

    def patched__getattribute__(_self, item):
        from django.conf import settings as django_settings
        settings = getattr(django_settings, settings_name)
        return old__getattribute__(settings, item)

    settings_class.__getattribute__ = patched__getattribute__
    return settings_class()
  1. django_settings.USER_SETTINGS 现在是设置 class 的一个实例。定义 override_settings:
  2. 而不是 get_django_settings_class_with_override_settings
import copy

def override_settings(settings, **overrides):
    copied_settings = copy.deepcopy(settings)
    for setting, value in overrides.items():
        setattr(copied_settings, setting, value)
    assert copied_settings != settings
    return copied_settings

用法:

@overridable_settings
class USER_SETTINGS(object):
from speedy.core.base.test import utils

# @override_settings(USER_SETTINGS=get_django_settings_class_with_override_settings(django_settings_class=django_settings.USER_SETTINGS, MIN_SLUG_LENGTH=tests_settings.OVERRIDE_USER_SETTINGS.MIN_SLUG_LENGTH))
@override_settings(USER_SETTINGS=utils.override_settings(django_settings.USER_SETTINGS, MIN_SLUG_LENGTH=tests_settings.OVERRIDE_USER_SETTINGS.MIN_SLUG_LENGTH))
def test_slug_min_length_fail_username_min_length_ok(self):

方式 3:创建信号接收器 setting_changed 以更新别名

  • 专业版:对模型的代码更改最少。
  • 缺点:在 Caveat.
  • 中关于依赖属性的表现力不如 Way 1

来自 https://docs.djangoproject.com/en/dev/topics/testing/tools/#overriding-settings:

When overriding settings, make sure to handle the cases in which your app’s code uses a cache or similar feature that retains state even if the setting is changed. Django provides the django.test.signals.setting_changed signal that lets you register callbacks to clean up and otherwise reset state when settings are changed.

Django itself uses this signal to reset various data.

from django.core.signals import setting_changed
from django.dispatch.dispatcher import receiver

def register_django_setting_alias(setting_alias, django_setting):
    def decorator(cls):
        @receiver(setting_changed, weak=False)
        def update_setting_alias(setting, value, **_):
            if setting == django_setting:
                setattr(cls, setting_alias, value)
        return cls
    return decorator

用法:

@register_django_setting_alias('settings', 'USER_SETTINGS')
class User(PermissionsMixin, Entity, AbstractBaseUser):
    settings = django_settings.USER_SETTINGS