Fix running of test/tests.py

This commit is contained in:
Thomas Krijnen
2026-01-08 11:49:29 +01:00
parent f5b2358c2e
commit ae79996eb6
8 changed files with 65 additions and 122 deletions
@@ -169,6 +169,7 @@ def open(
print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx print(products[0].id(), products[0].GlobalId) # 122 2XQ$n5SLP5MBLyL442paFx
print(products[0] == model[122] == model["2XQ$n5SLP5MBLyL442paFx"]) # True print(products[0] == model[122] == model["2XQ$n5SLP5MBLyL442paFx"]) # True
""" """
path = Path(path) path = Path(path)
if not path.exists(): if not path.exists():
raise FileNotFoundError(f"Path does not exist: '{path}'.") raise FileNotFoundError(f"Path does not exist: '{path}'.")
@@ -205,7 +206,33 @@ def open(
f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap) f = ifcopenshell_wrapper.open(str(path.absolute()), mmap=mmap)
else: else:
f = ifcopenshell_wrapper.open(str(path.absolute())) f = ifcopenshell_wrapper.open(str(path.absolute()))
return file(f)
f.post_init()
READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
if not f.good():
from . import Error, SchemaError
exc, msg = {
READ_ERROR: lambda: (IOError, "Unable to open file for reading"),
NO_HEADER: lambda: (Error, "Unable to parse IFC SPF header"),
UNSUPPORTED_SCHEMA: lambda: (
SchemaError,
"Unsupported schema: %s" % ",".join(f.header.file_schema.schema_identifiers),
),
INVALID_SYNTAX: lambda: (Error, "Syntax error during parse, check logs"),
# This is the case when passing uninitialized_tag
UNKNOWN: lambda: (None, None),
}[f.good().value()]()
if exc is not None:
raise exc(msg)
return f
def create_entity(type: str, schema: str = "IFC4", *args: Any, **kwargs: Any) -> entity_instance: def create_entity(type: str, schema: str = "IFC4", *args: Any, **kwargs: Any) -> entity_instance:
@@ -270,7 +270,7 @@ get_attr_stmt_entity = "%(null_check)s return ((express::Base)(get_attribute_val
get_attr_stmt_array = "%(null_check)s std::vector<express::Base> es = get_attribute_value(%(index)d); return cast_vector<%(list_instance_type)s>(es);" get_attr_stmt_array = "%(null_check)s std::vector<express::Base> es = get_attribute_value(%(index)d); return cast_vector<%(list_instance_type)s>(es);"
get_attr_stmt_nested_array = "%(null_check)s std::vector<std::vector<express::Base>> es = get_attribute_value(%(index)d); return cast_vector_vector<%(list_instance_type)s>(es);" get_attr_stmt_nested_array = "%(null_check)s std::vector<std::vector<express::Base>> es = get_attribute_value(%(index)d); return cast_vector_vector<%(list_instance_type)s>(es);"
get_inverse = "return cast_vector<%(type)s>(data()->file()->getInverse(data()->id(), %(schema_name_upper)s_types[%(type_index)d], %(index)d));" get_inverse = "return cast_vector<%(type)s>(file()->getInverse(data()->id(), %(schema_name_upper)s_types[%(type_index)d], %(index)d));"
set_attr_stmt = ( set_attr_stmt = (
"%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s" "%(check_optional_set_begin)sset_attribute_value(%(index)d, %(star_if_optional)sv);%(check_optional_set_else)sunset_attribute_value(%(index)d);%(check_optional_set_end)s"
+6 -48
View File
@@ -20,11 +20,9 @@ from __future__ import annotations
import os import os
import re import re
import numbers import numbers
import time
import zipfile import zipfile
import functools import functools
import ifcopenshell import ifcopenshell
import weakref
import types import types
from pathlib import Path from pathlib import Path
from typing import Any, Optional, TYPE_CHECKING, Union, overload, Literal, TypedDict from typing import Any, Optional, TYPE_CHECKING, Union, overload, Literal, TypedDict
@@ -414,13 +412,13 @@ class rocksdb_file_storage:
self._prefix = prefix self._prefix = prefix
def items(self): def items(self):
it = self.file.wrapped_data.key_value_store_iter(self._prefix) it = self.file.key_value_store_iter(self._prefix)
while it and it.valid(): while it and it.valid():
yield it.key(), it.value() yield it.key(), it.value()
it.next() it.next()
def read(self, key): def read(self, key):
return self.file.wrapped_data.key_value_store_query(key) return self.file.key_value_store_query(key)
def by_id(self, name): def by_id(self, name):
if isinstance(name, tuple): if isinstance(name, tuple):
@@ -480,25 +478,6 @@ class rocksdb_file_storage:
return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2])) return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
class file_header:
def __init__(self, file, header_data):
self.file = file
self.header_data = header_data
# @todo these are probably no longer necessary now as we no longer depend on decoration of file
@property
def file_description(self) -> ifcopenshell.entity_instance:
return self.header_data.file_description_py()
@property
def file_name(self) -> ifcopenshell.entity_instance:
return self.header_data.file_name_py()
@property
def file_schema(self) -> ifcopenshell.entity_instance:
return self.header_data.file_schema_py()
class file_mixin: class file_mixin:
"""Base class for containing IFC files. """Base class for containing IFC files.
@@ -611,7 +590,7 @@ class file_mixin:
# @todo we should probably check that values for # @todo we should probably check that values for
# attributes are not passed as duplicates using # attributes are not passed as duplicates using
# both regular arguments and keyword arguments. # both regular arguments and keyword arguments.
kwargs_attrs = [(e.wrapped_data.get_argument_index(name), arg) for name, arg in kwargs.items()] kwargs_attrs = [(e.get_argument_index(name), arg) for name, arg in kwargs.items()]
attrs = list(enumerate(args)) + kwargs_attrs attrs = list(enumerate(args)) + kwargs_attrs
if len(attrs) > len(e): if len(attrs) > len(e):
@@ -799,11 +778,11 @@ class file_mixin:
if with_attribute_indices and not allow_duplicate: if with_attribute_indices and not allow_duplicate:
raise ValueError("with_attribute_indices requires allow_duplicate to be True") raise ValueError("with_attribute_indices requires allow_duplicate to be True")
inverses = [entity_instance(e, self) for e in self.get_inverse(inst.wrapped_data)] inverses = self.get_inverse(inst)
if allow_duplicate: if allow_duplicate:
if with_attribute_indices: if with_attribute_indices:
idxs = self.get_inverse_indices(inst.wrapped_data) idxs = self.get_inverse_indices(inst)
# TODO: include in typing. # TODO: include in typing.
return list(zip(inverses, idxs)) return list(zip(inverses, idxs))
else: else:
@@ -811,17 +790,6 @@ class file_mixin:
return set(inverses) return set(inverses)
def get_total_inverses(self, inst: ifcopenshell.entity_instance) -> int:
"""Returns the number of entities that reference this entity
This is equivalent to `len(model.get_inverse(element))`, but
significantly faster.
:param inst: The entity instance to get inverse relationships
:returns: The total number of references
"""
return self.get_total_inverses(inst.wrapped_data)
def remove(self, inst: ifcopenshell.entity_instance) -> None: def remove(self, inst: ifcopenshell.entity_instance) -> None:
"""Deletes an IFC object in the file. """Deletes an IFC object in the file.
@@ -833,7 +801,7 @@ class file_mixin:
""" """
if self.transaction: if self.transaction:
self.transaction.store_delete(inst) self.transaction.store_delete(inst)
return self.remove(inst.wrapped_data) return self.remove(inst)
def batch(self): def batch(self):
"""Low-level mechanism to speed up deletion of large subgraphs""" """Low-level mechanism to speed up deletion of large subgraphs"""
@@ -904,16 +872,6 @@ class file_mixin:
def to_string(self) -> str: def to_string(self) -> str:
return self.to_string() return self.to_string()
@property
def header(self) -> file_header:
# TODO: Workaround for old builds, remove after build stabilizes.
# TODO: No need for `wrapped_data.header` to be a method - should use `@property`?
header = self.header
if isinstance(header, types.MethodType):
return file_header(self, self.header())
else:
return self.header
@property @property
def storage(self) -> Optional[rocksdb_file_storage]: def storage(self) -> Optional[rocksdb_file_storage]:
""" """
@@ -23,8 +23,7 @@ import sys
import operator import operator
from .. import open, ifcopenshell_wrapper from .. import open, ifcopenshell_wrapper
from ..file import file from ifcopenshell import file, entity_instance
from ..entity_instance import entity_instance
from . import has_occ from . import has_occ
@@ -305,7 +304,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
self.settings = settings self.settings = settings
if isinstance(file_or_filename, file): if isinstance(file_or_filename, file):
self.file = file self.file = file
file_or_filename = file_or_filename.wrapped_data file_or_filename = file_or_filename
else: else:
file_or_filename = self.file = open(file_or_filename) file_or_filename = self.file = open(file_or_filename)
@@ -367,13 +366,13 @@ class tree(ifcopenshell_wrapper.tree):
def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None): def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None):
args = [self] args = [self]
if file is not None: if file is not None:
args.append(file.wrapped_data) args.append(file)
if settings is not None: if settings is not None:
args.append(settings) args.append(settings)
ifcopenshell_wrapper.tree.__init__(*args) ifcopenshell_wrapper.tree.__init__(*args)
def add_file(self, file: file, settings: settings) -> None: def add_file(self, file: file, settings: settings) -> None:
ifcopenshell_wrapper.tree.add_file(self, file.wrapped_data, settings) ifcopenshell_wrapper.tree.add_file(self, file, settings)
def add_iterator(self, iterator: iterator) -> None: def add_iterator(self, iterator: iterator) -> None:
ifcopenshell_wrapper.tree.add_file(self, iterator) ifcopenshell_wrapper.tree.add_file(self, iterator)
@@ -387,7 +386,7 @@ class tree(ifcopenshell_wrapper.tree):
) -> list[entity_instance]: ) -> list[entity_instance]:
def unwrap(value): def unwrap(value):
if isinstance(value, entity_instance): if isinstance(value, entity_instance):
return value.wrapped_data return value
elif all(map(lambda v: hasattr(value, v), "XYZ")): elif all(map(lambda v: hasattr(value, v), "XYZ")):
return value.X(), value.Y(), value.Z() return value.X(), value.Y(), value.Z()
return value return value
@@ -406,12 +405,12 @@ class tree(ifcopenshell_wrapper.tree):
args.append(kwargs.get("completely_within", False)) args.append(kwargs.get("completely_within", False))
if "extend" in kwargs: if "extend" in kwargs:
args.append(kwargs["extend"]) args.append(kwargs["extend"])
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)] return ifcopenshell_wrapper.tree.select(*args)
def select_box(self, value, **kwargs) -> list[entity_instance]: def select_box(self, value, **kwargs) -> list[entity_instance]:
def unwrap(value): def unwrap(value):
if isinstance(value, entity_instance): if isinstance(value, entity_instance):
return value.wrapped_data return value
elif hasattr(value, "Get"): elif hasattr(value, "Get"):
return value.Get()[:3], value.Get()[3:] return value.Get()[:3], value.Get()[3:]
return value return value
@@ -421,7 +420,7 @@ class tree(ifcopenshell_wrapper.tree):
args.append(kwargs.get("completely_within", False)) args.append(kwargs.get("completely_within", False))
if "extend" in kwargs: if "extend" in kwargs:
args.append(kwargs.get("extend", -1.0e-5)) args.append(kwargs.get("extend", -1.0e-5))
return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)] return ifcopenshell_wrapper.tree.select_box(*args)
def clash_intersection_many( def clash_intersection_many(
self, self,
@@ -430,13 +429,13 @@ class tree(ifcopenshell_wrapper.tree):
tolerance: float = 0.002, tolerance: float = 0.002,
check_all: bool = True, check_all: bool = True,
) -> tuple[ifcopenshell_wrapper.clash, ...]: ) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], tolerance, check_all] args = [self, set_a, set_b, tolerance, check_all]
return ifcopenshell_wrapper.tree.clash_intersection_many(*args) return ifcopenshell_wrapper.tree.clash_intersection_many(*args)
def clash_collision_many( def clash_collision_many(
self, set_a: Iterable[entity_instance], set_b: Iterable[entity_instance], allow_touching=False self, set_a: Iterable[entity_instance], set_b: Iterable[entity_instance], allow_touching=False
) -> tuple[ifcopenshell_wrapper.clash, ...]: ) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], allow_touching] args = [self, set_a, set_b, allow_touching]
return ifcopenshell_wrapper.tree.clash_collision_many(*args) return ifcopenshell_wrapper.tree.clash_collision_many(*args)
def clash_clearance_many( def clash_clearance_many(
@@ -446,7 +445,7 @@ class tree(ifcopenshell_wrapper.tree):
clearance: float = 0.05, clearance: float = 0.05,
check_all: bool = False, check_all: bool = False,
) -> tuple[ifcopenshell_wrapper.clash, ...]: ) -> tuple[ifcopenshell_wrapper.clash, ...]:
args = [self, [e.wrapped_data for e in set_a], [e.wrapped_data for e in set_b], clearance, check_all] args = [self, set_a, set_b, clearance, check_all]
return ifcopenshell_wrapper.tree.clash_clearance_many(*args) return ifcopenshell_wrapper.tree.clash_clearance_many(*args)
@staticmethod @staticmethod
@@ -509,7 +508,7 @@ def create_shape(
return wrap_shape_creation( return wrap_shape_creation(
settings, settings,
ifcopenshell_wrapper.create_shape( ifcopenshell_wrapper.create_shape(
settings, inst.wrapped_data, repr.wrapped_data if repr is not None else None, geometry_library settings, inst, repr if repr is not None else None, geometry_library
), ),
) )
@@ -525,7 +524,7 @@ def map_shape(settings: settings, inst: entity_instance) -> ifcopenshell_wrapper
>>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components >>> ifcopenshell.geom.map_shape(ifcopenshell.geom.settings(), point).components
(0.0, 0.0, 0.0) (0.0, 0.0, 0.0)
""" """
return ifcopenshell_wrapper.map_shape(settings, inst.wrapped_data) return ifcopenshell_wrapper.map_shape(settings, inst)
@overload @overload
@@ -618,20 +617,17 @@ def iterate(
def make_shape_function(fn): def make_shape_function(fn):
def entity_instance_or_none(e):
return None if e is None else entity_instance(e)
if has_occ: if has_occ:
def _(schema, string_or_shape, *args): def _(schema, string_or_shape, *args):
if isinstance(string_or_shape, TopoDS.TopoDS_Shape): if isinstance(string_or_shape, TopoDS.TopoDS_Shape):
string_or_shape = utils.serialize_shape(string_or_shape) string_or_shape = utils.serialize_shape(string_or_shape)
return entity_instance_or_none(fn(schema, string_or_shape, *args)) return fn(schema, string_or_shape, *args)
else: else:
def _(schema, string, *args): def _(schema, string, *args):
return entity_instance_or_none(fn(schema, string, *args)) return fn(schema, string, *args)
return _ return _
+6 -2
View File
@@ -774,8 +774,12 @@ express::Base IfcParse::impl::rocks_db_file_storage::create(const IfcParse::decl
} }
express::Base IfcParse::impl::in_memory_file_storage::create(const IfcParse::declaration* decl, int id) { express::Base IfcParse::impl::in_memory_file_storage::create(const IfcParse::declaration* decl, int id) {
auto instance_name = id == -1 ? (int)file->FreshId() : id; uint32_t instance_name;
if (decl->as_entity() == nullptr && decl->as_type_declaration() == nullptr) { if (decl->as_entity() != nullptr) {
instance_name = id == -1 ? (int)file->FreshId() : id;
} else if (decl->as_type_declaration() != nullptr) {
instance_name = 0;
} else {
throw std::runtime_error("Requires and entity or type declaration"); throw std::runtime_error("Requires and entity or type declaration");
} }
auto ptr = byid_.insert({instance_name, std::make_shared<InstanceData>(file, decl, instance_name, decl->as_entity() ? in_memory_attribute_storage(decl->as_entity()->attribute_count()) : in_memory_attribute_storage(1))}).first; auto ptr = byid_.insert({instance_name, std::make_shared<InstanceData>(file, decl, instance_name, decl->as_entity() ? in_memory_attribute_storage(decl->as_entity()->attribute_count()) : in_memory_attribute_storage(1))}).first;
+1 -3
View File
@@ -1840,8 +1840,6 @@ express::Base IfcFile::addEntity(const express::Base& entity, int id) {
return mit->second; return mit->second;
} }
express::Base new_entity;
// Obtain all forward references by a depth-first // Obtain all forward references by a depth-first
// traversal and add them to the file. // traversal and add them to the file.
try { try {
@@ -1862,7 +1860,7 @@ express::Base IfcFile::addEntity(const express::Base& entity, int id) {
// container and entity is created. The attribute references // container and entity is created. The attribute references
// need to be updated to point to instances in this file. // need to be updated to point to instances in this file.
IfcFile* other_file = entity.file(); IfcFile* other_file = entity.file();
create(&entity.declaration(), id); auto new_entity = create(&entity.declaration(), id);
auto* decl = &entity.declaration(); auto* decl = &entity.declaration();
auto num_attributes = (decl->as_entity() ? decl->as_entity()->attribute_count() : 1); auto num_attributes = (decl->as_entity() ? decl->as_entity()->attribute_count() : 1);
+6 -48
View File
@@ -342,6 +342,7 @@ private:
%pythoncode %{ %pythoncode %{
schema = property(schema_name) schema = property(schema_name)
header = property(header)
old_init = __init__ old_init = __init__
def __init__(self, schema=None, schema_version=None): def __init__(self, schema=None, schema_version=None):
@@ -519,7 +520,7 @@ private:
return IfcUtil::ArgumentTypeToString(helper_fn_attribute_type($self, i)); return IfcUtil::ArgumentTypeToString(helper_fn_attribute_type($self, i));
} }
const std::string& get_argument_name(unsigned int i) const { const std::string& attribute_name(unsigned int i) const {
if ($self->declaration().as_entity()) { if ($self->declaration().as_entity()) {
return $self->declaration().as_entity()->attribute_by_index(i)->name(); return $self->declaration().as_entity()->attribute_by_index(i)->name();
} else if (i == 0) { } else if (i == 0) {
@@ -821,31 +822,9 @@ private:
} }
} }
// Expose FileDescription and FileName header entities
// to make them readable even if they were not filled properly before.
// Though it is invalid IFC, technically.
// FileSchema is not exposed as IFC file won't load if it's invalid.
%extend IfcParse::FileDescription {
AttributeValue description() const { return $self->getArgument(0); }
AttributeValue implementation_level() const { return $self->getArgument(1); }
};
%extend IfcParse::FileName {
AttributeValue name() const { return $self->getArgument(0); }
AttributeValue time_stamp() const { return $self->getArgument(1); }
AttributeValue author() const { return $self->getArgument(2); }
AttributeValue organization() const { return $self->getArgument(3); }
AttributeValue preprocessor_version() const { return $self->getArgument(4); }
AttributeValue originating_system() const { return $self->getArgument(5); }
AttributeValue authorization() const { return $self->getArgument(6); }
};
%extend IfcParse::IfcSpfHeader { %extend IfcParse::IfcSpfHeader {
// Cast to base class pointers for SWIG, because // Upcast to header instances for SWIG, because
// it has no idea about the schema definitions. // it has no idea about the schema definitions.
// The code to access these methods as attributes
// is in file.py
express::Base file_description_py() { express::Base file_description_py() {
return $self->file_description(); return $self->file_description();
} }
@@ -855,32 +834,11 @@ private:
express::Base file_schema_py() { express::Base file_schema_py() {
return $self->file_schema(); return $self->file_schema();
} }
};
%extend IfcParse::FileDescription {
%pythoncode %{ %pythoncode %{
# Hide the getters with read-write property implementations file_description = property(file_description_py)
description = property(description, description) file_name = property(file_name_py)
implementation_level = property(implementation_level, implementation_level) file_schema = property(file_schema_py)
%}
};
%extend IfcParse::FileName {
%pythoncode %{
name = property(name, name)
time_stamp = property(time_stamp, time_stamp)
author = property(author, author)
organization = property(organization, organization)
preprocessor_version = property(preprocessor_version, preprocessor_version)
originating_system = property(originating_system, originating_system)
authorization = property(authorization, authorization)
%}
};
%extend IfcParse::FileSchema {
%pythoncode %{
# Hide the getters with read-write property implementations
schema_identifiers = property(schema_identifiers, schema_identifiers)
%} %}
}; };
+2
View File
@@ -53,6 +53,8 @@ f2 = ifcopenshell.file(schema=f.schema)
prop2 = f2.add(prop) prop2 = f2.add(prop)
assert str(prop) == str(prop2).replace(str(prop2.id()), str(prop.id())) assert str(prop) == str(prop2).replace(str(prop2.id()), str(prop.id()))
assert prop2.id() == 1 assert prop2.id() == 1
# Adding the same instance returns the previous copy
assert f2.add(prop) == prop2
# A recursively obtained python dictionary representation # A recursively obtained python dictionary representation
# matches for copied instances as well # matches for copied instances as well