mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
Work towards v1.0 data model with encapsulated weak_ptr as basis for instances
This commit is contained in:
@@ -43,75 +43,7 @@ except ImportError:
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def set_derived_attribute(*args):
|
||||
raise TypeError("Unable to set derived attribute")
|
||||
|
||||
|
||||
def set_unsupported_attribute(*args):
|
||||
raise TypeError("This is an unsupported attribute type")
|
||||
|
||||
|
||||
# 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.
|
||||
MethodList = list[Callable[[ifcopenshell_wrapper.entity_instance, int, Any], Union[None, NoReturn]]]
|
||||
"""List of setter methods for class attributes."""
|
||||
_method_dict: dict[str, MethodList] = {}
|
||||
"""Mapping of entity classes (e.g. 'IFC4.IfcWall') to MethodLists."""
|
||||
|
||||
|
||||
def register_schema_attributes(schema: ifcopenshell_wrapper.schema_definition) -> None:
|
||||
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()
|
||||
type_strs = cast(Sequence[str], type_strs)
|
||||
|
||||
# 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_attribute
|
||||
if mname == "setArgumentAsDerived"
|
||||
else (
|
||||
set_unsupported_attribute
|
||||
if mname == "setArgumentAsUnknown"
|
||||
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:
|
||||
class entity_instance_mixin:
|
||||
"""Represents an entity (wall, slab, property, etc) of an IFC model
|
||||
|
||||
An IFC model consists of entities. Examples of entities include walls,
|
||||
@@ -155,67 +87,33 @@ class entity_instance:
|
||||
print(wall.__class__) # <class 'ifcopenshell.entity_instance'>
|
||||
"""
|
||||
|
||||
wrapped_data: ifcopenshell_wrapper.entity_instance
|
||||
method_list: Union[MethodList, None] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
e: Union[ifcopenshell_wrapper.entity_instance, tuple[str, str]],
|
||||
file: Union[ifcopenshell.file, None] = None,
|
||||
):
|
||||
"""
|
||||
:param e: Wrapper's ``entity_instance`` or a tuple ``(schema_identifier, ifc_class)``.
|
||||
"""
|
||||
# Instances of this class will be created and removed very often,
|
||||
# so it's important to keep it very optimized.
|
||||
|
||||
if isinstance(e, tuple):
|
||||
e = ifcopenshell_wrapper.new_IfcBaseClass(*e)
|
||||
object.__setattr__(self, "wrapped_data", e)
|
||||
|
||||
# Make sure the file is not gc'ed while we have live instances
|
||||
e.file = file
|
||||
|
||||
def __del__(self):
|
||||
"""
|
||||
#2471 while the precise chain of action is unclear, creating
|
||||
instance references prevents file gc, even with all instance
|
||||
refs deleted. This is a work-around for that.
|
||||
"""
|
||||
# Avoid infinite recursion if entity is failed to initialize
|
||||
# and wrapped_data is unset. Hacky since we override
|
||||
# both __dict__ and __dir__.
|
||||
try:
|
||||
wrapped_data = object.__getattribute__(self, "wrapped_data")
|
||||
wrapped_data.file = None
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
@property
|
||||
def file(self):
|
||||
# ugh circular imports, name collisions
|
||||
from . import file
|
||||
|
||||
return file.from_pointer(self.wrapped_data.file_pointer())
|
||||
raise NotImplementedError
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name in ("this", "thisown") or name.startswith("_swig_"):
|
||||
return object.__getattr__(self, name)
|
||||
"""
|
||||
Any aggregate attributes (e.g. `SET`) are returns as Python tuples.
|
||||
|
||||
Inverse attributes are always returned as tuples, even it's not a set origially in IFC
|
||||
Inverse attributes are returned as tuples, even it's not a set origially in IFC
|
||||
(e.g. IfcFeatureElementSubtraction.VoidsElements)
|
||||
(unless settings.unpack_non_aggregate_inverses is used, which is necessary for express rule execution)
|
||||
"""
|
||||
INVALID, FORWARD, INVERSE = range(3)
|
||||
attr_cat = self.wrapped_data.get_attribute_category(name)
|
||||
if attr_cat == FORWARD:
|
||||
idx = self.wrapped_data.get_argument_index(name)
|
||||
if _method_dict[self.is_a(True)][idx] != set_derived_attribute:
|
||||
# A bit ugly, but we fall through to derived attribute handling below
|
||||
return entity_instance.wrap_value(self.wrapped_data.get_argument(idx), self.wrapped_data.file)
|
||||
INVALID, FORWARD, INVERSE, DERIVED = range(4)
|
||||
attr_cat = self.get_attribute_category(name)
|
||||
if attr_cat == INVALID:
|
||||
raise AttributeError(
|
||||
"entity instance of type '%s' has no attribute '%s'" % (self.is_a(True), name)
|
||||
)
|
||||
elif attr_cat == FORWARD:
|
||||
idx = self.get_argument_index(name)
|
||||
return self.get_argument(idx)
|
||||
elif attr_cat == INVERSE:
|
||||
vs = entity_instance.wrap_value(self.wrapped_data.get_inverse(name), self.wrapped_data.file)
|
||||
vs = self.get_inverse(name)
|
||||
if settings.unpack_non_aggregate_inverses:
|
||||
schema_name = self.wrapped_data.is_a(True).split(".")[0]
|
||||
schema_name = self.is_a(True).split(".")[0]
|
||||
ent: ifcopenshell_wrapper.entity
|
||||
ent = ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(self.is_a())
|
||||
inv = next(i for i in ent.all_inverse_attributes() if i.name() == name)
|
||||
@@ -225,44 +123,38 @@ class entity_instance:
|
||||
else:
|
||||
vs = None
|
||||
return vs
|
||||
elif attr_cat == DERIVED:
|
||||
schema_name = self.is_a(True).split(".")[0]
|
||||
try:
|
||||
rules = importlib.import_module(f"ifcopenshell.express.rules.{schema_name}")
|
||||
except:
|
||||
import os
|
||||
|
||||
# derived attribute perhaps?
|
||||
schema_name = self.wrapped_data.is_a(True).split(".")[0]
|
||||
try:
|
||||
rules = importlib.import_module(f"ifcopenshell.express.rules.{schema_name}")
|
||||
except:
|
||||
import os
|
||||
current_dir_files = {fn.lower(): fn for fn in os.listdir(".")}
|
||||
exp_filename = schema_name.lower() + ".exp"
|
||||
schema_path = current_dir_files.get(exp_filename)
|
||||
if schema_path is None:
|
||||
raise Exception(
|
||||
f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'."
|
||||
)
|
||||
fn = schema_path[:-4] + ".py"
|
||||
if not os.path.exists(fn):
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True
|
||||
)
|
||||
time.sleep(1.0)
|
||||
rules = importlib.import_module(schema_name)
|
||||
|
||||
current_dir_files = {fn.lower(): fn for fn in os.listdir(".")}
|
||||
exp_filename = schema_name.lower() + ".exp"
|
||||
schema_path = current_dir_files.get(exp_filename)
|
||||
if schema_path is None:
|
||||
raise Exception(
|
||||
f"Couldn't find express file '{schema_name.lower()}.exp' in the current folder: '{os.getcwd()}'."
|
||||
)
|
||||
fn = schema_path[:-4] + ".py"
|
||||
if not os.path.exists(fn):
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "ifcopenshell.express.rule_compiler", schema_path, fn], check=True
|
||||
)
|
||||
time.sleep(1.0)
|
||||
rules = importlib.import_module(schema_name)
|
||||
def yield_supertypes():
|
||||
decl = ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(self.is_a())
|
||||
while decl:
|
||||
yield decl.name()
|
||||
decl = decl.supertype()
|
||||
|
||||
def yield_supertypes():
|
||||
decl = ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(self.is_a())
|
||||
while decl:
|
||||
yield decl.name()
|
||||
decl = decl.supertype()
|
||||
|
||||
for sty in yield_supertypes():
|
||||
fn = getattr(rules, f"calc_{sty}_{name}", None)
|
||||
if fn:
|
||||
return fn(self)
|
||||
|
||||
if attr_cat != FORWARD:
|
||||
raise AttributeError(
|
||||
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), name)
|
||||
)
|
||||
for sty in yield_supertypes():
|
||||
fn = getattr(rules, f"calc_{sty}_{name}", None)
|
||||
if fn:
|
||||
return fn(self)
|
||||
|
||||
@staticmethod
|
||||
def walk(f: Callable[[Any], bool], g: Callable[[Any], Any], value: Any) -> Any:
|
||||
@@ -295,180 +187,53 @@ class entity_instance:
|
||||
"""
|
||||
|
||||
if isinstance(value, (tuple, list)):
|
||||
return tuple(map(functools.partial(entity_instance.walk, f, g), value))
|
||||
return tuple(map(functools.partial(entity_instance_mixin.walk, f, g), value))
|
||||
elif f(value):
|
||||
return g(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def wrap_value(v, file: ifcopenshell.file):
|
||||
def wrap(e: ifcopenshell_wrapper.entity_instance) -> entity_instance:
|
||||
return entity_instance(e, file)
|
||||
|
||||
def is_instance(e: Any) -> bool:
|
||||
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: Union[int, str]) -> str:
|
||||
"""Return the data type of a positional attribute of the element
|
||||
|
||||
:param attr: The index or name of the attribute
|
||||
"""
|
||||
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: int) -> str:
|
||||
"""Return the name of a positional attribute of the element
|
||||
|
||||
:param attr_idx: The index of the attribute
|
||||
"""
|
||||
return self.wrapped_data.get_argument_name(attr_idx)
|
||||
|
||||
def __setattr__(self, key: str, value: Any) -> None:
|
||||
index = self.wrapped_data.get_argument_index(key)
|
||||
if key in ("this", "thisown") or key.startswith("_swig_"):
|
||||
return object.__setattr__(self, key, value)
|
||||
|
||||
index = self.get_argument_index(key)
|
||||
try:
|
||||
self[index] = value
|
||||
except IndexError as e:
|
||||
# get_argument_index returns 0xFFFFFFFF if attribute is not found
|
||||
if index == 0xFFFFFFFF:
|
||||
raise AttributeError(
|
||||
"entity instance of type '%s' has no attribute '%s'" % (self.wrapped_data.is_a(True), key)
|
||||
"entity instance of type '%s' has no attribute '%s'" % (self.is_a(True), key)
|
||||
)
|
||||
raise e
|
||||
|
||||
def __getitem__(self, key: int) -> Any:
|
||||
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)
|
||||
return self.get_argument(key)
|
||||
|
||||
def __setitem__(self, idx: int, value: T) -> T:
|
||||
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().__setattr__("method_list", _method_dict[self.is_a(True)])
|
||||
|
||||
method = self.method_list[idx]
|
||||
|
||||
if value is None:
|
||||
if method is not set_derived_attribute:
|
||||
try:
|
||||
self.wrapped_data.setArgumentAsNull(idx)
|
||||
except RuntimeError as e:
|
||||
if e.args == ("Attribute not set",):
|
||||
raise TypeError(
|
||||
"attribute '%s' is not optional for entity instance of type '%s'"
|
||||
% (self.wrapped_data.get_argument_name(idx), self.wrapped_data.is_a(True))
|
||||
)
|
||||
raise e
|
||||
else:
|
||||
try:
|
||||
self.method_list[idx](self.wrapped_data, idx, entity_instance.unwrap_value(value))
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
"attribute '%s' for entity '%s' is expecting value of type '%s', got '%s'."
|
||||
% (
|
||||
self.wrapped_data.get_argument_name(idx),
|
||||
self.wrapped_data.is_a(True),
|
||||
self.wrapped_data.get_argument_type(idx),
|
||||
type(value).__name__,
|
||||
)
|
||||
)
|
||||
if self.file and self.file.transaction:
|
||||
self.file.transaction.store_edit(self, idx, value)
|
||||
|
||||
self.set_attribute_value_py(idx, value)
|
||||
|
||||
return value
|
||||
|
||||
def __len__(self):
|
||||
return len(self.wrapped_data)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.wrapped_data)
|
||||
|
||||
def to_string(self, valid_spf=True) -> str:
|
||||
"""Returns a string representation of the current entity instance.
|
||||
Equal to str(self) when valid_spf=False. When valid_spf is True
|
||||
returns a representation of the string that conforms to valid Step
|
||||
Physical File notation. The difference being entity names in upper
|
||||
case and string attribute values with unicode values encoded per
|
||||
the specific control directives.
|
||||
"""
|
||||
|
||||
return self.wrapped_data.to_string(valid_spf)
|
||||
|
||||
@overload
|
||||
def is_a(self) -> str: ...
|
||||
@overload
|
||||
def is_a(self, ifc_class: str) -> bool: ...
|
||||
@overload
|
||||
def is_a(self, with_schema: bool) -> str: ...
|
||||
def is_a(self, *args: Union[str, bool]) -> Union[str, bool]:
|
||||
"""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
|
||||
or if specified as a boolean then will define whether
|
||||
returned IFC class name should include schema name
|
||||
(e.g. "IFC4.IfcWall" if `True` and "IfcWall" if `False`).
|
||||
If omitted will act as `False`.
|
||||
:returns: Either the name of the class, or a boolean if it passes the check
|
||||
|
||||
Example:
|
||||
|
||||
.. code:: python
|
||||
|
||||
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) -> int:
|
||||
"""Return the STEP numerical identifier"""
|
||||
return self.wrapped_data.id()
|
||||
|
||||
def __eq__(self, other: entity_instance) -> bool:
|
||||
def __eq__(self, other: entity_instance_mixin) -> bool:
|
||||
if not isinstance(self, type(other)):
|
||||
return False
|
||||
elif None in (self.wrapped_data.file, other.wrapped_data.file):
|
||||
# when not added to a file, we can only compare attribute values
|
||||
# and we need this for where rule evaluation
|
||||
return self.get_info_2(recursive=True, include_identifier=False) == other.get_info_2(
|
||||
recursive=True, include_identifier=False
|
||||
)
|
||||
else:
|
||||
# Proper entity instances have a stable identity by means of the numeric
|
||||
# step id. Selected type instances (such as IfcPropertySingleValue.NominalValue
|
||||
# always have id=0, so we compare <type, value, file pointer>
|
||||
if self.id():
|
||||
return self.wrapped_data == other.wrapped_data
|
||||
else:
|
||||
return (self.is_a(), self[0], self.wrapped_data.file_pointer()) == (
|
||||
other.is_a(),
|
||||
other[0],
|
||||
other.wrapped_data.file_pointer(),
|
||||
)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def is_entity(self) -> bool:
|
||||
"""Tests whether the instance is an entity type as opposed to a simple data type.
|
||||
|
||||
:return: True if the instance is an entity
|
||||
"""
|
||||
schema_name = self.wrapped_data.is_a(True).split(".")[0]
|
||||
schema_name = self.is_a(True).split(".")[0]
|
||||
decl = ifcopenshell_wrapper.schema_by_name(schema_name).declaration_by_name(self.is_a())
|
||||
return isinstance(decl, ifcopenshell_wrapper.entity)
|
||||
|
||||
@@ -505,9 +270,9 @@ class entity_instance:
|
||||
:return: bool: The comparison predicate applied to self and other
|
||||
"""
|
||||
|
||||
if isinstance(other, entity_instance):
|
||||
if isinstance(other, entity_instance_mixin):
|
||||
a, b = map(tuple, (self, other))
|
||||
if any(map(entity_instance.is_entity, (self, other))):
|
||||
if any(map(entity_instance_mixin.is_entity, (self, other))):
|
||||
a = (self.is_a(),) + a
|
||||
b = (other.is_a(),) + b
|
||||
elif self.is_entity():
|
||||
@@ -540,17 +305,17 @@ class entity_instance:
|
||||
# step id. Selected type instances (such as IfcPropertySingleValue.NominalValue
|
||||
# always have id=0, so we hash <type, value, file pointer>
|
||||
if id_ := self.id():
|
||||
return hash((id_, self.wrapped_data.file_pointer()))
|
||||
return hash((id_, self.file_pointer()))
|
||||
else:
|
||||
return hash((self.is_a(), self[0], self.wrapped_data.file_pointer()))
|
||||
return hash((self.is_a(), self[0], self.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()),
|
||||
map(str, self.get_attribute_names()),
|
||||
map(str, self.get_inverse_attribute_names()),
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -595,7 +360,7 @@ class entity_instance:
|
||||
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:
|
||||
if self.get_attribute_names()[i] in ignore:
|
||||
continue
|
||||
attr_value = self[i]
|
||||
|
||||
@@ -604,10 +369,10 @@ class entity_instance:
|
||||
if recursive or scalar_only:
|
||||
|
||||
def is_instance(e):
|
||||
return isinstance(e, entity_instance)
|
||||
return isinstance(e, entity_instance_mixin)
|
||||
|
||||
def get_info_(inst):
|
||||
return entity_instance.get_info(
|
||||
return entity_instance_mixin.get_info(
|
||||
inst,
|
||||
include_identifier=include_identifier,
|
||||
recursive=recursive,
|
||||
@@ -619,7 +384,7 @@ class entity_instance:
|
||||
to_include["v"] = False
|
||||
return None
|
||||
|
||||
attr_value = entity_instance.walk(
|
||||
attr_value = entity_instance_mixin.walk(
|
||||
is_instance, get_info_ if recursive else do_ignore, attr_value
|
||||
)
|
||||
|
||||
@@ -651,4 +416,4 @@ class entity_instance:
|
||||
assert recursive
|
||||
assert return_type is dict
|
||||
assert len(ignore) == 0
|
||||
return ifcopenshell_wrapper.get_info_cpp(self.wrapped_data, include_identifier)
|
||||
return ifcopenshell_wrapper.get_info_cpp(self, include_identifier)
|
||||
|
||||
Reference in New Issue
Block a user