From 839a00c2a897e6e4c9696d6d7e8b64305a987efa Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 5 Jan 2023 20:54:17 +1100 Subject: [PATCH] Run black on validation, lazy-load _pytest.assertion so that it isn't a hard dependency for non-express rule validation. --- .../blenderbim/bim/module/debug/operator.py | 2 +- .../ifcopenshell/express/rule_executor.py | 78 ++++++++++--------- .../ifcopenshell/validate.py | 78 ++++++++----------- 3 files changed, 74 insertions(+), 84 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index 5d723020ea..b0a56ed496 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -78,7 +78,7 @@ class ValidateIfcFile(bpy.types.Operator): logger = logging.getLogger("validate") logger.setLevel(logging.DEBUG) - ifcopenshell.validate.validate(IfcStore.get_file(), logger) + ifcopenshell.validate.validate(IfcStore.get_file(), logger, express_rules=True) return {"FINISHED"} diff --git a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py index 475aeafb97..8b0f282608 100644 --- a/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py +++ b/src/ifcopenshell-python/ifcopenshell/express/rule_executor.py @@ -3,19 +3,19 @@ import ast import collections import ifcopenshell from dataclasses import dataclass -from _pytest import assertion from codegen import indent + def reverse_compile(s): - return s.strip().replace('len(', 'SIZEOF(').replace('assert ', '') + return s.strip().replace("len(", "SIZEOF(").replace("assert ", "") @dataclass class error(Exception): - rule_name : str - rule_definition : str - violation : str - instance : ifcopenshell.entity_instance = None + rule_name: str + rule_definition: str + violation: str + instance: ifcopenshell.entity_instance = None def __str__(self): inst = "" @@ -27,7 +27,7 @@ class error(Exception): def fix_type(v): if isinstance(v, (list, tuple)): # 1-based indexing: - # + # # @todo this is not the best way, because it still allows to index the 0-th element, # but given the existing body of rules this should be sufficient. # return type(v)([None]) + type(v)(map(fix_type, v)) @@ -40,27 +40,27 @@ def fix_type(v): def run(f, logger): + from _pytest import assertion + fn = os.path.join(os.path.dirname(__file__), "rules", f"{f.schema}.py") source = open(fn, "r").read() a = ast.parse(source) assertion.rewrite.rewrite_asserts(mod=a, source=source) - cd = compile(a, f"{f.schema}.py", 'exec') + cd = compile(a, f"{f.schema}.py", "exec") scope = {} exec(cd, scope) S = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema) - - rules = list(filter(lambda x: hasattr(x, 'SCOPE'), scope.values())) - - for R in [r for r in rules if r.SCOPE == 'file']: + + rules = list(filter(lambda x: hasattr(x, "SCOPE"), scope.values())) + + for R in [r for r in rules if r.SCOPE == "file"]: try: R()(f) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno - logger.error(str(error( - R.__name__, - reverse_compile(source.split("\n")[ln-1]), - reverse_compile(e.args[0]) - ))) + logger.error( + str(error(R.__name__, reverse_compile(source.split("\n")[ln - 1]), reverse_compile(e.args[0]))) + ) types = {} subtypes = collections.defaultdict(list) @@ -72,13 +72,15 @@ def run(f, logger): D = collections.defaultdict(list) for r in rules: - if r.SCOPE == 'type': + if r.SCOPE == "type": + def visit(nm): D[nm].append(r) for nm2 in subtypes[nm]: visit(nm2) + visit(r.TYPE_NAME) - + def type_name(ty): if isinstance(ty, ifcopenshell.ifcopenshell_wrapper.named_type): return type_name(ty.declared_type()) @@ -89,28 +91,34 @@ def run(f, logger): pass else: return ty.name() - + def check(value, type, instance): if value is None: return - + if type_name(type) in D: for R in D[type_name(type)]: try: R()(fix_type(value)) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno - logger.error(str(error( - R.__name__, - reverse_compile(source.split("\n")[ln-1]), - reverse_compile(e.args[0]), - instance - ))) + logger.error( + str( + error( + R.__name__, + reverse_compile(source.split("\n")[ln - 1]), + reverse_compile(e.args[0]), + instance, + ) + ) + ) # @nb something can be a named type with rules and still be an aggregation. # case in point IfcCompoundPlaneAngleMeasure. Therefore only unpack named # type references from this point onwards. - while isinstance(type, (ifcopenshell.ifcopenshell_wrapper.named_type, ifcopenshell.ifcopenshell_wrapper.type_declaration)): + while isinstance( + type, (ifcopenshell.ifcopenshell_wrapper.named_type, ifcopenshell.ifcopenshell_wrapper.type_declaration) + ): type = type.declared_type() if isinstance(value, (list, tuple)): @@ -125,7 +133,6 @@ def run(f, logger): else: # unpack the type instance check(value[0], S.declaration_by_name(value.is_a()), instance=inst) - for inst in f: values = list(inst) @@ -138,18 +145,17 @@ def run(f, logger): else: check(val, attr.type_of_attribute(), instance=inst) - for R in [r for r in rules if r.SCOPE == 'entity']: + for R in [r for r in rules if r.SCOPE == "entity"]: for inst in f.by_type(R.TYPE_NAME): try: R()(inst) except Exception as e: ln = e.__traceback__.tb_next.tb_lineno - logger.error(str(error( - R.__name__, - reverse_compile(source.split("\n")[ln-1]), - reverse_compile(e.args[0]), - inst - ))) + logger.error( + str( + error(R.__name__, reverse_compile(source.split("\n")[ln - 1]), reverse_compile(e.args[0]), inst) + ) + ) if __name__ == "__main__": diff --git a/src/ifcopenshell-python/ifcopenshell/validate.py b/src/ifcopenshell-python/ifcopenshell/validate.py index b884a3f8e0..8675de8443 100644 --- a/src/ifcopenshell-python/ifcopenshell/validate.py +++ b/src/ifcopenshell-python/ifcopenshell/validate.py @@ -55,9 +55,7 @@ class json_logger: self.instance = instance def log(self, level, message, *args, **kwargs): - self.statements.append( - log_entry_type(level, message % args, kwargs.get("instance"))._asdict() - ) + self.statements.append(log_entry_type(level, message % args, kwargs.get("instance"))._asdict()) def __getattr__(self, level): return functools.partial(self.log, level, instance=self.instance) @@ -104,14 +102,8 @@ def annotate_inst_attr_pos(inst, pos): def format(val): - if ( - isinstance(val, tuple) - and val - and isinstance(val[0], ifcopenshell.entity_instance) - ): - return "[\n%s\n ]" % "\n".join( - " {}. {}".format(*x) for x in enumerate(val, start=1) - ) + if isinstance(val, tuple) and val and isinstance(val[0], ifcopenshell.entity_instance): + return "[\n%s\n ]" % "\n".join(" {}. {}".format(*x) for x in enumerate(val, start=1)) else: return repr(val) @@ -127,22 +119,21 @@ def assert_valid_inverse(attr, val, schema): b1 = attr.bound1() b2 = attr.bound2() - attr_formatted = ( - f"{attr.name()} : {aggr} [{b1}:{b2}] OF {ent_ref} FOR {attr_ref}" - ) + attr_formatted = f"{attr.name()} : {aggr} [{b1}:{b2}] OF {ent_ref} FOR {attr_ref}" - raise ValidationError( - f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n" - ) + raise ValidationError(f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n") return True + select_members_cache = {} + + def get_select_members(schema, ty): cache_key = schema.name(), ty.name() from_cache = select_members_cache.get(cache_key) if from_cache: return from_cache - + def inner(ty): if isinstance(ty, select_type): for st in ty.select_list(): @@ -152,11 +143,12 @@ def get_select_members(schema, ty): for st in ty.subtypes(): yield from inner(st) elif isinstance(ty, type_declaration): - yield ty.name() - + yield ty.name() + v = select_members_cache[cache_key] = set(inner(ty)) return v + def assert_valid(attr_type, val, schema, no_throw=False, attr=None): type_wrappers = (named_type,) if not isinstance(val, ifcopenshell.entity_instance): @@ -177,9 +169,7 @@ def assert_valid(attr_type, val, schema, no_throw=False, attr=None): else: invalid = type(val) != simple_type_python elif isinstance(attr_type, (entity_type, type_declaration)): - invalid = not isinstance(val, ifcopenshell.entity_instance) or not val.is_a( - attr_type.name() - ) + invalid = not isinstance(val, ifcopenshell.entity_instance) or not val.is_a(attr_type.name()) elif isinstance(attr_type, select_type): val_to_use = val if isinstance(schema.declaration_by_name(val.is_a()), enumeration_type): @@ -200,20 +190,14 @@ def assert_valid(attr_type, val, schema, no_throw=False, attr=None): elif isinstance(attr_type, aggregation_type): b1, b2 = attr_type.bound1(), attr_type.bound2() ty = attr_type.type_of_element() - invalid = ( - len(val) < b1 - or (b2 != -1 and len(val) > b2) - or not all(assert_valid(ty, v, schema) for v in val) - ) + invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) or not all(assert_valid(ty, v, schema) for v in val) else: raise NotImplementedError("Not impl %s %s" % (type(attr_type), attr_type)) if no_throw: - return not invalid + return not invalid elif invalid: - raise ValidationError( - f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n" - ) + raise ValidationError(f"With attribute:\n {attr or attr_type}\nValue:\n {val}\nNot valid\n") else: return True @@ -239,9 +223,7 @@ def log_internal_cpp_errors(filename, logger): for offsets, msg in zip(chr_offsets, msgs): if offsets: - line = lines[bisect.bisect_left(cs, int(offsets[0]))].decode( - "ascii", errors="ignore" - ) + line = lines[bisect.bisect_left(cs, int(offsets[0]))].decode("ascii", errors="ignore") m = chr_offset_re.sub("", msg["message"]) if hasattr(logger, "set_instance"): @@ -250,18 +232,21 @@ def log_internal_cpp_errors(filename, logger): else: logger.error("For instance:\n %s\n%s", line, m) + entity_attribute_map = {} + + def get_entity_attributes(schema, entity): cache_key = schema.name(), entity from_cache = entity_attribute_map.get(cache_key) if from_cache: return from_cache - + entity_attrs = ( ent := schema.declaration_by_name(entity), ent.all_attributes(), ) - + entity_attribute_map[cache_key] = entity_attrs return entity_attrs @@ -285,16 +270,16 @@ def validate(f, logger, express_rules=False): It is recommended to supply the path to the file, so that internal C++ errors reported during the parse stage are also captured. """ - + # Originally there was no way in Python to distinguish on an entity instance attribute value whether the # value supplied in the model was NIL ($) or 'missing because derived in subtype' (*). For validation this # however this may be important, and hence a feature switch has been implemented to return *-values as # instances of a dedicated type `ifcopenshell.ifcopenshell_wrapper.attribute_value_derived`. - attribute_value_derived_org = ifcopenshell.ifcopenshell_wrapper.get_feature('use_attribute_value_derived') - ifcopenshell.ifcopenshell_wrapper.set_feature('use_attribute_value_derived', True) - + attribute_value_derived_org = ifcopenshell.ifcopenshell_wrapper.get_feature("use_attribute_value_derived") + ifcopenshell.ifcopenshell_wrapper.set_feature("use_attribute_value_derived", True) + filename = None - + if not isinstance(f, ifcopenshell.file): # get_log() clears log existing output @@ -313,7 +298,7 @@ def validate(f, logger, express_rules=False): logger.set_instance(inst) entity, attrs = get_entity_attributes(schema, inst.is_a()) - + if entity.is_abstract(): e = "Entity %s is abstract" % entity.name() if hasattr(logger, "set_instance"): @@ -341,9 +326,7 @@ def validate(f, logger, express_rules=False): has_invalid_value = True if not has_invalid_value: - for i, (attr, val, is_derived) in enumerate( - zip(attrs, values, entity.derived()) - ): + for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())): if is_derived and not isinstance(val, ifcopenshell.ifcopenshell_wrapper.attribute_value_derived): if hasattr(logger, "set_instance"): @@ -401,11 +384,12 @@ def validate(f, logger, express_rules=False): log_internal_cpp_errors(filename, logger) # Restore the original value for 'use_attribute_value_derived' - ifcopenshell.ifcopenshell_wrapper.set_feature('use_attribute_value_derived', attribute_value_derived_org) + ifcopenshell.ifcopenshell_wrapper.set_feature("use_attribute_value_derived", attribute_value_derived_org) if express_rules: ifcopenshell.express.rule_executor.run(f, logger) + if __name__ == "__main__": import sys import logging