diff --git a/src/bonsai/bonsai/bim/module/project/data.py b/src/bonsai/bonsai/bim/module/project/data.py index db64041a89..8ad010bc25 100644 --- a/src/bonsai/bonsai/bim/module/project/data.py +++ b/src/bonsai/bonsai/bim/module/project/data.py @@ -162,7 +162,7 @@ class ProjectLibraryData: library_file = IfcStore.library_file if library_file is None or library_file.schema == "IFC2X3": return results - root = tool.Project.get_root_context(library_file) + root = library_file.by_type("IfcProject")[0] results.append((str(root.id()), f"{root.is_a()} {root.Name or 'Unnamed'}", root.Description or "")) for library_id, data in cls.data["project_libraries"].items(): results.append((str(library_id), data["Name"] or "Unnamed", data["Description"] or "")) diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index b702762e6b..995c96725d 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -281,7 +281,7 @@ class RefreshLibrary(bpy.types.Operator): elements = {e for e in elements if not tool.Project.is_element_assigned_to_project_library(e, rels)} self.props.add_library_project_library("Unassigned", len(elements), 0, False) - root_context = tool.Project.get_root_context(library_file) + root_context = library_file.by_type("IfcProject")[0] hierarchy = tool.Project.get_project_hierarchy(library_file) tool.Project.load_project_libraries_to_ui(root_context, hierarchy) return {"FINISHED"} @@ -807,7 +807,7 @@ class AddProjectLibrary(bpy.types.Operator): props = tool.Project.get_project_props() library_file = IfcStore.library_file assert library_file - root_context = tool.Project.get_root_context(library_file) + root_context = library_file.by_type("IfcProject")[0] project_library = ifcopenshell.api.root.create_entity(library_file, "IfcProjectLibrary") ifcopenshell.api.project.assign_declaration(library_file, [project_library], root_context) ProjectLibraryData.load() # Update enum. diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index 2224effc4b..8d8b843729 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -388,30 +388,14 @@ class Project(bonsai.core.tool.Project): def get_parent_library( cls, project_library: ifcopenshell.entity_instance ) -> Union[ifcopenshell.entity_instance, None]: - """Return the IfcContext that declares or nests ``project_library``. - - Every IfcProjectLibrary in a supported (spec-valid) file is either nested - under another library or declared to the file's IfcProject. Returns ``None`` - only as a defensive fallback for malformed data with neither relationship. - """ + """Return the IfcContext that declares or nests ``project_library``, or ``None`` + if neither relationship is present.""" if nests := project_library.Nests: return nests[0].RelatingObject if has_context := project_library.HasContext: return has_context[0].RelatingContext return None - @classmethod - def get_root_context(cls, ifc_file: ifcopenshell.file) -> ifcopenshell.entity_instance: - """Return the file's IfcProject. - - Per the IFC Project Context concept template, every project data set (this - includes library files) shall contain exactly one IfcProject; there is no - such thing as a spec-valid file rooted on IfcProjectLibrary alone. A file - without an IfcProject is not supported and this raises IndexError. Caller is - responsible for the IFC2X3 guard; IfcContext does not exist in that schema. - """ - return ifc_file.by_type("IfcProject")[0] - @classmethod def get_project_hierarchy(cls, ifc_file: ifcopenshell.file) -> HiearchyDict: """Get project hierarchy in the following form: diff --git a/src/bonsai/test/bim/module/project/test_project_library_data.py b/src/bonsai/test/bim/module/project/test_project_library_data.py index a6bf12f86b..699e8a31b3 100644 --- a/src/bonsai/test/bim/module/project/test_project_library_data.py +++ b/src/bonsai/test/bim/module/project/test_project_library_data.py @@ -55,23 +55,18 @@ class TestLibraryFile(NewIfc): """Project-library UI code operating on a spec-valid model (IfcProject root). A file containing only IfcProjectLibrary and no IfcProject is not valid IFC and - is not supported; see test_get_root_context_raises_for_a_file_without_a_project. + is not supported; see test_parent_libraries_enum_raises_for_a_file_without_a_project. """ - def test_get_root_context_returns_the_project(self): - library_file = _make_library_file() - project = library_file.by_type("IfcProject")[0] - - root = tool.Project.get_root_context(library_file) - - assert root == project - - def test_get_root_context_raises_for_a_file_without_a_project(self): + def test_parent_libraries_enum_raises_for_a_file_without_a_project(self): library_file = ifcopenshell.api.project.create_file(version="IFC4") ifcopenshell.api.root.create_entity(library_file, ifc_class="IfcProjectLibrary", name="RootLib") - - with pytest.raises(IndexError): - tool.Project.get_root_context(library_file) + IfcStore.library_file = library_file + try: + with pytest.raises(IndexError): + ProjectLibraryData.parent_libraries_enum() + finally: + IfcStore.library_file = None def test_get_parent_library_returns_project_for_declared_root_library(self): library_file = _make_library_file() @@ -80,6 +75,12 @@ class TestLibraryFile(NewIfc): assert tool.Project.get_parent_library(root) == project + def test_get_parent_library_returns_none_for_an_orphaned_library(self): + library_file = ifcopenshell.api.project.create_file(version="IFC4") + orphan = ifcopenshell.api.root.create_entity(library_file, ifc_class="IfcProjectLibrary", name="Orphan") + + assert tool.Project.get_parent_library(orphan) is None + def test_get_project_hierarchy_roots_libraries_under_the_project(self): library_file = _make_library_file(with_child=True) project = library_file.by_type("IfcProject")[0] @@ -92,12 +93,6 @@ class TestLibraryFile(NewIfc): assert child in hierarchy[root] def test_project_library_data_loads_with_unique_enum_keys(self): - # Regression test for the ci-bonsai-daily failure: parent_libraries_enum() - # must never emit two entries with the same STEP id (Blender's EnumProperty - # requires unique keys). The original failure only occurred because the root - # context could incorrectly resolve to an IfcProjectLibrary that was also - # collected by project_libraries(); on a spec-valid model root is always the - # IfcProject, whose id never collides with a library id. IfcStore.library_file = _make_library_file(with_child=True) try: ProjectLibraryData.is_loaded = False