Skip to content

Schema validate_partial segment violates structure of error_dict when complex invalidation raised #148

@mklein0

Description

@mklein0

The code merging the invalid result from the validate_partial function cannot handle complex/nested invalid errors.

https://github.com/formencode/formencode/blob/2.0.0a1/formencode/schema.py#L205-L216

            for validator in self.chained_validators:
                if (not hasattr(validator, 'validate_partial') or not getattr(
                        validator, 'validate_partial_form', False)):
                    continue
                try:
                    validator.validate_partial(value_dict, state)
                except Invalid as e:
                    sub_errors = e.unpack_errors()
                    if not isinstance(sub_errors, dict):
                        # Can't do anything here
                        continue
                    merge_dicts(errors, sub_errors)

This code works for a validator like formencode.validators.FieldsMatch which only invalidates the current level of the dict. But if the validator is another Schema validator which returns a nested error_dict, the structure of the resulting error dict is inconsistent.

The following is an example unit test: test_sample.py

from formencode import (
    Invalid,
    validators,
)
from formencode.schema import Schema


class Name(Schema):
    fname = validators.String(not_empty=True)
    mi = validators.String(max=1, if_missing=None, if_empty=None)
    lname = validators.String(not_empty=True)


class ChainedTest(Schema):
    a = validators.String()
    a_confirm = validators.String()

    b = validators.String()
    b_confirm = validators.String()

    chained_validators = [validators.FieldsMatch('a', 'a_confirm'),
                            validators.FieldsMatch('b', 'b_confirm')]


class NestedSchemaValidator(Schema):

    name = Name(if_missing=None)
    chained_test = ChainedTest(if_missing=None)


class PartialFormValidator(validators.FormValidator):
    """
    Call the NestedSchemaValidator validator as a post/chained validator to a form
    """
    validate_partial_form = True

    def validate_partial(self, field_dict, state):
        self._validate_python(field_dict, state)

    def _validate_python(self, field_dict, state):
        NestedSchemaValidator().to_python(field_dict, state)


class PartialFormSchemaValidator(Schema):

    allow_extra_fields = True
    chained_validators = [
        PartialFormValidator(),
    ]

def test_partial_form_with_nested_forms_validators():

    def invalid_eq(self, other):
        if not isinstance(other, type(self)):
            return False

        # state is more debug than identity information
        return (
            self.msg == other.msg
            and self.error_dict == other.error_dict
            and self.error_list == other.error_list
        )

    Invalid.__eq__ = invalid_eq

    nested_validator = NestedSchemaValidator()
    partial_validator = PartialFormSchemaValidator()

    try:
        partial_validator.to_python(
            {'chained_test': {'a': '1', 'a_confirm': '2', 'b': '3', 'b_confirm': '4'}}
        )
    except Invalid as e:
        partial_error = e

    else:
        assert False

    try:
        nested_validator.to_python(
            {'chained_test': {'a': '1', 'a_confirm': '2', 'b': '3', 'b_confirm': '4'}}
        )
    except Invalid as e:
        expected_error = e

    else:
        assert False

    assert expected_error.error_dict == partial_error.error_dict

    try:
        partial_validator.to_python(
            {'chained_test': {}}
        )
    except Invalid as e:
        partial_error = e

    else:
        assert False

    try:
        nested_validator.to_python(
            {'chained_test': {}}
        )
    except Invalid as e:
        expected_error = e

    else:
        assert False

    assert expected_error.error_dict == partial_error.error_dict

Sample run:

$ nosetests test_sample.py 
F
======================================================================
FAIL: test_sample.test_partial_form_with_nested_forms_validators
----------------------------------------------------------------------
Traceback (most recent call last):
  File "VirtualEnvs/formencode/lib/python3.6/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/Repositories/git/3rdparty/formencode/test_sample.py", line 89, in test_partial_form_with_nested_forms_validators
    assert expected_error.error_dict == partial_error.error_dict
AssertionError: 
>>  assert Invalid('chained_test: a_confirm: Fields do not match\nb_confirm: Fields do not match', {'chained_test': {'a': '1', 'a_confirm': '2', 'b': '3', 'b_confirm': '4'}}, None, None, {'chained_test': Invalid('a_confirm: Fields do not match\nb_confirm: Fields do not match', {'a': '1', 'a_confirm': '2', 'b': '3', 'b_confirm': '4'}, None, None, {'a_confirm': 'Fields do not match', 'b_confirm': 'Fields do not match'})}).error_dict == Invalid('chained_test: a_confirm: Fields do not match              \nb_confirm: Fields do not match', {'chained_test': {'a': '1', 'a_confirm': '2', 'b': '3', 'b_confirm': '4'}}, None, None, {'chained_test': {'a_confirm': 'Fields do not match', 'b_confirm': 'Fields do not match'}}).error_dict
    

----------------------------------------------------------------------
Ran 1 test in 0.023s

FAILED (failures=1)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions