Don't rely on util for basic ifcopenshell module capabilities. Keep util as an optional module for users to load.

This commit is contained in:
Dion Moult
2024-05-07 10:32:02 +10:00
parent 28d205b4a3
commit 7e13ed746e
4 changed files with 60 additions and 49 deletions
@@ -36,8 +36,8 @@ from __future__ import print_function
import os
import sys
import tempfile
import zipfile
import tempfile
from pathlib import Path
from typing import Optional
@@ -73,9 +73,11 @@ from . import guid
from .file import file
from .entity_instance import entity_instance, register_schema_attributes
from .sql import sqlite, sqlite_entity
try:
from .stream import stream, stream_entity
except: pass
except:
pass
READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER
@@ -84,11 +86,13 @@ UNSUPPORTED_SCHEMA = ifcopenshell_wrapper.file_open_status.UNSUPPORTED_SCHEMA
class Error(Exception):
"""Error used when a generic problem occurs"""
pass
class SchemaError(Error):
"""Error used when an IFC schema related problem occurs"""
pass
@@ -114,7 +118,7 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa
"""
path = Path(path)
if format is None:
format = ifcopenshell.util.file.guess_format(path)
format = guess_format(path)
if format == ".ifcXML":
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()))
if f:
@@ -141,8 +145,7 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa
NO_HEADER: (Error, "Unable to parse IFC SPF header"),
UNSUPPORTED_SCHEMA: (
SchemaError,
"Unsupported schema: %s"
% ",".join(f.header.file_schema.schema_identifiers),
"Unsupported schema: %s" % ",".join(f.header.file_schema.schema_identifiers),
),
}[f.good().value()]
raise exc(msg)
@@ -226,4 +229,31 @@ def schema_by_name(
return ifcopenshell_wrapper.schema_by_name(schema)
def guess_format(path: Path) -> Union[str | None]:
"""Try to guess format using file extension
IFCs may be serialised as different formats. The most common is a ``.ifc``
file, which is plaintext and stores data using the STEP Physical File
format. IFC can also be stored as a Zipfile, XML, JSON, or SQL.
This will return the canonical form of the format. For example, if a path
has the extension of .xml or .ifcxml (case insensitive), it will return
.ifcXML.
:return: Either .ifc, .ifcZIP, .ifcXML, .ifcJSON, .ifcSQLite, or None.
"""
suffix = path.suffix.lower()
if suffix == ".ifc":
return ".ifc"
elif suffix in (".ifczip", ".zip"):
return ".ifcZIP"
elif suffix in (".ifcxml", ".xml"):
return ".ifcXML"
elif suffix in (".ifcjson", ".json"):
return ".ifcJSON"
elif suffix in (".ifcsqlite", ".sqlite", ".db"):
return ".ifcSQLite"
return None
from .main import *
+13 -9
View File
@@ -27,11 +27,10 @@ import re
import numbers
import zipfile
import functools
import ifcopenshell
from pathlib import Path
from typing import Optional, Any
import ifcopenshell.util.element
import ifcopenshell.util.file
from . import ifcopenshell_wrapper
from .entity_instance import entity_instance
@@ -120,11 +119,19 @@ class Transaction:
for inverse in self.file.get_inverse(element):
inverse_references = []
for i, attribute in enumerate(inverse):
if ifcopenshell.util.element.has_element_reference(attribute, element):
if self.has_element_reference(attribute, element):
inverse_references.append((i, self.serialise_value(inverse, attribute)))
inverses[inverse.id()] = inverse_references
return inverses
def has_element_reference(self, value: Any, element: ifcopenshell.entity_instance) -> bool:
if isinstance(value, (tuple, list)):
for v in value:
if self.has_element_reference(v, element):
return True
return False
return value == element
def rollback(self):
for operation in self.operations[::-1]:
if operation["action"] == "create":
@@ -376,14 +383,11 @@ class file(object):
match = re.match(reg, self.wrapped_data.schema)
version_tuple = tuple(
map(
lambda pp: int(pp[1][len(pp[0]):]) if pp[1] else None,
lambda pp: int(pp[1][len(pp[0]) :]) if pp[1] else None,
((p, match.group(p)) for p in prefixes),
)
)
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]))
elif attr == "schema_identifier":
return self.wrapped_data.schema
elif attr == "schema_version":
@@ -576,7 +580,7 @@ class file(object):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
if format == None:
format = ifcopenshell.util.file.guess_format(path)
format = ifcopenshell.guess_format(path)
if format == ".ifcXML":
serializer = ifcopenshell_wrapper.XmlSerializer(self, str(path))
serializer.finalize()
+12 -6
View File
@@ -2,7 +2,6 @@ try:
import re
import json
import ifcopenshell.util.schema
from .file import file
from . import ifcopenshell_wrapper
from .entity_instance import entity_instance
@@ -56,6 +55,8 @@ class sqlite(file):
self.preprocess_schema()
def preprocess_schema(self):
import ifcopenshell.util.schema
self.ifc_class_subtypes = {}
self.ifc_class_attributes = {}
self.ifc_class_inverse_attributes = {}
@@ -122,6 +123,9 @@ class sqlite(file):
return entity
def by_type(self, type, include_subtypes=True):
# TODO use cached subtypes
import ifcopenshell.util.schema
if self.class_map:
results = []
subtypes = self.ifc_class_subtypes[type] if include_subtypes else self.ifc_class_subtypes[type][0:1]
@@ -167,7 +171,9 @@ class sqlite(file):
return results
def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False):
query = f"SELECT inverses FROM {inst.sqlite_wrapper.ifc_class} WHERE `ifc_id` = {inst.sqlite_wrapper.id} LIMIT 1"
query = (
f"SELECT inverses FROM {inst.sqlite_wrapper.ifc_class} WHERE `ifc_id` = {inst.sqlite_wrapper.id} LIMIT 1"
)
self.cursor.execute(query)
row = self.cursor.fetchone()
if not row or not row[0]:
@@ -198,9 +204,9 @@ class sqlite(file):
"verts": np.frombuffer(row["verts"]).tolist() if row["verts"] else [],
"edges": np.frombuffer(row["edges"], dtype=np.int64).tolist() if row["edges"] else [],
"faces": np.frombuffer(row["faces"], dtype=np.int64).tolist() if row["faces"] else [],
"material_ids": np.frombuffer(row["material_ids"], dtype=np.int64).tolist()
if row["material_ids"]
else [],
"material_ids": (
np.frombuffer(row["material_ids"], dtype=np.int64).tolist() if row["material_ids"] else []
),
"materials": json.loads(row["materials"]) if row["materials"] else [],
}
shapes[row["ifc_id"]] = {
@@ -353,7 +359,7 @@ class sqlite_entity(entity_instance):
def get_info(self, include_identifier=True, recursive=False, return_type=dict, ignore=(), scalar_only=False):
info = {"id": self.sqlite_wrapper.id, "type": self.sqlite_wrapper.ifc_class}
if not self.sqlite_wrapper.attribute_cache:
self.__getitem__(0) # This will get all attributes
self.__getitem__(0) # This will get all attributes
info.update(self.sqlite_wrapper.attribute_cache)
return info
@@ -1,29 +0,0 @@
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# 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 <http://www.gnu.org/licenses/>.
from pathlib import Path
def guess_format(path: Path) -> "str | None":
"""Try to guess format using file extension"""
if path.suffix.lower() in (".ifczip", ".zip"):
return ".ifcZIP"
elif path.suffix.lower() in (".ifcxml", ".xml"):
return ".ifcXML"
elif path.suffix.lower() in (".ifcsqlite", ".sqlite", ".db"):
return ".ifcSQLite"