mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 10:11:46 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user