Run black on validation, lazy-load _pytest.assertion so that it isn't a hard dependency for non-express rule validation.

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