# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2021 Thomas Krijnen # # This file is part of IfcOpenShell. # # IfcOpenShell is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . from __future__ import absolute_import from __future__ import division from __future__ import print_function import functools import numbers import itertools from . import ifcopenshell_wrapper try: import logging except ImportError as e: logging = type("logger", (object,), {"exception": staticmethod(lambda s: print(s))}) def set_derived_atribute(*args): raise TypeError("Unable to set derived attribute") # For every schema and its entities populate a list # of functions for every entity attribute (including # inherited attributes) to set that particular # attribute by index. # For example. IFC2X3.IfcWall with have a list of # 9 methods. The first will point at # ifcopenshell.ifcopenshell_wrapper.entity_instance.setArgumentAsString # because the first attribute GlobalId ultimately # is of type string. # Previously, resolving the appropriate function was # done for each invocation of __setitem__. Now this # mapping is built once during initialization of the # module. _method_dict = {} def register_schema_attributes(schema): for decl in schema.declarations(): if hasattr(decl, "argument_types"): fq_name = ".".join((schema.name(), decl.name())) # get type strings as reported by IfcOpenShell C++ type_strs = decl.argument_types() # convert case for setter function type_strs = [x.title().replace(" ", "") for x in type_strs] # binary and enumeration are passed from python as string as well type_strs = [x.replace("Binary", "String") for x in type_strs] type_strs = [x.replace("Enumeration", "String") for x in type_strs] # prefix to get method names fn_names = ["setArgumentAs" + x for x in type_strs] # resolve to actual functions in wrapper functions = [ set_derived_atribute if mname == "setArgumentAsDerived" else getattr(ifcopenshell_wrapper.entity_instance, mname) for mname in fn_names ] _method_dict[fq_name] = functions for nm in ifcopenshell_wrapper.schema_names(): schema = ifcopenshell_wrapper.schema_by_name(nm) register_schema_attributes(schema) class entity_instance(object): """This is the base Python class for all IFC objects. An instantiated entity_instance will have methods of Python and the IFC class itself. Example:: ifc_file = ifcopenshell.open(file_path) products = ifc_file.by_type("IfcProduct") print(products[0].__class__) >>> print(products[0].Representation) >>> #423=IfcProductDefinitionShape($,$,(#409,#421)) """ def __init__(self, e, file=None): if isinstance(e, tuple): e = ifcopenshell_wrapper.new_IfcBaseClass(*e) super(entity_instance, self).__setattr__("wrapped_data", e) super(entity_instance, self).__setattr__("method_list", None) self.wrapped_data.file = file def __getattr__(self, name): INVALID, FORWARD, INVERSE = range(3) attr_cat = self.wrapped_data.get_attribute_category(name) if attr_cat == FORWARD: return entity_instance.wrap_value( self.wrapped_data.get_argument(self.wrapped_data.get_argument_index(name)), self.wrapped_data.file ) elif attr_cat == INVERSE: return entity_instance.wrap_value(self.wrapped_data.get_inverse(name), self.wrapped_data.file) else: raise AttributeError( "entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), name) ) @staticmethod def walk(f, g, value): if isinstance(value, (tuple, list)): return tuple(map(functools.partial(entity_instance.walk, f, g), value)) elif f(value): return g(value) else: return value @staticmethod def wrap_value(v, file): def wrap(e): return entity_instance(e, file) def is_instance(e): return isinstance(e, ifcopenshell_wrapper.entity_instance) return entity_instance.walk(is_instance, wrap, v) @staticmethod def unwrap_value(v): def unwrap(e): return e.wrapped_data def is_instance(e): return isinstance(e, entity_instance) return entity_instance.walk(is_instance, unwrap, v) def attribute_type(self, attr): """Return the data type of a positional attribute of the element :param attr: The index of the attribute :type attr: int :rtype: string """ attr_idx = attr if isinstance(attr, numbers.Integral) else self.wrapped_data.get_argument_index(attr) return self.wrapped_data.get_argument_type(attr_idx) def attribute_name(self, attr_idx): """Return the name of a positional attribute of the element :param attr_idx: The index of the attribute :type attr_idx: int :rtype: string """ return self.wrapped_data.get_argument_name(attr_idx) def __setattr__(self, key, value): index = self.wrapped_data.get_argument_index(key) self[index] = value def __getitem__(self, key): if key < 0 or key >= len(self): raise IndexError("Attribute index {} out of range for instance of type {}".format(key, self.is_a())) return entity_instance.wrap_value(self.wrapped_data.get_argument(key), self.wrapped_data.file) def __setitem__(self, idx, value): if self.wrapped_data.file and self.wrapped_data.file.transaction: self.wrapped_data.file.transaction.store_edit(self, idx, value) if self.method_list is None: super(entity_instance, self).__setattr__("method_list", _method_dict[self.is_a(True)]) method = self.method_list[idx] if value is None: if method is not set_derived_atribute: self.wrapped_data.setArgumentAsNull(idx) else: self.method_list[idx](self.wrapped_data, idx, entity_instance.unwrap_value(value)) return value def __len__(self): return len(self.wrapped_data) def __repr__(self): return repr(self.wrapped_data) def is_a(self, *args): """Return the IFC class name of an instance, or checks if an instance belongs to a class. The check will also return true if a parent class name is provided. :param args: If specified, is a case insensitive IFC class name to check :type args: string :returns: Either the name of the class, or a boolean if it passes the check :rtype: string|bool Example:: f = ifcopenshell.file() f.create_entity('IfcPerson') f.is_a() >>> 'IfcPerson' f.is_a('IfcPerson') >>> True """ return self.wrapped_data.is_a(*args) def id(self): """Return the STEP numerical identifier :rtype: int """ return self.wrapped_data.id() def __eq__(self, other): if not isinstance(self, type(other)): return False return self.wrapped_data == other.wrapped_data def __hash__(self): return hash((self.id(), self.wrapped_data.file_pointer())) def __dir__(self): return sorted( set( itertools.chain( dir(type(self)), map(str, self.wrapped_data.get_attribute_names()), map(str, self.wrapped_data.get_inverse_attribute_names()), ) ) ) def get_info(self, include_identifier=True, recursive=False, return_type=dict, ignore=()): """Return a dictionary of the entity_instance's properties (Python and IFC) and their values. :param include_identifier: Whether or not to include the STEP numerical identifier :type include_identifier: bool :param recursive: Whether or not to convert referenced IFC elements into dictionaries too. All attributes also apply recursively :type recursive: bool :param return_type: The return data type to be casted into :type return_type: dict|list|other :param ignore: A list of attribute names to ignore :type ignore: set|list :returns: A dictionary of properties and their corresponding values :rtype: dict Example:: ifc_file = ifcopenshell.open(file_path) products = ifc_file.by_type("IfcProduct") obj_info = products[0].get_info() print(obj_info.keys()) >>> dict_keys(['Description', 'Name', 'BuildingAddress', 'LongName', 'GlobalId', 'ObjectPlacement', 'OwnerHistory', 'ObjectType', >>> ...'ElevationOfTerrain', 'CompositionType', 'id', 'Representation', 'type', 'ElevationOfRefHeight']) """ def _(): try: if include_identifier: yield "id", self.id() yield "type", self.is_a() except BaseException: logging.exception("unhandled exception while getting id / type info on {}".format(self)) for i in range(len(self)): try: if self.wrapped_data.get_attribute_names()[i] in ignore: continue attr_value = self[i] if recursive: def is_instance(e): return isinstance(e, entity_instance) def get_info_(inst): # for ty in ignore: # if inst.is_a(ty): # return None return entity_instance.get_info( inst, include_identifier=include_identifier, recursive=recursive, return_type=return_type, ignore=ignore, ) attr_value = entity_instance.walk(is_instance, get_info_, attr_value) yield self.attribute_name(i), attr_value except BaseException: logging.exception("unhandled exception occurred setting attribute name for {}".format(self)) return return_type(_()) __dict__ = property(get_info) def get_info_2(self, include_identifier=True, recursive=False, return_type=dict, ignore=()): assert include_identifier assert recursive assert return_type is dict assert len(ignore) == 0 return ifcopenshell_wrapper.get_info_cpp(self.wrapped_data)