523 lines
21 KiB
Python
523 lines
21 KiB
Python
"""parse XML files and build a space with objects
|
|
it aggregates this files and manage redefine and exists attributes
|
|
|
|
Created by:
|
|
EOLE (http://eole.orion.education.fr)
|
|
Copyright (C) 2005-2018
|
|
|
|
Forked by:
|
|
Cadoles (http://www.cadoles.com)
|
|
Copyright (C) 2019-2021
|
|
|
|
distribued with GPL-2 or later license
|
|
|
|
This program 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.
|
|
|
|
This program 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 this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
"""
|
|
from typing import Optional
|
|
|
|
from .i18n import _
|
|
from .xmlreflector import XMLReflector
|
|
from .utils import valid_variable_family_name
|
|
from .error import SpaceObjShallNotBeUpdated, DictConsistencyError
|
|
from .path import Path
|
|
|
|
# RougailObjSpace's elements that shall be forced to the Redefinable type
|
|
FORCE_REDEFINABLES = ('family', 'follower', 'service', 'disknod', 'variables')
|
|
# RougailObjSpace's elements that shall be forced to the UnRedefinable type
|
|
FORCE_UNREDEFINABLES = ('value',)
|
|
# RougailObjSpace's elements that shall not be modify
|
|
UNREDEFINABLE = ('multi', 'type',)
|
|
# RougailObjSpace's elements that did not created automaticly
|
|
FORCE_ELEMENTS = ('property_', 'information')
|
|
# XML text are convert has name
|
|
FORCED_TEXT_ELTS_AS_NAME = ('choice', 'property', 'value',)
|
|
|
|
FORCE_TAG = {'family': 'variable'}
|
|
|
|
|
|
# _____________________________________________________________________________
|
|
# special types definitions for the Object Space's internal representation
|
|
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): # pylint: disable=R0903
|
|
"""Atomic object (means can only define one time)
|
|
"""
|
|
|
|
|
|
class Redefinable(RootRougailObject): # pylint: disable=R0903
|
|
"""Object that could be redefine
|
|
"""
|
|
|
|
|
|
class UnRedefinable(RootRougailObject): # pylint: disable=R0903
|
|
"""Object that could not be redefine
|
|
"""
|
|
|
|
|
|
class ObjSpace: # pylint: disable=R0903
|
|
"""
|
|
Base object space
|
|
"""
|
|
|
|
|
|
def convert_boolean(value: str) -> bool:
|
|
"""Boolean coercion. The Rougail XML may contain srings like `True` or `False`
|
|
"""
|
|
if isinstance(value, bool):
|
|
return value
|
|
if value == 'True':
|
|
return True
|
|
return False
|
|
|
|
|
|
class RougailObjSpace:
|
|
"""Rougail ObjectSpace is an object's reflexion of the XML elements
|
|
"""
|
|
|
|
def __init__(self,
|
|
xmlreflector: XMLReflector,
|
|
rougailconfig: 'RougailConfig',
|
|
) -> None:
|
|
self.space = ObjSpace()
|
|
self.paths = Path(rougailconfig)
|
|
|
|
self.forced_text_elts_as_name = set(FORCED_TEXT_ELTS_AS_NAME)
|
|
self.list_conditions = {}
|
|
self.valid_enums = {}
|
|
self.booleans_attributs = []
|
|
self.has_dyn_option = False
|
|
self.types = {}
|
|
|
|
self.make_object_space_classes(xmlreflector)
|
|
self.rougailconfig = rougailconfig
|
|
|
|
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 xmlreflector.dtd.iterelements():
|
|
attrs = {}
|
|
if dtd_elt.name in FORCE_REDEFINABLES:
|
|
clstype = Redefinable
|
|
elif not dtd_elt.attributes() and dtd_elt.name not in FORCE_UNREDEFINABLES:
|
|
clstype = Atom
|
|
else:
|
|
clstype = UnRedefinable
|
|
forced_text_elt = dtd_elt.type == 'mixed'
|
|
for dtd_attr in dtd_elt.iterattributes():
|
|
if set(dtd_attr.itervalues()) == {'True', 'False'}:
|
|
# it's a boolean
|
|
self.booleans_attributs.append(dtd_attr.name)
|
|
if dtd_attr.default_value:
|
|
# set default value for this attribute
|
|
default_value = dtd_attr.default_value
|
|
if dtd_attr.name in self.booleans_attributs:
|
|
default_value = convert_boolean(default_value)
|
|
attrs[dtd_attr.name] = default_value
|
|
if dtd_attr.name.endswith('_type'):
|
|
self.types[dtd_attr.name] = default_value
|
|
if dtd_attr.name == 'redefine':
|
|
# has a redefine attribute, so it's a Redefinable object
|
|
clstype = Redefinable
|
|
if dtd_attr.name == 'name' and forced_text_elt:
|
|
# child.text should be transform has a "name" attribute
|
|
forced_text_elt = False
|
|
|
|
if forced_text_elt is True:
|
|
self.forced_text_elts_as_name.add(dtd_elt.name)
|
|
|
|
# create ObjectSpace object
|
|
setattr(self, dtd_elt.name, type(dtd_elt.name.capitalize(), (clstype,), attrs))
|
|
for elt in FORCE_ELEMENTS:
|
|
setattr(self, elt, type(self._get_elt_name(elt), (RootRougailObject,), dict()))
|
|
|
|
@staticmethod
|
|
def _get_elt_name(elt) -> str:
|
|
name = elt.capitalize()
|
|
if name.endswith('_'):
|
|
name = name[:-1]
|
|
return name
|
|
|
|
def xml_parse_document(self,
|
|
xmlfile,
|
|
document,
|
|
namespace,
|
|
):
|
|
"""Parses a Rougail XML file and populates the RougailObjSpace
|
|
"""
|
|
redefine_variables = []
|
|
self._xml_parse(xmlfile,
|
|
document,
|
|
self.space,
|
|
namespace,
|
|
redefine_variables,
|
|
)
|
|
|
|
def _xml_parse(self, # pylint: disable=R0913
|
|
xmlfile,
|
|
document,
|
|
space,
|
|
namespace,
|
|
redefine_variables,
|
|
) -> None:
|
|
# var to check unique family name in a XML file
|
|
family_names = []
|
|
for child in document:
|
|
if not isinstance(child.tag, str):
|
|
# doesn't proceed the XML commentaries
|
|
continue
|
|
if child.tag == 'family':
|
|
if child.attrib['name'] in family_names:
|
|
msg = _(f'Family "{child.attrib["name"]}" is set several times')
|
|
raise DictConsistencyError(msg, 44, [xmlfile])
|
|
family_names.append(child.attrib['name'])
|
|
try:
|
|
# variable objects creation
|
|
exists, variableobj = self.get_variableobj(xmlfile,
|
|
child,
|
|
space,
|
|
namespace,
|
|
redefine_variables,
|
|
)
|
|
except SpaceObjShallNotBeUpdated:
|
|
continue
|
|
self.set_text(child,
|
|
variableobj,
|
|
)
|
|
self.set_attributes(xmlfile,
|
|
child,
|
|
variableobj,
|
|
)
|
|
self.remove(child,
|
|
variableobj,
|
|
redefine_variables,
|
|
)
|
|
if not exists:
|
|
self.set_path(namespace,
|
|
document,
|
|
variableobj,
|
|
space,
|
|
)
|
|
self.add_to_tree_structure(variableobj,
|
|
space,
|
|
child,
|
|
namespace,
|
|
)
|
|
if list(child) != []:
|
|
self._xml_parse(xmlfile,
|
|
child,
|
|
variableobj,
|
|
namespace,
|
|
redefine_variables,
|
|
)
|
|
|
|
def get_variableobj(self,
|
|
xmlfile: str,
|
|
child: list,
|
|
space,
|
|
namespace,
|
|
redefine_variables,
|
|
): # pylint: disable=R0913
|
|
"""
|
|
retrieves or creates Rougail Object Subspace objects
|
|
"""
|
|
tag = FORCE_TAG.get(child.tag, child.tag)
|
|
obj = getattr(self, 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 False, getattr(space, child.tag)
|
|
return False, obj(xmlfile, name)
|
|
# UnRedefinable object
|
|
if child.tag not in vars(space):
|
|
setattr(space, child.tag, [])
|
|
return False, 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,
|
|
): # pylint: disable=R0913
|
|
"""A redefinable object could be created or updated
|
|
"""
|
|
existed_var = self.get_existed_obj(name,
|
|
xmlfile,
|
|
space,
|
|
child,
|
|
namespace,
|
|
)
|
|
if existed_var:
|
|
# if redefine is set to object, default value is False
|
|
# otherwise it's always a redefinable object
|
|
default_redefine = child.tag in FORCE_REDEFINABLES
|
|
redefine = convert_boolean(subspace.get('redefine', default_redefine))
|
|
if redefine is True:
|
|
if isinstance(existed_var, self.variable): # pylint: disable=E1101
|
|
if namespace == self.rougailconfig['variable_namespace']:
|
|
redefine_variables.append(name)
|
|
else:
|
|
redefine_variables.append(space.path + '.' + name)
|
|
existed_var.xmlfiles.append(xmlfile)
|
|
return True, existed_var
|
|
exists = convert_boolean(subspace.get('exists', True))
|
|
if exists is False:
|
|
raise SpaceObjShallNotBeUpdated()
|
|
msg = _(f'"{child.tag}" named "{name}" cannot be re-created in "{xmlfile}", '
|
|
f'already defined')
|
|
raise DictConsistencyError(msg, 45, existed_var.xmlfiles)
|
|
# object deos not exists
|
|
exists = convert_boolean(subspace.get('exists', False))
|
|
if exists is True:
|
|
# manage object only if already exists, so cancel
|
|
raise SpaceObjShallNotBeUpdated()
|
|
redefine = convert_boolean(subspace.get('redefine', False))
|
|
if redefine is True:
|
|
# cannot redefine an inexistant object
|
|
msg = _(f'Redefined object: "{name}" does not exist yet')
|
|
raise DictConsistencyError(msg, 46, [xmlfile])
|
|
tag = FORCE_TAG.get(child.tag, child.tag)
|
|
if tag not in vars(space):
|
|
setattr(space, tag, {})
|
|
obj = getattr(self, child.tag)(xmlfile, name)
|
|
return False, obj
|
|
|
|
def get_existed_obj(self,
|
|
name: str,
|
|
xmlfile: str,
|
|
space: str,
|
|
child,
|
|
namespace: str,
|
|
) -> None:
|
|
"""if an object exists, return it
|
|
"""
|
|
if child.tag in ['variable', 'family']:
|
|
valid_variable_family_name(name, [xmlfile])
|
|
if child.tag == 'variable': # pylint: disable=E1101
|
|
if namespace != self.rougailconfig['variable_namespace']:
|
|
name = space.path + '.' + name
|
|
if not self.paths.path_is_defined(name):
|
|
return None
|
|
old_family_name = self.paths.get_variable_family_path(name)
|
|
if space.path != old_family_name:
|
|
msg = _(f'Variable "{name}" was previously create in family "{old_family_name}", '
|
|
f'now it is in "{space.path}"')
|
|
raise DictConsistencyError(msg, 47, space.xmlfiles)
|
|
return self.paths.get_variable(name)
|
|
# it's not a family
|
|
tag = FORCE_TAG.get(child.tag, child.tag)
|
|
children = getattr(space, tag, {})
|
|
if name in children and isinstance(children[name], getattr(self, child.tag)):
|
|
return children[name]
|
|
return None
|
|
|
|
def set_text(self,
|
|
child,
|
|
variableobj,
|
|
) -> None:
|
|
"""set text
|
|
"""
|
|
if child.text is None or child.tag in self.forced_text_elts_as_name:
|
|
return
|
|
text = child.text.strip()
|
|
if text:
|
|
variableobj.text = text
|
|
|
|
def set_attributes(self,
|
|
xmlfile,
|
|
child,
|
|
variableobj,
|
|
):
|
|
""" set attributes to an object
|
|
"""
|
|
redefine = convert_boolean(child.attrib.get('redefine', False))
|
|
if redefine and child.tag == 'variable':
|
|
# delete old values
|
|
has_value = hasattr(variableobj, 'value')
|
|
if has_value and len(child) != 0:
|
|
del variableobj.value
|
|
for attr, val in child.attrib.items():
|
|
if redefine and attr in UNREDEFINABLE:
|
|
msg = _(f'cannot redefine attribute "{attr}" for variable "{child.attrib["name"]}"'
|
|
f' already defined')
|
|
raise DictConsistencyError(msg, 48, variableobj.xmlfiles[:-1])
|
|
if attr in self.booleans_attributs:
|
|
val = convert_boolean(val)
|
|
if attr == 'name' and getattr(variableobj, 'name', None):
|
|
# do not redefine name
|
|
continue
|
|
setattr(variableobj, attr, val)
|
|
|
|
def remove(self,
|
|
child,
|
|
variableobj,
|
|
redefine_variables,
|
|
):
|
|
"""Rougail object tree manipulations
|
|
"""
|
|
if child.tag == 'variable':
|
|
if child.attrib.get('remove_choice', False):
|
|
if variableobj.type != 'choice':
|
|
msg = _(f'cannot remove choices for variable "{variableobj.path}"'
|
|
f' the variable has type "{variableobj.type}"')
|
|
raise DictConsistencyError(msg, 33, variableobj.xmlfiles)
|
|
variableobj.choice = []
|
|
if child.attrib.get('remove_check', False):
|
|
self.remove_check(variableobj.name)
|
|
if child.attrib.get('remove_condition', False):
|
|
self.remove_condition(variableobj.name)
|
|
if child.attrib.get('remove_fill', False):
|
|
self.remove_fill(variableobj.name)
|
|
if child.tag == 'fill':
|
|
for target in child:
|
|
if target.tag == 'target' and target.text in redefine_variables:
|
|
self.remove_fill(target.text)
|
|
|
|
def remove_check(self, name):
|
|
"""Remove a check with a specified target
|
|
"""
|
|
if hasattr(self.space.constraints, 'check'):
|
|
remove_checks = []
|
|
for idx, check in enumerate(self.space.constraints.check): # pylint: disable=E1101
|
|
for target in check.target:
|
|
if target.name == name:
|
|
remove_checks.append(idx)
|
|
remove_checks.sort(reverse=True)
|
|
for idx in remove_checks:
|
|
self.space.constraints.check.pop(idx) # pylint: disable=E1101
|
|
|
|
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): # 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] # pylint: disable=E1101
|
|
|
|
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
|
|
for target in fill.target:
|
|
if target.name == 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,
|
|
namespace,
|
|
document,
|
|
variableobj,
|
|
space,
|
|
):
|
|
"""Fill self.paths attributes
|
|
"""
|
|
if isinstance(variableobj, self.variable): # pylint: disable=E1101
|
|
if 'name' in document.attrib:
|
|
family_name = document.attrib['name']
|
|
else:
|
|
family_name = namespace
|
|
|
|
if isinstance(space, self.family) and space.leadership:
|
|
leader = space.path
|
|
else:
|
|
leader = None
|
|
self.paths.add_variable(namespace,
|
|
variableobj.name,
|
|
space.path,
|
|
document.attrib.get('dynamic') is not None,
|
|
variableobj,
|
|
leader,
|
|
)
|
|
elif isinstance(variableobj, self.family): # pylint: disable=E1101
|
|
family_name = variableobj.name
|
|
if namespace != self.rougailconfig['variable_namespace']:
|
|
family_name = space.path + '.' + family_name
|
|
self.paths.add_family(namespace,
|
|
family_name,
|
|
variableobj,
|
|
space.path,
|
|
)
|
|
elif isinstance(variableobj, self.variables):
|
|
variableobj.path = variableobj.name
|
|
|
|
@staticmethod
|
|
def add_to_tree_structure(variableobj,
|
|
space,
|
|
child,
|
|
namespace: str,
|
|
) -> None:
|
|
"""add a variable to the tree
|
|
"""
|
|
variableobj.namespace = namespace
|
|
if isinstance(variableobj, Redefinable):
|
|
name = variableobj.name
|
|
tag = FORCE_TAG.get(child.tag, child.tag)
|
|
getattr(space, tag)[name] = variableobj
|
|
elif isinstance(variableobj, UnRedefinable):
|
|
getattr(space, child.tag).append(variableobj)
|
|
else:
|
|
setattr(space, child.tag, variableobj)
|