# -*- coding: utf-8 -*- """ Gestion du mini-langage de template On travaille sur les fichiers cibles """ from importlib.machinery import SourceFileLoader from shutil import copy import logging from typing import Dict, Any from subprocess import call from os import listdir, makedirs, getcwd, chdir from os.path import dirname, join, isfile, abspath, normpath, relpath from Cheetah.Template import Template as ChtTemplate from Cheetah.NameMapper import NotFound as CheetahNotFound try: from tiramisu3 import Config from tiramisu3.error import PropertiesOptionError # pragma: no cover except ModuleNotFoundError: # pragma: no cover from tiramisu import Config from tiramisu.error import PropertiesOptionError from .config import Config from .error import FileNotFound, TemplateError from .i18n import _ from .utils import normalize_family log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @classmethod def cl_compile(kls, *args, **kwargs): kwargs['compilerSettings'] = {'directiveStartToken' : '%', 'cheetahVarStartToken' : '%%', 'EOLSlurpToken' : '%', 'PSPStartToken' : 'µ' * 10, 'PSPEndToken' : 'µ' * 10, 'commentStartToken' : 'µ' * 10, 'commentEndToken' : 'µ' * 10, 'multiLineCommentStartToken' : 'µ' * 10, 'multiLineCommentEndToken' : 'µ' * 10} return kls.old_compile(*args, **kwargs) ChtTemplate.old_compile = ChtTemplate.compile ChtTemplate.compile = cl_compile class CheetahTemplate(ChtTemplate): """classe pour personnaliser et faciliter la construction du template Cheetah """ def __init__(self, filename: str, context, eosfunc: Dict, destfilename, variable, ): """Initialize Creole CheetahTemplate """ extra_context = {'normalize_family': normalize_family, 'rougail_filename': destfilename } if variable: extra_context['rougail_variable'] = variable ChtTemplate.__init__(self, file=filename, searchList=[context, eosfunc, extra_context]) # FORK of Cheetah function, do not replace '\\' by '/' def serverSidePath(self, path=None, normpath=normpath, abspath=abspath ): # strange... if path is None and isinstance(self, str): path = self if path: return normpath(abspath(path)) # original code return normpath(abspath(path.replace("\\", '/'))) elif hasattr(self, '_filePath') and self._filePath: # pragma: no cover return normpath(abspath(self._filePath)) else: # pragma: no cover return None class CreoleValue: def __str__(self): return str(self._value) class CreoleLeaderIndex(CreoleValue): def __init__(self, value, follower, index, ) -> None: self._value = value self._follower = follower self._index = index def __getattr__(self, name): if name not in self._follower: raise AttributeError() value = self._follower[name] if isinstance(value, PropertiesOptionError): raise AttributeError() return value def __lt__(self, value): return self._value.__lt__(value) def __le__(self, value): return self._value.__le__(value) def __eq__(self, value): return self._value.__eq__(value) def __ne__(self, value): return self._value.__ne__(value) def __gt__(self, value): return self._value.__gt__(value) def __ge__(self, value): return self._value >= value def __add__(self, value): return self._value.__add__(value) def __radd__(self, value): return value + self._value class CreoleLeader(CreoleValue): def __init__(self, value, ) -> None: self._value = value self._follower = {} def __getitem__(self, index): """Get a leader.follower at requested index. """ followers = {key: values[index] for key, values in self._follower.items()} return CreoleLeaderIndex(self._value[index], followers, index, ) def __iter__(self): """Iterate over leader.follower. Return synchronised value of leader.follower. """ for index in range(len(self._value)): yield self.__getitem__(index) def __len__(self): return len(self._value) def __contains__(self, value): return self._value.__contains__(value) async def add_follower(self, config, name: str, path: str, ): self._follower[name] = [] for index in range(len(self._value)): try: value = await config.option(path, index).value.get() except PropertiesOptionError as err: value = err self._follower[name].append(value) class CreoleExtra: def __init__(self, suboption: Dict) -> None: self.suboption = suboption def __getattr__(self, key: str) -> Any: return self.suboption[key] def __iter__(self): return iter(self.suboption.values()) class CreoleTemplateEngine: """Engine to process Creole cheetah template """ def __init__(self, config: Config, eosfunc_file: str, distrib_dir: str, tmp_dir: str, dest_dir: str, ) -> None: self.config = config self.dest_dir = dest_dir self.tmp_dir = tmp_dir self.distrib_dir = distrib_dir eos = {} if eosfunc_file is not None: eosfunc = SourceFileLoader('eosfunc', eosfunc_file).load_module() for func in dir(eosfunc): if not func.startswith('_'): eos[func] = getattr(eosfunc, func) self.eosfunc = eos self.rougail_variables_dict = {} async def load_eole_variables_rougail(self, optiondescription, ): for option in await optiondescription.list('all'): if await option.option.isoptiondescription(): if await option.option.isleadership(): for idx, suboption in enumerate(await option.list('all')): if idx == 0: leader = CreoleLeader(await suboption.value.get()) self.rougail_variables_dict[await suboption.option.name()] = leader else: await leader.add_follower(self.config, await suboption.option.name(), await suboption.option.path(), ) else: await self.load_eole_variables_rougail(option) else: self.rougail_variables_dict[await option.option.name()] = await option.value.get() async def load_eole_variables(self, optiondescription, ): families = {} for family in await optiondescription.list('all'): variables = {} for variable in await family.list('all'): if await variable.option.isoptiondescription(): if await variable.option.isleadership(): for idx, suboption in enumerate(await variable.list('all')): if idx == 0: leader = CreoleLeader(await suboption.value.get()) leader_name = await suboption.option.name() else: await leader.add_follower(self.config, await suboption.option.name(), await suboption.option.path(), ) variables[leader_name] = leader else: subfamilies = await self.load_eole_variables(variable) variables[await variable.option.name()] = subfamilies else: variables[await variable.option.name()] = await variable.value.get() families[await family.option.name()] = CreoleExtra(variables) return CreoleExtra(families) def patch_template(self, filename: str, tmp_dir: str, patch_dir: str, ) -> None: """Apply patch to a template """ patch_cmd = ['patch', '-d', tmp_dir, '-N', '-p1'] patch_no_debug = ['-s', '-r', '-', '--backup-if-mismatch'] patch_file = join(patch_dir, f'{filename}.patch') if isfile(patch_file): log.info(_("Patching template '{filename}' with '{patch_file}'")) rel_patch_file = relpath(patch_file, tmp_dir) ret = call(patch_cmd + patch_no_debug + ['-i', rel_patch_file]) if ret: # pragma: no cover patch_cmd_err = ' '.join(patch_cmd + ['-i', rel_patch_file]) log.error(_(f"Error applying patch: '{rel_patch_file}'\nTo reproduce and fix this error {patch_cmd_err}")) copy(join(self.distrib_dir, filename), tmp_dir) def prepare_template(self, filename: str, tmp_dir: str, patch_dir: str, ) -> None: """Prepare template source file """ log.info(_("Copy template: '{filename}' -> '{tmp_dir}'")) copy(filename, tmp_dir) self.patch_template(filename, tmp_dir, patch_dir) def process(self, source: str, true_destfilename: str, destfilename: str, variable: Any, ): """Process a cheetah template """ # full path of the destination file log.info(_(f"Cheetah processing: '{destfilename}'")) try: cheetah_template = CheetahTemplate(source, self.rougail_variables_dict, self.eosfunc, true_destfilename, variable, ) data = str(cheetah_template) except CheetahNotFound as err: # pragma: no cover varname = err.args[0][13:-1] raise TemplateError(_(f"Error: unknown variable used in template {source} to {destfilename} : {varname}")) except Exception as err: # pragma: no cover raise TemplateError(_(f"Error while instantiating template {source} to {destfilename}: {err}")) with open(destfilename, 'w') as file_h: file_h.write(data) def instance_file(self, filevar: Dict, tmp_dir: str, dest_dir: str, ) -> None: """Run templatisation on one file """ log.info(_("Instantiating file '{filename}'")) if 'variable' in filevar: variable = filevar['variable'] else: variable = None filenames = filevar['name'] if not isinstance(filenames, list): filenames = [filenames] if variable: variable = [variable] for idx, filename in enumerate(filenames): destfilename = join(dest_dir, filename[1:]) makedirs(dirname(destfilename), exist_ok=True) if variable: var = variable[idx] else: var = None source = join(tmp_dir, filevar['source']) if filevar['templating']: self.process(source, filename, destfilename, var, ) else: copy(source, destfilename) async def instance_files(self) -> None: """Run templatisation on all files """ ori_dir = getcwd() tmp_dir = relpath(self.tmp_dir, self.distrib_dir) dest_dir = relpath(self.dest_dir, self.distrib_dir) patch_dir = relpath(Config['patch_dir'], self.distrib_dir) chdir(self.distrib_dir) for option in await self.config.option.list(type='all'): namespace = await option.option.name() if namespace == Config['variable_namespace']: await self.load_eole_variables_rougail(option) else: families = await self.load_eole_variables(option) self.rougail_variables_dict[namespace] = families for template in listdir('.'): self.prepare_template(template, tmp_dir, patch_dir) for service_obj in await self.config.option('services').list('all'): for fills in await service_obj.list('all'): if await fills.option.name() in ['files', 'overrides']: for fill_obj in await fills.list('all'): fill = await fill_obj.value.dict() filename = fill['source'] if not isfile(filename): # pragma: no cover raise FileNotFound(_(f"File {filename} does not exist.")) if fill.get('activate', False): self.instance_file(fill, tmp_dir, dest_dir, ) else: log.debug(_("Instantiation of file '{filename}' disabled")) chdir(ori_dir) async def generate(config: Config, eosfunc_file: str, distrib_dir: str, tmp_dir: str, dest_dir: str, ) -> None: engine = CreoleTemplateEngine(config, eosfunc_file, distrib_dir, tmp_dir, dest_dir, ) await engine.instance_files()