From 3255053ac4595d3daf399dc37262b7428d75229c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 13 Sep 2022 22:09:40 +1000 Subject: [PATCH] Implement all restrictions for IfcTester --- src/ifctester/ifctester/facet.py | 173 ++++++++---------------- src/ifctester/test/ids_doc_generator.py | 2 +- src/ifctester/test/test_facet.py | 73 ++++++---- 3 files changed, 107 insertions(+), 141 deletions(-) diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 1b5660dcb8..f914b97256 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -708,131 +708,72 @@ class Material(Facet): class Restriction: - def __init__(self, options="", type="pattern", base="string"): - if type in ["enumeration", "pattern", "bounds"]: - self.type = type - self.base = base - self.options = options - if ( - (type == "enumeration" and isinstance(options, list)) - or (type == "bounds" and isinstance(options, dict)) - or (type == "pattern" and isinstance(options, str)) - ): - self.options = options - else: - raise Exception("Options were not properly defined.") + def __init__(self, options={}, base="string"): + self.base = base + self.options = options def parse(self, ids_dict): - if ids_dict: - try: - self.base = ids_dict["@base"][3:] - except KeyError: - self.base = "String" - - for n in ids_dict: - if n == "enumeration": - self.type = "enumeration" - self.options = [] - for x in ids_dict[n]: - self.options.append(x["@value"]) - elif n[-7:] == "clusive": - self.type = "bounds" - self.options = {} - self.options.append({n: ids_dict[n]["@value"]}) - elif n[-5:] == "ength": - self.type = "length" - if n[3:6] == "min": - self.options.append(">=") - elif n[3:6] == "max": - self.options.append("<=") - else: - self.options.append("==") - self.options[-1] += str(ids_dict[n]["@value"]) - elif n == "pattern": - self.type = "pattern" - self.options = ids_dict[n]["@value"] - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace - elif n == "@base": - pass - else: - print("Error! Restriction not implemented") + if not ids_dict: + return self + self.base = ids_dict.get("@base", "xs:string")[3:] + for key, value in ids_dict.items(): + if key == "@base": + continue + if isinstance(value, dict): + self.options[key[3:]] = value["@value"] + else: + self.options[key[3:]] = [v["@value"] for v in value] return self def asdict(self): - rest_dict = {"@base": "xs:" + self.base} - if self.type == "enumeration": - for option in self.options: - if "xs:enumeration" not in rest_dict: - rest_dict["xs:enumeration"] = [{"@value": option}] + result = {"@base": "xs:" + self.base} + for constraint, value in self.options.items(): + value = [value] if not isinstance(value, list) else value + for v in value: + if constraint in ["length", "minLength", "maxLength"]: + value_dict = {"@value": v} else: - rest_dict["xs:enumeration"].append({"@value": option}) - elif self.type == "bounds": - for option in self.options: - rest_dict["xs:" + option] = [{"@value": str(self.options[option]), "@fixed": False}] - elif self.type == "pattern": - if "xs:pattern" not in rest_dict: - rest_dict["xs:pattern"] = [{"@value": self.options}] - else: - rest_dict["xs:pattern"].append({"@value": self.options}) - return rest_dict - - def __eq__(self, other): - result = False - if self and (other or other == 0): - if self.type == "enumeration" and self.base == "bool": - self.options = [x.lower() for x in self.options] - result = str(other).lower() in self.options - elif self.type == "enumeration": - result = other in [cast_to_value(o, other) for o in self.options] - elif self.type == "bounds": - result = True - for sign in self.options.keys(): - if sign == "minInclusive" and other < self.options[sign]: - result = False - elif sign == "maxInclusive" and other > self.options[sign]: - result = False - elif sign == "minExclusive" and other <= self.options[sign]: - result = False - elif sign == "maxExclusive" and other >= self.options[sign]: - result = False - elif self.type == "length": - for op in self.options: - if eval(str(len(other)) + op): # TODO eval not safe? - result = True - elif self.type == "pattern": - if isinstance(self.options, list): - # TODO handle case with multiple pattern options - translated_pattern = identities.translate_pattern(self.options[0]) - else: - translated_pattern = identities.translate_pattern(self.options) - regex_pattern = re.compile(translated_pattern) - if regex_pattern.fullmatch(other) is not None: - result = True - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace + value_dict = {"@value": str(v)} + result.setdefault(f"xs:{constraint}", []).append(value_dict) return result + def __eq__(self, other): + if other is None: + return False + for constraint, value in self.options.items(): + if constraint == "enumeration": + if other not in [cast_to_value(v, other) for v in value]: + return False + elif constraint == "pattern": + value = value if isinstance(value, list) else [value] + for pattern in value: + if re.compile(identities.translate_pattern(pattern)).fullmatch(other) is None: + return False + elif constraint == "length": + if len(str(other)) != int(value): + return False + elif constraint == "maxLength": + if len(str(other)) > int(value): + return False + elif constraint == "minLength": + if len(str(other)) < int(value): + return False + elif constraint == "maxExclusive": + if float(other) >= value: + return False + elif constraint == "maxInclusive": + if float(other) > value: + return False + elif constraint == "minExclusive": + if float(other) <= value: + return False + elif constraint == "minInclusive": + if float(other) < value: + return False + return True + def __str__(self): - if self.type == "enumeration": - return "one of '%s'" % "' or '".join(self.options) - elif self.type == "bounds": - bounds = { - "minInclusive": "larger or equal ", - "maxInclusive": "smaller or equal ", - "minExclusive": "larger than ", - "maxExclusive": "smaller than ", - } - return "of value %s" % ", and ".join([bounds[x] + str(self.options[x]) for x in self.options]) - elif self.type == "length": - return "%s letters long" % " and ".join(self.options) - elif self.type == "pattern": - return "the pattern '%s'" % self.options - # TODO add fractionDigits - # TODO add totalDigits - # TODO add whiteSpace + return str(self.options) class Result: diff --git a/src/ifctester/test/ids_doc_generator.py b/src/ifctester/test/ids_doc_generator.py index 61bb8e6dc3..575b43cbde 100644 --- a/src/ifctester/test/ids_doc_generator.py +++ b/src/ifctester/test/ids_doc_generator.py @@ -273,7 +273,7 @@ spec = ifctester.ids.Specification( ) specs.specifications.append(spec) spec.applicability.append(ifctester.ids.Entity(name="IFCWALLTYPE")) -restriction = ifctester.ids.Restriction(options="(-|[0-9]{2,3})\/(-|[0-9]{2,3})\/(-|[0-9]{2,3})", type="pattern") +restriction = ifctester.ids.Restriction(options={"pattern": "(-|[0-9]{2,3})\/(-|[0-9]{2,3})\/(-|[0-9]{2,3})"}) spec.requirements.append( ifctester.ids.Property( propertySet="Pset_WallCommon", diff --git a/src/ifctester/test/test_facet.py b/src/ifctester/test/test_facet.py index 2cae4dabc4..ccda23b59b 100644 --- a/src/ifctester/test/test_facet.py +++ b/src/ifctester/test/test_facet.py @@ -178,7 +178,7 @@ class TestEntity: facet = Entity(name="IFCWALL", predefinedType="X") run("Overridden predefined types should pass", facet=facet, inst=wall, expected=True) - restriction = Restriction(options=["IFCWALL", "IFCSLAB"], type="enumeration") + restriction = Restriction(options={"enumeration": ["IFCWALL", "IFCSLAB"]}) facet = Entity(name=restriction) ifc = ifcopenshell.file() run("Entities can be specified as an enumeration 1/3", facet=facet, inst=ifc.createIfcWall(), expected=True) @@ -187,7 +187,7 @@ class TestEntity: ifc = ifcopenshell.file() run("Entities can be specified as an enumeration 3/3", facet=facet, inst=ifc.createIfcBeam(), expected=False) - restriction = Restriction(options="IFC.*TYPE", type="pattern") + restriction = Restriction(options={"pattern": "IFC.*TYPE"}) facet = Entity(name=restriction) ifc = ifcopenshell.file() run( @@ -204,7 +204,7 @@ class TestEntity: expected=True, ) - restriction = Restriction(options="FOO.*", type="pattern") + restriction = Restriction(options={"pattern": "FOO.*"}) facet = Entity(name="IFCWALL", predefinedType=restriction) ifc = ifcopenshell.file() wall = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall", predefined_type="FOOBAR") @@ -594,7 +594,7 @@ class TestAttribute: expected=False, ) - restriction = Restriction(options=".*Name.*", type="pattern") + restriction = Restriction(options={"pattern": ".*Name.*"}) facet = Attribute(name=restriction) ifc = ifcopenshell.file() run( @@ -605,7 +605,7 @@ class TestAttribute: ), expected=True, ) - restriction = Restriction(options=["Name", "Description"], type="enumeration") + restriction = Restriction(options={"enumeration": ["Name", "Description"]}) facet = Attribute(name=restriction) ifc = ifcopenshell.file() run( @@ -622,7 +622,7 @@ class TestAttribute: expected=True, ) - restriction = Restriction(options=["Foo", "Bar"], type="enumeration") + restriction = Restriction(options={"enumeration": ["Foo", "Bar"]}) facet = Attribute(name="Name", value=restriction) ifc = ifcopenshell.file() run("Value restrictions may be used 1/3", facet=facet, inst=ifc.createIfcWall(Name="Foo"), expected=True) @@ -639,7 +639,7 @@ class TestAttribute: facet = Attribute(name="Description", value="Foobar") run("Attributes are not inherited by the occurrence", facet=facet, inst=wall, expected=False) - restriction = Restriction(options=["42"], type="enumeration", base="string") + restriction = Restriction(options={"enumeration": ["42", "43"]}) facet = Attribute(name="RefractionIndex", value=restriction) ifc = ifcopenshell.file() run( @@ -649,7 +649,7 @@ class TestAttribute: expected=True, ) - restriction = Restriction(options={"minInclusive": 42, "maxInclusive": 42}, type="bounds", base="decimal") + restriction = Restriction(options={"minInclusive": 42, "maxInclusive": 42}, base="decimal") facet = Attribute(name="RefractionIndex", value=restriction) ifc = ifcopenshell.file() run( @@ -774,13 +774,13 @@ class TestClassification: run("Systems should match exactly 4/5", facet=facet, inst=element11, expected=True) run("Systems should match exactly 5/5", facet=facet, inst=element22, expected=True) - restriction = Restriction(options="1.*", type="pattern") + restriction = Restriction(options={"pattern": "1.*"}) facet = Classification(value=restriction) run("Restrictions can be used for values 1/3", facet=facet, inst=element1, expected=True) run("Restrictions can be used for values 2/3", facet=facet, inst=element11, expected=True) run("Restrictions can be used for values 3/3", facet=facet, inst=element22, expected=False) - restriction = Restriction(options="Foo.*", type="pattern") + restriction = Restriction(options={"pattern": "Foo.*"}) facet = Classification(system=restriction) run("Restrictions can be used for systems 1/2", facet=facet, inst=element0, expected=False) run("Restrictions can be used for systems 2/2", facet=facet, inst=element1, expected=True) @@ -1129,7 +1129,7 @@ class TestProperty: run("Complex properties are not supported 2/2", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() - restriction = Restriction(options="Foo_.*", type="pattern") + restriction = Restriction(options={"pattern": "Foo_.*"}) facet = Property(propertySet=restriction, name="Foo", measure="IfcLabel") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") @@ -1142,7 +1142,7 @@ class TestProperty: run("All matching property sets must satisfy requirements 3/3", facet=facet, inst=element, expected=True) ifc = self.setup_ifc() - restriction = Restriction(options="Foo.*", type="pattern") + restriction = Restriction(options={"pattern": "Foo.*"}) facet = Property(propertySet="Foo_Bar", name=restriction, value="x", measure="IfcLabel") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") @@ -1154,8 +1154,8 @@ class TestProperty: run("All matching properties must satisfy requirements 3/3", facet=facet, inst=element, expected=False) ifc = self.setup_ifc() - restriction1 = Restriction(options="Foo.*", type="pattern") - restriction2 = Restriction(options=["x", "y"], type="enumeration") + restriction1 = Restriction(options={"pattern": "Foo.*"}) + restriction2 = Restriction(options={"enumeration": ["x", "y"]}) facet = Property(propertySet="Foo_Bar", name=restriction1, value=restriction2, measure="IfcLabel") element = ifcopenshell.api.run("root.create_entity", ifc, ifc_class="IfcWall") pset = ifcopenshell.api.run("pset.add_pset", ifc, product=element, name="Foo_Bar") @@ -1527,40 +1527,45 @@ class TestPartOf: class TestRestriction: + def test_creating_a_restriction(self): + restriction = Restriction(options={"enumeration": ["foo", "bar"]}) + assert restriction.asdict() == {"@base": "xs:string", "xs:enumeration": [{"@value": "foo"}, {"@value": "bar"}]} + def test_enumeration(self): - restriction = Restriction(options=["foo", "bar"], type="enumeration") + restriction = Restriction(options={"enumeration": ["foo", "bar"]}) assert restriction == "foo" assert restriction == "bar" + assert restriction != "Foo" assert restriction != "baz" def test_bounds(self): - restriction = Restriction(options={"minInclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") + restriction = Restriction(options={"minInclusive": 0, "maxExclusive": 10}, base="integer") assert restriction == 0 assert restriction != 10 assert restriction == 5 assert restriction != -1 def test_pattern(self): - restriction = Restriction(options="[A-Z]{2}[0-9]{2}", type="pattern") + restriction = Restriction(options={"pattern": "[A-Z]{2}[0-9]{2}"}) assert restriction == "AB01" assert restriction != "AB" assert restriction != "01" - def test_filtering_using_an_enumeration(self): + def test_filtering_using_restrictions(self): set_facet("restriction") ifc = ifcopenshell.file() - restriction = Restriction(options=["Foo", "Bar"], type="enumeration") + restriction = Restriction(options={"enumeration": ["Foo", "Bar"]}) facet = Attribute(name="Name", value=restriction) element = ifc.createIfcWall(Name="Foo") run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=True) element.Name = "Bar" - run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=True) + run("An enumeration matches case sensitively 2/3", facet=facet, inst=element, expected=True) element.Name = "Baz" - run("An enumeration matches case sensitively 1/3", facet=facet, inst=element, expected=False) + run("An enumeration matches case sensitively 3/3", facet=facet, inst=element, expected=False) ifc = ifcopenshell.file() - restriction = Restriction(options={"minInclusive": 0, "maxInclusive": 10}, type="bounds", base="integer") + restriction = Restriction(options={"minInclusive": 0, "maxInclusive": 10}, base="integer") element = ifc.createIfcSurfaceStyleRefraction(RefractionIndex=0) facet = Attribute(name="RefractionIndex", value=restriction) run("A bound can be inclusive 1/4", facet=facet, inst=element, expected=True) @@ -1571,7 +1576,7 @@ class TestRestriction: element.RefractionIndex = 100 run("A bound can be inclusive 4/4", facet=facet, inst=element, expected=False) - restriction = Restriction(options={"minExclusive": 0, "maxExclusive": 10}, type="bounds", base="integer") + restriction = Restriction(options={"minExclusive": 0, "maxExclusive": 10}, base="integer") facet = Attribute(name="RefractionIndex", value=restriction) element.RefractionIndex = 0 run("A bound can be inclusive 1/3", facet=facet, inst=element, expected=False) @@ -1581,7 +1586,7 @@ class TestRestriction: run("A bound can be inclusive 3/3", facet=facet, inst=element, expected=False) ifc = ifcopenshell.file() - restriction = Restriction(options="[A-Z]{2}[0-9]{2}", type="pattern") + restriction = Restriction(options={"pattern": "[A-Z]{2}[0-9]{2}"}) facet = Attribute(name="Name", value=restriction) element = ifc.createIfcWall(Name="WT01") run("Regex patterns can be used 1/3", facet=facet, inst=element, expected=True) @@ -1589,3 +1594,23 @@ class TestRestriction: run("Regex patterns can be used 2/3", facet=facet, inst=element, expected=True) element.Name = "A5" run("Regex patterns can be used 3/3", facet=facet, inst=element, expected=False) + + ifc = ifcopenshell.file() + restriction = Restriction(options={"length": 2}) + facet = Attribute(name="Name", value=restriction) + element = ifc.createIfcWall(Name="AB") + run("Length checks can be used 1/2", facet=facet, inst=element, expected=True) + element.Name = "ABC" + run("Length checks can be used 1/2", facet=facet, inst=element, expected=False) + + ifc = ifcopenshell.file() + restriction = Restriction(options={"minLength": 2, "maxLength": 3}) + facet = Attribute(name="Name", value=restriction) + element = ifc.createIfcWall(Name="A") + run("Max and min length checks can be used 1/3", facet=facet, inst=element, expected=False) + element.Name = "AB" + run("Max and min length checks can be used 2/3", facet=facet, inst=element, expected=True) + element.Name = "ABC" + run("Max and min length checks can be used 3/3", facet=facet, inst=element, expected=True) + element.Name = "ABCD" + run("Max and min length checks can be used 4/3", facet=facet, inst=element, expected=False)