mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 23:36:20 +00:00
ifcquery/ifcedit: enable shell scripting by composing query and edit commands
Add --format ids to ifcquery to output step IDs suitable for piping into ifcedit parameters. Add ifcedit foreach to apply an operation to every element in a query result. Extend clash and relations output so --format ids extracts all involved element IDs, enabling one-liners like clash detection piped directly into render. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# Tests for ifcedit.foreach
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.owner.settings
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import pytest
|
||||
|
||||
from ifcedit.foreach import _substitute, run_foreach
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model(model):
|
||||
return model
|
||||
|
||||
|
||||
class TestSubstitute:
|
||||
def test_single_field(self):
|
||||
assert _substitute("--product {id}", {"id": 42}) == "--product 42"
|
||||
|
||||
def test_multiple_fields(self):
|
||||
result = _substitute("{type} #{id} ({name})", {"id": 5, "type": "IfcWall", "name": "W1"})
|
||||
assert result == "IfcWall #5 (W1)"
|
||||
|
||||
def test_no_placeholder(self):
|
||||
assert _substitute("hello", {"id": 1}) == "hello"
|
||||
|
||||
def test_unknown_placeholder_unchanged(self):
|
||||
assert _substitute("{unknown}", {"id": 1}) == "{unknown}"
|
||||
|
||||
|
||||
class TestRunForeach:
|
||||
def _items(self, model, ifc_class):
|
||||
return [{"id": e.id(), "type": e.is_a(), "name": e.Name} for e in model.by_type(ifc_class)]
|
||||
|
||||
def test_rename_single(self, model):
|
||||
items = self._items(model, "IfcWall")
|
||||
result = run_foreach(model, "attribute", "edit_attributes", {"product": "{id}", "attributes": '{"Name": "R"}'}, items)
|
||||
assert result["ok"] is True
|
||||
assert result["count"] == 1
|
||||
assert result["errors"] == []
|
||||
assert model.by_type("IfcWall")[0].Name == "R"
|
||||
|
||||
def test_rename_multiple(self, model):
|
||||
items = self._items(model, "IfcElement")
|
||||
result = run_foreach(model, "attribute", "edit_attributes", {"product": "{id}", "attributes": '{"Name": "X"}'}, items)
|
||||
assert result["ok"] is True
|
||||
assert result["count"] == len(items)
|
||||
|
||||
def test_empty_list(self, model):
|
||||
result = run_foreach(model, "root", "remove_product", {"product": "{id}"}, [])
|
||||
assert result["ok"] is True
|
||||
assert result["count"] == 0
|
||||
assert result["errors"] == []
|
||||
|
||||
def test_bad_id_collects_error(self, model):
|
||||
items = [{"id": 999999, "type": "IfcWall", "name": "X"}]
|
||||
result = run_foreach(model, "root", "remove_product", {"product": "{id}"}, items)
|
||||
assert result["ok"] is False
|
||||
assert result["count"] == 0
|
||||
assert len(result["errors"]) == 1
|
||||
assert result["errors"][0]["index"] == 0
|
||||
|
||||
def test_non_dict_item_collects_error(self, model):
|
||||
result = run_foreach(model, "root", "remove_product", {"product": "{id}"}, ["not_a_dict"])
|
||||
assert result["ok"] is False
|
||||
assert len(result["errors"]) == 1
|
||||
|
||||
def test_partial_failure_counts_successes(self, model):
|
||||
wall_id = model.by_type("IfcWall")[0].id()
|
||||
items = [
|
||||
{"id": wall_id, "type": "IfcWall", "name": "W"},
|
||||
{"id": 999999, "type": "IfcWall", "name": "Bad"},
|
||||
]
|
||||
result = run_foreach(model, "attribute", "edit_attributes", {"product": "{id}", "attributes": '{"Name": "Ok"}'}, items)
|
||||
assert result["ok"] is False
|
||||
assert result["count"] == 1
|
||||
assert len(result["errors"]) == 1
|
||||
@@ -3,15 +3,17 @@ import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import ifcopenshell
|
||||
import pytest
|
||||
|
||||
|
||||
def run_ifcedit(*args):
|
||||
def run_ifcedit(*args, stdin=None):
|
||||
"""Run ifcedit as a subprocess and return (stdout, stderr, returncode)."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcedit", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
input=stdin,
|
||||
)
|
||||
return result.stdout, result.stderr, result.returncode
|
||||
|
||||
@@ -98,3 +100,82 @@ class TestRunCommand:
|
||||
stdout, stderr, rc = run_ifcedit("run", model_file, "invalid_path")
|
||||
assert rc != 0
|
||||
assert "module.function" in stderr
|
||||
|
||||
|
||||
class TestForeachCommand:
|
||||
def _select_json(self, model, ifc_class):
|
||||
"""Build a JSON array like ifcquery select would produce."""
|
||||
elements = model.by_type(ifc_class)
|
||||
return json.dumps([{"id": e.id(), "type": e.is_a(), "name": getattr(e, "Name", None)} for e in elements])
|
||||
|
||||
def test_foreach_rename(self, model, model_file):
|
||||
walls_json = self._select_json(model, "IfcWall")
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "attribute.edit_attributes",
|
||||
"--product", "{id}", "--attributes", '{"Name": "Renamed"}',
|
||||
stdin=walls_json,
|
||||
)
|
||||
assert rc == 0, f"stderr: {stderr}"
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
assert data["count"] == 1
|
||||
assert data["errors"] == []
|
||||
updated = ifcopenshell.open(model_file)
|
||||
assert updated.by_type("IfcWall")[0].Name == "Renamed"
|
||||
|
||||
def test_foreach_multiple_elements(self, model, model_file):
|
||||
# Build a two-item list by selecting all IfcObject (includes spatial structure + elements)
|
||||
elements_json = self._select_json(model, "IfcObject")
|
||||
items = json.loads(elements_json)
|
||||
assert len(items) >= 2
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "attribute.edit_attributes",
|
||||
"--product", "{id}", "--attributes", '{"Name": "Bulk"}',
|
||||
stdin=elements_json,
|
||||
)
|
||||
assert rc == 0, f"stderr: {stderr}"
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
assert data["count"] == len(items)
|
||||
|
||||
def test_foreach_empty_list(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "attribute.edit_attributes",
|
||||
"--product", "{id}", "--attributes", '{"Name": "X"}',
|
||||
stdin="[]",
|
||||
)
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
assert data["count"] == 0
|
||||
|
||||
def test_foreach_invalid_json_stdin(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "root.remove_product", "--product", "{id}",
|
||||
stdin="not json",
|
||||
)
|
||||
assert rc != 0
|
||||
assert "Error" in stderr
|
||||
|
||||
def test_foreach_not_array_stdin(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "root.remove_product", "--product", "{id}",
|
||||
stdin='{"id": 1}',
|
||||
)
|
||||
assert rc != 0
|
||||
assert "Error" in stderr
|
||||
|
||||
def test_foreach_output_to_different_file(self, model, model_file, tmp_path):
|
||||
import os
|
||||
output = str(tmp_path / "out.ifc")
|
||||
walls_json = self._select_json(model, "IfcWall")
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"foreach", model_file, "attribute.edit_attributes",
|
||||
"-o", output,
|
||||
"--product", "{id}", "--attributes", '{"Name": "OutFile"}',
|
||||
stdin=walls_json,
|
||||
)
|
||||
assert rc == 0, f"stderr: {stderr}"
|
||||
assert os.path.exists(output)
|
||||
updated = ifcopenshell.open(output)
|
||||
assert updated.by_type("IfcWall")[0].Name == "OutFile"
|
||||
|
||||
Reference in New Issue
Block a user