Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions model.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,46 @@

import ifcopenshell

from . import mvdxml_expression

extracted_data = list[dict["rule", Any]]
concept_data = dict[str, Any]
verification_matrix = dict[str, dict[str, int]]


def _parse_mvdxml_token(value: str) -> Any:
value = value.strip()
if value.lower() == "true":
return True
if value.lower() == "false":
return False
return ast.literal_eval(value)
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
# mvdXML parameters are frequently unquoted identifiers or namespaced
# codes, e.g. ``ContextType=Model`` or ``CRSName[Value]=EPSG:5555``.
return value


def _iter_expressions(tree: Iterable[Any]) -> Iterator[tuple[Any, ...]]:
"""Yield every expression in a ``TemplateRules`` tree.

``parser.parse_template_rules`` produces a nested tuple tree:
TemplateRules group -> TemplateRule (the ``;``-separated expressions of
one ``Parameters`` attribute) -> expression (tokens: ``node`` | ``AND`` |
``OR``). Operators *between* sibling rules are strings and are skipped:
every expression must hold. Callers may therefore pass either a full
parser tree or a bare ``mvdxml_expression.parse()`` result.
"""
for item in tree:
if isinstance(item, str):
continue
if item and all(
isinstance(token, (mvdxml_expression.node, str)) for token in item
):
yield tuple(item)
else:
yield from _iter_expressions(item)


def _merge_dictionaries(dicts: Iterable[dict[rule, Any]]) -> dict[rule, Any]:
Expand Down Expand Up @@ -252,7 +281,7 @@ def extract(
return extracted_entities_data

def validate(self, data: extracted_data) -> tuple[bool, str]:
rules = [value[0] for value in self.rules() if not isinstance(value, str)]
rules = list(_iter_expressions(self.rules()))

def transform_data(values: dict[rule, Any]) -> dict[str | None, Any]:
return {
Expand Down Expand Up @@ -283,8 +312,10 @@ def translate(value: Any) -> Any:
)
if value.b == "Type":
item = values.get(value.a)
# Non-entity values (str, float, ...) have no
# IFC type: the type test simply does not hold.
return bool(
item is not None
isinstance(item, ifcopenshell.entity_instance)
and item.is_a(_parse_mvdxml_token(value.c))
)
if value.b == "Exists":
Expand Down
15 changes: 9 additions & 6 deletions tests/test_graphviz.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# This file was generated with the assistance of an AI coding tool.

from unittest import mock

import pytest

import ifcopenshell
Expand Down Expand Up @@ -132,12 +134,13 @@ def test_from_graphviz_entity_leaf_filters_ifc_type() -> None:
"""
parsed = template.from_graphviz(source)

class entity(ifcopenshell.entity_instance):
def __init__(self, ifc_class: str):
self.ifc_class = ifc_class

def is_a(self, ifc_class: str) -> bool:
return self.ifc_class == ifc_class
def entity(ifc_class: str) -> ifcopenshell.entity_instance:
# A Mock passes the isinstance() check in rule.extract() without
# subclassing entity_instance, whose __setattr__/__getattr__ require
# wrapped_data and otherwise recurse.
instance = mock.Mock(spec=ifcopenshell.entity_instance)
instance.is_a.side_effect = lambda cls: cls == ifc_class
return instance

class relationship:
def __init__(self, relating_property_definition: entity):
Expand Down
61 changes: 61 additions & 0 deletions tests/test_mvd.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,64 @@ def test_top_level_unbound_rule_has_no_parent_binding() -> None:
parsed_template = template("IfcWall", "Status", (top_level,))

assert parsed_template.binding_for(top_level) is None


@pytest.mark.parametrize(
"value,expected",
[
("Model", "Model"), # ContextType[Value]=Model
("IfcLocalPlacement", "IfcLocalPlacement"), # entity name as a parameter
("EPSG:5555", "EPSG:5555"), # not valid Python syntax either
(" QTO_OCCURRENCEDRIVEN ", "QTO_OCCURRENCEDRIVEN"),
("'IfcLabel'", "IfcLabel"), # quoted literals keep working
("TRUE", True),
("42", 42),
],
)
def test_parse_mvdxml_token_falls_back_to_the_raw_string(
value: str, expected: object
) -> None:
assert model._parse_mvdxml_token(value) == expected


def test_empty_template_rules_group_is_skipped() -> None:
status = rule("AttributeRule", "Status", bind="Status")
concept = concept_or_applicability(
"status",
template("IfcWall", "Status", (status,)),
((), "and", (parse_expression("Status='complete'"),)),
)

valid, _ = concept.validate([{status: "complete"}])

assert valid


def test_every_template_rule_expression_is_evaluated() -> None:
status = rule("AttributeRule", "Status", bind="Status")
concept = concept_or_applicability(
"status",
template("IfcWall", "Status", (status,)),
(parse_expression("Status='complete'; Status='draft'"),),
)

valid, _ = concept.validate([{status: "complete"}])

assert not valid


def test_official_reference_view_expressions_all_parse() -> None:
roots = parse(EXAMPLES / "officials" / "ReferenceView_V1-2.mvdxml")

tokens = [
token
for root in roots
for concept in root.concepts()
for expression in model._iter_expressions(concept.rules())
for token in expression
if not isinstance(token, str)
]

assert len(tokens) > 400
for token in tokens:
model._parse_mvdxml_token(token.c)