rougail/src/rougail/objspace.py

535 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,
False,
)
def _xml_parse(self, # pylint: disable=R0913
xmlfile,
document,
space,
namespace,
redefine_variables,
is_dynamic,
) -> None:
# var to check unique family name in a XML file
family_names = []
for child in document:
if is_dynamic:
is_sub_dynamic = True
else:
is_sub_dynamic = document.attrib.get('dynamic') is not None
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,
is_sub_dynamic,
)
self.add_to_tree_structure(variableobj,
space,
child,
namespace,
)
if list(child) != []:
self._xml_parse(xmlfile,
child,
variableobj,
namespace,
redefine_variables,
is_sub_dynamic,
)
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 child.tag == 'service':
return child.attrib['name'] + '.' + child.attrib.get('type', 'service')
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,
is_dynamic,
):
"""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,
is_dynamic,
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)