mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Validation output enhancements
This commit is contained in:
@@ -39,7 +39,12 @@ class error(Exception):
|
|||||||
inst = ""
|
inst = ""
|
||||||
if self.instance:
|
if self.instance:
|
||||||
inst = f"On instance:\n{indent(4, str(self.instance))}\n"
|
inst = f"On instance:\n{indent(4, str(self.instance))}\n"
|
||||||
return f"{inst}Rule {self.rule_name}:\n{indent(4, self.rule_definition)}\nViolated by:\n{indent(4, self.violation)}"
|
if self.rule_name:
|
||||||
|
return f"{inst}Rule {self.rule_name}:\n{indent(4, self.rule_definition)}\nViolated by:\n{indent(4, self.violation)}"
|
||||||
|
else:
|
||||||
|
if inst:
|
||||||
|
inst = f"\n\n{inst}"
|
||||||
|
return f"{self.rule_definition}\n\nViolated by:\n{indent(4, self.violation)}{inst}"
|
||||||
|
|
||||||
|
|
||||||
def fix_type(v):
|
def fix_type(v):
|
||||||
@@ -60,6 +65,19 @@ def fix_type(v):
|
|||||||
def run(f, logger):
|
def run(f, logger):
|
||||||
from _pytest import assertion
|
from _pytest import assertion
|
||||||
|
|
||||||
|
if hasattr(logger, "set_instance"):
|
||||||
|
# when using the json logger, we notify it of the relevant instance
|
||||||
|
pre_annotate_instance = lambda instance: logger.set_state('instance', instance)
|
||||||
|
post_annotate_instance = lambda instance: instance
|
||||||
|
pre_annotate_attribute = lambda attribute: logger.set_state('attribute', attribute)
|
||||||
|
post_annotate_attribute = lambda attribute: None
|
||||||
|
else:
|
||||||
|
# when using the normal text logger the instance is appended to the method
|
||||||
|
pre_annotate_instance = lambda instance: None
|
||||||
|
post_annotate_instance = lambda instance: instance
|
||||||
|
pre_annotate_attribute = lambda attribute: None
|
||||||
|
post_annotate_attribute = lambda attribute: attribute
|
||||||
|
|
||||||
orig = ifcopenshell.settings.unpack_non_aggregate_inverses
|
orig = ifcopenshell.settings.unpack_non_aggregate_inverses
|
||||||
ifcopenshell.settings.unpack_non_aggregate_inverses = True
|
ifcopenshell.settings.unpack_non_aggregate_inverses = True
|
||||||
|
|
||||||
@@ -74,21 +92,26 @@ def run(f, logger):
|
|||||||
|
|
||||||
rules = list(filter(lambda x: hasattr(x, "SCOPE"), scope.values()))
|
rules = list(filter(lambda x: hasattr(x, "SCOPE"), scope.values()))
|
||||||
|
|
||||||
|
logger.set_state('type', 'global_rule')
|
||||||
|
|
||||||
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
|
||||||
|
pre_annotate_attribute(R.__name__)
|
||||||
logger.error(
|
logger.error(
|
||||||
str(
|
str(
|
||||||
error(
|
error(
|
||||||
R.__name__,
|
post_annotate_attribute(R.__name__),
|
||||||
reverse_compile(source.split("\n")[ln - 1]),
|
reverse_compile(source.split("\n")[ln - 1]),
|
||||||
reverse_compile(e.args[0]),
|
reverse_compile(e.args[0]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.set_state('type', 'simpletype_rule')
|
||||||
|
|
||||||
types = {}
|
types = {}
|
||||||
subtypes = collections.defaultdict(list)
|
subtypes = collections.defaultdict(list)
|
||||||
for d in S.declarations():
|
for d in S.declarations():
|
||||||
@@ -131,13 +154,15 @@ def run(f, logger):
|
|||||||
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
|
||||||
|
pre_annotate_instance(instance)
|
||||||
|
pre_annotate_attribute(f"{R.TYPE_NAME}.{R.RULE_NAME}")
|
||||||
logger.error(
|
logger.error(
|
||||||
str(
|
str(
|
||||||
error(
|
error(
|
||||||
R.__name__,
|
post_annotate_attribute(f"{R.TYPE_NAME}.{R.RULE_NAME}"),
|
||||||
reverse_compile(source.split("\n")[ln - 1]),
|
reverse_compile(source.split("\n")[ln - 1]),
|
||||||
reverse_compile(e.args[0]),
|
reverse_compile(e.args[0]),
|
||||||
instance,
|
post_annotate_instance(instance),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -183,19 +208,23 @@ def run(f, logger):
|
|||||||
else:
|
else:
|
||||||
check(val, attr.type_of_attribute(), instance=inst)
|
check(val, attr.type_of_attribute(), instance=inst)
|
||||||
|
|
||||||
|
logger.set_state('type', 'entity_rule')
|
||||||
|
|
||||||
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
|
||||||
|
pre_annotate_instance(inst)
|
||||||
|
pre_annotate_attribute(f"{R.TYPE_NAME}.{R.RULE_NAME}")
|
||||||
logger.error(
|
logger.error(
|
||||||
str(
|
str(
|
||||||
error(
|
error(
|
||||||
R.__name__,
|
post_annotate_attribute(f"{R.TYPE_NAME}.{R.RULE_NAME}"),
|
||||||
reverse_compile(source.split("\n")[ln - 1]),
|
reverse_compile(source.split("\n")[ln - 1]),
|
||||||
reverse_compile(e.args[0]),
|
reverse_compile(e.args[0]),
|
||||||
inst,
|
post_annotate_instance(inst),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -40,25 +40,27 @@ attribute = ifcopenshell.ifcopenshell_wrapper.attribute
|
|||||||
|
|
||||||
|
|
||||||
class ValidationError(Exception):
|
class ValidationError(Exception):
|
||||||
pass
|
def __init__(self, message, attribute=None):
|
||||||
|
super(ValidationError, self).__init__(message)
|
||||||
|
self.attribute = attribute
|
||||||
|
|
||||||
|
|
||||||
log_entry_type = namedtuple("log_entry_type", ("level", "message", "instance"))
|
log_entry_type = namedtuple("log_entry_type", ("level", "message", "instance", "attribute"))
|
||||||
|
|
||||||
|
|
||||||
class json_logger:
|
class json_logger:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.statements = []
|
self.statements = []
|
||||||
self.instance = None
|
self.state = {}
|
||||||
|
|
||||||
def set_instance(self, instance):
|
def set_state(self, key, value):
|
||||||
self.instance = instance
|
self.state[key] = value
|
||||||
|
|
||||||
def log(self, level, message, *args, **kwargs):
|
def log(self, level, message, *args):
|
||||||
self.statements.append(log_entry_type(level, message % args, kwargs.get("instance"))._asdict())
|
self.statements.append({"level": level, "message": message % args, **self.state})
|
||||||
|
|
||||||
def __getattr__(self, level):
|
def __getattr__(self, level):
|
||||||
return functools.partial(self.log, level, instance=self.instance)
|
return functools.partial(self.log, level)
|
||||||
|
|
||||||
|
|
||||||
simple_type_python_mapping = {
|
simple_type_python_mapping = {
|
||||||
@@ -129,7 +131,7 @@ def assert_valid_inverse(attr, val, schema):
|
|||||||
|
|
||||||
attr_formatted = f"{attr.name()} : {aggr_str}{ent_ref} FOR {attr_ref}"
|
attr_formatted = f"{attr.name()} : {aggr_str}{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", attr.name())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ def assert_valid(attr_type, val, schema, no_throw=False, attr=None):
|
|||||||
if no_throw:
|
if no_throw:
|
||||||
return not invalid
|
return not invalid
|
||||||
elif 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", (attr or attr_type).name())
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -231,12 +233,13 @@ 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("ascii", errors="ignore")
|
line = lines[bisect.bisect_left(cs, int(offsets[0]))].decode("ascii", errors="ignore").rstrip()
|
||||||
m = chr_offset_re.sub("", msg["message"])
|
m = chr_offset_re.sub("", msg["message"])
|
||||||
|
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
logger.set_instance(line)
|
logger.set_state('instance', line)
|
||||||
logger.error(m)
|
logger.set_state('attribute', None)
|
||||||
|
logger.error("%s:\n\n%s" % (m, line))
|
||||||
else:
|
else:
|
||||||
logger.error("For instance:\n %s\n%s", line, m)
|
logger.error("For instance:\n %s\n%s", line, m)
|
||||||
|
|
||||||
@@ -288,6 +291,8 @@ def validate(f, logger, express_rules=False):
|
|||||||
|
|
||||||
filename = None
|
filename = None
|
||||||
|
|
||||||
|
logger.set_state('type', 'schema')
|
||||||
|
|
||||||
if not isinstance(f, ifcopenshell.file):
|
if not isinstance(f, ifcopenshell.file):
|
||||||
|
|
||||||
# get_log() clears log existing output
|
# get_log() clears log existing output
|
||||||
@@ -302,14 +307,15 @@ def validate(f, logger, express_rules=False):
|
|||||||
|
|
||||||
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema)
|
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema)
|
||||||
for inst in f:
|
for inst in f:
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
logger.set_instance(inst)
|
logger.set_state('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_state"):
|
||||||
|
logger.set_state('attribute', None)
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
else:
|
else:
|
||||||
logger.error("For instance:\n %s\n%s", inst, e)
|
logger.error("For instance:\n %s\n%s", inst, e)
|
||||||
@@ -321,8 +327,9 @@ def validate(f, logger, express_rules=False):
|
|||||||
values[i] = inst[i]
|
values[i] = inst[i]
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
logger.error("Invalid attribute value for %s.%s", entity, attrs[i])
|
logger.set_state('attribute', f"{entity.name()}.{attrs[i].name()}")
|
||||||
|
logger.error("Invalid attribute value")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
"For instance:\n %s\n %s\nInvalid attribute value for %s.%s",
|
"For instance:\n %s\n %s\nInvalid attribute value for %s.%s",
|
||||||
@@ -337,8 +344,9 @@ def validate(f, logger, express_rules=False):
|
|||||||
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 is_derived and not isinstance(val, ifcopenshell.ifcopenshell_wrapper.attribute_value_derived):
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
logger.error("Attribute %s.%s is derived in subtype", entity, attr)
|
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||||
|
logger.error("Attribute is derived in subtype")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
"For instance:\n %s\n %s\nWith attribute:\n %s\nDerived in subtype\n",
|
"For instance:\n %s\n %s\nWith attribute:\n %s\nDerived in subtype\n",
|
||||||
@@ -348,8 +356,9 @@ def validate(f, logger, express_rules=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if val is None and not attr.optional() and not is_derived:
|
if val is None and not attr.optional() and not is_derived:
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
logger.error("Attribute %s.%s not optional", entity, attr)
|
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||||
|
logger.error("Attribute not optional")
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
"For instance:\n %s\n %s\nWith attribute:\n %s\nNot optional\n",
|
"For instance:\n %s\n %s\nWith attribute:\n %s\nNot optional\n",
|
||||||
@@ -363,7 +372,8 @@ def validate(f, logger, express_rules=False):
|
|||||||
try:
|
try:
|
||||||
assert_valid(attr_type, val, schema, attr=attr)
|
assert_valid(attr_type, val, schema, attr=attr)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
|
logger.set_state('attribute', e.attribute)
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -378,7 +388,8 @@ def validate(f, logger, express_rules=False):
|
|||||||
try:
|
try:
|
||||||
assert_valid_inverse(attr, val, schema)
|
assert_valid_inverse(attr, val, schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
if hasattr(logger, "set_instance"):
|
if hasattr(logger, "set_state"):
|
||||||
|
logger.set_state('attribute', f"{entity.name()}.{attr.name()}")
|
||||||
logger.error(str(e))
|
logger.error(str(e))
|
||||||
else:
|
else:
|
||||||
logger.error("For instance:\n %s\n%s", inst, e)
|
logger.error("For instance:\n %s\n%s", inst, e)
|
||||||
@@ -395,6 +406,9 @@ def validate(f, logger, express_rules=False):
|
|||||||
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:
|
||||||
|
if hasattr(logger, 'set_state'):
|
||||||
|
logger.set_state('instance', None)
|
||||||
|
logger.set_state('attribute', None)
|
||||||
ifcopenshell.express.rule_executor.run(f, logger)
|
ifcopenshell.express.rule_executor.run(f, logger)
|
||||||
|
|
||||||
|
|
||||||
@@ -416,7 +430,14 @@ if __name__ == "__main__":
|
|||||||
validate(fn, logger, "--rules" in flags)
|
validate(fn, logger, "--rules" in flags)
|
||||||
|
|
||||||
if "--json" in flags:
|
if "--json" in flags:
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
conv = str
|
conv = str
|
||||||
if "--spf" in flags:
|
if "--spf" in flags:
|
||||||
conv = lambda x: x.to_string() if isinstance(x, ifcopenshell.entity_instance) else str(x)
|
conv = lambda x: x.to_string() if isinstance(x, ifcopenshell.entity_instance) else str(x)
|
||||||
|
if "--fields" in flags:
|
||||||
|
def conv(x):
|
||||||
|
if isinstance(x, ifcopenshell.entity_instance):
|
||||||
|
return x.get_info(scalar_only=True)
|
||||||
|
else:
|
||||||
|
return str(x)
|
||||||
print("\n".join(json.dumps(x, default=conv) for x in logger.statements))
|
print("\n".join(json.dumps(x, default=conv) for x in logger.statements))
|
||||||
|
|||||||
Reference in New Issue
Block a user