diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b2de738c79b3ad38c6bf77a2abb3611a6511eb..c307c15ac9ceb0a039f6406d0cb9148931c2837e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CFood that creates a Record for each line in a csv file - `generic_analysis.py` allows to easily call scripts to perform analyses in server side scripting [EXPERIMENTAL] +- New keyword "role" in yaml data model that allows creation of Records and Files. +- It is now possible to set values of properties and default values of properties + directly in the yaml model. ### Changed ### diff --git a/src/caosadvancedtools/models/parser.py b/src/caosadvancedtools/models/parser.py index e56a492fa3e9199a312d374a622770e7836f42cb..f385a1a3cdfaa91d3d61e6474743cd0549318e0d 100644 --- a/src/caosadvancedtools/models/parser.py +++ b/src/caosadvancedtools/models/parser.py @@ -25,7 +25,7 @@ import yaml from .data_model import DataModel # Keywords which are allowed in data model descriptions. -KEYWORDS = ["parent", +KEYWORDS = ["parent", # TODO: can we remove that, see: #36 "importance", "datatype", # for example TEXT, INTEGER or REFERENCE "unit", @@ -35,8 +35,11 @@ KEYWORDS = ["parent", "suggested_properties", "inherit_from_recommended", "inherit_from_suggested", - "inherit_from_obligatory", ] + "inherit_from_obligatory", + "role", + "value", ] +# TODO: check whether it's really ignored # These KEYWORDS are not forbidden as properties, but merely ignored. KEYWORDS_IGNORED = [ "unit", @@ -109,6 +112,10 @@ def parse_model_from_string(string): class Parser(object): def __init__(self): + """ + Initialize an empty parer object and initialize + the dictionary of entities and the list of treated elements. + """ self.model = {} self.treated = [] @@ -177,13 +184,11 @@ class Parser(object): ymlmodel["extern"] = [] for name in ymlmodel["extern"]: - if db.execute_query("COUNT Property {}".format(name)) > 0: - self.model[name] = db.execute_query( - "FIND Property WITH name={}".format(name), unique=True) - - elif db.execute_query("COUNT RecordType {}".format(name)) > 0: - self.model[name] = db.execute_query( - "FIND RecordType WITH name={}".format(name), unique=True) + for role in ("Property", "RecordType", "Record", "File"): + if db.execute_query("COUNT {} {}".format(role, name)) > 0: + self.model[name] = db.execute_query( + "FIND {} WITH name={}".format(role, name), unique=True) + break else: raise Exception("Did not find {}".format(name)) @@ -235,6 +240,8 @@ class Parser(object): """ adds names of Properties and RecordTypes to the model dictionary Properties are also initialized. + + name is the key of the yaml element and definition the value. """ if name == "__line__": @@ -258,9 +265,25 @@ class Parser(object): # and create the new property self.model[name] = db.Property(name=name, datatype=definition["datatype"]) + elif (self.model[name] is None and isinstance(definition, dict) + and "role" in definition): + if definition["role"] == "RecordType": + self.model[name] = db.RecordType(name=name) + elif definition["role"] == "Record": + self.model[name] = db.Record(name=name) + elif definition["role"] == "File": + self.model[name] = db.File(name=name) + elif definition["role"] == "Property": + self.model[name] = db.Property(name=name) + else: + raise RuntimeError("Unknown role {} in definition of entity.".format( + definition["role"])) - # add other definitions recursively + # for setting values of properties directly: + if not isinstance(definition, dict): + return + # add other definitions recursively for prop_type in ["recommended_properties", "suggested_properties", "obligatory_properties"]: @@ -303,8 +326,12 @@ class Parser(object): name=n, importance=importance, datatype=db.LIST(_get_listdatatype(e["datatype"]))) + elif e is None: + self.model[ent_name].add_property(name=n, + importance=importance) else: self.model[ent_name].add_property(name=n, + value=e, importance=importance) def _inherit(self, name, prop, inheritance): @@ -328,6 +355,10 @@ class Parser(object): if definition is None: return + # for setting values of properties directly: + if not isinstance(definition, dict): + return + if ("datatype" in definition and definition["datatype"].startswith("LIST")): @@ -344,6 +375,9 @@ class Parser(object): if prop_name == "unit": self.model[name].unit = prop + elif prop_name == "value": + self.model[name].value = prop + elif prop_name == "description": self.model[name].description = prop @@ -372,6 +406,10 @@ class Parser(object): elif prop_name == "datatype": continue + # role has already been used + elif prop_name == "role": + continue + elif prop_name == "inherit_from_obligatory": self._inherit(name, prop, db.OBLIGATORY) elif prop_name == "inherit_from_recommended": diff --git a/unittests/test_parser.py b/unittests/test_parser.py index 161e2873a9c01f9ce415818116b9e4cf9aeadb5c..a14cb1ab516e87eb64bc78deadc0481c8501a058 100644 --- a/unittests/test_parser.py +++ b/unittests/test_parser.py @@ -15,6 +15,8 @@ def to_file(string): return f.name +# TODO: check purpose of this function... add documentation + def parse_str(string): parse_model_from_yaml(to_file(string)) @@ -68,7 +70,8 @@ RT2: a: """ - self.assertRaises(TwiceDefinedException, lambda: parse_model_from_yaml(to_file(string))) + self.assertRaises(TwiceDefinedException, + lambda: parse_model_from_yaml(to_file(string))) def test_typical_case(self): string = """ @@ -103,7 +106,8 @@ RT5: - RT1: - RT2: """ - self.assertRaises(ValueError, lambda: parse_model_from_yaml(to_file(string))) + self.assertRaises( + ValueError, lambda: parse_model_from_yaml(to_file(string))) def test_unknown_kwarg(self): string = """ @@ -111,7 +115,8 @@ RT1: datetime: p1: """ - self.assertRaises(ValueError, lambda: parse_model_from_yaml(to_file(string))) + self.assertRaises( + ValueError, lambda: parse_model_from_yaml(to_file(string))) def test_definition_in_inheritance(self): string = """ @@ -121,7 +126,8 @@ RT2: - RT1: description: "tach" """ - self.assertRaises(ValueError, lambda: parse_model_from_yaml(to_file(string))) + self.assertRaises( + ValueError, lambda: parse_model_from_yaml(to_file(string))) def test_inheritance(self): string = """ @@ -301,6 +307,8 @@ class ExternTest(unittest.TestCase): class ErrorMessageTest(unittest.TestCase): """Tests for understandable error messages.""" + # Note: This was changed with implementation of role keyword + @unittest.expectedFailure def test_non_dict(self): """When a value is given, where a list or mapping is expected.""" recordtype_value = """ @@ -328,3 +336,66 @@ A: with self.assertRaises(YamlDefinitionError) as yde: parse_str(string) assert("line {}".format(line) in yde.exception.args[0]) + + +def test_define_role(): + model = """ +A: + role: Record +""" + entities = parse_model_from_string(model) + assert "A" in entities + assert isinstance(entities["A"], db.Record) + assert entities["A"].role == "Record" + + model = """ +A: + role: Record + inherit_from_obligatory: + - C + obligatory_properties: + b: +b: + datatype: INTEGER +C: + obligatory_properties: + b: +D: + role: RecordType +""" + entities = parse_model_from_string(model) + for l, ent in (("A", "Record"), ("b", "Property"), + ("C", "RecordType"), ("D", "RecordType")): + assert l in entities + assert isinstance(entities[l], getattr(db, ent)) + assert entities[l].role == ent + + assert entities["A"].parents[0].name == "C" + assert entities["A"].name == "A" + + assert entities["A"].properties[0].name == "b" + assert entities["A"].properties[0].value is None + + assert entities["C"].properties[0].name == "b" + assert entities["C"].properties[0].value is None + + model = """ +A: + role: Record + obligatory_properties: + b: 42 +b: + datatype: INTEGER +""" + + entities = parse_model_from_string(model) + assert entities["A"].get_property("b").value == 42 + assert entities["b"].value is None + + model = """ +b: + datatype: INTEGER + value: 18 +""" + entities = parse_model_from_string(model) + assert entities["b"].value == 18