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 os
import sys import sys
import tempfile
import zipfile import zipfile
import tempfile
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -73,9 +73,11 @@ from . import guid
from .file import file from .file import file
from .entity_instance import entity_instance, register_schema_attributes from .entity_instance import entity_instance, register_schema_attributes
from .sql import sqlite, sqlite_entity from .sql import sqlite, sqlite_entity
try: try:
from .stream import stream, stream_entity from .stream import stream, stream_entity
except: pass except:
pass
READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR READ_ERROR = ifcopenshell_wrapper.file_open_status.READ_ERROR
NO_HEADER = ifcopenshell_wrapper.file_open_status.NO_HEADER 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): class Error(Exception):
"""Error used when a generic problem occurs""" """Error used when a generic problem occurs"""
pass pass
class SchemaError(Error): class SchemaError(Error):
"""Error used when an IFC schema related problem occurs""" """Error used when an IFC schema related problem occurs"""
pass pass
@@ -114,7 +118,7 @@ def open(path: "os.PathLike | str", format: str = None, should_stream: bool = Fa
""" """
path = Path(path) path = Path(path)
if format is None: if format is None:
format = ifcopenshell.util.file.guess_format(path) format = guess_format(path)
if format == ".ifcXML": if format == ".ifcXML":
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute())) f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()))
if f: 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"), NO_HEADER: (Error, "Unable to parse IFC SPF header"),
UNSUPPORTED_SCHEMA: ( UNSUPPORTED_SCHEMA: (
SchemaError, SchemaError,
"Unsupported schema: %s" "Unsupported schema: %s" % ",".join(f.header.file_schema.schema_identifiers),
% ",".join(f.header.file_schema.schema_identifiers),
), ),
}[f.good().value()] }[f.good().value()]
raise exc(msg) raise exc(msg)
@@ -226,4 +229,31 @@ def schema_by_name(
return ifcopenshell_wrapper.schema_by_name(schema) 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 * from .main import *
+13 -9
View File
@@ -27,11 +27,10 @@ import re
import numbers import numbers
import zipfile import zipfile
import functools import functools
import ifcopenshell
from pathlib import Path from pathlib import Path
from typing import Optional, Any from typing import Optional, Any
import ifcopenshell.util.element
import ifcopenshell.util.file
from . import ifcopenshell_wrapper from . import ifcopenshell_wrapper
from .entity_instance import entity_instance from .entity_instance import entity_instance
@@ -120,11 +119,19 @@ class Transaction:
for inverse in self.file.get_inverse(element): for inverse in self.file.get_inverse(element):
inverse_references = [] inverse_references = []
for i, attribute in enumerate(inverse): 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))) inverse_references.append((i, self.serialise_value(inverse, attribute)))
inverses[inverse.id()] = inverse_references inverses[inverse.id()] = inverse_references
return inverses 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): def rollback(self):
for operation in self.operations[::-1]: for operation in self.operations[::-1]:
if operation["action"] == "create": if operation["action"] == "create":
@@ -376,14 +383,11 @@ class file(object):
match = re.match(reg, self.wrapped_data.schema) match = re.match(reg, self.wrapped_data.schema)
version_tuple = tuple( version_tuple = tuple(
map( 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), ((p, match.group(p)) for p in prefixes),
) )
) )
return "".join( return "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, version_tuple[0:2]))
"".join(map(str, t)) if t[1] else ""
for t in zip(prefixes, version_tuple[0:2])
)
elif attr == "schema_identifier": elif attr == "schema_identifier":
return self.wrapped_data.schema return self.wrapped_data.schema
elif attr == "schema_version": elif attr == "schema_version":
@@ -576,7 +580,7 @@ class file(object):
path = Path(path) path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
if format == None: if format == None:
format = ifcopenshell.util.file.guess_format(path) format = ifcopenshell.guess_format(path)
if format == ".ifcXML": if format == ".ifcXML":
serializer = ifcopenshell_wrapper.XmlSerializer(self, str(path)) serializer = ifcopenshell_wrapper.XmlSerializer(self, str(path))
serializer.finalize() serializer.finalize()
+12 -6
View File
@@ -2,7 +2,6 @@ try:
import re import re
import json import json
import ifcopenshell.util.schema
from .file import file from .file import file
from . import ifcopenshell_wrapper from . import ifcopenshell_wrapper
from .entity_instance import entity_instance from .entity_instance import entity_instance
@@ -56,6 +55,8 @@ class sqlite(file):
self.preprocess_schema() self.preprocess_schema()
def preprocess_schema(self): def preprocess_schema(self):
import ifcopenshell.util.schema
self.ifc_class_subtypes = {} self.ifc_class_subtypes = {}
self.ifc_class_attributes = {} self.ifc_class_attributes = {}
self.ifc_class_inverse_attributes = {} self.ifc_class_inverse_attributes = {}
@@ -122,6 +123,9 @@ class sqlite(file):
return entity return entity
def by_type(self, type, include_subtypes=True): def by_type(self, type, include_subtypes=True):
# TODO use cached subtypes
import ifcopenshell.util.schema
if self.class_map: if self.class_map:
results = [] results = []
subtypes = self.ifc_class_subtypes[type] if include_subtypes else self.ifc_class_subtypes[type][0:1] 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 return results
def get_inverse(self, inst, allow_duplicate=False, with_attribute_indices=False): 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) self.cursor.execute(query)
row = self.cursor.fetchone() row = self.cursor.fetchone()
if not row or not row[0]: if not row or not row[0]:
@@ -198,9 +204,9 @@ class sqlite(file):
"verts": np.frombuffer(row["verts"]).tolist() if row["verts"] else [], "verts": np.frombuffer(row["verts"]).tolist() if row["verts"] else [],
"edges": np.frombuffer(row["edges"], dtype=np.int64).tolist() if row["edges"] 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 [], "faces": np.frombuffer(row["faces"], dtype=np.int64).tolist() if row["faces"] else [],
"material_ids": np.frombuffer(row["material_ids"], dtype=np.int64).tolist() "material_ids": (
if row["material_ids"] np.frombuffer(row["material_ids"], dtype=np.int64).tolist() if row["material_ids"] else []
else [], ),
"materials": json.loads(row["materials"]) if row["materials"] else [], "materials": json.loads(row["materials"]) if row["materials"] else [],
} }
shapes[row["ifc_id"]] = { 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): 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} info = {"id": self.sqlite_wrapper.id, "type": self.sqlite_wrapper.ifc_class}
if not self.sqlite_wrapper.attribute_cache: 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) info.update(self.sqlite_wrapper.attribute_cache)
return info 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"