diff --git a/src/rougail/annotator/constrainte.py b/src/rougail/annotator/constrainte.py index 51bce614..fedc78e3 100644 --- a/src/rougail/annotator/constrainte.py +++ b/src/rougail/annotator/constrainte.py @@ -335,7 +335,6 @@ class ConstrainteAnnotator: new_target = self.objectspace.target(variable.xmlfiles) new_target.type = type_ new_target.name = listvar - new_target.index = target.index new_targets.append(new_target) remove_targets.append(target_idx) remove_targets.sort(reverse=True) diff --git a/src/rougail/annotator/variable.py b/src/rougail/annotator/variable.py index d490276d..61a902cb 100644 --- a/src/rougail/annotator/variable.py +++ b/src/rougail/annotator/variable.py @@ -64,10 +64,19 @@ class VariableAnnotator: if variable.type != 'symlink' and not hasattr(variable, 'description'): variable.description = variable.name if hasattr(variable, 'value'): - for value in variable.value: + value_to_del = [] + for idx, value in enumerate(variable.value): if not hasattr(value, 'type'): value.type = variable.type - value.name = CONVERT_OPTION.get(value.type, {}).get('func', str)(value.name) + if 'name' not in vars(value): + value_to_del.append(idx) + else: + value.name = CONVERT_OPTION.get(value.type, {}).get('func', str)(value.name) + value_to_del.sort(reverse=True) + for idx in value_to_del: + del variable.value[idx] + if not variable.value: + del variable.value for key, value in RENAME_ATTIBUTES.items(): setattr(variable, value, getattr(variable, key)) setattr(variable, key, None) diff --git a/src/rougail/error.py b/src/rougail/error.py index 9148621d..ada8a764 100644 --- a/src/rougail/error.py +++ b/src/rougail/error.py @@ -2,12 +2,15 @@ class ConfigError(Exception): pass + class FileNotFound(ConfigError): pass + class TemplateError(ConfigError): pass + class TemplateDisabled(TemplateError): """Template is disabled. """ diff --git a/src/rougail/objspace.py b/src/rougail/objspace.py index a8645746..7853ef2a 100644 --- a/src/rougail/objspace.py +++ b/src/rougail/objspace.py @@ -1,3 +1,8 @@ +"""parse XML files and build a space with objects +it aggregates this files and manage redefine and exists attributes +""" +from typing import Optional + from .i18n import _ from .xmlreflector import XMLReflector from .utils import normalize_family @@ -19,30 +24,39 @@ FORCED_TEXT_ELTS_AS_NAME = ('choice', 'property', 'value', 'target') # _____________________________________________________________________________ # special types definitions for the Object Space's internal representation -class RootRougailObject: - def __init__(self, xmlfiles): +class RootRougailObject: # pylint: disable=R0903 + """Root object + """ + def __init__(self, + xmlfiles, + name=None, + ): if not isinstance(xmlfiles, list): xmlfiles = [xmlfiles] self.xmlfiles = xmlfiles + if name: + self.name = name -class Atom(RootRougailObject): - pass +class Atom(RootRougailObject): # pylint: disable=R0903 + """Atomic object (means can only define one time) + """ -class Redefinable(RootRougailObject): - pass +class Redefinable(RootRougailObject): # pylint: disable=R0903 + """Object that could be redefine + """ -class UnRedefinable(RootRougailObject): - pass +class UnRedefinable(RootRougailObject): # pylint: disable=R0903 + """Object that could not be redefine + """ -class ObjSpace: +class ObjSpace: # pylint: disable=R0903 """ Base object space """ - pass class RougailObjSpace: @@ -52,26 +66,24 @@ class RougailObjSpace: def __init__(self, xmlreflector: XMLReflector, ) -> None: - self.index = 0 - self.xmlreflector = xmlreflector self.space = ObjSpace() self.paths = Path() - self.redefine_variables = None - self.forced_text_elts = set() self.forced_text_elts_as_name = set(FORCED_TEXT_ELTS_AS_NAME) self.list_conditions = {} self.booleans_attributs = [] - self.make_object_space_classes() + self.make_object_space_classes(xmlreflector) - def make_object_space_classes(self): + def make_object_space_classes(self, + xmlreflector: XMLReflector, + ) -> None: """Create Rougail ObjectSpace class types from DDT file It enables us to create objects like: File(), Variable(), Ip(), Family(), Constraints()... and so on. """ - for dtd_elt in self.xmlreflector.dtd.iterelements(): + for dtd_elt in xmlreflector.dtd.iterelements(): attrs = {} if dtd_elt.name in FORCE_REDEFINABLES: clstype = Redefinable @@ -95,7 +107,6 @@ class RougailObjSpace: clstype = Redefinable if dtd_attr.name == 'name' and forced_text_elt: # child.text should be transform has a "name" attribute - self.forced_text_elts.add(dtd_elt.name) forced_text_elt = False if forced_text_elt is True: @@ -104,14 +115,19 @@ class RougailObjSpace: # create ObjectSpace object setattr(self, dtd_elt.name, type(dtd_elt.name.capitalize(), (clstype,), attrs)) for elt in FORCE_ELEMENTS: - name = elt.capitalize() - if name.endswith('_'): - name = name[:-1] - setattr(self, elt, type(name, (RootRougailObject,), dict())) + setattr(self, elt, type(self._get_elt_name(elt), (RootRougailObject,), dict())) - def display_xmlfiles(self, - xmlfiles: list, - ) -> str: + @staticmethod + def _get_elt_name(elt) -> str: + name = elt.capitalize() + if name.endswith('_'): + name = name[:-1] + return name + + @staticmethod + def display_xmlfiles(xmlfiles: list) -> str: + """The function format xmlfiles informations to generate errors + """ if len(xmlfiles) == 1: return '"' + xmlfiles[0] + '"' return '"' + '", "'.join(xmlfiles[:-1]) + '"' + ' and ' + '"' + xmlfiles[-1] + '"' @@ -124,33 +140,42 @@ class RougailObjSpace: ): """Parses a Rougail XML file and populates the RougailObjSpace """ + redefine_variables = [] + self._xml_parse(xmlfile, + document, + space, + namespace, + redefine_variables, + ) + + def _xml_parse(self, + xmlfile, + document, + space, + namespace, + redefine_variables, + ) -> None: # var to check unique family name in a XML file family_names = [] for child in document: - # this index enables us to reorder objects if not isinstance(child.tag, str): # doesn't proceed the XML commentaries continue if child.tag == 'family': if child.attrib['name'] in family_names: - raise DictConsistencyError(_(f'Family "{child.attrib["name"]}" is set several times in "{xmlfile}"'), 44) + msg = _(f'Family "{child.attrib["name"]}" is set several times in "{xmlfile}"') + raise DictConsistencyError(msg, 44) family_names.append(child.attrib['name']) - if child.tag == 'variables': - # variables has no name, so force namespace name - child.attrib['name'] = namespace - if child.tag == 'value' and child.text is None: - # remove empty value - continue - # variable objects creation try: + # variable objects creation variableobj = self.get_variableobj(xmlfile, child, space, namespace, + redefine_variables, ) except SpaceObjShallNotBeUpdated: continue - self.index += 1 self.set_text(child, variableobj, ) @@ -160,9 +185,9 @@ class RougailObjSpace: ) self.remove(child, variableobj, + redefine_variables, ) - self.set_path(child, - namespace, + self.set_path(namespace, document, variableobj, ) @@ -172,52 +197,67 @@ class RougailObjSpace: namespace, ) if list(child) != []: - self.xml_parse_document(xmlfile, - child, - variableobj, - namespace, - ) + self._xml_parse(xmlfile, + child, + variableobj, + namespace, + redefine_variables, + ) def get_variableobj(self, xmlfile: str, child: list, space, namespace, + redefine_variables, ): """ retrieves or creates Rougail Object Subspace objects """ obj = getattr(self, child.tag) + name = self._get_name(child, namespace) if Redefinable in obj.__mro__: return self.create_or_update_redefinable_object(xmlfile, child.attrib, space, child, + name, namespace, + redefine_variables, ) if Atom in obj.__mro__: if child.tag in vars(space): # Atom instance has to be a singleton here # we do not re-create it, we reuse it return getattr(space, child.tag) - return obj(xmlfile) + return obj(xmlfile, name) if child.tag not in vars(space): setattr(space, child.tag, []) - return obj(xmlfile) + return obj(xmlfile, name) + + def _get_name(self, + child, + namespace: str, + ) -> Optional[str]: + if child.tag == 'variables': + return namespace + if 'name' in child.attrib: + return child.attrib['name'] + if child.text and child.tag in self.forced_text_elts_as_name: + return child.text.strip() + return None def create_or_update_redefinable_object(self, xmlfile, subspace, space, child, + name, namespace, + redefine_variables, ): - if child.tag in self.forced_text_elts_as_name: - name = child.text - else: - name = subspace['name'] - if child.tag == 'family': - name = normalize_family(name) + """A redefinable object could be created or updated + """ existed_var = self.get_existed_obj(name, space, child, @@ -229,47 +269,65 @@ class RougailObjSpace: default_redefine = child.tag in FORCE_REDEFINABLES redefine = self.convert_boolean(subspace.get('redefine', default_redefine)) if redefine is True: + if isinstance(existed_var, self.variable): # pylint: disable=E1101 + if namespace == Config['variable_namespace']: + redefine_variables.append(name) + else: + redefine_variables.append(space.path + '.' + name) existed_var.xmlfiles.append(xmlfile) return existed_var exists = self.convert_boolean(subspace.get('exists', True)) if exists is False: raise SpaceObjShallNotBeUpdated() xmlfiles = self.display_xmlfiles(existed_var.xmlfiles) - raise DictConsistencyError(_(f'"{child.tag}" named "{name}" cannot be re-created in "{xmlfile}", already defined in {xmlfiles}'), 45) - # if this object must only be modified if object already exists + msg = _(f'"{child.tag}" named "{name}" cannot be re-created in "{xmlfile}", ' + f'already defined in {xmlfiles}') + raise DictConsistencyError(msg, 45) + # object deos not exists exists = self.convert_boolean(subspace.get('exists', False)) if exists is True: + # manage object only if already exists, so cancel raise SpaceObjShallNotBeUpdated() redefine = self.convert_boolean(subspace.get('redefine', False)) - if redefine is False: - if child.tag not in vars(space): - setattr(space, child.tag, {}) - return getattr(self, child.tag)(xmlfile) - raise DictConsistencyError(_(f'Redefined object in "{xmlfile}": "{name}" does not exist yet'), 46) + if redefine is True: + # cannot redefine an inexistant object + msg = _(f'Redefined object in "{xmlfile}": "{name}" does not exist yet') + raise DictConsistencyError(msg, 46) + if child.tag not in vars(space): + setattr(space, child.tag, {}) + return getattr(self, child.tag)(xmlfile, name) def get_existed_obj(self, name: str, space: str, child, namespace: str, - ): - if isinstance(space, self.family): + ) -> None: + """if an object exists, return it + """ + if child.tag == 'family': + name = normalize_family(name) + if isinstance(space, self.family): # pylint: disable=E1101 if namespace != Config['variable_namespace']: name = space.path + '.' + name if not self.paths.path_is_defined(name): - return + return None old_family_name = self.paths.get_variable_family_name(name) if namespace != Config['variable_namespace']: old_family_name = namespace + '.' + old_family_name if space.path != old_family_name: xmlfiles = self.display_xmlfiles(space.xmlfiles) - raise DictConsistencyError(_(f'Variable was previously create in family "{old_family_name}", now it is in "{space.path}" in {xmlfiles}'), 47) + msg = _(f'Variable was previously create in family "{old_family_name}", ' + f'now it is in "{space.path}" in {xmlfiles}') + raise DictConsistencyError(msg, 47) return self.paths.get_variable_obj(name) children = getattr(space, child.tag, {}) if name in children: return children[name] + return None - def convert_boolean(self, value): + @staticmethod + def convert_boolean(value: str) -> bool: """Boolean coercion. The Rougail XML may contain srings like `True` or `False` """ if isinstance(value, bool): @@ -281,15 +339,13 @@ class RougailObjSpace: def set_text(self, child, variableobj, - ): - if child.text is None: + ) -> None: + """set text + """ + if child.text is None or child.tag in self.forced_text_elts_as_name: return text = child.text.strip() - if not text: - return - if child.tag in self.forced_text_elts_as_name: - variableobj.name = text - else: + if text: variableobj.text = text def set_attributes(self, @@ -297,6 +353,8 @@ class RougailObjSpace: child, variableobj, ): + """ set attributes to an object + """ redefine = self.convert_boolean(child.attrib.get('redefine', False)) if redefine and child.tag == 'variable': # delete old values @@ -306,7 +364,9 @@ class RougailObjSpace: for attr, val in child.attrib.items(): if redefine and attr in UNREDEFINABLE: xmlfiles = self.display_xmlfiles(variableobj.xmlfiles[:-1]) - raise DictConsistencyError(_(f'cannot redefine attribute "{attr}" for variable "{child.attrib["name"]}" in "{xmlfile}", already defined in {xmlfiles}'), 48) + msg = _(f'cannot redefine attribute "{attr}" for variable "{child.attrib["name"]}"' + f' in "{xmlfile}", already defined in {xmlfiles}') + raise DictConsistencyError(msg, 48) if attr in self.booleans_attributs: val = self.convert_boolean(val) if attr == 'name' and getattr(variableobj, 'name', None): @@ -317,6 +377,7 @@ class RougailObjSpace: def remove(self, child, variableobj, + redefine_variables, ): """Rougail object tree manipulations """ @@ -327,67 +388,63 @@ class RougailObjSpace: self.remove_condition(variableobj.name) if child.attrib.get('remove_fill', False): self.remove_fill(variableobj.name) - if child.tag == 'fill' and child.attrib['target'] in self.redefine_variables: + if child.tag == 'fill' and child.attrib['target'] in redefine_variables: self.remove_fill(child.attrib['target']) def remove_check(self, name): - if hasattr(self.space, 'constraints') and hasattr(self.space.constraints, 'check'): - remove_checks = [] - for idx, check in enumerate(self.space.constraints.check): - if hasattr(check, 'target') and check.target == name: - remove_checks.append(idx) + """Remove a check with a specified target + """ + remove_checks = [] + for idx, check in enumerate(self.space.constraints.check): # pylint: disable=E1101 + if check.target == name: + remove_checks.append(idx) + remove_checks.sort(reverse=True) + for idx in remove_checks: + self.space.constraints.check.pop(idx) # pylint: disable=E1101 - remove_checks = list(set(remove_checks)) - remove_checks.sort(reverse=True) - for idx in remove_checks: - self.space.constraints.check.pop(idx) - - def remove_condition(self, name): + def remove_condition(self, + name: str, + ) -> None: + """Remove a condition with a specified source + """ remove_conditions = [] - for idx, condition in enumerate(self.space.constraints.condition): + for idx, condition in enumerate(self.space.constraints.condition): # pylint: disable=E1101 if condition.source == name: remove_conditions.append(idx) + remove_conditions.sort(reverse=True) for idx in remove_conditions: - del self.space.constraints.condition[idx] + del self.space.constraints.condition[idx] # pylint: disable=E1101 - def remove_fill(self, name): - if hasattr(self.space, 'constraints') and hasattr(self.space.constraints, 'fill'): - remove_fills= [] - for idx, fill in enumerate(self.space.constraints.fill): - if hasattr(fill, 'target') and fill.target == name: - remove_fills.append(idx) - - remove_fills = list(set(remove_fills)) - remove_fills.sort(reverse=True) - for idx in remove_fills: - self.space.constraints.fill.pop(idx) + def remove_fill(self, + name: str, + ) -> None: + """Remove a fill with a specified target + """ + remove_fills = [] + for idx, fill in enumerate(self.space.constraints.fill): # pylint: disable=E1101 + if fill.target == name: + remove_fills.append(idx) + remove_fills.sort(reverse=True) + for idx in remove_fills: + self.space.constraints.fill.pop(idx) # pylint: disable=E1101 def set_path(self, - child, namespace, document, variableobj, ): """Fill self.paths attributes """ - if child.tag == 'variable': - family_name = document.attrib['name'] - family_name = normalize_family(family_name) + if isinstance(variableobj, self.variable): # pylint: disable=E1101 + family_name = normalize_family(document.attrib['name']) self.paths.add_variable(namespace, - child.attrib['name'], + variableobj.name, family_name, document.attrib.get('dynamic') is not None, variableobj, ) - if child.attrib.get('redefine', 'False') == 'True': - if namespace == Config['variable_namespace']: - self.redefine_variables.append(child.attrib['name']) - else: - self.redefine_variables.append(namespace + '.' + family_name + '.' + - child.attrib['name']) - - elif child.tag == 'family': - family_name = normalize_family(child.attrib['name']) + elif isinstance(variableobj, self.family): # pylint: disable=E1101 + family_name = normalize_family(variableobj.name) if namespace != Config['variable_namespace']: family_name = namespace + '.' + family_name self.paths.add_family(namespace, @@ -396,14 +453,14 @@ class RougailObjSpace: ) variableobj.path = self.paths.get_family_path(family_name, namespace) - def add_to_tree_structure(self, - variableobj, + @staticmethod + def add_to_tree_structure(variableobj, space, child, - namespace, - ): - if not hasattr(variableobj, 'index'): - variableobj.index = self.index + namespace: str, + ) -> None: + """add a variable to the tree + """ variableobj.namespace = namespace if isinstance(variableobj, Redefinable): name = variableobj.name