diff --git a/invenio/modules/jsonalchemy/parser.py b/invenio/modules/jsonalchemy/parser.py index 67b372cbc..bb6ac4c7b 100644 --- a/invenio/modules/jsonalchemy/parser.py +++ b/invenio/modules/jsonalchemy/parser.py @@ -1,919 +1,918 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2013 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ invenio.modules.jsonalchemy.parser --------------------------------- Fields and models configuration loader. This module uses `pyparsing ` to read from thedifferent configuration files the field and model definitions. Extensions to both parsers could be added inside jsonext.parsers """ import os import six from pyparsing import ParseException, FollowedBy, Suppress, OneOrMore, Word, \ LineEnd, ZeroOrMore, Optional, Literal, alphas, alphanums, \ originalTextFor, nestedExpr, quotedString, removeQuotes, lineEnd, \ empty, col, restOfLine, delimitedList, Each, Keyword, commaSeparatedList, \ Group from .errors import FieldParserException, ModelParserException from .registry import fields_definitions, models_definitions, parsers ParseException.defaultWhitespaceChars = (' \r\t') COMMENT = (Literal("#") + restOfLine + LineEnd()).suppress() IDENT = Word(alphanums + '_') DICT_DEF = originalTextFor(nestedExpr('{', '}')) LIST_DEF = originalTextFor(nestedExpr('[', ']')) DICT_ACCESS = LIST_ACCESS = originalTextFor(IDENT + nestedExpr('[', ']')) PYTHON_ALLOWED_EXPR = (DICT_DEF ^ LIST_DEF ^ DICT_ACCESS ^ LIST_ACCESS ^ restOfLine ).setParseAction(lambda toks: toks[0]) def indentedBlock(expr, indent_stack, indent=True): """ Helper method for defining space-delimited indentation blocks, such as those used to define block statements in Python source code. There is also a version in pyparsing but doesn't seem to be working fine with JSONAlchemy cfg files. """ #TODO: update this to use indentStact from new versions of pyparsing def check_sub_indent(string, location, tokens): """Helper functions to check indentation""" cur_col = col(location, string) if cur_col > indent_stack[-1]: indent_stack.append(cur_col) else: raise ParseException(string, location, "not a subentry") def check_unindent(string, location, tokens): """Helper function to check 'undentation'""" if location >= len(string): return cur_col = col(location, string) if not(cur_col < indent_stack[-1] and cur_col <= indent_stack[-2]): raise ParseException(string, location, "not an unindent") def do_unindent(): """Helper function to unindent""" indent_stack.pop() indent = lineEnd.suppress() + empty + empty.copy()\ .setParseAction(check_sub_indent) undent = FollowedBy(empty).setParseAction(check_unindent) undent.setParseAction(do_unindent) return indent + expr + undent def _create_field_parser(): """ Creates a parser that can handle field definitions BFN like grammar:: """ indent_stack = [1] #Independent/special decorators persistent_identifier = ( Keyword('@persistent_identifier').suppress() + nestedExpr() ).setResultsName('pid').setParseAction(lambda toks: int(toks[0][0])) override = Keyword('@override').suppress()\ .setResultsName('override')\ .setParseAction(lambda toks: True) extend = Keyword('@extend').suppress()\ .setResultsName('extend')\ .setParseAction(lambda toks: True) rule_decorators = (Optional(persistent_identifier) & Optional(override) & Optional(extend)) # Field definition decorators field_decorators = Each( [Optional(p.parser.parse_element(indent_stack)) for p in parsers if issubclass(p.parser, DecoratorBaseExtensionParser)]) # Creator rules creator_body = ( Optional(field_decorators).setResultsName('decorators') + Word(alphas, alphanums + '_') + Literal(',').suppress() + quotedString.setParseAction(removeQuotes) + Literal(',').suppress() + PYTHON_ALLOWED_EXPR ).setParseAction(lambda toks: { 'source_format': toks[-3], 'source_tags': toks[-2].split(' '), 'function': compile(toks[-1].strip(), '', 'eval'), 'type': 'creator', 'decorators': toks.decorators.asDict()} ).setResultsName('creator_def', listAllMatches=True) creator = (Keyword('creator:').suppress() + indentedBlock(OneOrMore(creator_body), indent_stack)) # Derived and calculated rules der_calc_body = (Optional(field_decorators).setResultsName('decorators') + PYTHON_ALLOWED_EXPR) derived = ( Keyword('derived:').suppress() + indentedBlock(der_calc_body, indent_stack) ).setParseAction(lambda toks: { 'source_format': 'derived', 'source_tags': None, 'function': compile(toks[-1].strip(), '', 'eval'), 'type': 'derived', 'decorators': toks.decorators.asDict()}).setResultsName('derived_def') calculated = ( Keyword('calculated:').suppress() + indentedBlock(der_calc_body, indent_stack) ).setParseAction(lambda toks: { 'source_format': 'calculated', 'source_tags': None, 'function': compile(toks[-1].strip(), '', 'eval'), 'type': 'calculated', 'decorators': toks.decorators.asDict() }).setResultsName('calculated_def') rule_sections = [Optional(creator | derived | calculated), ] rule_sections.extend([Optional(p.parser.parse_element(indent_stack)) for p in parsers if issubclass(p.parser, FieldBaseExtensionParser)]) json_id = (IDENT + Optional(Suppress(',') + delimitedList(Word(alphanums + '_'))) + Suppress(':') ).setResultsName('field')\ .setParseAction(lambda toks: {'json_id': toks[0], 'aliases': toks[1:]}) rule = Group(Optional(rule_decorators) + json_id + indentedBlock(Each(rule_sections), indent_stack) ) return OneOrMore(COMMENT.suppress() | rule) def _create_model_parser(): """ Creates a parser that can handle model definitions. BFN like grammar:: Note: Unlike the field configuration files where you can specify more than one field inside each file for the models only one definition is allowed by file. """ def build_dict_for_fields(tokens): """ Build the dictionary wih the field definitions: {'field_name': 'json_identifier'} """ dict_ = dict() for token in tokens: if len(token) == 1: dict_[token[0]] = token[0] else: dict_[token[1]] = token[0] return dict_ indent_stack = [1] field = Group(Word(alphanums + '_') + Optional(Literal('=').suppress() + Word(alphanums + '_'))) fields = (Keyword('fields:').suppress() + indentedBlock(ZeroOrMore(field), indent_stack) ).setParseAction(build_dict_for_fields).setResultsName('fields') bases = (Keyword('bases:').suppress() + indentedBlock(commaSeparatedList, indent_stack) ).setResultsName('bases') sections = [fields, Optional(bases), ] sections.extend([Optional(p.parser.parse_element(indent_stack)) for p in parsers if issubclass(p.parser, ModelBaseExtensionParser)]) rules = Each(sections) return ZeroOrMore(COMMENT) & rules class FieldParser(object): """Field definitions parser""" _field_definitions = {} """Dictionary containing all the rules needed to create and validate json fields""" _legacy_field_matchings = {} """Dictionary containing matching between the legacy master format and the current json""" _field_extensions = None """Field only parser extensions""" _decorator_before_extensions = None """Decorator before only parser extensions""" _decorator_on_extensions = None """Decorator on only parser extensions""" _decorator_after_extensions = None """Decorator after only parser extensions""" def __init__(self, namespace): #Autodiscover cfg files self.files = list(fields_definitions(namespace)) self.__namespace = namespace @classmethod def field_extensions(cls): """From the parser registry gets only the field parser extensions""" if cls._field_extensions is None: cls._field_extensions = dict( (module.parser.__parsername__, module.parser) for module in parsers if issubclass(module.parser, FieldBaseExtensionParser)) return cls._field_extensions @classmethod def decorator_before_extensions(cls): """From the parser registry gets only the field parser extensions""" if cls._decorator_before_extensions is None: cls._decorator_before_extensions = dict( (module.parser.__parsername__, module.parser) for module in parsers if issubclass(module.parser, DecoratorBeforeEvalBaseExtensionParser)) return cls._decorator_before_extensions @classmethod def decorator_on_extensions(cls): """From the parser registry gets only the field parser extensions""" if cls._decorator_on_extensions is None: cls._decorator_on_extensions = dict( (module.parser.__parsername__, module.parser) for module in parsers if issubclass(module.parser, DecoratorOnEvalBaseExtensionParser)) return cls._decorator_on_extensions @classmethod def decorator_after_extensions(cls): """From the parser registry gets only the field parser extensions""" if cls._decorator_after_extensions is None: cls._decorator_after_extensions = dict( (module.parser.__parsername__, module.parser) for module in parsers if issubclass(module.parser, DecoratorAfterEvalBaseExtensionParser)) return cls._decorator_after_extensions @classmethod def field_definitions(cls, namespace): """ From a given namespace gets all the field definitions from it. If the namespace does not exist, it tries to create it first """ if namespace not in cls._field_definitions: cls.reparse(namespace) return cls._field_definitions.get(namespace) @classmethod def field_definition_model_based(cls, field_name, model_name, namespace): """ Based on a model name (and namespace) it gets the real field definition. """ new_model = ModelParser.resolve_models(model_name, namespace) json_id = field_name for j, f in six.iteritems(new_model['fields']): if f == field_name: json_id = j break return cls.field_definitions(namespace).get(json_id, None) @classmethod def legacy_field_matchings(cls, namespace): """ Gets all the legacy mappings for a given namespace. If the namespace does not exist, it tries to create it first :see: guess_legacy_field_names() """ if namespace not in cls._legacy_field_matchings: cls.reparse(namespace) return cls._legacy_field_matchings.get(namespace) @classmethod def reparse(cls, namespace): """ Invalidates the cached version of all the fields inside the given namespace and parse them again. """ cls._field_definitions[namespace] = {} cls._legacy_field_matchings = {} cls(namespace)._create() def _create(self): """ Fills up _field_definitions and _legacy_field_matchings dictionary with the rules defined inside the configuration files. This method should not be used (unless you really know what your are doing), use instead :meth:`reparse` """ stand_by_rules = [] for field_file in self.files: parser = _create_field_parser() try: rules = parser.parseFile(field_file, parseAll=True) except ParseException as e: raise FieldParserException( "Cannot parse file '%s',\n%s" % (field_file, str(e))) for rule in rules: if (rule.field['json_id'] in self.__class__._field_definitions[self.__namespace])\ and not rule.extend and not rule.override: raise FieldParserException( "Name error: '%s' field is duplicated '%s'" % (rule.field['json_id'], field_file)) if (rule.field['json_id'] not in self.__class__._field_definitions[self.__namespace])\ and (rule.extend or rule.override): stand_by_rules.append(rule) else: self._create_rule(rule) for rule in stand_by_rules: if rule.field['json_id'] not in \ self.__class__._field_definitions[self.__namespace]: raise FieldParserException( "Name error: '%s' field is not defined but is " "marked as 'extend' or 'override'" % (rule.field['json_id'], )) self._create_rule(rule) def _create_rule(self, rule): """ Creates the field and legacy definitions. The result looks like this:: {key: { override: True/False, extend: True/False, aliases: [], pid: num/None, rules: {'master_format_1': [{rule1}, {rule2}, ...], 'master_format_2': [....], ...... 'calculated': [....], 'derived': [...]} .... extensions .... } } Each of the rule (rule1, rule2, etc.) has the same content:: {'source_format' : source_format/calculated/derived, 'source_tag' : source_tag/None, 'function' : python code to apply to the master value, 'decorators' : {} } """ json_id = rule.field['json_id'] #TODO: check if pyparsing can handle this! all_type_def = [] if rule.creator_def: all_type_def.extend(rule.creator_def.asList()) if rule.calculated_def: all_type_def.append(rule.calculated_def) elif rule.derived_def: all_type_def.append(rule.derived_def) rules = self.__class__._field_definitions[self.__namespace][json_id]\ .get('rules', {}) if rule.extend else dict() for field_def in all_type_def: self.__create_decorators_content(rule, field_def) if field_def['source_format'] not in rules: rules[field_def['source_format']] = list() rules[field_def['source_format']].append(field_def) if 'json' not in rules: rules['json'] = [{'source_format': 'json', 'source_tags': [json_id], 'function': compile('value', '', 'eval'), 'type': 'creator', 'decorators': {'before': {}, 'on': {}, 'after': {} } }] rule_dict = dict() rule_dict['aliases'] = rule.field['aliases'] rule_dict['pid'] = rule.pid if rule.pid is not '' else None rule_dict['override'] = rule.override if rule.override else False rule_dict['extend'] = rule.extend if rule.extend else False rule_dict['rules'] = rules if rule.override: self.__class__._field_definitions[self.__namespace][json_id]\ .update(rule_dict) elif rule.extend: self.__class__._field_definitions[self.__namespace][json_id][ 'aliases'].extend(rule_dict['aliases']) self.__class__._field_definitions[self.__namespace][json_id][ 'extend'] = True else: self.__class__._field_definitions[self.__namespace][json_id] = \ rule_dict self.__resolve_parser_extensions(rule) def __resolve_parser_extensions(self, rule): """ For each of the extension available it tries to apply it in the incoming rule """ json_id = rule.field['json_id'] for name, parser in six.iteritems(self.__class__.field_extensions()): if getattr(rule, name, None): self.__class__._field_definitions[self.__namespace][ json_id][name] = parser.create_element(rule, self.__namespace) def __create_decorators_content(self, rule, field_def): """Extracts from the rule all the possible decorators""" decorators = {'before': {}, 'on': {}, 'after': {}} for name, parser in six.iteritems(self.__class__.decorator_before_extensions()): if name in field_def['decorators']: decorators['before'][name] = \ parser.create_element(rule, field_def, field_def['decorators'][name], self.__namespace) for name, parser in six.iteritems(self.__class__.decorator_on_extensions()): if name in field_def['decorators']: decorators['on'][name] = \ parser.create_element(rule, field_def, field_def['decorators'][name], self.__namespace) for name, parser in six.iteritems(self.__class__.decorator_after_extensions()): if name in field_def['decorators']: decorators['after'][name] = \ parser.create_element(rule, field_def, field_def['decorators'][name], self.__namespace) field_def['decorators'] = decorators class ModelParser(object): """Record model parser""" _model_definitions = {} """Contains all the model definitions order by namespace""" _parser_extensions = None """Model only parser extensions""" def __init__(self, namespace): self.files = list(models_definitions(namespace)) self.__namespace = namespace @classmethod def parser_extensions(cls): """From the parser registry gets only the model parser extensions""" if cls._parser_extensions is None: cls._parser_extensions = \ dict((module.parser.__parsername__, module.parser) for module in parsers if issubclass(module.parser, ModelBaseExtensionParser)) return cls._parser_extensions @classmethod def model_definitions(cls, namespace): """ From a given namespace gets all the model definitions from it. If the namespace does not exist, it tries to create it first """ if namespace not in cls._model_definitions: cls.reparse(namespace) return cls._model_definitions.get(namespace) @classmethod def resolve_models(cls, model_list, namespace): """ From a given list of model definitions resolves all the field conflicts and returns a new model definition containing all the information from the model list. The field definitions are resolved from left-to-right. :param model_list: It could be also a string, in which case the model definition is returned as it is. :return: Dictionary containing the union of the model definitions. """ if model_list == '__default__': return { 'fields': dict( zip(FieldParser.field_definitions(namespace).keys(), FieldParser.field_definitions(namespace).keys())), 'bases': [], } if isinstance(model_list, six.string_types): try: return cls.model_definitions(namespace)[model_list] except KeyError: return { 'fields': dict( zip(FieldParser.field_definitions(namespace).keys(), FieldParser.field_definitions(namespace).keys())), 'bases': [], } new_model = {'fields': dict(), 'bases': list()} for model in model_list: if model == '__default__': new_model['fields'].update( zip(FieldParser.field_definitions(namespace).keys(), FieldParser.field_definitions(namespace).keys())) elif model not in cls.model_definitions(namespace): new_model['fields'].update( dict(zip(FieldParser.field_definitions(namespace).keys(), FieldParser.field_definitions(namespace).keys()))) else: model_def = cls.model_definitions(namespace).get(model, {}) new_model['fields'].update(model_def.get('fields', {})) new_model['bases'].extend(model_def.get('bases', [])) for key, value in six.iteritems(model_def): if key in ('fields', 'bases'): continue new_model[key] = cls.parser_extensions()[key]\ .extend_model(new_model.get(key), value) return new_model @classmethod def reparse(cls, namespace): """ Invalidates the cached version of all the models inside the given namespace and parse it again. """ cls._model_definitions[namespace] = {} cls(namespace)._create() def _create(self): """ Fills up _model_definitions dictionary with what is written inside the *.cfg model descriptions It also resolve inheritance at creation time and name matching for the field names present inside the model file The result looks like this:: {'model': {'fields': {'name_for_fieldfield1': json_id1, 'name_for_field2': json_id2, .... 'name_for_fieldN': fieldN }, 'bases: [(inherit_from_list), ...] }, ... } This method should not be used (unless you really know what your are doing), use instead :meth:`reparse` :raises: ModelParserException in case of missing model definition (helpful if we use inheritance) or in case of unknown field name. """ for model_file in self.files: parser = _create_model_parser() model_name = os.path.basename(model_file).split('.')[0] if model_name in \ self.__class__._model_definitions[self.__namespace]: raise ModelParserException( "Already defined model: %s" % (model_name,)) self.__class__._model_definitions[self.__namespace][model_name] = { 'fields': {}, 'bases': [], } try: model_definition = parser.parseFile(model_file, parseAll=True) except ParseException as e: raise ModelParserException( "Cannot parse file %s,\n%s" % (model_file, str(e))) if not model_definition.fields: raise ModelParserException("Field definition needed") if any([json_id not in FieldParser.field_definitions(self.__namespace) for json_id in model_definition.fields.values()]): raise ModelParserException( "At least one field is no find in the field " "definitions for file '%s'" % (model_file)) self.__class__._model_definitions[self.__namespace][model_name][ 'fields'] = model_definition.fields self.__class__._model_definitions[self.__namespace][model_name][ 'bases'] = model_definition.bases.asList() \ if model_definition.bases else [] self.__resolve_parser_extensions(model_name, model_definition) self.__resolve_inheritance() def __resolve_inheritance(self): """Resolves the inheritance""" def resolve_ext_inheritance(ext_name, model_definition): for inherit_from in model_definition['bases']: base_model = self.__class__.model_definitions( self.__namespace)[inherit_from] model_definition[ext_name] = \ self.__class__.parser_extensions()[ext_name].inherit_model( model_definition.get(ext_name), resolve_ext_inheritance(ext_name, base_model)) return model_definition.get(ext_name) def resolve_field_inheritance(model_definition): fields = {} for inherit_from in model_definition['bases']: base_model = self.__class__.model_definitions( self.__namespace)[inherit_from] fields.update(resolve_field_inheritance(base_model)) if fields: inverted_fields = dict((v, k) for k, v in six.iteritems(fields)) inverted_model_fields = dict((v, k) for k, v in six.iteritems( model_definition['fields'])) inverted_fields.update(inverted_model_fields) fields = dict((v, k) for k, v in six.iteritems(inverted_fields)) else: fields.update(model_definition['fields']) return fields for model_definition in \ self.__class__.model_definitions(self.__namespace).values(): model_definition['fields'] = resolve_field_inheritance( model_definition) for name, model_ext in \ six.iteritems(self.__class__.parser_extensions()): - if name in model_definition: - model_definition[name] = resolve_ext_inheritance( - name, model_definition) + model_definition[name] = resolve_ext_inheritance( + name, model_definition) def __resolve_parser_extensions(self, model_name, model_def): """ For each of the extension available it tries to apply it in the incoming rule """ for name, parser in six.iteritems(self.__class__.parser_extensions()): if name in model_def: self.__class__._model_definitions[self.__namespace][ model_name][name] = parser.create_element( model_def, self.__namespace) def guess_legacy_field_names(fields, master_format, namespace): """ Using the legacy rules written in the config file (@legacy) tries to find the equivalent json field for one or more legacy fields. >>> guess_legacy_fields(('100__a', '245'), 'marc', 'recordext') {'100__a':['authors[0].full_name'], '245':['title']} """ res = {} if isinstance(fields, six.string_types): fields = (fields, ) for field in fields: try: res[field] = FieldParser.legacy_field_matchings( namespace)[master_format].get(field, []) except (KeyError, TypeError): res[field] = [] return res def get_producer_rules(field, code, namespace, model=['__default__']): # pylint: disable=W0102 """ From the field definitions gets all the producer rules related with the field and the code (using also the namespace). For each producer rule the first element are the 'preconditions' to apply the rules and the second one are the actual rules. >>> get_producer_rules('_first_author', 'json_for_marc', 'recordext') [((), {'100__a': 'full_name', '100__e': 'relator_name', '100__h': 'CCID', '100__i': 'INSPIRE_number', '100__u': 'affiliation'})] >>> get_producer_rules('title', 'json_for_marc', 'recordext') [[((), {'245__a': 'title', '245__b': 'subtitle', '245__k': 'form'})] """ try: return FieldParser.field_definition_model_based(field, model, namespace)\ .get('producer', {}).get(code, []) except AttributeError: raise KeyError(field) class BaseExtensionParser(type): # pylint: disable=R0921 """Metaclass for the configuration file extensions""" def __new__(mcs, name, bases, dict_): if not dict_.get('__parsername__'): dict_['__parsername__'] = name.lower().replace('parser', '') return super(BaseExtensionParser, mcs).__new__(mcs, name, bases, dict_) @classmethod def parse_element(mcs, indent_stack): """ Using pyparsing defines a piece of the grammar to parse the extension from configuration file :return: pyparsing ParseElement """ raise NotImplementedError() @classmethod def create_element(mcs, *args, **kwargs): """ Once the extension is parsed defines the actions that have to be taken to store inside the field_definitions the information needed or useful. """ raise NotImplementedError() @classmethod def add_info_to_field(mcs, *args, **kwargs): """ Defines which information goes inside the ``__meta_metadata__`` dictionary and how. """ raise NotImplementedError() @classmethod def evaluate(mcs, *args, **kwargs): """ Once the extension information is added to the field, whenever it gets accessed or modify this method is call for each of the extension set in the metadata of this field. """ raise NotImplementedError() class FieldBaseExtensionParser(six.with_metaclass(BaseExtensionParser)): # pylint: disable=W0223,W0232,R0903,R0921 """Base class for field parser extensions.""" @classmethod def add_info_to_field(cls, json_id, info): """ Should create the content of ``__meta_metadata__.json.extension_name`` """ raise NotImplementedError() @classmethod def evaluate(cls, json, field_name, action, args): """ Depending on the extension perform the actions that it defines using the current value as parameter. (It could cause side effects on the current json) """ raise NotImplementedError() class ModelBaseExtensionParser(six.with_metaclass(BaseExtensionParser)): # pylint: disable=W0223,W0232,R0903,R0921 """Base class for model parser extensions.""" @classmethod def inherit_model(cls, current_value, base_value): """ When a model inherits from other (or several) it should resolve the inheritance taking the current value and the base value from the extension. """ raise NotImplementedError() @classmethod def extend_model(cls, current_value, new_value): """ When a json object is using several models this method should provide the logic to extend the content of the extensions. :return: the content of model[extension] """ raise NotImplementedError() @classmethod def add_info_to_field(cls, info): """ Defines which information goes inside the ``__meta_metadata__.__model__`` dictionary and how. """ raise NotImplementedError() @classmethod def evaluate(cls, obj, args): """ Gets the current object (typically a SmartJson object) and modifies it accordingly with the extension nature. """ raise NotImplementedError() class DecoratorBaseExtensionParser(six.with_metaclass(BaseExtensionParser)): # pylint: disable=W0223,W0232,R0903 """ Base class for decorator parser extension""" pass class DecoratorBeforeEvalBaseExtensionParser(DecoratorBaseExtensionParser): # pylint: disable=W0223,W0232,R0903,R0921 """ Base class for decorator parser extensions, this ones will be evaluated *before* any operation on the value. """ @classmethod def evaluate(cls, reader, args): """Evaluates ``args`` and returns a boolean depending on them""" raise NotImplementedError() class DecoratorOnEvalBaseExtensionParser(DecoratorBaseExtensionParser): # pylint: disable=W0223,W0232,R0903,R0921 """ Base class for decorator parser extensions, this ones will be evaluated *while* the rule gets evaluated with the input value. (Therefore they have access to ``value``) This decorators are only useful for ``creator`` definitions. """ @classmethod def evaluate(cls, value, namespace, args): """ Evaluates ``args`` with the master value from the input and returns a boolean depending on them. """ raise NotImplementedError() class DecoratorAfterEvalBaseExtensionParser(DecoratorBaseExtensionParser): # pylint: disable=W0223,W0232,R0903,R0921 """ Base class for decorator parser extensions, this one will be evaluated *after* the rule gets evaluated anb before setting the value to the json. """ @classmethod def add_info_to_field(cls, json_id, info, args): """ When adding a new field to the json, if its definition uses the current decorator it adds the needed content in a way that ``evaluate`` can use. """ raise NotImplementedError() @classmethod def evaluate(cls, json, field_name, action, args): """ Depending on the decorator performs the actions that it defines using the current value as parameter. (It could cause side effects on the current json). """ raise NotImplementedError()