mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Feature: express rules (#2662)
* temp reasonable state, missing rule_head contents * better state, with init() and rule_id, but some missing parameters * performance; embed root in dict; fix lookup * Add ast_utils * Fixes related to listnode and typo * First steps with code gen * Finalize first steps towards working code generation * Backwards compat * Rule execution, error formatting, test cases * Where rule: support indexing, more expressions, test plane angle measure * positive length measure test cases * Venture into entity rules, test actor role * Fixed for not unary op and address test cases * Test group qualifier and air terminal type * typeof() implementation and test annotation curve occ * Flexible handling of expression, type set intersection, IfcAnnotationSurface test cases * Functions, derived attrs, fix branch order, allow to terminate branches, apply -1 to index * Implement express query(), test cases for 2x3 bspline curve * Function args, if-else, repeat, entity instance construction, cshape test cases * Multiple function call arguments and other small but influential changes. IfcDotProject() almost working except for variable case * Make lowercase and fix assignment to qualified lhs * Entity instance schema without file, aggregate assignment workaround, range fix, lowercase instance locals, extruded area direction test cases * Enumeration item handling, query() robustness, arbitrary profile test cases * Empty aggregate_initializer, xor and instance equality, nested expression fix in IfcCrossProduct, axis2-3d test cases * Numeric stable sort with indices greater then 10, get ast node parent, redeclared derived support, nested expression robustness, define entity functions, case statements, xor, mod, bool literals, nvl, unknown, conversion based unit test cases * Safeguard for schema name case norm, case norm query variable_id * free instance comparison, supertype serialization fix, include inherited attributes in locals, proper elif-else in case stmt, loindex, shape rep test cases * Union operator on set, retain general_aggr_types in parse tree, test cases for property set * Escape stmt, rule locals and statements * Return calculated values for redeclared derived attributes * fix for rules without stmt, add derived and inverse attributes to locals, filter unused locals, wrap exists() arg with lambda to catch indexerror, support repeated aggr init element, typeof() none check, enum namespace uppercase * Regen 2x3 rules * Rule test filter only on basename * Update fixtures for compliance with full body of rules * Add rule support to ifcopenshell.validate * Implement usedin() function, test cases for IfcWallSC MLS * blength * Run code generation on all schemas * Try to eliminate runtime pyparsing dep * Try to eliminate runtime pyparsing dep
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
import os
|
||||
import ast
|
||||
import collections
|
||||
|
||||
from dataclasses import dataclass
|
||||
from _pytest import assertion
|
||||
|
||||
import ifcopenshell
|
||||
from ifcopenshell.validate import json_logger
|
||||
|
||||
from codegen import indent
|
||||
|
||||
def reverse_compile(s):
|
||||
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
|
||||
|
||||
def __str__(self):
|
||||
inst = ""
|
||||
if self.instance:
|
||||
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)}"
|
||||
|
||||
|
||||
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))
|
||||
|
||||
# We don't do this anymore, because it doesn't fix instance attribute lookups
|
||||
# We now instead perform a -1 on the index qualifier in the code generation
|
||||
pass
|
||||
# @todo enrich entity instances with code to evaluate derived attributes
|
||||
return v
|
||||
|
||||
|
||||
def run(f, logger):
|
||||
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')
|
||||
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']:
|
||||
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])
|
||||
)))
|
||||
|
||||
types = {}
|
||||
subtypes = collections.defaultdict(list)
|
||||
for d in S.declarations():
|
||||
if isinstance(d, ifcopenshell.ifcopenshell_wrapper.type_declaration):
|
||||
types[d.name()] = d
|
||||
if isinstance(d.declared_type(), ifcopenshell.ifcopenshell_wrapper.named_type):
|
||||
subtypes[d.declared_type().declared_type().name()].append(d.name())
|
||||
|
||||
D = collections.defaultdict(list)
|
||||
for r in rules:
|
||||
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())
|
||||
elif isinstance(ty, ifcopenshell.ifcopenshell_wrapper.aggregation_type):
|
||||
# breakpoint()
|
||||
pass
|
||||
elif isinstance(ty, ifcopenshell.ifcopenshell_wrapper.simple_type):
|
||||
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
|
||||
)))
|
||||
|
||||
# @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)):
|
||||
type = type.declared_type()
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
assert isinstance(type, ifcopenshell.ifcopenshell_wrapper.aggregation_type)
|
||||
ty = type.type_of_element()
|
||||
for v in value:
|
||||
check(v, ty, instance=inst)
|
||||
elif isinstance(value, ifcopenshell.entity_instance):
|
||||
if isinstance(S.declaration_by_name(value.is_a()), ifcopenshell.ifcopenshell_wrapper.entity):
|
||||
# top level entity instances will be checked on their own
|
||||
pass
|
||||
else:
|
||||
# unpack the type instance
|
||||
check(value[0], S.declaration_by_name(value.is_a()), instance=inst)
|
||||
|
||||
|
||||
for inst in f:
|
||||
values = list(inst)
|
||||
entity = S.declaration_by_name(inst.is_a())
|
||||
attrs = entity.all_attributes()
|
||||
for i, (attr, val, is_derived) in enumerate(zip(attrs, values, entity.derived())):
|
||||
if is_derived:
|
||||
# @todo
|
||||
pass
|
||||
else:
|
||||
check(val, attr.type_of_attribute(), instance=inst)
|
||||
|
||||
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
|
||||
)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import ifcopenshell
|
||||
|
||||
filenames = [x for x in sys.argv[1:] if not x.startswith("--")]
|
||||
flags = set(x for x in sys.argv[1:] if x.startswith("--"))
|
||||
|
||||
for fn in filenames:
|
||||
if "--json" in flags:
|
||||
logger = json_logger()
|
||||
else:
|
||||
logger = logging.getLogger("validate")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
f = ifcopenshell.open(fn)
|
||||
|
||||
run(f, logger)
|
||||
|
||||
if "--json" in flags:
|
||||
print("\n".join(json.dumps(x, default=str) for x in logger.statements))
|
||||
Reference in New Issue
Block a user