mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Add ifcedit: CLI wrapper for ifcopenshell.api mutation functions (#7846)
ifcedit is a new command-line tool for executing ifcopenshell.api mutations from the shell. It wraps the entire API surface — any function callable via ifcopenshell.api can be invoked without writing Python.
Subcommands:
list [module] — list all API modules, or functions within a module
docs <module.function> — full documentation (params, types, descriptions)
run <file> <module.function> [--param value ...] — execute a mutation; overwrites input file by default, or use -o <output> to write elsewhere; --dry-run validates without executing
quantify list — list available QTO rules
quantify run <file> <rule> — run quantity take-off, writing IfcElementQuantity psets back to the file
Parameter coercion: entity references can be passed as step IDs (strings); lists, dicts, booleans, and None are handled automatically.
Usage:
python3 -m ifcedit run model.ifc root.remove_product --product 42
python3 -m ifcedit docs geometry.edit_object_placement
Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
@@ -0,0 +1,63 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.api.owner.settings
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model():
|
||||
"""Create an IFC4 model with a spatial hierarchy and a wall."""
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
|
||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
|
||||
ifcopenshell.api.unit.assign_unit(f)
|
||||
|
||||
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
|
||||
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
|
||||
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
||||
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
||||
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model_file(model, tmp_path):
|
||||
"""Write the model fixture to a temp file and return the path."""
|
||||
path = tmp_path / "test.ifc"
|
||||
model.write(str(path))
|
||||
return str(path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library():
|
||||
"""Create an IFC4 library with a single IfcWallType asset."""
|
||||
lib = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
ifcopenshell.api.root.create_entity(lib, ifc_class="IfcProject", name="TestLibrary")
|
||||
ifcopenshell.api.unit.assign_unit(lib)
|
||||
ifcopenshell.api.root.create_entity(lib, ifc_class="IfcWallType", name="WAL01")
|
||||
return lib
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library_file(library, tmp_path):
|
||||
"""Write the library fixture to a temp file and return the path."""
|
||||
path = tmp_path / "library.ifc"
|
||||
library.write(str(path))
|
||||
return str(path)
|
||||
@@ -0,0 +1,158 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import json
|
||||
from typing import Literal, Optional, Union
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.project
|
||||
import pytest
|
||||
|
||||
from ifcedit.coerce import coerce_value
|
||||
|
||||
|
||||
class TestStringCoercion:
|
||||
def test_plain_string(self):
|
||||
assert coerce_value("hello", str) == "hello"
|
||||
|
||||
def test_empty_string(self):
|
||||
assert coerce_value("", str) == ""
|
||||
|
||||
|
||||
class TestIntCoercion:
|
||||
def test_plain_int(self):
|
||||
assert coerce_value("42", int) == 42
|
||||
|
||||
def test_hash_prefix(self):
|
||||
assert coerce_value("#42", int) == 42
|
||||
|
||||
def test_negative(self):
|
||||
assert coerce_value("-5", int) == -5
|
||||
|
||||
|
||||
class TestFloatCoercion:
|
||||
def test_plain_float(self):
|
||||
assert coerce_value("3.14", float) == pytest.approx(3.14)
|
||||
|
||||
def test_integer_as_float(self):
|
||||
assert coerce_value("5", float) == 5.0
|
||||
|
||||
|
||||
class TestBoolCoercion:
|
||||
def test_true_values(self):
|
||||
for val in ("true", "True", "TRUE", "1", "yes"):
|
||||
assert coerce_value(val, bool) is True
|
||||
|
||||
def test_false_values(self):
|
||||
for val in ("false", "False", "0", "no"):
|
||||
assert coerce_value(val, bool) is False
|
||||
|
||||
|
||||
class TestOptionalCoercion:
|
||||
def test_optional_string(self):
|
||||
assert coerce_value("hello", Optional[str]) == "hello"
|
||||
|
||||
def test_optional_none(self):
|
||||
assert coerce_value("none", Optional[str]) is None
|
||||
assert coerce_value("None", Optional[str]) is None
|
||||
|
||||
def test_optional_int(self):
|
||||
assert coerce_value("42", Optional[int]) == 42
|
||||
|
||||
|
||||
class TestUnionCoercion:
|
||||
def test_union_str_int(self):
|
||||
# Tries str first (or int first depending on order), both work
|
||||
result = coerce_value("hello", Union[str, int])
|
||||
assert result == "hello"
|
||||
|
||||
def test_union_int_none(self):
|
||||
result = coerce_value("42", Union[int, None])
|
||||
assert result == 42
|
||||
|
||||
|
||||
class TestLiteralCoercion:
|
||||
def test_valid_literal(self):
|
||||
assert coerce_value("IFC4", Literal["IFC2X3", "IFC4", "IFC4X3"]) == "IFC4"
|
||||
|
||||
def test_invalid_literal(self):
|
||||
with pytest.raises(ValueError, match="not one of"):
|
||||
coerce_value("IFC5", Literal["IFC2X3", "IFC4", "IFC4X3"])
|
||||
|
||||
|
||||
class TestDictCoercion:
|
||||
def test_json_dict(self):
|
||||
result = coerce_value('{"IsExternal": true, "FireRating": "2HR"}', dict[str, object])
|
||||
assert result == {"IsExternal": True, "FireRating": "2HR"}
|
||||
|
||||
def test_mixed_float_int_list_coerced_to_float(self):
|
||||
# [0.419, 0, 0.908] — JSON integer 0 mixed with floats must become float
|
||||
# so ifcopenshell AGGREGATE OF DOUBLE attributes (e.g. DirectionRatios) don't reject the list
|
||||
result = coerce_value('{"DirectionRatios": [0.419, 0, 0.908]}', dict[str, object])
|
||||
assert result["DirectionRatios"] == pytest.approx([0.419, 0.0, 0.908])
|
||||
assert all(isinstance(v, float) for v in result["DirectionRatios"])
|
||||
|
||||
def test_pure_int_list_not_coerced(self):
|
||||
# All-integer lists (e.g. face indices) must stay as ints
|
||||
result = coerce_value('{"CoordIndex": [0, 1, 2]}', dict[str, object])
|
||||
assert result["CoordIndex"] == [0, 1, 2]
|
||||
assert all(isinstance(v, int) for v in result["CoordIndex"])
|
||||
|
||||
|
||||
class TestListCoercion:
|
||||
def test_comma_separated(self):
|
||||
result = coerce_value("a,b,c", list[str])
|
||||
assert result == ["a", "b", "c"]
|
||||
|
||||
def test_json_array(self):
|
||||
result = coerce_value("[1, 2, 3]", list[int])
|
||||
assert result == [1, 2, 3]
|
||||
|
||||
|
||||
class TestEntityCoercion:
|
||||
def test_entity_by_id(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = coerce_value(str(wall.id()), ifcopenshell.entity_instance, model)
|
||||
assert result == wall
|
||||
|
||||
def test_entity_with_hash(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = coerce_value(f"#{wall.id()}", ifcopenshell.entity_instance, model)
|
||||
assert result == wall
|
||||
|
||||
def test_entity_not_found(self, model):
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
coerce_value("999999", ifcopenshell.entity_instance, model)
|
||||
|
||||
def test_entity_list(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = coerce_value(str(wall.id()), list[ifcopenshell.entity_instance], model)
|
||||
assert len(result) == 1
|
||||
assert result[0] == wall
|
||||
|
||||
def test_entity_list_multiple(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
storey = model.by_type("IfcBuildingStorey")[0]
|
||||
result = coerce_value(f"{wall.id()},{storey.id()}", list[ifcopenshell.entity_instance], model)
|
||||
assert len(result) == 2
|
||||
|
||||
def test_entity_no_model(self):
|
||||
with pytest.raises(ValueError, match="without an IFC model"):
|
||||
coerce_value("42", ifcopenshell.entity_instance, None)
|
||||
|
||||
|
||||
class TestFileCoercion:
|
||||
def test_opens_file_from_path(self, model_file):
|
||||
result = coerce_value(model_file, ifcopenshell.file)
|
||||
assert isinstance(result, ifcopenshell.file)
|
||||
|
||||
def test_entity_from_lookup_file(self, model_file):
|
||||
lib = ifcopenshell.open(model_file)
|
||||
wall = lib.by_type("IfcWall")[0]
|
||||
empty_model = ifcopenshell.api.project.create_file()
|
||||
result = coerce_value(str(wall.id()), ifcopenshell.entity_instance, empty_model, lookup_file=lib)
|
||||
assert result.id() == wall.id()
|
||||
assert result.is_a("IfcWall")
|
||||
|
||||
|
||||
class TestFallback:
|
||||
def test_no_type_hint(self):
|
||||
assert coerce_value("hello", None) == "hello"
|
||||
@@ -0,0 +1,104 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
from ifcedit.discover import function_docs, list_functions, list_modules
|
||||
|
||||
|
||||
class TestListModules:
|
||||
def test_returns_list(self):
|
||||
result = list_modules()
|
||||
assert isinstance(result, list)
|
||||
assert len(result) > 0
|
||||
|
||||
def test_module_structure(self):
|
||||
result = list_modules()
|
||||
for entry in result:
|
||||
assert "module" in entry
|
||||
assert "description" in entry
|
||||
assert "functions" in entry
|
||||
assert "count" in entry
|
||||
assert isinstance(entry["functions"], list)
|
||||
assert entry["count"] == len(entry["functions"])
|
||||
|
||||
def test_known_modules_present(self):
|
||||
result = list_modules()
|
||||
module_names = [m["module"] for m in result]
|
||||
for expected in ("root", "spatial", "pset", "aggregate", "unit"):
|
||||
assert expected in module_names
|
||||
|
||||
def test_root_module_has_functions(self):
|
||||
result = list_modules()
|
||||
root = next(m for m in result if m["module"] == "root")
|
||||
assert "create_entity" in root["functions"]
|
||||
assert root["count"] >= 3
|
||||
|
||||
|
||||
class TestListFunctions:
|
||||
def test_root_functions(self):
|
||||
result = list_functions("root")
|
||||
assert isinstance(result, list)
|
||||
names = [f["name"] for f in result]
|
||||
assert "create_entity" in names
|
||||
|
||||
def test_function_structure(self):
|
||||
result = list_functions("root")
|
||||
for fn in result:
|
||||
assert "name" in fn
|
||||
assert "description" in fn
|
||||
assert "params" in fn
|
||||
|
||||
def test_create_entity_params(self):
|
||||
result = list_functions("root")
|
||||
create = next(f for f in result if f["name"] == "create_entity")
|
||||
param_names = [p["name"] for p in create["params"]]
|
||||
assert "ifc_class" in param_names
|
||||
assert "name" in param_names
|
||||
|
||||
def test_pset_functions(self):
|
||||
result = list_functions("pset")
|
||||
names = [f["name"] for f in result]
|
||||
assert "add_pset" in names
|
||||
assert "edit_pset" in names
|
||||
|
||||
|
||||
class TestFunctionDocs:
|
||||
def test_create_entity_docs(self):
|
||||
result = function_docs("root", "create_entity")
|
||||
assert result["module"] == "root"
|
||||
assert result["function"] == "create_entity"
|
||||
assert result["description"]
|
||||
assert isinstance(result["params"], list)
|
||||
assert len(result["params"]) > 0
|
||||
|
||||
def test_params_have_types(self):
|
||||
result = function_docs("root", "create_entity")
|
||||
for param in result["params"]:
|
||||
assert "name" in param
|
||||
assert "type" in param
|
||||
|
||||
def test_params_have_descriptions(self):
|
||||
result = function_docs("root", "create_entity")
|
||||
ifc_class = next(p for p in result["params"] if p["name"] == "ifc_class")
|
||||
assert "description" in ifc_class
|
||||
assert len(ifc_class["description"]) > 0
|
||||
|
||||
def test_return_type(self):
|
||||
result = function_docs("root", "create_entity")
|
||||
assert "return_type" in result
|
||||
|
||||
def test_assign_container_docs(self):
|
||||
result = function_docs("spatial", "assign_container")
|
||||
assert result["module"] == "spatial"
|
||||
param_names = [p["name"] for p in result["params"]]
|
||||
assert "products" in param_names
|
||||
assert "relating_structure" in param_names
|
||||
|
||||
def test_unknown_function_raises(self):
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
function_docs("root", "nonexistent_function")
|
||||
|
||||
def test_edit_pset_docs(self):
|
||||
result = function_docs("pset", "edit_pset")
|
||||
param_names = [p["name"] for p in result["params"]]
|
||||
assert "pset" in param_names
|
||||
assert "properties" in param_names
|
||||
@@ -0,0 +1,100 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def run_ifcedit(*args):
|
||||
"""Run ifcedit as a subprocess and return (stdout, stderr, returncode)."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcedit", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout, result.stderr, result.returncode
|
||||
|
||||
|
||||
class TestListCommand:
|
||||
def test_list_all_modules(self):
|
||||
stdout, stderr, rc = run_ifcedit("list")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert isinstance(data, list)
|
||||
module_names = [m["module"] for m in data]
|
||||
assert "root" in module_names
|
||||
assert "spatial" in module_names
|
||||
|
||||
def test_list_module_functions(self):
|
||||
stdout, stderr, rc = run_ifcedit("list", "root")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert isinstance(data, list)
|
||||
names = [f["name"] for f in data]
|
||||
assert "create_entity" in names
|
||||
|
||||
def test_list_text_format(self):
|
||||
stdout, stderr, rc = run_ifcedit("--format", "text", "list")
|
||||
assert rc == 0
|
||||
assert "root" in stdout
|
||||
|
||||
|
||||
class TestDocsCommand:
|
||||
def test_docs_create_entity(self):
|
||||
stdout, stderr, rc = run_ifcedit("docs", "root.create_entity")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["module"] == "root"
|
||||
assert data["function"] == "create_entity"
|
||||
assert "params" in data
|
||||
|
||||
def test_docs_invalid_path(self):
|
||||
stdout, stderr, rc = run_ifcedit("docs", "invalid_path")
|
||||
assert rc != 0
|
||||
assert "module.function" in stderr
|
||||
|
||||
def test_docs_unknown_function(self):
|
||||
stdout, stderr, rc = run_ifcedit("docs", "root.nonexistent")
|
||||
assert rc != 0
|
||||
|
||||
|
||||
class TestRunCommand:
|
||||
def test_create_entity(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"run", model_file, "root.create_entity", "--ifc_class", "IfcWall", "--name", "CLIWall"
|
||||
)
|
||||
assert rc == 0, f"stderr: {stderr}"
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
assert data["result"]["type"] == "IfcWall"
|
||||
assert data["result"]["name"] == "CLIWall"
|
||||
|
||||
def test_dry_run(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit("run", model_file, "root.create_entity", "--dry-run", "--ifc_class", "IfcWall")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
assert data["dry_run"] is True
|
||||
|
||||
def test_output_to_different_file(self, model_file, tmp_path):
|
||||
output = str(tmp_path / "output.ifc")
|
||||
stdout, stderr, rc = run_ifcedit(
|
||||
"run", model_file, "root.create_entity", "-o", output, "--ifc_class", "IfcSlab"
|
||||
)
|
||||
assert rc == 0, f"stderr: {stderr}"
|
||||
data = json.loads(stdout)
|
||||
assert data["ok"] is True
|
||||
|
||||
import os
|
||||
|
||||
assert os.path.exists(output)
|
||||
|
||||
def test_run_error_bad_function(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit("run", model_file, "root.nonexistent")
|
||||
assert rc != 0
|
||||
|
||||
def test_run_invalid_function_path(self, model_file):
|
||||
stdout, stderr, rc = run_ifcedit("run", model_file, "invalid_path")
|
||||
assert rc != 0
|
||||
assert "module.function" in stderr
|
||||
@@ -0,0 +1,87 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
from __future__ import annotations
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
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.quantify import AVAILABLE_RULES, list_rules, run_quantify
|
||||
|
||||
|
||||
class TestListRules:
|
||||
def test_returns_list(self):
|
||||
result = list_rules()
|
||||
assert isinstance(result, list)
|
||||
|
||||
def test_each_entry_has_name(self):
|
||||
result = list_rules()
|
||||
for entry in result:
|
||||
assert "name" in entry
|
||||
|
||||
def test_ifc4_rule_present(self):
|
||||
result = list_rules()
|
||||
names = [r["name"] for r in result]
|
||||
assert "IFC4QtoBaseQuantities" in names
|
||||
|
||||
def test_ifc4x3_rule_present(self):
|
||||
result = list_rules()
|
||||
names = [r["name"] for r in result]
|
||||
assert "IFC4X3QtoBaseQuantities" in names
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def quantify_model():
|
||||
"""Create an IFC4 model with a wall element."""
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
|
||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
|
||||
ifcopenshell.api.unit.assign_unit(f)
|
||||
|
||||
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
|
||||
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
|
||||
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
||||
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
||||
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
||||
|
||||
return f
|
||||
|
||||
|
||||
class TestRunQuantify:
|
||||
def test_returns_ok_true(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities")
|
||||
assert result["ok"] is True
|
||||
|
||||
def test_returns_rule_name(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities")
|
||||
assert result["rule"] == "IFC4QtoBaseQuantities"
|
||||
|
||||
def test_returns_elements_quantified(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities")
|
||||
assert "elements_quantified" in result
|
||||
assert isinstance(result["elements_quantified"], int)
|
||||
|
||||
def test_unknown_rule_returns_error(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "NonExistentRule")
|
||||
assert result["ok"] is False
|
||||
assert "error" in result
|
||||
|
||||
def test_selector_restricts_elements(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities", selector="IfcWall")
|
||||
assert result["ok"] is True
|
||||
assert result["rule"] == "IFC4QtoBaseQuantities"
|
||||
|
||||
def test_empty_selector_runs_on_all(self, quantify_model):
|
||||
result = run_quantify(quantify_model, "IFC4QtoBaseQuantities", selector=None)
|
||||
assert result["ok"] is True
|
||||
@@ -0,0 +1,97 @@
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
|
||||
from ifcedit.run import run_api, serialize_result
|
||||
|
||||
|
||||
class TestRunApi:
|
||||
def test_create_entity(self, model):
|
||||
result = run_api(model, "root", "create_entity", {"ifc_class": "IfcWall", "name": "NewWall"})
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcWall"
|
||||
assert result["result"]["name"] == "NewWall"
|
||||
assert isinstance(result["result"]["id"], int)
|
||||
|
||||
def test_create_entity_default_class(self, model):
|
||||
result = run_api(model, "root", "create_entity", {})
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcBuildingElementProxy"
|
||||
|
||||
def test_assign_container(self, model):
|
||||
wall = ifcopenshell.api.root.create_entity(model, ifc_class="IfcWall", name="TestWall2")
|
||||
storey = model.by_type("IfcBuildingStorey")[0]
|
||||
result = run_api(
|
||||
model,
|
||||
"spatial",
|
||||
"assign_container",
|
||||
{"products": str(wall.id()), "relating_structure": str(storey.id())},
|
||||
)
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcRelContainedInSpatialStructure"
|
||||
|
||||
def test_add_pset(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = run_api(model, "pset", "add_pset", {"product": str(wall.id()), "name": "Pset_WallCommon"})
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcPropertySet"
|
||||
|
||||
def test_unknown_function(self, model):
|
||||
result = run_api(model, "root", "nonexistent", {})
|
||||
assert result["ok"] is False
|
||||
assert "Cannot find" in result["error"]
|
||||
|
||||
def test_unknown_parameter(self, model):
|
||||
result = run_api(model, "root", "create_entity", {"bogus_param": "value"})
|
||||
assert result["ok"] is False
|
||||
assert "Unknown parameter" in result["error"]
|
||||
|
||||
def test_bad_entity_reference(self, model):
|
||||
result = run_api(model, "pset", "add_pset", {"product": "999999", "name": "Pset_WallCommon"})
|
||||
assert result["ok"] is False
|
||||
assert "not found" in result["error"]
|
||||
|
||||
|
||||
class TestAppendAsset:
|
||||
def test_append_asset_from_library(self, model, library_file):
|
||||
lib = ifcopenshell.open(library_file)
|
||||
wall_type = lib.by_type("IfcWallType")[0]
|
||||
result = run_api(
|
||||
model,
|
||||
"project",
|
||||
"append_asset",
|
||||
{"library": library_file, "element": str(wall_type.id())},
|
||||
)
|
||||
assert result["ok"] is True
|
||||
assert result["result"]["type"] == "IfcWallType"
|
||||
assert model.by_type("IfcWallType"), "wall type should have been appended to the model"
|
||||
|
||||
|
||||
class TestSerializeResult:
|
||||
def test_none(self):
|
||||
assert serialize_result(None) is None
|
||||
|
||||
def test_string(self):
|
||||
assert serialize_result("hello") == "hello"
|
||||
|
||||
def test_int(self):
|
||||
assert serialize_result(42) == 42
|
||||
|
||||
def test_entity(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = serialize_result(wall)
|
||||
assert result["id"] == wall.id()
|
||||
assert result["type"] == "IfcWall"
|
||||
assert result["name"] == "Wall001"
|
||||
|
||||
def test_list(self, model):
|
||||
walls = model.by_type("IfcWall")
|
||||
result = serialize_result(walls)
|
||||
assert isinstance(result, list)
|
||||
assert all(isinstance(r, dict) for r in result)
|
||||
|
||||
def test_dict(self):
|
||||
result = serialize_result({"key": "value"})
|
||||
assert result == {"key": "value"}
|
||||
Reference in New Issue
Block a user