diff --git a/src/ifcopenshell-python/ifcopenshell/__init__.py b/src/ifcopenshell-python/ifcopenshell/__init__.py index 3f4a6acaa2..7c936f7b06 100644 --- a/src/ifcopenshell-python/ifcopenshell/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/__init__.py @@ -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 * diff --git a/src/ifcopenshell-python/ifcopenshell/file.py b/src/ifcopenshell-python/ifcopenshell/file.py index 3ca1854db9..40986a3735 100644 --- a/src/ifcopenshell-python/ifcopenshell/file.py +++ b/src/ifcopenshell-python/ifcopenshell/file.py @@ -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() diff --git a/src/ifcopenshell-python/ifcopenshell/sql.py b/src/ifcopenshell-python/ifcopenshell/sql.py index 546b2b4101..343a09561a 100644 --- a/src/ifcopenshell-python/ifcopenshell/sql.py +++ b/src/ifcopenshell-python/ifcopenshell/sql.py @@ -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 diff --git a/src/ifcopenshell-python/ifcopenshell/util/file.py b/src/ifcopenshell-python/ifcopenshell/util/file.py deleted file mode 100644 index 2898f5448c..0000000000 --- a/src/ifcopenshell-python/ifcopenshell/util/file.py +++ /dev/null @@ -1,29 +0,0 @@ -# IfcOpenShell - IFC toolkit and geometry engine -# Copyright (C) 2021 Dion Moult -# -# 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 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"