From 4d237061450f64d1d74b7a8bc3a6ab6a28cfa087 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 11 Mar 2024 12:06:56 +0500 Subject: [PATCH] more typing --- src/blenderbim/blenderbim/bim/ifc.py | 56 ++++---- .../ifcopenshell/entity_instance.py | 33 ++++- .../ifcopenshell/util/element.py | 121 +++++++++++++----- 3 files changed, 148 insertions(+), 62 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index f58cd080bd..ec68020847 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -23,33 +23,39 @@ import hashlib import zipfile import tempfile import ifcopenshell +import ifcopenshell.geom +import ifcopenshell.ifcopenshell_wrapper import blenderbim.bim.handler import blenderbim.tool as tool from pathlib import Path from blenderbim.tool.brick import BrickStore +from typing import Set, Union + + +IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] class IfcStore: path = "" - file = None - schema = None - cache = None - cache_path = None - id_map = {} - guid_map = {} - edited_objs = set() + file: ifcopenshell.file = None + schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None + cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None + cache_path: str = None + id_map: dict[int, IFC_CONNECTED_TYPE] = {} + guid_map: dict[str, IFC_CONNECTED_TYPE] = {} + edited_objs: Set[bpy.types.Object] = set() pset_template_path = "" - pset_template_file = None + pset_template_file: ifcopenshell.file = None classification_path = "" - classification_file = None + classification_file: ifcopenshell.file = None library_path = "" - library_file = None + library_file: ifcopenshell.file = None current_transaction = "" last_transaction = "" history = [] future = [] schema_identifiers = ["IFC4", "IFC2X3", "IFC4X3"] - session_files = {} + session_files: dict[str, ifcopenshell.file] = {} @staticmethod def purge(): @@ -117,7 +123,7 @@ class IfcStore: IfcStore.get_cache() @staticmethod - def load_file(path): + def load_file(path) -> None: if not os.path.isfile(path): return extension = path.split(".")[-1] @@ -146,7 +152,7 @@ class IfcStore: return IfcStore.schema @staticmethod - def get_element(id_or_guid): + def get_element(id_or_guid: Union[int, str]) -> IFC_CONNECTED_TYPE: if isinstance(id_or_guid, int): map_object = IfcStore.id_map else: @@ -159,7 +165,7 @@ class IfcStore: return obj @staticmethod - def relink_all_objects(): + def relink_all_objects() -> None: if not IfcStore.get_file(): return for obj in bpy.data.objects: @@ -172,7 +178,7 @@ class IfcStore: IfcStore.relink_object(obj) @staticmethod - def relink_object(obj): + def relink_object(obj: IFC_CONNECTED_TYPE) -> None: if not obj: return if obj.BIMObjectProperties.ifc_definition_id: @@ -193,7 +199,7 @@ class IfcStore: IfcStore.commit_link_element(data) @staticmethod - def link_element(element, obj): + def link_element(element: ifcopenshell.entity_instance, obj: IFC_CONNECTED_TYPE) -> None: # Please use tool.Ifc.link() instead of this method. We want to # refactor this class and deprecate usage of IfcStore in favour of # tools. @@ -259,7 +265,7 @@ class IfcStore: # TODO We're handling id_map and guid_map, but what about edited_objs? This might cause big problems. @staticmethod - def rollback_unlink_element(data): + def rollback_unlink_element(data) -> None: if "id" not in data or "obj" not in data: return obj = bpy.data.objects.get(data["obj"]) @@ -268,13 +274,13 @@ class IfcStore: IfcStore.guid_map[data["guid"]] = obj @staticmethod - def commit_unlink_element(data): + def commit_unlink_element(data) -> None: del IfcStore.id_map[data["id"]] if data["guid"]: del IfcStore.guid_map[data["guid"]] @staticmethod - def unlink_element(element=None, obj=None): + def unlink_element(element: ifcopenshell.entity_instance = None, obj: IFC_CONNECTED_TYPE = None) -> None: if element is None: try: element = tool.Ifc.get_entity(obj) @@ -320,7 +326,7 @@ class IfcStore: ) @staticmethod - def execute_ifc_operator(operator, context, is_invoke=False): + def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False): bpy.context.scene.BIMProperties.is_dirty = True is_top_level_operator = not bool(IfcStore.current_transaction) @@ -354,17 +360,17 @@ class IfcStore: return result @staticmethod - def begin_transaction(operator): + def begin_transaction(operator: bpy.types.Operator) -> None: IfcStore.current_transaction = str(uuid.uuid4()) operator.transaction_key = IfcStore.current_transaction @staticmethod - def end_transaction(operator): + def end_transaction(operator: bpy.types.Operator) -> None: IfcStore.current_transaction = "" operator.transaction_key = "" @staticmethod - def add_transaction_operation(operator, rollback=None, commit=None): + def add_transaction_operation(operator: bpy.types.Operator, rollback=None, commit=None) -> None: key = getattr(operator, "transaction_key", None) data = getattr(operator, "transaction_data", None) bpy.context.scene.BIMProperties.last_transaction = key @@ -380,7 +386,7 @@ class IfcStore: IfcStore.future = [] @staticmethod - def undo(until_key=None): + def undo(until_key=None) -> None: BrickStore.undo() if not IfcStore.history: return @@ -395,7 +401,7 @@ class IfcStore: IfcStore.future.append(event) @staticmethod - def redo(until_key=None): + def redo(until_key=None) -> None: BrickStore.redo() if not IfcStore.future: diff --git a/src/ifcopenshell-python/ifcopenshell/entity_instance.py b/src/ifcopenshell-python/ifcopenshell/entity_instance.py index 7d364df900..ed38efeb4b 100644 --- a/src/ifcopenshell-python/ifcopenshell/entity_instance.py +++ b/src/ifcopenshell-python/ifcopenshell/entity_instance.py @@ -30,7 +30,7 @@ import functools import subprocess import sys import time -from typing import Union +from typing import Union, Any, Callable from . import ifcopenshell_wrapper from . import settings @@ -196,7 +196,36 @@ class entity_instance(object): ) @staticmethod - def walk(f, g, value): + def walk(f: Callable[[Any], bool], g: Callable[[Any], Any], value: Any) -> Any: + """ + Applies transformation to `value` based on a given condition. + If value is a nested structure (e.g., a list or a tuple) will apply transformation to it's elements. + . + + :param f: A callable that takes a single argument and returns a boolean value. It represents the condition + :type f: Callable + :param g: A callable that takes a single argument and returns a transformed value. It represents the transformation + :type g: Callable + :param value: Any object, the input value to be processed + :type value: Any + :return: Transformed value + :rtype: Any + + Example: + + .. code:: python + + # Define condition and transformation functions + condition = lambda v: v == old + transform = lambda v: new + + # Usage example + attribute_value = element.RelatedElements + print(old in attribute_value, new in attribute_value) # True, False + result = element.walk(condition, transform, element.RelatedElements) + print(old in attribute_value, new in attribute_value) # False, True + """ + if isinstance(value, (tuple, list)): return tuple(map(functools.partial(entity_instance.walk, f, g), value)) elif f(value): diff --git a/src/ifcopenshell-python/ifcopenshell/util/element.py b/src/ifcopenshell-python/ifcopenshell/util/element.py index 6ad072d48d..64ff5dd5a6 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/element.py +++ b/src/ifcopenshell-python/ifcopenshell/util/element.py @@ -18,10 +18,18 @@ from __future__ import annotations import ifcopenshell -from typing import List +from typing import List, Any, Callable -def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should_inherit=True, verbose=False): +def get_pset( + element: ifcopenshell.entity_instance, + name: str, + prop: str = None, + psets_only=False, + qtos_only=False, + should_inherit=True, + verbose=False, +) -> dict: """Retrieve a single property set or single property This is more efficient than ifcopenshell.util.element.get_psets if you know @@ -107,7 +115,9 @@ def get_pset(element, name, prop=None, psets_only=False, qtos_only=False, should return value -def get_psets(element, psets_only=False, qtos_only=False, should_inherit=True, verbose=False): +def get_psets( + element: ifcopenshell.entity_instance, psets_only=False, qtos_only=False, should_inherit=True, verbose=False +) -> dict: """Retrieve property sets, their related properties' names & values and ids. If should_inherit is true, the pset "id" only refers to the ID of the @@ -301,7 +311,7 @@ def get_properties(properties, verbose=False): return results -def get_predefined_type(element): +def get_predefined_type(element: ifcopenshell.entity_instance) -> str: """Retrieves the PrefefinedType attribute of an element. If the predefined type is user defined, the custom type (such as object @@ -334,7 +344,7 @@ def get_predefined_type(element): return predefined_type -def get_type(element): +def get_type(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """Retrieves the construction type element of an element occurrence :param element: The element occurrence @@ -359,7 +369,7 @@ def get_type(element): return relationship.RelatingType -def get_types(type): +def get_types(type: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: """Get all the occurrences of a type element :param type: The type element @@ -408,7 +418,9 @@ def get_shape_aspects(element: ifcopenshell.entity_instance) -> List[ifcopenshel return shape_aspects -def get_material(element, should_skip_usage=False, should_inherit=True): +def get_material( + element: ifcopenshell.entity_instance, should_skip_usage=False, should_inherit=True +) -> ifcopenshell.entity_instance: """Gets the material of the element The material may be a single material, material set (layered, profiled, or @@ -449,7 +461,7 @@ def get_material(element, should_skip_usage=False, should_inherit=True): return get_material(relating_type, should_skip_usage) -def get_materials(element, should_inherit=True): +def get_materials(element: ifcopenshell.entity_instance, should_inherit=True) -> List[ifcopenshell.entity_instance]: """Gets individual materials of an element If the element has a material set, the individual materials of that set are @@ -484,7 +496,7 @@ def get_materials(element, should_inherit=True): return list(material.Materials) -def get_styles(element): +def get_styles(element: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: """Retrieves the styles used in an element's representation. Styles may be retreived from the material or the body representation. @@ -528,7 +540,9 @@ def get_styles(element): return styles -def get_elements_by_material(ifc_file, material): +def get_elements_by_material( + ifc_file: ifcopenshell.file, material: ifcopenshell.entity_instance +) -> List[ifcopenshell.entity_instance]: """Retrieves the elements related to a material. This includes elements using the material as part of a material set or set @@ -570,7 +584,9 @@ def get_elements_by_material(ifc_file, material): return results -def get_elements_by_style(ifc_file, style): +def get_elements_by_style( + ifc_file: ifcopenshell.file, style: ifcopenshell.entity_instance +) -> List[ifcopenshell.entity_instance]: """Retrieves the elements whose geometric representation uses a style :param ifc_file: The IFC file @@ -610,7 +626,9 @@ def get_elements_by_style(ifc_file, style): return results -def get_elements_by_representation(ifc_file, representation): +def get_elements_by_representation( + ifc_file: ifcopenshell.file, representation: ifcopenshell.entity_instance +) -> List[ifcopenshell.entity_instance]: """Gets all elements using a geometric representation :param ifc_file: The IFC file @@ -642,7 +660,9 @@ def get_elements_by_representation(ifc_file, representation): return results -def get_elements_by_layer(ifc_file, layer): +def get_elements_by_layer( + ifc_file: ifcopenshell.file, layer: ifcopenshell.entity_instance +) -> List[ifcopenshell.entity_instance]: """Get all the elements that are used by a presentation layer :param ifc_file: The IFC file @@ -663,7 +683,9 @@ def get_elements_by_layer(ifc_file, layer): return results -def get_layers(ifc_file, element): +def get_layers( + ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance +) -> List[ifcopenshell.entity_instance]: """Get the CAD layers that an element is part of An element may have portions or all of its geometry assigned to a @@ -701,7 +723,9 @@ def get_layers(ifc_file, element): return layers -def get_container(element, should_get_direct=False, ifc_class=None): +def get_container( + element: ifcopenshell.entity_instance, should_get_direct=False, ifc_class: str = None +) -> ifcopenshell.entity_instance: """ Retrieves the spatial structure container of an element. @@ -717,6 +741,7 @@ def get_container(element, should_get_direct=False, ifc_class=None): example, you may be after the storey, not a space. :type ifc_class: str :return: The direct or indirect container of the element or None. + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -749,7 +774,7 @@ def get_container(element, should_get_direct=False, ifc_class=None): container = get_aggregate(container) -def get_referenced_structures(element): +def get_referenced_structures(element: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: """Retreives a list of referenced spatial elements Typically useful for multistorey elements, such as columns or facade @@ -758,6 +783,8 @@ def get_referenced_structures(element): :param element: The IFC element :type element: ifcopenshell.entity_instance.entity_instance + :return: A list of IfcSpatialElement + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -771,7 +798,7 @@ def get_referenced_structures(element): return [] -def get_decomposition(element, is_recursive=True): +def get_decomposition(element: ifcopenshell.entity_instance, is_recursive=True) -> List[ifcopenshell.entity_instance]: """ Retrieves all subelements of an element based on the spatial decomposition hierarchy. This includes all subspaces and elements contained in subspaces, @@ -813,11 +840,13 @@ def get_decomposition(element, is_recursive=True): return results -def get_grouped_by(element): +def get_grouped_by(element: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: """Retrieves all subelements of an element based on the group. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: All subelements of the group + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -836,12 +865,14 @@ def get_grouped_by(element): return results -def get_aggregate(element): +def get_aggregate(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Retrieves the aggregate parent of an element. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The aggregate of the element + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -855,12 +886,14 @@ def get_aggregate(element): return element.Decomposes[0].RelatingObject -def get_nest(element): +def get_nest(element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Retrieves the nest parent of an element. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The nested whole of the element + :rtype: ifcopenshell.entity_instance.entity_instance Example: @@ -877,12 +910,14 @@ def get_nest(element): return element.Decomposes[0].RelatingObject -def get_parts(element): +def get_parts(element: ifcopenshell.entity_instance) -> List[ifcopenshell.entity_instance]: """ Retrieves the parts of an element that have an aggregation relationship. :param element: The IFC element + :type element: ifcopenshell.entity_instance.entity_instance :return: The parts of the element + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -896,16 +931,17 @@ def get_parts(element): return element.IsDecomposedBy[0].RelatedObjects -def get_components(element, include_ports=False): +def get_components(element: ifcopenshell.entity_instance, include_ports=False) -> List[ifcopenshell.entity_instance]: """ Retrieves the components of an element that have an nest relationship. For nested ports, see ifcopenshell.util.system. :param element: The IFC element - :return: The components of the element :param include_ports: Default as False. Set to true if you also want to get ports. :type include_ports: bool,optional + :return: The components of the element + :rtype: list[ifcopenshell.entity_instance.entity_instance] Example: @@ -924,13 +960,13 @@ def get_components(element, include_ports=False): return element.IsDecomposedBy[0].RelatedObjects -def replace_attribute(element, old, new): - for i, attribute in enumerate(element): - if has_element_reference(attribute, old): - element[i] = element.walk(lambda v: v == old, lambda v: new, attribute) +def replace_attribute(element: ifcopenshell.entity_instance, old: Any, new: Any) -> None: + for i, attribute_value in enumerate(element): + if has_element_reference(attribute_value, old): + element[i] = element.walk(lambda v: v == old, lambda v: new, attribute_value) -def has_element_reference(value, element): +def has_element_reference(value: Any, element: ifcopenshell.entity_instance) -> bool: if isinstance(value, (tuple, list)): for v in value: if has_element_reference(v, element): @@ -939,7 +975,7 @@ def has_element_reference(value, element): return value == element -def remove_deep(ifc_file, element): +def remove_deep(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> None: """Recursively purges a subgraph safely. Do not use, use remove_deep2() instead. @@ -954,7 +990,7 @@ def remove_deep(ifc_file, element): ifc_file.unbatch() -def batch_remove_deep2(ifc_file): +def batch_remove_deep2(ifc_file: ifcopenshell.file) -> None: """Enable batch removal after running remove_deep2 using serialisation See #944 and #3226. Removing elements in an IFC graph is slow as a lot of @@ -990,7 +1026,7 @@ def batch_remove_deep2(ifc_file): ifc_file.to_delete = set() -def unbatch_remove_deep2(ifc_file): +def unbatch_remove_deep2(ifc_file: ifcopenshell.file) -> ifcopenshell.file: """Finish removing elements batched from remove_deep2 using string replacement See documentation for batch_remove_deep2. @@ -1020,7 +1056,12 @@ def unbatch_remove_deep2(ifc_file): return ifcopenshell.file.from_string("\n".join(result)) -def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): +def remove_deep2( + ifc_file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + also_consider: List[ifcopenshell.entity_instance] = [], + do_not_delete: List[ifcopenshell.entity_instance] = [], +) -> None: """Recursively purges a subgraph safely, starting at an element This should always be used instead of remove_deep. See #1812. The start @@ -1047,6 +1088,10 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): :param ifc_file: The IFC file object :type ifc_file: ifcopenshell.file.file + :param also_consider: elements to also consider as a part of a subgraph + :type also_consider: list[ifcopenshell.entity_instance.entity_instance], optional + :param do_not_delete: elements to protect from deletion + :type do_not_delete: list[ifcopenshell.entity_instance.entity_instance], optional :param element: The starting element that defines the subgraph :type element: ifcopenshell.entity_instance.entity_instance """ @@ -1088,7 +1133,7 @@ def remove_deep2(ifc_file, element, also_consider=[], do_not_delete=[]): # ifc_file.unbatch() -def copy(ifc_file, element): +def copy(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance: """ Copy a single element. Any referenced elements are not copied. @@ -1112,7 +1157,13 @@ def copy(ifc_file, element): return new -def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_entities=None): +def copy_deep( + ifc_file: ifcopenshell.file, + element: ifcopenshell.entity_instance, + exclude: List[str] = None, + exclude_callback: Callable[[ifcopenshell.entity_instance], bool] = None, + copied_entities: dict[int, ifcopenshell.entity_instance] = None, +) -> ifcopenshell.entity_instance: """ Recursively copy an element and all of its directly related subelements. @@ -1132,7 +1183,7 @@ def copy_deep(ifc_file, element, exclude=None, exclude_callback=None, copied_ent :param copied_entities: A dictionary of IDs as keys and entities as values to reuse when coming across the same entity twice. This can typically be left as None. - :type copied_entities: dict[int:ifcopenshell.entity_instance.entity_instance] + :type copied_entities: dict[int:ifcopenshell.entity_instance.entity_instance], optional :return: The newly copied element :rtype: ifcopenshell.entity_instance.entity_instance """