mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 06:32:09 +00:00
typing
This commit is contained in:
@@ -23,6 +23,7 @@ import numpy as np
|
|||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
import ifcopenshell.api.layer
|
import ifcopenshell.api.layer
|
||||||
|
import ifcopenshell.api.root
|
||||||
import ifcopenshell.api.style
|
import ifcopenshell.api.style
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import ifcopenshell.util.placement
|
import ifcopenshell.util.placement
|
||||||
@@ -48,6 +49,9 @@ from ifcopenshell.util.shape_builder import ShapeBuilder
|
|||||||
from typing import Any, Union, Literal, get_args, TYPE_CHECKING, assert_never
|
from typing import Any, Union, Literal, get_args, TYPE_CHECKING, assert_never
|
||||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bpy._typing import rna_enums
|
||||||
|
|
||||||
|
|
||||||
class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
|
class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
|
||||||
bl_idname = "bim.edit_object_placement"
|
bl_idname = "bim.edit_object_placement"
|
||||||
@@ -1006,10 +1010,12 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
|
return OverrideDuplicateMove.execute_ifc_duplicate_operator(self, context)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def execute_duplicate_operator(self, context, linked=False):
|
def execute_duplicate_operator(
|
||||||
|
operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False
|
||||||
|
) -> set["rna_enums.OperatorReturnItems"]:
|
||||||
# Deep magick from the dawn of time
|
# Deep magick from the dawn of time
|
||||||
if tool.Ifc.get():
|
if tool.Ifc.get():
|
||||||
IfcStore.execute_ifc_operator(self, context)
|
IfcStore.execute_ifc_operator(operator, context)
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
if linked:
|
if linked:
|
||||||
@@ -1019,15 +1025,15 @@ class OverrideDuplicateMove(bpy.types.Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def execute_ifc_duplicate_operator(self, context, linked=False):
|
def execute_ifc_duplicate_operator(operator: bpy.types.Operator, context: bpy.types.Context, linked: bool = False):
|
||||||
for obj in context.selected_objects:
|
for obj in context.selected_objects:
|
||||||
if element := tool.Ifc.get_entity(obj):
|
if element := tool.Ifc.get_entity(obj):
|
||||||
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
if element.is_a("IfcAnnotation") and element.ObjectType == "DRAWING":
|
||||||
tool.Blender.deselect_object(obj)
|
tool.Blender.deselect_object(obj)
|
||||||
self.report({"ERROR"}, "Drawing not duplicated.")
|
operator.report({"ERROR"}, "Drawing not duplicated.")
|
||||||
elif tool.Geometry.is_locked(element):
|
elif tool.Geometry.is_locked(element):
|
||||||
tool.Blender.deselect_object(obj)
|
tool.Blender.deselect_object(obj)
|
||||||
self.report({"ERROR"}, lock_error_message(obj.name))
|
operator.report({"ERROR"}, lock_error_message(obj.name))
|
||||||
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
|
old_to_new, new_active_obj = tool.Geometry.duplicate_ifc_objects(
|
||||||
set(context.selected_objects), linked=linked, active_object=context.active_object
|
set(context.selected_objects), linked=linked, active_object=context.active_object
|
||||||
)
|
)
|
||||||
@@ -1557,7 +1563,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
continue
|
continue
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
if element:
|
if element:
|
||||||
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
|
ifcopenshell.api.root.remove_product(ifc_file, product=element)
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
|
bpy.ops.bim.update_representation(obj=self.target.name, ifc_representation_class="")
|
||||||
else:
|
else:
|
||||||
@@ -1676,7 +1682,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
assert False, f"Unexpected item type: {item.is_a()}. This is a bug."
|
assert False, f"Unexpected item type: {item.is_a()}. This is a bug."
|
||||||
|
|
||||||
items.append(copied_item)
|
items.append(copied_item)
|
||||||
ifcopenshell.api.run("root.remove_product", ifc_file, product=element)
|
ifcopenshell.api.root.remove_product(ifc_file, product=element)
|
||||||
representation.Items = items
|
representation.Items = items
|
||||||
bpy.ops.object.join()
|
bpy.ops.object.join()
|
||||||
core.switch_representation(
|
core.switch_representation(
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ def duplicate_drawing(
|
|||||||
ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
|
ifc.run("group.edit_group", group=new_group, attributes={"Name": drawing_name, "ObjectType": "DRAWING"})
|
||||||
ifc.run("group.assign_group", group=new_group, products=[new_drawing])
|
ifc.run("group.assign_group", group=new_group, products=[new_drawing])
|
||||||
if should_duplicate_annotations:
|
if should_duplicate_annotations:
|
||||||
new_annotations = []
|
new_annotations: list[ifcopenshell.entity_instance] = []
|
||||||
annotation_objs = [ifc.get_object(a) for a in drawing_tool.get_group_elements(group) if a != drawing]
|
annotation_objs = [ifc.get_object(a) for a in drawing_tool.get_group_elements(group) if a != drawing]
|
||||||
old_to_new, _ = geometry.duplicate_ifc_objects(annotation_objs)
|
old_to_new, _ = geometry.duplicate_ifc_objects(annotation_objs)
|
||||||
for new_elements in old_to_new.values():
|
for new_elements in old_to_new.values():
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import ifcopenshell.util.unit
|
|||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
import bonsai.core.drawing
|
import bonsai.core.drawing
|
||||||
import bonsai.core.geometry
|
import bonsai.core.geometry
|
||||||
|
import bonsai.core.root
|
||||||
import bonsai.core.spatial
|
import bonsai.core.spatial
|
||||||
import bonsai.core.style
|
import bonsai.core.style
|
||||||
import bonsai.core.system
|
import bonsai.core.system
|
||||||
@@ -2013,8 +2014,11 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def duplicate_ifc_objects(
|
def duplicate_ifc_objects(
|
||||||
cls, objects_to_duplicate: Iterable[bpy.types.Object], active_object=None, linked=False
|
cls,
|
||||||
) -> dict:
|
objects_to_duplicate: Iterable[bpy.types.Object],
|
||||||
|
active_object: Optional[bpy.types.Object] = None,
|
||||||
|
linked: bool = False,
|
||||||
|
) -> tuple[dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]], Union[bpy.types.Object, None]]:
|
||||||
# Handle arrays
|
# Handle arrays
|
||||||
objects_to_duplicate = set(objects_to_duplicate)
|
objects_to_duplicate = set(objects_to_duplicate)
|
||||||
arrays_to_duplicate, array_children = cls.process_arrays_for_duplication(objects_to_duplicate)
|
arrays_to_duplicate, array_children = cls.process_arrays_for_duplication(objects_to_duplicate)
|
||||||
@@ -2026,7 +2030,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
# Track decompositions so they can be recreated after the operation
|
# Track decompositions so they can be recreated after the operation
|
||||||
decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
decomposition_relationships = tool.Root.get_decomposition_relationships(objects_to_duplicate)
|
||||||
connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate)
|
connection_relationships = tool.Root.get_connection_relationships(objects_to_duplicate)
|
||||||
old_to_new = {}
|
old_to_new: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
|
||||||
|
|
||||||
for obj in objects_to_duplicate:
|
for obj in objects_to_duplicate:
|
||||||
element = tool.Ifc.get_entity(obj)
|
element = tool.Ifc.get_entity(obj)
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ class Ifc(bonsai.core.tool.Ifc):
|
|||||||
return IfcStore.get_schema()
|
return IfcStore.get_schema()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_entity(cls, obj: IFC_CONNECTED_TYPE) -> Union[ifcopenshell.entity_instance, None]:
|
def get_entity(
|
||||||
|
cls, obj: Union[IFC_CONNECTED_TYPE, tool.Geometry.TYPES_WITH_MESH_PROPERTIES]
|
||||||
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
"""Get linked IFC entity based on obj's ifc_definition_id.
|
"""Get linked IFC entity based on obj's ifc_definition_id.
|
||||||
|
|
||||||
Return None if object is not linked to IFC or it's linked to non-existent element.
|
Return None if object is not linked to IFC or it's linked to non-existent element.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import sys
|
|||||||
import json
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
import bonsai.core.tool
|
import bonsai.core.tool
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional, Type, Union, TypedDict, Literal
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
|
||||||
@@ -241,20 +241,43 @@ def voider():
|
|||||||
prophet.verify()
|
prophet.verify()
|
||||||
|
|
||||||
|
|
||||||
|
Call = TypedDict("Call", {"name": str, "args": tuple[Any, ...], "kwargs": dict[str, Any]})
|
||||||
|
Prediction = TypedDict("Prediction", {"type": Literal["SHOULD_BE_CALLED"], "number": Optional[int], "call": Call})
|
||||||
|
|
||||||
|
|
||||||
class Prophecy:
|
class Prophecy:
|
||||||
def __init__(self, cls):
|
"""
|
||||||
|
Rough outline how it works:
|
||||||
|
1. Test run pass:
|
||||||
|
- Remember calls (all calls should also have ``.should_be_called()`` after).
|
||||||
|
- Remember predictions.
|
||||||
|
- Associate return values with calls.
|
||||||
|
|
||||||
|
2. Core function pass:
|
||||||
|
- Remember calls.
|
||||||
|
- Use return values from the first pass.
|
||||||
|
|
||||||
|
3. Verification pass:
|
||||||
|
- Ensure all predicted calls actually happened.
|
||||||
|
"""
|
||||||
|
|
||||||
|
subject: Type
|
||||||
|
|
||||||
|
def __init__(self, cls: Type):
|
||||||
self.subject = cls
|
self.subject = cls
|
||||||
self.predictions: list[dict] = []
|
self.predictions: list[Prediction] = []
|
||||||
self.calls: list[dict] = []
|
self.calls: list[Call] = []
|
||||||
self.return_values: dict[str, Any] = {}
|
self.return_values: dict[str, Any] = {}
|
||||||
self.should_call: Optional[dict] = None
|
self.should_call: Optional[Call] = None
|
||||||
|
|
||||||
def __getattr__(self, attr: str):
|
def __getattr__(self, attr: str):
|
||||||
if not hasattr(self.subject, attr):
|
if not hasattr(self.subject, attr):
|
||||||
raise AttributeError(f"Prophecy {self.subject} has no attribute {attr}")
|
raise AttributeError(f"Prophecy {self.subject} has no attribute {attr}")
|
||||||
|
|
||||||
def decorate(*args, **kwargs):
|
# It also returns `Any` but it only happens during `subject.xxx` call.
|
||||||
call = {"name": attr, "args": args, "kwargs": kwargs}
|
def decorate(*args: Any, **kwargs: Any) -> Self:
|
||||||
|
"""Remember a call."""
|
||||||
|
call: Call = {"name": attr, "args": args, "kwargs": kwargs}
|
||||||
# Ensure that signature is valid
|
# Ensure that signature is valid
|
||||||
getattr(self.subject, attr)(*args, **kwargs)
|
getattr(self.subject, attr)(*args, **kwargs)
|
||||||
key = json.dumps(call, sort_keys=True)
|
key = json.dumps(call, sort_keys=True)
|
||||||
@@ -265,18 +288,20 @@ class Prophecy:
|
|||||||
|
|
||||||
return decorate
|
return decorate
|
||||||
|
|
||||||
def should_be_called(self, number=None):
|
def should_be_called(self, number: Optional[int] = None) -> Self:
|
||||||
|
"""Predict the last added call."""
|
||||||
self.should_call = self.calls.pop()
|
self.should_call = self.calls.pop()
|
||||||
self.predictions.append({"type": "SHOULD_BE_CALLED", "number": number, "call": self.should_call})
|
self.predictions.append({"type": "SHOULD_BE_CALLED", "number": number, "call": self.should_call})
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def will_return(self, value: Any) -> Self:
|
def will_return(self, value: Any) -> Self:
|
||||||
|
"""Remember a return value for the last predicted call."""
|
||||||
key = json.dumps(self.should_call, sort_keys=True)
|
key = json.dumps(self.should_call, sort_keys=True)
|
||||||
self.return_values[key] = value
|
self.return_values[key] = value
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def verify(self) -> None:
|
def verify(self) -> None:
|
||||||
predicted_calls = []
|
predicted_calls: list[Call] = []
|
||||||
for prediction in self.predictions:
|
for prediction in self.predictions:
|
||||||
predicted_calls.append(prediction["call"])
|
predicted_calls.append(prediction["call"])
|
||||||
if prediction["type"] == "SHOULD_BE_CALLED":
|
if prediction["type"] == "SHOULD_BE_CALLED":
|
||||||
@@ -285,7 +310,7 @@ class Prophecy:
|
|||||||
if call not in predicted_calls:
|
if call not in predicted_calls:
|
||||||
raise Exception(f"Unpredicted call: {call}")
|
raise Exception(f"Unpredicted call: {call}")
|
||||||
|
|
||||||
def verify_should_be_called(self, prediction: dict) -> None:
|
def verify_should_be_called(self, prediction: Prediction) -> None:
|
||||||
if prediction["number"]:
|
if prediction["number"]:
|
||||||
count = self.calls.count(prediction["call"])
|
count = self.calls.count(prediction["call"])
|
||||||
if count != prediction["number"]:
|
if count != prediction["number"]:
|
||||||
|
|||||||
Reference in New Issue
Block a user