Django Form源码分析之Field验证逻辑

闻人梓
2023-12-01

引言

上一篇对BaseForm的分析中,我只提及了在Form层次的输入验证,在Form.full_clean()主要调用的两个函数self._clean_field(), self._clean_form()。其中,self._clean_field方法代表了Field层次的输入验证。
在Django官方文档中,验证逻辑依次按照如下流程图:

Created with Raphaël 2.1.0 field.to_python() field.validate() field.run_validators() field.clean() form.clean_<field_name>() form.clean()

其中self._clean_form对应着form.clean()。
同样,在引言中给出Field的初始化方便查阅

def __init__(self, required=True, widget=None, label=None, initial=None,
                 help_text='', error_messages=None, show_hidden_initial=False,
                 validators=[], localize=False, disabled=False, label_suffix=None):
        # required -- Boolean that specifies whether the field is required.
        #             True by default.
        # widget -- A Widget class, or instance of a Widget class, that should
        #           be used for this Field when displaying it. Each Field has a
        #           default Widget that it'll use if you don't specify this. In
        #           most cases, the default widget is TextInput.
        # label -- A verbose name for this field, for use in displaying this
        #          field in a form. By default, Django will use a "pretty"
        #          version of the form field name, if the Field is part of a
        #          Form.
        # initial -- A value to use in this Field's initial display. This value
        #            is *not* used as a fallback if data isn't given.
        # help_text -- An optional string to use as "help text" for this Field.
        # error_messages -- An optional dictionary to override the default
        #                   messages that the field will raise.
        # show_hidden_initial -- Boolean that specifies if it is needed to render a
        #                        hidden widget with initial value after widget.
        # validators -- List of additional validators to use
        # localize -- Boolean that specifies if the field should be localized.
        # disabled -- Boolean that specifies whether the field is disabled, that
        #             is its widget is shown in the form but not editable.
        # label_suffix -- Suffix to be added to the label. Overrides
        #                 form's label_suffix.
        self.required, self.label, self.initial = required, label, initial
        self.show_hidden_initial = show_hidden_initial
        self.help_text = help_text
        self.disabled = disabled
        self.label_suffix = label_suffix
        widget = widget or self.widget
        if isinstance(widget, type):
            widget = widget()

        # Trigger the localization machinery if needed.
        self.localize = localize
        if self.localize:
            widget.is_localized = True

        # Let the widget know whether it should display as required.
        widget.is_required = self.required

        # Hook into self.widget_attrs() for any Field-specific HTML attributes.
        extra_attrs = self.widget_attrs(widget)
        if extra_attrs:
            widget.attrs.update(extra_attrs)

        self.widget = widget

        # Increase the creation counter, and save our local copy.
        self.creation_counter = Field.creation_counter
        Field.creation_counter += 1

        messages = {}
        for c in reversed(self.__class__.__mro__):
            messages.update(getattr(c, 'default_error_messages', {}))
        messages.update(error_messages or {})
        self.error_messages = messages

        self.validators = self.default_validators + validators
        super(Field, self).__init__()

Form._clean_field()

def _clean_fields(self):
        for name, field in self.fields.items():
            # value_from_datadict() gets the data from the data dictionaries.
            # Each widget type knows how to retrieve its own data, because some
            # widgets split data over several HTML fields.
            if field.disabled:
                value = self.initial.get(name, field.initial)
            else:
                value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
            try:
                if isinstance(field, FileField):
                    initial = self.initial.get(name, field.initial)
                    value = field.clean(value, initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)

在Form中self.fields是一个OrderDict结构,可以近似地看为一个key为field的声明名字(string),value为Field(class)的一个dict。
首先看一下Field.disabled这个属性,在初始化的时候默认为False,这个属性是什么意思呢?

# disabled -- Boolean that specifies whether the field is disabled, that
        #             is its widget is shown in the form but not editable.

当清楚这个属性的作用后,再看判断的逻辑:

if field.disabled:
                value = self.initial.get(name, field.initial)
            else:
                value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))

即当Filed不可用的时候,value会先从form.initial中获取,如果没有则会从field.initial中获取,这也是在对BaseForm的源码分析中,form.initial的优先级比field.initial高的缘故。(对initial参数的意义有疑惑的也应该去参考一下)
当Field可用的时候,将直接从self.data中获取相应的key。

def value_from_datadict(self, data, files, name):
        """
        Given a dictionary of data and this widget's name, returns the value
        of this widget. Returns None if it's not provided.
        """
        return data.get(name)

字段在检查完可用性和初始值之后开始调用Field.clean()。

Field.clean()

def clean(self, value):
        """
        Validates the given value and returns its "cleaned" value as an
        appropriate Python object.

        Raises ValidationError for any errors.
        """
        value = self.to_python(value)
        self.validate(value)
        self.run_validators(value)
        return value

在field.clean()方法中依次调用了field.to_python(), field.validate(), field.run_validators。同时,在这四个方法中raise的ValidationError都会被form._clean_field()捕捉。

Field.to_python()

def to_python(self, value):
        return value

to_python()主要用于返回正确的python类型,在Field中的实现非常简单,但是在不同的Field中可能会有不同的实现,如CharField:

def to_python(self, value):
        "Returns a Unicode object."
        if value in self.empty_values:
            return ''
        value = force_text(value)
        if self.strip:
            value = value.strip()
        return value

Field.validate()


def validate(self, value):
        if value in self.empty_values and self.required:
            raise ValidationError(self.error_messages['required'], code='required')

validate()主要验证field的require属性,如果required为True而value却为一个空值就会引发ValidationError。那么,如果我想更改field的required验证输出的错误信息该怎么办?
首先,要先清楚self.error_messages是如何初始化的:

messages = {}
        for c in reversed(self.__class__.__mro__):
            messages.update(getattr(c, 'default_error_messages', {}))
        messages.update(error_messages or {})
        self.error_messages = messages

MRO全名为Method Resolution Order,Python在进行多重继承(关于调用super的时候发生了什么可以参考这篇文章)的时候实例调用方法时会按照MRO的list顺序依次向上寻找,MRO的list是由C3算法计算而成,可以参考这篇文章。那么在这里是什么意思呢?
可以看到,self.__classs__.__mro__本来应该返回实例继承从最近的基类到最远的基类的list,因为使用了reversed方法所以从最顶端的基类开始。(访问class的__mro__以及调用mro()都能得到mro列表)message依次更新每个类下的default_error_messages属性,而这个属性将会是一个字典。
假设从IntegerField开始:

# IntegerField
class IntegerField(Field):
    widget = NumberInput
    default_error_messages = {
        'invalid': _('Enter a whole number.'),
    }

# Field
class Field(six.with_metaclass(RenameFieldMethods, object)):
    widget = TextInput  # Default widget to use when rendering this type of Field.
    hidden_widget = HiddenInput  # Default widget to use when rendering this as "hidden".
    default_validators = []  # Default set of validators
    # Add an 'invalid' entry to default_error_message if you want a specific
    # field error message not raised by the field validators.
    default_error_messages = {
        'required': _('This field is required.'),
    }

message先更新Field的default_error_messages,接着是IntegerField。最后message更新初始化参数中的error_message。
因此,如果我想更改field中默认的required属性,只需在参数中传入含有key为required的error_message字典,如果你想更改require验证的code属性,就只能重写validate方法了。

Field.run_validators()

def run_validators(self, value):
        if value in self.empty_values:
            return
        errors = []
        for v in self.validators:
            try:
                v(value)
            except ValidationError as e:
                if hasattr(e, 'code') and e.code in self.error_messages:
                    e.message = self.error_messages[e.code]
                errors.extend(e.error_list)
        if errors:
            raise ValidationError(errors)

Field.run_validators()依次调用self.validators中的由开发者自定义的validator。(该方法推荐不应该重载)

Form.clean_<field_name>

最后是针对特定字段的验证逻辑,这又是如何实现的呢?

try:
                if isinstance(field, FileField):
                    initial = self.initial.get(name, field.initial)
                    value = field.clean(value, initial)
                else:
                    value = field.clean(value)
                self.cleaned_data[name] = value
                if hasattr(self, 'clean_%s' % name):
                    value = getattr(self, 'clean_%s' % name)()
                    self.cleaned_data[name] = value
            except ValidationError as e:
                self.add_error(name, e)

在调用了field.clean()之后,通过hasattr方法判定开发者是否有针对各字段定义验证。
至此,整个Form表单验证的流程逻辑就清晰了。

 类似资料: