Skip to content

Commit c51e36d

Browse files
committed
ifcpatch - a static way to use ifcpatch
1 parent b393c12 commit c51e36d

7 files changed

Lines changed: 94 additions & 29 deletions

File tree

src/ifcopenshell-python/docs/ifcpatch.rst

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,19 @@ example, we'll extract out all `IfcWall` elements.
5757
ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a "IfcWall"
5858
cat output.ifc
5959
60+
You can also alias it to a command:
61+
62+
.. code-block:: bash
63+
64+
alias ifcpatch='python -m ifcpatch'
65+
66+
Alternatively, you can package it as an executable.
67+
68+
.. code-block:: bash
69+
70+
python make.py
71+
./dist/ifcpatch
72+
6073
Here is a minimal example of how to use IfcPatch as a library:
6174

6275
.. code-block:: python
@@ -71,18 +84,22 @@ Here is a minimal example of how to use IfcPatch as a library:
7184
})
7285
ifcpatch.write(output, "output.ifc")
7386
74-
You can also alias it to a command:
75-
76-
.. code-block:: bash
7787
78-
alias ifcpatch='python -m ifcpatch'
88+
Alternatively, there is a less dynamic way to use IfcPatch
89+
that allows seeing available arguments, their descriptions, types, default values, etc.
7990

80-
Alternatively, you can package it as an executable.
91+
..code-block:: python
8192

82-
.. code-block:: bash
83-
84-
python make.py
85-
./dist/ifcpatch
93+
import ifcopenshell
94+
import ifcpatch
95+
from ifcpatch.recipes import ExtractElements
96+
97+
patcher = ExtractElements.Patcher(
98+
ifcopenshell.open("input.ifc"),
99+
query="IfcWall",
100+
)
101+
patcher.patch()
102+
ifcpatch.write(patcher.get_output(), "output.ifc")
86103

87104
Patch recipes
88105
-------------

src/ifcpatch/ifcpatch/__init__.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,27 @@ class ArgumentsDict(TypedDict):
4545
arguments: NotRequired[Sequence[Any]]
4646

4747

48-
def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
48+
class BasePatcher:
49+
def __init__(self, file: ifcopenshell.file, logger: Union[logging.Logger, None]):
50+
self.file = file
51+
self.logger = ensure_logger(logger)
52+
53+
def patch(self) -> None:
54+
raise NotImplementedError
55+
56+
def get_output(self) -> Union[ifcopenshell.file, str, None]:
57+
if hasattr(self, "file_patched"):
58+
return self.file_patched # pyright: ignore[reportAttributeAccessIssue]
59+
return self.file
60+
61+
62+
def ensure_logger(logger: Union[logging.Logger, None] = None) -> logging.Logger:
63+
if logger is not None:
64+
return logger
65+
return logging.getLogger("IFCPatch")
66+
67+
68+
def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str, None]:
4969
"""Execute a patch recipe
5070
5171
The details of how the patch recipe is executed depends on the definition of
@@ -88,7 +108,7 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
88108
"""
89109
if "log" in args:
90110
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
91-
logger = logging.getLogger("IFCPatch")
111+
logger = ensure_logger()
92112
if recipe_dir := os.environ.get("IFCPATCH_RECIPE_DIR"):
93113
spec = importlib.util.spec_from_file_location(args["recipe"], os.path.join(recipe_dir, args["recipe"] + ".py"))
94114
recipe = importlib.util.module_from_spec(spec)
@@ -103,17 +123,17 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
103123
else:
104124
patcher = recipe.Patcher(args.get("file"), logger, arguments)
105125
patcher.patch()
106-
output = getattr(patcher, "file_patched", patcher.file)
126+
output = BasePatcher.get_output(patcher)
107127
return output
108128

109129

110-
def write(output: Union[ifcopenshell.file, str], filepath: Union[Path, str]) -> None:
130+
def write(output: Union[ifcopenshell.file, str, None], filepath: Union[Path, str]) -> None:
111131
"""Write the output of an IFC patch to a file
112132
113133
Typically a patch output would be a patched IFC model file object, or as a
114134
string. This function lets you agnostically write that output to a filepath.
115135
116-
:param output: The results from ifcpatch.execute()
136+
:param output: The results from ``ifcpatch.execute()`` / ``Patcher.get_output()``
117137
:param filepath: A filepath to where the results of the patched model should
118138
be written to.
119139
:return: None

src/ifcpatch/ifcpatch/recipes/ConvertLengthUnit.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import ifcopenshell.util.pset
2323
import ifcopenshell.util.element
2424
import ifcopenshell.util.unit
25+
import ifcpatch
2526
from logging import Logger
2627

2728
import typing
29+
from typing import Union
2830

2931
LengthUnit = typing.Literal[
3032
"ATTOMETER",
@@ -50,11 +52,11 @@
5052
]
5153

5254

53-
class Patcher:
55+
class Patcher(ifcpatch.BasePatcher):
5456
def __init__(
5557
self,
5658
file: ifcopenshell.file,
57-
logger: Logger,
59+
logger: Union[Logger, None] = None,
5860
unit: LengthUnit = "METER",
5961
):
6062
"""Converts the length unit of a model to the specified unit
@@ -74,8 +76,7 @@ def __init__(
7476
# Convert to feet
7577
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
7678
"""
77-
self.file = file
78-
self.logger = logger
79+
super().__init__(file, logger)
7980
self.unit = unit
8081
self.file_patched: ifcopenshell.file
8182

src/ifcpatch/ifcpatch/recipes/ExtractElements.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@
2121
import ifcopenshell.api.project
2222
import ifcopenshell.guid
2323
import ifcopenshell.util.selector
24+
import ifcpatch
2425
from typing import Union
2526
from logging import Logger
2627

2728

28-
class Patcher:
29+
class Patcher(ifcpatch.BasePatcher):
2930
def __init__(
3031
self,
3132
file: ifcopenshell.file,
32-
logger: Logger,
33+
logger: Union[Logger, None] = None,
3334
query: str = "IfcWall",
3435
assume_asset_uniqueness_by_name: bool = True,
3536
):
@@ -62,8 +63,7 @@ def __init__(
6263
# Extract all walls and slabs
6364
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
6465
"""
65-
self.file = file
66-
self.logger = logger
66+
super().__init__(file, logger)
6767
self.query = query
6868
self.assume_asset_uniqueness_by_name = assume_asset_uniqueness_by_name
6969

src/ifcpatch/ifcpatch/recipes/Ifc2Sql.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import ifcopenshell.util.schema
3737
import ifcopenshell.util.shape
3838
import ifcopenshell.util.unit
39+
import ifcpatch
3940
from pathlib import Path
4041
from typing import Any, TYPE_CHECKING, Literal, Union
4142
from typing_extensions import assert_never
@@ -61,11 +62,11 @@
6162
DEFAULT_DATABASE_NAME = "database"
6263

6364

64-
class Patcher:
65+
class Patcher(ifcpatch.BasePatcher):
6566
def __init__(
6667
self,
6768
file: ifcopenshell.file,
68-
logger: logging.Logger,
69+
logger: Union[logging.Logger, None] = None,
6970
sql_type: SQLTypes = "SQLite",
7071
host: str = "localhost",
7172
username: str = "root",
@@ -116,7 +117,7 @@ def __init__(
116117
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
117118
)
118119
"""
119-
self.file = file
120+
super().__init__(file, logger)
120121
self.logger = logger
121122
self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower()
122123
self.host = host

src/ifcpatch/ifcpatch/recipes/Migrate.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818

1919
import ifcopenshell
2020
import ifcopenshell.util.schema
21+
import ifcpatch
2122
import typing
23+
from typing import Union
2224
from logging import Logger
2325

2426

25-
class Patcher:
27+
class Patcher(ifcpatch.BasePatcher):
2628
def __init__(
2729
self,
2830
file: ifcopenshell.file,
29-
logger: Logger,
31+
logger: Union[Logger, None] = None,
3032
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
3133
):
3234
"""Migrate from one IFC version to another
@@ -43,8 +45,7 @@ def __init__(
4345
# Upgrade an IFC2X3 model to IFC4
4446
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
4547
"""
46-
self.file = file
47-
self.logger = logger
48+
super().__init__(file, logger)
4849
self.schema = schema
4950

5051
def patch(self):

src/ifcpatch/test/test_ifcpatch.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
# You should have received a copy of the GNU Lesser General Public License
1717
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
1818

19+
import tempfile
20+
import ifcopenshell.api.root
1921
import ifcpatch
2022
from pathlib import Path
2123

@@ -32,3 +34,26 @@ def test_parsing_docs(self):
3234
expected_keys = ("class_", "description", "output", "inputs")
3335
for key in expected_keys:
3436
assert key in docs
37+
38+
def test_static_ifcpatch_execution(self):
39+
from ifcpatch.recipes import ExtractElements
40+
41+
ifc_file = ifcopenshell.file()
42+
project = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcProject")
43+
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall")
44+
45+
patcher = ExtractElements.Patcher(ifc_file, query="IfcWall")
46+
patcher.patch()
47+
48+
output = patcher.get_output()
49+
assert isinstance(output, ifcopenshell.file)
50+
assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId
51+
assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId
52+
53+
output_path = Path(tempfile.mktemp())
54+
try:
55+
assert not output_path.exists()
56+
ifcpatch.write(patcher.get_output(), output_path)
57+
assert output_path.exists()
58+
finally:
59+
output_path.unlink()

0 commit comments

Comments
 (0)