def test_get_raises_invalid_validator_id(self): """Tests if class method 'get' in _Validator raises exception for invalid validator id. """ with self.assertRaisesRegexp( Exception, 'Invalid validator id: some invalid validator method name'): schema_utils.get_validator('some invalid validator method name')
def test_get_raises_invalid_validator_id(self): # type: () -> None """Tests if class method 'get' in _Validator raises exception for invalid validator id. """ with self.assertRaisesRegexp( # type: ignore[no-untyped-call] Exception, 'Invalid validator id: some invalid validator method name'): schema_utils.get_validator('some invalid validator method name')
def test_has_length_at_least_validator(self): """Tests if static method has_length_at_least returns true iff given list has length of at least the given value. """ has_len_at_least = schema_utils.get_validator('has_length_at_least') self.assertTrue(has_len_at_least(['elem'], 0)) self.assertTrue(has_len_at_least(['elem'], 1)) # boundary self.assertFalse(has_len_at_least(['elem'], 2))
def test_is_at_most_validator(self): """Tests if static method is_at_most returns true iff obj is at most a value. """ is_at_most = schema_utils.get_validator('is_at_most') self.assertTrue(is_at_most(2, 3)) self.assertTrue(is_at_most(2, 2)) # boundary self.assertFalse(is_at_most(2, 1))
def test_is_valid_numeric_expression_validator(self): """Tests for the is_valid_math_expression static method with numeric type. """ is_valid_math_expression = schema_utils.get_validator( 'is_valid_math_expression') self.assertFalse(is_valid_math_expression('a+b*2', False)) self.assertTrue(is_valid_math_expression('3+4/2', False))
def test_is_nonempty_validator(self): """Tests if static method is_nonempty returns true iff obj is not an empty str. """ is_nonempty = schema_utils.get_validator('is_nonempty') self.assertTrue(is_nonempty('non-empty string')) self.assertTrue(is_nonempty(' ')) self.assertTrue(is_nonempty(' ')) self.assertFalse(is_nonempty(''))
def test_is_url_fragment(self): validate_url_fragment = schema_utils.get_validator('is_url_fragment') self.assertTrue(validate_url_fragment('math')) self.assertTrue(validate_url_fragment('computer-science')) self.assertTrue(validate_url_fragment('bio-tech')) self.assertFalse(validate_url_fragment('')) self.assertFalse(validate_url_fragment('Abc')) self.assertFalse(validate_url_fragment('!@#$%^&*()_+='))
def test_is_valid_algebraic_expression_validator(self): # type: () -> None """Tests for the is_valid_algebraic_expression static method with algebraic type. """ is_valid_algebraic_expression = schema_utils.get_validator( 'is_valid_algebraic_expression') self.assertTrue(is_valid_algebraic_expression('a+b*2')) self.assertFalse(is_valid_algebraic_expression('3+4/2'))
def test_is_supported_audio_language_code(self): is_supported_audio_language_code = schema_utils.get_validator( 'is_supported_audio_language_code') self.assertTrue(is_supported_audio_language_code('en')) self.assertTrue(is_supported_audio_language_code('fr')) self.assertTrue(is_supported_audio_language_code('de')) self.assertFalse(is_supported_audio_language_code('')) self.assertFalse(is_supported_audio_language_code('zz')) self.assertFalse(is_supported_audio_language_code('test'))
def test_is_regex_matched(self): # type: () -> None is_regex_matched = schema_utils.get_validator('is_regex_matched') self.assertTrue(is_regex_matched( 'exploration.EXP_ID_1.WzEuNjI2NTgxNDQwOTVlKzEyXQ==WzE3NThd', r'(exploration|collection)\.\w+\.\w+')) self.assertFalse(is_regex_matched( 'WzEuNjI2NTgxNDQwOTVlKzEyXQ==WzE3NThd', r'(exploration|collection)\.\w+\.\w+'))
def _validate_validator(obj_type, validator): # type: (Text, Dict[Text, Any]) -> None """Validates the value of a 'validator' field. Args: obj_type: str. The type of the object. validator: dict. The Specs that needs to be validated. Raises: AssertionError. The object fails to validate against the schema. """ reference_dict = VALIDATOR_SPECS[obj_type] assert 'id' in validator, 'id is not present in validator' assert validator['id'] in reference_dict, ( '%s is not present in reference_dict' % validator['id']) customization_keys = list(validator.keys()) customization_keys.remove('id') assert ( set(customization_keys) == set(reference_dict[validator['id']].keys())), ( 'Missing keys: %s, Extra keys: %s' % ( list( set(reference_dict[validator['id']].keys()) - set(customization_keys)), list( set(customization_keys) - set(reference_dict[validator['id']].keys())))) for key in customization_keys: value = validator[key] schema = reference_dict[validator['id']][key] try: schema_utils.normalize_against_schema(value, schema) except Exception as e: raise AssertionError(e) # Check that the id corresponds to a valid normalizer function. validator_fn = schema_utils.get_validator(validator['id']) assert set(inspect.getargspec(validator_fn).args) == set( customization_keys + ['obj']), ( 'Missing keys: %s, Extra keys: %s' % ( list( set(customization_keys + ['obj']) - set(inspect.getargspec(validator_fn).args)), list( set(inspect.getargspec(validator_fn).args) - set(customization_keys + ['obj']))))
def _validate_validator(obj_type, validator): """Validates the value of a 'validator' field.""" reference_dict = VALIDATOR_SPECS[obj_type] assert 'id' in validator and validator['id'] in reference_dict customization_keys = list(validator.keys()) customization_keys.remove('id') assert (set(customization_keys) == set(reference_dict[validator['id']].keys())) for key in customization_keys: value = validator[key] schema = reference_dict[validator['id']][key] try: schema_utils.normalize_against_schema(value, schema) except Exception as e: raise AssertionError(e) # Check that the id corresponds to a valid normalizer function. validator_fn = schema_utils.get_validator(validator['id']) assert set(inspect.getargspec(validator_fn).args) == set( customization_keys + ['obj'])
def test_is_valid_math_equation_validator(self): # type: () -> None """Tests for the is_valid_math_equation static method.""" is_valid_math_equation = schema_utils.get_validator( 'is_valid_math_equation') self.assertTrue(is_valid_math_equation('a+b=c')) self.assertTrue(is_valid_math_equation('x^2+y^2=z^2')) self.assertTrue(is_valid_math_equation('y = m*x + b')) self.assertTrue( is_valid_math_equation('alpha^a + beta^b = gamma^(-c)')) self.assertTrue(is_valid_math_equation('a+b=0')) self.assertTrue(is_valid_math_equation('0=a+b')) self.assertTrue(is_valid_math_equation('(a/b)+c=(4^3)*a')) self.assertTrue(is_valid_math_equation('2^alpha-(-3) = 3')) self.assertTrue(is_valid_math_equation('(a+b)^2 = a^2 + b^2 + 2*a*b')) self.assertTrue(is_valid_math_equation('(a+b)^2 = a^2 + b^2 + 2ab')) self.assertTrue(is_valid_math_equation('x/a + y/b = 1')) self.assertTrue(is_valid_math_equation('3 = -5 + pi^x')) self.assertTrue(is_valid_math_equation('0.4 + 0.5 = alpha * 4')) self.assertTrue(is_valid_math_equation('sqrt(a+b)=c - gamma/2.4')) self.assertTrue(is_valid_math_equation('abs(35 - x) = 22.3')) self.assertFalse(is_valid_math_equation('3 -= 2/a')) self.assertFalse(is_valid_math_equation('3 == 2/a')) self.assertFalse(is_valid_math_equation('x + y = ')) self.assertFalse(is_valid_math_equation('(a+b = 0)')) self.assertFalse(is_valid_math_equation('pi = 3.1415')) self.assertFalse(is_valid_math_equation('a+b=0=a-b')) self.assertFalse(is_valid_math_equation('alpha - beta/c')) self.assertFalse(is_valid_math_equation('2^alpha-(-3*) = 3')) self.assertFalse(is_valid_math_equation('a~b = 0')) self.assertFalse(is_valid_math_equation('a+b<=0')) self.assertFalse(is_valid_math_equation('a+b>=0')) self.assertFalse(is_valid_math_equation('a+b<0')) self.assertFalse(is_valid_math_equation('a+b>0')) self.assertFalse(is_valid_math_equation('5+3=8')) self.assertFalse(is_valid_math_equation('(a+(b)=0')) self.assertFalse(is_valid_math_equation('a+b=c:)'))
def _convert_state_v34_dict_to_v35_dict(cls, question_state_dict): """Converts from version 34 to 35. Version 35 upgrades all explorations that use the MathExpressionInput interaction to use one of AlgebraicExpressionInput, NumericExpressionInput, or MathEquationInput interactions. Args: question_state_dict: dict. A dict where each key-value pair represents respectively, a state name and a dict used to initialize a State domain object. Returns: dict. The converted question_state_dict. """ is_valid_algebraic_expression = schema_utils.get_validator( 'is_valid_algebraic_expression') is_valid_numeric_expression = schema_utils.get_validator( 'is_valid_numeric_expression') is_valid_math_equation = schema_utils.get_validator( 'is_valid_math_equation') ltt = latex2text.LatexNodes2Text() if question_state_dict['interaction']['id'] == 'MathExpressionInput': new_answer_groups = [] types_of_inputs = set() for group in question_state_dict['interaction']['answer_groups']: new_answer_group = copy.deepcopy(group) for rule_spec in new_answer_group['rule_specs']: rule_input = ltt.latex_to_text(rule_spec['inputs']['x']) rule_input = exp_domain.clean_math_expression(rule_input) type_of_input = exp_domain.TYPE_INVALID_EXPRESSION if is_valid_algebraic_expression(rule_input): type_of_input = ( exp_domain.TYPE_VALID_ALGEBRAIC_EXPRESSION) elif is_valid_numeric_expression(rule_input): type_of_input = exp_domain.TYPE_VALID_NUMERIC_EXPRESSION elif is_valid_math_equation(rule_input): type_of_input = exp_domain.TYPE_VALID_MATH_EQUATION types_of_inputs.add(type_of_input) if type_of_input != exp_domain.TYPE_INVALID_EXPRESSION: rule_spec['inputs']['x'] = rule_input if type_of_input == exp_domain.TYPE_VALID_MATH_EQUATION: rule_spec['inputs']['y'] = 'both' rule_spec['rule_type'] = 'MatchesExactlyWith' new_answer_groups.append(new_answer_group) if exp_domain.TYPE_INVALID_EXPRESSION not in types_of_inputs: # If at least one rule input is an equation, we remove # all other rule inputs that are expressions. if exp_domain.TYPE_VALID_MATH_EQUATION in types_of_inputs: new_interaction_id = exp_domain.TYPE_VALID_MATH_EQUATION for group in new_answer_groups: new_rule_specs = [] for rule_spec in group['rule_specs']: if is_valid_math_equation( rule_spec['inputs']['x']): new_rule_specs.append(rule_spec) group['rule_specs'] = new_rule_specs # Otherwise, if at least one rule_input is an algebraic # expression, we remove all other rule inputs that are # numeric expressions. elif exp_domain.TYPE_VALID_ALGEBRAIC_EXPRESSION in ( types_of_inputs): new_interaction_id = ( exp_domain.TYPE_VALID_ALGEBRAIC_EXPRESSION) for group in new_answer_groups: new_rule_specs = [] for rule_spec in group['rule_specs']: if is_valid_algebraic_expression( rule_spec['inputs']['x']): new_rule_specs.append(rule_spec) group['rule_specs'] = new_rule_specs else: new_interaction_id = ( exp_domain.TYPE_VALID_NUMERIC_EXPRESSION) # Removing answer groups that have no rule specs left after # the filtration done above. new_answer_groups = [ answer_group for answer_group in new_answer_groups if (len(answer_group['rule_specs']) != 0) ] # Removing feedback keys, from voiceovers_mapping and # translations_mapping, that correspond to the rules that # got deleted. old_answer_groups_feedback_keys = [ answer_group['outcome']['feedback']['content_id'] for answer_group in ( question_state_dict['interaction']['answer_groups']) ] new_answer_groups_feedback_keys = [ answer_group['outcome']['feedback']['content_id'] for answer_group in (new_answer_groups) ] content_ids_to_delete = set( old_answer_groups_feedback_keys) - set( new_answer_groups_feedback_keys) for content_id in content_ids_to_delete: if content_id in question_state_dict[ 'recorded_voiceovers']['voiceovers_mapping']: del question_state_dict['recorded_voiceovers'][ 'voiceovers_mapping'][content_id] if content_id in question_state_dict[ 'written_translations']['translations_mapping']: del question_state_dict['written_translations'][ 'translations_mapping'][content_id] question_state_dict['interaction']['id'] = new_interaction_id question_state_dict['interaction']['answer_groups'] = ( new_answer_groups) if question_state_dict['interaction']['solution']: correct_answer = question_state_dict['interaction'][ 'solution']['correct_answer']['ascii'] correct_answer = exp_domain.clean_math_expression( correct_answer) question_state_dict['interaction']['solution'][ 'correct_answer'] = correct_answer return question_state_dict
def map(item): is_valid_math_expression = schema_utils.get_validator( 'is_valid_math_expression') is_valid_math_equation = schema_utils.get_validator( 'is_valid_math_equation') ltt = latex2text.LatexNodes2Text() unicode_to_text_mapping = ( MathExpressionValidationOneOffJob.UNICODE_TO_TEXT) inverse_trig_fns_mapping = ( MathExpressionValidationOneOffJob.INVERSE_TRIG_FNS_MAPPING) trig_fns = MathExpressionValidationOneOffJob.TRIG_FNS if not item.deleted: exploration = exp_fetchers.get_exploration_from_model(item) for state_name, state in exploration.states.items(): if state.interaction.id == 'MathExpressionInput': for group in state.interaction.answer_groups: for rule_spec in group.rule_specs: rule_input = ltt.latex_to_text( rule_spec.inputs['x']) # Shifting powers in trig functions to the end. # For eg. 'sin^2(x)' -> 'sin(x)^2'. for trig_fn in trig_fns: rule_input = re.sub( r'%s(\^\d)\((.)\)' % trig_fn, r'%s(\2)\1' % trig_fn, rule_input) # Adding parens to trig functions that don't have # any. For eg. 'cosA' -> 'cos(A)'. for trig_fn in trig_fns: rule_input = re.sub(r'%s(?!\()(.)' % trig_fn, r'%s(\1)' % trig_fn, rule_input) # The pylatexenc lib outputs the unicode values of # special characters like sqrt and pi, which is why # they need to be replaced with their corresponding # text values before performing validation. for unicode_char, text in ( unicode_to_text_mapping.items()): rule_input = rule_input.replace( unicode_char, text) # Replacing trig functions that have format which is # incompatible with the validations. for invalid_trig_fn, valid_trig_fn in ( inverse_trig_fns_mapping.items()): rule_input = rule_input.replace( invalid_trig_fn, valid_trig_fn) validity = 'Invalid' if is_valid_math_expression(rule_input): validity = 'Valid Expression' elif is_valid_math_equation(rule_input): validity = 'Valid Equation' output_values = '%s %s: %s' % (item.id, state_name, rule_input) yield (validity, output_values.encode('utf-8'))