Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
mklein0 opened this issue Feb 17, 2019 · 0 comments

Comments

@mklein0
Copy link

mklein0 commented Feb 17, 2019

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant