diff --git a/src/caoscrawler/crawl.py b/src/caoscrawler/crawl.py index b0f576a2c73342cc1301ff0f27b74bb519768541..3a78392e56b55b152f10760b7bec9f9b205263af 100644 --- a/src/caoscrawler/crawl.py +++ b/src/caoscrawler/crawl.py @@ -49,12 +49,17 @@ from caosdb.apiutils import compare_entities, merge_entities from copy import deepcopy from jsonschema import validate +from .macros import defmacro_constructor, macro_constructor import importlib SPECIAL_PROPERTIES_STRICT = ("description", "name", "id", "path") SPECIAL_PROPERTIES_NOT_STRICT = ("file", "checksum", "size") +# Register the macro functions from the submodule: +yaml.SafeLoader.add_constructor("!defmacro", defmacro_constructor) +yaml.SafeLoader.add_constructor("!macro", macro_constructor) + def check_identical(record1: db.Entity, record2: db.Entity, ignore_id=False): """ diff --git a/src/caoscrawler/macros/__init__.py b/src/caoscrawler/macros/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0acfb1763039a3bb800bbf0e26d6940b49d045cf --- /dev/null +++ b/src/caoscrawler/macros/__init__.py @@ -0,0 +1 @@ +from .macro_yaml_object import defmacro_constructor, macro_constructor diff --git a/src/caoscrawler/macros/macro_yaml_object.py b/src/caoscrawler/macros/macro_yaml_object.py new file mode 100644 index 0000000000000000000000000000000000000000..2022f518fe1e3d627fbdcb87595898561f825348 --- /dev/null +++ b/src/caoscrawler/macros/macro_yaml_object.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2022 Alexander Schlemmer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# + +# Function to expand a macro in yaml +# A. Schlemmer, 05/2022 + +from dataclasses import dataclass +from typing import Any +from copy import deepcopy +from string import Template + +@dataclass +class MacroDefinition: + """ + Stores a macro definition. + name: Name of the macro + params: variables and default values to be substituted in keys or values + definition: A dictionary that will be substituted including parameters + """ + name: str + params: dict[str, Any] + definition: Any + +# This dictionary stores the macro definitions +macro_store: dict[str, MacroDefinition] = dict() + + +def substitute(propvalue, values: dict): + """ + Substitution of variables in strings using the variable substitution + library from python's standard library. + """ + propvalue_template = Template(propvalue) + return propvalue_template.safe_substitute(**values) + + +def substitute_dict(sourced: dict[str, Any], values: dict[str, Any]): + """ + Create a copy of sourced. + Afterwards recursively do variable substitution on all keys and values. + """ + d = deepcopy(sourced) + # Changes in keys: + replace: dict[str, str] = dict() + for k in d: + replacement = substitute(k, values) + if replacement != k: + replace[k] = replacement + for k, v in replace.items(): + d[v] = d[k] + del d[k] + # Changes in values: + for k, v in d.items(): + if isinstance(v, str): + d[k] = substitute(v, values) + elif isinstance(v, list): + subst_list = list() + for i in d[k]: + if isinstance(i, str): + subst_list.append(substitute(i, values)) + else: + subst_list.append(i) + elif isinstance(v, dict): + d[k] = substitute_dict(v, values) + else: + pass + return d + + +def defmacro_constructor(loader, node): + """ + Function for registering macros in yaml files. + + It can be registered in pyaml using: + yaml.SafeLoader.add_constructor("!defmacro", defmacro_constructor) + """ + value = loader.construct_mapping(node, deep=True) + macro = MacroDefinition( + value["name"], value["params"], + value["definition"]) + macro_store[macro.name] = macro + return {} + + +def macro_constructor(loader, node): + """ + Function for substituting macros in yaml files. + + It can be registered in pyaml using: + yaml.SafeLoader.add_constructor("!macro", macro_constructor) + """ + value = loader.construct_mapping(node, deep=True) + name = value["name"] + macro = macro_store[name] + params = deepcopy(macro.params) + params.update(value["params"]) + definition = deepcopy(macro.definition) + + return substitute_dict(definition, params) diff --git a/unittests/test_macros.py b/unittests/test_macros.py new file mode 100644 index 0000000000000000000000000000000000000000..2beced2f1f701535e70026e9cc760123ff965a0c --- /dev/null +++ b/unittests/test_macros.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2022 Alexander Schlemmer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# + +from caoscrawler.macros import defmacro_constructor, macro_constructor +import yaml + +def test_macros(): + yaml.SafeLoader.add_constructor("!defmacro", defmacro_constructor) + yaml.SafeLoader.add_constructor("!macro", macro_constructor) + dat = yaml.load(""" +defs: +- !defmacro + name: test + params: + a: 2 + b: bla + c: $variable + definition: + expanded_$b: + blubb: ok$a + $b: $c + +testnode: + obl: !macro + name: test + params: + a: 4 + b: yea +""", Loader=yaml.SafeLoader) + assert dat["testnode"]["obl"]["expanded_yea"]["blubb"] == "ok4" + assert dat["testnode"]["obl"]["expanded_yea"]["yea"] == "$variable" + assert "expanded_bla" not in dat["testnode"]["obl"] + assert "bla" not in dat["testnode"]["obl"]["expanded_yea"]